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; kindViews?: Record; }; 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; views: Record; byId: Map; }; const ROOT = join(process.cwd(), "../examples/dataspaces"); let cache: Map | 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 { if (cache) return cache; const loaded = new Map(); 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(); 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, }; }