Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions server/typescript/packages/docs-site/src/builders/object-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface FieldRow { name: string; type: string; isArray: boolean; requir

/** Render an attr value for display: an object (e.g. view.badge @variantMap) as
* ordered `k: v` pairs, an array as a comma list, else the scalar. */
function fmtAttrValue(v: unknown): string {
export function fmtAttrValue(v: unknown): string {
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
return Object.entries(v as Record<string, unknown>)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
Expand All @@ -45,15 +45,45 @@ function fmtAttrValue(v: unknown): string {
if (Array.isArray(v)) return v.map(String).join(", ");
return String(v);
}

/** A node's authored attrs not already rendered by a specific renderer, as
* escaped key/value pairs — consumed so they don't surface as "not rendered by
* any page" in coverage. `skip` names the attrs a specific renderer handles.
* Deterministic (sorted by name). This is the generic catch-all that documents
* a consumer's own attrs (e.g. object.@dataflow) + the structural ones a
* bespoke renderer doesn't (e.g. relationship.@onDelete). */
function otherAttrs(node: MetaData, cov: CoverageTracker, skip: Set<string>): ViewAttr[] {
return [...node.ownAttrs()]
.filter(([n]) => !skip.has(n))
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([n, v]) => { cov.consumeAttr(node, n); return { name: esc(n), value: esc(fmtAttrValue(v)) }; });
}

/** Consume a node + its whole subtree (nodes + attrs) so a section that renders
* only a summary (e.g. a grid layout's top-level attrs) doesn't leave its nested
* config flagged as un-rendered. */
function consumeSubtree(node: MetaData, cov: CoverageTracker): void {
cov.consumeNode(node);
for (const [n] of node.ownAttrs()) cov.consumeAttr(node, n);
for (const c of node.ownChildren()) consumeSubtree(c, cov);
}

export interface IndexRow { name: string; kind: string; fields: string; extra: string; unique: boolean; }
export interface ValidatorRow { scope: "field" | "object"; subject: string; rule: string; // human-readable, HTML-escaped
}
export interface RelationRow { name: string; toName: string; toHref: string; cardinality: string; }
export interface OriginRow { field: string; from: string; via: string; }
export interface RelationRow { name: string; toName: string; toHref: string; cardinality: string; attrs: ViewAttr[]; }
export interface OriginRow { field: string; from: string; via: string; attrs: ViewAttr[]; }
export interface LayoutRow { kind: string; attrs: ViewAttr[]; }

// Field attrs fieldRow renders specifically (as their own badges / enum / ref);
// everything else authored on a field (e.g. @column, @storage) is rendered
// generically as a badge so it isn't flagged "not rendered" in coverage.
const FIELD_KNOWN_ATTRS = new Set(["required", "deprecated", "maxLength", "dbColumnType", "default", "xmlText", "values", "objectRef", "description"]);
export interface HierRow { name: string; href: string; level: number; self: boolean; }
export interface ObjectPageData {
name: string; kindBadge: string; isAbstract: boolean; isView: boolean; generation: string;
pkg: string; href: string; breadcrumbHtml: string; desc: string; tableName?: string | undefined; pkHtml?: string | undefined;
objectAttrs: ViewAttr[]; storageAttrs: ViewAttr[]; layouts: LayoutRow[];
ownFields: FieldRow[]; inheritedFields: FieldRow[]; indexes: IndexRow[]; validators: ValidatorRow[];
relations: RelationRow[]; origins: OriginRow[]; hierarchy: HierRow[]; inheritanceMermaid?: string | undefined;
neighborhoodMermaid?: string | undefined; neighborhoodLegend?: { pkg: string; fill: string; stroke: string }[] | undefined; neighborhoodMore?: number | undefined;
Expand Down Expand Up @@ -115,6 +145,12 @@ function fieldRow(f: MetaData, ownerHref: string, g: LinkGraph, cov: CoverageTra
.map(([an, av]) => { cov.consumeAttr(v, an); return { name: esc(an), value: esc(fmtAttrValue(av)) }; });
views.push({ subType: esc(v.subType), attrs: vAttrs });
}
// any other authored field attrs (e.g. @column DB name, @storage) as badges
for (const [n, val] of [...f.ownAttrs()].sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))) {
if (FIELD_KNOWN_ATTRS.has(n)) continue;
cov.consumeAttr(f, n);
bits.push(badge({ text: `@${n}=${fmtAttrValue(val)}`, cls: "badge-soft badge-ghost" }));
}
const desc = esc(a("description") ?? "");
return { name: f.name, type: f.subType, isArray: f.resolvedIsArray(), required, badgesHtml: bits.join(" "), desc, enumValues, views, refHref, refName, inheritedFrom: undefined, anchor: `f-${f.name}` };
}
Expand All @@ -136,6 +172,8 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
const o = dn.node;
cov.consumeNode(o);
cov.consumeAttr(o, "description");
// consumer/structural attrs authored on the object itself (e.g. @dataflow, @neo4j)
const objectAttrs = otherAttrs(o, cov, new Set(["description"]));

