@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
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
34
34
|
TEMPLATE_ATTR_FORMAT,
|
|
35
35
|
PACKAGE_SEPARATOR,
|
|
36
|
+
refMatchesObject,
|
|
36
37
|
} from "@metaobjectsdev/metadata";
|
|
37
38
|
import { fields, isArray } from "./fr010-field-mapping.js";
|
|
38
39
|
import { mirrorName } from "./extract-delegate-emitter.js";
|
|
@@ -40,7 +41,7 @@ import { enumUnionAliasName } from "./inferred-types.js";
|
|
|
40
41
|
import { enumValues } from "../enum-meta.js";
|
|
41
42
|
|
|
42
43
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
43
|
-
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
44
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
@@ -64,9 +65,9 @@ function isObjectField(field: MetaData): boolean {
|
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* The union-alias type name for a `field.enum` with effective `@values`, or undefined when the
|
|
67
|
-
* field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the
|
|
68
|
-
* emitter
|
|
69
|
-
*
|
|
68
|
+
* field is not a value-constrained enum. Reuses `enumUnionAliasName` — the SAME naming the entity
|
|
69
|
+
* inferred-types emitter types the field as — so the cast target resolves to the exact alias
|
|
70
|
+
* exported from the owning VO's entity module. `ownerName` is the owning value-object's interface name.
|
|
70
71
|
*/
|
|
71
72
|
function enumAlias(field: MetaData, ownerName: string): string | undefined {
|
|
72
73
|
if (field.subType !== FIELD_SUBTYPE_ENUM) return undefined;
|
|
@@ -76,11 +77,12 @@ function enumAlias(field: MetaData, ownerName: string): string | undefined {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* True iff the field is required IN THE STRICT PAYLOAD TYPE.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* (`m.f!` vs `m.f ??
|
|
83
|
-
*
|
|
80
|
+
* True iff the field is required IN THE STRICT PAYLOAD TYPE. The strict payload IS the VO's own
|
|
81
|
+
* generated entity-module interface (`renderValueObjectInterface`), which types a required field
|
|
82
|
+
* `f: T` and an optional one `f?: T` (i.e. `T | undefined` — NOT `T | null`). So the mapper's
|
|
83
|
+
* optionality assumption (`m.f!` vs `m.f ?? undefined`) has to agree with THAT interface, and an
|
|
84
|
+
* absent optional maps to `undefined`, never `null`. This predicate matches the interface's
|
|
85
|
+
* required test (boolean `true` only) so the two never skew.
|
|
84
86
|
*/
|
|
85
87
|
function isFieldRequired(field: MetaData): boolean {
|
|
86
88
|
return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
|
|
@@ -93,8 +95,10 @@ function mapperName(vo: MetaData): string {
|
|
|
93
95
|
|
|
94
96
|
/**
|
|
95
97
|
* The mapper-body initializer expression for one field, reading mirror member `m.<name>` and
|
|
96
|
-
* mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ??
|
|
97
|
-
*
|
|
98
|
+
* mapping it onto the strict payload's exact optionality (required → `m.f!`; optional → `m.f ?? undefined`).
|
|
99
|
+
* The strict payload is the VO's generated entity-module interface, whose optional fields are
|
|
100
|
+
* `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`.
|
|
101
|
+
* Nested single/array objects recurse into their toStrict<Type> mapper, guarding when optional.
|
|
98
102
|
*/
|
|
99
103
|
function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
100
104
|
const name = field.name;
|
|
@@ -104,25 +108,25 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
|
104
108
|
const target = refVo(field, root);
|
|
105
109
|
if (target === undefined) {
|
|
106
110
|
// Unresolved @objectRef — the payload type would be `unknown`; pass through as-is.
|
|
107
|
-
return required ? `m.${name}!` : `m.${name} ??
|
|
111
|
+
return required ? `m.${name}!` : `m.${name} ?? undefined`;
|
|
108
112
|
}
|
|
109
113
|
const fn = mapperName(target);
|
|
110
114
|
if (isArray(field)) {
|
|
111
115
|
// Required array-of-objects: each element mapped; element nulls dropped at the type level
|
|
112
116
|
// via the non-null assertion (extract never yields null elements for a present array).
|
|
113
117
|
if (required) return `m.${name}!.map((e) => ${fn}(e!))`;
|
|
114
|
-
return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) :
|
|
118
|
+
return `m.${name} ? m.${name}!.map((e) => ${fn}(e!)) : undefined`;
|
|
115
119
|
}
|
|
116
120
|
// Single nested object.
|
|
117
121
|
if (required) return `${fn}(m.${name}!)`;
|
|
118
|
-
return `m.${name} ? ${fn}(m.${name}) :
|
|
122
|
+
return `m.${name} ? ${fn}(m.${name}) : undefined`;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
// Scalar ARRAY (e.g. `field.string` with isArray): the mirror types it `(T | null)[] | null`
|
|
122
|
-
// but the strict payload types it `T[]` (required) / `T[]
|
|
126
|
+
// but the strict payload types it `T[]` (required) / `T[]?` (optional). A bare `m.f!`
|
|
123
127
|
// would leave the element type `T | null`, a `tsc --strict` TS2322 error. Filter out null
|
|
124
128
|
// elements so the element type narrows to non-null (consistent with the lost-element DROP policy
|
|
125
|
-
// already used for required arrays-of-objects above).
|
|
129
|
+
// already used for required arrays-of-objects above). An absent optional array maps to `undefined`.
|
|
126
130
|
//
|
|
127
131
|
// ENUM arrays: the mirror element is a plain `string`, but the strict payload types it as the
|
|
128
132
|
// closed `<Alias>[]` union. The null-filter alone narrows to `string[]`, not `<Alias>[]` — a
|
|
@@ -136,23 +140,23 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
|
136
140
|
return alias !== undefined ? `(${filtered}) as ${alias}[]` : filtered;
|
|
137
141
|
}
|
|
138
142
|
const filtered = `m.${name}.filter((x): x is NonNullable<typeof x> => x != null)`;
|
|
139
|
-
const guarded = `m.${name} == null ?
|
|
143
|
+
const guarded = `m.${name} == null ? undefined : ${filtered}`;
|
|
140
144
|
return alias !== undefined
|
|
141
|
-
? `m.${name} == null ?
|
|
145
|
+
? `m.${name} == null ? undefined : (${filtered}) as ${alias}[]`
|
|
142
146
|
: guarded;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
// Scalar / enum (single): the strict payload's optionality decides the shape.
|
|
146
|
-
// Required → non-null assertion; optional → `??
|
|
150
|
+
// Required → non-null assertion; optional → `?? undefined` (matches the entity-module `f?: T`).
|
|
147
151
|
//
|
|
148
152
|
// ENUM scalar: the mirror member is a plain `string`, but the strict payload types it as the
|
|
149
153
|
// closed `<Alias>` union — assigning `string` into `<Alias>` is a `tsc --strict` TS2322 error.
|
|
150
154
|
// So the value is CAST to `<Alias>`. Sound for the same reason as enum arrays above: the engine
|
|
151
155
|
// already validated membership (or extract throws on a lost required field).
|
|
152
156
|
if (alias !== undefined) {
|
|
153
|
-
return required ? `m.${name}! as ${alias}` : `(m.${name} ??
|
|
157
|
+
return required ? `m.${name}! as ${alias}` : `(m.${name} ?? undefined) as ${alias} | undefined`;
|
|
154
158
|
}
|
|
155
|
-
return required ? `m.${name}!` : `m.${name} ??
|
|
159
|
+
return required ? `m.${name}!` : `m.${name} ?? undefined`;
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
/**
|
|
@@ -202,24 +206,44 @@ function emitMapper(
|
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* (the alias is hoisted above the interface there), so the extractor's `as <Alias>` casts need
|
|
208
|
-
* the alias names imported alongside the interface names. Deduped, in discovery order.
|
|
209
|
+
* One payload-type import group: the strict types (VO interface + its own enum
|
|
210
|
+
* union-aliases) imported from a single VO entity module.
|
|
209
211
|
*/
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
interface PayloadImportGroup {
|
|
213
|
+
/** The VO whose entity module exports these types (`./<module>.js`). */
|
|
214
|
+
module: string;
|
|
215
|
+
/** The strict type names exported by that module, in discovery order. */
|
|
216
|
+
types: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Group the strict payload types reachable from `vo` BY THE ENTITY MODULE THAT EXPORTS THEM.
|
|
221
|
+
*
|
|
222
|
+
* Each value-object gets its own generated entity module (`entityFile()` emits `<VO>.ts` exporting
|
|
223
|
+
* `export interface <VO>`), and `renderEnumTypeAliases` hoists every `field.enum` union-alias INTO
|
|
224
|
+
* the OWNING VO's module (`export type <Owner><Field> = ...` co-located with the interface). So a
|
|
225
|
+
* VO's interface AND the aliases for its own enum fields are imported from `./<VO>.js` — NOT from a
|
|
226
|
+
* single `payloads.ts` (which no generator emits). Deduped, in discovery order, one group per VO.
|
|
227
|
+
*/
|
|
228
|
+
function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGroup[] {
|
|
229
|
+
const groups: PayloadImportGroup[] = [];
|
|
230
|
+
const seenVo = new Set<string>();
|
|
231
|
+
const seenAlias = new Set<string>();
|
|
213
232
|
const visit = (cur: MetaData) => {
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
if (seenVo.has(cur.name)) return;
|
|
234
|
+
seenVo.add(cur.name);
|
|
235
|
+
// The VO interface + its OWN enum aliases share the VO's entity module.
|
|
236
|
+
const types: string[] = [cur.name];
|
|
217
237
|
for (const f of fields(cur)) {
|
|
218
238
|
const alias = enumAlias(f, cur.name);
|
|
219
|
-
if (alias !== undefined && !
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
if (alias !== undefined && !seenAlias.has(alias)) {
|
|
240
|
+
seenAlias.add(alias);
|
|
241
|
+
types.push(alias);
|
|
222
242
|
}
|
|
243
|
+
}
|
|
244
|
+
groups.push({ module: cur.name, types });
|
|
245
|
+
// Recurse into nested object refs (their interfaces live in their own modules).
|
|
246
|
+
for (const f of fields(cur)) {
|
|
223
247
|
if (isObjectField(f)) {
|
|
224
248
|
const target = refVo(f, root);
|
|
225
249
|
if (target !== undefined) visit(target);
|
|
@@ -227,7 +251,7 @@ function reachablePayloadTypes(vo: MetaData, root: MetaData): string[] {
|
|
|
227
251
|
}
|
|
228
252
|
};
|
|
229
253
|
visit(vo);
|
|
230
|
-
return
|
|
254
|
+
return groups;
|
|
231
255
|
}
|
|
232
256
|
|
|
233
257
|
/** Collect the mirror-interface names reachable from `vo` (root mirror + nested VO mirrors). */
|
|
@@ -286,10 +310,16 @@ export function renderExtractor(root: MetaData, templateName: string): string {
|
|
|
286
310
|
const extractName = `extract${templateName}`;
|
|
287
311
|
const rootMapper = mapperName(vo);
|
|
288
312
|
|
|
289
|
-
const
|
|
313
|
+
const payloadGroups = reachablePayloadGroups(vo, root);
|
|
290
314
|
const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror);
|
|
291
315
|
const mappers = emitMappers(vo, root, rootMirror);
|
|
292
316
|
|
|
317
|
+
// One type-only import per VO entity module (the VO interface + its own enum
|
|
318
|
+
// union-aliases co-located there). NOT a single non-existent `./payloads.js`.
|
|
319
|
+
const payloadImports = payloadGroups
|
|
320
|
+
.map((g) => `import type { ${g.types.join(", ")} } from "./${g.module}.js";`)
|
|
321
|
+
.join("\n");
|
|
322
|
+
|
|
293
323
|
const lostMsg =
|
|
294
324
|
`${extractName}: lost required field(s): `;
|
|
295
325
|
|
|
@@ -301,13 +331,13 @@ export function renderExtractor(root: MetaData, templateName: string): string {
|
|
|
301
331
|
`// mapping the all-nullable mirror onto the strict payload. No registry / binding / factory.\n` +
|
|
302
332
|
`\n` +
|
|
303
333
|
`import {\n ${extractLenientWithName},\n type ${mirrorTypes.join(",\n type ")},\n} from "./${templateName}.output.js";\n` +
|
|
304
|
-
|
|
334
|
+
`${payloadImports}\n` +
|
|
305
335
|
`import type { MetaRoot } from "@metaobjectsdev/metadata";\n` +
|
|
306
336
|
`import type { ExtractionResult } from "@metaobjectsdev/render";\n` +
|
|
307
337
|
`\n` +
|
|
308
338
|
`/**\n` +
|
|
309
339
|
` * Extract a fully-typed \`${strictType}\` from dirty \`text\` using the loaded \`root\` (which must\n` +
|
|
310
|
-
` * declare the "${
|
|
340
|
+
` * declare the "${strictType}" payload value-object). Runs the tolerant extract, then maps the\n` +
|
|
311
341
|
` * extracted mirror onto the strict payload.\n` +
|
|
312
342
|
` *\n` +
|
|
313
343
|
` * @throws Error iff a \`@required\` field was lost (the strict opt-in gate).\n` +
|
|
@@ -8,8 +8,6 @@ import {
|
|
|
8
8
|
FIELD_SUBTYPE_STRING,
|
|
9
9
|
FIELD_SUBTYPE_INT,
|
|
10
10
|
FIELD_SUBTYPE_LONG,
|
|
11
|
-
FIELD_SUBTYPE_SHORT,
|
|
12
|
-
FIELD_SUBTYPE_BYTE,
|
|
13
11
|
FIELD_SUBTYPE_DOUBLE,
|
|
14
12
|
FIELD_SUBTYPE_FLOAT,
|
|
15
13
|
FIELD_SUBTYPE_DECIMAL,
|
|
@@ -54,8 +52,6 @@ function defaultViewForSubType(subType: string): string {
|
|
|
54
52
|
return VIEW_SUBTYPE_CHECKBOX;
|
|
55
53
|
case FIELD_SUBTYPE_INT:
|
|
56
54
|
case FIELD_SUBTYPE_LONG:
|
|
57
|
-
case FIELD_SUBTYPE_SHORT:
|
|
58
|
-
case FIELD_SUBTYPE_BYTE:
|
|
59
55
|
case FIELD_SUBTYPE_DOUBLE:
|
|
60
56
|
case FIELD_SUBTYPE_FLOAT:
|
|
61
57
|
case FIELD_SUBTYPE_DECIMAL:
|
|
@@ -92,8 +88,6 @@ export function zodTypeFor(field: MetaField): string {
|
|
|
92
88
|
return "z.string()";
|
|
93
89
|
case FIELD_SUBTYPE_INT:
|
|
94
90
|
case FIELD_SUBTYPE_LONG:
|
|
95
|
-
case FIELD_SUBTYPE_SHORT:
|
|
96
|
-
case FIELD_SUBTYPE_BYTE:
|
|
97
91
|
case FIELD_SUBTYPE_CURRENCY:
|
|
98
92
|
return "z.number().int()";
|
|
99
93
|
case FIELD_SUBTYPE_DOUBLE:
|
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
|
|
6
6
|
FIELD_SUBTYPE_BOOLEAN,
|
|
7
7
|
FIELD_SUBTYPE_INT,
|
|
8
|
-
FIELD_SUBTYPE_SHORT,
|
|
9
|
-
FIELD_SUBTYPE_BYTE,
|
|
10
8
|
FIELD_SUBTYPE_LONG,
|
|
11
9
|
FIELD_SUBTYPE_DOUBLE,
|
|
12
10
|
FIELD_SUBTYPE_FLOAT,
|
|
@@ -14,18 +12,19 @@ import {
|
|
|
14
12
|
FIELD_SUBTYPE_DATE,
|
|
15
13
|
FIELD_SUBTYPE_TIME,
|
|
16
14
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
15
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
17
16
|
opsForSubType,
|
|
18
17
|
} from "@metaobjectsdev/metadata";
|
|
19
18
|
import { sortableFields } from "./filter-shared.js";
|
|
20
19
|
|
|
21
20
|
const NUMBER_SUBTYPES = new Set<string>([
|
|
22
21
|
FIELD_SUBTYPE_INT,
|
|
23
|
-
FIELD_SUBTYPE_SHORT,
|
|
24
|
-
FIELD_SUBTYPE_BYTE,
|
|
25
22
|
FIELD_SUBTYPE_LONG,
|
|
26
23
|
FIELD_SUBTYPE_DOUBLE,
|
|
27
24
|
FIELD_SUBTYPE_FLOAT,
|
|
28
25
|
FIELD_SUBTYPE_DECIMAL,
|
|
26
|
+
// currency is integer minor units — coerces as a number on the wire.
|
|
27
|
+
FIELD_SUBTYPE_CURRENCY,
|
|
29
28
|
]);
|
|
30
29
|
|
|
31
30
|
const DATETIME_SUBTYPES = new Set<string>([
|
|
@@ -42,13 +41,20 @@ function filterSubTypeFor(fieldSubType: string): "string" | "number" | "boolean"
|
|
|
42
41
|
return "string";
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
function filterableFields(entity: MetaObject): MetaField[] {
|
|
44
|
+
function filterableFields(entity: MetaObject, exclude?: string): MetaField[] {
|
|
46
45
|
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in allowlists.
|
|
47
|
-
return entity
|
|
46
|
+
return entity
|
|
47
|
+
.fields()
|
|
48
|
+
.filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true && c.name !== exclude);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* `exclude` (FR-017): drop a field from the allowlist. Used by per-subtype TPH
|
|
53
|
+
* allowlists to omit the discriminator — it's pinned by the per-subtype route
|
|
54
|
+
* path, so a client must not filter on it.
|
|
55
|
+
*/
|
|
56
|
+
export function renderFilterAllowlist(entity: MetaObject, exclude?: string): Code {
|
|
57
|
+
const fields = filterableFields(entity, exclude);
|
|
52
58
|
if (fields.length === 0) {
|
|
53
59
|
return code`
|
|
54
60
|
import type { FilterAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
@@ -72,11 +78,12 @@ ${rows}
|
|
|
72
78
|
`;
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
export function renderSortAllowlist(entity: MetaObject): Code {
|
|
81
|
+
export function renderSortAllowlist(entity: MetaObject, exclude?: string): Code {
|
|
76
82
|
// Sortable = explicit @sortable === true, OR (no @sortable AND @filterable === true).
|
|
77
83
|
// @sortable: false explicitly opts out.
|
|
78
84
|
// Uses shared isSortableField predicate — must stay in sync with renderFilterType.
|
|
79
|
-
|
|
85
|
+
// `exclude` (FR-017): per-subtype TPH allowlists omit the discriminator.
|
|
86
|
+
const sortable = sortableFields(entity).filter((f) => f.name !== exclude);
|
|
80
87
|
if (sortable.length === 0) {
|
|
81
88
|
return code`
|
|
82
89
|
import type { SortAllowlist } from "@metaobjectsdev/runtime-ts/drizzle-fastify";
|
|
@@ -8,8 +8,6 @@ import {
|
|
|
8
8
|
FIELD_ATTR_FILTERABLE,
|
|
9
9
|
FIELD_SUBTYPE_BOOLEAN,
|
|
10
10
|
FIELD_SUBTYPE_INT,
|
|
11
|
-
FIELD_SUBTYPE_SHORT,
|
|
12
|
-
FIELD_SUBTYPE_BYTE,
|
|
13
11
|
FIELD_SUBTYPE_LONG,
|
|
14
12
|
FIELD_SUBTYPE_DOUBLE,
|
|
15
13
|
FIELD_SUBTYPE_FLOAT,
|
|
@@ -23,8 +21,6 @@ import { isSortableField } from "./filter-shared.js";
|
|
|
23
21
|
// matching the entity field representation (exact decimal string, not lossy number).
|
|
24
22
|
const NUMBER_VALUE_SUBTYPES = new Set<string>([
|
|
25
23
|
FIELD_SUBTYPE_INT,
|
|
26
|
-
FIELD_SUBTYPE_SHORT,
|
|
27
|
-
FIELD_SUBTYPE_BYTE,
|
|
28
24
|
FIELD_SUBTYPE_LONG,
|
|
29
25
|
FIELD_SUBTYPE_DOUBLE,
|
|
30
26
|
FIELD_SUBTYPE_FLOAT,
|
|
@@ -49,9 +45,15 @@ function renderFieldUnion(field: MetaField): string {
|
|
|
49
45
|
return `${tsName} | { ${opEntries.join("; ")} }`;
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
/**
|
|
49
|
+
* `exclude` (FR-017): drop a field from the client filter type. Used by
|
|
50
|
+
* per-subtype TPH filter types to omit the discriminator (it's pinned by the
|
|
51
|
+
* per-subtype route path), keeping the client `<Sub>Filter` type aligned with
|
|
52
|
+
* the server's per-subtype allowlist.
|
|
53
|
+
*/
|
|
54
|
+
export function renderFilterType(entity: MetaObject, exclude?: string): Code {
|
|
53
55
|
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in filter types.
|
|
54
|
-
const allFields = entity.fields();
|
|
56
|
+
const allFields = entity.fields().filter((c) => c.name !== exclude);
|
|
55
57
|
const filterableFieldsList = allFields.filter((c) => c.ownAttr(FIELD_ATTR_FILTERABLE) === true);
|
|
56
58
|
// Sort union uses isSortableField — same predicate as renderSortAllowlist to prevent
|
|
57
59
|
// client/server mismatches (@filterable: true + @sortable: false must be excluded from both).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
2
|
+
import { TYPE_TEMPLATE } from "@metaobjectsdev/metadata";
|
|
3
|
+
|
|
4
|
+
/** All template nodes of `subType` anywhere in the tree (top-level OR nested in entities). */
|
|
5
|
+
export function findTemplates(root: MetaData, subType: string): MetaData[] {
|
|
6
|
+
const out: MetaData[] = [];
|
|
7
|
+
const visit = (node: MetaData) => {
|
|
8
|
+
for (const child of node.ownChildren()) {
|
|
9
|
+
if (child.type === TYPE_TEMPLATE && child.subType === subType) out.push(child);
|
|
10
|
+
visit(child);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
visit(root);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// server/typescript/packages/codegen-ts/src/templates/fr010-field-mapping.ts
|
|
2
2
|
//
|
|
3
|
-
// Shared field-kind mapping for the FR-010 codegen emitters (
|
|
4
|
-
//
|
|
3
|
+
// Shared field-kind mapping for the FR-010 codegen emitters (the output-format-spec
|
|
4
|
+
// emitter et al.). Maps a metadata field subtype onto the render engine's
|
|
5
5
|
// FieldKind member, the idiomatic nullable TS type used by the extract mirror interface,
|
|
6
6
|
// and the ExtractMap accessor that reads it from the forgiving outcome map.
|
|
7
7
|
//
|
|
@@ -13,14 +13,11 @@ import {
|
|
|
13
13
|
type MetaData,
|
|
14
14
|
TYPE_FIELD,
|
|
15
15
|
FIELD_SUBTYPE_STRING,
|
|
16
|
-
FIELD_SUBTYPE_CLASS,
|
|
17
16
|
FIELD_SUBTYPE_UUID,
|
|
18
17
|
FIELD_SUBTYPE_DATE,
|
|
19
18
|
FIELD_SUBTYPE_TIME,
|
|
20
19
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
21
20
|
FIELD_SUBTYPE_INT,
|
|
22
|
-
FIELD_SUBTYPE_SHORT,
|
|
23
|
-
FIELD_SUBTYPE_BYTE,
|
|
24
21
|
FIELD_SUBTYPE_LONG,
|
|
25
22
|
FIELD_SUBTYPE_CURRENCY,
|
|
26
23
|
FIELD_SUBTYPE_DOUBLE,
|
|
@@ -34,6 +31,7 @@ import {
|
|
|
34
31
|
FIELD_ATTR_COERCE_DEFAULT,
|
|
35
32
|
FIELD_ATTR_DEFAULT,
|
|
36
33
|
FIELD_ATTR_NORMALIZE,
|
|
34
|
+
FIELD_ATTR_XML_TEXT,
|
|
37
35
|
NORMALIZE_DEFAULT,
|
|
38
36
|
type NormalizeMode,
|
|
39
37
|
} from "@metaobjectsdev/metadata";
|
|
@@ -42,7 +40,6 @@ import {
|
|
|
42
40
|
export function scalarKind(subType: string): string | null {
|
|
43
41
|
switch (subType) {
|
|
44
42
|
case FIELD_SUBTYPE_STRING:
|
|
45
|
-
case FIELD_SUBTYPE_CLASS:
|
|
46
43
|
case FIELD_SUBTYPE_UUID:
|
|
47
44
|
case FIELD_SUBTYPE_DATE:
|
|
48
45
|
case FIELD_SUBTYPE_TIME:
|
|
@@ -53,8 +50,6 @@ export function scalarKind(subType: string): string | null {
|
|
|
53
50
|
case FIELD_SUBTYPE_DECIMAL:
|
|
54
51
|
return "STRING";
|
|
55
52
|
case FIELD_SUBTYPE_INT:
|
|
56
|
-
case FIELD_SUBTYPE_SHORT:
|
|
57
|
-
case FIELD_SUBTYPE_BYTE:
|
|
58
53
|
return "INT";
|
|
59
54
|
case FIELD_SUBTYPE_LONG:
|
|
60
55
|
case FIELD_SUBTYPE_CURRENCY:
|
|
@@ -86,6 +81,13 @@ export function isRequired(field: MetaData): boolean {
|
|
|
86
81
|
return typeof v === "string" && v.toLowerCase() === "true";
|
|
87
82
|
}
|
|
88
83
|
|
|
84
|
+
/** True iff the field's @xmlText is explicitly true (the XML text-content extract marker). */
|
|
85
|
+
export function xmlText(field: MetaData): boolean {
|
|
86
|
+
const v = field.ownAttr(FIELD_ATTR_XML_TEXT);
|
|
87
|
+
if (v === true) return true;
|
|
88
|
+
return typeof v === "string" && v.toLowerCase() === "true";
|
|
89
|
+
}
|
|
90
|
+
|
|
89
91
|
/** The string members of an enum field's @values attr (empty when absent). */
|
|
90
92
|
export function enumValues(field: MetaData): string[] {
|
|
91
93
|
const v = field.ownAttr(FIELD_ATTR_VALUES);
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
FIELD_SUBTYPE_OBJECT,
|
|
15
15
|
FIELD_SUBTYPE_STRING,
|
|
16
16
|
FIELD_SUBTYPE_INT,
|
|
17
|
-
FIELD_SUBTYPE_SHORT,
|
|
18
|
-
FIELD_SUBTYPE_BYTE,
|
|
19
17
|
FIELD_SUBTYPE_LONG,
|
|
20
18
|
FIELD_SUBTYPE_DOUBLE,
|
|
21
19
|
FIELD_SUBTYPE_FLOAT,
|
|
@@ -25,23 +23,36 @@ import {
|
|
|
25
23
|
FIELD_SUBTYPE_DATE,
|
|
26
24
|
FIELD_SUBTYPE_TIME,
|
|
27
25
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
28
|
-
FIELD_SUBTYPE_CLASS,
|
|
29
26
|
FIELD_SUBTYPE_UUID,
|
|
30
27
|
FIELD_ATTR_REQUIRED,
|
|
31
28
|
FIELD_ATTR_OBJECT_REF,
|
|
32
29
|
} from "@metaobjectsdev/metadata";
|
|
33
30
|
import { variableNameFromEntity, toPascalCase } from "../naming.js";
|
|
31
|
+
import { stripPackage } from "@metaobjectsdev/metadata";
|
|
34
32
|
import { enumValues } from "../enum-meta.js";
|
|
35
33
|
import { renderDocsFor } from "./jsdoc.js";
|
|
34
|
+
import { sharedEnumForField } from "../enum-shared.js";
|
|
35
|
+
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
|
|
36
|
+
import type { RenderContext } from "../render-context.js";
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
|
|
40
|
+
*
|
|
41
|
+
* `tphBase` (FR-017): when this entity is a TPH discriminator base, the
|
|
42
|
+
* discriminated-union type (emitted by the tph-discriminator template) owns the
|
|
43
|
+
* bare `<Base>` name, so the raw single-table row type is emitted as `<Base>Row`
|
|
44
|
+
* to avoid a duplicate `export type <Base>`. Insert/Update keep their names
|
|
45
|
+
* (no collision); they describe the physical TPH table row shape.
|
|
46
|
+
*/
|
|
47
|
+
export function renderInferredTypes(entity: MetaObject, tphBase = false): Code {
|
|
38
48
|
const varName = variableNameFromEntity(entity.name);
|
|
39
49
|
const selectSym = imp("InferSelectModel@drizzle-orm");
|
|
40
50
|
const insertSym = imp("InferInsertModel@drizzle-orm");
|
|
41
51
|
const docs = renderDocsFor(entity);
|
|
42
52
|
const docsPrefix = docs ? `${docs}\n` : "";
|
|
53
|
+
const rowName = tphBase ? `${entity.name}Row` : entity.name;
|
|
43
54
|
return code`
|
|
44
|
-
${docsPrefix}export type ${
|
|
55
|
+
${docsPrefix}export type ${rowName} = ${selectSym}<typeof ${varName}>;
|
|
45
56
|
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
|
|
46
57
|
export type ${entity.name}Update = Partial<${entity.name}Insert>;
|
|
47
58
|
`;
|
|
@@ -71,12 +82,23 @@ export function enumUnionString(values: string[]): string {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
/**
|
|
74
|
-
* Emit
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
85
|
+
* Emit the enum type-alias section for an entity file. Three cases per field:
|
|
86
|
+
*
|
|
87
|
+
* • inline enum (members declared directly on the field; no root-abstract super)
|
|
88
|
+
* → `export type <Entity><Field> = "A" | "B";` — UNCHANGED (byte-identical).
|
|
89
|
+
* • shared materialized enum (extends a NON-@provided root-level abstract
|
|
90
|
+
* field.enum) → re-export the materialized type from the shared `./enums`
|
|
91
|
+
* module (`export { type E } from "./enums"`) instead of redeclaring it. The
|
|
92
|
+
* type is materialized ONCE in enums.ts (FR-019).
|
|
93
|
+
* • provided enum (extends a @provided root-level abstract field.enum) →
|
|
94
|
+
* re-export the type from the configured external module
|
|
95
|
+
* (`export { type E } from "<providedEnumModule>"`); metaobjects emits no
|
|
96
|
+
* declaration for it. A missing config is a codegen-time error.
|
|
97
|
+
*
|
|
98
|
+
* `ctx` is required to compute the shared/provided import specifiers. Returns
|
|
99
|
+
* null when the entity has no enum-alias lines to emit.
|
|
78
100
|
*/
|
|
79
|
-
export function renderEnumTypeAliases(entity: MetaObject): Code | null {
|
|
101
|
+
export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext): Code | null {
|
|
80
102
|
// De-duplicate by type-alias name — multiple fields can extend the same abstract enum.
|
|
81
103
|
const seen = new Set<string>();
|
|
82
104
|
const lines: string[] = [];
|
|
@@ -91,7 +113,21 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
|
|
|
91
113
|
if (seen.has(typeName)) continue;
|
|
92
114
|
seen.add(typeName);
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
// Without a RenderContext (bare unit-test calls) the shared/provided import
|
|
117
|
+
// specifiers can't be computed — fall back to inline emission. Real runs
|
|
118
|
+
// always pass ctx (entity-file template), so shared materialization applies.
|
|
119
|
+
const shared = ctx !== undefined ? sharedEnumForField(field) : undefined;
|
|
120
|
+
if (shared === undefined) {
|
|
121
|
+
// Inline enum — emit the literal union exactly as before.
|
|
122
|
+
lines.push(`export type ${typeName} = ${enumUnionString(values)};`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Shared / provided enum — re-export from the materialized module or the
|
|
126
|
+
// configured external module; never redeclare the union here.
|
|
127
|
+
const spec = shared.provided
|
|
128
|
+
? providedEnumImportSpecifier(ctx!, shared.name)
|
|
129
|
+
: sharedEnumImportSpecifier(ctx!, entity.package);
|
|
130
|
+
lines.push(`export { type ${shared.name} } from ${JSON.stringify(spec)};`);
|
|
95
131
|
}
|
|
96
132
|
|
|
97
133
|
return lines.length > 0 ? code`${lines.join("\n")}` : null;
|
|
@@ -103,11 +139,8 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
|
|
|
103
139
|
|
|
104
140
|
const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
|
|
105
141
|
[FIELD_SUBTYPE_STRING]: "string",
|
|
106
|
-
[FIELD_SUBTYPE_CLASS]: "string",
|
|
107
142
|
[FIELD_SUBTYPE_UUID]: "string",
|
|
108
143
|
[FIELD_SUBTYPE_INT]: "number",
|
|
109
|
-
[FIELD_SUBTYPE_SHORT]: "number",
|
|
110
|
-
[FIELD_SUBTYPE_BYTE]: "number",
|
|
111
144
|
[FIELD_SUBTYPE_LONG]: "number",
|
|
112
145
|
[FIELD_SUBTYPE_DOUBLE]: "number",
|
|
113
146
|
[FIELD_SUBTYPE_FLOAT]: "number",
|
|
@@ -122,19 +155,62 @@ const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
|
|
|
122
155
|
[FIELD_SUBTYPE_TIMESTAMP]: "string",
|
|
123
156
|
};
|
|
124
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The PLAIN-STRING TS type expression for a field — the SINGLE source of truth
|
|
160
|
+
* for "what TS type does the codegen give this field". `valueObjectFieldType`
|
|
161
|
+
* (which returns a `Code` so cross-module `field.object` refs hoist via
|
|
162
|
+
* `imp(...)`) makes the SAME per-branch decisions; this string form exists for
|
|
163
|
+
* consumers (the api-docs field-shape builder) that need the type name as text,
|
|
164
|
+
* not a hoisting `Code`. The branch logic MUST stay in lock-step with
|
|
165
|
+
* `valueObjectFieldType` below.
|
|
166
|
+
*
|
|
167
|
+
* • field.object → the referenced object's bare (package-stripped) name, `[]`
|
|
168
|
+
* when an array; `unknown` / `unknown[]` when the @objectRef is missing.
|
|
169
|
+
* • field.enum → the same enum-union alias `enumUnionAliasName` emits
|
|
170
|
+
* (`<Owner><Field>` or the abstract super's PascalCase), `string` fallback.
|
|
171
|
+
* • scalar → SCALAR_TS_BY_SUBTYPE (else `unknown`), `[]` when an array.
|
|
172
|
+
*/
|
|
173
|
+
export function fieldTsTypeString(ownerName: string, field: MetaField): string {
|
|
174
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
175
|
+
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
176
|
+
if (typeof ref === "string" && ref.length > 0) {
|
|
177
|
+
const base = stripPackage(ref);
|
|
178
|
+
return field.isArray ? `${base}[]` : base;
|
|
179
|
+
}
|
|
180
|
+
return field.isArray ? "unknown[]" : "unknown";
|
|
181
|
+
}
|
|
182
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
183
|
+
const values = enumValues(field);
|
|
184
|
+
if (values !== undefined) {
|
|
185
|
+
// The emitted TS type is an enum-union ALIAS (`<Owner><Field>`), but its
|
|
186
|
+
// definition IS this literal union — inline it so the documented shape is
|
|
187
|
+
// self-contained (an agent sees the exact allowed values, not an opaque
|
|
188
|
+
// alias name). Array enums wrap the parenthesized union: `(A | B)[]`.
|
|
189
|
+
const union = enumUnionString(values);
|
|
190
|
+
return field.isArray ? `(${union})[]` : union;
|
|
191
|
+
}
|
|
192
|
+
return field.isArray ? "string[]" : "string";
|
|
193
|
+
}
|
|
194
|
+
const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
|
|
195
|
+
return field.isArray ? `${scalar}[]` : scalar;
|
|
196
|
+
}
|
|
197
|
+
|
|
125
198
|
/**
|
|
126
199
|
* One-line TS type expression for a field on a value-only object.
|
|
127
200
|
* Returns a `Code` so cross-module `field.object` refs can be hoisted via
|
|
128
201
|
* ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
|
|
129
202
|
*/
|
|
130
|
-
function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
|
|
203
|
+
function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: RenderContext): Code {
|
|
131
204
|
// field.object: import the referenced TS interface from its sibling module
|
|
132
205
|
// so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
|
|
133
206
|
// import strategy, just for the type alias instead of the schema constant.
|
|
134
207
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
135
208
|
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
136
209
|
if (typeof ref === "string" && ref.length > 0) {
|
|
137
|
-
|
|
210
|
+
// @objectRef may be authored fully-qualified (acme::sales::Brief) or bare; the
|
|
211
|
+
// referenced interface + its sibling module are named by the BARE short name.
|
|
212
|
+
const base = stripPackage(ref);
|
|
213
|
+
const refImp = imp(`${base}@./${base}.js`);
|
|
138
214
|
return field.isArray ? code`${refImp}[]` : code`${refImp}`;
|
|
139
215
|
}
|
|
140
216
|
return field.isArray ? code`unknown[]` : code`unknown`;
|
|
@@ -145,6 +221,20 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
|
|
|
145
221
|
const values = enumValues(field);
|
|
146
222
|
if (values !== undefined) {
|
|
147
223
|
const alias = enumUnionAliasName(entity.name, field);
|
|
224
|
+
// FR-019: a shared/provided enum's type lives in another module (./enums or
|
|
225
|
+
// the provided module). Use imp() so ts-poet hoists `import { type E }` —
|
|
226
|
+
// the local interface can then reference E. Inline enums reference the
|
|
227
|
+
// locally-declared `<Entity><Field>` alias as before.
|
|
228
|
+
if (ctx !== undefined) {
|
|
229
|
+
const shared = sharedEnumForField(field);
|
|
230
|
+
if (shared !== undefined) {
|
|
231
|
+
const spec = shared.provided
|
|
232
|
+
? providedEnumImportSpecifier(ctx, shared.name)
|
|
233
|
+
: sharedEnumImportSpecifier(ctx, entity.package);
|
|
234
|
+
const sym = imp(`${shared.name}@${spec}`);
|
|
235
|
+
return field.isArray ? code`${sym}[]` : code`${sym}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
148
238
|
return field.isArray ? code`${alias}[]` : code`${alias}`;
|
|
149
239
|
}
|
|
150
240
|
return field.isArray ? code`string[]` : code`string`;
|
|
@@ -162,7 +252,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
|
|
|
162
252
|
* trip through Drizzle nullable columns, so the null-bridge is unnecessary
|
|
163
253
|
* here — and forces consumers into a residual cast at the call site.
|
|
164
254
|
*/
|
|
165
|
-
export function renderValueObjectInterface(entity: MetaObject): Code {
|
|
255
|
+
export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderContext): Code {
|
|
166
256
|
const docs = renderDocsFor(entity);
|
|
167
257
|
const docsPrefix = docs ? `${docs}\n` : "";
|
|
168
258
|
|
|
@@ -170,7 +260,7 @@ export function renderValueObjectInterface(entity: MetaObject): Code {
|
|
|
170
260
|
for (const field of entity.fields()) {
|
|
171
261
|
const required = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
|
|
172
262
|
const optional = required ? "" : "?";
|
|
173
|
-
const tsType = valueObjectFieldType(entity, field);
|
|
263
|
+
const tsType = valueObjectFieldType(entity, field, ctx);
|
|
174
264
|
lines.push(code` ${field.name}${optional}: ${tsType};`);
|
|
175
265
|
}
|
|
176
266
|
|