@metaobjectsdev/docs-site 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -14
- package/dist/builders/object-data.d.ts +5 -0
- package/dist/builders/object-data.d.ts.map +1 -1
- package/dist/builders/object-data.js +12 -3
- package/dist/builders/object-data.js.map +1 -1
- package/dist/builders/report-data.d.ts +44 -0
- package/dist/builders/report-data.d.ts.map +1 -0
- package/dist/builders/report-data.js +99 -0
- package/dist/builders/report-data.js.map +1 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/coverage.js +3 -1
- package/dist/coverage.js.map +1 -1
- package/dist/link-graph.d.ts +4 -2
- package/dist/link-graph.d.ts.map +1 -1
- package/dist/link-graph.js +44 -47
- package/dist/link-graph.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +2 -0
- package/dist/site.js.map +1 -1
- package/package.json +4 -4
- package/src/builders/object-data.ts +19 -3
- package/src/builders/report-data.ts +149 -0
- package/src/coverage.ts +8 -2
- package/src/link-graph.ts +47 -44
- package/src/site.ts +2 -0
- package/templates/chrome-foot.mustache +1 -1
- package/templates/object.html.mustache +19 -3
package/src/link-graph.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
import type { MetaData, MetaObject, MetaRelationship } from "@metaobjectsdev/metadata";
|
|
2
|
-
import {
|
|
1
|
+
import type { MetaData, MetaObject, MetaRelationship, MetaRoot } from "@metaobjectsdev/metadata";
|
|
2
|
+
import {
|
|
3
|
+
deriveM2MFields, resolveRelationshipReference,
|
|
4
|
+
OBJECT_REPORT_ATTR_FROM, OBJECT_SUBTYPE_REPORT, TYPE_OBJECT,
|
|
5
|
+
RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH, IDENTITY_REFERENCE_ATTR_REFERENCES,
|
|
6
|
+
FIELD_ATTR_OBJECT_REF, ORIGIN_PASSTHROUGH_ATTR_FROM, TEMPLATE_ATTR_PAYLOAD_REF, TEMPLATE_ATTR_RESPONSE_REF,
|
|
7
|
+
refTarget,
|
|
8
|
+
} from "@metaobjectsdev/metadata";
|
|
3
9
|
import { type LoadedModel, treeOf } from "./load.js";
|
|
4
10
|
|
|
5
11
|
export interface DocNode { kind: "object" | "prompt" | "output"; name: string; pkg: string; pkgPath: string; href: string; node: MetaData; tree: string; }
|
|
6
12
|
export interface Ref {
|
|
7
13
|
from: string; to: string; via: string;
|
|
8
|
-
kind: "field" | "fk" | "extends" | "payload" | "response" | "relationship" | "origin";
|
|
14
|
+
kind: "field" | "fk" | "extends" | "payload" | "response" | "relationship" | "origin" | "report";
|
|
9
15
|
cardinality?: "one" | "many" | undefined;
|
|
10
16
|
through?: string | undefined; // junction FQN (M:N)
|
|
11
17
|
sourceJoinField?: string | undefined; // junction source FK (M:N)
|
|
@@ -30,8 +36,11 @@ export class LinkGraph {
|
|
|
30
36
|
private _to = new Map<string, Ref[]>();
|
|
31
37
|
private _extBy = new Map<string, DocNode[]>();
|
|
32
38
|
private _origins = new Map<string, OriginRef[]>();
|
|
39
|
+
/** The loaded root the graph was built from (a report's derived columns resolve against it). */
|
|
40
|
+
readonly root: MetaRoot;
|
|
33
41
|
|
|
34
42
|
constructor(model: LoadedModel) {
|
|
43
|
+
this.root = model.root;
|
|
35
44
|
for (const o of model.root.ownChildren()) {
|
|
36
45
|
let kind: DocNode["kind"] | undefined;
|
|
37
46
|
if (o.type === "object") kind = "object";
|
|
@@ -48,16 +57,20 @@ export class LinkGraph {
|
|
|
48
57
|
(this._from.get(r.from) ?? this._from.set(r.from, []).get(r.from)!).push(r);
|
|
49
58
|
(this._to.get(r.to) ?? this._to.set(r.to, []).get(r.to)!).push(r);
|
|
50
59
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
// ADR-0059 R7 — an edge points at the object the reference attribute is bound to,
|
|
61
|
+
// resolved where the attribute was declared, never re-resolved in an inheritor's package.
|
|
62
|
+
const bound = (node: MetaData, attr: string): string | undefined => {
|
|
63
|
+
const target = refTarget(node, attr);
|
|
64
|
+
if (target === undefined) return undefined;
|
|
65
|
+
const key = fqnOf(target);
|
|
66
|
+
return this._nodes.has(key) ? key : undefined;
|
|
54
67
|
};
|
|
55
68
|
// M:N through-junction edge. Derives the junction FK fields via the metadata
|
|
56
69
|
// SSOT (deriveM2MFields) — hetero, directed (@sourceRefField), or symmetric
|
|
57
70
|
// (@symmetric). On a derivation failure (ambiguous self-join) the edge is
|
|
58
71
|
// SKIPPED — generation never fails.
|
|
59
|
-
const addM2mEdge = (from: string, to: string, rel: MetaRelationship, obj: MetaObject,
|
|
60
|
-
const through = rel.through ? (
|
|
72
|
+
const addM2mEdge = (from: string, to: string, rel: MetaRelationship, obj: MetaObject, onDelete: string | undefined, subtype: string | undefined): void => {
|
|
73
|
+
const through = rel.through ? (bound(rel, RELATIONSHIP_ATTR_THROUGH) ?? rel.through) : undefined;
|
|
61
74
|
let sourceJoinField: string | undefined, targetJoinField: string | undefined;
|
|
62
75
|
try {
|
|
63
76
|
const f = deriveM2MFields(rel, obj, model.root);
|
|
@@ -72,11 +85,8 @@ export class LinkGraph {
|
|
|
72
85
|
const fqn = fqnOf(dn.node);
|
|
73
86
|
if (dn.kind === "object") {
|
|
74
87
|
for (const f of dn.node.childrenOfType("field")) {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
const to = resolveRef(ref, dn.pkg);
|
|
78
|
-
if (to) addRef({ from: fqn, to, via: f.name, kind: "field" });
|
|
79
|
-
}
|
|
88
|
+
const to = bound(f, FIELD_ATTR_OBJECT_REF);
|
|
89
|
+
if (to) addRef({ from: fqn, to, via: f.name, kind: "field" });
|
|
80
90
|
}
|
|
81
91
|
// dn.node is typed MetaData; the enclosing dn.kind === "object" guard
|
|
82
92
|
// guarantees it is a MetaObject at runtime. The `unknown` bridge is needed
|
|
@@ -88,15 +98,13 @@ export class LinkGraph {
|
|
|
88
98
|
// reference whose (target, first-field) a belongs-to relationship covered.
|
|
89
99
|
const coveredFk = new Set<string>();
|
|
90
100
|
for (const rel of obj.relationships()) {
|
|
91
|
-
const
|
|
92
|
-
if (typeof objectRef !== "string") continue;
|
|
93
|
-
const to = resolveRef(objectRef, dn.pkg);
|
|
101
|
+
const to = bound(rel, RELATIONSHIP_ATTR_OBJECT_REF);
|
|
94
102
|
if (!to) continue;
|
|
95
103
|
const cardinality = rel.cardinality === "many" ? "many" : rel.cardinality === "one" ? "one" : undefined;
|
|
96
104
|
const onDelete = rel.onDelete;
|
|
97
105
|
const subtype = rel.subType;
|
|
98
106
|
if (cardinality === "many" && rel.through !== undefined) {
|
|
99
|
-
addM2mEdge(fqn, to, rel, obj,
|
|
107
|
+
addM2mEdge(fqn, to, rel, obj, onDelete, subtype); // Task 3 helper
|
|
100
108
|
continue;
|
|
101
109
|
}
|
|
102
110
|
// belongs-to (1:N, one) — resolve the identity.reference this relationship
|
|
@@ -109,38 +117,39 @@ export class LinkGraph {
|
|
|
109
117
|
// uses; ambiguous cases resolve to undefined and simply leave every candidate
|
|
110
118
|
// reference to draw its own "fk" edge, which is the safe (extra-info, not
|
|
111
119
|
// missing-edge) outcome for a docs graph.
|
|
112
|
-
const
|
|
113
|
-
|
|
120
|
+
const match = resolveRelationshipReference(
|
|
121
|
+
obj, rel.name, refTarget(rel, RELATIONSHIP_ATTR_OBJECT_REF), rel.sourceRefField,
|
|
122
|
+
);
|
|
114
123
|
const fkField = match?.fields?.[0];
|
|
115
124
|
if (fkField) coveredFk.add(`${to}::${fkField}`);
|
|
116
125
|
addRef({ from: fqn, to, via: rel.name, kind: "relationship", cardinality, onDelete, subtype });
|
|
117
126
|
}
|
|
118
127
|
for (const id of dn.node.childrenOfType("identity")) {
|
|
119
128
|
if (id.subType !== "reference") continue;
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const via = Array.isArray(fieldsValue) ? fieldsValue.join(", ") : String(fieldsValue);
|
|
128
|
-
addRef({ from: fqn, to, via, kind: "fk" });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
129
|
+
const to = bound(id, IDENTITY_REFERENCE_ATTR_REFERENCES);
|
|
130
|
+
if (!to) continue;
|
|
131
|
+
const fieldsValue = id.attr("fields") ?? id.name;
|
|
132
|
+
const firstField = Array.isArray(fieldsValue) ? String(fieldsValue[0] ?? "") : String(fieldsValue);
|
|
133
|
+
if (coveredFk.has(`${to}::${firstField}`)) continue; // superseded by a relationship edge
|
|
134
|
+
const via = Array.isArray(fieldsValue) ? fieldsValue.join(", ") : String(fieldsValue);
|
|
135
|
+
addRef({ from: fqn, to, via, kind: "fk" });
|
|
131
136
|
}
|
|
132
137
|
for (const f of dn.node.childrenOfType("field")) {
|
|
133
138
|
for (const org of f.childrenOfType("origin")) {
|
|
134
139
|
const from = String(org.attr("from") ?? ""), via = String(org.attr("via") ?? "");
|
|
135
140
|
(this._origins.get(fqn) ?? this._origins.set(fqn, []).get(fqn)!).push({ field: f.name, from, via });
|
|
136
141
|
// connect a projection to its source object so it is not orphaned on diagrams:
|
|
137
|
-
// `from` is "pkg::Entity.field"
|
|
138
|
-
const
|
|
139
|
-
const srcRef = dot > 0 ? from.slice(0, dot) : "";
|
|
140
|
-
const to = srcRef ? resolveRef(srcRef, dn.pkg) : undefined;
|
|
142
|
+
// `from` is "pkg::Entity.field"; its bound target is the source object.
|
|
143
|
+
const to = bound(org, ORIGIN_PASSTHROUGH_ATTR_FROM);
|
|
141
144
|
if (to && to !== fqn) addRef({ from: fqn, to, via: `${f.name} (origin)`, kind: "origin" });
|
|
142
145
|
}
|
|
143
146
|
}
|
|
147
|
+
// FR-044: a report reads from its @from entity. The edge is what puts the report on
|
|
148
|
+
// that entity's page and diagrams, and keeps a report from reading as an orphan.
|
|
149
|
+
if (dn.node.type === TYPE_OBJECT && dn.node.subType === OBJECT_SUBTYPE_REPORT) {
|
|
150
|
+
const to = bound(dn.node, OBJECT_REPORT_ATTR_FROM);
|
|
151
|
+
if (to) addRef({ from: fqn, to, via: OBJECT_REPORT_ATTR_FROM, kind: "report" });
|
|
152
|
+
}
|
|
144
153
|
const sup = dn.node.superResolved;
|
|
145
154
|
if (sup) {
|
|
146
155
|
const supFqn = fqnOf(sup);
|
|
@@ -148,18 +157,12 @@ export class LinkGraph {
|
|
|
148
157
|
(this._extBy.get(supFqn) ?? this._extBy.set(supFqn, []).get(supFqn)!).push(dn);
|
|
149
158
|
}
|
|
150
159
|
} else {
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
const to = resolveRef(p, dn.pkg);
|
|
154
|
-
if (to) addRef({ from: fqn, to, via: "payloadRef", kind: "payload" });
|
|
155
|
-
}
|
|
160
|
+
const toPayload = bound(dn.node, TEMPLATE_ATTR_PAYLOAD_REF);
|
|
161
|
+
if (toPayload) addRef({ from: fqn, to: toPayload, via: "payloadRef", kind: "payload" });
|
|
156
162
|
// A prompt's @responseRef is the response value-object it PRODUCES — the
|
|
157
163
|
// missing hop of the data-flow pipeline (prompt → response VO → entity).
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
const to = resolveRef(r, dn.pkg);
|
|
161
|
-
if (to) addRef({ from: fqn, to, via: "responseRef", kind: "response" });
|
|
162
|
-
}
|
|
164
|
+
const toResponse = bound(dn.node, TEMPLATE_ATTR_RESPONSE_REF);
|
|
165
|
+
if (toResponse) addRef({ from: fqn, to: toResponse, via: "responseRef", kind: "response" });
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
}
|
package/src/site.ts
CHANGED
|
@@ -189,6 +189,7 @@ export async function generateSite(opts: SiteOptions): Promise<SiteResult> {
|
|
|
189
189
|
const objectTocHtml = (d: ObjectPageData): string => {
|
|
190
190
|
const sections: { id: string; label: string; present: boolean }[] = [
|
|
191
191
|
{ id: "s-overview", label: "Overview", present: !!d.desc },
|
|
192
|
+
{ id: "s-report", label: "Report", present: d.report !== undefined },
|
|
192
193
|
{ id: "s-fields", label: "Fields", present: d.ownFields.length > 0 },
|
|
193
194
|
{ id: "s-indexes", label: "Indexes & keys", present: d.indexes.length > 0 },
|
|
194
195
|
{ id: "s-validators", label: "Validators", present: d.validators.length > 0 },
|
|
@@ -196,6 +197,7 @@ export async function generateSite(opts: SiteOptions): Promise<SiteResult> {
|
|
|
196
197
|
{ id: "s-provenance", label: "Field provenance", present: d.origins.length > 0 },
|
|
197
198
|
{ id: "s-inheritance", label: "Inheritance", present: !!d.inheritanceMermaid },
|
|
198
199
|
{ id: "s-neighborhood", label: "Neighborhood", present: !!d.neighborhoodMermaid },
|
|
200
|
+
{ id: "s-reporting", label: "Reporting", present: d.reporting !== undefined },
|
|
199
201
|
{ id: "s-referenced-by", label: "Referenced by", present: d.referencedBy.length > 0 },
|
|
200
202
|
];
|
|
201
203
|
return sections
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
</div>
|
|
12
12
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
13
13
|
</dialog>
|
|
14
|
-
<footer class="pl-footer text-xs opacity-40 px-10 pb-6">generated {{stamp}} · {{commit}} · metadata-docs</footer>
|
|
14
|
+
<footer class="pl-footer text-xs opacity-40 px-10 pb-6">generated{{#stamp}} {{stamp}}{{/stamp}}{{#commit}} · {{commit}}{{/commit}} · metadata-docs</footer>
|
|
15
15
|
<script>window.__REL_ROOT__ = "{{relRoot}}";</script>
|
|
16
16
|
<script src="{{relRoot}}assets/site.js"></script>
|
|
17
17
|
</body></html>
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
{{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}{{#storageAttrs}} · {{name}} <code>{{value}}</code>{{/storageAttrs}}</div>
|
|
9
9
|
{{#desc}}<section id="s-overview" class="mt-4"><p class="text-sm leading-relaxed">{{desc}}</p></section>{{/desc}}
|
|
10
10
|
{{#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}}
|
|
11
|
+
{{#report}}
|
|
12
|
+
<section id="s-report" class="mt-6"><h2 class="text-lg font-semibold">Report</h2>
|
|
13
|
+
<div class="font-mono text-xs opacity-70 mt-1">from <a class="link font-mono" href="{{fromHref}}">{{fromName}}</a>{{#viewName}} · view <code>{{viewName}}</code>{{/viewName}}{{#notServed}} · <span class="badge badge-soft badge-warning badge-xs">{{notServed}}</span>{{/notServed}}{{#rowsHtml}} · rows: {{{rowsHtml}}}{{/rowsHtml}}{{#scopeHtml}} · row scope {{{scopeHtml}}}{{/scopeHtml}}</div>
|
|
14
|
+
<div class="overflow-x-auto"><table class="table table-xs"><thead><tr><th>Column</th><th>Type</th><th>Nullable</th><th>Role</th><th>Definition</th></tr></thead><tbody>
|
|
15
|
+
{{#columns}}<tr><td class="font-mono">{{name}}</td><td class="opacity-60">{{type}}</td><td class="opacity-60">{{nullable}}</td><td class="opacity-60">{{role}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
|
|
16
|
+
{{/columns}}
|
|
17
|
+
</tbody></table></div></section>
|
|
18
|
+
{{/report}}
|
|
11
19
|
|
|
12
|
-
<section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
|
|
20
|
+
{{^report}}<section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
|
|
13
21
|
<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>
|
|
14
22
|
{{#ownFields}}<tr id="{{anchor}}"><td class="font-mono">{{name}}{{#isArray}}[]{{/isArray}}</td><td class="opacity-60">{{type}}</td><td>{{{badgesHtml}}}</td><td class="text-xs">{{desc}}</td></tr>
|
|
15
23
|
{{#enumValues.length}}<tr><td></td><td colspan="3" class="pl-4">{{#enumValues}}<span class="badge badge-soft badge-accent badge-xs mr-1">{{value}}{{#deflt}} ★{{/deflt}}</span>{{/enumValues}}</td></tr>{{/enumValues.length}}
|
|
@@ -18,7 +26,7 @@
|
|
|
18
26
|
</tbody></table></div>
|
|
19
27
|
{{#inheritedFields.length}}<div class="collapse collapse-arrow bg-base-100 border border-base-300 mt-2"><input type="checkbox"><div class="collapse-title text-sm font-semibold opacity-70">Inherited fields ({{inheritedFields.length}})</div><div class="collapse-content"><table class="table table-xs opacity-80"><tbody>
|
|
20
28
|
{{#inheritedFields}}<tr id="{{anchor}}"><td class="font-mono">{{name}}{{#isArray}}[]{{/isArray}} <span class="text-[10px] opacity-50">↑ <a class="link" href="{{inheritedFrom.href}}">{{inheritedFrom.name}}</a></span></td><td class="opacity-60">{{type}}</td><td>{{{badgesHtml}}}</td></tr>{{/inheritedFields}}
|
|
21
|
-
</tbody></table></div></div>{{/inheritedFields.length}}</section>
|
|
29
|
+
</tbody></table></div></div>{{/inheritedFields.length}}</section>{{/report}}
|
|
22
30
|
|
|
23
31
|
{{#indexes.length}}<section id="s-indexes" class="mt-6"><h2 class="text-lg font-semibold">Indexes & keys</h2>
|
|
24
32
|
<table class="table table-xs"><thead><tr><th>Name</th><th>Kind</th><th>Fields</th><th>Detail</th></tr></thead><tbody>
|
|
@@ -46,6 +54,14 @@
|
|
|
46
54
|
<div class="text-[11px] opacity-50 mt-1"><span class="font-mono">▭</span> entity · <span class="font-mono">⬭</span> value object · <span class="font-mono">▱</span> view <span class="opacity-70">(ER boxes: dashed = value, dotted = view)</span></div>
|
|
47
55
|
{{#neighborhoodMore}}<div class="text-xs opacity-50 mt-1">+{{neighborhoodMore}} more neighbor(s){{#referencedBy.length}} — see <a class="link" href="#s-referenced-by">Referenced by</a>{{/referencedBy.length}}.</div>{{/neighborhoodMore}}</section>{{/neighborhoodMermaid}}
|
|
48
56
|
|
|
49
|
-
{{#
|
|
57
|
+
{{#reporting}}<section id="s-reporting" class="mt-6"><h2 class="text-lg font-semibold">Reporting</h2>
|
|
58
|
+
<table class="table table-xs"><tbody>
|
|
59
|
+
{{#dimensions}}<tr><td class="text-xs opacity-50">dimension</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
|
|
60
|
+
{{/dimensions}}{{#measures}}<tr><td class="text-xs opacity-50">measure</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
|
|
61
|
+
{{/measures}}{{#segments}}<tr><td class="text-xs opacity-50">segment</td><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{kind}}</td><td class="text-xs">{{{definitionHtml}}}</td></tr>
|
|
62
|
+
{{/segments}}</tbody></table>
|
|
63
|
+
{{#reports.length}}<div class="text-sm mt-1"><span class="opacity-50">reports</span> {{#reports}}<a class="link font-mono text-xs" href="{{href}}">{{name}}</a> {{/reports}}</div>{{/reports.length}}</section>
|
|
64
|
+
|
|
65
|
+
{{/reporting}}{{#referencedBy.length}}<section id="s-referenced-by" class="mt-6 text-sm"><span class="opacity-50">referenced by</span> {{#referencedBy}}<a class="link font-mono text-xs" href="{{href}}" title="{{via}}">{{name}}</a> {{/referencedBy}}</section>{{/referencedBy.length}}
|
|
50
66
|
{{#usedByTemplates.length}}<div class="text-sm mt-1"><span class="opacity-50">used by templates</span> {{#usedByTemplates}}<a class="link font-mono text-xs" href="{{href}}">{{name}}</a> {{/usedByTemplates}}</div>{{/usedByTemplates.length}}
|
|
51
67
|
<div class="text-[10px] opacity-40 mt-6 font-mono">{{sourceFile}}</div>
|