// inheritance hierarchy rows (ancestors nearest-last so level increases downward) + self + direct children
const anc = g.ancestors(fqn); // nearest-first
Expand All @@ -150,27 +188,32 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)

// storage: table vs view, generation, pk
let tableName: string | undefined, isView = false, pkHtml: string | undefined, generation = "";
const storageAttrs: ViewAttr[] = [];
for (const s of o.childrenOfType("source")) {
cov.consumeNode(s);
const t = s.attr("table"); if (t !== undefined) { cov.consumeAttr(s, "table"); tableName = String(t); }
const kind = s.attr("kind"); if (kind !== undefined) { cov.consumeAttr(s, "kind"); if (String(kind) === "view") isView = true; }
// extra source attrs — e.g. a view source's @view (view name) — rendered in the storage section
storageAttrs.push(...otherAttrs(s, cov, new Set(["table", "kind"])));
}
// indexes: identity (pk/secondary) + index.lookup with tuning detail
const indexes: IndexRow[] = [];
for (const id of o.childrenOfType("identity")) {
cov.consumeNode(id);
const flds = id.attr("fields"); if (flds !== undefined) cov.consumeAttr(id, "fields");
const fields = Array.isArray(flds) ? flds.join(", ") : String(flds ?? "");
// other authored identity attrs (e.g. @constraintName) rendered into the detail cell
const idExtra = otherAttrs(id, cov, new Set(["fields", "references", "enforce", "generation"])).map((x) => `@${x.name}=${x.value}`).join(" · ");
if (id.subType === "primary") {
pkHtml = `<code>${esc(fields)}</code>`;
const gen = id.attr("generation"); if (gen !== undefined) { cov.consumeAttr(id, "generation"); generation = String(gen); }
indexes.push({ name: id.name, kind: "primary", fields, extra: "", unique: true });
indexes.push({ name: id.name, kind: "primary", fields, extra: idExtra, unique: true });
} else if (id.subType === "secondary") {
indexes.push({ name: id.name, kind: "unique", fields, extra: "", unique: true });
indexes.push({ name: id.name, kind: "unique", fields, extra: idExtra, unique: true });
} else if (id.subType === "reference") {
const ref = id.attr("references"); if (ref !== undefined) cov.consumeAttr(id, "references");
const enf = id.attr("enforce"); if (enf !== undefined) cov.consumeAttr(id, "enforce");
indexes.push({ name: id.name, kind: "fk", fields, extra: [ref ? `→ ${esc(String(ref))}` : "", enf === false || enf === "false" ? "logical" : ""].filter(Boolean).join(" · "), unique: false });
indexes.push({ name: id.name, kind: "fk", fields, extra: [ref ? `→ ${esc(String(ref))}` : "", enf === false || enf === "false" ? "logical" : "", idExtra].filter(Boolean).join(" · "), unique: false });
}
}
for (const ix of o.childrenOfType("index")) {
Expand Down Expand Up @@ -201,17 +244,38 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
}
validators.sort((a, b) => a.subject.localeCompare(b.subject));

