@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
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
TEMPLATE_ATTR_SUBJECT_REF,
|
|
44
44
|
TEMPLATE_ATTR_HTML_BODY_REF,
|
|
45
45
|
TEMPLATE_ATTR_TEXT_BODY_REF,
|
|
46
|
+
refMatchesObject,
|
|
46
47
|
} from "@metaobjectsdev/metadata";
|
|
47
48
|
import {
|
|
48
49
|
verify,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
} from "@metaobjectsdev/render";
|
|
54
55
|
|
|
55
56
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
56
|
-
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
57
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
@@ -192,7 +193,7 @@ export function renderRenderHelper(
|
|
|
192
193
|
|
|
193
194
|
return `import { render } from "@metaobjectsdev/render";
|
|
194
195
|
import type { Provider, EmailDocument } from "@metaobjectsdev/render";
|
|
195
|
-
import type { ${payloadRef} } from "
|
|
196
|
+
import type { ${payloadRef} } from "./${payloadRef}.js";
|
|
196
197
|
|
|
197
198
|
/**
|
|
198
199
|
* Render the ${templateName} email (subject + html body${typeof textBodyRef === "string" ? " + text body" : ""}) from a
|
|
@@ -230,7 +231,7 @@ export function ${fnName}(payload: ${payloadRef}, provider: Provider): EmailDocu
|
|
|
230
231
|
|
|
231
232
|
return `import { render } from "@metaobjectsdev/render";
|
|
232
233
|
import type { Provider } from "@metaobjectsdev/render";
|
|
233
|
-
import type { ${payloadRef} } from "
|
|
234
|
+
import type { ${payloadRef} } from "./${payloadRef}.js";
|
|
234
235
|
|
|
235
236
|
/**
|
|
236
237
|
* Render the ${templateName} document from a typed ${payloadRef} payload. Wraps the
|
|
@@ -15,17 +15,31 @@
|
|
|
15
15
|
// the existing queries-file template). The entity's Drizzle table const is
|
|
16
16
|
// imported alongside the Zod schemas + constants from the sibling Entity.ts.
|
|
17
17
|
|
|
18
|
-
import { code, imp } from "ts-poet";
|
|
18
|
+
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
19
19
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
20
|
+
import {
|
|
21
|
+
TYPE_FIELD,
|
|
22
|
+
resolveColumnName,
|
|
23
|
+
} from "@metaobjectsdev/metadata";
|
|
20
24
|
import { type RenderContext } from "../render-context.js";
|
|
21
|
-
import { entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
|
|
25
|
+
import { crossEntitySpecifier, entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
|
|
22
26
|
import { GENERATED_HEADER } from "../constants.js";
|
|
23
|
-
import { variableNameFromEntity } from "../naming.js";
|
|
27
|
+
import { variableNameFromEntity, routesHandlerName } from "../naming.js";
|
|
24
28
|
import { isProjection } from "../projection/projection-detector.js";
|
|
29
|
+
import type { RelationEntry } from "../relation-resolver.js";
|
|
30
|
+
import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
|
|
25
31
|
|
|
26
32
|
export function renderRoutesFile(entity: MetaObject, ctx: RenderContext): string {
|
|
33
|
+
// FR-017 Tier 2 — a TPH discriminator base mounts polymorphic list/get at the
|
|
34
|
+
// base path plus a full per-subtype CRUD route set scoped to each
|
|
35
|
+
// discriminator value. (Subtype entities are filtered out of the routes
|
|
36
|
+
// generator entirely — their routes live here.)
|
|
37
|
+
if (isTphDiscriminatorBase(entity, ctx.loadedRoot)) {
|
|
38
|
+
return renderTphRoutesFile(entity, ctx);
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
const entityName = entity.name;
|
|
28
|
-
const handlerName =
|
|
42
|
+
const handlerName = routesHandlerName(entityName);
|
|
29
43
|
// Import the entity's own file. Same target → relative "./Entity"; cross
|
|
30
44
|
// target → importBase-qualified package path.
|
|
31
45
|
const entityFileSpec = entityModuleSpecifier(
|
|
@@ -113,6 +127,19 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
113
127
|
const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
|
|
114
128
|
const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
115
129
|
|
|
130
|
+
// FR-018 M:N traversal: for each many-to-many navigation declared on this
|
|
131
|
+
// entity, emit a mountM2mRoute(...) that traverses the junction. The junction
|
|
132
|
+
// FK columns were derived from the junction's identity.reference children (the
|
|
133
|
+
// SSOT) by the relation-resolver pre-pass; here we resolve them to physical
|
|
134
|
+
// column names for the Drizzle two-stage join.
|
|
135
|
+
const m2mEntries = (ctx.relationMap.get(entityName) ?? []).filter(
|
|
136
|
+
(e): e is RelationEntry & { junctionEntity: string } => e.junctionEntity !== undefined,
|
|
137
|
+
);
|
|
138
|
+
// Two fastify-scope variants: under an apiPrefix the mounts live inside the
|
|
139
|
+
// register-block (`instance`); otherwise they bind directly to `fastify`.
|
|
140
|
+
const m2mMountsPrefixed = renderM2mMounts(m2mEntries, entity, ctx, "instance");
|
|
141
|
+
const m2mMountsFlat = renderM2mMounts(m2mEntries, entity, ctx, "fastify");
|
|
142
|
+
|
|
116
143
|
const literalImports = code`
|
|
117
144
|
import { db } from ${JSON.stringify(dbImportSpec)};
|
|
118
145
|
import {
|
|
@@ -148,7 +175,7 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
148
175
|
sortAllowlist: ${entityName}SortAllowlist,
|
|
149
176
|
dialect: ${JSON.stringify(ctx.dialect)},
|
|
150
177
|
});
|
|
151
|
-
}, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
|
|
178
|
+
${m2mMountsPrefixed} }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
|
|
152
179
|
}
|
|
153
180
|
`
|
|
154
181
|
: code`
|
|
@@ -172,8 +199,208 @@ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
|
172
199
|
sortAllowlist: ${entityName}SortAllowlist,
|
|
173
200
|
dialect: ${JSON.stringify(ctx.dialect)},
|
|
174
201
|
});
|
|
175
|
-
}
|
|
202
|
+
${m2mMountsFlat}}
|
|
176
203
|
`;
|
|
177
204
|
|
|
178
205
|
return header + literalImports.toString() + body.toString();
|
|
179
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Render the M:N traversal mounts for an entity as a single Code fragment to
|
|
210
|
+
* interpolate INTO the handler-body code template (so the junction/target table
|
|
211
|
+
* + mountM2mRoute imports hoist with the rest of the body's imports, not inline
|
|
212
|
+
* mid-function). `fastifyVar` is the in-scope Fastify reference (`instance`
|
|
213
|
+
* under an apiPrefix register-block, else `fastify`). Returns "" when the entity
|
|
214
|
+
* has no M:N relationships — CRUD-only output stays byte-identical to before.
|
|
215
|
+
*/
|
|
216
|
+
function renderM2mMounts(
|
|
217
|
+
entries: ReadonlyArray<RelationEntry & { junctionEntity: string }>,
|
|
218
|
+
source: MetaObject,
|
|
219
|
+
ctx: RenderContext,
|
|
220
|
+
fastifyVar: string,
|
|
221
|
+
): Code | string {
|
|
222
|
+
if (entries.length === 0) return "";
|
|
223
|
+
const mounts = entries.map((e) => renderM2mMount(e, source, ctx, fastifyVar));
|
|
224
|
+
return code`${joinCode(mounts, { on: "\n", trim: false })}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Render one M:N traversal mount. The junction + target Drizzle table consts are
|
|
230
|
+
* imported from their sibling entity files (imp() lets ts-poet track + emit the
|
|
231
|
+
* import). The source/target FK columns + the target PK are resolved to PHYSICAL
|
|
232
|
+
* column names via resolveColumnName (the runtime two-stage join queries by
|
|
233
|
+
* column). mountM2mRoute appends `/:id/<relationName>` to the source $path.
|
|
234
|
+
*/
|
|
235
|
+
function renderM2mMount(
|
|
236
|
+
entry: RelationEntry & { junctionEntity: string },
|
|
237
|
+
source: MetaObject,
|
|
238
|
+
ctx: RenderContext,
|
|
239
|
+
fastifyVar: string,
|
|
240
|
+
): Code {
|
|
241
|
+
const junctionVarSym = imp(
|
|
242
|
+
`${variableNameFromEntity(entry.junctionEntity)}@${crossEntitySpecifier(
|
|
243
|
+
ctx.outputLayout,
|
|
244
|
+
source.package,
|
|
245
|
+
ctx.packageOf.get(entry.junctionEntity),
|
|
246
|
+
entry.junctionEntity,
|
|
247
|
+
ctx.extStyle,
|
|
248
|
+
)}`,
|
|
249
|
+
);
|
|
250
|
+
const targetVarSym = imp(
|
|
251
|
+
`${variableNameFromEntity(entry.targetEntity)}@${crossEntitySpecifier(
|
|
252
|
+
ctx.outputLayout,
|
|
253
|
+
source.package,
|
|
254
|
+
ctx.packageOf.get(entry.targetEntity),
|
|
255
|
+
entry.targetEntity,
|
|
256
|
+
ctx.extStyle,
|
|
257
|
+
)}`,
|
|
258
|
+
);
|
|
259
|
+
const mountM2mRouteSym = imp("mountM2mRoute@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
260
|
+
const junction = ctx.loadedRoot.findObject(entry.junctionEntity);
|
|
261
|
+
const target = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
262
|
+
const sourceColumn = junction
|
|
263
|
+
? resolveJunctionColumn(junction, entry.sourceJoinField!, ctx)
|
|
264
|
+
: entry.sourceJoinField!;
|
|
265
|
+
const targetColumn = junction
|
|
266
|
+
? resolveJunctionColumn(junction, entry.targetJoinField!, ctx)
|
|
267
|
+
: entry.targetJoinField!;
|
|
268
|
+
const targetPkColumn = target
|
|
269
|
+
? resolveJunctionColumn(target, ctx.pkMap.get(entry.targetEntity)?.fieldName ?? "id", ctx)
|
|
270
|
+
: "id";
|
|
271
|
+
|
|
272
|
+
return code` ${mountM2mRouteSym}({
|
|
273
|
+
fastify: ${fastifyVar},
|
|
274
|
+
path: ${source.name}.$path,
|
|
275
|
+
relationName: ${JSON.stringify(entry.name)},
|
|
276
|
+
db,
|
|
277
|
+
junctionTable: ${junctionVarSym},
|
|
278
|
+
targetTable: ${targetVarSym},
|
|
279
|
+
sourceColumn: ${JSON.stringify(sourceColumn)},
|
|
280
|
+
targetColumn: ${JSON.stringify(targetColumn)},
|
|
281
|
+
targetPkColumn: ${JSON.stringify(targetPkColumn)},
|
|
282
|
+
symmetric: ${entry.symmetric ? "true" : "false"},
|
|
283
|
+
});`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Resolve a field's physical column name on an entity (defaults if missing). */
|
|
287
|
+
function resolveJunctionColumn(entity: MetaObject, fieldName: string, ctx: RenderContext): string {
|
|
288
|
+
const field = entity.ownChildren().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
|
|
289
|
+
if (!field) return fieldName;
|
|
290
|
+
return resolveColumnName(field, ctx.columnNamingStrategy);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* FR-017 Tier 2 — the routes file for a TPH discriminator base.
|
|
295
|
+
*
|
|
296
|
+
* Mounts a polymorphic list/get route set at the base path (`GET /auths`,
|
|
297
|
+
* `GET /auths/:id` — rows carry the discriminator by value), then a full
|
|
298
|
+
* per-subtype CRUD route set at `<basePath>/<discriminatorValue lowercased>`
|
|
299
|
+
* (`/auths/bridge`, ...). The per-subtype create body OMITS the discriminator
|
|
300
|
+
* (the URL names the subtype); the runtime helper injects it. The per-subtype
|
|
301
|
+
* route set is scoped to its discriminator value via the `discriminator` option
|
|
302
|
+
* (cross-subtype get/update/delete 404; update strips the discriminator).
|
|
303
|
+
*
|
|
304
|
+
* Subtype route segment defaults to the lowercased `@discriminatorValue`
|
|
305
|
+
* (`"Bridge"` → `bridge`) — a robust, value-derived path that matches the
|
|
306
|
+
* FR-017 design's `/auths/bridge` examples. Fastify resolves the static
|
|
307
|
+
* `/auths/bridge` ahead of the parametric `/auths/:id`, so the two coexist.
|
|
308
|
+
*/
|
|
309
|
+
function renderTphRoutesFile(base: MetaObject, ctx: RenderContext): string {
|
|
310
|
+
const baseName = base.name;
|
|
311
|
+
const handlerName = routesHandlerName(baseName);
|
|
312
|
+
// Single source of truth for the discriminator field + subtypes + route segments.
|
|
313
|
+
const plan = tphPlan(base, ctx.loadedRoot)!;
|
|
314
|
+
const discField = plan.discriminatorField;
|
|
315
|
+
const tableVar = variableNameFromEntity(baseName);
|
|
316
|
+
|
|
317
|
+
const baseFileSpec = entityModuleSpecifier(
|
|
318
|
+
ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
|
|
319
|
+
);
|
|
320
|
+
const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, base.package, ctx.dbImport);
|
|
321
|
+
|
|
322
|
+
const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
|
|
323
|
+
const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
324
|
+
const dbSym = imp(`db@${dbImportSpec}`);
|
|
325
|
+
const tableSym = imp(`${tableVar}@${baseFileSpec}`);
|
|
326
|
+
const baseConstSym = imp(`${baseName}@${baseFileSpec}`);
|
|
327
|
+
const baseInsertSym = imp(`${baseName}InsertSchema@${baseFileSpec}`);
|
|
328
|
+
const baseUpdateSym = imp(`${baseName}UpdateSchema@${baseFileSpec}`);
|
|
329
|
+
const baseFilterSym = imp(`${baseName}FilterAllowlist@${baseFileSpec}`);
|
|
330
|
+
const baseSortSym = imp(`${baseName}SortAllowlist@${baseFileSpec}`);
|
|
331
|
+
|
|
332
|
+
const fastifyRef = ctx.apiPrefix ? "instance" : "fastify";
|
|
333
|
+
const dialectLit = JSON.stringify(ctx.dialect);
|
|
334
|
+
|
|
335
|
+
const polymorphic = code`
|
|
336
|
+
${mountCrudRoutesSym}({
|
|
337
|
+
fastify: ${fastifyRef},
|
|
338
|
+
path: ${baseConstSym}.$path,
|
|
339
|
+
db: ${dbSym},
|
|
340
|
+
table: ${tableSym},
|
|
341
|
+
insertSchema: ${baseInsertSym},
|
|
342
|
+
updateSchema: ${baseUpdateSym},
|
|
343
|
+
filterAllowlist: ${baseFilterSym},
|
|
344
|
+
sortAllowlist: ${baseSortSym},
|
|
345
|
+
dialect: ${dialectLit},
|
|
346
|
+
expose: ["list", "get"],
|
|
347
|
+
});`;
|
|
348
|
+
|
|
349
|
+
const subtypeMounts: Code[] = plan.subtypes.map(({ entity: sub, value, routeSegment: segment }) => {
|
|
350
|
+
const subFileSpec = entityModuleSpecifier(
|
|
351
|
+
ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
|
|
352
|
+
);
|
|
353
|
+
const subInsertSym = imp(`${sub.name}InsertSchema@${subFileSpec}`);
|
|
354
|
+
// FR-017 Tier 3: each subtype carries its OWN filter/sort allowlist
|
|
355
|
+
// (discriminator excluded — it's pinned by this path).
|
|
356
|
+
const subFilterSym = imp(`${sub.name}FilterAllowlist@${subFileSpec}`);
|
|
357
|
+
const subSortSym = imp(`${sub.name}SortAllowlist@${subFileSpec}`);
|
|
358
|
+
return code`
|
|
359
|
+
${mountCrudRoutesSym}({
|
|
360
|
+
fastify: ${fastifyRef},
|
|
361
|
+
path: ${baseConstSym}.$path + ${JSON.stringify("/" + segment)},
|
|
362
|
+
db: ${dbSym},
|
|
363
|
+
table: ${tableSym},
|
|
364
|
+
insertSchema: ${subInsertSym}.omit({ ${discField}: true }),
|
|
365
|
+
updateSchema: ${subInsertSym}.omit({ ${discField}: true }).partial(),
|
|
366
|
+
filterAllowlist: ${subFilterSym},
|
|
367
|
+
sortAllowlist: ${subSortSym},
|
|
368
|
+
dialect: ${dialectLit},
|
|
369
|
+
discriminator: { column: ${JSON.stringify(discField)}, value: ${JSON.stringify(value)} },
|
|
370
|
+
});`;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const mounts = joinCode([polymorphic, ...subtypeMounts], { on: "\n" });
|
|
374
|
+
|
|
375
|
+
const fn = ctx.apiPrefix
|
|
376
|
+
? code`
|
|
377
|
+
/**
|
|
378
|
+
* Mount polymorphic + per-subtype REST endpoints for the ${baseName} TPH hierarchy.
|
|
379
|
+
*
|
|
380
|
+
* GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
|
|
381
|
+
* /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
|
|
382
|
+
*/
|
|
383
|
+
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
384
|
+
await fastify.register(async (instance) => {
|
|
385
|
+
${mounts}
|
|
386
|
+
}, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
|
|
387
|
+
}
|
|
388
|
+
`
|
|
389
|
+
: code`
|
|
390
|
+
/**
|
|
391
|
+
* Mount polymorphic + per-subtype REST endpoints for the ${baseName} TPH hierarchy.
|
|
392
|
+
*
|
|
393
|
+
* GET ${baseName}.$path (+ /:id) lists/gets the discriminated union; each
|
|
394
|
+
* /${baseName}.$path/<subtype> path is a full per-subtype CRUD set.
|
|
395
|
+
*/
|
|
396
|
+
export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
|
|
397
|
+
${mounts}
|
|
398
|
+
}
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
const header =
|
|
402
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
403
|
+
`// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
|
|
404
|
+
`// Customize via ${baseName}.extra.ts in this directory (e.g., auth, additional handlers).\n`;
|
|
405
|
+
return header + fn.toString();
|
|
406
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// FR-017 Tier 1 — TS discriminated-union + type guards + dispatcher emission.
|
|
2
|
+
//
|
|
3
|
+
// For an entity that carries `@discriminator`, this template emits:
|
|
4
|
+
// 1. `export type <Base> = <Sub1> | <Sub2> | ...` — discriminated union of
|
|
5
|
+
// every concrete subtype declaring @discriminatorValue against this base.
|
|
6
|
+
// 2. `export function is<Sub>(value: <Base>): value is <Sub>` — one type
|
|
7
|
+
// guard per subtype, checking the discriminator field's value.
|
|
8
|
+
// 3. `export function parse<Base>(row: unknown): <Base>` — runtime dispatcher
|
|
9
|
+
// that reads the discriminator off the raw row and parses with the
|
|
10
|
+
// matching subtype's Zod schema.
|
|
11
|
+
//
|
|
12
|
+
// When the entity does NOT carry @discriminator, returns null. When the entity
|
|
13
|
+
// carries @discriminator but has no concrete subtypes yet (refactor-in-progress
|
|
14
|
+
// shape — covered by FR-014 fixture `tph-discriminator-string-no-subtypes`),
|
|
15
|
+
// returns null too: there are no subtype names to union.
|
|
16
|
+
|
|
17
|
+
import { code, joinCode, imp, type Code } from "ts-poet";
|
|
18
|
+
import {
|
|
19
|
+
type MetaObject,
|
|
20
|
+
type MetaField,
|
|
21
|
+
type MetaRoot,
|
|
22
|
+
OBJECT_ATTR_DISCRIMINATOR,
|
|
23
|
+
OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
24
|
+
OBJECT_SUBTYPE_ENTITY,
|
|
25
|
+
} from "@metaobjectsdev/metadata";
|
|
26
|
+
|
|
27
|
+
interface SubtypeBinding {
|
|
28
|
+
subtype: MetaObject;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One concrete subtype in a {@link TphPlan}. */
|
|
33
|
+
export interface TphSubtypePlan {
|
|
34
|
+
/** The concrete subtype entity. */
|
|
35
|
+
entity: MetaObject;
|
|
36
|
+
/** Its `@discriminatorValue`. */
|
|
37
|
+
value: string;
|
|
38
|
+
/** The per-subtype REST route segment (e.g. `"bridge"`). The ONE place this
|
|
39
|
+
* rule is derived — see {@link tphRouteSegment}. */
|
|
40
|
+
routeSegment: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The single source of truth for a TPH base's polymorphic shape: the
|
|
45
|
+
* discriminator field name, the concrete subtypes (stable name-sorted order),
|
|
46
|
+
* each subtype's `@discriminatorValue`, and its per-subtype route segment.
|
|
47
|
+
*
|
|
48
|
+
* Every generator in the stack (entity, queries, routes, hooks, grid, forms)
|
|
49
|
+
* derives its TPH behavior from this one model rather than re-walking the root
|
|
50
|
+
* and re-deriving the segment / write-shape independently — so the route-segment
|
|
51
|
+
* rule and subtype set can never drift between, say, the generated routes and
|
|
52
|
+
* the generated hooks that call them.
|
|
53
|
+
*/
|
|
54
|
+
export interface TphPlan {
|
|
55
|
+
base: MetaObject;
|
|
56
|
+
discriminatorField: string;
|
|
57
|
+
subtypes: TphSubtypePlan[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Memoized per base instance — the plan is pure over the (immutable, fully
|
|
61
|
+
// resolved) post-load model, and a base belongs to exactly one root, so caching
|
|
62
|
+
// by the base node identity is safe and erases the repeated root walks.
|
|
63
|
+
const _tphPlanCache = new WeakMap<MetaObject, TphPlan | null>();
|
|
64
|
+
|
|
65
|
+
/** The per-subtype REST route segment for a discriminator value. The ONE place
|
|
66
|
+
* this rule lives: `routesFile` and the TanStack hooks both read it through the
|
|
67
|
+
* plan, so generated hooks can't call a URL the generated routes don't serve. */
|
|
68
|
+
export function tphRouteSegment(discriminatorValue: string): string {
|
|
69
|
+
return discriminatorValue.toLowerCase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The {@link TphPlan} for a discriminator base, or `null` when `base` is not a
|
|
73
|
+
* discriminator base (no `@discriminator`, or no concrete subtypes). */
|
|
74
|
+
export function tphPlan(base: MetaObject, root: MetaRoot): TphPlan | null {
|
|
75
|
+
const cached = _tphPlanCache.get(base);
|
|
76
|
+
if (cached !== undefined) return cached;
|
|
77
|
+
const discriminatorField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
78
|
+
let plan: TphPlan | null = null;
|
|
79
|
+
if (typeof discriminatorField === "string" && discriminatorField !== "") {
|
|
80
|
+
const bindings = collectConcreteSubtypes(base, root);
|
|
81
|
+
if (bindings.length > 0) {
|
|
82
|
+
plan = {
|
|
83
|
+
base,
|
|
84
|
+
discriminatorField,
|
|
85
|
+
subtypes: bindings.map((b) => ({
|
|
86
|
+
entity: b.subtype,
|
|
87
|
+
value: b.value,
|
|
88
|
+
routeSegment: tphRouteSegment(b.value),
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
_tphPlanCache.set(base, plan);
|
|
94
|
+
return plan;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when this entity is a TPH discriminator base — it carries
|
|
98
|
+
* `@discriminator` AND at least one concrete subtype declares
|
|
99
|
+
* `@discriminatorValue` extending it. This is the predicate the generator
|
|
100
|
+
* stack uses to switch into single-table-inheritance emission. */
|
|
101
|
+
export function isTphDiscriminatorBase(obj: MetaObject, root: MetaRoot): boolean {
|
|
102
|
+
return tphPlan(obj, root) !== null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The concrete subtypes bound to this discriminator base, in stable
|
|
106
|
+
* (name-sorted) order. Returns `[]` when `base` is not a discriminator base. */
|
|
107
|
+
export function tphConcreteSubtypes(base: MetaObject, root: MetaRoot): MetaObject[] {
|
|
108
|
+
return tphPlan(base, root)?.subtypes.map((s) => s.entity) ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The subtype-only fields that must be folded into the base's single TPH table.
|
|
113
|
+
* For each concrete subtype, every effective field NOT already on the base is
|
|
114
|
+
* collected (effective, so fields declared on abstract intermediate levels of a
|
|
115
|
+
* multi-level hierarchy are captured too). Deduplicated by field name across
|
|
116
|
+
* subtypes — two subtypes sharing a column name contribute one column. The
|
|
117
|
+
* caller emits each as a nullable column (rows of other subtypes store NULL).
|
|
118
|
+
*/
|
|
119
|
+
export function collectTphSubtypeFields(base: MetaObject, root: MetaRoot): MetaField[] {
|
|
120
|
+
const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
121
|
+
if (typeof discFieldName !== "string" || discFieldName === "") return [];
|
|
122
|
+
|
|
123
|
+
const baseFieldNames = new Set(base.fields().map((f) => f.name));
|
|
124
|
+
const seen = new Set<string>();
|
|
125
|
+
const out: MetaField[] = [];
|
|
126
|
+
for (const { subtype } of collectConcreteSubtypes(base, root)) {
|
|
127
|
+
for (const f of subtype.fields()) {
|
|
128
|
+
if (baseFieldNames.has(f.name)) continue; // base column — already emitted
|
|
129
|
+
if (seen.has(f.name)) continue; // shared subtype column — emit once
|
|
130
|
+
seen.add(f.name);
|
|
131
|
+
out.push(f);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Render the TPH union + guards + dispatcher block, or null when the entity
|
|
138
|
+
* is not a discriminator-bearing base with at least one concrete subtype. */
|
|
139
|
+
export function renderTphDiscriminatorUnion(
|
|
140
|
+
base: MetaObject,
|
|
141
|
+
root: MetaRoot,
|
|
142
|
+
): Code | null {
|
|
143
|
+
const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
144
|
+
if (typeof discFieldName !== "string" || discFieldName === "") return null;
|
|
145
|
+
|
|
146
|
+
const subtypes = collectConcreteSubtypes(base, root);
|
|
147
|
+
if (subtypes.length === 0) return null;
|
|
148
|
+
|
|
149
|
+
const baseName = base.name;
|
|
150
|
+
|
|
151
|
+
// 1. Union type alias. Subtype names are imported lazily via ts-poet `imp()`
|
|
152
|
+
// so they resolve cross-module without manual import wiring.
|
|
153
|
+
const unionMembers: Code[] = subtypes.map((b) => {
|
|
154
|
+
const sub = imp(`t:${b.subtype.name}@./${b.subtype.name}.js`);
|
|
155
|
+
return code`${sub}`;
|
|
156
|
+
});
|
|
157
|
+
const unionType = code`export type ${baseName} = ${joinCode(unionMembers, { on: " | " })};`;
|
|
158
|
+
|
|
159
|
+
// 2. Type guards.
|
|
160
|
+
const guards: Code[] = subtypes.map((b) => {
|
|
161
|
+
const sub = imp(`t:${b.subtype.name}@./${b.subtype.name}.js`);
|
|
162
|
+
return code`
|
|
163
|
+
/** True when value is a ${b.subtype.name} (discriminated by ${discFieldName} === "${b.value}"). */
|
|
164
|
+
export function is${b.subtype.name}(value: ${baseName}): value is ${sub} {
|
|
165
|
+
return value.${discFieldName} === "${b.value}";
|
|
166
|
+
}`;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 3. Dispatcher. The head-read uses z.object so the discriminator is read
|
|
170
|
+
// without committing the row to any subtype yet.
|
|
171
|
+
const z = imp("z@zod");
|
|
172
|
+
const enumLiterals = subtypes.map((b) => JSON.stringify(b.value)).join(", ");
|
|
173
|
+
|
|
174
|
+
const caseBranches: Code[] = subtypes.map((b) => {
|
|
175
|
+
const schema = imp(`${b.subtype.name}Schema@./${b.subtype.name}.js`);
|
|
176
|
+
return code` case ${JSON.stringify(b.value)}: return ${schema}.parse(row);`;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const dispatcher = code`
|
|
180
|
+
/**
|
|
181
|
+
* Parse a row from the ${baseName} table, dispatching by the
|
|
182
|
+
* \`${discFieldName}\` discriminator value to the matching subtype's
|
|
183
|
+
* Zod schema. Throws on unknown discriminator values.
|
|
184
|
+
*/
|
|
185
|
+
export function parse${baseName}(row: unknown): ${baseName} {
|
|
186
|
+
const head = ${z}.object({ ${discFieldName}: ${z}.enum([${enumLiterals}]) }).parse(row);
|
|
187
|
+
switch (head.${discFieldName}) {
|
|
188
|
+
${joinCode(caseBranches, { on: "\n" })}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
return code`
|
|
194
|
+
${unionType}
|
|
195
|
+
|
|
196
|
+
${joinCode(guards, { on: "\n" })}
|
|
197
|
+
|
|
198
|
+
${dispatcher}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Walk every top-level object.entity in the root and return the concrete
|
|
203
|
+
* subtypes whose @discriminatorValue is bound to this base via extends.
|
|
204
|
+
* Abstract intermediates are skipped (they don't have polymorphic instances). */
|
|
205
|
+
function collectConcreteSubtypes(base: MetaObject, root: MetaRoot): SubtypeBinding[] {
|
|
206
|
+
const bindings: SubtypeBinding[] = [];
|
|
207
|
+
for (const obj of root.objects()) {
|
|
208
|
+
if (obj.subType !== OBJECT_SUBTYPE_ENTITY) continue;
|
|
209
|
+
if (obj.isAbstract === true) continue;
|
|
210
|
+
if (obj === base) continue;
|
|
211
|
+
|
|
212
|
+
const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
|
|
213
|
+
if (typeof value !== "string" || value === "") continue;
|
|
214
|
+
|
|
215
|
+
// Walk this entity's extends chain looking for `base`.
|
|
216
|
+
let cursor = obj.superResolved;
|
|
217
|
+
let found = false;
|
|
218
|
+
while (cursor !== undefined) {
|
|
219
|
+
if (cursor === base) {
|
|
220
|
+
found = true;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
cursor = cursor.superResolved;
|
|
224
|
+
}
|
|
225
|
+
if (!found) continue;
|
|
226
|
+
|
|
227
|
+
bindings.push({ subtype: obj, value });
|
|
228
|
+
}
|
|
229
|
+
// Stable order by subtype name so emission is deterministic.
|
|
230
|
+
bindings.sort((a, b) => a.subtype.name.localeCompare(b.subtype.name));
|
|
231
|
+
return bindings;
|
|
232
|
+
}
|
|
@@ -10,16 +10,50 @@
|
|
|
10
10
|
|
|
11
11
|
import { joinCode, type Code } from "ts-poet";
|
|
12
12
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
13
|
+
import type { RenderContext } from "../render-context.js";
|
|
13
14
|
import { renderValueObjectInterface, renderEnumTypeAliases } from "./inferred-types.js";
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
renderInsertSchemaOnly,
|
|
17
|
+
isTphSubtype,
|
|
18
|
+
renderTphSubtypeReadSchema,
|
|
19
|
+
tphDiscriminatorPin,
|
|
20
|
+
} from "./zod-validators.js";
|
|
21
|
+
import { renderEntityConstants } from "./entity-constants.js";
|
|
22
|
+
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
23
|
+
import { renderFilterType } from "./filter-type.js";
|
|
15
24
|
import { GENERATED_HEADER } from "../constants.js";
|
|
16
25
|
|
|
17
|
-
export function renderValueObjectFile(obj: MetaObject): string {
|
|
18
|
-
const enumAliases = renderEnumTypeAliases(obj);
|
|
26
|
+
export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: RenderContext): string {
|
|
27
|
+
const enumAliases = renderEnumTypeAliases(obj, ctx);
|
|
28
|
+
// FR-017 Tier 2: a TPH subtype lands here (it inherits the base's source.rdb
|
|
29
|
+
// but declares none of its own). In addition to the insert schema it emits a
|
|
30
|
+
// full read schema `<Sub>Schema` so parse<Base>(row) can dispatch to it.
|
|
31
|
+
const tphSubtype = isTphSubtype(obj);
|
|
32
|
+
const tphReadSchema = tphSubtype ? renderTphSubtypeReadSchema(obj) : null;
|
|
33
|
+
// FR-017 Tier 3: a TPH subtype also emits its field-metadata constants object
|
|
34
|
+
// (the `<Sub>` const), so the React form generator can render per-field
|
|
35
|
+
// labels / rules / inputs the same way it does for ordinary entities.
|
|
36
|
+
const tphConstants = tphSubtype ? renderEntityConstants(obj, apiPrefix) : null;
|
|
37
|
+
// FR-017 Tier 3: per-subtype filter + sort allowlists, excluding the
|
|
38
|
+
// discriminator (it's pinned by the per-subtype route path, so a client must
|
|
39
|
+
// not filter on it). Included fields are the subtype's own + inherited base
|
|
40
|
+
// filterable fields. Drives the per-subtype REST routes' filter layer.
|
|
41
|
+
const discField = tphSubtype ? tphDiscriminatorPin(obj)?.fieldName : undefined;
|
|
42
|
+
const tphFilterAllowlist = tphSubtype ? renderFilterAllowlist(obj, discField) : null;
|
|
43
|
+
const tphSortAllowlist = tphSubtype ? renderSortAllowlist(obj, discField) : null;
|
|
44
|
+
// FR-017 Tier 3: the per-subtype CLIENT filter type, discriminator-excluded —
|
|
45
|
+
// kept in lockstep with the per-subtype allowlist above so a typed
|
|
46
|
+
// `<Sub>Filter` can't express a filter the server allowlist would 400.
|
|
47
|
+
const tphFilterType = tphSubtype ? renderFilterType(obj, discField) : null;
|
|
19
48
|
const sections: Code[] = [
|
|
20
|
-
renderValueObjectInterface(obj),
|
|
49
|
+
renderValueObjectInterface(obj, ctx),
|
|
21
50
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
51
|
+
...(tphReadSchema !== null ? [tphReadSchema] : []),
|
|
22
52
|
renderInsertSchemaOnly(obj),
|
|
53
|
+
...(tphConstants !== null ? [tphConstants] : []),
|
|
54
|
+
...(tphFilterAllowlist !== null ? [tphFilterAllowlist] : []),
|
|
55
|
+
...(tphSortAllowlist !== null ? [tphSortAllowlist] : []),
|
|
56
|
+
...(tphFilterType !== null ? [tphFilterType] : []),
|
|
23
57
|
];
|
|
24
58
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
25
59
|
const header =
|