@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
|
@@ -22,15 +22,11 @@ export interface DocsRenderOpts {
|
|
|
22
22
|
dialect: Dialect;
|
|
23
23
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
24
24
|
loadedRoot: MetaRoot;
|
|
25
|
-
/** Names of generators present in the pipeline — drives the "Generated code"
|
|
26
|
-
* section. Always includes "entity-file" implicitly. Recognized names:
|
|
27
|
-
* "queries-file", "routes-file", "routes-file-hono". */
|
|
28
|
-
generatorNames?: ReadonlySet<string>;
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
/** Backward-compatible entry point: builds the EntityDocData payload and
|
|
32
|
-
* renders it via the framework template.
|
|
33
|
-
*
|
|
28
|
+
* renders it via the framework template. Output is gated by
|
|
29
|
+
* `docs-file-conformance.test.ts`. */
|
|
34
30
|
export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string {
|
|
35
31
|
const data = buildEntityDocData(entity, {
|
|
36
32
|
dialect: opts.dialect,
|
|
@@ -38,9 +34,6 @@ export function renderDocsFile(entity: MetaObject, opts: DocsRenderOpts): string
|
|
|
38
34
|
? { columnNamingStrategy: opts.columnNamingStrategy }
|
|
39
35
|
: {}),
|
|
40
36
|
loadedRoot: opts.loadedRoot,
|
|
41
|
-
...(opts.generatorNames !== undefined
|
|
42
|
-
? { generatorNames: opts.generatorNames }
|
|
43
|
-
: {}),
|
|
44
37
|
});
|
|
45
38
|
return render({
|
|
46
39
|
ref: "docs/entity-page.md",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// plus the relations() block auto-emitted at the end.
|
|
4
4
|
|
|
5
5
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
6
|
-
import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
|
|
6
|
+
import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
|
|
7
7
|
import {
|
|
8
8
|
IDENTITY_SUBTYPE_SECONDARY, FIELD_SUBTYPE_LONG,
|
|
9
9
|
IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, IDENTITY_ATTR_UNIQUE,
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
FIELD_ATTR_AUTO_SET,
|
|
12
12
|
} from "@metaobjectsdev/metadata";
|
|
13
13
|
import { type RenderContext } from "../render-context.js";
|
|
14
|
-
import { crossEntitySpecifier } from "../import-path.js";
|
|
14
|
+
import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
|
|
15
15
|
import { mapColumnType, type ColumnSpec } from "../column-mapper.js";
|
|
16
|
-
import { tableNameFromEntity,
|
|
16
|
+
import { tableNameFromEntity, columnNameFromField } from "../naming.js";
|
|
17
17
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
18
18
|
import { renderDocsFor } from "./jsdoc.js";
|
|
19
|
+
import { collectTphSubtypeFields } from "./tph-discriminator.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Render the Drizzle table definition for one entity, including:
|
|
@@ -32,17 +33,17 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
32
33
|
const tableFnSym = imp(`${tableFn}@${importModule}`);
|
|
33
34
|
|
|
34
35
|
const tableName = obj.dbTable ?? tableNameFromEntity(obj.name, ctx.columnNamingStrategy);
|
|
35
|
-
const varName =
|
|
36
|
+
const varName = ctx.collectionName(obj.name);
|
|
36
37
|
|
|
37
38
|
const primary = obj.primaryIdentity();
|
|
38
|
-
const rawPkFields = primary?.
|
|
39
|
+
const rawPkFields = primary?.attr(IDENTITY_ATTR_FIELDS);
|
|
39
40
|
const pkFieldsList: string[] = Array.isArray(rawPkFields)
|
|
40
41
|
? rawPkFields as string[]
|
|
41
42
|
: typeof rawPkFields === "string"
|
|
42
43
|
? rawPkFields.split(",").map((f) => f.trim()).filter(Boolean)
|
|
43
44
|
: [];
|
|
44
45
|
const pkFieldNames = new Set<string>(pkFieldsList);
|
|
45
|
-
const pkGeneration = primary?.
|
|
46
|
+
const pkGeneration = primary?.attr(IDENTITY_ATTR_GENERATION) as string | undefined;
|
|
46
47
|
|
|
47
48
|
const fkMap = buildFkMapForEntity(obj, ctx);
|
|
48
49
|
|
|
@@ -55,9 +56,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
55
56
|
const secondaryIdentities = obj.secondaryIdentities();
|
|
56
57
|
const uniqueFieldNames = new Set<string>();
|
|
57
58
|
for (const sec of secondaryIdentities) {
|
|
58
|
-
const uniqueAttr = sec.
|
|
59
|
+
const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
|
|
59
60
|
if (uniqueAttr === false) continue; // explicit non-unique → don't mark column
|
|
60
|
-
const fields = sec.
|
|
61
|
+
const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
|
|
61
62
|
if (!Array.isArray(fields) || fields.length !== 1) continue; // multi-col uniques use a callback index, not a column flag
|
|
62
63
|
uniqueFieldNames.add(fields[0]!);
|
|
63
64
|
}
|
|
@@ -71,9 +72,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
71
72
|
const fkInfo = fkMap.get(child.name);
|
|
72
73
|
// Compute the column spec once per field and reuse it for both the column
|
|
73
74
|
// line and the CHECK collection.
|
|
74
|
-
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy);
|
|
75
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
|
|
75
76
|
const fieldDocs = renderDocsFor(child);
|
|
76
|
-
const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package);
|
|
77
|
+
const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package, obj.name);
|
|
77
78
|
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
78
79
|
if (spec.checkConstraint !== undefined) {
|
|
79
80
|
checkConstraints.push({
|
|
@@ -83,6 +84,29 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// FR-017 Tier 2 — TPH single-table inheritance. When this entity is a
|
|
88
|
+
// discriminator base, fold every concrete subtype's own columns into this
|
|
89
|
+
// one table. Subtype-only columns are ALWAYS nullable (a row of any other
|
|
90
|
+
// subtype stores NULL there) and never carry a DB default (a default would
|
|
91
|
+
// stamp onto other-subtype inserts), regardless of the field's @required.
|
|
92
|
+
// Subtype entities emit no table of their own (the value-object path).
|
|
93
|
+
for (const child of collectTphSubtypeFields(obj, ctx.loadedRoot)) {
|
|
94
|
+
const spec = mapColumnType(child, ctx.dialect, ctx.columnNamingStrategy, ctx.timestampMode);
|
|
95
|
+
const fieldDocs = renderDocsFor(child);
|
|
96
|
+
const columnLine = renderColumn(
|
|
97
|
+
spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, obj.name, true,
|
|
98
|
+
);
|
|
99
|
+
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
100
|
+
// Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
|
|
101
|
+
// (not false), so other-subtype rows with NULL pass the check.
|
|
102
|
+
if (spec.checkConstraint !== undefined) {
|
|
103
|
+
checkConstraints.push({
|
|
104
|
+
name: `chk_${tableName}_${spec.dbName}`,
|
|
105
|
+
expr: spec.checkConstraint,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
// Build all table callback entries
|
|
87
111
|
const callbackEntries: Code[] = [];
|
|
88
112
|
|
|
@@ -91,13 +115,13 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
for (const sec of secondaryIdentities) {
|
|
94
|
-
const fields = sec.
|
|
118
|
+
const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
|
|
95
119
|
if (!Array.isArray(fields) || fields.length === 0) continue;
|
|
96
120
|
const indexName = `idx_${tableName}_${fields.map((f) => columnNameFromField(f, ctx.columnNamingStrategy)).join("_")}`;
|
|
97
121
|
// @unique on the identity defaults to true (preserves back-compat with
|
|
98
122
|
// foundations fixtures that assumed secondary identities were always
|
|
99
123
|
// unique). Explicit @unique: false → ordinary non-unique index.
|
|
100
|
-
const uniqueAttr = sec.
|
|
124
|
+
const uniqueAttr = sec.attr(IDENTITY_ATTR_UNIQUE);
|
|
101
125
|
const isUnique = uniqueAttr !== false;
|
|
102
126
|
const indexFn = isUnique ? "uniqueIndex" : "index";
|
|
103
127
|
const indexSym = imp(`${indexFn}@${importModule}`);
|
|
@@ -160,11 +184,15 @@ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, F
|
|
|
160
184
|
const fkField = fkFieldNames[0]!;
|
|
161
185
|
const targetName = ref.targetEntity;
|
|
162
186
|
if (!targetName) continue;
|
|
163
|
-
|
|
187
|
+
// @references may be authored bare OR package-qualified, and the loader can
|
|
188
|
+
// resolve it to an FQN (e.g. the YAML front-end qualifies it). Strip the
|
|
189
|
+
// package so the lookup matches the object's bare name — mirrors the
|
|
190
|
+
// relation-resolver, which already does this.
|
|
191
|
+
const targetObj = ctx.loadedRoot.findObject(stripPackage(targetName));
|
|
164
192
|
if (!targetObj) continue;
|
|
165
193
|
const targetPkField = ref.resolvedTargetPkField(ctx.loadedRoot) ?? "id";
|
|
166
194
|
result.set(fkField, {
|
|
167
|
-
targetVarName:
|
|
195
|
+
targetVarName: ctx.collectionName(targetObj.name),
|
|
168
196
|
targetEntityName: targetObj.name,
|
|
169
197
|
targetPkField,
|
|
170
198
|
});
|
|
@@ -212,6 +240,12 @@ function renderColumn(
|
|
|
212
240
|
isComposite: boolean,
|
|
213
241
|
isUnique: boolean = false,
|
|
214
242
|
entityPackage: string | undefined = undefined,
|
|
243
|
+
// Name of the entity this column belongs to — used to detect a self-referential
|
|
244
|
+
// FK (target entity === this entity), which Drizzle emits without a self-import.
|
|
245
|
+
currentEntityName: string = "",
|
|
246
|
+
// FR-017 Tier 2 — TPH subtype-only column: force nullable (drop .notNull())
|
|
247
|
+
// and suppress any DB default (other-subtype rows must stay NULL here).
|
|
248
|
+
forceNullable: boolean = false,
|
|
215
249
|
): Code {
|
|
216
250
|
const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
|
|
217
251
|
|
|
@@ -261,6 +295,9 @@ function renderColumn(
|
|
|
261
295
|
if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
|
|
262
296
|
// Avoid double-emitting .unique() if it was already appended above.
|
|
263
297
|
if (isUnique && m === ".unique()") continue;
|
|
298
|
+
// TPH subtype-only column: never .notNull() / .unique() — rows of other
|
|
299
|
+
// subtypes store NULL, so neither constraint can hold across the table.
|
|
300
|
+
if (forceNullable && (m === ".notNull()" || m === ".unique()")) continue;
|
|
264
301
|
modifiersStr += m;
|
|
265
302
|
}
|
|
266
303
|
|
|
@@ -268,7 +305,7 @@ function renderColumn(
|
|
|
268
305
|
// the `sql` import via imp(); a raw `.default(sql`...`)` would leave `sql`
|
|
269
306
|
// unresolved in the generated file.
|
|
270
307
|
let sqlDefaultSegment: Code | null = null;
|
|
271
|
-
if (spec.defaultExpr !== undefined && !isPk) {
|
|
308
|
+
if (spec.defaultExpr !== undefined && !isPk && !forceNullable) {
|
|
272
309
|
if (spec.defaultExpr.kind === "now") {
|
|
273
310
|
if (ctx.dialect === "sqlite") {
|
|
274
311
|
const sqlSym = imp("sql@drizzle-orm");
|
|
@@ -289,21 +326,30 @@ function renderColumn(
|
|
|
289
326
|
// FK .references() uses imp() so ts-poet tracks the cross-entity import.
|
|
290
327
|
let fkRefSegment: Code | null = null;
|
|
291
328
|
if (fkInfo !== undefined && !isPk) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
329
|
+
if (fkInfo.targetEntityName === currentEntityName) {
|
|
330
|
+
// Self-referential FK (e.g. createdBy → this same table). Drizzle requires
|
|
331
|
+
// referencing the local table const directly — NOT a self-import — with an
|
|
332
|
+
// explicit `Any*Column` return type to break the circular type inference.
|
|
333
|
+
const anyColType = ctx.dialect === "sqlite" ? "AnySQLiteColumn" : "AnyPgColumn";
|
|
334
|
+
const anyColSym = imp(`${anyColType}@${spec.importModule}`);
|
|
335
|
+
fkRefSegment = code`.references((): ${anyColSym} => ${fkInfo.targetVarName}.${fkInfo.targetPkField})`;
|
|
336
|
+
} else {
|
|
337
|
+
const targetSpec = crossEntitySpecifier(
|
|
338
|
+
ctx.outputLayout,
|
|
339
|
+
entityPackage,
|
|
340
|
+
ctx.packageOf.get(fkInfo.targetEntityName),
|
|
341
|
+
fkInfo.targetEntityName,
|
|
342
|
+
ctx.extStyle,
|
|
343
|
+
);
|
|
344
|
+
const targetVarSym = imp(`${fkInfo.targetVarName}@${targetSpec}`);
|
|
345
|
+
fkRefSegment = code`.references(() => ${targetVarSym}.${fkInfo.targetPkField})`;
|
|
346
|
+
}
|
|
301
347
|
}
|
|
302
348
|
|
|
303
349
|
// @autoSet fields: emit .$defaultFn(() => new Date().toISOString()) so Drizzle
|
|
304
350
|
// inserts stamp the server-side timestamp automatically. This means callers don't
|
|
305
351
|
// need to supply createdAt / updatedAt in INSERT calls — Drizzle fills them in.
|
|
306
|
-
const autoSet = field.
|
|
352
|
+
const autoSet = field.attr(FIELD_ATTR_AUTO_SET);
|
|
307
353
|
const autoSetSuffix = (autoSet === "onCreate" || autoSet === "onUpdate")
|
|
308
354
|
? `.$defaultFn(() => new Date().toISOString())`
|
|
309
355
|
: "";
|
|
@@ -316,11 +362,17 @@ function renderColumn(
|
|
|
316
362
|
let dollarTypeSegment: Code | string = "";
|
|
317
363
|
if (spec.dollarTypeRef !== undefined) {
|
|
318
364
|
const ref = spec.dollarTypeRef;
|
|
365
|
+
const suffix = ref.array ? "[]" : "";
|
|
319
366
|
if (ref.kind === "scalar") {
|
|
320
|
-
dollarTypeSegment = `.$type<${ref.tsType}
|
|
367
|
+
dollarTypeSegment = `.$type<${ref.tsType}${suffix}>()`;
|
|
321
368
|
} else {
|
|
322
|
-
|
|
323
|
-
|
|
369
|
+
// Resolve the VO module through the shared layout/package/extStyle-aware
|
|
370
|
+
// helper so the .$type<VO> import matches the field's TS type + Zod schema.
|
|
371
|
+
const moduleSpec = valueObjectModuleSpecifier(
|
|
372
|
+
ref.name, ctx.packageOf, entityPackage, ctx.outputLayout, ctx.extStyle,
|
|
373
|
+
);
|
|
374
|
+
const refSym = imp(`${ref.name}@${moduleSpec}`);
|
|
375
|
+
dollarTypeSegment = ref.array ? code`.$type<${refSym}[]>()` : code`.$type<${refSym}>()`;
|
|
324
376
|
}
|
|
325
377
|
}
|
|
326
378
|
|
|
@@ -67,7 +67,7 @@ function humanize(s: string): string {
|
|
|
67
67
|
* "Subscriber" → "/subscribers"
|
|
68
68
|
* "WorkoutEvent" → "/workout_events"
|
|
69
69
|
*/
|
|
70
|
-
function resourcePath(entity: MetaData): string {
|
|
70
|
+
export function resourcePath(entity: MetaData): string {
|
|
71
71
|
const overrideAttr = entity.ownAttr("routePath");
|
|
72
72
|
if (typeof overrideAttr === "string" && overrideAttr.length > 0) {
|
|
73
73
|
return overrideAttr.startsWith("/") ? overrideAttr : `/${overrideAttr}`;
|
|
@@ -160,12 +160,12 @@ function renderFieldRules(field: MetaField): string | undefined {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Field-level @required attr (if not already covered by validator).
|
|
163
|
-
if (!hasRequired && field.
|
|
163
|
+
if (!hasRequired && field.attr(FIELD_ATTR_REQUIRED) === true) {
|
|
164
164
|
ruleParts.push(`required: ${JSON.stringify(`${humanize(field.name)} is required`)}`);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// Field-level @maxLength attr (if not already covered).
|
|
168
|
-
const maxLenAttr = field.
|
|
168
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
169
169
|
if (!hasMaxLength && typeof maxLenAttr === "number") {
|
|
170
170
|
ruleParts.push(
|
|
171
171
|
`maxLength: { value: ${maxLenAttr}, message: ${JSON.stringify(`Must be ${maxLenAttr} characters or fewer`)} }`,
|
|
@@ -15,6 +15,7 @@ import { renderZodValidators } from "./zod-validators.js";
|
|
|
15
15
|
import { renderEntityConstants } from "./entity-constants.js";
|
|
16
16
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
17
17
|
import { renderFilterType } from "./filter-type.js";
|
|
18
|
+
import { renderTphDiscriminatorUnion, isTphDiscriminatorBase } from "./tph-discriminator.js";
|
|
18
19
|
import { GENERATED_HEADER } from "../constants.js";
|
|
19
20
|
import { isProjection } from "../projection/projection-detector.js";
|
|
20
21
|
import { renderProjectionDecl } from "./projection-decl.js";
|
|
@@ -53,7 +54,7 @@ export function renderEntityFile(
|
|
|
53
54
|
// it. The entity-file generator suppresses this entirely when
|
|
54
55
|
// emitAbstractShapes is off; here we only guarantee "shape, never table".
|
|
55
56
|
if (isAbstract(entity)) {
|
|
56
|
-
return renderValueObjectFile(entity);
|
|
57
|
+
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// --- Projection path (read-only: view-backed entity with no table source) ---
|
|
@@ -72,19 +73,29 @@ export function renderEntityFile(
|
|
|
72
73
|
// the shape (LLM tool_use input_schema, REST body parsing) use the Zod
|
|
73
74
|
// schema; consumers that need the type use the interface.
|
|
74
75
|
if (!hasWritableRdbSource(entity)) {
|
|
75
|
-
return renderValueObjectFile(entity);
|
|
76
|
+
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// --- Vanilla / write-through entity path ---
|
|
79
|
-
const enumAliases = renderEnumTypeAliases(entity);
|
|
80
|
+
const enumAliases = renderEnumTypeAliases(entity, ctx);
|
|
81
|
+
// FR-017 Tier 1: when this entity carries @discriminator AND has concrete
|
|
82
|
+
// subtypes, append the discriminated-union type alias, type guards, and
|
|
83
|
+
// the parse<Base>(row) dispatcher. Returns null otherwise (no subtypes, or
|
|
84
|
+
// not a discriminator-bearing entity); the section is suppressed cleanly.
|
|
85
|
+
const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
|
|
86
|
+
// FR-017: when a discriminator base also has a union block, the union owns the
|
|
87
|
+
// bare `<Base>` type — so the inferred Drizzle row type is emitted as
|
|
88
|
+
// `<Base>Row` to avoid a duplicate `export type <Base>`.
|
|
89
|
+
const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
|
|
80
90
|
const sections: Code[] = [
|
|
81
91
|
renderDrizzleSchema(entity, ctx),
|
|
82
|
-
renderInferredTypes(entity),
|
|
92
|
+
renderInferredTypes(entity, tphBase, ctx),
|
|
83
93
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
84
|
-
renderZodValidators(entity),
|
|
94
|
+
renderZodValidators(entity, ctx),
|
|
85
95
|
renderEntityConstants(entity, ctx.apiPrefix),
|
|
86
96
|
...(allowlists ? [renderFilterAllowlist(entity), renderSortAllowlist(entity)] : []),
|
|
87
97
|
renderFilterType(entity),
|
|
98
|
+
...(tphBlock !== null ? [tphBlock] : []),
|
|
88
99
|
];
|
|
89
100
|
|
|
90
101
|
// Render ts-poet body first (ts-poet hoists imp()-tracked imports to the top),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// FR-019 — shared enums module.
|
|
2
|
+
//
|
|
3
|
+
// Emits, ONCE per run, the materialized (non-@provided) shared enum types that
|
|
4
|
+
// at least one concrete entity field references via `extends` of a root-level
|
|
5
|
+
// abstract `field.enum`. Each enum yields:
|
|
6
|
+
// • `export type <E> = "A" | "B";` — the cross-port type identity
|
|
7
|
+
// • `export const <E>Enum = z.enum(["A","B"]);` — the shared Zod validator
|
|
8
|
+
//
|
|
9
|
+
// Consuming entity files import these instead of redeclaring the union inline.
|
|
10
|
+
// @provided enums are NEVER materialized here (they live in hand-written code,
|
|
11
|
+
// imported from the configured providedEnumModule).
|
|
12
|
+
|
|
13
|
+
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
14
|
+
import type { MetaRoot } from "@metaobjectsdev/metadata";
|
|
15
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
16
|
+
import { materializedSharedEnums, type SharedEnum } from "../enum-shared.js";
|
|
17
|
+
import { enumUnionString } from "./inferred-types.js";
|
|
18
|
+
|
|
19
|
+
/** Basename (no extension) of the shared-enums module emitted at the entity-module target root. */
|
|
20
|
+
export const SHARED_ENUMS_BASENAME = "enums";
|
|
21
|
+
|
|
22
|
+
/** The exported Zod-constant name for a shared enum (`<E>Enum`). */
|
|
23
|
+
export function sharedEnumZodConstName(enumName: string): string {
|
|
24
|
+
return `${enumName}Enum`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One enum's two declarations (type alias + shared z.enum const). */
|
|
28
|
+
function renderOneSharedEnum(e: SharedEnum): Code {
|
|
29
|
+
const z = imp("z@zod");
|
|
30
|
+
const members = e.values.map((v) => JSON.stringify(v)).join(", ");
|
|
31
|
+
return code`
|
|
32
|
+
export type ${e.name} = ${enumUnionString(e.values)};
|
|
33
|
+
export const ${sharedEnumZodConstName(e.name)} = ${z}.enum([${members}]);
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The full shared-enums module body, or null when the model has no materialized
|
|
39
|
+
* shared enums (so the generator emits no file at all).
|
|
40
|
+
*/
|
|
41
|
+
export function renderSharedEnumsFile(root: MetaRoot): string | null {
|
|
42
|
+
const enums = materializedSharedEnums(root);
|
|
43
|
+
if (enums.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
const body = joinCode(enums.map(renderOneSharedEnum), { on: "\n" }).toString();
|
|
46
|
+
const header =
|
|
47
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
48
|
+
`// Shared enum types (FR-019): one declaration per reused package-level enum.\n`;
|
|
49
|
+
return header + body;
|
|
50
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// server/typescript/packages/codegen-ts/src/templates/extract-delegate-emitter.ts
|
|
2
2
|
//
|
|
3
|
-
// FR-010
|
|
3
|
+
// FR-010 — the runtime-DELEGATING extract emitter (the single metadata-driven extract path).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// array-of-object components NULL — the historical FR-010 codegen gap. This module emits
|
|
8
|
-
// the additive delegating overload that CLOSES that gap by wrapping the runtime extract:
|
|
5
|
+
// This module emits the loader-delegating extract entry point that reads the live metadata
|
|
6
|
+
// directly and populates nested-object and array-of-object components in full:
|
|
9
7
|
//
|
|
10
8
|
// extract<Name>(root: MetaRoot, text, opts?) -> ExtractionResult<<Name>Extracted>
|
|
11
9
|
//
|
|
@@ -29,16 +27,17 @@ import {
|
|
|
29
27
|
FIELD_SUBTYPE_ENUM,
|
|
30
28
|
FIELD_ATTR_OBJECT_REF,
|
|
31
29
|
PACKAGE_SEPARATOR,
|
|
30
|
+
refMatchesObject,
|
|
32
31
|
} from "@metaobjectsdev/metadata";
|
|
33
32
|
import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
|
|
34
33
|
|
|
35
34
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
36
|
-
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
35
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
40
39
|
function refVo(field: MetaData, root: MetaData): MetaData | undefined {
|
|
41
|
-
const ref = field.
|
|
40
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
42
41
|
if (typeof ref !== "string") return undefined;
|
|
43
42
|
const direct = findObject(root, ref);
|
|
44
43
|
if (direct !== undefined) return direct;
|
|
@@ -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 {
|
|
@@ -49,7 +50,7 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
|
49
50
|
|
|
50
51
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
51
52
|
function refVo(field: MetaData, root: MetaData): MetaData | undefined {
|
|
52
|
-
const ref = field.
|
|
53
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
53
54
|
if (typeof ref !== "string") return undefined;
|
|
54
55
|
const direct = findObject(root, ref);
|
|
55
56
|
if (direct !== undefined) return direct;
|
|
@@ -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,14 +77,15 @@ 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
|
-
return field.
|
|
88
|
+
return field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/** The mirror→strict mapper name for a value-object (`toStrict<Name>`). */
|
|
@@ -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` +
|