// relationships
// relationships — bespoke name/target/cardinality + generic extras (@onDelete/@onUpdate/…)
const relAttrs = new Map<string, ViewAttr[]>();
for (const rel of o.childrenOfType("relationship")) {
cov.consumeNode(rel); cov.consumeAttr(rel, "objectRef"); cov.consumeAttr(rel, "cardinality");
relAttrs.set(rel.name, otherAttrs(rel, cov, new Set(["objectRef", "cardinality", "through", "sourceRefField", "symmetric"])));
}
const relations: RelationRow[] = g.relationshipsOf(fqn).map((r) => {
const t = g.byFqn(r.toFqn);
return { name: r.name, toName: t?.name ?? r.toFqn, toHref: t ? g.relHref(dn.href, t.href) : "", cardinality: r.cardinality };
return { name: r.name, toName: t?.name ?? r.toFqn, toHref: t ? g.relHref(dn.href, t.href) : "", cardinality: r.cardinality, attrs: relAttrs.get(r.name) ?? [] };
});
for (const rel of o.childrenOfType("relationship")) { cov.consumeNode(rel); cov.consumeAttr(rel, "objectRef"); cov.consumeAttr(rel, "cardinality"); }

// origin provenance
const origins: OriginRow[] = g.originsOf(fqn).map((r) => ({ field: r.field, from: esc(r.from), via: esc(r.via) }))
// origin provenance — bespoke field/from/via + generic extras (@of/@agg/@filter/…)
const originAttrs = new Map<string, ViewAttr[]>();
for (const f of o.childrenOfType("field")) for (const org of f.childrenOfType("origin")) {
cov.consumeNode(org); cov.consumeAttr(org, "from"); cov.consumeAttr(org, "via");
const cur = originAttrs.get(f.name) ?? [];
cur.push(...otherAttrs(org, cov, new Set(["from", "via"])));
originAttrs.set(f.name, cur);
}
const origins: OriginRow[] = g.originsOf(fqn).map((r) => ({ field: r.field, from: esc(r.from), via: esc(r.via), attrs: originAttrs.get(r.field) ?? [] }))
.sort((a, b) => a.field.localeCompare(b.field));
for (const f of o.childrenOfType("field")) for (const org of f.childrenOfType("origin")) { cov.consumeNode(org); cov.consumeAttr(org, "from"); cov.consumeAttr(org, "via"); }

// grid/form layout (admin-ui presentation on projections): render the layout node's
// own attrs (@pageSize/@defaultSortOrder/…); consume nested column config so it
// isn't flagged un-rendered.
const layouts: LayoutRow[] = [];
for (const l of o.childrenOfType("layout")) {
cov.consumeNode(l);
const attrs = otherAttrs(l, cov, new Set());
for (const c of l.ownChildren()) consumeSubtree(c, cov);
layouts.push({ kind: esc(l.subType), attrs });
}

// fields own vs inherited
const ownNames = new Set(o.ownChildren().filter((c) => c.type === "field").map((c) => c.name));
Expand Down Expand Up @@ -282,7 +346,7 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
return {
name: dn.name, kindBadge: o.subType, isAbstract: o.isAbstract, isView, generation,
pkg: dn.pkg, href: dn.href, breadcrumbHtml: crumbs.join(" / "), desc: esc(o.attr("description") ?? ""),
tableName, pkHtml, ownFields, inheritedFields, indexes, validators, relations, origins,
tableName, pkHtml, objectAttrs, storageAttrs, layouts, ownFields, inheritedFields, indexes, validators, relations, origins,
hierarchy, inheritanceMermaid, neighborhoodMermaid, neighborhoodLegend, neighborhoodMore, referencedBy, references, usedByTemplates, sourceFile: src,
};
}
17 changes: 14 additions & 3 deletions server/typescript/packages/docs-site/src/builders/output-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import { LinkGraph } from "../link-graph.js";
import type { CoverageTracker } from "../coverage.js";
import type { CommentDocs } from "../yaml-comments.js";
import { esc } from "../badges.js";
import { fmtAttrValue } from "./object-data.js";

export interface OutputPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; format: string; kind: string; textRef: string; textRefResolves: boolean; payloadName: string; payloadHref: string; desc: string; fields: { name: string; type: string; isArray: boolean; wire: string; note: string; refHtml: string }[]; }
// Rendered specifically (own fields/badges); everything else a template authors
// (e.g. a consumer's @dataflow) is rendered generically as extraAttrsHtml.
const OUTPUT_KNOWN_ATTRS = new Set(["payloadRef", "textRef", "format", "kind", "description"]);

