CONTENT-WP-0002-T03 renders manifest columns through a pure row mapper and replaceable cell renderers. Assistant: grok Assistant-Session: 01a1346c-fdff-7741-b1af-b7283661554b
276 lines
8.5 KiB
TypeScript
276 lines
8.5 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { columnDefs, HOME_COLUMNS, mapBrowserRow, type BrowserRow, type ColumnDef } from "./browser-row";
|
|
|
|
type KindRecord = { id: string; label: string; plural: string };
|
|
|
|
type EntityRecord = {
|
|
id: string;
|
|
kindId: string;
|
|
title: string;
|
|
tldr: string;
|
|
summary: string;
|
|
tags?: string[];
|
|
sourceRefs?: string[];
|
|
claimRefs?: string[];
|
|
};
|
|
|
|
type SourceRecord = { id: string; title: string; license: string };
|
|
|
|
type ClaimRecord = { id: string; subjectId: string; key: string; value?: string; status: string };
|
|
|
|
type RelationRecord = { id: string; predicate: string; from: string; to: string };
|
|
|
|
type SnapshotFile = {
|
|
dataspace: { id: string; title: string; description: string };
|
|
kinds: KindRecord[];
|
|
sources?: SourceRecord[];
|
|
entities: EntityRecord[];
|
|
relations?: RelationRecord[];
|
|
claims?: ClaimRecord[];
|
|
};
|
|
|
|
type KindViewConfig = { columns?: string[]; facets?: string[] };
|
|
|
|
type ManifestFile = {
|
|
relationLabels?: Record<string, { forward?: string; inverse?: string }>;
|
|
kindViews?: Record<string, KindViewConfig>;
|
|
};
|
|
|
|
export type DataspaceNav = {
|
|
id: string;
|
|
title: string;
|
|
kinds: { id: string; plural: string; href: string }[];
|
|
};
|
|
|
|
export type DataspaceView = {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
kinds: { id: string; label: string; plural: string; href: string }[];
|
|
rows: BrowserRow[];
|
|
columns: ColumnDef[];
|
|
};
|
|
|
|
export type KindView = {
|
|
dataspaceId: string;
|
|
dataspaceTitle: string;
|
|
kindId: string;
|
|
label: string;
|
|
plural: string;
|
|
rows: BrowserRow[];
|
|
columns: ColumnDef[];
|
|
};
|
|
|
|
export type RelationView = {
|
|
wording: string;
|
|
direction: "out" | "in";
|
|
otherTitle: string;
|
|
otherHref: string | null;
|
|
};
|
|
|
|
export type EntityView = {
|
|
id: string;
|
|
title: string;
|
|
tldr: string;
|
|
summary: string;
|
|
tags: string[];
|
|
dataspaceId: string;
|
|
dataspaceTitle: string;
|
|
kindId: string;
|
|
kindLabel: string;
|
|
kindHref: string;
|
|
sources: { title: string; license: string }[];
|
|
claims: { key: string; status: string; value: string | null }[];
|
|
relations: RelationView[];
|
|
};
|
|
|
|
type Loaded = {
|
|
snapshot: SnapshotFile;
|
|
labels: Map<string, { forward: string; inverse: string }>;
|
|
views: Record<string, KindViewConfig>;
|
|
byId: Map<string, EntityRecord>;
|
|
};
|
|
|
|
const ROOT = join(process.cwd(), "../examples/dataspaces");
|
|
|
|
let cache: Map<string, Loaded> | null = null;
|
|
|
|
export function entitySlug(id: string, kindId: string): string {
|
|
const marker = `:${kindId}:`;
|
|
const at = id.lastIndexOf(marker);
|
|
if (at >= 0) return id.slice(at + marker.length);
|
|
const tail = id.split(":").pop();
|
|
return tail && tail.length > 0 ? tail : id;
|
|
}
|
|
|
|
export function kindHref(dataspaceId: string, kindId: string): string {
|
|
return `/${dataspaceId}/${kindId}/`;
|
|
}
|
|
|
|
export function entityHref(dataspaceId: string, kindId: string, id: string): string {
|
|
return `/${dataspaceId}/${kindId}/${encodeURIComponent(entitySlug(id, kindId))}/`;
|
|
}
|
|
|
|
function loadAll(): Map<string, Loaded> {
|
|
if (cache) return cache;
|
|
const loaded = new Map<string, Loaded>();
|
|
for (const name of readdirSync(ROOT).sort()) {
|
|
const snapshotPath = join(ROOT, name, "snapshot.json");
|
|
let snapshot: SnapshotFile;
|
|
try {
|
|
snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as SnapshotFile;
|
|
} catch {
|
|
continue;
|
|
}
|
|
if (!snapshot.dataspace?.id || !Array.isArray(snapshot.kinds) || !Array.isArray(snapshot.entities)) continue;
|
|
let manifest: ManifestFile = {};
|
|
try {
|
|
manifest = JSON.parse(readFileSync(join(ROOT, name, "manifest.json"), "utf8")) as ManifestFile;
|
|
} catch {
|
|
manifest = {};
|
|
}
|
|
const labels = new Map<string, { forward: string; inverse: string }>();
|
|
for (const [predicate, wording] of Object.entries(manifest.relationLabels ?? {})) {
|
|
labels.set(predicate, {
|
|
forward: wording.forward ?? predicate,
|
|
inverse: wording.inverse ?? predicate,
|
|
});
|
|
}
|
|
loaded.set(snapshot.dataspace.id, {
|
|
snapshot,
|
|
labels,
|
|
views: manifest.kindViews ?? {},
|
|
byId: new Map(snapshot.entities.map((entity) => [entity.id, entity])),
|
|
});
|
|
}
|
|
cache = loaded;
|
|
return loaded;
|
|
}
|
|
|
|
function kindOf(loaded: Loaded, kindId: string): KindRecord | null {
|
|
return loaded.snapshot.kinds.find((kind) => kind.id === kindId) ?? null;
|
|
}
|
|
|
|
function rowFor(loaded: Loaded, entity: EntityRecord): BrowserRow {
|
|
const kind = kindOf(loaded, entity.kindId);
|
|
const dataspaceId = loaded.snapshot.dataspace.id;
|
|
return mapBrowserRow(
|
|
{
|
|
id: entity.id,
|
|
title: entity.title,
|
|
tldr: entity.tldr,
|
|
tags: entity.tags,
|
|
kindLabel: kind?.label ?? entity.kindId,
|
|
route: entityHref(dataspaceId, entity.kindId, entity.id),
|
|
kindHref: kindHref(dataspaceId, entity.kindId),
|
|
},
|
|
loaded.views[entity.kindId]?.facets ?? [],
|
|
);
|
|
}
|
|
|
|
export function listNavigation(): DataspaceNav[] {
|
|
return [...loadAll().values()].map((loaded) => ({
|
|
id: loaded.snapshot.dataspace.id,
|
|
title: loaded.snapshot.dataspace.title,
|
|
kinds: loaded.snapshot.kinds.map((kind) => ({
|
|
id: kind.id,
|
|
plural: kind.plural,
|
|
href: kindHref(loaded.snapshot.dataspace.id, kind.id),
|
|
})),
|
|
}));
|
|
}
|
|
|
|
export function findDataspace(id: string): DataspaceView | null {
|
|
const loaded = loadAll().get(id);
|
|
if (!loaded) return null;
|
|
return {
|
|
id: loaded.snapshot.dataspace.id,
|
|
title: loaded.snapshot.dataspace.title,
|
|
description: loaded.snapshot.dataspace.description,
|
|
kinds: loaded.snapshot.kinds.map((kind) => ({
|
|
id: kind.id,
|
|
label: kind.label,
|
|
plural: kind.plural,
|
|
href: kindHref(loaded.snapshot.dataspace.id, kind.id),
|
|
})),
|
|
rows: loaded.snapshot.entities.map((entity) => rowFor(loaded, entity)),
|
|
columns: columnDefs(HOME_COLUMNS),
|
|
};
|
|
}
|
|
|
|
export function findKind(dataspaceId: string, kindId: string): KindView | null {
|
|
const loaded = loadAll().get(dataspaceId);
|
|
if (!loaded) return null;
|
|
const kind = kindOf(loaded, kindId);
|
|
if (!kind) return null;
|
|
return {
|
|
dataspaceId,
|
|
dataspaceTitle: loaded.snapshot.dataspace.title,
|
|
kindId: kind.id,
|
|
label: kind.label,
|
|
plural: kind.plural,
|
|
rows: loaded.snapshot.entities.filter((entity) => entity.kindId === kind.id).map((entity) => rowFor(loaded, entity)),
|
|
columns: columnDefs(loaded.views[kind.id]?.columns ?? ["name", "tldr", "tags"]),
|
|
};
|
|
}
|
|
|
|
function hrefForEntity(loaded: Loaded, id: string): string | null {
|
|
const entity = loaded.byId.get(id);
|
|
if (!entity) return null;
|
|
return entityHref(loaded.snapshot.dataspace.id, entity.kindId, entity.id);
|
|
}
|
|
|
|
export function findEntity(dataspaceId: string, kindId: string, slug: string): EntityView | null {
|
|
const loaded = loadAll().get(dataspaceId);
|
|
if (!loaded) return null;
|
|
const kind = kindOf(loaded, kindId);
|
|
if (!kind) return null;
|
|
const matches = loaded.snapshot.entities.filter(
|
|
(entity) => entity.kindId === kind.id && entitySlug(entity.id, kind.id) === slug,
|
|
);
|
|
if (matches.length !== 1) return null;
|
|
const entity = matches[0];
|
|
const sources = new Map((loaded.snapshot.sources ?? []).map((source) => [source.id, source]));
|
|
const relations: RelationView[] = [];
|
|
for (const relation of loaded.snapshot.relations ?? []) {
|
|
const wording = loaded.labels.get(relation.predicate);
|
|
if (relation.from === entity.id) {
|
|
const other = loaded.byId.get(relation.to);
|
|
relations.push({
|
|
wording: wording?.forward ?? relation.predicate,
|
|
direction: "out",
|
|
otherTitle: other?.title ?? relation.to,
|
|
otherHref: hrefForEntity(loaded, relation.to),
|
|
});
|
|
} else if (relation.to === entity.id) {
|
|
const other = loaded.byId.get(relation.from);
|
|
relations.push({
|
|
wording: wording?.inverse ?? relation.predicate,
|
|
direction: "in",
|
|
otherTitle: other?.title ?? relation.from,
|
|
otherHref: hrefForEntity(loaded, relation.from),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
id: entity.id,
|
|
title: entity.title,
|
|
tldr: entity.tldr,
|
|
summary: entity.summary,
|
|
tags: entity.tags ?? [],
|
|
dataspaceId,
|
|
dataspaceTitle: loaded.snapshot.dataspace.title,
|
|
kindId: kind.id,
|
|
kindLabel: kind.label,
|
|
kindHref: kindHref(dataspaceId, kind.id),
|
|
sources: (entity.sourceRefs ?? []).map((id) => {
|
|
const source = sources.get(id);
|
|
return { title: source?.title ?? id, license: source?.license ?? "" };
|
|
}),
|
|
claims: (loaded.snapshot.claims ?? [])
|
|
.filter((claim) => claim.subjectId === entity.id)
|
|
.map((claim) => ({ key: claim.key, status: claim.status, value: claim.value ?? null })),
|
|
relations,
|
|
};
|
|
}
|