@metaobjectsdev/codegen-ts 0.9.0 → 0.11.0-rc.1
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 +12 -6
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +68 -28
- 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 +439 -167
- 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/import-path.d.ts +18 -0
- package/dist/import-path.d.ts.map +1 -1
- package/dist/import-path.js +21 -0
- package/dist/import-path.js.map +1 -1
- 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 +101 -2
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +46 -0
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +39 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +52 -3
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +14 -6
- package/dist/payload-codegen.js.map +1 -1
- package/dist/pk-resolver.d.ts.map +1 -1
- package/dist/pk-resolver.js +4 -2
- package/dist/pk-resolver.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +52 -26
- 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 +25 -2
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +7 -0
- 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 +20 -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 +72 -23
- 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 +3 -3
- 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 +6 -8
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +58 -41
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +2 -6
- 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 +18 -10
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-shared.js +2 -2
- package/dist/templates/filter-shared.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 +10 -6
- 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 +15 -11
- 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 +121 -19
- 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-format-spec-emitter.js +1 -1
- package/dist/templates/output-format-spec-emitter.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +31 -80
- 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 +113 -5
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +7 -2
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +15 -15
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +12 -3
- 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 +5 -5
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +1 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +184 -7
- 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 +33 -5
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +65 -2
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +202 -22
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +103 -34
- package/src/column-mapper.ts +79 -32
- 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 +483 -189
- 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/import-path.ts +28 -0
- package/src/index.ts +55 -4
- package/src/metaobjects-config.ts +146 -2
- package/src/naming.ts +73 -3
- package/src/payload-codegen.ts +16 -5
- package/src/pk-resolver.ts +4 -2
- package/src/projection/extract-view-spec.ts +50 -31
- package/src/relation-resolver.ts +103 -1
- package/src/render-context.ts +32 -2
- package/src/render-engine/embedded-templates.generated.ts +14 -0
- package/src/render-engine/framework-provider.ts +25 -11
- package/src/runner.ts +24 -0
- package/src/templates/docs-file.ts +2 -9
- package/src/templates/drizzle-schema.ts +80 -28
- package/src/templates/entity-constants.ts +3 -3
- package/src/templates/entity-file.ts +16 -5
- package/src/templates/enums-file.ts +50 -0
- package/src/templates/extract-delegate-emitter.ts +6 -7
- package/src/templates/extractor.ts +70 -40
- package/src/templates/field-meta.ts +1 -7
- package/src/templates/filter-allowlist.ts +18 -11
- package/src/templates/filter-shared.ts +2 -2
- package/src/templates/filter-type.ts +9 -7
- package/src/templates/find-templates.ts +15 -0
- package/src/templates/fr010-field-mapping.ts +15 -13
- package/src/templates/inferred-types.ts +122 -21
- package/src/templates/mermaid-er.ts +176 -8
- package/src/templates/output-format-spec-emitter.ts +1 -1
- package/src/templates/output-parser.ts +31 -80
- package/src/templates/output-prompt.ts +2 -1
- package/src/templates/queries-file.ts +133 -4
- package/src/templates/queries.ts +21 -15
- package/src/templates/relations-block.ts +19 -3
- package/src/templates/render-helper.ts +5 -4
- package/src/templates/routes-file-hono.ts +1 -2
- package/src/templates/routes-file.ts +234 -7
- package/src/templates/tph-discriminator.ts +232 -0
- package/src/templates/value-object-file.ts +39 -5
- package/src/templates/zod-validators.ts +225 -21
- 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/dist/templates/extract-schema-emitter.d.ts +0 -8
- package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
- package/dist/templates/extract-schema-emitter.js +0 -81
- package/dist/templates/extract-schema-emitter.js.map +0 -1
- package/src/templates/extract-schema-emitter.ts +0 -111
|
@@ -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, ctx) : 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] : []),
|
|
22
|
-
|
|
51
|
+
...(tphReadSchema !== null ? [tphReadSchema] : []),
|
|
52
|
+
renderInsertSchemaOnly(obj, ctx),
|
|
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 =
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
|
|
11
11
|
|
|
12
12
|
import { code, joinCode, imp, type Code } from "ts-poet";
|
|
13
|
-
import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
13
|
+
import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
|
|
14
14
|
import {
|
|
15
15
|
FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
|
|
16
16
|
FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
|
|
@@ -24,18 +24,92 @@ import {
|
|
|
24
24
|
AUTO_SET_ON_CREATE, AUTO_SET_ON_UPDATE,
|
|
25
25
|
VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_PATTERN,
|
|
26
26
|
GENERATION_INCREMENT, GENERATION_UUID,
|
|
27
|
+
OBJECT_ATTR_DISCRIMINATOR, OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
27
28
|
} from "@metaobjectsdev/metadata";
|
|
28
29
|
import { enumValues, zodEnumExpr } from "../enum-meta.js";
|
|
29
30
|
import { renderDocsFor } from "./jsdoc.js";
|
|
31
|
+
import { sharedEnumForField } from "../enum-shared.js";
|
|
32
|
+
import { sharedEnumImportSpecifier } from "../enum-import.js";
|
|
33
|
+
import { sharedEnumZodConstName } from "./enums-file.js";
|
|
34
|
+
import type { RenderContext } from "../render-context.js";
|
|
35
|
+
import { valueObjectModuleSpecifier } from "../import-path.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FR-017 Tier 1 — when this object is a TPH subtype (@discriminatorValue set
|
|
39
|
+
* and an ancestor carries @discriminator), return the discriminator-field-name
|
|
40
|
+
* → pinned-literal-value pair. Subtypes emit `<field>: z.literal("<value>")`
|
|
41
|
+
* instead of the inherited field's normal type expression. Returns undefined
|
|
42
|
+
* when the object is not a TPH subtype.
|
|
43
|
+
*/
|
|
44
|
+
export function tphDiscriminatorPin(obj: MetaObject): { fieldName: string; value: string } | undefined {
|
|
45
|
+
const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
|
|
46
|
+
if (typeof value !== "string" || value === "") return undefined;
|
|
47
|
+
|
|
48
|
+
// Walk the extends chain to find the root carrying @discriminator.
|
|
49
|
+
let cursor = obj.superResolved;
|
|
50
|
+
while (cursor !== undefined) {
|
|
51
|
+
const fieldName = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
52
|
+
if (typeof fieldName === "string" && fieldName !== "") {
|
|
53
|
+
return { fieldName, value };
|
|
54
|
+
}
|
|
55
|
+
cursor = cursor.superResolved;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when this object is a TPH subtype — it declares @discriminatorValue
|
|
61
|
+
* and an ancestor carries @discriminator. */
|
|
62
|
+
export function isTphSubtype(obj: MetaObject): boolean {
|
|
63
|
+
return tphDiscriminatorPin(obj) !== undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* FR-017 Tier 2 — the per-subtype FULL read schema `<Sub>Schema`. Unlike the
|
|
68
|
+
* insert schema, this includes every effective field (PK included) so a raw DB
|
|
69
|
+
* row parses through it. The discriminator field is pinned to its literal value
|
|
70
|
+
* (`type: z.literal("Bridge")`) so the schema rejects a row of another subtype.
|
|
71
|
+
*
|
|
72
|
+
* This is the schema parse<Base>(row) dispatches to (see tph-discriminator.ts).
|
|
73
|
+
* Non-required columns are `.nullable()`-tolerant: a nullable TPH column read
|
|
74
|
+
* back from the DB arrives as `null`, not `undefined`, so the read schema must
|
|
75
|
+
* accept null (the insert schema, by contrast, makes them `.optional()`).
|
|
76
|
+
*/
|
|
77
|
+
export function renderTphSubtypeReadSchema(obj: MetaObject, ctx?: RenderContext): Code {
|
|
78
|
+
const z = imp("z@zod");
|
|
79
|
+
const tphPin = tphDiscriminatorPin(obj);
|
|
80
|
+
|
|
81
|
+
const fieldLines: Code[] = [];
|
|
82
|
+
for (const child of obj.fields()) {
|
|
83
|
+
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
84
|
+
fieldLines.push(code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const expr = zodFieldExpr(child, obj, ctx);
|
|
88
|
+
// zodFieldExpr already appends `.optional()` for non-required fields; add
|
|
89
|
+
// `.nullable()` on top so a NULL column value (the TPH default for any
|
|
90
|
+
// subtype-only column) parses cleanly.
|
|
91
|
+
fieldLines.push(
|
|
92
|
+
fieldWillBeOptional(child) ? code` ${child.name}: ${expr}.nullable()` : code` ${child.name}: ${expr}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const docs = renderDocsFor(obj);
|
|
97
|
+
const docsPrefix = docs ? `${docs}\n` : "";
|
|
98
|
+
return code`
|
|
99
|
+
${docsPrefix}export const ${obj.name}Schema = ${z}.object({
|
|
100
|
+
${joinCode(fieldLines, { on: ",\n" })}
|
|
101
|
+
});
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
30
104
|
|
|
31
105
|
/** Auto-generated PK field names that should be omitted from InsertSchema. */
|
|
32
106
|
function autoGenPkFieldNames(obj: MetaObject): Set<string> {
|
|
33
107
|
const out = new Set<string>();
|
|
34
108
|
const primary = obj.primaryIdentity();
|
|
35
109
|
if (primary) {
|
|
36
|
-
const generation = primary.
|
|
110
|
+
const generation = primary.attr(IDENTITY_ATTR_GENERATION);
|
|
37
111
|
if (generation === GENERATION_INCREMENT || generation === GENERATION_UUID) {
|
|
38
|
-
const fields = primary.
|
|
112
|
+
const fields = primary.attr(IDENTITY_ATTR_FIELDS);
|
|
39
113
|
const fieldsList = Array.isArray(fields) ? fields : (typeof fields === "string" ? [fields] : []);
|
|
40
114
|
for (const f of fieldsList) out.add(String(f));
|
|
41
115
|
}
|
|
@@ -52,9 +126,10 @@ function autoGenPkFieldNames(obj: MetaObject): Set<string> {
|
|
|
52
126
|
* so consumer imports don't churn. A future polish PR could add a `<Name>Schema`
|
|
53
127
|
* alias for clarity.
|
|
54
128
|
*/
|
|
55
|
-
export function renderInsertSchemaOnly(obj: MetaObject): Code {
|
|
129
|
+
export function renderInsertSchemaOnly(obj: MetaObject, ctx?: RenderContext): Code {
|
|
56
130
|
const z = imp("z@zod");
|
|
57
131
|
const autoGenPkFields = autoGenPkFieldNames(obj);
|
|
132
|
+
const tphPin = tphDiscriminatorPin(obj);
|
|
58
133
|
|
|
59
134
|
const insertFieldLines: Code[] = [];
|
|
60
135
|
for (const child of obj.fields()) {
|
|
@@ -62,16 +137,24 @@ export function renderInsertSchemaOnly(obj: MetaObject): Code {
|
|
|
62
137
|
// FR-013: @readOnly fields are populated by DB / replication / external
|
|
63
138
|
// owner; the application has no path to write them. Exclude from the
|
|
64
139
|
// create-shape schema entirely.
|
|
65
|
-
if (child.
|
|
140
|
+
if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
|
|
66
141
|
|
|
67
|
-
|
|
142
|
+
// FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
|
|
143
|
+
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
144
|
+
insertFieldLines.push(
|
|
145
|
+
code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
|
|
146
|
+
);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
|
|
68
151
|
|
|
69
152
|
if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
|
|
70
153
|
insertFieldLines.push(
|
|
71
154
|
code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
|
|
72
155
|
);
|
|
73
156
|
} else {
|
|
74
|
-
insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child)}`);
|
|
157
|
+
insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
|
|
75
158
|
}
|
|
76
159
|
}
|
|
77
160
|
|
|
@@ -86,9 +169,90 @@ ${joinCode(insertFieldLines, { on: ",\n" })}
|
|
|
86
169
|
`;
|
|
87
170
|
}
|
|
88
171
|
|
|
89
|
-
|
|
172
|
+
/** One documented field in an Insert/Update schema's accepted shape. */
|
|
173
|
+
export interface SchemaFieldShape {
|
|
174
|
+
/** The field name (the schema property key). */
|
|
175
|
+
name: string;
|
|
176
|
+
/** Whether the property is optional in the schema (`.optional()` / omitted-OK). */
|
|
177
|
+
optional: boolean;
|
|
178
|
+
/** For the @discriminator field on a TPH subtype's InsertSchema: the pinned
|
|
179
|
+
* literal value (`z.literal("Bridge")`). Undefined otherwise. */
|
|
180
|
+
pinnedLiteral?: string;
|
|
181
|
+
/** True for @autoSet timestamp fields the schema fills server-side
|
|
182
|
+
* (`z.string().optional().transform(...)`). */
|
|
183
|
+
autoSet?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The field SET (name + optionality) the `<Name>InsertSchema` accepts — derived
|
|
188
|
+
* by the SAME iteration + skip rules `renderInsertSchemaOnly` /
|
|
189
|
+
* `renderZodValidators` use to EMIT that schema, so a documented create-payload
|
|
190
|
+
* shape can never drift from the real schema:
|
|
191
|
+
* • auto-generated PK fields are omitted (caller doesn't provide them);
|
|
192
|
+
* • @readOnly fields are omitted (DB / replication owns the write path);
|
|
193
|
+
* • a TPH subtype's @discriminator field is a pinned `z.literal(value)`;
|
|
194
|
+
* • @autoSet fields are present but optional (server fills them);
|
|
195
|
+
* • every other field's optionality is `fieldWillBeOptional` (not required, or
|
|
196
|
+
* carries a @default).
|
|
197
|
+
*/
|
|
198
|
+
export function insertSchemaFields(obj: MetaObject): SchemaFieldShape[] {
|
|
199
|
+
const autoGenPkFields = autoGenPkFieldNames(obj);
|
|
200
|
+
const tphPin = tphDiscriminatorPin(obj);
|
|
201
|
+
const out: SchemaFieldShape[] = [];
|
|
202
|
+
for (const child of obj.fields()) {
|
|
203
|
+
if (autoGenPkFields.has(child.name)) continue;
|
|
204
|
+
if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
|
|
205
|
+
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
206
|
+
out.push({ name: child.name, optional: false, pinnedLiteral: tphPin.value });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
|
|
210
|
+
if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
|
|
211
|
+
out.push({ name: child.name, optional: true, autoSet: true });
|
|
212
|
+
} else {
|
|
213
|
+
out.push({ name: child.name, optional: fieldWillBeOptional(child) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The field SET the `<Name>UpdateSchema` accepts — same iteration + skip rules
|
|
221
|
+
* as `insertSchemaFields`, but mirroring the UpdateSchema branch of
|
|
222
|
+
* `renderZodValidators`:
|
|
223
|
+
* • a TPH subtype's @discriminator field is OMITTED (clients can't change subtype);
|
|
224
|
+
* • @autoSet onCreate fields are OMITTED (creation timestamps are immutable);
|
|
225
|
+
* • @autoSet onUpdate fields are present + optional (server fills them);
|
|
226
|
+
* • every other field is optional (PATCH semantics).
|
|
227
|
+
*/
|
|
228
|
+
export function updateSchemaFields(obj: MetaObject): SchemaFieldShape[] {
|
|
229
|
+
const autoGenPkFields = autoGenPkFieldNames(obj);
|
|
230
|
+
const tphPin = tphDiscriminatorPin(obj);
|
|
231
|
+
const out: SchemaFieldShape[] = [];
|
|
232
|
+
for (const child of obj.fields()) {
|
|
233
|
+
if (autoGenPkFields.has(child.name)) continue;
|
|
234
|
+
if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
|
|
235
|
+
// TPH subtype discriminator: omitted from the update schema entirely.
|
|
236
|
+
if (tphPin !== undefined && child.name === tphPin.fieldName) continue;
|
|
237
|
+
const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
|
|
238
|
+
if (autoSet === AUTO_SET_ON_CREATE) {
|
|
239
|
+
// Omitted: creation timestamps cannot change after creation.
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (autoSet === AUTO_SET_ON_UPDATE) {
|
|
243
|
+
out.push({ name: child.name, optional: true, autoSet: true });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// All non-autoSet fields are optional in the update schema (PATCH semantics).
|
|
247
|
+
out.push({ name: child.name, optional: true });
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code {
|
|
90
253
|
const z = imp("z@zod");
|
|
91
254
|
const autoGenPkFields = autoGenPkFieldNames(obj);
|
|
255
|
+
const tphPin = tphDiscriminatorPin(obj);
|
|
92
256
|
|
|
93
257
|
const insertFieldLines: Code[] = [];
|
|
94
258
|
const updateFieldLines: Code[] = [];
|
|
@@ -98,9 +262,20 @@ export function renderZodValidators(obj: MetaObject): Code {
|
|
|
98
262
|
// The DB / trigger / replication owns the write path; the app must not
|
|
99
263
|
// pass these values in POST/PATCH bodies (routesFile enforces the same
|
|
100
264
|
// contract at the boundary with a 400 response).
|
|
101
|
-
if (child.
|
|
265
|
+
if (child.attr(FIELD_ATTR_READ_ONLY) === true) continue;
|
|
266
|
+
|
|
267
|
+
// FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
|
|
268
|
+
// The discriminator is implicit on subtype rows (controlled by URL / insert
|
|
269
|
+
// path) — the app never writes it via the body and never updates it.
|
|
270
|
+
// Insert: pinned literal. Update: omitted entirely (clients can't change subtype).
|
|
271
|
+
if (tphPin !== undefined && child.name === tphPin.fieldName) {
|
|
272
|
+
insertFieldLines.push(
|
|
273
|
+
code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
|
|
274
|
+
);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
102
277
|
|
|
103
|
-
const autoSet = child.
|
|
278
|
+
const autoSet = child.attr(FIELD_ATTR_AUTO_SET);
|
|
104
279
|
|
|
105
280
|
// Insert schema: @autoSet fields use transform (always override client input).
|
|
106
281
|
if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
|
|
@@ -108,7 +283,7 @@ export function renderZodValidators(obj: MetaObject): Code {
|
|
|
108
283
|
code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
|
|
109
284
|
);
|
|
110
285
|
} else {
|
|
111
|
-
insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child)}`);
|
|
286
|
+
insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
|
|
112
287
|
}
|
|
113
288
|
|
|
114
289
|
// Update schema: @autoSet onCreate → omit entirely; onUpdate → transform
|
|
@@ -122,7 +297,7 @@ export function renderZodValidators(obj: MetaObject): Code {
|
|
|
122
297
|
// All non-autoSet fields are optional in the update schema (PATCH semantics).
|
|
123
298
|
// zodFieldExpr already appends .optional() when the field is non-required
|
|
124
299
|
// OR has a default; only append once more when it didn't.
|
|
125
|
-
const baseExpr = zodFieldExpr(child);
|
|
300
|
+
const baseExpr = zodFieldExpr(child, obj, ctx);
|
|
126
301
|
updateFieldLines.push(
|
|
127
302
|
fieldWillBeOptional(child) ? code` ${child.name}: ${baseExpr}` : code` ${child.name}: ${baseExpr}.optional()`,
|
|
128
303
|
);
|
|
@@ -146,15 +321,24 @@ ${joinCode(updateFieldLines, { on: ",\n" })}
|
|
|
146
321
|
`;
|
|
147
322
|
}
|
|
148
323
|
|
|
149
|
-
function zodFieldExpr(field: MetaField): Code {
|
|
324
|
+
function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext): Code {
|
|
150
325
|
// FIELD_SUBTYPE_OBJECT: emit z.array(<Ref>InsertSchema) / <Ref>InsertSchema
|
|
151
326
|
// via an imp() so ts-poet hoists the cross-module import. Without this the
|
|
152
327
|
// field used to collapse to z.string() / z.array(z.string()) and downstream
|
|
153
328
|
// JSON Schema (e.g. LLM tool_use input_schema) lost the nested object shape.
|
|
154
329
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
155
|
-
const ref = field.
|
|
330
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
156
331
|
if (typeof ref === "string" && ref.length > 0) {
|
|
157
|
-
|
|
332
|
+
// @objectRef may be authored fully-qualified or bare — the referenced
|
|
333
|
+
// <Ref>InsertSchema is named by the BARE short name. The import MODULE is
|
|
334
|
+
// resolved via the shared layout/package/extStyle-aware helper (the SAME
|
|
335
|
+
// one the field's TS type + Drizzle .$type<> use) so all three agree.
|
|
336
|
+
// Without owner/ctx (bare unit-test calls) fall back to the flat same-dir.
|
|
337
|
+
const refBase = stripPackage(ref);
|
|
338
|
+
const moduleSpec = (ctx && owner)
|
|
339
|
+
? valueObjectModuleSpecifier(refBase, ctx.packageOf, owner.package, ctx.outputLayout, ctx.extStyle)
|
|
340
|
+
: `./${refBase}.js`;
|
|
341
|
+
const refImp = imp(`${refBase}InsertSchema@${moduleSpec}`);
|
|
158
342
|
let base: Code = code`${refImp}`;
|
|
159
343
|
if (field.isArray) base = code`z.array(${base})`;
|
|
160
344
|
return appendValidatorChain(base, field);
|
|
@@ -187,7 +371,27 @@ function zodFieldExpr(field: MetaField): Code {
|
|
|
187
371
|
break;
|
|
188
372
|
case FIELD_SUBTYPE_ENUM: {
|
|
189
373
|
const values = enumValues(field);
|
|
190
|
-
|
|
374
|
+
if (values === undefined) {
|
|
375
|
+
baseStr = "z.string()";
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
// FR-019: a field extending a MATERIALIZED root-level abstract enum uses the
|
|
379
|
+
// shared `<E>Enum` Zod const (imported from ./enums) instead of inlining
|
|
380
|
+
// z.enum([...]). A @provided enum keeps inline z.enum([...]) — validation
|
|
381
|
+
// stays metaobjects-owned (the @values SSOT); only the TS type is external.
|
|
382
|
+
// Inline enums (and bare-ctx unit-test calls) keep inlining as before.
|
|
383
|
+
if (ctx !== undefined) {
|
|
384
|
+
const shared = sharedEnumForField(field);
|
|
385
|
+
if (shared !== undefined && !shared.provided) {
|
|
386
|
+
const constName = sharedEnumZodConstName(shared.name);
|
|
387
|
+
const spec = sharedEnumImportSpecifier(ctx, owner?.package);
|
|
388
|
+
const sharedConst = imp(`${constName}@${spec}`);
|
|
389
|
+
let base: Code = code`${sharedConst}`;
|
|
390
|
+
if (field.isArray) base = code`z.array(${base})`;
|
|
391
|
+
return appendValidatorChain(base, field);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
baseStr = zodEnumExpr(values);
|
|
191
395
|
break;
|
|
192
396
|
}
|
|
193
397
|
case FIELD_SUBTYPE_STRING:
|
|
@@ -204,11 +408,11 @@ function zodFieldExpr(field: MetaField): Code {
|
|
|
204
408
|
/** Mirrors the optional-or-not decision inside appendValidatorChain so the update-schema
|
|
205
409
|
* caller can avoid stacking a second `.optional()` onto an already-optional expression. */
|
|
206
410
|
function fieldWillBeOptional(field: MetaField): boolean {
|
|
207
|
-
let isRequired = field.
|
|
411
|
+
let isRequired = field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
208
412
|
for (const child of field.validators()) {
|
|
209
413
|
if (child.subType === VALIDATOR_SUBTYPE_REQUIRED) isRequired = true;
|
|
210
414
|
}
|
|
211
|
-
const hasDefault = field.
|
|
415
|
+
const hasDefault = field.attr(FIELD_ATTR_DEFAULT) !== undefined;
|
|
212
416
|
return !isRequired || hasDefault;
|
|
213
417
|
}
|
|
214
418
|
|
|
@@ -226,8 +430,8 @@ const NUMERIC_FIELD_SUBTYPES = new Set<string>([
|
|
|
226
430
|
* - array (any element) → .min/.max = element count (validator.array)
|
|
227
431
|
*/
|
|
228
432
|
function appendValidatorChain(base: Code, field: MetaField): Code {
|
|
229
|
-
let isRequired = field.
|
|
230
|
-
let maxLen: number | undefined = field.
|
|
433
|
+
let isRequired = field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
434
|
+
let maxLen: number | undefined = field.attr(FIELD_ATTR_MAX_LENGTH) as number | undefined;
|
|
231
435
|
let minLen: number | undefined;
|
|
232
436
|
let pattern: string | undefined;
|
|
233
437
|
let numMin: number | undefined;
|
|
@@ -278,7 +482,7 @@ function appendValidatorChain(base: Code, field: MetaField): Code {
|
|
|
278
482
|
// Fields with DB-level defaults are optional in the InsertSchema: the caller
|
|
279
483
|
// can omit them and the DB will fill in. Otherwise required-with-default
|
|
280
484
|
// would force callers to repeat the default at every call site.
|
|
281
|
-
const hasDefault = field.
|
|
485
|
+
const hasDefault = field.attr(FIELD_ATTR_DEFAULT) !== undefined;
|
|
282
486
|
if (!isRequired || hasDefault) chain = code`${chain}.optional()`;
|
|
283
487
|
return chain;
|
|
284
488
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{{generatedMarker}}}
|
|
2
|
+
|
|
3
|
+
# {{title}}
|
|
4
|
+
|
|
5
|
+
Generated API reference for {{project}}; call these exactly as written. {{importNote}}
|
|
6
|
+
{{#hasSetup}}
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
{{#setup}}
|
|
10
|
+
- `{{handle}}` — {{{note}}} `{{{snippetInline}}}`
|
|
11
|
+
{{/setup}}
|
|
12
|
+
{{/hasSetup}}
|
|
13
|
+
{{#units}}
|
|
14
|
+
|
|
15
|
+
## {{node}}
|
|
16
|
+
{{#groups}}
|
|
17
|
+
|
|
18
|
+
`{{importHeader}}`
|
|
19
|
+
{{#symbols}}
|
|
20
|
+
- `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}
|
|
21
|
+
{{/symbols}}
|
|
22
|
+
{{/groups}}
|
|
23
|
+
{{#example}}
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```ts
|
|
27
|
+
{{{example}}}
|
|
28
|
+
```
|
|
29
|
+
{{/example}}
|
|
30
|
+
{{/units}}
|