export interface OutputPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; format: string; kind: string; textRef: string; textRefResolves: boolean; payloadName: string; payloadHref: string; desc: string; extraAttrsHtml: string; fields: { name: string; type: string; isArray: boolean; wire: string; note: string; refHtml: string }[]; }

export function buildOutputPage(fqn: string, g: LinkGraph, cov: CoverageTracker, docs: CommentDocs, sourceDirs: string[] = []): OutputPageData {
const dn = g.byFqn(fqn)!;
const t = dn.node;
cov.consumeNode(t); cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); cov.consumeAttr(t, "format");
cov.consumeNode(t); cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); cov.consumeAttr(t, "format"); cov.consumeAttr(t, "kind");
const format = String(t.attr("format") ?? "text");
// any other authored template attrs (e.g. @maxChars, @requiredTags, a consumer's @dataflow)
const extraAttrsHtml = [...t.ownAttrs()]
.filter(([n]) => !OUTPUT_KNOWN_ATTRS.has(n))
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
.map(([n, v]) => { cov.consumeAttr(t, n); return `<span class="badge badge-ghost badge-sm font-mono">@${esc(n)}=${esc(fmtAttrValue(v))}</span>`; })
.join(" ");
const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
const payload = pRef ? g.byFqn(pRef.to) : undefined;
const fields = (payload?.node.childrenOfType("field") ?? []).map((f) => {
Expand All @@ -29,7 +40,7 @@ export function buildOutputPage(fqn: string, g: LinkGraph, cov: CoverageTracker,
const textRefResolves = sourceDirs.some((d) => existsSync(join(d, ...textRef.split("/")) + ".mustache"));
return { name: dn.name, pkg: dn.pkg, href: dn.href,
breadcrumbHtml: `<a href="${esc(g.relHref(dn.href, "index.html"))}">index</a> / <a href="${esc("index.html")}">${esc(dn.pkg)}</a> / ${esc(dn.name)}`,
format, kind: String(t.attr("kind") ?? "document"), textRef, textRefResolves,
format, kind: String(t.attr("kind") ?? "document"), textRef, textRefResolves, extraAttrsHtml,
payloadName: payload?.name ?? "", payloadHref: payload ? esc(g.relHref(dn.href, payload.href)) : "",
desc: payload ? esc(payload.node.attr("description") ?? docs.objectDesc.get(payload.name) ?? "") : "", fields };
}
11 changes: 11 additions & 0 deletions server/typescript/packages/docs-site/src/builders/prompt-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { LinkGraph } from "../link-graph.js";
import type { CoverageTracker } from "../coverage.js";
import { highlightMustache } from "../mustache-highlight.js";
import { esc } from "../badges.js";
import { fmtAttrValue } from "./object-data.js";

// Template attrs rendered specifically (as their own badges) — everything else a
// template authors (e.g. a consumer's @dataflow) is rendered generically below.
const PROMPT_KNOWN_ATTRS = new Set(["format", "maxTokens", "requiredSlots", "model", "responseRef", "maxChars", "promptStyle", "payloadRef", "textRef", "description"]);

export interface PayloadTreeRow { indent: number; name: string; type: string; isArray: boolean; anchor: string; desc: string; refHtml: string; }
export interface PromptPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; attrsHtml: string; desc: string; payloadName: string; payloadHref: string; payloadTree: PayloadTreeRow[]; sourceHtml?: string | undefined; sourceMissingNote?: string | undefined; tocHtml?: string | undefined; packageFiles: { file: string; html: string }[]; }
Expand All @@ -17,6 +22,12 @@ export function buildPromptPage(fqn: string, g: LinkGraph, cov: CoverageTracker,
const v = t.attr(a); if (v !== undefined) { cov.consumeAttr(t, a); attrs.push(`<span class="badge badge-ghost badge-sm">@${esc(a)} ${esc(v)}</span>`); }
}
cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef");
// any other authored attrs (e.g. a consumer's @dataflow) rendered generically
for (const [n, v] of [...t.ownAttrs()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
if (PROMPT_KNOWN_ATTRS.has(n)) continue;
cov.consumeAttr(t, n);
attrs.push(`<span class="badge badge-ghost badge-sm font-mono">@${esc(n)}=${esc(fmtAttrValue(v))}</span>`);
}
// payload tree (root + one nested level)
const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
const payload = pRef ? g.byFqn(pRef.to) : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
{{#isAbstract}}<span class="badge badge-ghost badge-sm align-middle">abstract</span>{{/isAbstract}}</h1>
<div class="font-mono text-xs opacity-60 mt-1">
{{#tableName}}{{#isView}}view{{/isView}}{{^isView}}table{{/isView}} <code>{{tableName}}</code>{{/tableName}}
{{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}</div>
{{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}{{#storageAttrs}} · {{name}} <code>{{value}}</code>{{/storageAttrs}}</div>
{{#desc}}<section id="s-overview" class="mt-4"><p class="text-sm leading-relaxed">{{desc}}</p></section>{{/desc}}
{{#objectAttrs.length}}<section id="s-attributes" class="mt-4 text-xs"><span class="opacity-50 mr-1">attributes</span>{{#objectAttrs}}<span class="badge badge-soft badge-ghost badge-xs mr-1 font-mono">@{{name}}={{value}}</span>{{/objectAttrs}}</section>{{/objectAttrs.length}}

<section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
<div class="overflow-x-auto"><table class="table table-xs"><thead><tr><th>Field</th><th>Type</th><th></th><th>Description</th></tr></thead><tbody>
Expand All @@ -28,10 +29,13 @@
<table class="table table-xs"><tbody>{{#validators}}<tr><td class="font-mono text-xs opacity-70">{{subject}}</td><td class="font-mono text-xs">{{rule}}</td></tr>{{/validators}}</tbody></table></section>{{/validators.length}}

{{#relations.length}}<section id="s-relationships" class="mt-6"><h2 class="text-lg font-semibold">Relationships</h2>
<table class="table table-xs"><tbody>{{#relations}}<tr><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{cardinality}}</td><td><a class="link font-mono text-xs" href="{{toHref}}">{{toName}}</a></td></tr>{{/relations}}</tbody></table></section>{{/relations.length}}
<table class="table table-xs"><tbody>{{#relations}}<tr><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{cardinality}}</td><td><a class="link font-mono text-xs" href="{{toHref}}">{{toName}}</a></td><td class="text-xs opacity-60">{{#attrs}}<span class="font-mono mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/relations}}</tbody></table></section>{{/relations.length}}

{{#origins.length}}<section id="s-provenance" class="mt-6"><h2 class="text-lg font-semibold">Field provenance</h2>
<table class="table table-xs"><thead><tr><th>Field</th><th>From</th><th>Via</th></tr></thead><tbody>{{#origins}}<tr><td class="font-mono">{{field}}</td><td class="font-mono text-xs opacity-70">{{from}}</td><td class="font-mono text-xs opacity-70">{{via}}</td></tr>{{/origins}}</tbody></table></section>{{/origins.length}}
<table class="table table-xs"><thead><tr><th>Field</th><th>From</th><th>Via</th><th></th></tr></thead><tbody>{{#origins}}<tr><td class="font-mono">{{field}}</td><td class="font-mono text-xs opacity-70">{{from}}</td><td class="font-mono text-xs opacity-70">{{via}}</td><td class="text-xs opacity-60">{{#attrs}}<span class="font-mono mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/origins}}</tbody></table></section>{{/origins.length}}

{{#layouts.length}}<section id="s-layout" class="mt-6"><h2 class="text-lg font-semibold">Layout</h2>
<table class="table table-xs"><tbody>{{#layouts}}<tr><td class="font-mono text-xs opacity-70">{{kind}}</td><td class="text-xs">{{#attrs}}<span class="font-mono opacity-60 mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/layouts}}</tbody></table></section>{{/layouts.length}}

{{#inheritanceMermaid}}<section id="s-inheritance" class="mt-6"><h2 class="text-lg font-semibold">Inheritance</h2>
<div class="card bg-base-100 border border-base-300 p-2 pl-diagram"><pre class="mermaid">{{inheritanceMermaid}}</pre></div></section>{{/inheritanceMermaid}}
Expand Down
Loading
Loading