@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
|
@@ -18,26 +18,14 @@ import {
|
|
|
18
18
|
IDENTITY_ATTR_GENERATION,
|
|
19
19
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
20
20
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
21
|
+
RELATIONSHIP_ATTR_THROUGH,
|
|
22
|
+
RELATIONSHIP_ATTR_SOURCE_REF_FIELD,
|
|
23
|
+
RELATIONSHIP_ATTR_SYMMETRIC,
|
|
21
24
|
RELATIONSHIP_SUBTYPE_COMPOSITION,
|
|
22
25
|
RELATIONSHIP_SUBTYPE_AGGREGATION,
|
|
23
26
|
RELATIONSHIP_SUBTYPE_ASSOCIATION,
|
|
24
27
|
FIELD_SUBTYPE_ENUM,
|
|
25
28
|
FIELD_SUBTYPE_OBJECT,
|
|
26
|
-
FIELD_SUBTYPE_STRING,
|
|
27
|
-
FIELD_SUBTYPE_CLASS,
|
|
28
|
-
FIELD_SUBTYPE_UUID,
|
|
29
|
-
FIELD_SUBTYPE_INT,
|
|
30
|
-
FIELD_SUBTYPE_SHORT,
|
|
31
|
-
FIELD_SUBTYPE_BYTE,
|
|
32
|
-
FIELD_SUBTYPE_LONG,
|
|
33
|
-
FIELD_SUBTYPE_DOUBLE,
|
|
34
|
-
FIELD_SUBTYPE_FLOAT,
|
|
35
|
-
FIELD_SUBTYPE_DECIMAL,
|
|
36
|
-
FIELD_SUBTYPE_CURRENCY,
|
|
37
|
-
FIELD_SUBTYPE_BOOLEAN,
|
|
38
|
-
FIELD_SUBTYPE_DATE,
|
|
39
|
-
FIELD_SUBTYPE_TIME,
|
|
40
|
-
FIELD_SUBTYPE_TIMESTAMP,
|
|
41
29
|
FIELD_ATTR_REQUIRED,
|
|
42
30
|
FIELD_ATTR_UNIQUE,
|
|
43
31
|
FIELD_ATTR_OBJECT_REF,
|
|
@@ -51,196 +39,424 @@ import {
|
|
|
51
39
|
VALIDATOR_ATTR_MIN,
|
|
52
40
|
VALIDATOR_ATTR_MAX,
|
|
53
41
|
DOC_ATTR_DESCRIPTION,
|
|
42
|
+
DOC_ATTR_SUMMARY,
|
|
43
|
+
FIELD_ATTR_DB_COLUMN_TYPE,
|
|
54
44
|
stripPackage,
|
|
55
45
|
} from "@metaobjectsdev/metadata";
|
|
56
|
-
import {
|
|
46
|
+
import type { Dialect } from "../column-mapper.js";
|
|
57
47
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
58
|
-
import {
|
|
48
|
+
import type { OutputLayout } from "../import-path.js";
|
|
49
|
+
import { docPageHref, docPageNode } from "../docs-paths.js";
|
|
50
|
+
import { fieldAnchorHtml } from "./field-anchor.js";
|
|
59
51
|
import { enumValues } from "../enum-meta.js";
|
|
60
52
|
import { hasWritableRdbSource } from "../source-detect.js";
|
|
61
53
|
import { GENERATED_HEADER } from "../constants.js";
|
|
54
|
+
import { renderEntityNeighborhoodErBlock } from "../templates/mermaid-er.js";
|
|
62
55
|
import type {
|
|
63
56
|
EntityDocData,
|
|
64
57
|
StorageFieldDoc,
|
|
65
58
|
IdentityDoc,
|
|
66
59
|
RelationshipDoc,
|
|
67
60
|
UsedByDoc,
|
|
68
|
-
|
|
61
|
+
ConstraintRow,
|
|
62
|
+
FieldDoc,
|
|
63
|
+
FieldDetailDoc,
|
|
69
64
|
} from "./docs-data.js";
|
|
70
65
|
|
|
71
66
|
export interface BuildDocDataOpts {
|
|
72
67
|
dialect: Dialect;
|
|
73
68
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
74
69
|
loadedRoot: MetaRoot;
|
|
75
|
-
/**
|
|
76
|
-
|
|
70
|
+
/** Page-placement layout. Defaults to "flat" (back-compat: same-dir links). */
|
|
71
|
+
layout?: OutputLayout;
|
|
72
|
+
/** Cross-links to this entity's GENERATED-SDK api pages, one per api surface
|
|
73
|
+
* (per language). Computed by the caller (docsFile) via the shared
|
|
74
|
+
* `apiSurfaceHref` so each resolves in BOTH layouts. ABSENT for model-only
|
|
75
|
+
* runs → default output byte-identical. */
|
|
76
|
+
apiRefs?: Array<{ label: string; href: string }>;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
[FIELD_SUBTYPE_BYTE]: "number",
|
|
86
|
-
[FIELD_SUBTYPE_LONG]: "number",
|
|
87
|
-
[FIELD_SUBTYPE_DOUBLE]: "number",
|
|
88
|
-
[FIELD_SUBTYPE_FLOAT]: "number",
|
|
89
|
-
// field.decimal is precision-exact: surfaced as a TS `string` (Drizzle pg
|
|
90
|
-
// `numeric` infers `string`). Keep the docs scalar mapping in lockstep.
|
|
91
|
-
[FIELD_SUBTYPE_DECIMAL]: "string",
|
|
92
|
-
[FIELD_SUBTYPE_CURRENCY]: "number",
|
|
93
|
-
[FIELD_SUBTYPE_BOOLEAN]: "boolean",
|
|
94
|
-
[FIELD_SUBTYPE_DATE]: "string",
|
|
95
|
-
[FIELD_SUBTYPE_TIME]: "string",
|
|
96
|
-
[FIELD_SUBTYPE_TIMESTAMP]: "string",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
function enumTypeAliasName(entity: MetaObject, field: MetaField): string {
|
|
100
|
-
const superField = field.resolveSuper();
|
|
101
|
-
return superField !== undefined
|
|
102
|
-
? toPascalCase(superField.name)
|
|
103
|
-
: `${entity.name}${toPascalCase(field.name)}`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isFieldRequired(field: MetaField): boolean {
|
|
79
|
+
/** Whether a field is required — `@required` true OR a `validator.required`
|
|
80
|
+
* child. The SINGLE source of truth for required-ness across the Constraints
|
|
81
|
+
* table, the Storage nullable rule, and (via the api-docs field-shape builder)
|
|
82
|
+
* the documented model-field optionality. Exported so the field-shape builder
|
|
83
|
+
* reuses the EXACT same rule rather than re-deriving it. */
|
|
84
|
+
export function isFieldRequired(field: MetaField): boolean {
|
|
107
85
|
if (field.ownAttr(FIELD_ATTR_REQUIRED) === true) return true;
|
|
108
86
|
return field.validators().some((v) => v.subType === VALIDATOR_SUBTYPE_REQUIRED);
|
|
109
87
|
}
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
89
|
+
/** The raw validator/limit facts for the Constraints table. Walks the field's
|
|
90
|
+
* validators ONCE, bucketed by subtype, plus the `@maxLength` attr.
|
|
91
|
+
* `buildConstraintRow()` consumes these — the SINGLE source of truth for the
|
|
92
|
+
* validator emission. The emission ORDER and exact strings come from here:
|
|
93
|
+
* regex pattern → maxLength-from-@maxLength → length-validator (min/max) →
|
|
94
|
+
* numeric-validator (min/max). */
|
|
95
|
+
interface ValidatorParts {
|
|
96
|
+
/** `@maxLength` attr value if a finite number, else undefined. */
|
|
97
|
+
maxLenAttr: number | undefined;
|
|
98
|
+
/** "pattern `...`" entries from regex validators. */
|
|
99
|
+
regexParts: string[];
|
|
100
|
+
/** "minLength: N" / "maxLength: N" entries from length validators. */
|
|
101
|
+
lengthParts: string[];
|
|
102
|
+
/** "min: N" / "max: N" entries from numeric validators. */
|
|
103
|
+
numericParts: string[];
|
|
104
|
+
}
|
|
117
105
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
106
|
+
function collectValidatorParts(field: MetaField): ValidatorParts {
|
|
107
|
+
const maxLenAttr = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
|
|
108
|
+
const regexParts: string[] = [];
|
|
109
|
+
const lengthParts: string[] = [];
|
|
110
|
+
const numericParts: string[] = [];
|
|
111
|
+
for (const v of field.validators()) {
|
|
112
|
+
if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
113
|
+
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
114
|
+
if (typeof pattern === "string" && pattern.length > 0) {
|
|
115
|
+
regexParts.push(`pattern \`${pattern}\``);
|
|
125
116
|
}
|
|
126
|
-
} else {
|
|
127
|
-
|
|
117
|
+
} else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
118
|
+
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
119
|
+
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
120
|
+
if (typeof min === "number") lengthParts.push(`minLength: ${min}`);
|
|
121
|
+
if (typeof max === "number" && typeof maxLenAttr !== "number") lengthParts.push(`maxLength: ${max}`);
|
|
122
|
+
} else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
|
|
123
|
+
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
124
|
+
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
125
|
+
if (typeof min === "number") numericParts.push(`min: ${min}`);
|
|
126
|
+
if (typeof max === "number") numericParts.push(`max: ${max}`);
|
|
128
127
|
}
|
|
129
|
-
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
maxLenAttr: typeof maxLenAttr === "number" ? maxLenAttr : undefined,
|
|
131
|
+
regexParts,
|
|
132
|
+
lengthParts,
|
|
133
|
+
numericParts,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The NEUTRAL logical type string (no backticks): the field's logical
|
|
138
|
+
* subtype (e.g. `string`, `enum`, `decimal`), suffixed `[]` for arrays, and
|
|
139
|
+
* the referenced object name for `field.object`. Language-agnostic — built
|
|
140
|
+
* from declared metadata, never re-derived into ANSI/ORM SQL. Shared by the
|
|
141
|
+
* Constraints table (`neutralTypeCell`) and the Storage table's physical-type
|
|
142
|
+
* fallback (`storageTypeCell`). */
|
|
143
|
+
export function neutralTypeStr(field: MetaField): string {
|
|
144
|
+
let base: string;
|
|
145
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
130
146
|
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
131
|
-
|
|
132
|
-
base = field.isArray ? `${refName}[]` : refName;
|
|
147
|
+
base = typeof ref === "string" && ref.length > 0 ? stripPackage(ref) : "object";
|
|
133
148
|
} else {
|
|
134
|
-
|
|
135
|
-
base = field.isArray ? `${scalar}[]` : scalar;
|
|
149
|
+
base = field.subType;
|
|
136
150
|
}
|
|
151
|
+
if (field.isArray) base = `${base}[]`;
|
|
152
|
+
return base;
|
|
153
|
+
}
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
/** Neutral logical type cell for the Constraints table — `neutralTypeStr`
|
|
156
|
+
* wrapped in backticks. */
|
|
157
|
+
function neutralTypeCell(field: MetaField): string {
|
|
158
|
+
return `\`${neutralTypeStr(field)}\``;
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return `${spec.fnName}(${dbName}, { ${parts.join(", ")} })`;
|
|
161
|
+
/** Neutral PHYSICAL type cell for the Storage table. Metadata-driven, no DDL
|
|
162
|
+
* re-derivation (ADR-0020): if the field declares a `@dbColumnType` physical
|
|
163
|
+
* override (e.g. `uuid`, `jsonb`, `timestamp_with_tz`) show it UPPERCASED;
|
|
164
|
+
* otherwise fall back to the same neutral LOGICAL type the Constraints table
|
|
165
|
+
* uses. Deliberately does NOT derive ANSI/ORM SQL so it can't drift vs the
|
|
166
|
+
* migrate engine or re-introduce language-specific DDL. Wrapped in backticks. */
|
|
167
|
+
function storageTypeCell(field: MetaField): string {
|
|
168
|
+
const dbColumnType = field.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
169
|
+
if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
|
|
170
|
+
return `\`${dbColumnType.toUpperCase()}\``;
|
|
155
171
|
}
|
|
156
|
-
return
|
|
172
|
+
return `\`${neutralTypeStr(field)}\``;
|
|
157
173
|
}
|
|
158
174
|
|
|
159
|
-
|
|
175
|
+
/** Build one neutral Constraints-table row for a field. Reuses the same
|
|
176
|
+
* per-field constraint logic as `constraintsCell()` (required-ness, maxLength,
|
|
177
|
+
* enum CHECK-sets, validators, default, unique, references), but splits the
|
|
178
|
+
* facts across the Required / Limits / Rules columns instead of one cell.
|
|
179
|
+
* Renders for every field, with or without storage. */
|
|
180
|
+
function buildConstraintRow(
|
|
160
181
|
entity: MetaObject,
|
|
161
182
|
field: MetaField,
|
|
162
183
|
pkFieldNames: Set<string>,
|
|
163
184
|
fkMap: Map<string, { targetEntity: string; targetField: string }>,
|
|
164
|
-
):
|
|
165
|
-
const
|
|
185
|
+
): ConstraintRow {
|
|
186
|
+
const isPk = pkFieldNames.has(field.name);
|
|
187
|
+
const required = isPk || isFieldRequired(field);
|
|
166
188
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
189
|
+
const limits: string[] = [];
|
|
190
|
+
const rules: string[] = [];
|
|
191
|
+
|
|
192
|
+
if (isPk) rules.push("primary key");
|
|
193
|
+
if (field.ownAttr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
|
|
194
|
+
|
|
195
|
+
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
196
|
+
const values = enumValues(field);
|
|
197
|
+
if (values !== undefined && values.length > 0) {
|
|
198
|
+
const list = values.map((v) => `\`${v}\``).join(", ");
|
|
199
|
+
rules.push(`one of ${list}`);
|
|
173
200
|
}
|
|
174
|
-
} else if (isFieldRequired(field)) {
|
|
175
|
-
parts.push("required");
|
|
176
|
-
} else {
|
|
177
|
-
parts.push("optional");
|
|
178
201
|
}
|
|
179
202
|
|
|
180
|
-
|
|
181
|
-
|
|
203
|
+
// Same validator facts as constraintsCell() (shared walk), arranged across
|
|
204
|
+
// the Limits / Rules columns instead of one cell.
|
|
205
|
+
const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
|
|
206
|
+
rules.push(...regexParts);
|
|
207
|
+
if (maxLenAttr !== undefined) limits.push(`maxLength: ${maxLenAttr}`);
|
|
208
|
+
limits.push(...lengthParts, ...numericParts);
|
|
209
|
+
|
|
210
|
+
const fk = fkMap.get(field.name);
|
|
211
|
+
if (fk !== undefined) {
|
|
212
|
+
rules.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const def = field.ownAttr(FIELD_ATTR_DEFAULT);
|
|
216
|
+
if (def !== undefined) rules.push(`default: \`${String(def)}\``);
|
|
217
|
+
|
|
218
|
+
const sup = field.resolveSuper();
|
|
219
|
+
if (sup !== undefined) rules.push(`extends \`${sup.name}\``);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
// The Field cell carries a stable HTML anchor (`<a id="field-<name>">`)
|
|
223
|
+
// before the backticked name, so the template-source annotator's
|
|
224
|
+
// `#field-<name>` links resolve. Slug = `fieldAnchorSlug(name)` — the SINGLE
|
|
225
|
+
// source shared with the annotator so anchor and link can't drift. The
|
|
226
|
+
// anchor is a language-independent HTML id, so the page stays neutral.
|
|
227
|
+
field: `${fieldAnchorHtml(field.name)}\`${field.name}\``,
|
|
228
|
+
required: required ? "yes" : "",
|
|
229
|
+
type: neutralTypeCell(field),
|
|
230
|
+
limits: limits.join(", "),
|
|
231
|
+
rules: rules.join(", "),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Build one row of the unified Fields table — collapses the per-field facts
|
|
236
|
+
* the old Storage + Constraints tables split between into a single Markdown
|
|
237
|
+
* row. PK/FK key role becomes a glyph prefix on the Field cell, the FK
|
|
238
|
+
* target becomes a `→ \`Target\`` suffix on the Type cell, the @column
|
|
239
|
+
* override (only when interesting) lands in the Storage cell, everything
|
|
240
|
+
* else (validators, defaults, enum CHECK-sets, references, unique, extends)
|
|
241
|
+
* goes into the Rules cell joined by " · ".
|
|
242
|
+
*
|
|
243
|
+
* Identity bullets remain a separate section above — they describe the
|
|
244
|
+
* *identity declarations* (composite keys, generation strategy, reference
|
|
245
|
+
* topology), not the per-field facts. */
|
|
246
|
+
function buildFieldRow(
|
|
247
|
+
entity: MetaObject,
|
|
248
|
+
field: MetaField,
|
|
249
|
+
pkFieldNames: Set<string>,
|
|
250
|
+
fkMap: Map<string, { targetEntity: string; targetField: string }>,
|
|
251
|
+
): FieldDoc {
|
|
252
|
+
const isPk = pkFieldNames.has(field.name);
|
|
253
|
+
const fk = fkMap.get(field.name);
|
|
254
|
+
const required = isPk || isFieldRequired(field);
|
|
255
|
+
|
|
256
|
+
// Field cell — anchor + glyph + name.
|
|
257
|
+
let glyph = "";
|
|
258
|
+
if (isPk) glyph = "🔑 ";
|
|
259
|
+
else if (fk !== undefined) glyph = "🔗 ";
|
|
260
|
+
const fieldCell = `${fieldAnchorHtml(field.name)}${glyph}\`${field.name}\``;
|
|
261
|
+
|
|
262
|
+
// Type cell — neutral logical type; for FK, append the target as a link.
|
|
263
|
+
let typeCell = neutralTypeCell(field);
|
|
264
|
+
if (fk !== undefined) {
|
|
265
|
+
typeCell = `${typeCell} → \`${fk.targetEntity}\``;
|
|
182
266
|
}
|
|
183
267
|
|
|
184
|
-
|
|
185
|
-
|
|
268
|
+
// Storage cell — only populated when interesting:
|
|
269
|
+
// - @column override that differs from the field name, OR
|
|
270
|
+
// - @dbColumnType physical override set
|
|
271
|
+
// Otherwise empty. Keeps the column noise-free for the 90% case where
|
|
272
|
+
// field name and column name agree.
|
|
273
|
+
const columnName = field.column;
|
|
274
|
+
const dbColumnType = field.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
275
|
+
const columnDiffers = typeof columnName === "string" && columnName !== field.name;
|
|
276
|
+
const hasPhysicalOverride = typeof dbColumnType === "string" && dbColumnType.length > 0;
|
|
277
|
+
let storageCell = "";
|
|
278
|
+
if (columnDiffers && hasPhysicalOverride) {
|
|
279
|
+
storageCell = `\`${columnName}\` \`${dbColumnType!.toUpperCase()}\``;
|
|
280
|
+
} else if (columnDiffers) {
|
|
281
|
+
storageCell = `\`${columnName}\``;
|
|
282
|
+
} else if (hasPhysicalOverride) {
|
|
283
|
+
storageCell = `\`${dbColumnType!.toUpperCase()}\``;
|
|
186
284
|
}
|
|
187
285
|
|
|
286
|
+
// Rules cell — joined facts. Same logic as buildConstraintRow's Rules
|
|
287
|
+
// column, plus the maxLength/length/numeric limits that used to live in
|
|
288
|
+
// the separate Limits cell (collapsed in to keep the table to 5 columns).
|
|
289
|
+
const rules: string[] = [];
|
|
290
|
+
if (field.ownAttr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
|
|
291
|
+
|
|
188
292
|
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
189
293
|
const values = enumValues(field);
|
|
190
294
|
if (values !== undefined && values.length > 0) {
|
|
191
|
-
const list = values.map((v) =>
|
|
192
|
-
|
|
295
|
+
const list = values.map((v) => `\`${v}\``).join(", ");
|
|
296
|
+
rules.push(`one of ${list}`);
|
|
193
297
|
}
|
|
194
298
|
}
|
|
195
299
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
300
|
+
const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
|
|
301
|
+
rules.push(...regexParts);
|
|
302
|
+
if (maxLenAttr !== undefined) rules.push(`maxLength: ${maxLenAttr}`);
|
|
303
|
+
rules.push(...lengthParts, ...numericParts);
|
|
304
|
+
|
|
305
|
+
// The FK reference is already encoded in typeCell — don't repeat it in rules.
|
|
306
|
+
const def = field.ownAttr(FIELD_ATTR_DEFAULT);
|
|
307
|
+
if (def !== undefined) rules.push(`default: \`${String(def)}\``);
|
|
308
|
+
|
|
309
|
+
const sup = field.resolveSuper();
|
|
310
|
+
if (sup !== undefined) rules.push(`extends \`${sup.name}\``);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
field: field.name,
|
|
314
|
+
fieldCell,
|
|
315
|
+
typeCell,
|
|
316
|
+
requiredCell: required ? "yes" : "",
|
|
317
|
+
storageCell,
|
|
318
|
+
rulesCell: rules.join(" · "),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Build an expanded per-field detail block — `### \`name\`` heading, italic
|
|
323
|
+
* @summary lead-in, @description paragraph, then a bullet list of every
|
|
324
|
+
* notable rule (validators, default, FK, extends-enum, column override).
|
|
325
|
+
* Returns `undefined` when the field has nothing extra to say (just type +
|
|
326
|
+
* required) — the caller filters these out so the section stays tight.
|
|
327
|
+
*
|
|
328
|
+
* The validator list is the most important value-add: the Fields table
|
|
329
|
+
* collapses `pattern \`X\` · maxLength: 200 · minLength: 3` into a single
|
|
330
|
+
* Rules cell; this section breaks them out as individual bullets so the
|
|
331
|
+
* reader can scan each rule on its own line. */
|
|
332
|
+
function buildFieldDetail(
|
|
333
|
+
field: MetaField,
|
|
334
|
+
pkFieldNames: Set<string>,
|
|
335
|
+
fkMap: Map<string, { targetEntity: string; targetField: string }>,
|
|
336
|
+
): FieldDetailDoc | undefined {
|
|
337
|
+
const desc = field.attr(DOC_ATTR_DESCRIPTION);
|
|
338
|
+
const summary = field.attr(DOC_ATTR_SUMMARY);
|
|
339
|
+
const hasDesc = typeof desc === "string" && desc.length > 0;
|
|
340
|
+
const hasSummary = typeof summary === "string" && summary.length > 0;
|
|
341
|
+
const sup = field.resolveSuper();
|
|
342
|
+
const fk = fkMap.get(field.name);
|
|
343
|
+
const def = field.ownAttr(FIELD_ATTR_DEFAULT);
|
|
344
|
+
const columnName = field.column;
|
|
345
|
+
const dbColumnType = field.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
346
|
+
const isUnique = field.ownAttr(FIELD_ATTR_UNIQUE) === true;
|
|
347
|
+
const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.isArray;
|
|
348
|
+
const enumVals = isEnum ? enumValues(field) : undefined;
|
|
349
|
+
const validators = field.validators();
|
|
350
|
+
const hasValidatorChildren = validators.some(
|
|
351
|
+
v => v.subType === VALIDATOR_SUBTYPE_LENGTH
|
|
352
|
+
|| v.subType === VALIDATOR_SUBTYPE_REGEX
|
|
353
|
+
|| v.subType === VALIDATOR_SUBTYPE_NUMERIC,
|
|
354
|
+
);
|
|
200
355
|
const maxLenAttr = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
356
|
+
|
|
357
|
+
// "Interesting enough to render a detail block" predicate. Plain typed
|
|
358
|
+
// fields with no authored annotations get skipped — the at-a-glance Fields
|
|
359
|
+
// table covered them already.
|
|
360
|
+
//
|
|
361
|
+
// Deliberately NOT counted as "interesting":
|
|
362
|
+
// - PK / required-ness (already a column in the table)
|
|
363
|
+
// - mechanical @column overrides (adopters typically set
|
|
364
|
+
// @column: PascalCase(name) wholesale; surfacing every field for
|
|
365
|
+
// that alone would defeat the section's purpose)
|
|
366
|
+
// The detail section's value is surfacing AUTHORED docs + validators +
|
|
367
|
+
// business rules, not physical column mapping.
|
|
368
|
+
const isInteresting =
|
|
369
|
+
hasDesc
|
|
370
|
+
|| hasSummary
|
|
371
|
+
|| sup !== undefined
|
|
372
|
+
|| fk !== undefined
|
|
373
|
+
|| def !== undefined
|
|
374
|
+
|| hasValidatorChildren
|
|
375
|
+
|| typeof maxLenAttr === "number"
|
|
376
|
+
|| (enumVals !== undefined && enumVals.length > 0)
|
|
377
|
+
|| isUnique
|
|
378
|
+
|| (typeof dbColumnType === "string" && dbColumnType.length > 0);
|
|
379
|
+
if (!isInteresting) return undefined;
|
|
380
|
+
|
|
381
|
+
const parts: string[] = [`### \`${field.name}\``];
|
|
382
|
+
|
|
383
|
+
if (hasSummary) {
|
|
384
|
+
parts.push("");
|
|
385
|
+
parts.push(`*${summary as string}*`);
|
|
386
|
+
}
|
|
387
|
+
if (hasDesc) {
|
|
388
|
+
parts.push("");
|
|
389
|
+
parts.push(String(desc).trim());
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Bullet list — one fact per line. Order: type → FK → required/PK → column
|
|
393
|
+
// → default → unique → extends → enum values → validators.
|
|
394
|
+
const bullets: string[] = [];
|
|
395
|
+
bullets.push(`**Type:** ${neutralTypeCell(field)}`);
|
|
396
|
+
if (fk !== undefined) {
|
|
397
|
+
bullets.push(`**References:** [\`${fk.targetEntity}.${fk.targetField}\`](${fk.targetEntity}.md)`);
|
|
398
|
+
}
|
|
399
|
+
if (pkFieldNames.has(field.name)) {
|
|
400
|
+
bullets.push("**Primary key**");
|
|
401
|
+
} else if (isFieldRequired(field)) {
|
|
402
|
+
bullets.push("**Required**");
|
|
403
|
+
}
|
|
404
|
+
if (typeof columnName === "string" && columnName !== field.name) {
|
|
405
|
+
bullets.push(`**Column:** \`${columnName}\``);
|
|
406
|
+
}
|
|
407
|
+
if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
|
|
408
|
+
bullets.push(`**Physical type:** \`${dbColumnType.toUpperCase()}\``);
|
|
409
|
+
}
|
|
410
|
+
if (def !== undefined) {
|
|
411
|
+
bullets.push(`**Default:** \`${String(def)}\``);
|
|
412
|
+
}
|
|
413
|
+
if (isUnique) bullets.push("**Unique**");
|
|
414
|
+
if (sup !== undefined) {
|
|
415
|
+
// The postprocess script rewrites `extends \`Name\`` → enum anchor link.
|
|
416
|
+
bullets.push(`**Extends:** \`${sup.name}\``);
|
|
417
|
+
}
|
|
418
|
+
if (enumVals !== undefined && enumVals.length > 0) {
|
|
419
|
+
const vals = enumVals.map((v) => `\`${v}\``).join(" · ");
|
|
420
|
+
bullets.push(`**Enum values:** ${vals}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Validators — one bullet per validator subtype (regex / length / numeric),
|
|
424
|
+
// rendered in declaration order so authors can rely on the order they
|
|
425
|
+
// wrote.
|
|
426
|
+
for (const v of validators) {
|
|
205
427
|
if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
206
428
|
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
207
429
|
if (typeof pattern === "string" && pattern.length > 0) {
|
|
208
|
-
|
|
430
|
+
bullets.push(`**Validator (regex):** pattern \`${pattern}\``);
|
|
209
431
|
}
|
|
210
432
|
} else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
211
433
|
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
212
434
|
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
213
|
-
|
|
214
|
-
if (typeof
|
|
435
|
+
const fragments: string[] = [];
|
|
436
|
+
if (typeof min === "number") fragments.push(`min ${min}`);
|
|
437
|
+
if (typeof max === "number") fragments.push(`max ${max}`);
|
|
438
|
+
if (fragments.length > 0) bullets.push(`**Validator (length):** ${fragments.join(", ")}`);
|
|
215
439
|
} else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
|
|
216
440
|
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
217
441
|
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
218
|
-
|
|
219
|
-
if (typeof
|
|
442
|
+
const fragments: string[] = [];
|
|
443
|
+
if (typeof min === "number") fragments.push(`min ${min}`);
|
|
444
|
+
if (typeof max === "number") fragments.push(`max ${max}`);
|
|
445
|
+
if (fragments.length > 0) bullets.push(`**Validator (numeric):** ${fragments.join(", ")}`);
|
|
220
446
|
}
|
|
221
447
|
}
|
|
222
|
-
|
|
448
|
+
// @maxLength is the shorthand; render alongside validators for consistency.
|
|
223
449
|
if (typeof maxLenAttr === "number") {
|
|
224
|
-
|
|
450
|
+
bullets.push(`**Max length:** ${maxLenAttr}`);
|
|
225
451
|
}
|
|
226
|
-
parts.push(...lengthParts, ...numericParts);
|
|
227
452
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
parts.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const def = field.ownAttr(FIELD_ATTR_DEFAULT);
|
|
234
|
-
if (def !== undefined) {
|
|
235
|
-
parts.push(`default: \`${String(def)}\``);
|
|
236
|
-
}
|
|
453
|
+
parts.push("");
|
|
454
|
+
for (const b of bullets) parts.push(`- ${b}`);
|
|
237
455
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
parts.
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return parts.join(", ");
|
|
456
|
+
return {
|
|
457
|
+
field: field.name,
|
|
458
|
+
block: parts.join("\n"),
|
|
459
|
+
};
|
|
244
460
|
}
|
|
245
461
|
|
|
246
462
|
function buildFkMap(
|
|
@@ -275,6 +491,11 @@ function entityDescription(entity: MetaObject): string | undefined {
|
|
|
275
491
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
276
492
|
}
|
|
277
493
|
|
|
494
|
+
function entitySummary(entity: MetaObject): string | undefined {
|
|
495
|
+
const v = entity.attr(DOC_ATTR_SUMMARY);
|
|
496
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
278
499
|
function describeIdentity(id: MetaIdentity): string {
|
|
279
500
|
const fields = id.fields;
|
|
280
501
|
const fieldList = fields.length === 1
|
|
@@ -315,6 +536,27 @@ function relationshipBullet(r: ReturnType<MetaObject["relationships"]>[number]):
|
|
|
315
536
|
case RELATIONSHIP_SUBTYPE_ASSOCIATION: label = "association"; break;
|
|
316
537
|
default: label = subtype;
|
|
317
538
|
}
|
|
539
|
+
|
|
540
|
+
// M:N (FR-018): the relationship traverses a junction (`@through`). Describe
|
|
541
|
+
// the edge as related-target THROUGH junction, and mark the self-join shape:
|
|
542
|
+
// symmetric (undirected) → "symmetric self-join"
|
|
543
|
+
// @sourceRefField set (directed) → "directed self-join via `<field>`"
|
|
544
|
+
// The junction/disambiguator are DECLARED facts (ADR-0020 — no re-derivation).
|
|
545
|
+
const throughRaw = r.ownAttr(RELATIONSHIP_ATTR_THROUGH);
|
|
546
|
+
if (typeof throughRaw === "string" && throughRaw.length > 0) {
|
|
547
|
+
const through = stripPackage(throughRaw);
|
|
548
|
+
const noteParts = [`${label}, through \`${through}\``];
|
|
549
|
+
if (r.ownAttr(RELATIONSHIP_ATTR_SYMMETRIC) === true) {
|
|
550
|
+
noteParts.push("symmetric self-join");
|
|
551
|
+
} else {
|
|
552
|
+
const srcRef = r.ownAttr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
553
|
+
if (typeof srcRef === "string" && srcRef.length > 0) {
|
|
554
|
+
noteParts.push(`directed self-join via \`${srcRef}\``);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return `\`${r.name}\` — ${card} → \`${target}\` (${noteParts.join(", ")})`;
|
|
558
|
+
}
|
|
559
|
+
|
|
318
560
|
return `\`${r.name}\` — ${card} → \`${target}\` (${label})`;
|
|
319
561
|
}
|
|
320
562
|
|
|
@@ -325,28 +567,47 @@ export function buildEntityDocData(
|
|
|
325
567
|
entity: MetaObject,
|
|
326
568
|
opts: BuildDocDataOpts,
|
|
327
569
|
): EntityDocData {
|
|
328
|
-
const strategy = opts.columnNamingStrategy ?? "snake_case";
|
|
329
570
|
const root = opts.loadedRoot;
|
|
571
|
+
const layout = opts.layout ?? "flat";
|
|
330
572
|
const primary = entity.primaryIdentity();
|
|
331
573
|
const pkFields = primary?.fields ?? [];
|
|
332
574
|
const pkFieldNames = new Set<string>(pkFields);
|
|
333
575
|
const fkMap = buildFkMap(entity, root);
|
|
334
576
|
|
|
335
|
-
// ---- Storage rows
|
|
577
|
+
// ---- Storage rows — NEUTRAL physical persistence MAPPING (ADR-0020): the
|
|
578
|
+
// physical column name, a neutral physical type (declared `@dbColumnType`
|
|
579
|
+
// override else the logical type), nullability, and the key role. NO
|
|
580
|
+
// TypeScript type, NO ORM DDL, NO ANSI re-derivation — declared metadata
|
|
581
|
+
// facts only. The value-add over the Constraints table is the field→column
|
|
582
|
+
// mapping + physical-type override + key role.
|
|
336
583
|
const storageRows: StorageFieldDoc[] = entity.fields().map((field) => {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
584
|
+
const isPk = pkFieldNames.has(field.name);
|
|
585
|
+
// Physical column name: the field's `@column` override if set, else the
|
|
586
|
+
// field name. (The Storage section shows the RAW declared mapping; column
|
|
587
|
+
// naming-strategy folding stays a codegen concern, not a docs fact.)
|
|
588
|
+
const columnName = field.column ?? field.name;
|
|
589
|
+
const columnCell = `\`${columnName}\``;
|
|
590
|
+
const typeCell = storageTypeCell(field);
|
|
591
|
+
// Nullable iff not required and not the PK (matches the Constraints table's
|
|
592
|
+
// required-ness rule).
|
|
593
|
+
const nullable = !(isPk || isFieldRequired(field));
|
|
594
|
+
const nullableCell = nullable ? "yes" : "no";
|
|
595
|
+
|
|
596
|
+
let keyCell = "";
|
|
597
|
+
if (isPk) {
|
|
598
|
+
keyCell = "primary key";
|
|
599
|
+
} else {
|
|
600
|
+
const fk = fkMap.get(field.name);
|
|
601
|
+
if (fk !== undefined) keyCell = `foreign key → \`${fk.targetEntity}\``;
|
|
602
|
+
}
|
|
603
|
+
|
|
344
604
|
return {
|
|
345
605
|
name: field.name,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
606
|
+
columnCell,
|
|
607
|
+
typeCell,
|
|
608
|
+
nullableCell,
|
|
609
|
+
keyCell,
|
|
610
|
+
rowLine: `| ${columnCell} | ${typeCell} | ${nullableCell} | ${keyCell} |`,
|
|
350
611
|
};
|
|
351
612
|
});
|
|
352
613
|
|
|
@@ -365,13 +626,40 @@ export function buildEntityDocData(
|
|
|
365
626
|
? rels.map((r) => ({ bullet: relationshipBullet(r) }))
|
|
366
627
|
: undefined;
|
|
367
628
|
|
|
368
|
-
// ----
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
629
|
+
// ---- Constraints (NEUTRAL — built from the object's OWN field metadata, so
|
|
630
|
+
// it renders for every object including value objects with no storage).
|
|
631
|
+
// KEPT FOR BACK-COMPAT — new templates render `fields` instead.
|
|
632
|
+
const constraintRows: ConstraintRow[] = entity
|
|
633
|
+
.fields()
|
|
634
|
+
.map((field) => buildConstraintRow(entity, field, pkFieldNames, fkMap));
|
|
635
|
+
const constraints = {
|
|
636
|
+
hasConstraints: constraintRows.length > 0,
|
|
637
|
+
rows: constraintRows,
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// ---- Fields (merged Storage + Constraints) — the single per-field table
|
|
641
|
+
// the new entity-page template renders. Same source of truth as the two
|
|
642
|
+
// legacy tables, just folded into one row.
|
|
643
|
+
const fieldRows: FieldDoc[] = entity
|
|
644
|
+
.fields()
|
|
645
|
+
.map((field) => buildFieldRow(entity, field, pkFieldNames, fkMap));
|
|
646
|
+
const fields = {
|
|
647
|
+
hasFields: fieldRows.length > 0,
|
|
648
|
+
rows: fieldRows,
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// ---- Field details — expanded per-field section, skipping plain fields
|
|
652
|
+
// that the at-a-glance table already covered. The deeper "field details
|
|
653
|
+
// below the table" pattern adopted by Stripe / FHIR / GraphQL — keep the
|
|
654
|
+
// table tight, surface authoring + validation depth below.
|
|
655
|
+
const fieldDetailRows: FieldDetailDoc[] = [];
|
|
656
|
+
for (const field of entity.fields()) {
|
|
657
|
+
const detail = buildFieldDetail(field, pkFieldNames, fkMap);
|
|
658
|
+
if (detail !== undefined) fieldDetailRows.push(detail);
|
|
659
|
+
}
|
|
660
|
+
const fieldDetails = {
|
|
661
|
+
hasDetails: fieldDetailRows.length > 0,
|
|
662
|
+
rows: fieldDetailRows,
|
|
375
663
|
};
|
|
376
664
|
|
|
377
665
|
// ---- UsedBy
|
|
@@ -381,39 +669,17 @@ export function buildEntityDocData(
|
|
|
381
669
|
const ref = child.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
382
670
|
if (typeof ref !== "string") continue;
|
|
383
671
|
if (stripPackage(ref) !== entity.name) continue;
|
|
672
|
+
// Link to the template's own doc page. The href is derived from the SAME
|
|
673
|
+
// page-placement function used to write the template page, so it resolves
|
|
674
|
+
// in BOTH layouts (flat → `./<Tmpl>.md`; package → a correct relative path
|
|
675
|
+
// like `../comms/OrderEmail.md`).
|
|
676
|
+
const href = docPageHref(layout, docPageNode(entity), docPageNode(child));
|
|
384
677
|
usedByMatches.push({
|
|
385
|
-
bullet:
|
|
678
|
+
bullet: `[\`template.${child.subType} ${child.name}\`](${href}) — uses \`${entity.name}\` as \`@payloadRef\``,
|
|
386
679
|
});
|
|
387
680
|
}
|
|
388
681
|
const usedBy = usedByMatches.length > 0 ? usedByMatches : undefined;
|
|
389
682
|
|
|
390
|
-
// ---- Generated
|
|
391
|
-
const gens = opts.generatorNames ?? new Set<string>();
|
|
392
|
-
const generated: GeneratedFileDoc[] = [];
|
|
393
|
-
generated.push({
|
|
394
|
-
filename: `${entity.name}.ts`,
|
|
395
|
-
description: "Drizzle table, Zod schemas, type aliases, enum literal unions.",
|
|
396
|
-
});
|
|
397
|
-
if (gens.has("queries-file") && !isValue) {
|
|
398
|
-
generated.push({
|
|
399
|
-
filename: `${entity.name}.queries.ts`,
|
|
400
|
-
description:
|
|
401
|
-
"typed CRUD helpers (find / list / create / update / delete; takes `db` as first param per ADR-0008).",
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
if (gens.has("routes-file") && !isValue) {
|
|
405
|
-
generated.push({
|
|
406
|
-
filename: `${entity.name}.routes.ts`,
|
|
407
|
-
description: `Fastify CRUD-5 route registration (\`register${entity.name}Routes\`).`,
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
if (gens.has("routes-file-hono") && !isValue) {
|
|
411
|
-
generated.push({
|
|
412
|
-
filename: `${entity.name}.routes.hono.ts`,
|
|
413
|
-
description: `Hono CRUD-5 route registration (\`register${entity.name}Routes\`).`,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
683
|
// Preamble header — built up exactly as the legacy emitter did.
|
|
418
684
|
const preambleLines: string[] = [];
|
|
419
685
|
const typeStr = `${entity.type}.${entity.subType}`;
|
|
@@ -432,6 +698,11 @@ export function buildEntityDocData(
|
|
|
432
698
|
descriptionQuote = desc.split("\n").map((l) => `> ${l}`.trimEnd()).join("\n");
|
|
433
699
|
}
|
|
434
700
|
|
|
701
|
+
// Summary — short single-line tagline. Rendered as italic lead-in just under
|
|
702
|
+
// the H1, ABOVE @description. Distinct enough that an entity can carry both
|
|
703
|
+
// (description = paragraph; summary = headline).
|
|
704
|
+
const summary = entitySummary(entity);
|
|
705
|
+
|
|
435
706
|
const data: EntityDocData = {
|
|
436
707
|
generatedMarker: `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`,
|
|
437
708
|
entity: {
|
|
@@ -439,12 +710,29 @@ export function buildEntityDocData(
|
|
|
439
710
|
type: typeStr,
|
|
440
711
|
},
|
|
441
712
|
preambleHeader,
|
|
442
|
-
|
|
443
|
-
|
|
713
|
+
fields,
|
|
714
|
+
fieldDetails,
|
|
715
|
+
constraints,
|
|
444
716
|
};
|
|
445
717
|
|
|
446
718
|
if (desc !== undefined) data.entity.description = desc;
|
|
719
|
+
if (summary !== undefined) {
|
|
720
|
+
data.entity.summary = summary;
|
|
721
|
+
data.summaryLead = `*${summary}*`;
|
|
722
|
+
}
|
|
447
723
|
if (descriptionQuote !== undefined) data.descriptionQuote = descriptionQuote;
|
|
724
|
+
|
|
725
|
+
// 1-hop neighborhood diagram — every entity it FKs into + every entity that
|
|
726
|
+
// FKs into it. Rendered just above the Relationships section in the entity
|
|
727
|
+
// page template. Skipped when the entity has zero neighbors (no orphan
|
|
728
|
+
// empty diagram block).
|
|
729
|
+
const neighborhoodErBlock = hasStorage
|
|
730
|
+
? renderEntityNeighborhoodErBlock(entity, root)
|
|
731
|
+
: undefined;
|
|
732
|
+
if (neighborhoodErBlock !== undefined) {
|
|
733
|
+
data.neighborhoodErBlock = neighborhoodErBlock;
|
|
734
|
+
data.hasNeighborhoodEr = true;
|
|
735
|
+
}
|
|
448
736
|
if (src !== undefined) data.entity.source = src;
|
|
449
737
|
if (entity.package !== undefined && entity.package !== "") {
|
|
450
738
|
data.entity.package = entity.package;
|
|
@@ -452,7 +740,7 @@ export function buildEntityDocData(
|
|
|
452
740
|
|
|
453
741
|
if (hasStorage) {
|
|
454
742
|
data.storage = {
|
|
455
|
-
tableHeader: "|
|
|
743
|
+
tableHeader: "| Column | Type | Nullable | Key |\n|---|---|---|---|",
|
|
456
744
|
rows: storageRows,
|
|
457
745
|
};
|
|
458
746
|
data.hasStorage = true;
|
|
@@ -469,6 +757,12 @@ export function buildEntityDocData(
|
|
|
469
757
|
data.usedBy = usedBy;
|
|
470
758
|
data.hasUsedBy = true;
|
|
471
759
|
}
|
|
760
|
+
// Cross-link to the api surfaces — present ONLY when the caller computed the
|
|
761
|
+
// hrefs (api surfaces emitted alongside model); model-only runs stay identical.
|
|
762
|
+
// `last` flags the final ref so the template renders an inline ` · ` separator.
|
|
763
|
+
if (opts.apiRefs !== undefined) {
|
|
764
|
+
data.apiRefs = opts.apiRefs.map((r, i, arr) => ({ ...r, last: i === arr.length - 1 }));
|
|
765
|
+
}
|
|
472
766
|
|
|
473
767
|
return data;
|
|
474
768
|
}
|