@metaobjectsdev/codegen-ts 0.14.1 → 0.15.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/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +36 -16
- package/dist/column-mapper.js.map +1 -1
- package/dist/generators/api-model.js +21 -13
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +27 -22
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-file.d.ts.map +1 -1
- package/dist/generators/docs-file.js +2 -1
- package/dist/generators/docs-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +4 -2
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/output-prompt-file.d.ts.map +1 -1
- package/dist/generators/output-prompt-file.js +5 -3
- package/dist/generators/output-prompt-file.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts.map +1 -1
- package/dist/generators/render-helper-file.js +5 -3
- 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 +2 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +3 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-doc-builder.d.ts.map +1 -1
- package/dist/generators/template-doc-builder.js +14 -11
- package/dist/generators/template-doc-builder.js.map +1 -1
- package/dist/generators/trace-helper-file.d.ts.map +1 -1
- package/dist/generators/trace-helper-file.js +11 -5
- package/dist/generators/trace-helper-file.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/naming.d.ts +10 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +47 -0
- package/dist/naming.js.map +1 -1
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +12 -9
- package/dist/payload-codegen.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +26 -3
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/projection/projection-detector.d.ts.map +1 -1
- package/dist/projection/projection-detector.js +4 -0
- package/dist/projection/projection-detector.js.map +1 -1
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +20 -12
- package/dist/relation-resolver.js.map +1 -1
- package/dist/source-detect.d.ts.map +1 -1
- package/dist/source-detect.js +4 -1
- package/dist/source-detect.js.map +1 -1
- package/dist/template-codegen/template-data.js +1 -1
- package/dist/template-codegen/template-data.js.map +1 -1
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +8 -4
- package/dist/templates/callable-file.js.map +1 -1
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +15 -11
- 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 +7 -2
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +2 -1
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +8 -4
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +22 -4
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/find-templates.d.ts.map +1 -1
- package/dist/templates/find-templates.js +3 -0
- package/dist/templates/find-templates.js.map +1 -1
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +5 -2
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +18 -14
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-format-spec-emitter.js +7 -3
- 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 +9 -5
- 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 +8 -4
- package/dist/templates/output-prompt.js.map +1 -1
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +4 -1
- 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 +12 -1
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/queries.d.ts +19 -1
- package/dist/templates/queries.d.ts.map +1 -1
- package/dist/templates/queries.js +49 -7
- package/dist/templates/queries.js.map +1 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +17 -10
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file.js +2 -1
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/tph-discriminator.d.ts.map +1 -1
- package/dist/templates/tph-discriminator.js +7 -0
- package/dist/templates/tph-discriminator.js.map +1 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +62 -16
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/column-mapper.ts +37 -16
- package/src/generators/api-model.ts +21 -13
- package/src/generators/docs-data-builder.ts +27 -22
- package/src/generators/docs-file.ts +2 -1
- package/src/generators/extractor-file.ts +4 -2
- package/src/generators/output-prompt-file.ts +5 -3
- package/src/generators/render-helper-file.ts +5 -3
- package/src/generators/routes-file-hono.ts +2 -1
- package/src/generators/routes-file.ts +2 -1
- package/src/generators/template-doc-builder.ts +14 -11
- package/src/generators/trace-helper-file.ts +11 -5
- package/src/index.ts +2 -0
- package/src/naming.ts +51 -0
- package/src/payload-codegen.ts +12 -9
- package/src/projection/extract-view-spec.ts +26 -3
- package/src/projection/projection-detector.ts +4 -0
- package/src/reference/queries.ts +9 -0
- package/src/reference/routes.ts +2 -1
- package/src/relation-resolver.ts +20 -12
- package/src/source-detect.ts +4 -1
- package/src/template-codegen/template-data.ts +1 -1
- package/src/templates/callable-file.ts +8 -4
- package/src/templates/entity-constants.ts +15 -11
- package/src/templates/entity-file.ts +7 -2
- package/src/templates/extract-delegate-emitter.ts +2 -1
- package/src/templates/extractor.ts +8 -4
- package/src/templates/field-meta.ts +25 -3
- package/src/templates/find-templates.ts +3 -0
- package/src/templates/fr010-field-mapping.ts +6 -1
- package/src/templates/inferred-types.ts +19 -13
- package/src/templates/output-format-spec-emitter.ts +7 -3
- package/src/templates/output-parser.ts +9 -5
- package/src/templates/output-prompt.ts +8 -4
- package/src/templates/projection-decl.ts +4 -1
- package/src/templates/queries-file.ts +14 -0
- package/src/templates/queries.ts +68 -8
- package/src/templates/render-helper.ts +17 -10
- package/src/templates/routes-file.ts +2 -1
- package/src/templates/tph-discriminator.ts +7 -0
- package/src/templates/zod-validators.ts +64 -15
package/src/naming.ts
CHANGED
|
@@ -121,6 +121,57 @@ export function deleteByIdFnName(entityName: string): string {
|
|
|
121
121
|
return `delete${entityName}ById`;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// ADR-0038 — reverse-relationship navigation as explicit FK finders.
|
|
126
|
+
//
|
|
127
|
+
// For each FK relationship (entity `E` references entity `T` via an
|
|
128
|
+
// `identity.reference` FK field), `E`'s query surface gains a finder returning
|
|
129
|
+
// the `E` rows matching a given `T` id. Two variants: a single-value finder and
|
|
130
|
+
// a batched (anti-N+1) `…In` finder.
|
|
131
|
+
//
|
|
132
|
+
// CANONICAL NAMING CONVENTION (the cross-port contract — every port replicates
|
|
133
|
+
// this spelling exactly):
|
|
134
|
+
//
|
|
135
|
+
// find<EPlural>By<FkField>(value) → SELECT … FROM E WHERE <fkColumn> = ?
|
|
136
|
+
// find<EPlural>By<FkField>In(values) → SELECT … FROM E WHERE <fkColumn> IN (…)
|
|
137
|
+
//
|
|
138
|
+
// where:
|
|
139
|
+
// - <EPlural> is the source entity name pluralized, PascalCase
|
|
140
|
+
// (`GameSession` → `GameSessions`) — the same spelling `list<Plural>` uses.
|
|
141
|
+
// - <FkField> is the FK FIELD name (NOT the relationship/navigation name and
|
|
142
|
+
// NOT the raw column), PascalCased, with a single trailing `Id` dropped if
|
|
143
|
+
// present. The FK field name is unique within an entity, so the finder name
|
|
144
|
+
// is unique by construction — this is what dissolves the same-pair collision
|
|
145
|
+
// and removes any need for a naming attribute.
|
|
146
|
+
//
|
|
147
|
+
// SAME-PAIR EXAMPLE (GameSession has THREE FKs to Scene):
|
|
148
|
+
// FK field `currentSceneId` → findGameSessionsByCurrentScene
|
|
149
|
+
// FK field `lastOpeningNarrativeSceneId` → findGameSessionsByLastOpeningNarrativeScene
|
|
150
|
+
// FK field `transitioningFromSceneId` → findGameSessionsByTransitioningFromScene
|
|
151
|
+
// Three distinct finders — no collision.
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Lower an FK field name to the `<FkField>` segment of a reverse finder name:
|
|
156
|
+
* PascalCase the field, then drop a single trailing `Id` if present.
|
|
157
|
+
* E.g. `currentSceneId` → `CurrentScene`, `authorId` → `Author`, `scene` → `Scene`.
|
|
158
|
+
*/
|
|
159
|
+
export function reverseFinderFkSegment(fkFieldName: string): string {
|
|
160
|
+
const pascal = toPascalCase(toCamelCase(fkFieldName));
|
|
161
|
+
// Drop a single trailing "Id" (but not a bare "Id" — that would yield "").
|
|
162
|
+
return pascal.length > 2 && pascal.endsWith("Id") ? pascal.slice(0, -2) : pascal;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Generated reverse single-value finder name: `find<EPlural>By<FkField>`. */
|
|
166
|
+
export function reverseFinderFnName(sourceEntityName: string, fkFieldName: string): string {
|
|
167
|
+
return `find${pluralize(sourceEntityName)}By${reverseFinderFkSegment(fkFieldName)}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Generated reverse batched finder name: `find<EPlural>By<FkField>In`. */
|
|
171
|
+
export function reverseFinderInFnName(sourceEntityName: string, fkFieldName: string): string {
|
|
172
|
+
return `${reverseFinderFnName(sourceEntityName, fkFieldName)}In`;
|
|
173
|
+
}
|
|
174
|
+
|
|
124
175
|
/**
|
|
125
176
|
* Generated Fastify route-registrar name: camelCase `<entity>Routes`. The routes
|
|
126
177
|
* generator (templates/routes-file.ts) emits a single exported
|
package/src/payload-codegen.ts
CHANGED
|
@@ -46,10 +46,11 @@ const SCALAR_TS: Record<string, string> = {
|
|
|
46
46
|
timestamp: "string",
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
49
50
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
50
51
|
// FR-032 — @payloadRef/@responseRef are FQN after the desugar/sweep; match on
|
|
51
52
|
// the effective FQN resolution key (with bare back-compat).
|
|
52
|
-
return root.
|
|
53
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -68,7 +69,7 @@ function fieldTsType(
|
|
|
68
69
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
69
70
|
const refName = typeof ref === "string" ? ref : "unknown";
|
|
70
71
|
// isArray is a structural property on MetaData, not an attr.
|
|
71
|
-
const isArray = field.
|
|
72
|
+
const isArray = field.resolvedIsArray();
|
|
72
73
|
const result: { type: string; refVo?: string } = {
|
|
73
74
|
type: isArray ? `${refName}[]` : refName,
|
|
74
75
|
};
|
|
@@ -83,15 +84,15 @@ function fieldTsType(
|
|
|
83
84
|
if (values !== undefined) {
|
|
84
85
|
const aliasName = enumUnionAliasName(ownerName, field as MetaField);
|
|
85
86
|
return {
|
|
86
|
-
type: field.
|
|
87
|
+
type: field.resolvedIsArray() ? `${aliasName}[]` : aliasName,
|
|
87
88
|
enumAlias: { name: aliasName, decl: `export type ${aliasName} = ${enumUnionString(values)};` },
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
// No @values → fall through to the raw-string representation.
|
|
91
|
-
return { type: field.
|
|
92
|
+
return { type: field.resolvedIsArray() ? "string[]" : "string" };
|
|
92
93
|
}
|
|
93
94
|
const scalar = SCALAR_TS[field.subType] ?? "unknown";
|
|
94
|
-
return { type: field.
|
|
95
|
+
return { type: field.resolvedIsArray() ? `${scalar}[]` : scalar };
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/** True iff the field's @required is explicitly set to true. */
|
|
@@ -169,9 +170,10 @@ function pascal(s: string): string {
|
|
|
169
170
|
|
|
170
171
|
/** Emit a typed render handle binding a template's @textRef + @format and typing its payload. */
|
|
171
172
|
export function generateRenderHandle(root: MetaData, templateName: string): string {
|
|
172
|
-
const tmpl = root.
|
|
173
|
+
const tmpl = root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === templateName);
|
|
173
174
|
if (!tmpl) throw new Error(`template "${templateName}" not found`);
|
|
174
|
-
|
|
175
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
176
|
+
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
175
177
|
// FR-032 — @payloadRef is an FQN after the desugar/sweep; the generated TS
|
|
176
178
|
// TYPE NAME is the resolved value-object's bare name (an FQN like
|
|
177
179
|
// `acme::ai::Payload` is not a valid TS identifier). Fall back to the last
|
|
@@ -179,8 +181,9 @@ export function generateRenderHandle(root: MetaData, templateName: string): stri
|
|
|
179
181
|
const payloadType =
|
|
180
182
|
(typeof payloadRef === "string" ? findObject(root, payloadRef)?.name : undefined) ??
|
|
181
183
|
(typeof payloadRef === "string" ? stripPackage(payloadRef) : String(payloadRef));
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
185
|
+
const textRef = tmpl.attr(TEMPLATE_ATTR_TEXT_REF);
|
|
186
|
+
const format = (tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
|
|
184
187
|
const fn = `render${pascal(templateName)}`;
|
|
185
188
|
return [
|
|
186
189
|
`import { render, type Provider } from "@metaobjectsdev/render";`,
|
|
@@ -100,7 +100,9 @@ export interface ExtractContext {
|
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
102
|
function findRelationship(obj: MetaData, name: string): MetaData | undefined {
|
|
103
|
-
|
|
103
|
+
// ADR-0039: resolving — a @via join hop may traverse a relationship the entity
|
|
104
|
+
// inherits via extends; own-only would silently drop the join.
|
|
105
|
+
return obj.children().find(
|
|
104
106
|
(c) => c.type === TYPE_RELATIONSHIP && c.name === name,
|
|
105
107
|
);
|
|
106
108
|
}
|
|
@@ -110,6 +112,9 @@ function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
|
110
112
|
// implements the four-step rule (kind-matching alias → legacy @table →
|
|
111
113
|
// source.name → entity-name fallback), so the call below correctly resolves
|
|
112
114
|
// @view / @materializedView / legacy @table for projection sources.
|
|
115
|
+
// ADR-0039: own — projection source classification (mirrors C# projection
|
|
116
|
+
// OwnSources / IsReadOnlyProjection): the view name comes from the projection's
|
|
117
|
+
// OWN read-only source, not one inherited via extends.
|
|
113
118
|
const viewSource = projection.ownChildren().find(
|
|
114
119
|
(c): c is MetaSource => c instanceof MetaSource && c.isReadOnly(),
|
|
115
120
|
);
|
|
@@ -171,6 +176,10 @@ function baseEntityFor(
|
|
|
171
176
|
// ERR_PROJECTION_IDENTITY_NOT_EXTENDED) rejects any projection whose identity
|
|
172
177
|
// is not dotted-extends-bound before codegen runs. If that loader gate is
|
|
173
178
|
// ever loosened, this function must grow the same fallback.
|
|
179
|
+
// ADR-0039: own — the base anchor is derived from the projection's OWN
|
|
180
|
+
// extends-bound declarations (refNamedOwner reads each node's own superRef,
|
|
181
|
+
// which only exists on locally-declared nodes); an inherited node carries no
|
|
182
|
+
// projection-local superRef, so own-iteration is correct here.
|
|
174
183
|
for (const identity of projection
|
|
175
184
|
.ownChildren()
|
|
176
185
|
.filter((c) => c.type === TYPE_IDENTITY)) {
|
|
@@ -178,6 +187,7 @@ function baseEntityFor(
|
|
|
178
187
|
if (named !== undefined) return named;
|
|
179
188
|
}
|
|
180
189
|
const targets = new Set<MetaObject>();
|
|
190
|
+
// ADR-0039: own — see above; base-entity derivation reads own extends-bound fields.
|
|
181
191
|
for (const f of projection.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
182
192
|
const named = refNamedOwner(f, root);
|
|
183
193
|
if (named !== undefined && named !== projection) targets.add(named);
|
|
@@ -253,10 +263,15 @@ function buildJoinTree(
|
|
|
253
263
|
): JoinTree {
|
|
254
264
|
const allPaths: Path[] = [];
|
|
255
265
|
|
|
266
|
+
// ADR-0039: own — a projection's DECLARED field set IS the exposure (FR-024/
|
|
267
|
+
// ADR-0028, inclusive + fail-closed); iterate the projection's own fields and
|
|
268
|
+
// each field's own origin. origin.* NEVER inherits (ADR-0029), so origin reads
|
|
269
|
+
// below are own by policy (category 4).
|
|
256
270
|
for (const field of projection.ownChildren()) {
|
|
257
271
|
if (field.type !== TYPE_FIELD) continue;
|
|
258
272
|
for (const origin of field.ownChildren()) {
|
|
259
273
|
if (origin.type !== TYPE_ORIGIN) continue;
|
|
274
|
+
// ADR-0039: own (category 4) — origin.* never inherits (ADR-0029).
|
|
260
275
|
const viaAttr = origin.subType === ORIGIN_SUBTYPE_AGGREGATE
|
|
261
276
|
? (origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA) as string | undefined)
|
|
262
277
|
: (origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA) as string | undefined);
|
|
@@ -276,7 +291,8 @@ function buildJoinTree(
|
|
|
276
291
|
for (const relName of relSegments) {
|
|
277
292
|
const rel = findRelationship(currentObj, relName);
|
|
278
293
|
if (!rel) break;
|
|
279
|
-
|
|
294
|
+
// ADR-0039: resolving — a traversed relationship may inherit @objectRef via extends.
|
|
295
|
+
const targetName = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
280
296
|
// @objectRef may be package-qualified ("pkg::Entity") — the directory loader
|
|
281
297
|
// qualifies a same-package objectRef even when authored bare — but findObject
|
|
282
298
|
// keys on the BARE name (like the @via/@of/@from paths above). Strip first, or
|
|
@@ -284,7 +300,8 @@ function buildJoinTree(
|
|
|
284
300
|
const target = targetName ? root.findObject(stripPackage(targetName)) : undefined;
|
|
285
301
|
if (!target || !targetName) break;
|
|
286
302
|
|
|
287
|
-
|
|
303
|
+
// ADR-0039: resolving — a traversed relationship may inherit @cardinality via extends.
|
|
304
|
+
const cardAttr = rel.attr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
|
|
288
305
|
const cardinality: "one" | "many" = cardAttr === CARDINALITY_ONE ? "one" : "many";
|
|
289
306
|
|
|
290
307
|
const ref = findReferenceBetween(currentObj as MetaObject, target);
|
|
@@ -391,6 +408,9 @@ function buildSelectSpec(
|
|
|
391
408
|
// extends-bound fields (`{ field.int: { name: id, extends: "Program.id" } }`).
|
|
392
409
|
|
|
393
410
|
// Fields explicitly declared on the projection.
|
|
411
|
+
// ADR-0039: own — the projection's DECLARED field set IS the exposure
|
|
412
|
+
// (FR-024/ADR-0028); iterate own fields + each field's own origin. origin.*
|
|
413
|
+
// NEVER inherits (ADR-0029), so the origin attr reads below are own (category 4).
|
|
394
414
|
for (const field of projection.ownChildren()) {
|
|
395
415
|
if (field.type !== TYPE_FIELD) continue;
|
|
396
416
|
const origin = field.ownChildren().find((c) => c.type === TYPE_ORIGIN);
|
|
@@ -409,6 +429,7 @@ function buildSelectSpec(
|
|
|
409
429
|
}
|
|
410
430
|
|
|
411
431
|
if (origin.subType === ORIGIN_SUBTYPE_PASSTHROUGH) {
|
|
432
|
+
// ADR-0039: own (category 4) — origin.* never inherits (ADR-0029).
|
|
412
433
|
const from = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_FROM) as string;
|
|
413
434
|
const dotIdx = from.indexOf(".");
|
|
414
435
|
if (dotIdx < 1) continue;
|
|
@@ -432,6 +453,7 @@ function buildSelectSpec(
|
|
|
432
453
|
sourceColumn: sourceColumnNameFor(targetField, ctx),
|
|
433
454
|
});
|
|
434
455
|
} else if (origin.subType === ORIGIN_SUBTYPE_AGGREGATE) {
|
|
456
|
+
// ADR-0039: own (category 4) — origin.* never inherits (ADR-0029).
|
|
435
457
|
const agg = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_AGG) as AggregateFunction;
|
|
436
458
|
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF) as string;
|
|
437
459
|
if (!agg || !of_) continue;
|
|
@@ -446,6 +468,7 @@ function buildSelectSpec(
|
|
|
446
468
|
if (!targetField) continue;
|
|
447
469
|
// Optional scoping filter — resolved against the aggregated entity (the @of
|
|
448
470
|
// entity, reached at `sourceAlias`), e.g. max(version) over only active rows.
|
|
471
|
+
// ADR-0039: own (category 4) — origin.* never inherits (ADR-0029).
|
|
449
472
|
const filterAttr = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_FILTER);
|
|
450
473
|
const filter = filterAttr !== undefined
|
|
451
474
|
? resolveAggregateFilter(filterAttr, targetEntity, sourceAlias, ctx)
|
|
@@ -2,12 +2,16 @@ import { MetaSource } from "@metaobjectsdev/metadata";
|
|
|
2
2
|
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
3
3
|
|
|
4
4
|
function hasReadOnlyKindSource(entity: MetaData): boolean {
|
|
5
|
+
// ADR-0039: own — projection source-kind classification. Mirrors C#
|
|
6
|
+
// IsReadOnlyProjection()/projection OwnSources: an entity's projection-ness is
|
|
7
|
+
// determined by its OWN declared source @kind, not one inherited via extends.
|
|
5
8
|
return entity.ownChildren().some(
|
|
6
9
|
(c) => c instanceof MetaSource && c.isReadOnly(),
|
|
7
10
|
);
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
function hasWritableKindSource(entity: MetaData): boolean {
|
|
14
|
+
// ADR-0039: own — projection source-kind classification (see hasReadOnlyKindSource).
|
|
11
15
|
return entity.ownChildren().some(
|
|
12
16
|
(c) => c instanceof MetaSource && c.isWritable(),
|
|
13
17
|
);
|
package/src/reference/queries.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
renderCreateFn,
|
|
28
28
|
renderUpdateFn,
|
|
29
29
|
renderDeleteByIdFn,
|
|
30
|
+
renderReverseFinderFns,
|
|
31
|
+
reverseFksFor,
|
|
30
32
|
isTphDiscriminatorBase,
|
|
31
33
|
isProjection,
|
|
32
34
|
isTphSubtype,
|
|
@@ -80,6 +82,13 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
|
|
|
80
82
|
renderDeleteByIdFn(obj, ctx),
|
|
81
83
|
];
|
|
82
84
|
|
|
85
|
+
// ADR-0038 — reverse-relationship navigation as explicit FK finders. One
|
|
86
|
+
// find<Plural>By<FkField> (+ batched …In) per FK this entity holds. OWNED:
|
|
87
|
+
// drop this loop if you don't want reverse finders.
|
|
88
|
+
for (const fk of reverseFksFor(obj)) {
|
|
89
|
+
sections.push(renderReverseFinderFns(obj, fk, ctx));
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
84
93
|
const header =
|
|
85
94
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
package/src/reference/routes.ts
CHANGED
|
@@ -40,7 +40,8 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
|
|
|
40
40
|
// per-entity opt-out via `@emitRoutes: false`; TPH subtypes get no standalone routes
|
|
41
41
|
// file (their routes live in the discriminator base's); AND-composed with your filter.
|
|
42
42
|
filter: (e: MetaObject) =>
|
|
43
|
-
|
|
43
|
+
// ADR-0039: resolving — a concrete entity may inherit its @emit* opt-out flag via extends.
|
|
44
|
+
e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && !isTphSubtype(e) && userFilter(e),
|
|
44
45
|
generate: perEntity(async (entity, ctx) => {
|
|
45
46
|
if (!ctx.renderContext) {
|
|
46
47
|
throw new Error("routes-file: renderContext is required (provided by runGen)");
|
package/src/relation-resolver.ts
CHANGED
|
@@ -69,12 +69,14 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
69
69
|
if (isProjection(obj)) continue;
|
|
70
70
|
|
|
71
71
|
for (const child of obj.relationships()) {
|
|
72
|
-
|
|
72
|
+
// ADR-0039: resolving — a relationship may inherit @cardinality via extends.
|
|
73
|
+
const cardinality = child.attr(RELATIONSHIP_ATTR_CARDINALITY) as string | undefined;
|
|
73
74
|
|
|
74
75
|
// FR-018 M:N: `@cardinality: "many"` + `@through` — derive the junction FK
|
|
75
76
|
// columns from the junction's identity.reference children and register a
|
|
76
77
|
// many(junction) navigation on the source.
|
|
77
|
-
|
|
78
|
+
// ADR-0039: resolving — @through may be inherited via extends.
|
|
79
|
+
if (cardinality === CARDINALITY_MANY && child.attr(RELATIONSHIP_ATTR_THROUGH) !== undefined) {
|
|
78
80
|
const m2m = buildM2mEntry(obj, child as MetaRelationship, root);
|
|
79
81
|
if (m2m) ensure(obj.name).push(m2m);
|
|
80
82
|
continue;
|
|
@@ -82,7 +84,8 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
82
84
|
|
|
83
85
|
if (cardinality !== CARDINALITY_ONE) continue;
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
// ADR-0039: resolving — @objectRef may be inherited via extends.
|
|
88
|
+
const targetEntityRaw = child.attr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
86
89
|
if (!targetEntityRaw) continue;
|
|
87
90
|
const targetEntity = stripPackage(targetEntityRaw);
|
|
88
91
|
|
|
@@ -104,11 +107,14 @@ export function buildRelationMap(root: MetaRoot): RelationMap {
|
|
|
104
107
|
fkField,
|
|
105
108
|
targetPkField: "id",
|
|
106
109
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
// ADR-0038: do NOT register a reverse lazy `many()` on the target. Those
|
|
111
|
+
// entries were named after the SOURCE entity (`variableNameFromEntity(obj)`),
|
|
112
|
+
// so two relationships from the same source to the same target produced
|
|
113
|
+
// duplicate object-literal keys and silently overwrote each other (the
|
|
114
|
+
// same-pair collision). Reverse 1:N navigation is now provided as explicit,
|
|
115
|
+
// unique-by-FK-field finders in the source's queries module
|
|
116
|
+
// (find<SourcePlural>By<FkField>), which are framework-free and non-N+1.
|
|
117
|
+
// The forward one() relation above stays.
|
|
112
118
|
}
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -149,8 +155,9 @@ function collectJunctionNames(root: MetaRoot): Set<string> {
|
|
|
149
155
|
const names = new Set<string>();
|
|
150
156
|
for (const obj of root.objects()) {
|
|
151
157
|
for (const rel of obj.relationships()) {
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
// ADR-0039: resolving — @cardinality/@through may be inherited via extends.
|
|
159
|
+
if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
|
|
160
|
+
const through = rel.attr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
|
|
154
161
|
if (through) names.add(stripPackage(through));
|
|
155
162
|
}
|
|
156
163
|
}
|
|
@@ -168,8 +175,9 @@ function buildM2mEntry(
|
|
|
168
175
|
rel: MetaRelationship,
|
|
169
176
|
root: MetaRoot,
|
|
170
177
|
): RelationEntry | null {
|
|
171
|
-
|
|
172
|
-
const
|
|
178
|
+
// ADR-0039: resolving — @objectRef/@through may be inherited via extends.
|
|
179
|
+
const targetRaw = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF) as string | undefined;
|
|
180
|
+
const throughRaw = rel.attr(RELATIONSHIP_ATTR_THROUGH) as string | undefined;
|
|
173
181
|
if (!targetRaw || !throughRaw) return null;
|
|
174
182
|
let fields;
|
|
175
183
|
try {
|
package/src/source-detect.ts
CHANGED
|
@@ -18,7 +18,10 @@ import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
|
18
18
|
* transit data — no migration, no ORM table to point at.
|
|
19
19
|
*/
|
|
20
20
|
export function hasWritableRdbSource(entity: MetaObject): boolean {
|
|
21
|
-
|
|
21
|
+
// ADR-0039: resolving — an entity may inherit its writable source.rdb via extends
|
|
22
|
+
// (the JVM port's "entity inheriting its source emitted NOTHING" bug); own-only
|
|
23
|
+
// would suppress the Drizzle table for such an entity.
|
|
24
|
+
for (const child of entity.children()) {
|
|
22
25
|
if (child.type !== TYPE_SOURCE) continue;
|
|
23
26
|
if (child.subType !== SOURCE_SUBTYPE_RDB) continue;
|
|
24
27
|
if (!(child instanceof MetaSource)) continue;
|
|
@@ -41,7 +41,7 @@ function fieldData(field: MetaField): FieldTemplateData {
|
|
|
41
41
|
name: field.name,
|
|
42
42
|
type: field.subType,
|
|
43
43
|
required: field.isRequired,
|
|
44
|
-
isArray: field.
|
|
44
|
+
isArray: field.resolvedIsArray(),
|
|
45
45
|
};
|
|
46
46
|
if (typeof field.maxLength === "number") d.maxLength = field.maxLength;
|
|
47
47
|
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
@@ -44,7 +44,8 @@ export function isCallableEntity(entity: MetaObject): boolean {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function callableSource(entity: MetaObject): MetaSource | undefined {
|
|
47
|
-
|
|
47
|
+
// ADR-0039: resolving — an entity may inherit its callable source.rdb via extends.
|
|
48
|
+
for (const child of entity.children()) {
|
|
48
49
|
if (child.type !== TYPE_SOURCE) continue;
|
|
49
50
|
if (!(child instanceof MetaSource)) continue;
|
|
50
51
|
if (CALLABLE_KINDS.has(child.effectiveKind)) return child;
|
|
@@ -63,14 +64,16 @@ export function renderCallableFile(entity: MetaObject): string {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
const procName = source.physicalName;
|
|
66
|
-
|
|
67
|
+
// ADR-0039: resolving — a source may inherit @parameterRef via extends.
|
|
68
|
+
const argsRef = source.attr(SOURCE_ATTR_PARAMETER_REF);
|
|
67
69
|
const argsObjectName = typeof argsRef === "string" && argsRef !== "" ? argsRef : undefined;
|
|
68
70
|
|
|
69
71
|
// Resolve the parameter value-object (when set) — same root as the entity.
|
|
70
72
|
const root = entity.root();
|
|
73
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()).
|
|
71
74
|
const argsObject =
|
|
72
75
|
argsObjectName !== undefined
|
|
73
|
-
? (root.
|
|
76
|
+
? (root.children().find(
|
|
74
77
|
(c) =>
|
|
75
78
|
c.subType === OBJECT_SUBTYPE_VALUE && c.name === argsObjectName,
|
|
76
79
|
) as MetaObject | undefined)
|
|
@@ -78,8 +81,9 @@ export function renderCallableFile(entity: MetaObject): string {
|
|
|
78
81
|
|
|
79
82
|
// Build the parameter list for the SQL call site. Declaration order = arg
|
|
80
83
|
// order. Empty when argsObject is undefined (zero-arg proc).
|
|
84
|
+
// ADR-0039: resolving — a value object may inherit fields via extends (shape reuse).
|
|
81
85
|
const paramFieldNames: string[] = argsObject
|
|
82
|
-
? argsObject.
|
|
86
|
+
? argsObject.children()
|
|
83
87
|
.filter((c) => c.type === TYPE_FIELD)
|
|
84
88
|
.map((f) => f.name)
|
|
85
89
|
: [];
|
|
@@ -68,7 +68,8 @@ function humanize(s: string): string {
|
|
|
68
68
|
* "WorkoutEvent" → "/workout_events"
|
|
69
69
|
*/
|
|
70
70
|
export function resourcePath(entity: MetaData): string {
|
|
71
|
-
|
|
71
|
+
// ADR-0039: resolving — a concrete entity may inherit @routePath via extends.
|
|
72
|
+
const overrideAttr = entity.attr("routePath");
|
|
72
73
|
if (typeof overrideAttr === "string" && overrideAttr.length > 0) {
|
|
73
74
|
return overrideAttr.startsWith("/") ? overrideAttr : `/${overrideAttr}`;
|
|
74
75
|
}
|
|
@@ -130,27 +131,29 @@ function renderFieldRules(field: MetaField): string | undefined {
|
|
|
130
131
|
let hasRequired = false;
|
|
131
132
|
let hasMaxLength = false;
|
|
132
133
|
|
|
134
|
+
// ADR-0039: resolving — a validator may inherit its config attrs (@min/@max/
|
|
135
|
+
// @pattern/@message/…) via extends.
|
|
133
136
|
for (const child of field.validators()) {
|
|
134
137
|
if (child.subType === VALIDATOR_SUBTYPE_REQUIRED) {
|
|
135
|
-
const msg = (child.
|
|
138
|
+
const msg = (child.attr("message") as string | undefined) ?? `${humanize(field.name)} is required`;
|
|
136
139
|
ruleParts.push(`required: ${JSON.stringify(msg)}`);
|
|
137
140
|
hasRequired = true;
|
|
138
141
|
} else if (child.subType === VALIDATOR_SUBTYPE_LENGTH) {
|
|
139
|
-
const min = child.
|
|
140
|
-
const max = child.
|
|
142
|
+
const min = child.attr(VALIDATOR_ATTR_MIN);
|
|
143
|
+
const max = child.attr(VALIDATOR_ATTR_MAX);
|
|
141
144
|
if (typeof min === "number") {
|
|
142
|
-
const msg = (child.
|
|
145
|
+
const msg = (child.attr("minMessage") as string | undefined) ?? `Must be at least ${min} characters`;
|
|
143
146
|
ruleParts.push(`minLength: { value: ${min}, message: ${JSON.stringify(msg)} }`);
|
|
144
147
|
}
|
|
145
148
|
if (typeof max === "number") {
|
|
146
|
-
const msg = (child.
|
|
149
|
+
const msg = (child.attr("maxMessage") as string | undefined) ?? `Must be ${max} characters or fewer`;
|
|
147
150
|
ruleParts.push(`maxLength: { value: ${max}, message: ${JSON.stringify(msg)} }`);
|
|
148
151
|
hasMaxLength = true;
|
|
149
152
|
}
|
|
150
153
|
} else if (child.subType === VALIDATOR_SUBTYPE_REGEX) {
|
|
151
|
-
const pattern = child.
|
|
154
|
+
const pattern = child.attr(VALIDATOR_ATTR_PATTERN);
|
|
152
155
|
if (typeof pattern === "string") {
|
|
153
|
-
const msg = (child.
|
|
156
|
+
const msg = (child.attr("message") as string | undefined) ?? "Invalid format";
|
|
154
157
|
// Emit as RegExp literal /.../ — `as const` preserves the value-ref.
|
|
155
158
|
// Forward-slash inside the pattern is escaped so the literal closes correctly.
|
|
156
159
|
const safe = pattern.replace(/\\/g, "\\\\").replace(/\//g, "\\/");
|
|
@@ -180,9 +183,10 @@ function renderFieldRules(field: MetaField): string | undefined {
|
|
|
180
183
|
function renderFieldEntry(field: MetaField): string {
|
|
181
184
|
const { view, viewNode } = resolveView(field);
|
|
182
185
|
const label = labelFor(field);
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
+
// ADR-0039: resolving — a view node may inherit @placeholder/@helpText/@htmlType via extends.
|
|
187
|
+
const placeholder = viewNode?.attr("placeholder") as string | undefined;
|
|
188
|
+
const helpText = viewNode?.attr("helpText") as string | undefined;
|
|
189
|
+
const htmlType = htmlTypeFromView(view, viewNode?.attr("htmlType") as string | undefined);
|
|
186
190
|
const rules = renderFieldRules(field);
|
|
187
191
|
|
|
188
192
|
const entries: string[] = [
|
|
@@ -11,7 +11,7 @@ import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
|
11
11
|
import type { RenderContext } from "../render-context.js";
|
|
12
12
|
import { renderDrizzleSchema } from "./drizzle-schema.js";
|
|
13
13
|
import { renderInferredTypes, renderEnumTypeAliases } from "./inferred-types.js";
|
|
14
|
-
import { renderZodValidators } from "./zod-validators.js";
|
|
14
|
+
import { renderZodValidators, isTphSubtype } from "./zod-validators.js";
|
|
15
15
|
import { renderEntityConstants } from "./entity-constants.js";
|
|
16
16
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
17
17
|
import { renderFilterType } from "./filter-type.js";
|
|
@@ -88,7 +88,12 @@ export function renderEntityFile(
|
|
|
88
88
|
// shape) OR the target is contract-only (a UI/wire package gets the read shape,
|
|
89
89
|
// not a DB table). Either way: interface + Zod, no migration footprint, no
|
|
90
90
|
// drizzle-orm. Consumers validate via the Zod schema and type via the interface.
|
|
91
|
-
|
|
91
|
+
//
|
|
92
|
+
// A TPH subtype (FR-017) also routes here: it inherits the base's writable
|
|
93
|
+
// source.rdb via extends (so hasWritableRdbSource is now true under the ADR-0039
|
|
94
|
+
// resolving read), but the base owns the single shared table — the subtype must
|
|
95
|
+
// emit its per-subtype read schema (<Sub>Schema), never its own pgTable.
|
|
96
|
+
if (!runtime || !hasWritableRdbSource(entity) || isTphSubtype(entity)) {
|
|
92
97
|
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
93
98
|
}
|
|
94
99
|
|
|
@@ -31,8 +31,9 @@ import {
|
|
|
31
31
|
} from "@metaobjectsdev/metadata";
|
|
32
32
|
import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
|
|
33
33
|
|
|
34
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
34
35
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
35
|
-
return root.
|
|
36
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
@@ -40,12 +40,14 @@ import { mirrorName } from "./extract-delegate-emitter.js";
|
|
|
40
40
|
import { enumUnionAliasName } from "./inferred-types.js";
|
|
41
41
|
import { enumValues } from "../enum-meta.js";
|
|
42
42
|
|
|
43
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
43
44
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
44
|
-
return root.
|
|
45
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
47
49
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
48
|
-
return root.
|
|
50
|
+
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
/** The @objectRef target VO for a nested-object field, or undefined when unresolvable. */
|
|
@@ -288,7 +290,8 @@ export function renderExtractor(root: MetaData, templateName: string): string {
|
|
|
288
290
|
if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
|
|
289
291
|
throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
|
|
290
292
|
}
|
|
291
|
-
|
|
293
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
294
|
+
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
292
295
|
if (typeof payloadRef !== "string") {
|
|
293
296
|
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
294
297
|
}
|
|
@@ -296,7 +299,8 @@ export function renderExtractor(root: MetaData, templateName: string): string {
|
|
|
296
299
|
if (!vo) {
|
|
297
300
|
throw new Error(`template "${templateName}" @payloadRef "${payloadRef}" not found in metadata root`);
|
|
298
301
|
}
|
|
299
|
-
|
|
302
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
303
|
+
const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
300
304
|
if (format !== "json" && format !== "xml") {
|
|
301
305
|
throw new Error(
|
|
302
306
|
`template "${templateName}" @format "${format}" has no extract API to extract over (json/xml only)`,
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
FIELD_SUBTYPE_CURRENCY,
|
|
19
19
|
FIELD_SUBTYPE_ENUM,
|
|
20
20
|
FIELD_SUBTYPE_UUID,
|
|
21
|
+
FIELD_SUBTYPE_URI,
|
|
22
|
+
FIELD_SUBTYPE_INET,
|
|
23
|
+
FIELD_ATTR_STRING_FORMAT,
|
|
24
|
+
STRING_FORMAT_EMAIL,
|
|
25
|
+
STRING_FORMAT_HOSTNAME,
|
|
21
26
|
VIEW_SUBTYPE_TEXT,
|
|
22
27
|
VIEW_SUBTYPE_DATE,
|
|
23
28
|
VIEW_SUBTYPE_NUMBER,
|
|
@@ -76,9 +81,24 @@ function defaultViewForSubType(subType: string): string {
|
|
|
76
81
|
*/
|
|
77
82
|
export function zodTypeFor(field: MetaField): string {
|
|
78
83
|
switch (field.subType) {
|
|
79
|
-
case FIELD_SUBTYPE_STRING:
|
|
84
|
+
case FIELD_SUBTYPE_STRING: {
|
|
85
|
+
// ADR-0036/0037 Wave 3: @stringFormat narrows a plain string. Codegen owns
|
|
86
|
+
// the canonical matcher (mirrors zod-validators.ts).
|
|
87
|
+
const fmt = field.attr(FIELD_ATTR_STRING_FORMAT);
|
|
88
|
+
if (fmt === STRING_FORMAT_EMAIL) return "z.string().email()";
|
|
89
|
+
if (fmt === STRING_FORMAT_HOSTNAME) {
|
|
90
|
+
return "z.string().regex(/^(?=.{1,253}$)([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/)";
|
|
91
|
+
}
|
|
92
|
+
return "z.string()";
|
|
93
|
+
}
|
|
80
94
|
case FIELD_SUBTYPE_UUID:
|
|
81
95
|
return "z.string()";
|
|
96
|
+
case FIELD_SUBTYPE_URI:
|
|
97
|
+
// ADR-0036/0037 Wave 3: a URI string — codegen owns the URL matcher.
|
|
98
|
+
return "z.string().url()";
|
|
99
|
+
case FIELD_SUBTYPE_INET:
|
|
100
|
+
// ADR-0036/0037 Wave 3: an IP string — codegen owns the IP matcher (v4+v6).
|
|
101
|
+
return "z.string().ip()";
|
|
82
102
|
case FIELD_SUBTYPE_BOOLEAN:
|
|
83
103
|
return "z.boolean()";
|
|
84
104
|
case FIELD_SUBTYPE_DATE:
|
|
@@ -116,8 +136,9 @@ export function currencyMetaFor(field: MetaField): { currency: string; locale: s
|
|
|
116
136
|
const currency =
|
|
117
137
|
(field.attr(FIELD_ATTR_CURRENCY) as string | undefined) ?? FIELD_ATTR_CURRENCY_DEFAULT;
|
|
118
138
|
const viewChild = field.views().find((c) => c.subType === VIEW_SUBTYPE_CURRENCY);
|
|
139
|
+
// ADR-0039: resolving — a view may inherit @locale via extends.
|
|
119
140
|
const locale =
|
|
120
|
-
(viewChild?.
|
|
141
|
+
(viewChild?.attr(VIEW_CURRENCY_ATTR_LOCALE) as string | undefined) ??
|
|
121
142
|
VIEW_CURRENCY_ATTR_LOCALE_DEFAULT;
|
|
122
143
|
return { currency, locale };
|
|
123
144
|
}
|
|
@@ -132,7 +153,8 @@ export function currencyMetaFor(field: MetaField): { currency: string; locale: s
|
|
|
132
153
|
*/
|
|
133
154
|
export function labelFor(field: MetaField): string {
|
|
134
155
|
for (const child of field.views()) {
|
|
135
|
-
|
|
156
|
+
// ADR-0039: resolving — a view may inherit @label via extends.
|
|
157
|
+
const label = child.attr("label");
|
|
136
158
|
if (typeof label === "string" && label.length > 0) return label;
|
|
137
159
|
}
|
|
138
160
|
return humanize(field.name);
|
|
@@ -5,6 +5,9 @@ import { TYPE_TEMPLATE } from "@metaobjectsdev/metadata";
|
|
|
5
5
|
export function findTemplates(root: MetaData, subType: string): MetaData[] {
|
|
6
6
|
const out: MetaData[] = [];
|
|
7
7
|
const visit = (node: MetaData) => {
|
|
8
|
+
// ADR-0039: own — structural declaration walk. Enumerates every template node
|
|
9
|
+
// physically DECLARED in the tree exactly once (each → one emitted file);
|
|
10
|
+
// resolving would re-visit an inherited template on every subclass.
|
|
8
11
|
for (const child of node.ownChildren()) {
|
|
9
12
|
if (child.type === TYPE_TEMPLATE && child.subType === subType) out.push(child);
|
|
10
13
|
visit(child);
|