@metaobjectsdev/codegen-ts 0.24.5 → 1.0.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 +37 -12
- package/dist/api-surface.d.ts +29 -0
- package/dist/api-surface.d.ts.map +1 -1
- package/dist/api-surface.js +47 -0
- package/dist/api-surface.js.map +1 -1
- package/dist/column-mapper.d.ts +12 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +1 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +24 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +54 -19
- package/dist/constants.js.map +1 -1
- package/dist/db-emitting.d.ts +6 -0
- package/dist/db-emitting.d.ts.map +1 -0
- package/dist/db-emitting.js +26 -0
- package/dist/db-emitting.js.map +1 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +14 -1
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +5 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts +19 -0
- package/dist/generators/agent-docs-file.d.ts.map +1 -0
- package/dist/generators/agent-docs-file.js +257 -0
- package/dist/generators/agent-docs-file.js.map +1 -0
- package/dist/generators/agent-requirements-page.d.ts +22 -0
- package/dist/generators/agent-requirements-page.d.ts.map +1 -0
- package/dist/generators/agent-requirements-page.js +175 -0
- package/dist/generators/agent-requirements-page.js.map +1 -0
- package/dist/generators/agent-schema-input.d.ts +69 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -0
- package/dist/generators/agent-schema-input.js +23 -0
- package/dist/generators/agent-schema-input.js.map +1 -0
- package/dist/generators/agent-schema-page.d.ts +21 -0
- package/dist/generators/agent-schema-page.d.ts.map +1 -0
- package/dist/generators/agent-schema-page.js +241 -0
- package/dist/generators/agent-schema-page.js.map +1 -0
- package/dist/generators/agent-ui-page.d.ts +22 -0
- package/dist/generators/agent-ui-page.d.ts.map +1 -0
- package/dist/generators/agent-ui-page.js +253 -0
- package/dist/generators/agent-ui-page.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -1
- package/dist/generators/api-docs-file.js +13 -6
- package/dist/generators/api-docs-file.js.map +1 -1
- package/dist/generators/api-model.d.ts +6 -0
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +54 -35
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/callable-file.js +1 -1
- package/dist/generators/callable-file.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +47 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +6 -2
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/index.d.ts +3 -8
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +19 -14
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/names-file.d.ts +17 -0
- package/dist/generators/names-file.d.ts.map +1 -0
- package/dist/generators/names-file.js +95 -0
- package/dist/generators/names-file.js.map +1 -0
- package/dist/generators/requirements-markdown.d.ts +18 -1
- package/dist/generators/requirements-markdown.d.ts.map +1 -1
- package/dist/generators/requirements-markdown.js +11 -9
- package/dist/generators/requirements-markdown.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +17 -2
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +9 -11
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +17 -2
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +10 -8
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +20 -6
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +38 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +9 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/names.d.ts +275 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +433 -0
- package/dist/names.js.map +1 -0
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +15 -3
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +25 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +79 -6
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/reference-templates.d.ts +1 -1
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +1 -1
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +12 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +1 -0
- package/dist/render-context.js.map +1 -1
- package/dist/retired-codegen-attrs.d.ts +32 -0
- package/dist/retired-codegen-attrs.d.ts.map +1 -0
- package/dist/retired-codegen-attrs.js +47 -0
- package/dist/retired-codegen-attrs.js.map +1 -0
- package/dist/routes-expose.d.ts +63 -0
- package/dist/routes-expose.d.ts.map +1 -0
- package/dist/routes-expose.js +57 -0
- package/dist/routes-expose.js.map +1 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +109 -7
- package/dist/runner.js.map +1 -1
- package/dist/templates/callable-file.d.ts +7 -2
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +54 -9
- package/dist/templates/callable-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +236 -65
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +15 -6
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +85 -161
- 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 +38 -4
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/entity-meta-file.d.ts +15 -1
- package/dist/templates/entity-meta-file.d.ts.map +1 -1
- package/dist/templates/entity-meta-file.js +12 -2
- package/dist/templates/entity-meta-file.js.map +1 -1
- package/dist/templates/entity-ui-descriptor.d.ts +112 -0
- package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
- package/dist/templates/entity-ui-descriptor.js +238 -0
- package/dist/templates/entity-ui-descriptor.js.map +1 -0
- package/dist/templates/field-meta.d.ts +46 -9
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +95 -21
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/names-decl.d.ts +38 -0
- package/dist/templates/names-decl.d.ts.map +1 -0
- package/dist/templates/names-decl.js +104 -0
- package/dist/templates/names-decl.js.map +1 -0
- package/dist/templates/projection-decl.d.ts +21 -0
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +53 -29
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +17 -2
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +13 -2
- package/dist/templates/requirement-test.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts +2 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +11 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts +2 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +54 -21
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +15 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +37 -1
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +27 -3
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +21 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +40 -5
- package/dist/templates/zod-validators.js.map +1 -1
- package/dist/view-context.d.ts +24 -0
- package/dist/view-context.d.ts.map +1 -0
- package/dist/view-context.js +81 -0
- package/dist/view-context.js.map +1 -0
- package/package.json +6 -6
- package/src/api-surface.ts +53 -0
- package/src/column-mapper.ts +13 -0
- package/src/constants.ts +91 -16
- package/src/db-emitting.ts +31 -0
- package/src/generator-registry.ts +14 -4
- package/src/generator.ts +5 -0
- package/src/generators/agent-docs-file.ts +297 -0
- package/src/generators/agent-requirements-page.ts +199 -0
- package/src/generators/agent-schema-input.ts +88 -0
- package/src/generators/agent-schema-page.ts +297 -0
- package/src/generators/agent-ui-page.ts +290 -0
- package/src/generators/api-docs-file.ts +13 -6
- package/src/generators/api-model.ts +63 -34
- package/src/generators/callable-file.ts +1 -1
- package/src/generators/docs-data-builder.ts +46 -2
- package/src/generators/docs-data.ts +6 -2
- package/src/generators/index.ts +25 -14
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +19 -10
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +64 -4
- package/src/metaobjects-config.ts +48 -2
- package/src/names.ts +629 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/reference/entity.ts +21 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +19 -3
- package/src/reference/routes-hono.ts +19 -9
- package/src/reference/routes.ts +25 -9
- package/src/reference-templates.ts +1 -1
- package/src/render-context.ts +13 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +122 -11
- package/src/templates/callable-file.ts +60 -8
- package/src/templates/drizzle-schema.ts +248 -60
- package/src/templates/entity-constants.ts +95 -184
- package/src/templates/entity-file.ts +46 -4
- package/src/templates/entity-meta-file.ts +16 -3
- package/src/templates/entity-ui-descriptor.ts +305 -0
- package/src/templates/field-meta.ts +100 -19
- package/src/templates/names-decl.ts +165 -0
- package/src/templates/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/requirement-test.ts +13 -2
- package/src/templates/routes-file-hono.ts +14 -2
- package/src/templates/routes-file.ts +69 -24
- package/src/templates/value-object-file.ts +15 -1
- package/src/templates/view-decl.ts +53 -3
- package/src/templates/zod-validators.ts +41 -5
- package/src/view-context.ts +88 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// The per-field FORM descriptor — the one derivation of "what does this field look like
|
|
2
|
+
// in a UI", shared by everything that answers that question.
|
|
3
|
+
//
|
|
4
|
+
// Two consumers, and the reason they must share:
|
|
5
|
+
//
|
|
6
|
+
// • `renderEntityConstants()` emits it as the `<Entity>` const in `<Entity>.meta.ts`,
|
|
7
|
+
// which `useEntityForm` reads at RUNTIME to render the form;
|
|
8
|
+
// • the `agent/ui.md` docs surface renders it as a table, which is what an agent reads
|
|
9
|
+
// BEFORE it writes code against that form.
|
|
10
|
+
//
|
|
11
|
+
// If those two were derived separately, the page describing the form and the descriptor
|
|
12
|
+
// driving it could disagree — and the disagreement would be invisible, because the page
|
|
13
|
+
// looks authoritative and nothing compares them. That is the same "a name resolved twice
|
|
14
|
+
// is a name that can disagree with itself" rule `names.ts` states for physical names, one
|
|
15
|
+
// tier up.
|
|
16
|
+
//
|
|
17
|
+
// #356 — THE SURFACE IS THE FORM. `view` resolves through `inferViewKind(field, "form")`,
|
|
18
|
+
// which selects the view named for the form context, so a field declaring several views is
|
|
19
|
+
// described by the one the generated `<Entity>.form.tsx` actually renders. The grid tiers
|
|
20
|
+
// compute their own view kind at codegen time and never read this.
|
|
21
|
+
//
|
|
22
|
+
// RULES ARE AN ORDERED LIST, NOT A KEYED OBJECT, and that is load-bearing rather than
|
|
23
|
+
// stylistic: the emitted `rules: { ... }` object preserves the order the field's validator
|
|
24
|
+
// children were declared in, so a field whose `validator.regex` precedes its
|
|
25
|
+
// `validator.required` emits `pattern` first. A keyed shape would impose one fixed order
|
|
26
|
+
// and silently rewrite every such entity's descriptor.
|
|
27
|
+
//
|
|
28
|
+
// The pattern is carried RAW. Escaping it into a `/.../` literal is a TypeScript-emission
|
|
29
|
+
// concern and belongs to the emitter, not to the description of the field.
|
|
30
|
+
|
|
31
|
+
import type { MetaData, MetaField, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
32
|
+
import {
|
|
33
|
+
VIEW_SUBTYPE_TEXT,
|
|
34
|
+
VIEW_SUBTYPE_TEXTAREA,
|
|
35
|
+
VIEW_SUBTYPE_NUMBER,
|
|
36
|
+
VIEW_SUBTYPE_CHECKBOX,
|
|
37
|
+
VIEW_SUBTYPE_DATE,
|
|
38
|
+
VIEW_SUBTYPE_PASSWORD,
|
|
39
|
+
VIEW_SUBTYPE_HIDDEN,
|
|
40
|
+
VIEW_SUBTYPE_DROPDOWN,
|
|
41
|
+
VIEW_SUBTYPE_RADIO,
|
|
42
|
+
VALIDATOR_SUBTYPE_REQUIRED,
|
|
43
|
+
VALIDATOR_SUBTYPE_LENGTH,
|
|
44
|
+
VALIDATOR_SUBTYPE_REGEX,
|
|
45
|
+
VALIDATOR_ATTR_MIN,
|
|
46
|
+
VALIDATOR_ATTR_MAX,
|
|
47
|
+
VALIDATOR_ATTR_PATTERN,
|
|
48
|
+
FIELD_ATTR_MAX_LENGTH,
|
|
49
|
+
FIELD_ATTR_REQUIRED,
|
|
50
|
+
pluralize,
|
|
51
|
+
toSnakeCase,
|
|
52
|
+
} from "@metaobjectsdev/metadata";
|
|
53
|
+
import { inferViewKind, currencyMetaFor, labelFor, humanize, valueObjectFor } from "./field-meta.js";
|
|
54
|
+
import { VIEW_CONTEXT_FORM } from "../view-context.js";
|
|
55
|
+
import { isProjection } from "../projection/projection-detector.js";
|
|
56
|
+
// `restPath` lives HERE rather than in api-surface.ts, which is where it used to sit and
|
|
57
|
+
// which now re-exports it. The descriptor is what emits `$path`, so the composition has to
|
|
58
|
+
// be reachable from this module; importing it back from `api-surface.js` would be a cycle,
|
|
59
|
+
// since that module imports `resourcePath` from this one. Neither of these two imports
|
|
60
|
+
// reaches back here, so moving the function down is the acyclic direction.
|
|
61
|
+
import { tphDiscriminatorBase, tphDiscriminatorPin } from "./zod-validators.js";
|
|
62
|
+
import { tphRouteSegment } from "./tph-discriminator.js";
|
|
63
|
+
|
|
64
|
+
/** One validation rule, in the order its source was declared. */
|
|
65
|
+
export type UiRule =
|
|
66
|
+
| { readonly kind: "required"; readonly message: string }
|
|
67
|
+
| { readonly kind: "minLength"; readonly value: number; readonly message: string }
|
|
68
|
+
| { readonly kind: "maxLength"; readonly value: number; readonly message: string }
|
|
69
|
+
/** `pattern` is the RAW authored regex source — never pre-escaped. */
|
|
70
|
+
| { readonly kind: "pattern"; readonly pattern: string; readonly message: string };
|
|
71
|
+
|
|
72
|
+
export interface UiFieldDescriptor {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly label: string;
|
|
75
|
+
/** The MetaView subtype resolved for the FORM context. */
|
|
76
|
+
readonly view: string;
|
|
77
|
+
/** Present only when the view maps to a real HTML `<input type=…>`. Derived from the
|
|
78
|
+
* view subtype — there is no authored override (see `htmlTypeFromView`). */
|
|
79
|
+
readonly htmlType?: string | undefined;
|
|
80
|
+
/** Empty when the field declares nothing to validate. */
|
|
81
|
+
readonly rules: readonly UiRule[];
|
|
82
|
+
/** Present only for a `field.currency`. */
|
|
83
|
+
readonly currency?: { readonly currency: string; readonly locale: string } | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Present when the generated FORM renders this field as a nested value-object
|
|
86
|
+
* sub-form rather than an input — a `field.object` whose `@objectRef` resolves (see
|
|
87
|
+
* `valueObjectFor`). Computed only when `buildEntityUiDescriptor` is given the loaded
|
|
88
|
+
* root; `renderEntityConstants` does not pass one and emits nothing for it, so the
|
|
89
|
+
* `<Entity>` const is byte-identical either way.
|
|
90
|
+
*/
|
|
91
|
+
readonly nested?: { readonly objectRef: string; readonly isArray: boolean } | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface EntityUiDescriptor {
|
|
95
|
+
readonly entity: string;
|
|
96
|
+
/** The address the generated routes SERVE this object at — `/subscribers`, and
|
|
97
|
+
* `/vehicles/car` for a TPH subtype. Emitted as `$path`. See {@link restPath}. */
|
|
98
|
+
readonly path: string;
|
|
99
|
+
/** Every field, INHERITED INCLUDED (ADR-0039 resolving `fields()`). */
|
|
100
|
+
readonly fields: readonly UiFieldDescriptor[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An object's OWN pluralized resource path — the one derivation of that spelling, and the
|
|
105
|
+
* input {@link restPath} composes an address from. It is NOT `$path` on its own.
|
|
106
|
+
*
|
|
107
|
+
* A projection is KEBAB-cased and an entity is SNAKE-cased, and that split is not a
|
|
108
|
+
* style choice: both spellings are already mounted. `renderProjectionDecl` emits
|
|
109
|
+
* `$path: "/order-summaries"` and `renderEntityConstants` emits `$path:
|
|
110
|
+
* "/order_summaries"`, and `routes-file.ts` mounts whichever const belongs to the object
|
|
111
|
+
* — so an address computed without the split is wrong for every multi-word projection.
|
|
112
|
+
* It used to live in a second function inside `projection-decl.ts`, which is exactly how
|
|
113
|
+
* `agent/ui.md` and `api/AGENT-API.md` both came to print `/order_summaries` for a
|
|
114
|
+
* projection served at `/order-summaries`.
|
|
115
|
+
*
|
|
116
|
+
* "Subscriber" → "/subscribers"
|
|
117
|
+
* "WorkoutEvent" → "/workout_events"
|
|
118
|
+
* "ProgramSummary" → "/program-summaries" (projection)
|
|
119
|
+
*
|
|
120
|
+
* A TPH SUBTYPE is not addressed by this path — it is mounted under its base — so this is
|
|
121
|
+
* an INPUT to {@link restPath}, not the answer. `$path` carries `restPath`; call this only
|
|
122
|
+
* when you specifically want an object's own pluralized name, never to build an address.
|
|
123
|
+
*/
|
|
124
|
+
export function resourcePath(entity: MetaData): string {
|
|
125
|
+
// The two compositions differ in ORDER as well as separator, and both are load-bearing:
|
|
126
|
+
// pluralize-then-snake is what the projection const has always emitted, snake-then-
|
|
127
|
+
// pluralize is what the entity const has. Neither may be "tidied" into the other.
|
|
128
|
+
return isProjection(entity)
|
|
129
|
+
? `/${toSnakeCase(pluralize(entity.name)).replace(/_/g, "-")}`
|
|
130
|
+
: `/${pluralize(toSnakeCase(entity.name))}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* THE address the generated routes serve this object at — and therefore what the
|
|
135
|
+
* `<Entity>` const emits as `$path`.
|
|
136
|
+
*
|
|
137
|
+
* `resourcePath` answers a different question: an object's own pluralized name. The two
|
|
138
|
+
* agree for everything EXCEPT a TPH subtype, which emits no routes file of its own —
|
|
139
|
+
* `routes-file.ts` mounts the whole hierarchy from the discriminator BASE, giving the
|
|
140
|
+
* union read-only routes at the base path and each subtype a full CRUD set at
|
|
141
|
+
* `<base path>/<route segment>`.
|
|
142
|
+
*
|
|
143
|
+
* `$path` used to carry `resourcePath`, so a subtype's own `$path` named an endpoint that
|
|
144
|
+
* did not exist. That was not confined to prose: `agent/ui.md` printed it as fact, and the
|
|
145
|
+
* TanStack `grid-hook-file.ts` builds its fetch URL from `<Entity>.$path` with no TPH
|
|
146
|
+
* branch at all, so an opted-in per-subtype grid requested an address nothing served.
|
|
147
|
+
* Fixing it here fixes every consumer, because every consumer reads the const.
|
|
148
|
+
*
|
|
149
|
+
* The composition is the same one `routes-file.ts` and `hooks-file.ts` emit as CODE
|
|
150
|
+
* (`Base.$path + "/car"`); they reference the const rather than a computed string, so this
|
|
151
|
+
* is the one place it can be evaluated. The SEGMENT rule is not restated —
|
|
152
|
+
* `tphRouteSegment` owns it, and all three read it from there.
|
|
153
|
+
*/
|
|
154
|
+
export function restPath(entity: MetaObject): string {
|
|
155
|
+
const pin = tphDiscriminatorPin(entity);
|
|
156
|
+
const base = tphDiscriminatorBase(entity);
|
|
157
|
+
if (pin === undefined || base === undefined) return resourcePath(entity);
|
|
158
|
+
return `${resourcePath(base)}/${tphRouteSegment(pin.value)}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Map a MetaView subtype to a real HTML `<input type=…>` value. Returns undefined for
|
|
163
|
+
* views that don't map to `<input>` at all (textarea, dropdown) — consumers render the
|
|
164
|
+
* right element type themselves.
|
|
165
|
+
*
|
|
166
|
+
* THE MAPPING IS THE ONLY SOURCE. An `@htmlType` override used to be read off the view
|
|
167
|
+
* node ahead of this switch; nothing registers that attribute, so the override was
|
|
168
|
+
* unreachable and this mapping was always the answer. A consumer needing a different
|
|
169
|
+
* `type=` edits the emitted `<Entity>.meta.ts`, whose hand edits the merge preserves.
|
|
170
|
+
*/
|
|
171
|
+
function htmlTypeFromView(view: string): string | undefined {
|
|
172
|
+
switch (view) {
|
|
173
|
+
case VIEW_SUBTYPE_TEXT:
|
|
174
|
+
return "text";
|
|
175
|
+
case VIEW_SUBTYPE_NUMBER:
|
|
176
|
+
return "number";
|
|
177
|
+
case VIEW_SUBTYPE_DATE:
|
|
178
|
+
return "date";
|
|
179
|
+
case VIEW_SUBTYPE_PASSWORD:
|
|
180
|
+
return "password";
|
|
181
|
+
case VIEW_SUBTYPE_CHECKBOX:
|
|
182
|
+
return "checkbox";
|
|
183
|
+
case VIEW_SUBTYPE_HIDDEN:
|
|
184
|
+
return "hidden";
|
|
185
|
+
case VIEW_SUBTYPE_RADIO:
|
|
186
|
+
return "radio";
|
|
187
|
+
case "month":
|
|
188
|
+
return "month";
|
|
189
|
+
case "email":
|
|
190
|
+
return "email";
|
|
191
|
+
case VIEW_SUBTYPE_TEXTAREA:
|
|
192
|
+
case VIEW_SUBTYPE_DROPDOWN:
|
|
193
|
+
return undefined;
|
|
194
|
+
default:
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The field's validation rules, in DECLARATION ORDER — see the ordering note at the top
|
|
201
|
+
* of this file.
|
|
202
|
+
*
|
|
203
|
+
* The field-level `@required` / `@maxLength` attrs append only when no validator child
|
|
204
|
+
* already supplied that rule, so the two authoring spellings never double up.
|
|
205
|
+
*/
|
|
206
|
+
function buildFieldRules(field: MetaField): UiRule[] {
|
|
207
|
+
const rules: UiRule[] = [];
|
|
208
|
+
let hasRequired = false;
|
|
209
|
+
let hasMaxLength = false;
|
|
210
|
+
|
|
211
|
+
// ADR-0039: resolving — a validator may inherit its config attrs (@min/@max/@pattern)
|
|
212
|
+
// via extends.
|
|
213
|
+
//
|
|
214
|
+
// THE MESSAGES ARE DERIVED, NOT AUTHORED. `@message` / `@minMessage` / `@maxMessage`
|
|
215
|
+
// used to be read here as overrides; no provider registers any of them, so the reads
|
|
216
|
+
// were unreachable and the defaults below were always what shipped. A consumer wanting
|
|
217
|
+
// different wording edits the emitted `rules` object in `<Entity>.meta.ts`, which is a
|
|
218
|
+
// generated file whose hand edits the three-way merge preserves. See ADR-0023.
|
|
219
|
+
for (const child of field.validators()) {
|
|
220
|
+
if (child.subType === VALIDATOR_SUBTYPE_REQUIRED) {
|
|
221
|
+
rules.push({ kind: "required", message: `${humanize(field.name)} is required` });
|
|
222
|
+
hasRequired = true;
|
|
223
|
+
} else if (child.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
224
|
+
const min = child.attr(VALIDATOR_ATTR_MIN);
|
|
225
|
+
const max = child.attr(VALIDATOR_ATTR_MAX);
|
|
226
|
+
if (typeof min === "number") {
|
|
227
|
+
rules.push({ kind: "minLength", value: min, message: `Must be at least ${min} characters` });
|
|
228
|
+
}
|
|
229
|
+
if (typeof max === "number") {
|
|
230
|
+
rules.push({ kind: "maxLength", value: max, message: `Must be ${max} characters or fewer` });
|
|
231
|
+
hasMaxLength = true;
|
|
232
|
+
}
|
|
233
|
+
} else if (child.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
234
|
+
const pattern = child.attr(VALIDATOR_ATTR_PATTERN);
|
|
235
|
+
if (typeof pattern === "string") {
|
|
236
|
+
rules.push({ kind: "pattern", pattern, message: "Invalid format" });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Field-level @required attr (if not already covered by validator).
|
|
242
|
+
if (!hasRequired && field.attr(FIELD_ATTR_REQUIRED) === true) {
|
|
243
|
+
rules.push({ kind: "required", message: `${humanize(field.name)} is required` });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Field-level @maxLength attr (if not already covered).
|
|
247
|
+
const maxLenAttr = field.attr(FIELD_ATTR_MAX_LENGTH);
|
|
248
|
+
if (!hasMaxLength && typeof maxLenAttr === "number") {
|
|
249
|
+
rules.push({
|
|
250
|
+
kind: "maxLength",
|
|
251
|
+
value: maxLenAttr,
|
|
252
|
+
message: `Must be ${maxLenAttr} characters or fewer`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return rules;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Describe one field's form presentation.
|
|
261
|
+
*
|
|
262
|
+
* `root` is OPTIONAL and only decides whether `nested` is filled in: resolving a
|
|
263
|
+
* `field.object`'s `@objectRef` needs the run's other objects, and the two callers differ
|
|
264
|
+
* — `renderEntityConstants` has no root (it is also ejected into adopter repos with a
|
|
265
|
+
* fixed signature) while the docs surface does. Passing one adds a member the const
|
|
266
|
+
* emitter ignores; it never changes any member the const emits.
|
|
267
|
+
*/
|
|
268
|
+
export function buildUiFieldDescriptor(field: MetaField, root?: MetaRoot): UiFieldDescriptor {
|
|
269
|
+
// `inferViewKind` IS the resolution: the view child declared for the FORM (own or
|
|
270
|
+
// inherited) wins, else the field subtype's default. This module used to keep a
|
|
271
|
+
// `resolveView` that returned the same string plus the view NODE, and the node existed
|
|
272
|
+
// only to read `@placeholder` / `@helpText` / `@htmlType` off it — none of which any
|
|
273
|
+
// provider registers, so all three reads were unreachable. With them gone the node has
|
|
274
|
+
// no reader and the two functions were the same function.
|
|
275
|
+
const view = inferViewKind(field, VIEW_CONTEXT_FORM);
|
|
276
|
+
const currencyMeta = currencyMetaFor(field, VIEW_CONTEXT_FORM);
|
|
277
|
+
const vo = root === undefined ? undefined : valueObjectFor(field, root);
|
|
278
|
+
return {
|
|
279
|
+
name: field.name,
|
|
280
|
+
label: labelFor(field, VIEW_CONTEXT_FORM),
|
|
281
|
+
view,
|
|
282
|
+
htmlType: htmlTypeFromView(view),
|
|
283
|
+
rules: buildFieldRules(field),
|
|
284
|
+
currency: currencyMeta === null
|
|
285
|
+
? undefined
|
|
286
|
+
: { currency: currencyMeta.currency, locale: currencyMeta.locale },
|
|
287
|
+
nested: vo === undefined
|
|
288
|
+
? undefined
|
|
289
|
+
: { objectRef: vo.name, isArray: field.resolvedIsArray() },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Describe one object's whole form surface. See {@link buildUiFieldDescriptor} for `root`. */
|
|
294
|
+
export function buildEntityUiDescriptor(obj: MetaObject, root?: MetaRoot): EntityUiDescriptor {
|
|
295
|
+
return {
|
|
296
|
+
entity: obj.name,
|
|
297
|
+
// `restPath`, not `resourcePath`: `$path` is the address this object is SERVED at.
|
|
298
|
+
// They differ only for a TPH subtype, and for that one case `resourcePath` names an
|
|
299
|
+
// endpoint nothing mounts (see restPath's note).
|
|
300
|
+
path: restPath(obj),
|
|
301
|
+
// ADR-0039: resolving `fields()` so inherited fields appear, exactly as the emitted
|
|
302
|
+
// descriptor has always done.
|
|
303
|
+
fields: obj.fields().map((f) => buildUiFieldDescriptor(f, root)),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// All helpers take a MetaField node.
|
|
5
5
|
|
|
6
6
|
import { MetaField } from "@metaobjectsdev/metadata";
|
|
7
|
+
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
8
|
+
import { stripPackage } from "@metaobjectsdev/metadata";
|
|
7
9
|
import {
|
|
8
10
|
FIELD_SUBTYPE_STRING,
|
|
9
11
|
FIELD_SUBTYPE_INT,
|
|
@@ -20,6 +22,8 @@ import {
|
|
|
20
22
|
FIELD_SUBTYPE_UUID,
|
|
21
23
|
FIELD_SUBTYPE_URI,
|
|
22
24
|
FIELD_SUBTYPE_INET,
|
|
25
|
+
FIELD_SUBTYPE_OBJECT,
|
|
26
|
+
FIELD_ATTR_OBJECT_REF,
|
|
23
27
|
FIELD_ATTR_STRING_FORMAT,
|
|
24
28
|
FIELD_ATTR_LENIENT,
|
|
25
29
|
STRING_FORMAT_EMAIL,
|
|
@@ -29,12 +33,14 @@ import {
|
|
|
29
33
|
VIEW_SUBTYPE_NUMBER,
|
|
30
34
|
VIEW_SUBTYPE_CHECKBOX,
|
|
31
35
|
VIEW_SUBTYPE_CURRENCY,
|
|
36
|
+
VIEW_SUBTYPE_DROPDOWN,
|
|
32
37
|
FIELD_ATTR_CURRENCY,
|
|
33
38
|
FIELD_ATTR_CURRENCY_DEFAULT,
|
|
34
39
|
VIEW_CURRENCY_ATTR_LOCALE,
|
|
35
40
|
VIEW_CURRENCY_ATTR_LOCALE_DEFAULT,
|
|
36
41
|
} from "@metaobjectsdev/metadata";
|
|
37
42
|
import { enumValues, zodEnumExpr } from "../enum-meta.js";
|
|
43
|
+
import { viewForContext } from "../view-context.js";
|
|
38
44
|
import { ZOD_INET_EXPR } from "./net-regex.js";
|
|
39
45
|
|
|
40
46
|
// ---------------------------------------------------------------------------
|
|
@@ -42,12 +48,16 @@ import { ZOD_INET_EXPR } from "./net-regex.js";
|
|
|
42
48
|
// ---------------------------------------------------------------------------
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
|
-
* Resolve the cell-renderer key (view kind) for a field
|
|
46
|
-
*
|
|
51
|
+
* Resolve the cell-renderer key (view kind) for a field, for the surface named
|
|
52
|
+
* by `context`.
|
|
53
|
+
*
|
|
54
|
+
* An explicit view declared for THAT surface wins; field subType determines the
|
|
55
|
+
* default. #356: `context` is required rather than defaulting, because a caller
|
|
56
|
+
* that omits it is exactly the caller that renders somebody else's view.
|
|
47
57
|
*/
|
|
48
|
-
export function inferViewKind(field: MetaField): string {
|
|
49
|
-
// Explicit view (own or inherited via extends) has highest priority.
|
|
50
|
-
const viewChild = field
|
|
58
|
+
export function inferViewKind(field: MetaField, context: string): string {
|
|
59
|
+
// Explicit view for this surface (own or inherited via extends) has highest priority.
|
|
60
|
+
const viewChild = viewForContext(field, context);
|
|
51
61
|
if (viewChild) return viewChild.subType;
|
|
52
62
|
// Field subtype → default view.
|
|
53
63
|
return defaultViewForSubType(field.subType);
|
|
@@ -69,6 +79,22 @@ function defaultViewForSubType(subType: string): string {
|
|
|
69
79
|
return VIEW_SUBTYPE_DATE;
|
|
70
80
|
case FIELD_SUBTYPE_CURRENCY:
|
|
71
81
|
return VIEW_SUBTYPE_CURRENCY;
|
|
82
|
+
case FIELD_SUBTYPE_ENUM:
|
|
83
|
+
// A closed member set is a CHOICE, and the generated form has always rendered it as
|
|
84
|
+
// one: `codegen-ts-react`'s `viewKindFor` returns `dropdown` for an enum declaring no
|
|
85
|
+
// view, and emits a `<select>` with an `<option>` per member.
|
|
86
|
+
//
|
|
87
|
+
// This branch was missing, so the same question had two answers. The emitted
|
|
88
|
+
// `<Entity>` descriptor — which `useEntityForm` reads at RUNTIME — fell through to
|
|
89
|
+
// `text` and told every consumer the control was a free-text input, for the one field
|
|
90
|
+
// subtype where free text is exactly what the model forbids. The form was right and
|
|
91
|
+
// the descriptor describing it was wrong.
|
|
92
|
+
//
|
|
93
|
+
// Fixed HERE rather than in the react tier because this is the shared resolver: the
|
|
94
|
+
// descriptor, the `agent/ui.md` page and the form must give one answer, and pinning
|
|
95
|
+
// the agreement in the tier that renders it would leave the two other readers still
|
|
96
|
+
// computing their own.
|
|
97
|
+
return VIEW_SUBTYPE_DROPDOWN;
|
|
72
98
|
default:
|
|
73
99
|
return VIEW_SUBTYPE_TEXT;
|
|
74
100
|
}
|
|
@@ -152,14 +178,25 @@ export function zodTypeFor(field: MetaField, timestampMode: "date" | "string" =
|
|
|
152
178
|
// ---------------------------------------------------------------------------
|
|
153
179
|
|
|
154
180
|
/**
|
|
155
|
-
* Resolve currency code + locale for a currency-subtype field
|
|
156
|
-
* Returns null for non-currency fields.
|
|
181
|
+
* Resolve currency code + locale for a currency-subtype field, for the surface
|
|
182
|
+
* named by `context`. Returns null for non-currency fields.
|
|
157
183
|
*/
|
|
158
|
-
export function currencyMetaFor(
|
|
184
|
+
export function currencyMetaFor(
|
|
185
|
+
field: MetaField,
|
|
186
|
+
context: string,
|
|
187
|
+
): { currency: string; locale: string } | null {
|
|
159
188
|
if (field.subType !== FIELD_SUBTYPE_CURRENCY) return null;
|
|
160
189
|
const currency =
|
|
161
190
|
(field.attr(FIELD_ATTR_CURRENCY) as string | undefined) ?? FIELD_ATTR_CURRENCY_DEFAULT;
|
|
162
|
-
|
|
191
|
+
// #356: the surface's own view carries the @locale for that surface — two
|
|
192
|
+
// currency views with different locales must not be resolved by position.
|
|
193
|
+
// When the surface's view is NOT a currency view (or there is none), keep the
|
|
194
|
+
// pre-#356 scan: an authored @locale anywhere on the field beats the default.
|
|
195
|
+
const contextView = viewForContext(field, context);
|
|
196
|
+
const viewChild =
|
|
197
|
+
contextView?.subType === VIEW_SUBTYPE_CURRENCY
|
|
198
|
+
? contextView
|
|
199
|
+
: field.views().find((c) => c.subType === VIEW_SUBTYPE_CURRENCY);
|
|
163
200
|
// ADR-0039: resolving — a view may inherit @locale via extends.
|
|
164
201
|
const locale =
|
|
165
202
|
(viewChild?.attr(VIEW_CURRENCY_ATTR_LOCALE) as string | undefined) ??
|
|
@@ -167,20 +204,58 @@ export function currencyMetaFor(field: MetaField): { currency: string; locale: s
|
|
|
167
204
|
return { currency, locale };
|
|
168
205
|
}
|
|
169
206
|
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// valueObjectFor
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The `object.value` a `field.object`'s `@objectRef` names, or `undefined` when the
|
|
213
|
+
* field is not an object field, declares no ref, or names one that is not in the run.
|
|
214
|
+
*
|
|
215
|
+
* THIS IS THE "DOES THE FORM NEST THIS FIELD?" PREDICATE, and it lives here because two
|
|
216
|
+
* tiers have to give one answer: `codegen-ts-react`'s form generator recurses into the
|
|
217
|
+
* referenced value object as a `<fieldset>` sub-form (or a `useFieldArray` repeatable
|
|
218
|
+
* group when the field is an array) instead of emitting an input at all, and
|
|
219
|
+
* `agent/ui.md` documents what that form renders. When they answered separately the page
|
|
220
|
+
* printed `text` — the field's resolved `view.*` subtype, which the form never consults
|
|
221
|
+
* for a nested field — as if it were the control.
|
|
222
|
+
*
|
|
223
|
+
* The resolution is BARE-NAME on purpose: it is the same lookup the Zod and
|
|
224
|
+
* inferred-type templates perform, and changing it here would change what the generated
|
|
225
|
+
* form nests. A package-aware resolution is a separate decision with its own blast
|
|
226
|
+
* radius (ADR-0042).
|
|
227
|
+
*/
|
|
228
|
+
export function valueObjectFor(field: MetaField, root: MetaRoot): MetaObject | undefined {
|
|
229
|
+
if (field.subType !== FIELD_SUBTYPE_OBJECT) return undefined;
|
|
230
|
+
// ADR-0039: resolving — @objectRef may be inherited via extends.
|
|
231
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
232
|
+
if (typeof ref !== "string" || ref.length === 0) return undefined;
|
|
233
|
+
const base = stripPackage(ref);
|
|
234
|
+
return root.objects().find((o) => o.name === base);
|
|
235
|
+
}
|
|
236
|
+
|
|
170
237
|
// ---------------------------------------------------------------------------
|
|
171
238
|
// labelFor
|
|
172
239
|
// ---------------------------------------------------------------------------
|
|
173
240
|
|
|
174
241
|
/**
|
|
175
|
-
* Resolve the human-readable label for a field
|
|
176
|
-
* Uses
|
|
242
|
+
* Resolve the human-readable label for a field, for the surface named by `context`.
|
|
243
|
+
* Uses the registered `@title` common attr — on that surface's view first, else on the
|
|
244
|
+
* field itself; otherwise humanizes the field name.
|
|
245
|
+
*
|
|
246
|
+
* #353 — this read `@label`, which no provider registers on any `view.*` subtype, so the
|
|
247
|
+
* override branch was unreachable: authoring it fails the strict load `meta verify` runs
|
|
248
|
+
* (ERR_UNKNOWN_ATTR). `title` is already a registered common attr on every node and
|
|
249
|
+
* already means "a noun phrase", so nothing was registered for this (ADR-0037 step 0).
|
|
177
250
|
*/
|
|
178
|
-
export function labelFor(field: MetaField): string {
|
|
179
|
-
for
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
251
|
+
export function labelFor(field: MetaField, context: string): string {
|
|
252
|
+
// #356: the label reads the view for THIS surface, not whichever view was
|
|
253
|
+
// declared first — a grid header must not retitle the form's field.
|
|
254
|
+
// ADR-0039: resolving — a view may inherit @title via extends.
|
|
255
|
+
const title = viewForContext(field, context)?.attr("title");
|
|
256
|
+
if (typeof title === "string" && title.length > 0) return title;
|
|
257
|
+
const fieldTitle = field.attr("title");
|
|
258
|
+
if (typeof fieldTitle === "string" && fieldTitle.length > 0) return fieldTitle;
|
|
184
259
|
return humanize(field.name);
|
|
185
260
|
}
|
|
186
261
|
|
|
@@ -188,8 +263,14 @@ export function labelFor(field: MetaField): string {
|
|
|
188
263
|
// Internal
|
|
189
264
|
// ---------------------------------------------------------------------------
|
|
190
265
|
|
|
191
|
-
/**
|
|
192
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Convert a camelCase or PascalCase field name to a human-friendly label.
|
|
268
|
+
*
|
|
269
|
+
* THE one copy in this package: `entity-ui-descriptor.ts` re-exports this rather than
|
|
270
|
+
* keeping a second body, because two spellings of "humanize" is two answers to what a
|
|
271
|
+
* field is CALLED — the label a form renders and the label the docs page prints.
|
|
272
|
+
*/
|
|
273
|
+
export function humanize(s: string): string {
|
|
193
274
|
return s
|
|
194
275
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
195
276
|
.replace(/_/g, " ")
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §A1/§A2 — `<Entity>Names`: the physical database names for one object, as constants a
|
|
3
|
+
* hand-written consumer references instead of a string literal.
|
|
4
|
+
*
|
|
5
|
+
* Shape copied from the FR-009 filter allowlist, which is the same problem (a per-entity
|
|
6
|
+
* name artifact) already solved in all five ports. Deliberately NOT folded into the entity
|
|
7
|
+
* descriptor: four of five ports have no descriptor to extend, and merging in TypeScript
|
|
8
|
+
* alone would make TS the odd port out on the axis this project protects hardest.
|
|
9
|
+
*
|
|
10
|
+
* An artifact whose object EXTENDS another one that has an artifact of its own spreads it
|
|
11
|
+
* rather than restating what it inherits — "extend from the parent, do not redo all the
|
|
12
|
+
* names". Two shapes, decided structurally:
|
|
13
|
+
*
|
|
14
|
+
* - the object declares its OWN source: spread only the super's `fields`, because the
|
|
15
|
+
* physical name, kind, schema and read-only-ness are this object's own. Spreading the
|
|
16
|
+
* whole super here would leak its `schema` onto a child that declares none.
|
|
17
|
+
* - the object INHERITS its source (a TPH subtype sharing its base's single table):
|
|
18
|
+
* spread the WHOLE super, so the table name is stated once, on the base.
|
|
19
|
+
*/
|
|
20
|
+
import type { ColumnNamingStrategy, MetaObject } from "@metaobjectsdev/metadata";
|
|
21
|
+
import { GENERATED_HEADER } from "../constants.js";
|
|
22
|
+
import { PHYSICAL_NAME_ATTR_BY_KIND, primaryRdbSource } from "@metaobjectsdev/metadata";
|
|
23
|
+
|
|
24
|
+
/** The physical-name alias keys, in the metamodel's own order. */
|
|
25
|
+
const PHYSICAL_NAME_ALIASES = [...PHYSICAL_NAME_ATTR_BY_KIND.values()] as const;
|
|
26
|
+
import {
|
|
27
|
+
resolveObjectNames, resolveSuperFragmentNames,
|
|
28
|
+
type FieldNames, type KeyNames, type ObjectNames, type SourceNames,
|
|
29
|
+
} from "../names.js";
|
|
30
|
+
|
|
31
|
+
export interface NamesDeclOpts {
|
|
32
|
+
readonly strategy?: ColumnNamingStrategy | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Module specifier for the super's names module (e.g. `"./BaseEntity.names"`), when this
|
|
35
|
+
* artifact extends one. Computed by the CALLER, which is the only place that knows the
|
|
36
|
+
* project's output layout and extension style. Omitted ⇒ the flat shape: every inherited
|
|
37
|
+
* column restated, which is what an ejected reference template that has not been updated
|
|
38
|
+
* still produces. Correct, just not deduplicated.
|
|
39
|
+
*/
|
|
40
|
+
readonly superSpecifier?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Render the FRAGMENT form — an abstract base that a sourced object extends, carrying
|
|
43
|
+
* columns and no physical name. See `resolveSuperFragmentNames`.
|
|
44
|
+
*/
|
|
45
|
+
readonly fragment?: boolean | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Back-compat: the second argument was the naming strategy, and an ejected copy still passes it. */
|
|
49
|
+
function normalize(opts?: ColumnNamingStrategy | NamesDeclOpts): NamesDeclOpts {
|
|
50
|
+
return typeof opts === "string" || opts === undefined ? { strategy: opts } : opts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderNamesDecl(
|
|
54
|
+
obj: MetaObject,
|
|
55
|
+
opts?: ColumnNamingStrategy | NamesDeclOpts,
|
|
56
|
+
): string {
|
|
57
|
+
const o = normalize(opts);
|
|
58
|
+
// `fragment` says "this is an ANCESTOR render" — emit even though the walk reached this
|
|
59
|
+
// object by climbing rather than by matching. It does NOT say which shape to render, and
|
|
60
|
+
// it must not: a caller that hardcodes `true` is correct for an abstract base with
|
|
61
|
+
// columns and no table, and wrong for the other ancestor this walk reaches — a TPH BASE
|
|
62
|
+
// under `meta gen --entities <Subtype>`, which owns the shared table. Rendered as a
|
|
63
|
+
// fragment it emits no source at all while the subtype still spreads it.
|
|
64
|
+
//
|
|
65
|
+
// Derived HERE rather than at the call site because the call sites are plural and one of
|
|
66
|
+
// them is EJECTED: `src/reference/names.ts` is copied into an adopter's repo by
|
|
67
|
+
// `meta init` and thereafter owned by them. Deciding it in the engine makes every copy
|
|
68
|
+
// already on disk correct without an edit, and leaves no caller able to get it wrong.
|
|
69
|
+
const n: ObjectNames | undefined = o.fragment === true && primaryRdbSource(obj) === undefined
|
|
70
|
+
? resolveSuperFragmentNames(obj, o.strategy)
|
|
71
|
+
: resolveObjectNames(obj, o.strategy);
|
|
72
|
+
if (n === undefined) return "";
|
|
73
|
+
|
|
74
|
+
const superSym = n.superNames === undefined || o.superSpecifier === undefined
|
|
75
|
+
? undefined
|
|
76
|
+
: `${n.superNames.name}Names`;
|
|
77
|
+
|
|
78
|
+
const q = (v: string): string => JSON.stringify(v);
|
|
79
|
+
// A key is emitted bare only when it is a valid identifier. Field names always are;
|
|
80
|
+
// an index name is author-chosen and routinely is not (`uq_cust_email` is, `2fa-idx`
|
|
81
|
+
// is not), so quoting is decided per key rather than per collection — an unquoted
|
|
82
|
+
// non-identifier key is a file that does not parse.
|
|
83
|
+
const key = (k: string): string => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(k) ? k : q(k));
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One `name`-keyed collection — `fields`, `identities`, `indexes`.
|
|
87
|
+
*
|
|
88
|
+
* With a super to spread, only what THIS object declares is emitted and the rest is
|
|
89
|
+
* reached through the parent's artifact, so a physical name is stated once. Without one,
|
|
90
|
+
* every member must be here: a consumer looks a column up by field name and an inherited
|
|
91
|
+
* miss falls back to a literal, which is the defect the artifact exists to remove.
|
|
92
|
+
*
|
|
93
|
+
* The collection key is always emitted, even when empty, because a child spreads
|
|
94
|
+
* `...Super.identities` unconditionally.
|
|
95
|
+
*/
|
|
96
|
+
const collection = (
|
|
97
|
+
label: string,
|
|
98
|
+
all: Readonly<Record<string, unknown>>,
|
|
99
|
+
own: Readonly<Record<string, unknown>>,
|
|
100
|
+
render: (v: never) => string,
|
|
101
|
+
): string => {
|
|
102
|
+
const rows = superSym === undefined ? all : own;
|
|
103
|
+
const body = Object.keys(rows).sort()
|
|
104
|
+
.map((k) => ` ${key(k)}: ${render(rows[k] as never)},`)
|
|
105
|
+
.join("\n");
|
|
106
|
+
const spread = superSym === undefined ? "" : ` ...${superSym}.${label},\n`;
|
|
107
|
+
if (spread === "" && body === "") return ` ${label}: {},`;
|
|
108
|
+
return ` ${label}: {\n${spread}${body}${body === "" ? "" : "\n"} },`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const fieldsBlock = collection("fields", n.fields, n.ownFields,
|
|
112
|
+
(f: FieldNames) => `{ name: ${q(f.name)}, column: ${q(f.column)} }`);
|
|
113
|
+
|
|
114
|
+
// `type` and `subType` on every entry, because the artifact mirrors the metadata tree
|
|
115
|
+
// and because on these two collections the subType is the only thing that says whether
|
|
116
|
+
// an index is unique — ADR-0040 put that in the type rather than in an attribute.
|
|
117
|
+
const renderKey = (k: KeyNames): string =>
|
|
118
|
+
`{ type: ${q(k.type)}, subType: ${q(k.subType)}, name: ${q(k.name)}` +
|
|
119
|
+
(k.index === undefined ? " }" : `, index: ${q(k.index)} }`);
|
|
120
|
+
const identitiesBlock = collection("identities", n.identities, n.ownIdentities, renderKey);
|
|
121
|
+
const indexesBlock = collection("indexes", n.indexes, n.ownIndexes, renderKey);
|
|
122
|
+
|
|
123
|
+
const header =
|
|
124
|
+
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
125
|
+
`// Source metadata: ${obj.name}\n` +
|
|
126
|
+
(superSym === undefined ? "" : `import { ${superSym} } from ${q(o.superSpecifier as string)};\n\n`);
|
|
127
|
+
|
|
128
|
+
// The object's own identity. `name` is the METAMODEL name — it held the physical name
|
|
129
|
+
// until 0.25.0, and that key changing meaning without changing shape is the one thing
|
|
130
|
+
// here a hand-written consumer adopts without a compile error.
|
|
131
|
+
const identity =
|
|
132
|
+
` type: ${q(n.type)},\n` +
|
|
133
|
+
` subType: ${q(n.subType)},\n` +
|
|
134
|
+
` name: ${q(n.name)},\n`;
|
|
135
|
+
|
|
136
|
+
/** One source, under its role. The physical name sits under the alias for its `@kind`. */
|
|
137
|
+
const renderSource = (role: string, src: SourceNames): string => {
|
|
138
|
+
const parts = [`type: ${q(src.type)}`, `subType: ${q(src.subType)}`, `kind: ${q(src.kind)}`];
|
|
139
|
+
if (src.schema !== undefined) parts.push(`schema: ${q(src.schema)}`);
|
|
140
|
+
for (const alias of PHYSICAL_NAME_ALIASES) {
|
|
141
|
+
const v = src[alias as keyof SourceNames];
|
|
142
|
+
if (typeof v === "string") parts.push(`${alias}: ${q(v)}`);
|
|
143
|
+
}
|
|
144
|
+
return ` ${key(role)}: { ${parts.join(", ")} },`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// A fragment declares no source and must never acquire one; a TPH subtype INHERITS its
|
|
148
|
+
// base's, so it spreads rather than restating — structural (the two resolve to the SAME
|
|
149
|
+
// node), never an equality test on the resolved strings.
|
|
150
|
+
const sourceRows = Object.keys(n.ownSources).sort()
|
|
151
|
+
.map((role) => renderSource(role, n.ownSources[role] as SourceNames)).join("\n");
|
|
152
|
+
const spreadSources = superSym !== undefined && n.inheritsSource ? ` ...${superSym}.sources,\n` : "";
|
|
153
|
+
const sourcesBlock =
|
|
154
|
+
spreadSources === "" && sourceRows === ""
|
|
155
|
+
? " sources: {},"
|
|
156
|
+
: ` sources: {\n${spreadSources}${sourceRows}${sourceRows === "" ? "" : "\n"} },`;
|
|
157
|
+
|
|
158
|
+
return `${header}export const ${obj.name}Names = {
|
|
159
|
+
${identity}${sourcesBlock}
|
|
160
|
+
${fieldsBlock}
|
|
161
|
+
${identitiesBlock}
|
|
162
|
+
${indexesBlock}
|
|
163
|
+
} as const;
|
|
164
|
+
`;
|
|
165
|
+
}
|