@metaobjectsdev/codegen-ts 0.9.0 → 0.10.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 +1 -1
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +24 -8
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -1
- package/dist/docs-paths.d.ts +58 -0
- package/dist/docs-paths.d.ts.map +1 -0
- package/dist/docs-paths.js +89 -0
- package/dist/docs-paths.js.map +1 -0
- package/dist/enum-import.d.ts +14 -0
- package/dist/enum-import.d.ts.map +1 -0
- package/dist/enum-import.js +35 -0
- package/dist/enum-import.js.map +1 -0
- package/dist/enum-shared.d.ts +32 -0
- package/dist/enum-shared.d.ts.map +1 -0
- package/dist/enum-shared.js +83 -0
- package/dist/enum-shared.js.map +1 -0
- package/dist/generator-registry.d.ts +22 -0
- package/dist/generator-registry.d.ts.map +1 -0
- package/dist/generator-registry.js +161 -0
- package/dist/generator-registry.js.map +1 -0
- package/dist/generator.d.ts +6 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/api-doc-render.d.ts +17 -0
- package/dist/generators/api-doc-render.d.ts.map +1 -0
- package/dist/generators/api-doc-render.js +431 -0
- package/dist/generators/api-doc-render.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts +21 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -0
- package/dist/generators/api-docs-file.js +112 -0
- package/dist/generators/api-docs-file.js.map +1 -0
- package/dist/generators/api-field-shape.d.ts +39 -0
- package/dist/generators/api-field-shape.d.ts.map +1 -0
- package/dist/generators/api-field-shape.js +92 -0
- package/dist/generators/api-field-shape.js.map +1 -0
- package/dist/generators/api-label.d.ts +3 -0
- package/dist/generators/api-label.d.ts.map +1 -0
- package/dist/generators/api-label.js +8 -0
- package/dist/generators/api-label.js.map +1 -0
- package/dist/generators/api-model.d.ts +122 -0
- package/dist/generators/api-model.d.ts.map +1 -0
- package/dist/generators/api-model.js +809 -0
- package/dist/generators/api-model.js.map +1 -0
- package/dist/generators/docs-data-builder.d.ts +26 -4
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +436 -164
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +136 -27
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/docs-data.js +1 -1
- package/dist/generators/docs-data.js.map +1 -1
- package/dist/generators/docs-file.d.ts +19 -0
- package/dist/generators/docs-file.d.ts.map +1 -1
- package/dist/generators/docs-file.js +154 -27
- package/dist/generators/docs-file.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +29 -14
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +2 -1
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/field-anchor.d.ts +7 -0
- package/dist/generators/field-anchor.d.ts.map +1 -0
- package/dist/generators/field-anchor.js +23 -0
- package/dist/generators/field-anchor.js.map +1 -0
- package/dist/generators/index.d.ts +8 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +6 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/mermaid-er.d.ts +14 -0
- package/dist/generators/mermaid-er.d.ts.map +1 -1
- package/dist/generators/mermaid-er.js +14 -0
- package/dist/generators/mermaid-er.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +3 -4
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/output-prompt-file.d.ts.map +1 -1
- package/dist/generators/output-prompt-file.js +2 -2
- package/dist/generators/output-prompt-file.js.map +1 -1
- package/dist/generators/prompt-render-file.d.ts.map +1 -1
- package/dist/generators/prompt-render-file.js +3 -4
- package/dist/generators/prompt-render-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +8 -3
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts.map +1 -1
- package/dist/generators/render-helper-file.js +2 -2
- package/dist/generators/render-helper-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +5 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +3 -0
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +6 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-doc-builder.d.ts +19 -0
- package/dist/generators/template-doc-builder.d.ts.map +1 -0
- package/dist/generators/template-doc-builder.js +220 -0
- package/dist/generators/template-doc-builder.js.map +1 -0
- package/dist/generators/template-doc-data.d.ts +62 -0
- package/dist/generators/template-doc-data.d.ts.map +1 -0
- package/dist/generators/template-doc-data.js +16 -0
- package/dist/generators/template-doc-data.js.map +1 -0
- package/dist/generators/template-payload-tree.d.ts +15 -0
- package/dist/generators/template-payload-tree.d.ts.map +1 -0
- package/dist/generators/template-payload-tree.js +61 -0
- package/dist/generators/template-payload-tree.js.map +1 -0
- package/dist/generators/template-source-annotate.d.ts +74 -0
- package/dist/generators/template-source-annotate.d.ts.map +1 -0
- package/dist/generators/template-source-annotate.js +184 -0
- package/dist/generators/template-source-annotate.js.map +1 -0
- package/dist/generators/template-source-render.d.ts +24 -0
- package/dist/generators/template-source-render.d.ts.map +1 -0
- package/dist/generators/template-source-render.js +175 -0
- package/dist/generators/template-source-render.js.map +1 -0
- package/dist/generators/trace-helper-file.d.ts +9 -0
- package/dist/generators/trace-helper-file.d.ts.map +1 -0
- package/dist/generators/trace-helper-file.js +196 -0
- package/dist/generators/trace-helper-file.js.map +1 -0
- package/dist/index.d.ts +29 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +75 -2
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +43 -0
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +19 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +41 -0
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +12 -4
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +51 -25
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/relation-resolver.d.ts +16 -0
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +82 -1
- package/dist/relation-resolver.js.map +1 -1
- package/dist/render-context.d.ts +4 -0
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js.map +1 -1
- package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
- package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
- package/dist/render-engine/embedded-templates.generated.js +15 -0
- package/dist/render-engine/embedded-templates.generated.js.map +1 -0
- package/dist/render-engine/framework-provider.d.ts.map +1 -1
- package/dist/render-engine/framework-provider.js +26 -13
- package/dist/render-engine/framework-provider.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +17 -0
- package/dist/runner.js.map +1 -1
- package/dist/templates/docs-file.d.ts +2 -6
- package/dist/templates/docs-file.d.ts.map +1 -1
- package/dist/templates/docs-file.js +2 -5
- package/dist/templates/docs-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +30 -2
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +7 -0
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +1 -1
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +16 -5
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/enums-file.d.ts +11 -0
- package/dist/templates/enums-file.d.ts.map +1 -0
- package/dist/templates/enums-file.js +44 -0
- package/dist/templates/enums-file.js.map +1 -0
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +5 -7
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extract-schema-emitter.d.ts.map +1 -1
- package/dist/templates/extract-schema-emitter.js +5 -1
- package/dist/templates/extract-schema-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +56 -39
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +1 -5
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-allowlist.d.ts +7 -2
- package/dist/templates/filter-allowlist.d.ts.map +1 -1
- package/dist/templates/filter-allowlist.js +17 -9
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-type.d.ts +7 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +9 -5
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/find-templates.d.ts +4 -0
- package/dist/templates/find-templates.d.ts.map +1 -0
- package/dist/templates/find-templates.js +15 -0
- package/dist/templates/find-templates.js.map +1 -0
- package/dist/templates/fr010-field-mapping.d.ts +2 -0
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +10 -6
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +44 -7
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +107 -16
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/mermaid-er.d.ts +35 -2
- package/dist/templates/mermaid-er.d.ts.map +1 -1
- package/dist/templates/mermaid-er.js +174 -7
- package/dist/templates/mermaid-er.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +30 -79
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.d.ts.map +1 -1
- package/dist/templates/output-prompt.js +2 -2
- package/dist/templates/output-prompt.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +112 -4
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +5 -0
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +7 -7
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/recover-schema-emitter.d.ts +8 -0
- package/dist/templates/recover-schema-emitter.d.ts.map +1 -0
- package/dist/templates/recover-schema-emitter.js +64 -0
- package/dist/templates/recover-schema-emitter.js.map +1 -0
- package/dist/templates/relations-block.js +10 -0
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +4 -4
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +183 -6
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/tph-discriminator.d.ts +56 -0
- package/dist/templates/tph-discriminator.d.ts.map +1 -0
- package/dist/templates/tph-discriminator.js +180 -0
- package/dist/templates/tph-discriminator.js.map +1 -0
- package/dist/templates/value-object-file.d.ts +2 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +32 -4
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +64 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +181 -8
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +103 -34
- package/src/column-mapper.ts +25 -8
- package/src/constants.ts +18 -0
- package/src/docs-paths.ts +128 -0
- package/src/enum-import.ts +43 -0
- package/src/enum-shared.ts +95 -0
- package/src/generator-registry.ts +204 -0
- package/src/generator.ts +6 -0
- package/src/generators/api-doc-render.ts +572 -0
- package/src/generators/api-docs-file.ts +146 -0
- package/src/generators/api-field-shape.ts +114 -0
- package/src/generators/api-label.ts +7 -0
- package/src/generators/api-model.ts +1067 -0
- package/src/generators/docs-data-builder.ts +479 -185
- package/src/generators/docs-data.ts +139 -28
- package/src/generators/docs-file.ts +205 -39
- package/src/generators/entity-file.ts +31 -15
- package/src/generators/extractor-file.ts +2 -1
- package/src/generators/field-anchor.ts +24 -0
- package/src/generators/index.ts +8 -1
- package/src/generators/mermaid-er.ts +14 -0
- package/src/generators/output-parser-file.ts +3 -4
- package/src/generators/output-prompt-file.ts +2 -1
- package/src/generators/prompt-render-file.ts +3 -4
- package/src/generators/queries-file.ts +9 -3
- package/src/generators/render-helper-file.ts +2 -1
- package/src/generators/routes-file-hono.ts +5 -1
- package/src/generators/routes-file.ts +7 -1
- package/src/generators/template-doc-builder.ts +306 -0
- package/src/generators/template-doc-data.ts +85 -0
- package/src/generators/template-payload-tree.ts +71 -0
- package/src/generators/template-source-annotate.ts +290 -0
- package/src/generators/template-source-render.ts +203 -0
- package/src/generators/trace-helper-file.ts +301 -0
- package/src/index.ts +55 -4
- package/src/metaobjects-config.ts +117 -2
- package/src/naming.ts +48 -0
- package/src/payload-codegen.ts +14 -3
- package/src/projection/extract-view-spec.ts +49 -30
- package/src/relation-resolver.ts +103 -1
- package/src/render-context.ts +4 -0
- package/src/render-engine/embedded-templates.generated.ts +14 -0
- package/src/render-engine/framework-provider.ts +25 -11
- package/src/runner.ts +21 -0
- package/src/templates/docs-file.ts +2 -9
- package/src/templates/drizzle-schema.ts +31 -1
- package/src/templates/entity-constants.ts +1 -1
- package/src/templates/entity-file.ts +16 -5
- package/src/templates/enums-file.ts +50 -0
- package/src/templates/extract-delegate-emitter.ts +5 -6
- package/src/templates/extractor.ts +68 -38
- package/src/templates/field-meta.ts +0 -6
- package/src/templates/filter-allowlist.ts +17 -10
- package/src/templates/filter-type.ts +8 -6
- package/src/templates/find-templates.ts +15 -0
- package/src/templates/fr010-field-mapping.ts +10 -8
- package/src/templates/inferred-types.ts +108 -18
- package/src/templates/mermaid-er.ts +176 -8
- package/src/templates/output-parser.ts +30 -79
- package/src/templates/output-prompt.ts +2 -1
- package/src/templates/queries-file.ts +132 -3
- package/src/templates/queries.ts +15 -7
- package/src/templates/relations-block.ts +17 -0
- package/src/templates/render-helper.ts +4 -3
- package/src/templates/routes-file.ts +233 -6
- package/src/templates/tph-discriminator.ts +232 -0
- package/src/templates/value-object-file.ts +38 -4
- package/src/templates/zod-validators.ts +204 -7
- package/templates/api/agent-api.md.mustache +30 -0
- package/templates/api/entity-api.md.mustache +69 -0
- package/templates/api/index.md.mustache +21 -0
- package/templates/docs/entity-page.md.mustache +33 -21
- package/templates/docs/template-page.md.mustache +56 -0
- package/src/templates/extract-schema-emitter.ts +0 -111
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TYPE_FIELD,
|
|
3
|
+
TYPE_IDENTITY,
|
|
3
4
|
TYPE_ORIGIN,
|
|
4
5
|
TYPE_RELATIONSHIP,
|
|
5
6
|
MetaSource,
|
|
@@ -79,26 +80,55 @@ export function projectionViewName(
|
|
|
79
80
|
return viewName(projection, { columnNamingStrategy });
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
|
|
85
|
+
* owner part of `<owner>.<child>...` resolved as an object. Mirrors the
|
|
86
|
+
* loader's `_refNamedOwner`: the ref names the anchor, never the physical
|
|
87
|
+
* declaring ancestor of an inherited child.
|
|
88
|
+
*/
|
|
89
|
+
function refNamedOwner(node: MetaData, root: MetaRoot): MetaObject | undefined {
|
|
90
|
+
const ref = (node as { superRef?: string }).superRef;
|
|
91
|
+
if (ref === undefined) return undefined;
|
|
92
|
+
const lastSep = ref.lastIndexOf("::");
|
|
93
|
+
const tail = lastSep === -1 ? ref : ref.slice(lastSep + 2);
|
|
94
|
+
const dot = tail.indexOf(".");
|
|
95
|
+
if (dot <= 0) return undefined;
|
|
96
|
+
return root.findObject(tail.slice(0, dot)) ?? undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
function baseEntityFor(
|
|
83
100
|
projection: MetaObject,
|
|
84
101
|
root: MetaRoot,
|
|
85
102
|
): MetaObject {
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// FR-024 base-anchor rules (mirror the loader's _deriveBaseEntity):
|
|
104
|
+
// 1) the extends-bound identity anchors the base entity;
|
|
105
|
+
// 2) else the single distinct entity targeted by extends-bound fields.
|
|
106
|
+
// The pre-FR-024 object-level `extends:` firehose is removed (B4b cutover).
|
|
107
|
+
//
|
|
108
|
+
// COUPLING NOTE: this intentionally derives the anchor ONLY from the ref's
|
|
109
|
+
// named owner (refNamedOwner), NOT the loader's `superResolved.parent`
|
|
110
|
+
// fallback for a non-dotted identity extends. That fallback is unreachable
|
|
111
|
+
// here because the loader gate (validate-identity-passthrough →
|
|
112
|
+
// ERR_PROJECTION_IDENTITY_NOT_EXTENDED) rejects any projection whose identity
|
|
113
|
+
// is not dotted-extends-bound before codegen runs. If that loader gate is
|
|
114
|
+
// ever loosened, this function must grow the same fallback.
|
|
115
|
+
for (const identity of projection
|
|
116
|
+
.ownChildren()
|
|
117
|
+
.filter((c) => c.type === TYPE_IDENTITY)) {
|
|
118
|
+
const named = refNamedOwner(identity, root);
|
|
119
|
+
if (named !== undefined) return named;
|
|
93
120
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
`Projection ${projection.name}: extends "${superName}" does not resolve to any entity.`,
|
|
99
|
-
);
|
|
121
|
+
const targets = new Set<MetaObject>();
|
|
122
|
+
for (const f of projection.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
123
|
+
const named = refNamedOwner(f, root);
|
|
124
|
+
if (named !== undefined && named !== projection) targets.add(named);
|
|
100
125
|
}
|
|
101
|
-
return
|
|
126
|
+
if (targets.size === 1) return [...targets][0]!;
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Projection ${projection.name}: cannot derive the base entity — declare an ` +
|
|
129
|
+
`extends-bound identity (identity.primary { name, extends: "<Entity>.<identity>" }) ` +
|
|
130
|
+
`to anchor the base (FR-024).`,
|
|
131
|
+
);
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
function sourceColumnNameFor(
|
|
@@ -270,22 +300,11 @@ function buildSelectSpec(
|
|
|
270
300
|
): SelectSpec {
|
|
271
301
|
const columns: SelectColumn[] = [];
|
|
272
302
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
(c) => c.type === TYPE_FIELD && c.name === baseField.name,
|
|
279
|
-
);
|
|
280
|
-
if (overridden) continue;
|
|
281
|
-
columns.push({
|
|
282
|
-
kind: "passthrough",
|
|
283
|
-
fieldName: baseField.name,
|
|
284
|
-
dbColAlias: sourceColumnNameFor(baseField, ctx),
|
|
285
|
-
sourceAlias: joinTree.baseAlias,
|
|
286
|
-
sourceColumn: sourceColumnNameFor(baseField, ctx),
|
|
287
|
-
});
|
|
288
|
-
}
|
|
303
|
+
// FR-024 (ADR-0028): the projection's DECLARED field set IS the exposure —
|
|
304
|
+
// the inclusive list, fail-closed by construction. The pre-FR-024 loop that
|
|
305
|
+
// emitted every base-entity field as an implicit passthrough (the firehose)
|
|
306
|
+
// is removed with the B4b cutover: base columns are declared explicitly as
|
|
307
|
+
// extends-bound fields (`{ field.int: { name: id, extends: "Program.id" } }`).
|
|
289
308
|
|
|
290
309
|
// Fields explicitly declared on the projection.
|
|
291
310
|
for (const field of projection.ownChildren()) {
|
package/src/relation-resolver.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Reads identity.reference declarations to determine the physical reference side.
|
|
7
7
|
|
|
8
|
-
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
8
|
+
import type { MetaRoot, MetaObject, MetaRelationship } from "@metaobjectsdev/metadata";
|
|
9
9
|
import {
|
|
10
10
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
11
11
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
12
|
+
RELATIONSHIP_ATTR_THROUGH,
|
|
12
13
|
CARDINALITY_ONE,
|
|
14
|
+
CARDINALITY_MANY,
|
|
15
|
+
deriveM2MFields,
|
|
13
16
|
stripPackage,
|
|
14
17
|
} from "@metaobjectsdev/metadata";
|
|
15
18
|
import { variableNameFromEntity } from "./naming.js";
|
|
@@ -26,6 +29,22 @@ export interface RelationEntry {
|
|
|
26
29
|
fkField?: string;
|
|
27
30
|
/** For cardinality 'one': the target entity's PK field (e.g., "id") */
|
|
28
31
|
targetPkField?: string;
|
|
32
|
+
/**
|
|
33
|
+
* FR-018 M:N navigation fields. Present only for a many-to-many navigation (a
|
|
34
|
+
* `@cardinality: "many"` relationship that declares `@through`). The Drizzle
|
|
35
|
+
* relations() block emits `many(<junction>)` for these; the routes file emits
|
|
36
|
+
* a `mountM2mRoute(...)` traversal. The junction FK fields are DERIVED from the
|
|
37
|
+
* junction entity's two `identity.reference` children (the SSOT), never
|
|
38
|
+
* restated on the relationship.
|
|
39
|
+
*/
|
|
40
|
+
/** The junction/through entity name (e.g., "PostTag"). M:N entries only. */
|
|
41
|
+
junctionEntity?: string;
|
|
42
|
+
/** Junction FK field holding the source-side key (logical field name, e.g. "postId"). M:N only. */
|
|
43
|
+
sourceJoinField?: string;
|
|
44
|
+
/** Junction FK field holding the target-side key (logical field name, e.g. "tagId"). M:N only. */
|
|
45
|
+
targetJoinField?: string;
|
|
46
|
+
/** Undirected self-join: union both junction FK columns at read time. M:N only. */
|
|
47
|
+
symmetric?: boolean;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
/** Map from entity name → list of relations for that entity's relations() block */
|
|
@@ -51,6 +70,16 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
51
70
|
|
|
52
71
|
for (const child of obj.relationships()) {
|
|
53
72
|
const cardinality = child.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
|
|
73
|
+
|
|
74
|
+
// FR-018 M:N: `@cardinality: "many"` + `@through` — derive the junction FK
|
|
75
|
+
// columns from the junction's identity.reference children and register a
|
|
76
|
+
// many(junction) navigation on the source.
|
|
77
|
+
if (cardinality === CARDINALITY_MANY && child.ownAttr(RELATIONSHIP_ATTR_THROUGH) !== undefined) {
|
|
78
|
+
const m2m = buildM2mEntry(obj, child as MetaRelationship, root);
|
|
79
|
+
if (m2m) ensure(obj.name).push(m2m);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
54
83
|
if (cardinality !== CARDINALITY_ONE) continue;
|
|
55
84
|
|
|
56
85
|
const targetEntityRaw = child.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
@@ -83,5 +112,78 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
114
|
|
|
115
|
+
// FR-018: junction entities reached via @through need their two belongs-to
|
|
116
|
+
// one() sides so the through-table is navigable in the Drizzle relational
|
|
117
|
+
// query API (db.query.posts.findMany({ with: { tags: { with: { tag: true }}}})).
|
|
118
|
+
// A junction is any entity named by some M:N relationship's @through.
|
|
119
|
+
for (const junctionName of collectJunctionNames(root)) {
|
|
120
|
+
const junction = root.findObject(junctionName);
|
|
121
|
+
if (!junction) continue;
|
|
122
|
+
const entries = ensure(junctionName);
|
|
123
|
+
for (const ref of junction.referenceIdentities()) {
|
|
124
|
+
const targetRaw = ref.targetEntity;
|
|
125
|
+
const fkField = ref.fields[0];
|
|
126
|
+
if (!targetRaw || !fkField) continue;
|
|
127
|
+
const targetEntity = stripPackage(targetRaw);
|
|
128
|
+
// The relation member is named after the target entity (camel singular);
|
|
129
|
+
// multiple references to the same entity (self-join junction) are
|
|
130
|
+
// disambiguated by the FK field name.
|
|
131
|
+
const refName = ref.name && ref.name.length > 0
|
|
132
|
+
? ref.name
|
|
133
|
+
: variableNameFromEntity(targetEntity);
|
|
134
|
+
entries.push({
|
|
135
|
+
name: refName,
|
|
136
|
+
cardinality: "one",
|
|
137
|
+
targetEntity,
|
|
138
|
+
fkField,
|
|
139
|
+
targetPkField: "id",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
86
144
|
return result;
|
|
87
145
|
}
|
|
146
|
+
|
|
147
|
+
/** Names of all entities that are the `@through` junction of some M:N relationship. */
|
|
148
|
+
function collectJunctionNames(root: MetaRoot): Set<string> {
|
|
149
|
+
const names = new Set<string>();
|
|
150
|
+
for (const obj of root.objects()) {
|
|
151
|
+
for (const rel of obj.relationships()) {
|
|
152
|
+
if (rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
|
|
153
|
+
const through = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
|
|
154
|
+
if (through) names.add(stripPackage(through));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return names;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the source-side M:N navigation entry: derive the junction FK fields from
|
|
162
|
+
* the junction's two identity.reference children (the SSOT), handling hetero /
|
|
163
|
+
* directed-self-join / symmetric. Returns null (skips the entry) if derivation
|
|
164
|
+
* fails — the loader validation pass surfaces the actionable error separately.
|
|
165
|
+
*/
|
|
166
|
+
function buildM2mEntry(
|
|
167
|
+
source: MetaObject,
|
|
168
|
+
rel: MetaRelationship,
|
|
169
|
+
root: MetaRoot,
|
|
170
|
+
): RelationEntry | null {
|
|
171
|
+
const targetRaw = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
172
|
+
const throughRaw = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
|
|
173
|
+
if (!targetRaw || !throughRaw) return null;
|
|
174
|
+
let fields;
|
|
175
|
+
try {
|
|
176
|
+
fields = deriveM2MFields(rel, source, root);
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
name: rel.name,
|
|
182
|
+
cardinality: "many",
|
|
183
|
+
targetEntity: stripPackage(targetRaw),
|
|
184
|
+
junctionEntity: stripPackage(throughRaw),
|
|
185
|
+
sourceJoinField: fields.sourceField,
|
|
186
|
+
targetJoinField: fields.targetField,
|
|
187
|
+
symmetric: rel.symmetric,
|
|
188
|
+
};
|
|
189
|
+
}
|
package/src/render-context.ts
CHANGED
|
@@ -52,6 +52,10 @@ export interface RenderContext {
|
|
|
52
52
|
relationMap: RelationMap;
|
|
53
53
|
/** Entity name → its metadata package (undefined if the entity has no package). Built once per run. */
|
|
54
54
|
packageOf: Map<string, string | undefined>;
|
|
55
|
+
/** FR-019: module specifier to import externally-PROVIDED shared enums from
|
|
56
|
+
* (`@provided: true` declarations). Undefined when unset — referencing a
|
|
57
|
+
* provided enum without it is a codegen-time error. */
|
|
58
|
+
providedEnumModule?: string;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @generated from templates/*/*.mustache — DO NOT EDIT.
|
|
2
|
+
// Regenerate: bun run scripts/generate-embedded-templates.ts (or scripts/sync-doc-templates.sh).
|
|
3
|
+
//
|
|
4
|
+
// Embedded framework doc templates so they resolve inside the
|
|
5
|
+
// `bun build --compile` standalone `meta` binary, where the on-disk
|
|
6
|
+
// `templates/` directory is unavailable. Keys are provider resolve refs
|
|
7
|
+
// (path under templates/ minus the .mustache suffix).
|
|
8
|
+
export const EMBEDDED_FRAMEWORK_TEMPLATES: Record<string, string> = {
|
|
9
|
+
"api/agent-api.md": "{{{generatedMarker}}}\n\n# {{title}}\n\nGenerated API reference for {{project}}; call these exactly as written. {{importNote}}\n{{#hasSetup}}\n\n## Setup\n{{#setup}}\n- `{{handle}}` — {{{note}}} `{{{snippetInline}}}`\n{{/setup}}\n{{/hasSetup}}\n{{#units}}\n\n## {{node}}\n{{#groups}}\n\n`{{importHeader}}`\n{{#symbols}}\n- `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}\n{{/symbols}}\n{{/groups}}\n{{#example}}\n\nExample:\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/units}}\n",
|
|
10
|
+
"api/entity-api.md": "{{{generatedMarker}}}\n\n# {{node}} API\n{{#modelPageHref}}\n\n**Model / metadata:** [{{node}}]({{modelPageHref}})\n{{/modelPageHref}}\n\n> Import paths are relative to your generated-output directory.\n{{#hasSetup}}\n\n## Setup\n\nObtain the runtime handles the calls below need:\n{{#setup}}\n\n- `{{handle}}` — {{{note}}}\n\n```ts\n{{{snippet}}}\n```\n{{/setup}}\n{{/hasSetup}}\n{{#unitExample}}\n\n## Example\n\n```ts\n{{{unitExample}}}\n```\n{{/unitExample}}\n{{#sections}}\n\n## {{heading}}\n{{#symbols}}\n\n### `{{signature}}`\n\n{{usage}}\n\n```ts\n{{importLine}}\n```\n{{#hasFields}}\n\n{{fieldsCaption}}:\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n{{#fieldRows}}\n| `{{field}}` | `{{{type}}}` | {{required}} | {{notes}} |\n{{/fieldRows}}\n{{/hasFields}}\n{{#mountNote}}\n\nMount: {{{mountNote}}}\n{{/mountNote}}\n{{#throws}}\n\nThrows: {{throws}}\n{{/throws}}\n{{#example}}\n\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/symbols}}\n{{/sections}}\n",
|
|
11
|
+
"api/index.md": "{{{generatedMarker}}}\n\n# {{title}}\n\n{{intro}}\n{{#hasEntities}}\n\n## Entities\n\n{{#entities}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/entities}}\n{{/hasEntities}}\n{{#hasTemplates}}\n\n## Templates\n\n{{#templates}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/templates}}\n{{/hasTemplates}}\n",
|
|
12
|
+
"docs/entity-page.md": "{{{generatedMarker}}}\n\n# {{entity.name}}\n{{#summaryLead}}\n\n{{{.}}}\n{{/summaryLead}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n{{#apiRefs.0}}\n\n**API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}\n{{/apiRefs.0}}\n\n{{{preambleHeader}}}\n{{#hasIdentities}}\n\n## Identity\n\n{{#identities}}\n- {{{bullet}}}\n{{/identities}}\n{{/hasIdentities}}\n{{#hasNeighborhoodEr}}\n\n## In context\n\n{{{neighborhoodErBlock}}}\n{{/hasNeighborhoodEr}}\n{{#fields.hasFields}}\n\n## Fields\n\n| Field | Type | Required | Column | Rules |\n|---|---|---|---|---|\n{{#fields.rows}}\n| {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |\n{{/fields.rows}}\n{{/fields.hasFields}}\n{{#fieldDetails.hasDetails}}\n\n## Field details\n\n{{#fieldDetails.rows}}\n{{{block}}}\n\n{{/fieldDetails.rows}}\n{{/fieldDetails.hasDetails}}\n{{#hasRelationships}}\n\n## Relationships\n\n{{#relationships}}\n- {{{bullet}}}\n{{/relationships}}\n{{/hasRelationships}}\n{{#hasUsedBy}}\n\n## Used by\n\n{{#usedBy}}\n- {{{bullet}}}\n{{/usedBy}}\n{{/hasUsedBy}}\n",
|
|
13
|
+
"docs/template-page.md": "{{{generatedMarker}}}\n\n# {{name}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n\n**Kind:** {{kind}}\n\n## Output\n{{^isEmail}}\n\n- Format: `{{format}}`\n{{/isEmail}}\n{{#isEmail}}\n\nMultipart email — rendered as the following parts:\n\n| Part | Source | Format | Escaping |\n|---|---|---|---|\n{{#parts}}\n| {{label}} | `{{ref}}` | `{{format}}` | {{#escaped}}escaped{{/escaped}}{{^escaped}}raw{{/escaped}} |\n{{/parts}}\n{{/isEmail}}\n\n## Input\n\n- Payload: [`{{payload.name}}`]({{payload.link}})\n{{#hasRequiredTags}}\n- Required fields:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Render contract\n\n- Every field referenced by the template is validated against the payload at generation time; an unknown field fails generation.\n{{#maxChars}}\n- Maximum length: {{.}} characters (rendering longer output fails).\n{{/maxChars}}\n{{#hasRequiredTags}}\n- Required tags must be present:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Source\n\n{{#sourceRefs}}\n- `{{.}}`\n{{/sourceRefs}}\n{{#templateSourceSection}}\n\n{{{.}}}\n{{/templateSourceSection}}\n\n## Capability\n\n{{capability}}\n",
|
|
14
|
+
};
|
|
@@ -16,6 +16,7 @@ import type { Provider } from "@metaobjectsdev/render";
|
|
|
16
16
|
import { existsSync, readFileSync } from "node:fs";
|
|
17
17
|
import { join, resolve, dirname } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { EMBEDDED_FRAMEWORK_TEMPLATES } from "./embedded-templates.generated.js";
|
|
19
20
|
|
|
20
21
|
/** Canonical shipped template — used to verify a candidate framework
|
|
21
22
|
* templates directory actually contains our defaults. Without this check a
|
|
@@ -31,8 +32,11 @@ const CANONICAL_TEMPLATE_REL = "docs/entity-page.md.mustache";
|
|
|
31
32
|
*
|
|
32
33
|
* Returns `undefined` when no on-disk templates dir can be found — e.g. inside
|
|
33
34
|
* the `bun build --compile` standalone binary, whose `import.meta.url` is a
|
|
34
|
-
* `/$bunfs/root` virtual path with no real `package.json` alongside it.
|
|
35
|
-
*
|
|
35
|
+
* `/$bunfs/root` virtual path with no real `package.json` alongside it. In that
|
|
36
|
+
* case `FrameworkTemplatesProvider.resolve` falls back to the bundled
|
|
37
|
+
* `EMBEDDED_FRAMEWORK_TEMPLATES` (embedded-templates.generated.ts), a plain
|
|
38
|
+
* string module generated from the canonical templates/docs/*.mustache and
|
|
39
|
+
* compiled into the binary. */
|
|
36
40
|
function findFrameworkTemplatesDir(start: string): string | undefined {
|
|
37
41
|
let dir = start;
|
|
38
42
|
while (true) {
|
|
@@ -81,23 +85,33 @@ export class FileSystemProvider implements Provider {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/** The framework defaults provider — resolves refs against codegen-ts's own
|
|
84
|
-
* on-disk `templates/` directory
|
|
88
|
+
* on-disk `templates/` directory, falling back to the bundled
|
|
89
|
+
* `EMBEDDED_FRAMEWORK_TEMPLATES` when no on-disk dir exists.
|
|
85
90
|
*
|
|
86
91
|
* Resolution is lazy: the directory is located on first `resolve()`, not at
|
|
87
92
|
* module import. This keeps merely importing this module side-effect-free,
|
|
88
93
|
* which matters for the `bun build --compile` standalone `meta` binary — its
|
|
89
94
|
* `import.meta.url` is a `/$bunfs/root` virtual path with no on-disk
|
|
90
|
-
* `templates/` dir
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
95
|
+
* `templates/` dir.
|
|
96
|
+
*
|
|
97
|
+
* ON-DISK FIRST, then embedded: the source/install layout (and adopter
|
|
98
|
+
* overrides chained ahead of this provider via `projectProvider`) always wins,
|
|
99
|
+
* so local edits to the shipped package `templates/` still take effect. Only
|
|
100
|
+
* when the on-disk dir is unresolved (the compiled binary) — or a ref the dir
|
|
101
|
+
* doesn't contain — do we consult the embedded map, which is generated from
|
|
102
|
+
* the same canonical templates and compiled into the binary. Unknown refs are
|
|
103
|
+
* `undefined` in the map, which is the correct miss. */
|
|
94
104
|
class FrameworkTemplatesProvider implements Provider {
|
|
95
105
|
resolve(ref: string): string | undefined {
|
|
106
|
+
// On-disk first: dev/install layout, plus shipped-package edits.
|
|
96
107
|
const dir = frameworkTemplatesDir();
|
|
97
|
-
if (dir
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
if (dir !== undefined) {
|
|
109
|
+
const path = join(dir, `${ref}.mustache`);
|
|
110
|
+
if (existsSync(path)) return readFileSync(path, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
// Embedded fallback: the binary case (no on-disk dir) or a ref the on-disk
|
|
113
|
+
// dir doesn't carry. `undefined` for unknown refs is the correct miss.
|
|
114
|
+
return EMBEDDED_FRAMEWORK_TEMPLATES[ref];
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
package/src/runner.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
* from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
|
|
21
21
|
const VALID_ENTITY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
22
|
|
|
23
|
+
/** ADR-0025: doc generators whose single door is `meta docs`. If a `meta gen`
|
|
24
|
+
* config still lists one (by its stable `name`), the runner warns + skips it. */
|
|
25
|
+
const DEPRECATED_DOC_GENERATORS = new Set(["docs-file", "api-docs"]);
|
|
26
|
+
|
|
23
27
|
export interface RunGenOpts {
|
|
24
28
|
config: MetaobjectsGenConfig;
|
|
25
29
|
metadata: MetaData;
|
|
@@ -143,9 +147,24 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
143
147
|
root.objects().map((o) => [o.name, o.package]),
|
|
144
148
|
);
|
|
145
149
|
|
|
150
|
+
// Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
|
|
151
|
+
// surface it on every generator's ctx.config so api-docs documents the Hono
|
|
152
|
+
// CRUD surface it actually emits (rather than silently omitting it).
|
|
153
|
+
const includeHonoRoutes = config.generators.some((g) => g.emitsHonoRoutes === true);
|
|
154
|
+
|
|
146
155
|
// 4. Run each generator with a per-target render context; collect with full path.
|
|
147
156
|
const emitted: { fullPath: string; content: string; generatedBy: string }[] = [];
|
|
148
157
|
for (const generator of config.generators) {
|
|
158
|
+
// ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
|
|
159
|
+
// still lists a deprecated doc generator is warned + skipped, not run — the
|
|
160
|
+
// generator stays as `meta docs`'s internal engine.
|
|
161
|
+
if (DEPRECATED_DOC_GENERATORS.has(generator.name)) {
|
|
162
|
+
warnings.push(
|
|
163
|
+
`[${generator.name}] docs are produced by 'meta docs' (ADR-0025); ` +
|
|
164
|
+
`remove ${generator.name === "api-docs" ? "apiDocsFile()" : "docsFile()"} from generators. Skipped.`,
|
|
165
|
+
);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
149
168
|
const selfTarget = targetOf(generator);
|
|
150
169
|
const renderContext = makeRenderContext({
|
|
151
170
|
dialect: config.dialect,
|
|
@@ -162,6 +181,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
162
181
|
packageOf,
|
|
163
182
|
selfTarget,
|
|
164
183
|
entityModuleTarget,
|
|
184
|
+
...(config.providedEnumModule !== undefined && { providedEnumModule: config.providedEnumModule }),
|
|
165
185
|
});
|
|
166
186
|
const ctx: GenContext = {
|
|
167
187
|
entities: safeEntities,
|
|
@@ -173,6 +193,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
173
193
|
dbImport: selfTarget.dbImport,
|
|
174
194
|
dialect: config.dialect,
|
|
175
195
|
outputLayout: selfTarget.outputLayout,
|
|
196
|
+
includeHonoRoutes,
|
|
176
197
|
},
|
|
177
198
|
renderContext,
|
|
178
199
|
...(projectRoot !== undefined && { projectRoot }),
|
|
@@ -22,15 +22,11 @@ export interface DocsRenderOpts {
|
|
|
22
22
|
dialect: Dialect;
|
|
23
23
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
24
24
|
loadedRoot: MetaRoot;
|
|
25
|
-
/** Names of generators present in the pipeline — drives the "Generated code"
|
|
26
|
-
* section. Always includes "entity-file" implicitly. Recognized names:
|
|
27
|
-
* "queries-file", "routes-file", "routes-file-hono". */
|
|
28
|
-
generatorNames?: ReadonlySet<string>;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
/** Backward-compatible entry point: builds the EntityDocData payload and
|
|
32
|
-
* renders it via the framework template.
|
|
33
|
-
*
|
|
28
|
+
* renders it via the framework template. Output is gated by
|
|
29
|
+
* `docs-file-conformance.test.ts`. */
|
|
34
30
|
export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string {
|
|
35
31
|
const data = buildEntityDocData(entity, {
|
|
36
32
|
dialect: opts.dialect,
|
|
@@ -38,9 +34,6 @@ export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string
|
|
|
38
34
|
? { columnNamingStrategy: opts.columnNamingStrategy }
|
|
39
35
|
: {}),
|
|
40
36
|
loadedRoot: opts.loadedRoot,
|
|
41
|
-
...(opts.generatorNames !== undefined
|
|
42
|
-
? { generatorNames: opts.generatorNames }
|
|
43
|
-
: {}),
|
|
44
37
|
});
|
|
45
38
|
return render({
|
|
46
39
|
ref: "docs/entity-page.md",
|
|
@@ -16,6 +16,7 @@ import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
|
|
|
16
16
|
import { tableNameFromEntity, variableNameFromEntity, columnNameFromField } from "../naming.js";
|
|
17
17
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
18
18
|
import { renderDocsFor } from "./jsdoc.js";
|
|
19
|
+
import { collectTphSubtypeFields } from "./tph-discriminator.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Render the Drizzle table definition for one entity, including:
|
|
@@ -83,6 +84,29 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// FR-017 Tier 2 — TPH single-table inheritance. When this entity is a
|
|
88
|
+
// discriminator base, fold every concrete subtype's own columns into this
|
|
89
|
+
// one table. Subtype-only columns are ALWAYS nullable (a row of any other
|
|
90
|
+
// subtype stores NULL there) and never carry a DB default (a default would
|
|
91
|
+
// stamp onto other-subtype inserts), regardless of the field's @required.
|
|
92
|
+
// Subtype entities emit no table of their own (the value-object path).
|
|
93
|
+
for (const child of collectTphSubtypeFields(obj, ctx.loadedRoot)) {
|
|
94
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
|
|
95
|
+
const fieldDocs = renderDocsFor(child);
|
|
96
|
+
const columnLine = renderColumn(
|
|
97
|
+
spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, true,
|
|
98
|
+
);
|
|
99
|
+
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
100
|
+
// Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
|
|
101
|
+
// (not false), so other-subtype rows with NULL pass the check.
|
|
102
|
+
if (spec.checkConstraint !== undefined) {
|
|
103
|
+
checkConstraints.push({
|
|
104
|
+
name: `chk_${tableName}_${spec.dbName}`,
|
|
105
|
+
expr: spec.checkConstraint,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
// Build all table callback entries
|
|
87
111
|
const callbackEntries: Code[] = [];
|
|
88
112
|
|
|
@@ -212,6 +236,9 @@ function renderColumn(
|
|
|
212
236
|
isComposite: boolean,
|
|
213
237
|
isUnique: boolean = false,
|
|
214
238
|
entityPackage: string | undefined = undefined,
|
|
239
|
+
// FR-017 Tier 2 — TPH subtype-only column: force nullable (drop .notNull())
|
|
240
|
+
// and suppress any DB default (other-subtype rows must stay NULL here).
|
|
241
|
+
forceNullable: boolean = false,
|
|
215
242
|
): Code {
|
|
216
243
|
const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
|
|
217
244
|
|
|
@@ -261,6 +288,9 @@ function renderColumn(
|
|
|
261
288
|
if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
|
|
262
289
|
// Avoid double-emitting .unique() if it was already appended above.
|
|
263
290
|
if (isUnique && m === ".unique()") continue;
|
|
291
|
+
// TPH subtype-only column: never .notNull() / .unique() — rows of other
|
|
292
|
+
// subtypes store NULL, so neither constraint can hold across the table.
|
|
293
|
+
if (forceNullable && (m === ".notNull()" || m === ".unique()")) continue;
|
|
264
294
|
modifiersStr += m;
|
|
265
295
|
}
|
|
266
296
|
|
|
@@ -268,7 +298,7 @@ function renderColumn(
|
|
|
268
298
|
// the `sql` import via imp(); a raw `.default(sql`...`)` would leave `sql`
|
|
269
299
|
// unresolved in the generated file.
|
|
270
300
|
let sqlDefaultSegment: Code | null = null;
|
|
271
|
-
if (spec.defaultExpr !== undefined && !isPk) {
|
|
301
|
+
if (spec.defaultExpr !== undefined && !isPk && !forceNullable) {
|
|
272
302
|
if (spec.defaultExpr.kind === "now") {
|
|
273
303
|
if (ctx.dialect === "sqlite") {
|
|
274
304
|
const sqlSym = imp("sql@drizzle-orm");
|
|
@@ -67,7 +67,7 @@ function humanize(s: string): string {
|
|
|
67
67
|
* "Subscriber" → "/subscribers"
|
|
68
68
|
* "WorkoutEvent" → "/workout_events"
|
|
69
69
|
*/
|
|
70
|
-
function resourcePath(entity: MetaData): string {
|
|
70
|
+
export function resourcePath(entity: MetaData): string {
|
|
71
71
|
const overrideAttr = entity.ownAttr("routePath");
|
|
72
72
|
if (typeof overrideAttr === "string" && overrideAttr.length > 0) {
|
|
73
73
|
return overrideAttr.startsWith("/") ? overrideAttr : `/${overrideAttr}`;
|
|
@@ -15,6 +15,7 @@ import { renderZodValidators } from "./zod-validators.js";
|
|
|
15
15
|
import { renderEntityConstants } from "./entity-constants.js";
|
|
16
16
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
17
17
|
import { renderFilterType } from "./filter-type.js";
|
|
18
|
+
import { renderTphDiscriminatorUnion, isTphDiscriminatorBase } from "./tph-discriminator.js";
|
|
18
19
|
import { GENERATED_HEADER } from "../constants.js";
|
|
19
20
|
import { isProjection } from "../projection/projection-detector.js";
|
|
20
21
|
import { renderProjectionDecl } from "./projection-decl.js";
|
|
@@ -53,7 +54,7 @@ export function renderEntityFile(
|
|
|
53
54
|
// it. The entity-file generator suppresses this entirely when
|
|
54
55
|
// emitAbstractShapes is off; here we only guarantee "shape, never table".
|
|
55
56
|
if (isAbstract(entity)) {
|
|
56
|
-
return renderValueObjectFile(entity);
|
|
57
|
+
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// --- Projection path (read-only: view-backed entity with no table source) ---
|
|
@@ -72,19 +73,29 @@ export function renderEntityFile(
|
|
|
72
73
|
// the shape (LLM tool_use input_schema, REST body parsing) use the Zod
|
|
73
74
|
// schema; consumers that need the type use the interface.
|
|
74
75
|
if (!hasWritableRdbSource(entity)) {
|
|
75
|
-
return renderValueObjectFile(entity);
|
|
76
|
+
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// --- Vanilla / write-through entity path ---
|
|
79
|
-
const enumAliases = renderEnumTypeAliases(entity);
|
|
80
|
+
const enumAliases = renderEnumTypeAliases(entity, ctx);
|
|
81
|
+
// FR-017 Tier 1: when this entity carries @discriminator AND has concrete
|
|
82
|
+
// subtypes, append the discriminated-union type alias, type guards, and
|
|
83
|
+
// the parse<Base>(row) dispatcher. Returns null otherwise (no subtypes, or
|
|
84
|
+
// not a discriminator-bearing entity); the section is suppressed cleanly.
|
|
85
|
+
const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
|
|
86
|
+
// FR-017: when a discriminator base also has a union block, the union owns the
|
|
87
|
+
// bare `<Base>` type — so the inferred Drizzle row type is emitted as
|
|
88
|
+
// `<Base>Row` to avoid a duplicate `export type <Base>`.
|
|
89
|
+
const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
|
|
80
90
|
const sections: Code[] = [
|
|
81
91
|
renderDrizzleSchema(entity, ctx),
|
|
82
|
-
renderInferredTypes(entity),
|
|
92
|
+
renderInferredTypes(entity, tphBase),
|
|
83
93
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
84
|
-
renderZodValidators(entity),
|
|
94
|
+
renderZodValidators(entity, ctx),
|
|
85
95
|
renderEntityConstants(entity, ctx.apiPrefix),
|
|
86
96
|
...(allowlists ? [renderFilterAllowlist(entity), renderSortAllowlist(entity)] : []),
|
|
87
97
|
renderFilterType(entity),
|
|
98
|
+
...(tphBlock !== null ? [tphBlock] : []),
|
|
88
99
|
];
|
|
89
100
|
|
|
90
101
|
// Render ts-poet body first (ts-poet hoists imp()-tracked imports to the top),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// FR-019 — shared enums module.
|
|
2
|
+
//
|
|
3
|
+
// Emits, ONCE per run, the materialized (non-@provided) shared enum types that
|
|
4
|
+
// at least one concrete entity field references via `extends` of a root-level
|
|
5
|
+
// abstract `field.enum`. Each enum yields:
|
|
6
|
+
// • `export type <E> = "A" | "B";` — the cross-port type identity
|
|
7
|
+
// • `export const <E>Enum = z.enum(["A","B"]);` — the shared Zod validator
|
|
8
|
+
//
|
|
9
|
+
// Consuming entity files import these instead of redeclaring the union inline.
|
|
10
|
+
// @provided enums are NEVER materialized here (they live in hand-written code,
|
|
11
|
+
// imported from the configured providedEnumModule).
|
|
12
|
+
|
|
13
|
+
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
14
|
+
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
15
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
16
|
+
import { materializedSharedEnums, type SharedEnum } from "../enum-shared.js";
|
|
17
|
+
import { enumUnionString } from "./inferred-types.js";
|
|
18
|
+
|
|
19
|
+
/** Basename (no extension) of the shared-enums module emitted at the entity-module target root. */
|
|
20
|
+
export const SHARED_ENUMS_BASENAME = "enums";
|
|
21
|
+
|
|
22
|
+
/** The exported Zod-constant name for a shared enum (`<E>Enum`). */
|
|
23
|
+
export function sharedEnumZodConstName(enumName: string): string {
|
|
24
|
+
return `${enumName}Enum`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One enum's two declarations (type alias + shared z.enum const). */
|
|
28
|
+
function renderOneSharedEnum(e: SharedEnum): Code {
|
|
29
|
+
const z = imp("z@zod");
|
|
30
|
+
const members = e.values.map((v) => JSON.stringify(v)).join(", ");
|
|
31
|
+
return code`
|
|
32
|
+
export type ${e.name} = ${enumUnionString(e.values)};
|
|
33
|
+
export const ${sharedEnumZodConstName(e.name)} = ${z}.enum([${members}]);
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The full shared-enums module body, or null when the model has no materialized
|
|
39
|
+
* shared enums (so the generator emits no file at all).
|
|
40
|
+
*/
|
|
41
|
+
export function renderSharedEnumsFile(root: MetaRoot): string | null {
|
|
42
|
+
const enums = materializedSharedEnums(root);
|
|
43
|
+
if (enums.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const body = joinCode(enums.map(renderOneSharedEnum), { on: "\n" }).toString();
|
|
46
|
+
const header =
|
|
47
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
48
|
+
`// Shared enum types (FR-019): one declaration per reused package-level enum.\n`;
|
|
49
|
+
return header + body;
|
|
50
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// server/typescript/packages/codegen-ts/src/templates/extract-delegate-emitter.ts
|
|
2
2
|
//
|
|
3
|
-
// FR-010
|
|
3
|
+
// FR-010 — the runtime-DELEGATING extract emitter (the single metadata-driven extract path).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// array-of-object components NULL — the historical FR-010 codegen gap. This module emits
|
|
8
|
-
// the additive delegating overload that CLOSES that gap by wrapping the runtime extract:
|
|
5
|
+
// This module emits the loader-delegating extract entry point that reads the live metadata
|
|
6
|
+
// directly and populates nested-object and array-of-object components in full:
|
|
9
7
|
//
|
|
10
8
|
// extract<Name>(root: MetaRoot, text, opts?) -> ExtractionResult<<Name>Extracted>
|
|
11
9
|
//
|
|
@@ -29,11 +27,12 @@ import {
|
|
|
29
27
|
FIELD_SUBTYPE_ENUM,
|
|
30
28
|
FIELD_ATTR_OBJECT_REF,
|
|
31
29
|
PACKAGE_SEPARATOR,
|
|
30
|
+
refMatchesObject,
|
|
32
31
|
} from "@metaobjectsdev/metadata";
|
|
33
32
|
import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
|
|
34
33
|
|
|
35
34
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
36
|
-
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
35
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|