@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
|
@@ -5,20 +5,28 @@
|
|
|
5
5
|
// D5: the notes attr is NEVER emitted. Per the Documentation Provider design.
|
|
6
6
|
|
|
7
7
|
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
DOC_ATTR_DESCRIPTION,
|
|
10
|
+
FIELD_SUBTYPE_OBJECT,
|
|
11
|
+
FIELD_ATTR_OBJECT_REF,
|
|
12
|
+
OBJECT_SUBTYPE_VALUE,
|
|
13
|
+
} from "@metaobjectsdev/metadata";
|
|
9
14
|
import { readDocAttrs } from "./jsdoc.js";
|
|
10
15
|
|
|
11
|
-
/** Render
|
|
12
|
-
* entities
|
|
13
|
-
*
|
|
14
|
-
|
|
16
|
+
/** Render JUST the fenced ```mermaid erDiagram``` block for the whole model
|
|
17
|
+
* (entities + identity.reference relationships) — NO per-entity prose. This is
|
|
18
|
+
* the single neutral ER-diagram builder; both `renderMermaidModel()` (the
|
|
19
|
+
* standalone model.md body) AND the neutral docs OVERVIEW page (`README.md`,
|
|
20
|
+
* emitted by the Tier-2 `meta docs` engine) consume it, so there is exactly
|
|
21
|
+
* ONE place ER topology is computed — no duplicate graph logic (ADR-0020).
|
|
22
|
+
*
|
|
23
|
+
* Abstract entities are excluded — they have no physical table to put in a
|
|
24
|
+
* diagram (matches migrate-ts/expected-schema.ts's same filter). */
|
|
25
|
+
export function renderMermaidErBlock(root: MetaRoot): string {
|
|
15
26
|
const entities = root
|
|
16
27
|
.objects()
|
|
17
28
|
.filter((o) => o.isEntity() && !o.isAbstract);
|
|
18
29
|
const parts: string[] = [];
|
|
19
|
-
|
|
20
|
-
parts.push("# Data Model");
|
|
21
|
-
parts.push("");
|
|
22
30
|
parts.push("```mermaid");
|
|
23
31
|
parts.push("erDiagram");
|
|
24
32
|
for (const line of renderRelationships(entities)) parts.push(` ${line}`);
|
|
@@ -27,6 +35,22 @@ export function renderMermaidModel(root: MetaRoot): string {
|
|
|
27
35
|
for (const line of renderEntityBlock(entity)) parts.push(` ${line}`);
|
|
28
36
|
}
|
|
29
37
|
parts.push("```");
|
|
38
|
+
return parts.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Render a docs/model.md body: Mermaid erDiagram + per-entity prose. Abstract
|
|
42
|
+
* entities are excluded — they have no physical table to put in a diagram
|
|
43
|
+
* (matches migrate-ts/expected-schema.ts's same filter). Reuses the shared
|
|
44
|
+
* `renderMermaidErBlock()` for the diagram so the ER logic is never duplicated. */
|
|
45
|
+
export function renderMermaidModel(root: MetaRoot): string {
|
|
46
|
+
const entities = root
|
|
47
|
+
.objects()
|
|
48
|
+
.filter((o) => o.isEntity() && !o.isAbstract);
|
|
49
|
+
const parts: string[] = [];
|
|
50
|
+
|
|
51
|
+
parts.push("# Data Model");
|
|
52
|
+
parts.push("");
|
|
53
|
+
parts.push(renderMermaidErBlock(root));
|
|
30
54
|
parts.push("");
|
|
31
55
|
|
|
32
56
|
for (const entity of entities) {
|
|
@@ -37,6 +61,150 @@ export function renderMermaidModel(root: MetaRoot): string {
|
|
|
37
61
|
return parts.join("\n");
|
|
38
62
|
}
|
|
39
63
|
|
|
64
|
+
/** Render a Mermaid flowchart for ONE focal entity and its direct neighbors
|
|
65
|
+
* (1-hop). Three node kinds are surfaced and styled distinctly:
|
|
66
|
+
*
|
|
67
|
+
* - **focal** — the entity this page is about (deeper blue)
|
|
68
|
+
* - **same** — entity in the focal's own package (blue)
|
|
69
|
+
* - **external** — entity in a different package (dashed gray)
|
|
70
|
+
* - **vo** — value object referenced via `field.object` (rounded
|
|
71
|
+
* purple) — composition rather than FK
|
|
72
|
+
*
|
|
73
|
+
* Every node has a Mermaid `click <node> "./<node>.md"` directive, so the
|
|
74
|
+
* rendered SVG is a true navigation surface — click any neighbor to jump to
|
|
75
|
+
* its page. Edges are labeled with the field name that connects them.
|
|
76
|
+
*
|
|
77
|
+
* This replaces the previous erDiagram-based renderer: Mermaid 10's erDiagram
|
|
78
|
+
* has near-zero styling and no click support, while flowchart supports both.
|
|
79
|
+
* The crow's-foot one-to-many notation is dropped — for a 1-hop context
|
|
80
|
+
* diagram, "what kind of node is this" matters more than ER cardinality.
|
|
81
|
+
*
|
|
82
|
+
* Returns `undefined` when the focal entity has no neighbors at all, so a
|
|
83
|
+
* template can `{{#mini}}…{{/mini}}` and skip the section cleanly. Abstract
|
|
84
|
+
* entities are excluded as neighbors (no physical rows). */
|
|
85
|
+
export function renderEntityNeighborhoodErBlock(
|
|
86
|
+
focal: MetaObject,
|
|
87
|
+
root: MetaRoot,
|
|
88
|
+
): string | undefined {
|
|
89
|
+
if (focal.isAbstract) return undefined;
|
|
90
|
+
|
|
91
|
+
// Resolve neighbor kind so the flowchart can color/shape it accordingly.
|
|
92
|
+
// Same-package = focal's effective package; differing or unknown = external.
|
|
93
|
+
// Value object = object.value subtype. Aborts return classify→undefined.
|
|
94
|
+
//
|
|
95
|
+
// `node.package` is the OWN attr only — entities that take their package
|
|
96
|
+
// from the file-default (`metadata.root: { package: ... }`) leave it
|
|
97
|
+
// undefined. Use `fileDefaultPackage` as the fallback so cross-file
|
|
98
|
+
// boundary detection works in the common case where adopters set the
|
|
99
|
+
// package once at the root, not on every entity. Mirrors the same
|
|
100
|
+
// resolution rule docs-paths.ts uses for page placement.
|
|
101
|
+
const byName = new Map<string, MetaObject>();
|
|
102
|
+
for (const obj of root.objects()) byName.set(obj.name, obj);
|
|
103
|
+
const effectivePkg = (n: MetaObject) => n.package ?? n.fileDefaultPackage;
|
|
104
|
+
const focalPackage = effectivePkg(focal);
|
|
105
|
+
|
|
106
|
+
const sameDomain = new Set<string>();
|
|
107
|
+
const external = new Set<string>();
|
|
108
|
+
const valueObjs = new Set<string>();
|
|
109
|
+
|
|
110
|
+
type NeighborKind = "same" | "external" | "vo";
|
|
111
|
+
function classify(name: string): NeighborKind | undefined {
|
|
112
|
+
if (name === focal.name) return "same"; // self-reference (parent-id etc.)
|
|
113
|
+
const target = byName.get(name);
|
|
114
|
+
if (!target) return undefined;
|
|
115
|
+
if (target.subType === OBJECT_SUBTYPE_VALUE) {
|
|
116
|
+
valueObjs.add(name);
|
|
117
|
+
return "vo";
|
|
118
|
+
}
|
|
119
|
+
if (target.isAbstract) return undefined;
|
|
120
|
+
if (effectivePkg(target) === focalPackage) {
|
|
121
|
+
sameDomain.add(name);
|
|
122
|
+
return "same";
|
|
123
|
+
}
|
|
124
|
+
external.add(name);
|
|
125
|
+
return "external";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const edges: Array<{ from: string; to: string; label: string }> = [];
|
|
129
|
+
|
|
130
|
+
// Outgoing FK — focal references other entities.
|
|
131
|
+
for (const ref of focal.referenceIdentities()) {
|
|
132
|
+
const target = ref.targetEntity;
|
|
133
|
+
const field = ref.fields[0];
|
|
134
|
+
if (typeof target !== "string" || typeof field !== "string") continue;
|
|
135
|
+
const targetName = target.split("::").pop()!;
|
|
136
|
+
if (classify(targetName) === undefined) continue;
|
|
137
|
+
edges.push({ from: focal.name, to: targetName, label: field });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Incoming FK — entities that reference the focal.
|
|
141
|
+
for (const other of root.objects().filter(o => o.isEntity() && !o.isAbstract)) {
|
|
142
|
+
if (other.name === focal.name) continue;
|
|
143
|
+
for (const ref of other.referenceIdentities()) {
|
|
144
|
+
const target = ref.targetEntity;
|
|
145
|
+
if (typeof target !== "string") continue;
|
|
146
|
+
if (target.split("::").pop() !== focal.name) continue;
|
|
147
|
+
const field = ref.fields[0];
|
|
148
|
+
if (typeof field !== "string") continue;
|
|
149
|
+
if (classify(other.name) === undefined) continue;
|
|
150
|
+
edges.push({ from: other.name, to: focal.name, label: field });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Value-object composition — `field.object @objectRef ContactInfo` etc.
|
|
155
|
+
// These aren't FKs (no row-level identity); they're embedded composition.
|
|
156
|
+
// Surfaced because adopters care: "what's inside this entity's `jsonb`
|
|
157
|
+
// column?" Click-through to the VO docs answers that.
|
|
158
|
+
for (const field of focal.fields()) {
|
|
159
|
+
if (field.subType !== FIELD_SUBTYPE_OBJECT) continue;
|
|
160
|
+
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
161
|
+
if (typeof ref !== "string" || ref.length === 0) continue;
|
|
162
|
+
const targetName = ref.split("::").pop()!;
|
|
163
|
+
if (classify(targetName) === undefined) continue;
|
|
164
|
+
edges.push({ from: focal.name, to: targetName, label: field.name });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (edges.length === 0) return undefined;
|
|
168
|
+
|
|
169
|
+
// Build the flowchart. Nodes first (so all are declared before edges),
|
|
170
|
+
// then edges, then click directives, then classDefs + class assignments.
|
|
171
|
+
const allNodes = new Set<string>([focal.name, ...sameDomain, ...external, ...valueObjs]);
|
|
172
|
+
const parts: string[] = ["```mermaid", "flowchart TB"];
|
|
173
|
+
|
|
174
|
+
for (const name of allNodes) {
|
|
175
|
+
if (valueObjs.has(name)) {
|
|
176
|
+
// Stadium (rounded-rect) shape signals composition / embedded value.
|
|
177
|
+
parts.push(` ${name}(["${name}"])`);
|
|
178
|
+
} else {
|
|
179
|
+
// Sharp rectangle for entities.
|
|
180
|
+
parts.push(` ${name}["${name}"]`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const { from, to, label } of edges) {
|
|
184
|
+
parts.push(` ${from} -->|"${label}"| ${to}`);
|
|
185
|
+
}
|
|
186
|
+
// Click directives — link every node (focal + neighbors) to its docs page.
|
|
187
|
+
// Flat-layout assumption: `./<Name>.md` lives next to the focal's page.
|
|
188
|
+
// Package-layout adopters can override the template; the data plumbing
|
|
189
|
+
// is identical either way.
|
|
190
|
+
for (const name of allNodes) {
|
|
191
|
+
parts.push(` click ${name} "./${name}.md"`);
|
|
192
|
+
}
|
|
193
|
+
// Visual styling: 4 node kinds, 4 classes. Color choices are deliberately
|
|
194
|
+
// muted so the diagram reads as documentation, not infographic art.
|
|
195
|
+
parts.push(" classDef focal fill:#dbeafe,stroke:#1e40af,stroke-width:2px,color:#1e293b;");
|
|
196
|
+
parts.push(" classDef same fill:#eff6ff,stroke:#3b82f6,color:#1e293b;");
|
|
197
|
+
parts.push(" classDef external fill:#f3f4f6,stroke:#9ca3af,stroke-dasharray:4 3,color:#374151;");
|
|
198
|
+
parts.push(" classDef vo fill:#faf5ff,stroke:#9333ea,color:#1e293b;");
|
|
199
|
+
parts.push(` class ${focal.name} focal;`);
|
|
200
|
+
const sameOthers = [...sameDomain].filter(n => n !== focal.name);
|
|
201
|
+
if (sameOthers.length > 0) parts.push(` class ${sameOthers.join(",")} same;`);
|
|
202
|
+
if (external.size > 0) parts.push(` class ${[...external].join(",")} external;`);
|
|
203
|
+
if (valueObjs.size > 0) parts.push(` class ${[...valueObjs].join(",")} vo;`);
|
|
204
|
+
parts.push("```");
|
|
205
|
+
return parts.join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
40
208
|
function renderRelationships(entities: MetaObject[]): string[] {
|
|
41
209
|
const lines: string[] = [];
|
|
42
210
|
for (const entity of entities) {
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Per-template renderer for template.output codegen. Walks the @payloadRef's
|
|
4
4
|
// value-object into a Zod schema and emits a dual-API parser (parse + safeParse)
|
|
5
|
-
// alongside the schema
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// alongside the schema, plus (for json/xml outputs) a single tolerant
|
|
6
|
+
// loader-delegating `extractLenient<Name>WithLoader(root, text)` that delegates to
|
|
7
|
+
// the metadata-driven runtime extract. The emitted file derives a local data type
|
|
8
|
+
// via `z.infer<typeof Schema>` and exports it as `<TemplateName>Data`. Consumers
|
|
9
|
+
// wiring `promptRender()` get a structurally identical payload-VO interface in
|
|
10
|
+
// `prompts.ts`; either type can be used interchangeably with parse results.
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
type MetaData,
|
|
@@ -18,12 +19,8 @@ import {
|
|
|
18
19
|
FIELD_ATTR_OBJECT_REF,
|
|
19
20
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
20
21
|
TEMPLATE_ATTR_FORMAT,
|
|
22
|
+
refMatchesObject,
|
|
21
23
|
} from "@metaobjectsdev/metadata";
|
|
22
|
-
import {
|
|
23
|
-
schemaLiteral,
|
|
24
|
-
mirrorInitializer,
|
|
25
|
-
} from "./extract-schema-emitter.js";
|
|
26
|
-
import { extractMapHelpersUsed } from "./fr010-field-mapping.js";
|
|
27
24
|
import {
|
|
28
25
|
nestedMirrorInterfaces,
|
|
29
26
|
nestedMappers,
|
|
@@ -46,7 +43,9 @@ const SCALAR_ZOD: Record<string, string> = {
|
|
|
46
43
|
};
|
|
47
44
|
|
|
48
45
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
49
|
-
|
|
46
|
+
// FR-032 — @payloadRef is FQN after the desugar/sweep; match on the effective
|
|
47
|
+
// FQN resolution key (with bare back-compat).
|
|
48
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
@@ -166,96 +165,52 @@ export function ${safeParseName}(
|
|
|
166
165
|
|
|
167
166
|
// ---- FR-010 tolerant extract block (json/xml only) ----
|
|
168
167
|
const extractedName = `${templateName}Extracted`;
|
|
169
|
-
const extractLenientFnName = `extractLenient${templateName}`;
|
|
170
|
-
const tryExtractLenientName = `tryExtractLenient${templateName}`;
|
|
171
168
|
const extractLenientWithName = `extractLenient${templateName}WithLoader`;
|
|
172
|
-
const schemaConstName = `${templateName}ExtractSchema`;
|
|
173
169
|
const payloadFqnConst = `${templateName.toUpperCase()}_PAYLOAD_NAME`;
|
|
174
170
|
const formatEnum = format.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
|
|
175
|
-
const schemaLit = schemaLiteral(vo, format, payloadRef);
|
|
176
|
-
const initializer = mirrorInitializer(vo);
|
|
177
|
-
const mapHelpers = extractMapHelpersUsed(vo);
|
|
178
171
|
|
|
179
|
-
// The nullable mirror is
|
|
180
|
-
// payload mirror's nested-object / array-of-object components are typed (not
|
|
181
|
-
// so a mirror interface is emitted for every reachable nested value-object.
|
|
182
|
-
// keeps the canonical `<Template>Extracted` name
|
|
183
|
-
// existing self-contained extract<Name>() initializer continues to satisfy it.
|
|
172
|
+
// The nullable mirror is the return shape of the delegating extract. Use the nested-aware
|
|
173
|
+
// emitter so the payload mirror's nested-object / array-of-object components are typed (not
|
|
174
|
+
// `unknown`), and so a mirror interface is emitted for every reachable nested value-object.
|
|
175
|
+
// The payload mirror keeps the canonical `<Template>Extracted` name.
|
|
184
176
|
const mirrorDecls = nestedMirrorInterfaces(vo, root, extractedName);
|
|
185
|
-
const payloadHasNested = hasNested(vo, root);
|
|
186
|
-
|
|
187
|
-
// Render-package imports the extract block needs. Only pull in the names the emitted
|
|
188
|
-
// source actually references, so the file has no unused imports (tsc noUnusedLocals-safe).
|
|
189
|
-
const renderImports = ["extract", "extractSchema", "Format"];
|
|
190
|
-
if (schemaLit.includes("scalar(")) renderImports.push("scalar");
|
|
191
|
-
if (schemaLit.includes("enumField(")) renderImports.push("enumField");
|
|
192
|
-
if (schemaLit.includes("FieldKind.")) renderImports.push("FieldKind");
|
|
193
|
-
renderImports.push("type ExtractSchema", "type ExtractOptions", "type ExtractionResult");
|
|
194
|
-
renderImports.push(...mapHelpers);
|
|
195
|
-
|
|
196
|
-
const selfContained = `/** Baked extract descriptor for the ${templateName} output. */
|
|
197
|
-
const ${schemaConstName}: ExtractSchema = ${schemaLit};
|
|
198
|
-
|
|
199
|
-
${mirrorDecls}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Self-contained tolerant best-effort extraction of a dirty LLM response; never throws.
|
|
203
|
-
* Returns a nullable mirror (\`${extractedName}\`) with fields null where lost/malformed,
|
|
204
|
-
* plus the per-field extraction report. Does NOT populate nested-object / array-of-object
|
|
205
|
-
* components (those stay null — the historical FR-010 gap). For full nested extraction, use
|
|
206
|
-
* \`${extractLenientWithName}(root, text)\`, which delegates to the runtime extract.
|
|
207
|
-
*/
|
|
208
|
-
export function ${extractLenientFnName}(
|
|
209
|
-
text: string,
|
|
210
|
-
opts?: ExtractOptions,
|
|
211
|
-
): ExtractionResult<${extractedName}> {
|
|
212
|
-
const outcome = extract(text, ${schemaConstName}, opts);
|
|
213
|
-
const d = outcome.data;
|
|
214
|
-
const data: ${extractedName} = ${initializer};
|
|
215
|
-
return { data, report: outcome.report };
|
|
216
|
-
}
|
|
217
177
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*/
|
|
222
|
-
export function ${tryExtractLenientName}(
|
|
223
|
-
text: string,
|
|
224
|
-
): { ok: boolean; result: ExtractionResult<${extractedName}> } {
|
|
225
|
-
const result = ${extractLenientFnName}(text);
|
|
226
|
-
const ok = !result.report.isEmpty() && !result.report.hasLostRequired();
|
|
227
|
-
return { ok, result };
|
|
228
|
-
}
|
|
229
|
-
`;
|
|
178
|
+
// Render-package imports the (single, loader-delegating) extract block needs. Kept minimal so
|
|
179
|
+
// the file has no unused imports (tsc noUnusedLocals-safe).
|
|
180
|
+
const renderImports = ["Format", "type ExtractOptions", "type ExtractionResult"];
|
|
230
181
|
|
|
231
|
-
// ---- Runtime-delegating extract (
|
|
182
|
+
// ---- Runtime-delegating extract (the single metadata-driven extract path) ----
|
|
232
183
|
// Resolves this payload's MetaObject from a loaded MetaRoot by its baked simple name and
|
|
233
184
|
// delegates to extractObject() in @metaobjectsdev/runtime-ts, which assembles the FULL nested
|
|
234
|
-
// object graph reflection-free
|
|
235
|
-
// nullable mirror graph by the generated from<VO>Extracted
|
|
236
|
-
// (a generated DAO calling the dynamic-metadata runtime)
|
|
185
|
+
// object graph reflection-free by reading the live metadata directly. The assembled ValueObject
|
|
186
|
+
// graph is then mapped into the typed nullable mirror graph by the generated from<VO>Extracted
|
|
187
|
+
// mappers. Codegen-wrapping-runtime (a generated DAO calling the dynamic-metadata runtime).
|
|
237
188
|
//
|
|
238
189
|
// The baked PAYLOAD_NAME is the resolved payload VO's SIMPLE name (root.findObject matches on
|
|
239
190
|
// the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so it returns
|
|
240
191
|
// the canonically-named `<Template>Extracted` mirror); nested mappers use their VO names.
|
|
241
192
|
const payloadName = vo.name;
|
|
242
193
|
const rootMapper = rootMapperName(templateName);
|
|
194
|
+
void hasNested;
|
|
243
195
|
const delegating = `
|
|
244
196
|
/** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime. */
|
|
245
197
|
export const ${payloadFqnConst} = ${JSON.stringify(payloadName)};
|
|
246
198
|
|
|
199
|
+
${mirrorDecls}
|
|
200
|
+
|
|
247
201
|
${nestedMappers(vo, root, rootMapper, extractedName)}
|
|
248
202
|
|
|
249
203
|
${delegateHelpers(usedHelpers(vo, root))}
|
|
250
204
|
|
|
251
205
|
/**
|
|
252
|
-
* Runtime-delegating tolerant extraction; never throws.
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* model), then maps the assembled graph into the typed
|
|
206
|
+
* Runtime-delegating tolerant best-effort extraction; never throws. FULLY populates
|
|
207
|
+
* nested-object and array-of-object components by delegating to the metadata-driven runtime
|
|
208
|
+
* \`extractObject\` (which assembles the whole graph reflection-free via the Phase A object
|
|
209
|
+
* model, reading the live metadata directly), then maps the assembled graph into the typed
|
|
210
|
+
* \`${extractedName}\` mirror.
|
|
256
211
|
*
|
|
257
212
|
* @param root a loaded MetaRoot (e.g. \`(await new MetaDataLoader().load(...)).root\`) that declares
|
|
258
|
-
* the \`${
|
|
213
|
+
* the \`${payloadName}\` value-object.
|
|
259
214
|
*/
|
|
260
215
|
export function ${extractLenientWithName}(
|
|
261
216
|
root: MetaRoot,
|
|
@@ -272,9 +227,6 @@ export function ${extractLenientWithName}(
|
|
|
272
227
|
`;
|
|
273
228
|
|
|
274
229
|
// The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata.
|
|
275
|
-
// It is always emitted (the gap-closing path), regardless of whether THIS payload has nested
|
|
276
|
-
// fields — a flat payload still benefits from the loader-driven path and keeps the API uniform.
|
|
277
|
-
void payloadHasNested;
|
|
278
230
|
const metadataImport = `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
|
|
279
231
|
const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
|
|
280
232
|
|
|
@@ -285,7 +237,6 @@ export function ${extractLenientWithName}(
|
|
|
285
237
|
runtimeImport +
|
|
286
238
|
`\n` +
|
|
287
239
|
`${strictBody}\n` +
|
|
288
|
-
`${selfContained}\n` +
|
|
289
240
|
`${delegating}`
|
|
290
241
|
);
|
|
291
242
|
}
|
|
@@ -17,11 +17,12 @@ import {
|
|
|
17
17
|
TEMPLATE_SUBTYPE_OUTPUT,
|
|
18
18
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
19
19
|
TEMPLATE_ATTR_FORMAT,
|
|
20
|
+
refMatchesObject,
|
|
20
21
|
} from "@metaobjectsdev/metadata";
|
|
21
22
|
import { specLiteral } from "./output-format-spec-emitter.js";
|
|
22
23
|
|
|
23
24
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
24
|
-
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
25
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Queries file composer — composes all CRUD function renderers (from queries.ts) into
|
|
2
2
|
// a complete <Entity>.queries.ts file with @generated header and correct imports.
|
|
3
3
|
|
|
4
|
-
import { code, joinCode, type Code } from "ts-poet";
|
|
5
|
-
import {
|
|
4
|
+
import { code, joinCode, imp, type Code } from "ts-poet";
|
|
5
|
+
import {
|
|
6
|
+
MetaObject,
|
|
7
|
+
OBJECT_ATTR_DISCRIMINATOR,
|
|
8
|
+
OBJECT_ATTR_DISCRIMINATOR_VALUE,
|
|
9
|
+
} from "@metaobjectsdev/metadata";
|
|
6
10
|
import { type RenderContext } from "../render-context.js";
|
|
7
11
|
import { entityModuleSpecifier } from "../import-path.js";
|
|
8
12
|
import {
|
|
@@ -11,11 +15,21 @@ import {
|
|
|
11
15
|
renderCreateFn,
|
|
12
16
|
renderUpdateFn,
|
|
13
17
|
renderDeleteByIdFn,
|
|
18
|
+
getPkInfo,
|
|
14
19
|
} from "./queries.js";
|
|
15
|
-
import { variableNameFromEntity } from "../naming.js";
|
|
20
|
+
import { variableNameFromEntity, pluralize } from "../naming.js";
|
|
16
21
|
import { GENERATED_HEADER } from "../constants.js";
|
|
22
|
+
import { isTphDiscriminatorBase, tphConcreteSubtypes } from "./tph-discriminator.js";
|
|
17
23
|
|
|
18
24
|
export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
|
|
25
|
+
// FR-017 Tier 2 — a TPH discriminator base gets a polymorphic queries file:
|
|
26
|
+
// base reads dispatch through parse<Base>, and per-subtype CRUD targets the
|
|
27
|
+
// single base table scoped to the discriminator value. (Subtype entities are
|
|
28
|
+
// filtered out of the queries generator entirely — their CRUD lives here.)
|
|
29
|
+
if (isTphDiscriminatorBase(obj, ctx.loadedRoot)) {
|
|
30
|
+
return renderTphQueriesFile(obj, ctx);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
const entityName = obj.name;
|
|
20
34
|
// Import the entity's own file. Same target → relative "./Entity"; cross
|
|
21
35
|
// target → importBase-qualified package path.
|
|
@@ -68,3 +82,118 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
|
|
|
68
82
|
`// Customize via ${entityName}.extra.ts in this directory (additional queries, custom logic).\n`;
|
|
69
83
|
return header + body;
|
|
70
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* FR-017 Tier 2 — the polymorphic + per-subtype queries file for a TPH base.
|
|
88
|
+
*
|
|
89
|
+
* Base reads (`find<Base>ById`, `list<BasePlural>`) project every row through
|
|
90
|
+
* `parse<Base>` so they return the discriminated union. There is intentionally
|
|
91
|
+
* NO `create<Base>` / `update<Base>` — you cannot instantiate an abstract base.
|
|
92
|
+
*
|
|
93
|
+
* Each concrete subtype gets list / findById (filtered to the discriminator
|
|
94
|
+
* value, parsed with `<Sub>Schema`) plus create / updateById / deleteById, all
|
|
95
|
+
* against the single base table. Creates inject the discriminator value;
|
|
96
|
+
* updates strip it (a row's subtype is immutable).
|
|
97
|
+
*/
|
|
98
|
+
function renderTphQueriesFile(base: MetaObject, ctx: RenderContext): string {
|
|
99
|
+
const baseName = base.name;
|
|
100
|
+
const tableVar = variableNameFromEntity(baseName);
|
|
101
|
+
const discField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR) as string;
|
|
102
|
+
const { fieldName: pkField, tsType: pkType } = getPkInfo(base, ctx);
|
|
103
|
+
|
|
104
|
+
const baseFileSpec = entityModuleSpecifier(
|
|
105
|
+
ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
|
|
106
|
+
);
|
|
107
|
+
const tableSym = imp(`${tableVar}@${baseFileSpec}`);
|
|
108
|
+
const baseTypeSym = imp(`t:${baseName}@${baseFileSpec}`);
|
|
109
|
+
const parseSym = imp(`parse${baseName}@${baseFileSpec}`);
|
|
110
|
+
const eqSym = imp("eq@drizzle-orm");
|
|
111
|
+
const andSym = imp("and@drizzle-orm");
|
|
112
|
+
|
|
113
|
+
const dbTypeImport =
|
|
114
|
+
ctx.dialect === "postgres"
|
|
115
|
+
? `import type { NodePgDatabase } from "drizzle-orm/node-postgres";`
|
|
116
|
+
: `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
|
|
117
|
+
const dbTypeAlias =
|
|
118
|
+
ctx.dialect === "postgres"
|
|
119
|
+
? `type Db = NodePgDatabase<Record<string, never>>;`
|
|
120
|
+
: `type Db = BaseSQLiteDatabase<"async", Record<string, never>>;`;
|
|
121
|
+
|
|
122
|
+
// --- Polymorphic base reads ---
|
|
123
|
+
const polymorphic = code`
|
|
124
|
+
export async function find${baseName}ById(db: Db, ${pkField}: ${pkType}): Promise<${baseTypeSym} | null> {
|
|
125
|
+
const [row] = await db.select().from(${tableSym}).where(${eqSym}(${tableSym}.${pkField}, ${pkField})).limit(1);
|
|
126
|
+
return row ? ${parseSym}(row) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function list${pluralize(baseName)}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${baseTypeSym}[]> {
|
|
130
|
+
let q = db.select().from(${tableSym}).$dynamic();
|
|
131
|
+
if (opts?.limit !== undefined) q = q.limit(opts.limit);
|
|
132
|
+
if (opts?.offset !== undefined) q = q.offset(opts.offset);
|
|
133
|
+
const rows = await q;
|
|
134
|
+
return rows.map((r) => ${parseSym}(r));
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
// --- Per-subtype CRUD against the single base table ---
|
|
139
|
+
const subtypeSections: Code[] = [];
|
|
140
|
+
for (const sub of tphConcreteSubtypes(base, ctx.loadedRoot)) {
|
|
141
|
+
const value = sub.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) as string;
|
|
142
|
+
const valueLit = JSON.stringify(value);
|
|
143
|
+
const subFileSpec = entityModuleSpecifier(
|
|
144
|
+
ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
|
|
145
|
+
);
|
|
146
|
+
const subTypeSym = imp(`t:${sub.name}@${subFileSpec}`);
|
|
147
|
+
const subSchemaSym = imp(`${sub.name}Schema@${subFileSpec}`);
|
|
148
|
+
const subInsertSym = imp(`${sub.name}InsertSchema@${subFileSpec}`);
|
|
149
|
+
|
|
150
|
+
subtypeSections.push(code`
|
|
151
|
+
export async function list${pluralize(sub.name)}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${subTypeSym}[]> {
|
|
152
|
+
let q = db.select().from(${tableSym}).where(${eqSym}(${tableSym}.${discField}, ${valueLit})).$dynamic();
|
|
153
|
+
if (opts?.limit !== undefined) q = q.limit(opts.limit);
|
|
154
|
+
if (opts?.offset !== undefined) q = q.offset(opts.offset);
|
|
155
|
+
const rows = await q;
|
|
156
|
+
return rows.map((r) => ${subSchemaSym}.parse(r));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function find${sub.name}ById(db: Db, ${pkField}: ${pkType}): Promise<${subTypeSym} | null> {
|
|
160
|
+
const [row] = await db.select().from(${tableSym})
|
|
161
|
+
.where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).limit(1);
|
|
162
|
+
return row ? ${subSchemaSym}.parse(row) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function create${sub.name}(db: Db, data: unknown): Promise<${subTypeSym}> {
|
|
166
|
+
const validated = ${subInsertSym}.parse(data);
|
|
167
|
+
const [row] = await db.insert(${tableSym}).values({ ...validated, ${discField}: ${valueLit} }).returning();
|
|
168
|
+
return ${subSchemaSym}.parse(row!);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function update${sub.name}ById(db: Db, ${pkField}: ${pkType}, data: unknown): Promise<${subTypeSym} | null> {
|
|
172
|
+
const validated = ${subInsertSym}.partial().parse(data) as Record<string, unknown>;
|
|
173
|
+
// The discriminator is immutable — a ${sub.name} can never become another subtype.
|
|
174
|
+
const { [${JSON.stringify(discField)}]: _disc, ...safe } = validated;
|
|
175
|
+
const [row] = await db.update(${tableSym}).set(safe)
|
|
176
|
+
.where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).returning();
|
|
177
|
+
return row ? ${subSchemaSym}.parse(row) : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function delete${sub.name}ById(db: Db, ${pkField}: ${pkType}): Promise<boolean> {
|
|
181
|
+
const deleted = await db.delete(${tableSym})
|
|
182
|
+
.where(${andSym}(${eqSym}(${tableSym}.${pkField}, ${pkField}), ${eqSym}(${tableSym}.${discField}, ${valueLit}))).returning();
|
|
183
|
+
return deleted.length > 0;
|
|
184
|
+
}
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const literalImports = code`
|
|
189
|
+
${dbTypeImport}
|
|
190
|
+
${dbTypeAlias}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const body = joinCode([literalImports, polymorphic, ...subtypeSections], { on: "\n" }).toString();
|
|
194
|
+
const header =
|
|
195
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
196
|
+
`// Source metadata: ${baseName} (${base.fqn()}) — TPH discriminator base\n` +
|
|
197
|
+
`// Customize via ${baseName}.extra.ts in this directory (additional queries, custom logic).\n`;
|
|
198
|
+
return header + body;
|
|
199
|
+
}
|
package/src/templates/queries.ts
CHANGED
|
@@ -5,10 +5,18 @@ import { code, imp, type Code } from "ts-poet";
|
|
|
5
5
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
6
6
|
import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
|
|
7
7
|
import type { RenderContext } from "../render-context.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
variableNameFromEntity,
|
|
10
|
+
pluralize,
|
|
11
|
+
findByIdFnName,
|
|
12
|
+
listFnName,
|
|
13
|
+
createFnName,
|
|
14
|
+
updateFnName,
|
|
15
|
+
deleteByIdFnName,
|
|
16
|
+
} from "../naming.js";
|
|
9
17
|
|
|
10
18
|
/** Get the PK field name and its TS type for a given entity. */
|
|
11
|
-
function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
|
|
19
|
+
export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
|
|
12
20
|
// Use primaryIdentity() to find the primary identity (may be inherited from extends:/super:).
|
|
13
21
|
const primary = entity.primaryIdentity();
|
|
14
22
|
const rawFields = primary?.ownAttr(IDENTITY_ATTR_FIELDS);
|
|
@@ -30,7 +38,7 @@ export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
|
|
|
30
38
|
const entityName = entity.name;
|
|
31
39
|
const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
|
|
32
40
|
const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
|
|
33
|
-
const fnName =
|
|
41
|
+
const fnName = findByIdFnName(entityName);
|
|
34
42
|
const eqSym = imp("eq@drizzle-orm");
|
|
35
43
|
|
|
36
44
|
return code`
|
|
@@ -46,7 +54,7 @@ export function renderListFn(entity: MetaObject, _ctx: RenderContext): Code {
|
|
|
46
54
|
const entityName = entity.name;
|
|
47
55
|
// Pluralize the PascalCase entity name, preserving capitalization
|
|
48
56
|
// (e.g., "Category" -> "Categories", not "Categorys").
|
|
49
|
-
const fnName =
|
|
57
|
+
const fnName = listFnName(entityName);
|
|
50
58
|
|
|
51
59
|
return code`
|
|
52
60
|
export async function ${fnName}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${entityName}[]> {
|
|
@@ -62,7 +70,7 @@ export function renderCreateFn(entity: MetaObject, _ctx: RenderContext): Code {
|
|
|
62
70
|
const varName = variableNameFromEntity(entity.name);
|
|
63
71
|
const entityName = entity.name;
|
|
64
72
|
const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
|
|
65
|
-
const fnName =
|
|
73
|
+
const fnName = createFnName(entityName);
|
|
66
74
|
const schemaName = `${entityName}InsertSchema`;
|
|
67
75
|
|
|
68
76
|
return code`
|
|
@@ -79,7 +87,7 @@ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
|
|
|
79
87
|
const entityName = entity.name;
|
|
80
88
|
const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
|
|
81
89
|
const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
|
|
82
|
-
const fnName =
|
|
90
|
+
const fnName = updateFnName(entityName);
|
|
83
91
|
const schemaName = `${entityName}InsertSchema`;
|
|
84
92
|
const eqSym = imp("eq@drizzle-orm");
|
|
85
93
|
|
|
@@ -96,7 +104,7 @@ export function renderDeleteByIdFn(entity: MetaObject, ctx: RenderContext): Code
|
|
|
96
104
|
const varName = variableNameFromEntity(entity.name);
|
|
97
105
|
const entityName = entity.name;
|
|
98
106
|
const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
|
|
99
|
-
const fnName =
|
|
107
|
+
const fnName = deleteByIdFnName(entityName);
|
|
100
108
|
const eqSym = imp("eq@drizzle-orm");
|
|
101
109
|
|
|
102
110
|
return code`
|
|
@@ -46,6 +46,23 @@ function renderRelationEntry(
|
|
|
46
46
|
thisVarName: string,
|
|
47
47
|
thisEntityPackage: string | undefined,
|
|
48
48
|
): Code {
|
|
49
|
+
// FR-018 M:N: the source navigates through the junction table, so the Drizzle
|
|
50
|
+
// many() targets the JUNCTION, not the target entity (the relational query API
|
|
51
|
+
// then hops junction → target via the junction's one() sides). The
|
|
52
|
+
// mountM2mRoute the routes generator emits performs the flattened two-stage
|
|
53
|
+
// traversal for the REST contract.
|
|
54
|
+
if (entry.cardinality === CARDINALITY_MANY && entry.junctionEntity !== undefined) {
|
|
55
|
+
const junctionSpec = crossEntitySpecifier(
|
|
56
|
+
ctx.outputLayout,
|
|
57
|
+
thisEntityPackage,
|
|
58
|
+
ctx.packageOf.get(entry.junctionEntity),
|
|
59
|
+
entry.junctionEntity,
|
|
60
|
+
ctx.extStyle,
|
|
61
|
+
);
|
|
62
|
+
const junctionVarSym = imp(`${variableNameFromEntity(entry.junctionEntity)}@${junctionSpec}`);
|
|
63
|
+
return code` ${entry.name}: many(${junctionVarSym})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
// Use imp() for cross-entity references so ts-poet tracks and emits the import.
|
|
50
67
|
const targetSpec = crossEntitySpecifier(
|
|
51
68
|
ctx.outputLayout,
|