@metaobjectsdev/codegen-ts 0.9.0 → 0.11.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/column-mapper.d.ts +12 -6
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +68 -28
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -1
- package/dist/docs-paths.d.ts +58 -0
- package/dist/docs-paths.d.ts.map +1 -0
- package/dist/docs-paths.js +89 -0
- package/dist/docs-paths.js.map +1 -0
- package/dist/enum-import.d.ts +14 -0
- package/dist/enum-import.d.ts.map +1 -0
- package/dist/enum-import.js +35 -0
- package/dist/enum-import.js.map +1 -0
- package/dist/enum-shared.d.ts +32 -0
- package/dist/enum-shared.d.ts.map +1 -0
- package/dist/enum-shared.js +83 -0
- package/dist/enum-shared.js.map +1 -0
- package/dist/generator-registry.d.ts +22 -0
- package/dist/generator-registry.d.ts.map +1 -0
- package/dist/generator-registry.js +161 -0
- package/dist/generator-registry.js.map +1 -0
- package/dist/generator.d.ts +6 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/api-doc-render.d.ts +17 -0
- package/dist/generators/api-doc-render.d.ts.map +1 -0
- package/dist/generators/api-doc-render.js +431 -0
- package/dist/generators/api-doc-render.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts +21 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -0
- package/dist/generators/api-docs-file.js +112 -0
- package/dist/generators/api-docs-file.js.map +1 -0
- package/dist/generators/api-field-shape.d.ts +39 -0
- package/dist/generators/api-field-shape.d.ts.map +1 -0
- package/dist/generators/api-field-shape.js +92 -0
- package/dist/generators/api-field-shape.js.map +1 -0
- package/dist/generators/api-label.d.ts +3 -0
- package/dist/generators/api-label.d.ts.map +1 -0
- package/dist/generators/api-label.js +8 -0
- package/dist/generators/api-label.js.map +1 -0
- package/dist/generators/api-model.d.ts +122 -0
- package/dist/generators/api-model.d.ts.map +1 -0
- package/dist/generators/api-model.js +809 -0
- package/dist/generators/api-model.js.map +1 -0
- package/dist/generators/docs-data-builder.d.ts +26 -4
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +439 -167
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +136 -27
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/docs-data.js +1 -1
- package/dist/generators/docs-data.js.map +1 -1
- package/dist/generators/docs-file.d.ts +19 -0
- package/dist/generators/docs-file.d.ts.map +1 -1
- package/dist/generators/docs-file.js +154 -27
- package/dist/generators/docs-file.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +29 -14
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +2 -1
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/field-anchor.d.ts +7 -0
- package/dist/generators/field-anchor.d.ts.map +1 -0
- package/dist/generators/field-anchor.js +23 -0
- package/dist/generators/field-anchor.js.map +1 -0
- package/dist/generators/index.d.ts +8 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +6 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/mermaid-er.d.ts +14 -0
- package/dist/generators/mermaid-er.d.ts.map +1 -1
- package/dist/generators/mermaid-er.js +14 -0
- package/dist/generators/mermaid-er.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +3 -4
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/output-prompt-file.d.ts.map +1 -1
- package/dist/generators/output-prompt-file.js +2 -2
- package/dist/generators/output-prompt-file.js.map +1 -1
- package/dist/generators/prompt-render-file.d.ts.map +1 -1
- package/dist/generators/prompt-render-file.js +3 -4
- package/dist/generators/prompt-render-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +8 -3
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts.map +1 -1
- package/dist/generators/render-helper-file.js +2 -2
- package/dist/generators/render-helper-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +5 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +3 -0
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +6 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-doc-builder.d.ts +19 -0
- package/dist/generators/template-doc-builder.d.ts.map +1 -0
- package/dist/generators/template-doc-builder.js +220 -0
- package/dist/generators/template-doc-builder.js.map +1 -0
- package/dist/generators/template-doc-data.d.ts +62 -0
- package/dist/generators/template-doc-data.d.ts.map +1 -0
- package/dist/generators/template-doc-data.js +16 -0
- package/dist/generators/template-doc-data.js.map +1 -0
- package/dist/generators/template-payload-tree.d.ts +15 -0
- package/dist/generators/template-payload-tree.d.ts.map +1 -0
- package/dist/generators/template-payload-tree.js +61 -0
- package/dist/generators/template-payload-tree.js.map +1 -0
- package/dist/generators/template-source-annotate.d.ts +74 -0
- package/dist/generators/template-source-annotate.d.ts.map +1 -0
- package/dist/generators/template-source-annotate.js +184 -0
- package/dist/generators/template-source-annotate.js.map +1 -0
- package/dist/generators/template-source-render.d.ts +24 -0
- package/dist/generators/template-source-render.d.ts.map +1 -0
- package/dist/generators/template-source-render.js +175 -0
- package/dist/generators/template-source-render.js.map +1 -0
- package/dist/generators/trace-helper-file.d.ts +9 -0
- package/dist/generators/trace-helper-file.d.ts.map +1 -0
- package/dist/generators/trace-helper-file.js +196 -0
- package/dist/generators/trace-helper-file.js.map +1 -0
- package/dist/import-path.d.ts +18 -0
- package/dist/import-path.d.ts.map +1 -1
- package/dist/import-path.js +21 -0
- package/dist/import-path.js.map +1 -1
- package/dist/index.d.ts +29 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -2
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +101 -2
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +46 -0
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/naming.d.ts +39 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +52 -3
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +14 -6
- package/dist/payload-codegen.js.map +1 -1
- package/dist/pk-resolver.d.ts.map +1 -1
- package/dist/pk-resolver.js +4 -2
- package/dist/pk-resolver.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +52 -26
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/relation-resolver.d.ts +16 -0
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +82 -1
- package/dist/relation-resolver.js.map +1 -1
- package/dist/render-context.d.ts +25 -2
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +7 -0
- package/dist/render-context.js.map +1 -1
- package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
- package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
- package/dist/render-engine/embedded-templates.generated.js +15 -0
- package/dist/render-engine/embedded-templates.generated.js.map +1 -0
- package/dist/render-engine/framework-provider.d.ts.map +1 -1
- package/dist/render-engine/framework-provider.js +26 -13
- package/dist/render-engine/framework-provider.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +20 -0
- package/dist/runner.js.map +1 -1
- package/dist/templates/docs-file.d.ts +2 -6
- package/dist/templates/docs-file.d.ts.map +1 -1
- package/dist/templates/docs-file.js +2 -5
- package/dist/templates/docs-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +72 -23
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +7 -0
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +3 -3
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +16 -5
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/enums-file.d.ts +11 -0
- package/dist/templates/enums-file.d.ts.map +1 -0
- package/dist/templates/enums-file.js +44 -0
- package/dist/templates/enums-file.js.map +1 -0
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +6 -8
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +58 -41
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +2 -6
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-allowlist.d.ts +7 -2
- package/dist/templates/filter-allowlist.d.ts.map +1 -1
- package/dist/templates/filter-allowlist.js +18 -10
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-shared.js +2 -2
- package/dist/templates/filter-shared.js.map +1 -1
- package/dist/templates/filter-type.d.ts +7 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +10 -6
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/find-templates.d.ts +4 -0
- package/dist/templates/find-templates.d.ts.map +1 -0
- package/dist/templates/find-templates.js +15 -0
- package/dist/templates/find-templates.js.map +1 -0
- package/dist/templates/fr010-field-mapping.d.ts +2 -0
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +15 -11
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +44 -7
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +121 -19
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/mermaid-er.d.ts +35 -2
- package/dist/templates/mermaid-er.d.ts.map +1 -1
- package/dist/templates/mermaid-er.js +174 -7
- package/dist/templates/mermaid-er.js.map +1 -1
- package/dist/templates/output-format-spec-emitter.js +1 -1
- package/dist/templates/output-format-spec-emitter.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +31 -80
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.d.ts.map +1 -1
- package/dist/templates/output-prompt.js +2 -2
- package/dist/templates/output-prompt.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +113 -5
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +7 -2
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +15 -15
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +12 -3
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +5 -5
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +1 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +184 -7
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/tph-discriminator.d.ts +56 -0
- package/dist/templates/tph-discriminator.d.ts.map +1 -0
- package/dist/templates/tph-discriminator.js +180 -0
- package/dist/templates/tph-discriminator.js.map +1 -0
- package/dist/templates/value-object-file.d.ts +2 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +33 -5
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +65 -2
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +202 -22
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +103 -34
- package/src/column-mapper.ts +79 -32
- package/src/constants.ts +18 -0
- package/src/docs-paths.ts +128 -0
- package/src/enum-import.ts +43 -0
- package/src/enum-shared.ts +95 -0
- package/src/generator-registry.ts +204 -0
- package/src/generator.ts +6 -0
- package/src/generators/api-doc-render.ts +572 -0
- package/src/generators/api-docs-file.ts +146 -0
- package/src/generators/api-field-shape.ts +114 -0
- package/src/generators/api-label.ts +7 -0
- package/src/generators/api-model.ts +1067 -0
- package/src/generators/docs-data-builder.ts +483 -189
- package/src/generators/docs-data.ts +139 -28
- package/src/generators/docs-file.ts +205 -39
- package/src/generators/entity-file.ts +31 -15
- package/src/generators/extractor-file.ts +2 -1
- package/src/generators/field-anchor.ts +24 -0
- package/src/generators/index.ts +8 -1
- package/src/generators/mermaid-er.ts +14 -0
- package/src/generators/output-parser-file.ts +3 -4
- package/src/generators/output-prompt-file.ts +2 -1
- package/src/generators/prompt-render-file.ts +3 -4
- package/src/generators/queries-file.ts +9 -3
- package/src/generators/render-helper-file.ts +2 -1
- package/src/generators/routes-file-hono.ts +5 -1
- package/src/generators/routes-file.ts +7 -1
- package/src/generators/template-doc-builder.ts +306 -0
- package/src/generators/template-doc-data.ts +85 -0
- package/src/generators/template-payload-tree.ts +71 -0
- package/src/generators/template-source-annotate.ts +290 -0
- package/src/generators/template-source-render.ts +203 -0
- package/src/generators/trace-helper-file.ts +301 -0
- package/src/import-path.ts +28 -0
- package/src/index.ts +55 -4
- package/src/metaobjects-config.ts +146 -2
- package/src/naming.ts +73 -3
- package/src/payload-codegen.ts +16 -5
- package/src/pk-resolver.ts +4 -2
- package/src/projection/extract-view-spec.ts +50 -31
- package/src/relation-resolver.ts +103 -1
- package/src/render-context.ts +32 -2
- package/src/render-engine/embedded-templates.generated.ts +14 -0
- package/src/render-engine/framework-provider.ts +25 -11
- package/src/runner.ts +24 -0
- package/src/templates/docs-file.ts +2 -9
- package/src/templates/drizzle-schema.ts +80 -28
- package/src/templates/entity-constants.ts +3 -3
- package/src/templates/entity-file.ts +16 -5
- package/src/templates/enums-file.ts +50 -0
- package/src/templates/extract-delegate-emitter.ts +6 -7
- package/src/templates/extractor.ts +70 -40
- package/src/templates/field-meta.ts +1 -7
- package/src/templates/filter-allowlist.ts +18 -11
- package/src/templates/filter-shared.ts +2 -2
- package/src/templates/filter-type.ts +9 -7
- package/src/templates/find-templates.ts +15 -0
- package/src/templates/fr010-field-mapping.ts +15 -13
- package/src/templates/inferred-types.ts +122 -21
- package/src/templates/mermaid-er.ts +176 -8
- package/src/templates/output-format-spec-emitter.ts +1 -1
- package/src/templates/output-parser.ts +31 -80
- package/src/templates/output-prompt.ts +2 -1
- package/src/templates/queries-file.ts +133 -4
- package/src/templates/queries.ts +21 -15
- package/src/templates/relations-block.ts +19 -3
- package/src/templates/render-helper.ts +5 -4
- package/src/templates/routes-file-hono.ts +1 -2
- package/src/templates/routes-file.ts +234 -7
- package/src/templates/tph-discriminator.ts +232 -0
- package/src/templates/value-object-file.ts +39 -5
- package/src/templates/zod-validators.ts +225 -21
- package/templates/api/agent-api.md.mustache +30 -0
- package/templates/api/entity-api.md.mustache +69 -0
- package/templates/api/index.md.mustache +21 -0
- package/templates/docs/entity-page.md.mustache +33 -21
- package/templates/docs/template-page.md.mustache +56 -0
- package/dist/templates/extract-schema-emitter.d.ts +0 -8
- package/dist/templates/extract-schema-emitter.d.ts.map +0 -1
- package/dist/templates/extract-schema-emitter.js +0 -81
- package/dist/templates/extract-schema-emitter.js.map +0 -1
- package/src/templates/extract-schema-emitter.ts +0 -111
|
@@ -2,166 +2,363 @@
|
|
|
2
2
|
// templates consume. The previous hand-coded `renderDocsFile()` mixed data
|
|
3
3
|
// extraction with string emission; this module is the data-only half — the
|
|
4
4
|
// markdown structure now lives in templates/docs/entity-page.md.mustache.
|
|
5
|
-
import { TYPE_TEMPLATE, TEMPLATE_ATTR_PAYLOAD_REF, OBJECT_SUBTYPE_VALUE, IDENTITY_SUBTYPE_PRIMARY, IDENTITY_SUBTYPE_SECONDARY, IDENTITY_SUBTYPE_REFERENCE, IDENTITY_ATTR_GENERATION, RELATIONSHIP_ATTR_CARDINALITY, RELATIONSHIP_ATTR_OBJECT_REF,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { TYPE_TEMPLATE, TEMPLATE_ATTR_PAYLOAD_REF, OBJECT_SUBTYPE_VALUE, IDENTITY_SUBTYPE_PRIMARY, IDENTITY_SUBTYPE_SECONDARY, IDENTITY_SUBTYPE_REFERENCE, IDENTITY_ATTR_GENERATION, RELATIONSHIP_ATTR_CARDINALITY, RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH, RELATIONSHIP_ATTR_SOURCE_REF_FIELD, RELATIONSHIP_ATTR_SYMMETRIC, RELATIONSHIP_SUBTYPE_COMPOSITION, RELATIONSHIP_SUBTYPE_AGGREGATION, RELATIONSHIP_SUBTYPE_ASSOCIATION, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_OBJECT, FIELD_ATTR_REQUIRED, FIELD_ATTR_UNIQUE, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX, VALIDATOR_SUBTYPE_NUMERIC, VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_ATTR_PATTERN, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, DOC_ATTR_DESCRIPTION, DOC_ATTR_SUMMARY, FIELD_ATTR_DB_COLUMN_TYPE, stripPackage, } from "@metaobjectsdev/metadata";
|
|
6
|
+
import { docPageHref, docPageNode } from "../docs-paths.js";
|
|
7
|
+
import { fieldAnchorHtml } from "./field-anchor.js";
|
|
8
8
|
import { enumValues } from "../enum-meta.js";
|
|
9
9
|
import { hasWritableRdbSource } from "../source-detect.js";
|
|
10
10
|
import { GENERATED_HEADER } from "../constants.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[FIELD_SUBTYPE_DOUBLE]: "number",
|
|
20
|
-
[FIELD_SUBTYPE_FLOAT]: "number",
|
|
21
|
-
// field.decimal is precision-exact: surfaced as a TS `string` (Drizzle pg
|
|
22
|
-
// `numeric` infers `string`). Keep the docs scalar mapping in lockstep.
|
|
23
|
-
[FIELD_SUBTYPE_DECIMAL]: "string",
|
|
24
|
-
[FIELD_SUBTYPE_CURRENCY]: "number",
|
|
25
|
-
[FIELD_SUBTYPE_BOOLEAN]: "boolean",
|
|
26
|
-
[FIELD_SUBTYPE_DATE]: "string",
|
|
27
|
-
[FIELD_SUBTYPE_TIME]: "string",
|
|
28
|
-
[FIELD_SUBTYPE_TIMESTAMP]: "string",
|
|
29
|
-
};
|
|
30
|
-
function enumTypeAliasName(entity, field) {
|
|
31
|
-
const superField = field.resolveSuper();
|
|
32
|
-
return superField !== undefined
|
|
33
|
-
? toPascalCase(superField.name)
|
|
34
|
-
: `${entity.name}${toPascalCase(field.name)}`;
|
|
35
|
-
}
|
|
36
|
-
function isFieldRequired(field) {
|
|
37
|
-
if (field.ownAttr(FIELD_ATTR_REQUIRED) === true)
|
|
11
|
+
import { renderEntityNeighborhoodErBlock } from "../templates/mermaid-er.js";
|
|
12
|
+
/** Whether a field is required — `@required` true OR a `validator.required`
|
|
13
|
+
* child. The SINGLE source of truth for required-ness across the Constraints
|
|
14
|
+
* table, the Storage nullable rule, and (via the api-docs field-shape builder)
|
|
15
|
+
* the documented model-field optionality. Exported so the field-shape builder
|
|
16
|
+
* reuses the EXACT same rule rather than re-deriving it. */
|
|
17
|
+
export function isFieldRequired(field) {
|
|
18
|
+
if (field.attr(FIELD_ATTR_REQUIRED) === true)
|
|
38
19
|
return true;
|
|
39
20
|
return field.validators().some((v) => v.subType === VALIDATOR_SUBTYPE_REQUIRED);
|
|
40
21
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
22
|
+
function collectValidatorParts(field) {
|
|
23
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
24
|
+
const regexParts = [];
|
|
25
|
+
const lengthParts = [];
|
|
26
|
+
const numericParts = [];
|
|
27
|
+
for (const v of field.validators()) {
|
|
28
|
+
if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
29
|
+
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
30
|
+
if (typeof pattern === "string" && pattern.length > 0) {
|
|
31
|
+
regexParts.push(`pattern \`${pattern}\``);
|
|
51
32
|
}
|
|
52
33
|
}
|
|
53
|
-
else {
|
|
54
|
-
|
|
34
|
+
else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
35
|
+
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
36
|
+
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
37
|
+
if (typeof min === "number")
|
|
38
|
+
lengthParts.push(`minLength: ${min}`);
|
|
39
|
+
if (typeof max === "number" && typeof maxLenAttr !== "number")
|
|
40
|
+
lengthParts.push(`maxLength: ${max}`);
|
|
41
|
+
}
|
|
42
|
+
else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
|
|
43
|
+
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
44
|
+
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
45
|
+
if (typeof min === "number")
|
|
46
|
+
numericParts.push(`min: ${min}`);
|
|
47
|
+
if (typeof max === "number")
|
|
48
|
+
numericParts.push(`max: ${max}`);
|
|
55
49
|
}
|
|
56
50
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
return {
|
|
52
|
+
maxLenAttr: typeof maxLenAttr === "number" ? maxLenAttr : undefined,
|
|
53
|
+
regexParts,
|
|
54
|
+
lengthParts,
|
|
55
|
+
numericParts,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The NEUTRAL logical type string (no backticks): the field's logical
|
|
59
|
+
* subtype (e.g. `string`, `enum`, `decimal`), suffixed `[]` for arrays, and
|
|
60
|
+
* the referenced object name for `field.object`. Language-agnostic — built
|
|
61
|
+
* from declared metadata, never re-derived into ANSI/ORM SQL. Shared by the
|
|
62
|
+
* Constraints table (`neutralTypeCell`) and the Storage table's physical-type
|
|
63
|
+
* fallback (`storageTypeCell`). */
|
|
64
|
+
export function neutralTypeStr(field) {
|
|
65
|
+
let base;
|
|
66
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
67
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
68
|
+
base = typeof ref === "string" && ref.length > 0 ? stripPackage(ref) : "object";
|
|
61
69
|
}
|
|
62
70
|
else {
|
|
63
|
-
|
|
64
|
-
base = field.isArray ? `${scalar}[]` : scalar;
|
|
71
|
+
base = field.subType;
|
|
65
72
|
}
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
if (field.isArray)
|
|
74
|
+
base = `${base}[]`;
|
|
75
|
+
return base;
|
|
68
76
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
/** Neutral logical type cell for the Constraints table — `neutralTypeStr`
|
|
78
|
+
* wrapped in backticks. */
|
|
79
|
+
function neutralTypeCell(field) {
|
|
80
|
+
return `\`${neutralTypeStr(field)}\``;
|
|
81
|
+
}
|
|
82
|
+
/** Neutral PHYSICAL type cell for the Storage table. Metadata-driven, no DDL
|
|
83
|
+
* re-derivation (ADR-0020): if the field declares a `@dbColumnType` physical
|
|
84
|
+
* override (e.g. `uuid`, `jsonb`, `timestamp_with_tz`) show it UPPERCASED;
|
|
85
|
+
* otherwise fall back to the same neutral LOGICAL type the Constraints table
|
|
86
|
+
* uses. Deliberately does NOT derive ANSI/ORM SQL so it can't drift vs the
|
|
87
|
+
* migrate engine or re-introduce language-specific DDL. Wrapped in backticks. */
|
|
88
|
+
function storageTypeCell(field) {
|
|
89
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
90
|
+
if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
|
|
91
|
+
return `\`${dbColumnType.toUpperCase()}\``;
|
|
92
|
+
}
|
|
93
|
+
return `\`${neutralTypeStr(field)}\``;
|
|
94
|
+
}
|
|
95
|
+
/** Build one neutral Constraints-table row for a field. Reuses the same
|
|
96
|
+
* per-field constraint logic as `constraintsCell()` (required-ness, maxLength,
|
|
97
|
+
* enum CHECK-sets, validators, default, unique, references), but splits the
|
|
98
|
+
* facts across the Required / Limits / Rules columns instead of one cell.
|
|
99
|
+
* Renders for every field, with or without storage. */
|
|
100
|
+
function buildConstraintRow(entity, field, pkFieldNames, fkMap) {
|
|
101
|
+
const isPk = pkFieldNames.has(field.name);
|
|
102
|
+
const required = isPk || isFieldRequired(field);
|
|
103
|
+
const limits = [];
|
|
104
|
+
const rules = [];
|
|
105
|
+
if (isPk)
|
|
106
|
+
rules.push("primary key");
|
|
107
|
+
if (field.attr(FIELD_ATTR_UNIQUE) === true)
|
|
108
|
+
rules.push("unique");
|
|
109
|
+
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
110
|
+
const values = enumValues(field);
|
|
111
|
+
if (values !== undefined && values.length > 0) {
|
|
112
|
+
const list = values.map((v) => `\`${v}\``).join(", ");
|
|
113
|
+
rules.push(`one of ${list}`);
|
|
81
114
|
}
|
|
82
|
-
return `${spec.fnName}(${dbName}, { ${parts.join(", ")} })`;
|
|
83
115
|
}
|
|
84
|
-
|
|
116
|
+
// Same validator facts as constraintsCell() (shared walk), arranged across
|
|
117
|
+
// the Limits / Rules columns instead of one cell.
|
|
118
|
+
const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
|
|
119
|
+
rules.push(...regexParts);
|
|
120
|
+
if (maxLenAttr !== undefined)
|
|
121
|
+
limits.push(`maxLength: ${maxLenAttr}`);
|
|
122
|
+
limits.push(...lengthParts, ...numericParts);
|
|
123
|
+
const fk = fkMap.get(field.name);
|
|
124
|
+
if (fk !== undefined) {
|
|
125
|
+
rules.push(`references \`${fk.targetEntity}.${fk.targetField}\``);
|
|
126
|
+
}
|
|
127
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
128
|
+
if (def !== undefined)
|
|
129
|
+
rules.push(`default: \`${String(def)}\``);
|
|
130
|
+
const sup = field.resolveSuper();
|
|
131
|
+
if (sup !== undefined)
|
|
132
|
+
rules.push(`extends \`${sup.name}\``);
|
|
133
|
+
return {
|
|
134
|
+
// The Field cell carries a stable HTML anchor (`<a id="field-<name>">`)
|
|
135
|
+
// before the backticked name, so the template-source annotator's
|
|
136
|
+
// `#field-<name>` links resolve. Slug = `fieldAnchorSlug(name)` — the SINGLE
|
|
137
|
+
// source shared with the annotator so anchor and link can't drift. The
|
|
138
|
+
// anchor is a language-independent HTML id, so the page stays neutral.
|
|
139
|
+
field: `${fieldAnchorHtml(field.name)}\`${field.name}\``,
|
|
140
|
+
required: required ? "yes" : "",
|
|
141
|
+
type: neutralTypeCell(field),
|
|
142
|
+
limits: limits.join(", "),
|
|
143
|
+
rules: rules.join(", "),
|
|
144
|
+
};
|
|
85
145
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
146
|
+
/** Build one row of the unified Fields table — collapses the per-field facts
|
|
147
|
+
* the old Storage + Constraints tables split between into a single Markdown
|
|
148
|
+
* row. PK/FK key role becomes a glyph prefix on the Field cell, the FK
|
|
149
|
+
* target becomes a `→ \`Target\`` suffix on the Type cell, the @column
|
|
150
|
+
* override (only when interesting) lands in the Storage cell, everything
|
|
151
|
+
* else (validators, defaults, enum CHECK-sets, references, unique, extends)
|
|
152
|
+
* goes into the Rules cell joined by " · ".
|
|
153
|
+
*
|
|
154
|
+
* Identity bullets remain a separate section above — they describe the
|
|
155
|
+
* *identity declarations* (composite keys, generation strategy, reference
|
|
156
|
+
* topology), not the per-field facts. */
|
|
157
|
+
function buildFieldRow(entity, field, pkFieldNames, fkMap) {
|
|
158
|
+
const isPk = pkFieldNames.has(field.name);
|
|
159
|
+
const fk = fkMap.get(field.name);
|
|
160
|
+
const required = isPk || isFieldRequired(field);
|
|
161
|
+
// Field cell — anchor + glyph + name.
|
|
162
|
+
let glyph = "";
|
|
163
|
+
if (isPk)
|
|
164
|
+
glyph = "🔑 ";
|
|
165
|
+
else if (fk !== undefined)
|
|
166
|
+
glyph = "🔗 ";
|
|
167
|
+
const fieldCell = `${fieldAnchorHtml(field.name)}${glyph}\`${field.name}\``;
|
|
168
|
+
// Type cell — neutral logical type; for FK, append the target as a link.
|
|
169
|
+
let typeCell = neutralTypeCell(field);
|
|
170
|
+
if (fk !== undefined) {
|
|
171
|
+
typeCell = `${typeCell} → \`${fk.targetEntity}\``;
|
|
172
|
+
}
|
|
173
|
+
// Storage cell — only populated when interesting:
|
|
174
|
+
// - @column override that differs from the field name, OR
|
|
175
|
+
// - @dbColumnType physical override set
|
|
176
|
+
// Otherwise empty. Keeps the column noise-free for the 90% case where
|
|
177
|
+
// field name and column name agree.
|
|
178
|
+
const columnName = field.column;
|
|
179
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
180
|
+
const columnDiffers = typeof columnName === "string" && columnName !== field.name;
|
|
181
|
+
const hasPhysicalOverride = typeof dbColumnType === "string" && dbColumnType.length > 0;
|
|
182
|
+
let storageCell = "";
|
|
183
|
+
if (columnDiffers && hasPhysicalOverride) {
|
|
184
|
+
storageCell = `\`${columnName}\` \`${dbColumnType.toUpperCase()}\``;
|
|
185
|
+
}
|
|
186
|
+
else if (columnDiffers) {
|
|
187
|
+
storageCell = `\`${columnName}\``;
|
|
188
|
+
}
|
|
189
|
+
else if (hasPhysicalOverride) {
|
|
190
|
+
storageCell = `\`${dbColumnType.toUpperCase()}\``;
|
|
191
|
+
}
|
|
192
|
+
// Rules cell — joined facts. Same logic as buildConstraintRow's Rules
|
|
193
|
+
// column, plus the maxLength/length/numeric limits that used to live in
|
|
194
|
+
// the separate Limits cell (collapsed in to keep the table to 5 columns).
|
|
195
|
+
const rules = [];
|
|
196
|
+
if (field.attr(FIELD_ATTR_UNIQUE) === true)
|
|
197
|
+
rules.push("unique");
|
|
198
|
+
if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
|
|
199
|
+
const values = enumValues(field);
|
|
200
|
+
if (values !== undefined && values.length > 0) {
|
|
201
|
+
const list = values.map((v) => `\`${v}\``).join(", ");
|
|
202
|
+
rules.push(`one of ${list}`);
|
|
94
203
|
}
|
|
95
204
|
}
|
|
205
|
+
const { maxLenAttr, regexParts, lengthParts, numericParts } = collectValidatorParts(field);
|
|
206
|
+
rules.push(...regexParts);
|
|
207
|
+
if (maxLenAttr !== undefined)
|
|
208
|
+
rules.push(`maxLength: ${maxLenAttr}`);
|
|
209
|
+
rules.push(...lengthParts, ...numericParts);
|
|
210
|
+
// The FK reference is already encoded in typeCell — don't repeat it in rules.
|
|
211
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
212
|
+
if (def !== undefined)
|
|
213
|
+
rules.push(`default: \`${String(def)}\``);
|
|
214
|
+
const sup = field.resolveSuper();
|
|
215
|
+
if (sup !== undefined)
|
|
216
|
+
rules.push(`extends \`${sup.name}\``);
|
|
217
|
+
return {
|
|
218
|
+
field: field.name,
|
|
219
|
+
fieldCell,
|
|
220
|
+
typeCell,
|
|
221
|
+
requiredCell: required ? "yes" : "",
|
|
222
|
+
storageCell,
|
|
223
|
+
rulesCell: rules.join(" · "),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Build an expanded per-field detail block — `### \`name\`` heading, italic
|
|
227
|
+
* @summary lead-in, @description paragraph, then a bullet list of every
|
|
228
|
+
* notable rule (validators, default, FK, extends-enum, column override).
|
|
229
|
+
* Returns `undefined` when the field has nothing extra to say (just type +
|
|
230
|
+
* required) — the caller filters these out so the section stays tight.
|
|
231
|
+
*
|
|
232
|
+
* The validator list is the most important value-add: the Fields table
|
|
233
|
+
* collapses `pattern \`X\` · maxLength: 200 · minLength: 3` into a single
|
|
234
|
+
* Rules cell; this section breaks them out as individual bullets so the
|
|
235
|
+
* reader can scan each rule on its own line. */
|
|
236
|
+
function buildFieldDetail(field, pkFieldNames, fkMap) {
|
|
237
|
+
const desc = field.attr(DOC_ATTR_DESCRIPTION);
|
|
238
|
+
const summary = field.attr(DOC_ATTR_SUMMARY);
|
|
239
|
+
const hasDesc = typeof desc === "string" && desc.length > 0;
|
|
240
|
+
const hasSummary = typeof summary === "string" && summary.length > 0;
|
|
241
|
+
const sup = field.resolveSuper();
|
|
242
|
+
const fk = fkMap.get(field.name);
|
|
243
|
+
const def = field.attr(FIELD_ATTR_DEFAULT);
|
|
244
|
+
const columnName = field.column;
|
|
245
|
+
const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
246
|
+
const isUnique = field.attr(FIELD_ATTR_UNIQUE) === true;
|
|
247
|
+
const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.isArray;
|
|
248
|
+
const enumVals = isEnum ? enumValues(field) : undefined;
|
|
249
|
+
const validators = field.validators();
|
|
250
|
+
const hasValidatorChildren = validators.some(v => v.subType === VALIDATOR_SUBTYPE_LENGTH
|
|
251
|
+
|| v.subType === VALIDATOR_SUBTYPE_REGEX
|
|
252
|
+
|| v.subType === VALIDATOR_SUBTYPE_NUMERIC);
|
|
253
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
254
|
+
// "Interesting enough to render a detail block" predicate. Plain typed
|
|
255
|
+
// fields with no authored annotations get skipped — the at-a-glance Fields
|
|
256
|
+
// table covered them already.
|
|
257
|
+
//
|
|
258
|
+
// Deliberately NOT counted as "interesting":
|
|
259
|
+
// - PK / required-ness (already a column in the table)
|
|
260
|
+
// - mechanical @column overrides (adopters typically set
|
|
261
|
+
// @column: PascalCase(name) wholesale; surfacing every field for
|
|
262
|
+
// that alone would defeat the section's purpose)
|
|
263
|
+
// The detail section's value is surfacing AUTHORED docs + validators +
|
|
264
|
+
// business rules, not physical column mapping.
|
|
265
|
+
const isInteresting = hasDesc
|
|
266
|
+
|| hasSummary
|
|
267
|
+
|| sup !== undefined
|
|
268
|
+
|| fk !== undefined
|
|
269
|
+
|| def !== undefined
|
|
270
|
+
|| hasValidatorChildren
|
|
271
|
+
|| typeof maxLenAttr === "number"
|
|
272
|
+
|| (enumVals !== undefined && enumVals.length > 0)
|
|
273
|
+
|| isUnique
|
|
274
|
+
|| (typeof dbColumnType === "string" && dbColumnType.length > 0);
|
|
275
|
+
if (!isInteresting)
|
|
276
|
+
return undefined;
|
|
277
|
+
const parts = [`### \`${field.name}\``];
|
|
278
|
+
if (hasSummary) {
|
|
279
|
+
parts.push("");
|
|
280
|
+
parts.push(`*${summary}*`);
|
|
281
|
+
}
|
|
282
|
+
if (hasDesc) {
|
|
283
|
+
parts.push("");
|
|
284
|
+
parts.push(String(desc).trim());
|
|
285
|
+
}
|
|
286
|
+
// Bullet list — one fact per line. Order: type → FK → required/PK → column
|
|
287
|
+
// → default → unique → extends → enum values → validators.
|
|
288
|
+
const bullets = [];
|
|
289
|
+
bullets.push(`**Type:** ${neutralTypeCell(field)}`);
|
|
290
|
+
if (fk !== undefined) {
|
|
291
|
+
bullets.push(`**References:** [\`${fk.targetEntity}.${fk.targetField}\`](${fk.targetEntity}.md)`);
|
|
292
|
+
}
|
|
293
|
+
if (pkFieldNames.has(field.name)) {
|
|
294
|
+
bullets.push("**Primary key**");
|
|
295
|
+
}
|
|
96
296
|
else if (isFieldRequired(field)) {
|
|
97
|
-
|
|
297
|
+
bullets.push("**Required**");
|
|
98
298
|
}
|
|
99
|
-
|
|
100
|
-
|
|
299
|
+
if (typeof columnName === "string" && columnName !== field.name) {
|
|
300
|
+
bullets.push(`**Column:** \`${columnName}\``);
|
|
101
301
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
302
|
+
if (typeof dbColumnType === "string" && dbColumnType.length > 0) {
|
|
303
|
+
bullets.push(`**Physical type:** \`${dbColumnType.toUpperCase()}\``);
|
|
104
304
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
305
|
+
if (def !== undefined) {
|
|
306
|
+
bullets.push(`**Default:** \`${String(def)}\``);
|
|
107
307
|
}
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
308
|
+
if (isUnique)
|
|
309
|
+
bullets.push("**Unique**");
|
|
310
|
+
if (sup !== undefined) {
|
|
311
|
+
// The postprocess script rewrites `extends \`Name\`` → enum anchor link.
|
|
312
|
+
bullets.push(`**Extends:** \`${sup.name}\``);
|
|
114
313
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
for (const v of field.validators()) {
|
|
314
|
+
if (enumVals !== undefined && enumVals.length > 0) {
|
|
315
|
+
const vals = enumVals.map((v) => `\`${v}\``).join(" · ");
|
|
316
|
+
bullets.push(`**Enum values:** ${vals}`);
|
|
317
|
+
}
|
|
318
|
+
// Validators — one bullet per validator subtype (regex / length / numeric),
|
|
319
|
+
// rendered in declaration order so authors can rely on the order they
|
|
320
|
+
// wrote.
|
|
321
|
+
for (const v of validators) {
|
|
124
322
|
if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
125
323
|
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
126
324
|
if (typeof pattern === "string" && pattern.length > 0) {
|
|
127
|
-
|
|
325
|
+
bullets.push(`**Validator (regex):** pattern \`${pattern}\``);
|
|
128
326
|
}
|
|
129
327
|
}
|
|
130
328
|
else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
131
329
|
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
132
330
|
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
331
|
+
const fragments = [];
|
|
133
332
|
if (typeof min === "number")
|
|
134
|
-
|
|
135
|
-
if (typeof max === "number"
|
|
136
|
-
|
|
333
|
+
fragments.push(`min ${min}`);
|
|
334
|
+
if (typeof max === "number")
|
|
335
|
+
fragments.push(`max ${max}`);
|
|
336
|
+
if (fragments.length > 0)
|
|
337
|
+
bullets.push(`**Validator (length):** ${fragments.join(", ")}`);
|
|
137
338
|
}
|
|
138
339
|
else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
|
|
139
340
|
const min = v.ownAttr(VALIDATOR_ATTR_MIN);
|
|
140
341
|
const max = v.ownAttr(VALIDATOR_ATTR_MAX);
|
|
342
|
+
const fragments = [];
|
|
141
343
|
if (typeof min === "number")
|
|
142
|
-
|
|
344
|
+
fragments.push(`min ${min}`);
|
|
143
345
|
if (typeof max === "number")
|
|
144
|
-
|
|
346
|
+
fragments.push(`max ${max}`);
|
|
347
|
+
if (fragments.length > 0)
|
|
348
|
+
bullets.push(`**Validator (numeric):** ${fragments.join(", ")}`);
|
|
145
349
|
}
|
|
146
350
|
}
|
|
147
|
-
|
|
351
|
+
// @maxLength is the shorthand; render alongside validators for consistency.
|
|
148
352
|
if (typeof maxLenAttr === "number") {
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
parts.push(
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
parts.push(`default: \`${String(def)}\``);
|
|
159
|
-
}
|
|
160
|
-
const sup = field.resolveSuper();
|
|
161
|
-
if (sup !== undefined) {
|
|
162
|
-
parts.push(`extends \`${sup.name}\``);
|
|
163
|
-
}
|
|
164
|
-
return parts.join(", ");
|
|
353
|
+
bullets.push(`**Max length:** ${maxLenAttr}`);
|
|
354
|
+
}
|
|
355
|
+
parts.push("");
|
|
356
|
+
for (const b of bullets)
|
|
357
|
+
parts.push(`- ${b}`);
|
|
358
|
+
return {
|
|
359
|
+
field: field.name,
|
|
360
|
+
block: parts.join("\n"),
|
|
361
|
+
};
|
|
165
362
|
}
|
|
166
363
|
function buildFkMap(entity, root) {
|
|
167
364
|
const out = new Map();
|
|
@@ -191,13 +388,17 @@ function entityDescription(entity) {
|
|
|
191
388
|
const v = entity.attr(DOC_ATTR_DESCRIPTION);
|
|
192
389
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
193
390
|
}
|
|
391
|
+
function entitySummary(entity) {
|
|
392
|
+
const v = entity.attr(DOC_ATTR_SUMMARY);
|
|
393
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
394
|
+
}
|
|
194
395
|
function describeIdentity(id) {
|
|
195
396
|
const fields = id.fields;
|
|
196
397
|
const fieldList = fields.length === 1
|
|
197
398
|
? `\`${fields[0]}\``
|
|
198
399
|
: `(${fields.map((f) => `\`${f}\``).join(", ")})`;
|
|
199
400
|
if (id.subType === IDENTITY_SUBTYPE_PRIMARY) {
|
|
200
|
-
const gen = id.
|
|
401
|
+
const gen = id.attr(IDENTITY_ATTR_GENERATION);
|
|
201
402
|
const genSuffix = typeof gen === "string" ? ` — generation: \`${gen}\`` : "";
|
|
202
403
|
return `**Primary key:** ${fieldList}${genSuffix}`;
|
|
203
404
|
}
|
|
@@ -235,33 +436,72 @@ function relationshipBullet(r) {
|
|
|
235
436
|
break;
|
|
236
437
|
default: label = subtype;
|
|
237
438
|
}
|
|
439
|
+
// M:N (FR-018): the relationship traverses a junction (`@through`). Describe
|
|
440
|
+
// the edge as related-target THROUGH junction, and mark the self-join shape:
|
|
441
|
+
// symmetric (undirected) → "symmetric self-join"
|
|
442
|
+
// @sourceRefField set (directed) → "directed self-join via `<field>`"
|
|
443
|
+
// The junction/disambiguator are DECLARED facts (ADR-0020 — no re-derivation).
|
|
444
|
+
const throughRaw = r.ownAttr(RELATIONSHIP_ATTR_THROUGH);
|
|
445
|
+
if (typeof throughRaw === "string" && throughRaw.length > 0) {
|
|
446
|
+
const through = stripPackage(throughRaw);
|
|
447
|
+
const noteParts = [`${label}, through \`${through}\``];
|
|
448
|
+
if (r.ownAttr(RELATIONSHIP_ATTR_SYMMETRIC) === true) {
|
|
449
|
+
noteParts.push("symmetric self-join");
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const srcRef = r.ownAttr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
453
|
+
if (typeof srcRef === "string" && srcRef.length > 0) {
|
|
454
|
+
noteParts.push(`directed self-join via \`${srcRef}\``);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return `\`${r.name}\` — ${card} → \`${target}\` (${noteParts.join(", ")})`;
|
|
458
|
+
}
|
|
238
459
|
return `\`${r.name}\` — ${card} → \`${target}\` (${label})`;
|
|
239
460
|
}
|
|
240
461
|
/** Build the EntityDocData payload for one entity. The single public-API
|
|
241
462
|
* entry point exported by this module; the markdown template applies
|
|
242
463
|
* against this shape. */
|
|
243
464
|
export function buildEntityDocData(entity, opts) {
|
|
244
|
-
const strategy = opts.columnNamingStrategy ?? "snake_case";
|
|
245
465
|
const root = opts.loadedRoot;
|
|
466
|
+
const layout = opts.layout ?? "flat";
|
|
246
467
|
const primary = entity.primaryIdentity();
|
|
247
468
|
const pkFields = primary?.fields ?? [];
|
|
248
469
|
const pkFieldNames = new Set(pkFields);
|
|
249
470
|
const fkMap = buildFkMap(entity, root);
|
|
250
|
-
// ---- Storage rows
|
|
471
|
+
// ---- Storage rows — NEUTRAL physical persistence MAPPING (ADR-0020): the
|
|
472
|
+
// physical column name, a neutral physical type (declared `@dbColumnType`
|
|
473
|
+
// override else the logical type), nullability, and the key role. NO
|
|
474
|
+
// TypeScript type, NO ORM DDL, NO ANSI re-derivation — declared metadata
|
|
475
|
+
// facts only. The value-add over the Constraints table is the field→column
|
|
476
|
+
// mapping + physical-type override + key role.
|
|
251
477
|
const storageRows = entity.fields().map((field) => {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
478
|
+
const isPk = pkFieldNames.has(field.name);
|
|
479
|
+
// Physical column name: the field's `@column` override if set, else the
|
|
480
|
+
// field name. (The Storage section shows the RAW declared mapping; column
|
|
481
|
+
// naming-strategy folding stays a codegen concern, not a docs fact.)
|
|
482
|
+
const columnName = field.column ?? field.name;
|
|
483
|
+
const columnCell = `\`${columnName}\``;
|
|
484
|
+
const typeCell = storageTypeCell(field);
|
|
485
|
+
// Nullable iff not required and not the PK (matches the Constraints table's
|
|
486
|
+
// required-ness rule).
|
|
487
|
+
const nullable = !(isPk || isFieldRequired(field));
|
|
488
|
+
const nullableCell = nullable ? "yes" : "no";
|
|
489
|
+
let keyCell = "";
|
|
490
|
+
if (isPk) {
|
|
491
|
+
keyCell = "primary key";
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
const fk = fkMap.get(field.name);
|
|
495
|
+
if (fk !== undefined)
|
|
496
|
+
keyCell = `foreign key → \`${fk.targetEntity}\``;
|
|
497
|
+
}
|
|
259
498
|
return {
|
|
260
499
|
name: field.name,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
500
|
+
columnCell,
|
|
501
|
+
typeCell,
|
|
502
|
+
nullableCell,
|
|
503
|
+
keyCell,
|
|
504
|
+
rowLine: `| ${columnCell} | ${typeCell} | ${nullableCell} | ${keyCell} |`,
|
|
265
505
|
};
|
|
266
506
|
});
|
|
267
507
|
const isValue = entity.subType === OBJECT_SUBTYPE_VALUE;
|
|
@@ -276,13 +516,39 @@ export function buildEntityDocData(entity, opts) {
|
|
|
276
516
|
const relationships = rels.length > 0
|
|
277
517
|
? rels.map((r) => ({ bullet: relationshipBullet(r) }))
|
|
278
518
|
: undefined;
|
|
279
|
-
// ----
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
519
|
+
// ---- Constraints (NEUTRAL — built from the object's OWN field metadata, so
|
|
520
|
+
// it renders for every object including value objects with no storage).
|
|
521
|
+
// KEPT FOR BACK-COMPAT — new templates render `fields` instead.
|
|
522
|
+
const constraintRows = entity
|
|
523
|
+
.fields()
|
|
524
|
+
.map((field) => buildConstraintRow(entity, field, pkFieldNames, fkMap));
|
|
525
|
+
const constraints = {
|
|
526
|
+
hasConstraints: constraintRows.length > 0,
|
|
527
|
+
rows: constraintRows,
|
|
528
|
+
};
|
|
529
|
+
// ---- Fields (merged Storage + Constraints) — the single per-field table
|
|
530
|
+
// the new entity-page template renders. Same source of truth as the two
|
|
531
|
+
// legacy tables, just folded into one row.
|
|
532
|
+
const fieldRows = entity
|
|
533
|
+
.fields()
|
|
534
|
+
.map((field) => buildFieldRow(entity, field, pkFieldNames, fkMap));
|
|
535
|
+
const fields = {
|
|
536
|
+
hasFields: fieldRows.length > 0,
|
|
537
|
+
rows: fieldRows,
|
|
538
|
+
};
|
|
539
|
+
// ---- Field details — expanded per-field section, skipping plain fields
|
|
540
|
+
// that the at-a-glance table already covered. The deeper "field details
|
|
541
|
+
// below the table" pattern adopted by Stripe / FHIR / GraphQL — keep the
|
|
542
|
+
// table tight, surface authoring + validation depth below.
|
|
543
|
+
const fieldDetailRows = [];
|
|
544
|
+
for (const field of entity.fields()) {
|
|
545
|
+
const detail = buildFieldDetail(field, pkFieldNames, fkMap);
|
|
546
|
+
if (detail !== undefined)
|
|
547
|
+
fieldDetailRows.push(detail);
|
|
548
|
+
}
|
|
549
|
+
const fieldDetails = {
|
|
550
|
+
hasDetails: fieldDetailRows.length > 0,
|
|
551
|
+
rows: fieldDetailRows,
|
|
286
552
|
};
|
|
287
553
|
// ---- UsedBy
|
|
288
554
|
const usedByMatches = [];
|
|
@@ -294,36 +560,16 @@ export function buildEntityDocData(entity, opts) {
|
|
|
294
560
|
continue;
|
|
295
561
|
if (stripPackage(ref) !== entity.name)
|
|
296
562
|
continue;
|
|
563
|
+
// Link to the template's own doc page. The href is derived from the SAME
|
|
564
|
+
// page-placement function used to write the template page, so it resolves
|
|
565
|
+
// in BOTH layouts (flat → `./<Tmpl>.md`; package → a correct relative path
|
|
566
|
+
// like `../comms/OrderEmail.md`).
|
|
567
|
+
const href = docPageHref(layout, docPageNode(entity), docPageNode(child));
|
|
297
568
|
usedByMatches.push({
|
|
298
|
-
bullet:
|
|
569
|
+
bullet: `[\`template.${child.subType} ${child.name}\`](${href}) — uses \`${entity.name}\` as \`@payloadRef\``,
|
|
299
570
|
});
|
|
300
571
|
}
|
|
301
572
|
const usedBy = usedByMatches.length > 0 ? usedByMatches : undefined;
|
|
302
|
-
// ---- Generated
|
|
303
|
-
const gens = opts.generatorNames ?? new Set();
|
|
304
|
-
const generated = [];
|
|
305
|
-
generated.push({
|
|
306
|
-
filename: `${entity.name}.ts`,
|
|
307
|
-
description: "Drizzle table, Zod schemas, type aliases, enum literal unions.",
|
|
308
|
-
});
|
|
309
|
-
if (gens.has("queries-file") && !isValue) {
|
|
310
|
-
generated.push({
|
|
311
|
-
filename: `${entity.name}.queries.ts`,
|
|
312
|
-
description: "typed CRUD helpers (find / list / create / update / delete; takes `db` as first param per ADR-0008).",
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
if (gens.has("routes-file") && !isValue) {
|
|
316
|
-
generated.push({
|
|
317
|
-
filename: `${entity.name}.routes.ts`,
|
|
318
|
-
description: `Fastify CRUD-5 route registration (\`register${entity.name}Routes\`).`,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
if (gens.has("routes-file-hono") && !isValue) {
|
|
322
|
-
generated.push({
|
|
323
|
-
filename: `${entity.name}.routes.hono.ts`,
|
|
324
|
-
description: `Hono CRUD-5 route registration (\`register${entity.name}Routes\`).`,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
573
|
// Preamble header — built up exactly as the legacy emitter did.
|
|
328
574
|
const preambleLines = [];
|
|
329
575
|
const typeStr = `${entity.type}.${entity.subType}`;
|
|
@@ -341,6 +587,10 @@ export function buildEntityDocData(entity, opts) {
|
|
|
341
587
|
if (desc !== undefined) {
|
|
342
588
|
descriptionQuote = desc.split("\n").map((l) => `> ${l}`.trimEnd()).join("\n");
|
|
343
589
|
}
|
|
590
|
+
// Summary — short single-line tagline. Rendered as italic lead-in just under
|
|
591
|
+
// the H1, ABOVE @description. Distinct enough that an entity can carry both
|
|
592
|
+
// (description = paragraph; summary = headline).
|
|
593
|
+
const summary = entitySummary(entity);
|
|
344
594
|
const data = {
|
|
345
595
|
generatedMarker: `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`,
|
|
346
596
|
entity: {
|
|
@@ -348,13 +598,29 @@ export function buildEntityDocData(entity, opts) {
|
|
|
348
598
|
type: typeStr,
|
|
349
599
|
},
|
|
350
600
|
preambleHeader,
|
|
351
|
-
|
|
352
|
-
|
|
601
|
+
fields,
|
|
602
|
+
fieldDetails,
|
|
603
|
+
constraints,
|
|
353
604
|
};
|
|
354
605
|
if (desc !== undefined)
|
|
355
606
|
data.entity.description = desc;
|
|
607
|
+
if (summary !== undefined) {
|
|
608
|
+
data.entity.summary = summary;
|
|
609
|
+
data.summaryLead = `*${summary}*`;
|
|
610
|
+
}
|
|
356
611
|
if (descriptionQuote !== undefined)
|
|
357
612
|
data.descriptionQuote = descriptionQuote;
|
|
613
|
+
// 1-hop neighborhood diagram — every entity it FKs into + every entity that
|
|
614
|
+
// FKs into it. Rendered just above the Relationships section in the entity
|
|
615
|
+
// page template. Skipped when the entity has zero neighbors (no orphan
|
|
616
|
+
// empty diagram block).
|
|
617
|
+
const neighborhoodErBlock = hasStorage
|
|
618
|
+
? renderEntityNeighborhoodErBlock(entity, root)
|
|
619
|
+
: undefined;
|
|
620
|
+
if (neighborhoodErBlock !== undefined) {
|
|
621
|
+
data.neighborhoodErBlock = neighborhoodErBlock;
|
|
622
|
+
data.hasNeighborhoodEr = true;
|
|
623
|
+
}
|
|
358
624
|
if (src !== undefined)
|
|
359
625
|
data.entity.source = src;
|
|
360
626
|
if (entity.package !== undefined && entity.package !== "") {
|
|
@@ -362,7 +628,7 @@ export function buildEntityDocData(entity, opts) {
|
|
|
362
628
|
}
|
|
363
629
|
if (hasStorage) {
|
|
364
630
|
data.storage = {
|
|
365
|
-
tableHeader: "|
|
|
631
|
+
tableHeader: "| Column | Type | Nullable | Key |\n|---|---|---|---|",
|
|
366
632
|
rows: storageRows,
|
|
367
633
|
};
|
|
368
634
|
data.hasStorage = true;
|
|
@@ -379,6 +645,12 @@ export function buildEntityDocData(entity, opts) {
|
|
|
379
645
|
data.usedBy = usedBy;
|
|
380
646
|
data.hasUsedBy = true;
|
|
381
647
|
}
|
|
648
|
+
// Cross-link to the api surfaces — present ONLY when the caller computed the
|
|
649
|
+
// hrefs (api surfaces emitted alongside model); model-only runs stay identical.
|
|
650
|
+
// `last` flags the final ref so the template renders an inline ` · ` separator.
|
|
651
|
+
if (opts.apiRefs !== undefined) {
|
|
652
|
+
data.apiRefs = opts.apiRefs.map((r, i, arr) => ({ ...r, last: i === arr.length - 1 }));
|
|
653
|
+
}
|
|
382
654
|
return data;
|
|
383
655
|
}
|
|
384
656
|
//# sourceMappingURL=docs-data-builder.js.map
|