@metaobjectsdev/codegen-ts 0.20.9 → 0.20.10
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/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +9 -5
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/barrel.js +1 -1
- package/dist/generators/barrel.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +7 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/extractor-file.d.ts.map +1 -1
- package/dist/generators/extractor-file.js +6 -1
- package/dist/generators/extractor-file.js.map +1 -1
- package/dist/generators/output-parser-file.d.ts.map +1 -1
- package/dist/generators/output-parser-file.js +6 -1
- package/dist/generators/output-parser-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +11 -6
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +4 -0
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +6 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +4 -0
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +6 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/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/collision-names.d.ts +19 -0
- package/dist/naming/collision-names.d.ts.map +1 -0
- package/dist/naming/collision-names.js +79 -0
- package/dist/naming/collision-names.js.map +1 -0
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +5 -71
- package/dist/payload-codegen.js.map +1 -1
- package/dist/render-context.d.ts +35 -3
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +23 -0
- package/dist/render-context.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +36 -2
- package/dist/runner.js.map +1 -1
- package/dist/source-detect.d.ts +10 -0
- package/dist/source-detect.d.ts.map +1 -1
- package/dist/source-detect.js +22 -0
- package/dist/source-detect.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +14 -3
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +13 -3
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/extract-delegate-emitter.d.ts +9 -4
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
- package/dist/templates/extract-delegate-emitter.js +46 -30
- package/dist/templates/extract-delegate-emitter.js.map +1 -1
- package/dist/templates/extractor.d.ts +2 -1
- package/dist/templates/extractor.d.ts.map +1 -1
- package/dist/templates/extractor.js +48 -33
- package/dist/templates/extractor.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +39 -16
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-parser.d.ts +2 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +37 -12
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/projection-decl.d.ts +1 -1
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +20 -9
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +5 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +12 -2
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +21 -15
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +1 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +35 -22
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/generators/api-model.ts +9 -5
- package/src/generators/barrel.ts +1 -1
- package/src/generators/entity-file.ts +7 -1
- package/src/generators/extractor-file.ts +6 -1
- package/src/generators/output-parser-file.ts +6 -1
- package/src/generators/queries-file.ts +12 -7
- package/src/generators/routes-file-hono.ts +7 -1
- package/src/generators/routes-file.ts +6 -1
- package/src/index.ts +1 -1
- package/src/naming/collision-names.ts +87 -0
- package/src/payload-codegen.ts +4 -79
- package/src/reference/barrel.ts +1 -1
- package/src/reference/queries.ts +14 -6
- package/src/reference/routes.ts +8 -1
- package/src/render-context.ts +51 -3
- package/src/runner.ts +36 -4
- package/src/source-detect.ts +20 -0
- package/src/templates/drizzle-schema.ts +14 -3
- package/src/templates/entity-file.ts +14 -6
- package/src/templates/extract-delegate-emitter.ts +52 -27
- package/src/templates/extractor.ts +49 -32
- package/src/templates/inferred-types.ts +39 -17
- package/src/templates/output-parser.ts +40 -12
- package/src/templates/projection-decl.ts +20 -10
- package/src/templates/value-object-file.ts +5 -1
- package/src/templates/view-decl.ts +30 -18
- package/src/templates/zod-validators.ts +35 -23
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
resolveObjectRef,
|
|
29
29
|
} from "@metaobjectsdev/metadata";
|
|
30
30
|
import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
|
|
31
|
+
import type { RenderContext } from "../render-context.js";
|
|
31
32
|
|
|
32
33
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
33
34
|
// ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
|
|
@@ -50,14 +51,20 @@ function isObjectField(field: MetaData): boolean {
|
|
|
50
51
|
return field.subType === FIELD_SUBTYPE_OBJECT;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/** The extracted-mirror interface name for a value-object (`<Name>Extracted`).
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/** The extracted-mirror interface name for a value-object (`<Name>Extracted`). ADR-0044/#228:
|
|
55
|
+
* `ctx` (optional) resolves the collision-scoped entity-domain emitted name (Task 3's
|
|
56
|
+
* `valueObjectEmittedName`), so a cross-package short-name collision qualifies both the
|
|
57
|
+
* entity module AND its extract mirror identically (`AcmeAlphaNoteExtracted`). Omitted →
|
|
58
|
+
* the bare `vo.name` (bare template unit-test calls; byte-identical to pre-#228 output). */
|
|
59
|
+
export function mirrorName(vo: MetaData, ctx?: RenderContext): string {
|
|
60
|
+
const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
|
|
61
|
+
return `${name}Extracted`;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
/** The mapper function name for a value-object (`from<Name>Extracted`). */
|
|
59
|
-
function mapperName(vo: MetaData): string {
|
|
60
|
-
|
|
64
|
+
/** The mapper function name for a value-object (`from<Name>Extracted`). See {@link mirrorName}. */
|
|
65
|
+
function mapperName(vo: MetaData, ctx?: RenderContext): string {
|
|
66
|
+
const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
|
|
67
|
+
return `from${name}Extracted`;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
// =============================================================================
|
|
@@ -65,10 +72,10 @@ function mapperName(vo: MetaData): string {
|
|
|
65
72
|
// =============================================================================
|
|
66
73
|
|
|
67
74
|
/** The nullable mirror TS type for one field — nested-aware (recurses into nested mirror names). */
|
|
68
|
-
function nestedMirrorType(field: MetaData, root: MetaData): string {
|
|
75
|
+
function nestedMirrorType(field: MetaData, root: MetaData, ctx?: RenderContext): string {
|
|
69
76
|
if (isObjectField(field)) {
|
|
70
77
|
const target = refVo(field, root);
|
|
71
|
-
const base = target !== undefined ? mirrorName(target) : "unknown";
|
|
78
|
+
const base = target !== undefined ? mirrorName(target, ctx) : "unknown";
|
|
72
79
|
const elem = `${base} | null`;
|
|
73
80
|
return isArray(field) ? `(${elem})[] | null` : elem;
|
|
74
81
|
}
|
|
@@ -92,10 +99,15 @@ function nestedMirrorType(field: MetaData, root: MetaData): string {
|
|
|
92
99
|
* name (passed in) so the existing self-contained extract<Name>() and the delegating overload
|
|
93
100
|
* share one mirror type. Returns the joined interface declarations in stable (BFS) order.
|
|
94
101
|
*/
|
|
95
|
-
export function nestedMirrorInterfaces(
|
|
102
|
+
export function nestedMirrorInterfaces(
|
|
103
|
+
vo: MetaData,
|
|
104
|
+
root: MetaData,
|
|
105
|
+
payloadMirror: string,
|
|
106
|
+
ctx?: RenderContext,
|
|
107
|
+
): string {
|
|
96
108
|
const out: string[] = [];
|
|
97
109
|
const seen = new Set<string>();
|
|
98
|
-
emitMirror(vo, root, payloadMirror, seen, out);
|
|
110
|
+
emitMirror(vo, root, payloadMirror, seen, out, ctx);
|
|
99
111
|
return out.join("\n\n");
|
|
100
112
|
}
|
|
101
113
|
|
|
@@ -105,9 +117,14 @@ function emitMirror(
|
|
|
105
117
|
interfaceName: string,
|
|
106
118
|
seen: Set<string>,
|
|
107
119
|
out: string[],
|
|
120
|
+
ctx?: RenderContext,
|
|
108
121
|
): void {
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
// ADR-0044/#228: dedupe by resolutionKey(), NOT the bare name — two distinct value-objects
|
|
123
|
+
// sharing a bare short name across packages (the collision case) are DIFFERENT nodes with
|
|
124
|
+
// DIFFERENT resolutionKey()s; bare-name dedupe would treat the second as "already seen" and
|
|
125
|
+
// silently DROP its mirror interface (and every mapper reading it downstream).
|
|
126
|
+
if (seen.has(vo.resolutionKey())) return;
|
|
127
|
+
seen.add(vo.resolutionKey());
|
|
111
128
|
|
|
112
129
|
const base = interfaceName.endsWith("Extracted")
|
|
113
130
|
? interfaceName.slice(0, -"Extracted".length)
|
|
@@ -118,7 +135,7 @@ function emitMirror(
|
|
|
118
135
|
);
|
|
119
136
|
lines.push(`export interface ${interfaceName} {`);
|
|
120
137
|
for (const f of fields(vo)) {
|
|
121
|
-
lines.push(` ${f.name}: ${nestedMirrorType(f, root)};`);
|
|
138
|
+
lines.push(` ${f.name}: ${nestedMirrorType(f, root, ctx)};`);
|
|
122
139
|
}
|
|
123
140
|
lines.push("}");
|
|
124
141
|
out.push(lines.join("\n"));
|
|
@@ -127,7 +144,7 @@ function emitMirror(
|
|
|
127
144
|
for (const f of fields(vo)) {
|
|
128
145
|
if (isObjectField(f)) {
|
|
129
146
|
const target = refVo(f, root);
|
|
130
|
-
if (target !== undefined) emitMirror(target, root, mirrorName(target), seen, out);
|
|
147
|
+
if (target !== undefined) emitMirror(target, root, mirrorName(target, ctx), seen, out, ctx);
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
}
|
|
@@ -149,10 +166,11 @@ export function nestedMappers(
|
|
|
149
166
|
root: MetaData,
|
|
150
167
|
rootMapperFn: string,
|
|
151
168
|
rootMirror: string,
|
|
169
|
+
ctx?: RenderContext,
|
|
152
170
|
): string {
|
|
153
171
|
const out: string[] = [];
|
|
154
172
|
const seen = new Set<string>();
|
|
155
|
-
emitMapper(vo, root, seen, out, { fn: rootMapperFn, mirror: rootMirror });
|
|
173
|
+
emitMapper(vo, root, seen, out, { fn: rootMapperFn, mirror: rootMirror }, ctx);
|
|
156
174
|
return out.join("\n\n");
|
|
157
175
|
}
|
|
158
176
|
|
|
@@ -167,13 +185,17 @@ function emitMapper(
|
|
|
167
185
|
seen: Set<string>,
|
|
168
186
|
out: string[],
|
|
169
187
|
override?: { fn: string; mirror: string },
|
|
188
|
+
ctx?: RenderContext,
|
|
170
189
|
): void {
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
// ADR-0044/#228: dedupe by resolutionKey() — see emitMirror for why bare-name dedupe drops
|
|
191
|
+
// the second colliding VO's mapper (silently misdirecting its extraction to the FIRST
|
|
192
|
+
// colliding VO's mapper — the exact wrong-data bug closed by this fix).
|
|
193
|
+
if (seen.has(vo.resolutionKey())) return;
|
|
194
|
+
seen.add(vo.resolutionKey());
|
|
173
195
|
|
|
174
|
-
const fn = override?.fn ?? mapperName(vo);
|
|
175
|
-
const mir = override?.mirror ?? mirrorName(vo);
|
|
176
|
-
const assigns = fields(vo).map((f) => ` ${f.name}: ${mapperArg(f, root)},`);
|
|
196
|
+
const fn = override?.fn ?? mapperName(vo, ctx);
|
|
197
|
+
const mir = override?.mirror ?? mirrorName(vo, ctx);
|
|
198
|
+
const assigns = fields(vo).map((f) => ` ${f.name}: ${mapperArg(f, root, ctx)},`);
|
|
177
199
|
const body = [
|
|
178
200
|
`/** Map an assembled ValueObject graph into a typed \`${mir}\` mirror. Generated; null-tolerant. */`,
|
|
179
201
|
`function ${fn}(o: unknown): ${mir} | null {`,
|
|
@@ -188,19 +210,19 @@ function emitMapper(
|
|
|
188
210
|
for (const f of fields(vo)) {
|
|
189
211
|
if (isObjectField(f)) {
|
|
190
212
|
const target = refVo(f, root);
|
|
191
|
-
if (target !== undefined) emitMapper(target, root, seen, out);
|
|
213
|
+
if (target !== undefined) emitMapper(target, root, seen, out, undefined, ctx);
|
|
192
214
|
}
|
|
193
215
|
}
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
/** The mirror-field initializer expression that reads `field` from the assembled object `o`. */
|
|
197
|
-
function mapperArg(field: MetaData, root: MetaData): string {
|
|
219
|
+
function mapperArg(field: MetaData, root: MetaData, ctx?: RenderContext): string {
|
|
198
220
|
const key = jsonStringLiteral(field.name);
|
|
199
221
|
|
|
200
222
|
if (isObjectField(field)) {
|
|
201
223
|
const target = refVo(field, root);
|
|
202
224
|
if (target === undefined) return "null /* unresolved @objectRef */";
|
|
203
|
-
const fn = mapperName(target);
|
|
225
|
+
const fn = mapperName(target, ctx);
|
|
204
226
|
if (isArray(field)) {
|
|
205
227
|
return `mapObjectList(readProp(o, ${key}), ${fn})`;
|
|
206
228
|
}
|
|
@@ -242,8 +264,10 @@ export function usedHelpers(vo: MetaData, root: MetaData): Set<string> {
|
|
|
242
264
|
const stack = [vo];
|
|
243
265
|
while (stack.length > 0) {
|
|
244
266
|
const cur = stack.pop()!;
|
|
245
|
-
|
|
246
|
-
|
|
267
|
+
// ADR-0044/#228: dedupe by resolutionKey() — bare-name dedupe would skip walking the
|
|
268
|
+
// SECOND colliding VO's fields entirely, silently missing a helper only IT needs.
|
|
269
|
+
if (seen.has(cur.resolutionKey())) continue;
|
|
270
|
+
seen.add(cur.resolutionKey());
|
|
247
271
|
for (const f of fields(cur)) {
|
|
248
272
|
if (isObjectField(f)) {
|
|
249
273
|
const target = refVo(f, root);
|
|
@@ -285,8 +309,9 @@ export function hasNested(vo: MetaData, root: MetaData): boolean {
|
|
|
285
309
|
const stack = [vo];
|
|
286
310
|
while (stack.length > 0) {
|
|
287
311
|
const cur = stack.pop()!;
|
|
288
|
-
|
|
289
|
-
seen.
|
|
312
|
+
// ADR-0044/#228: dedupe by resolutionKey() (see usedHelpers).
|
|
313
|
+
if (seen.has(cur.resolutionKey())) continue;
|
|
314
|
+
seen.add(cur.resolutionKey());
|
|
290
315
|
for (const f of cur.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
291
316
|
if (isObjectField(f)) {
|
|
292
317
|
const target = refVo(f, root);
|
|
@@ -37,6 +37,7 @@ import { fields, isArray } from "./fr010-field-mapping.js";
|
|
|
37
37
|
import { mirrorName } from "./extract-delegate-emitter.js";
|
|
38
38
|
import { enumUnionAliasName } from "./inferred-types.js";
|
|
39
39
|
import { enumValues } from "../enum-meta.js";
|
|
40
|
+
import type { RenderContext } from "../render-context.js";
|
|
40
41
|
|
|
41
42
|
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
42
43
|
// ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
|
|
@@ -87,9 +88,13 @@ function isFieldRequired(field: MetaData): boolean {
|
|
|
87
88
|
return field.attr(FIELD_ATTR_REQUIRED) === true;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
/** The mirror→strict mapper name for a value-object (`toStrict<Name>`).
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/** The mirror→strict mapper name for a value-object (`toStrict<Name>`). ADR-0044/#228: `ctx`
|
|
92
|
+
* (optional) resolves the collision-scoped entity-domain emitted name (matches Task 3's
|
|
93
|
+
* entity module), so the mapper name agrees with the strict payload type it targets under a
|
|
94
|
+
* cross-package short-name collision (`toStrictAcmeAlphaNote`). Omitted → bare `vo.name`. */
|
|
95
|
+
function mapperName(vo: MetaData, ctx?: RenderContext): string {
|
|
96
|
+
const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
|
|
97
|
+
return `toStrict${name}`;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
@@ -99,7 +104,7 @@ function mapperName(vo: MetaData): string {
|
|
|
99
104
|
* `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`.
|
|
100
105
|
* Nested single/array objects recurse into their toStrict<Type> mapper, guarding when optional.
|
|
101
106
|
*/
|
|
102
|
-
function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
107
|
+
function strictArg(field: MetaData, root: MetaData, ownerName: string, ctx?: RenderContext): string {
|
|
103
108
|
const name = field.name;
|
|
104
109
|
const required = isFieldRequired(field);
|
|
105
110
|
|
|
@@ -109,7 +114,7 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
|
109
114
|
// Unresolved @objectRef — the payload type would be `unknown`; pass through as-is.
|
|
110
115
|
return required ? `m.${name}!` : `m.${name} ?? undefined`;
|
|
111
116
|
}
|
|
112
|
-
const fn = mapperName(target);
|
|
117
|
+
const fn = mapperName(target, ctx);
|
|
113
118
|
if (isArray(field)) {
|
|
114
119
|
// Required array-of-objects: each element mapped; element nulls dropped at the type level
|
|
115
120
|
// via the non-null assertion (extract never yields null elements for a present array).
|
|
@@ -164,10 +169,10 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
|
|
|
164
169
|
* payload interface. The ROOT mapper reads the canonically-named root mirror (`<Template>Extracted`)
|
|
165
170
|
* since the template name may differ from the payload VO name.
|
|
166
171
|
*/
|
|
167
|
-
function emitMappers(payloadVo: MetaData, root: MetaData, rootMirror: string): string {
|
|
172
|
+
function emitMappers(payloadVo: MetaData, root: MetaData, rootMirror: string, ctx?: RenderContext): string {
|
|
168
173
|
const out: string[] = [];
|
|
169
174
|
const seen = new Set<string>();
|
|
170
|
-
emitMapper(payloadVo, root, seen, out, rootMirror);
|
|
175
|
+
emitMapper(payloadVo, root, seen, out, rootMirror, ctx);
|
|
171
176
|
return out.join("\n\n");
|
|
172
177
|
}
|
|
173
178
|
|
|
@@ -177,14 +182,17 @@ function emitMapper(
|
|
|
177
182
|
seen: Set<string>,
|
|
178
183
|
out: string[],
|
|
179
184
|
mirrorOverride?: string,
|
|
185
|
+
ctx?: RenderContext,
|
|
180
186
|
): void {
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
// ADR-0044/#228: dedupe by resolutionKey() — see extract-delegate-emitter's emitMirror for why
|
|
188
|
+
// bare-name dedupe silently drops the second colliding VO's toStrict mapper.
|
|
189
|
+
if (seen.has(vo.resolutionKey())) return;
|
|
190
|
+
seen.add(vo.resolutionKey());
|
|
183
191
|
|
|
184
|
-
const fn = mapperName(vo);
|
|
185
|
-
const strict = vo.name;
|
|
186
|
-
const mir = mirrorOverride ?? mirrorName(vo);
|
|
187
|
-
const assigns = fields(vo).map((f) => ` ${f.name}: ${strictArg(f, root,
|
|
192
|
+
const fn = mapperName(vo, ctx);
|
|
193
|
+
const strict = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
|
|
194
|
+
const mir = mirrorOverride ?? mirrorName(vo, ctx);
|
|
195
|
+
const assigns = fields(vo).map((f) => ` ${f.name}: ${strictArg(f, root, strict, ctx)},`);
|
|
188
196
|
out.push(
|
|
189
197
|
[
|
|
190
198
|
`/** Map the all-nullable \`${mir}\` mirror onto the strict \`${strict}\` payload. Generated. */`,
|
|
@@ -199,7 +207,7 @@ function emitMapper(
|
|
|
199
207
|
for (const f of fields(vo)) {
|
|
200
208
|
if (isObjectField(f)) {
|
|
201
209
|
const target = refVo(f, root);
|
|
202
|
-
if (target !== undefined) emitMapper(target, root, seen, out);
|
|
210
|
+
if (target !== undefined) emitMapper(target, root, seen, out, undefined, ctx);
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
}
|
|
@@ -224,23 +232,29 @@ interface PayloadImportGroup {
|
|
|
224
232
|
* VO's interface AND the aliases for its own enum fields are imported from `./<VO>.js` — NOT from a
|
|
225
233
|
* single `payloads.ts` (which no generator emits). Deduped, in discovery order, one group per VO.
|
|
226
234
|
*/
|
|
227
|
-
function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGroup[] {
|
|
235
|
+
function reachablePayloadGroups(vo: MetaData, root: MetaData, ctx?: RenderContext): PayloadImportGroup[] {
|
|
228
236
|
const groups: PayloadImportGroup[] = [];
|
|
229
237
|
const seenVo = new Set<string>();
|
|
230
238
|
const seenAlias = new Set<string>();
|
|
231
239
|
const visit = (cur: MetaData) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
// ADR-0044/#228: dedupe by resolutionKey() — bare-name dedupe would treat a colliding
|
|
241
|
+
// second VO as "already seen" and drop its own import group entirely.
|
|
242
|
+
if (seenVo.has(cur.resolutionKey())) return;
|
|
243
|
+
seenVo.add(cur.resolutionKey());
|
|
244
|
+
// The VO interface + its OWN enum aliases share the VO's entity module. The module target
|
|
245
|
+
// is the ADR-0044/#228 entity-domain EMITTED name (Task 3's entityFile() writes `<emitted>.ts`),
|
|
246
|
+
// so a cross-package short-name collision imports from the SAME qualified module the entity
|
|
247
|
+
// tier emits (e.g. `AcmeAlphaNote` from `./AcmeAlphaNote.js`, never bare `Note`).
|
|
248
|
+
const emittedName = ctx ? ctx.valueObjectEmittedName(cur) : cur.name;
|
|
249
|
+
const types: string[] = [emittedName];
|
|
236
250
|
for (const f of fields(cur)) {
|
|
237
|
-
const alias = enumAlias(f,
|
|
251
|
+
const alias = enumAlias(f, emittedName);
|
|
238
252
|
if (alias !== undefined && !seenAlias.has(alias)) {
|
|
239
253
|
seenAlias.add(alias);
|
|
240
254
|
types.push(alias);
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
|
-
groups.push({ module:
|
|
257
|
+
groups.push({ module: emittedName, types });
|
|
244
258
|
// Recurse into nested object refs (their interfaces live in their own modules).
|
|
245
259
|
for (const f of fields(cur)) {
|
|
246
260
|
if (isObjectField(f)) {
|
|
@@ -254,16 +268,16 @@ function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGrou
|
|
|
254
268
|
}
|
|
255
269
|
|
|
256
270
|
/** Collect the mirror-interface names reachable from `vo` (root mirror + nested VO mirrors). */
|
|
257
|
-
function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string): string[] {
|
|
271
|
+
function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string, ctx?: RenderContext): string[] {
|
|
258
272
|
const out: string[] = [rootMirror];
|
|
259
|
-
const seen = new Set<string>([vo.
|
|
273
|
+
const seen = new Set<string>([vo.resolutionKey()]);
|
|
260
274
|
const visit = (cur: MetaData) => {
|
|
261
275
|
for (const f of fields(cur)) {
|
|
262
276
|
if (isObjectField(f)) {
|
|
263
277
|
const target = refVo(f, root);
|
|
264
|
-
if (target !== undefined && !seen.has(target.
|
|
265
|
-
seen.add(target.
|
|
266
|
-
out.push(mirrorName(target));
|
|
278
|
+
if (target !== undefined && !seen.has(target.resolutionKey())) {
|
|
279
|
+
seen.add(target.resolutionKey());
|
|
280
|
+
out.push(mirrorName(target, ctx));
|
|
267
281
|
visit(target);
|
|
268
282
|
}
|
|
269
283
|
}
|
|
@@ -279,7 +293,7 @@ function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string):
|
|
|
279
293
|
* or if the target format is not json/xml (the extract tier requires the extract<Name> API, which
|
|
280
294
|
* only the json/xml output-parsers emit).
|
|
281
295
|
*/
|
|
282
|
-
export function renderExtractor(root: MetaData, templateName: string): string {
|
|
296
|
+
export function renderExtractor(root: MetaData, templateName: string, ctx?: RenderContext): string {
|
|
283
297
|
const tmpl = findTemplate(root, templateName);
|
|
284
298
|
if (!tmpl) {
|
|
285
299
|
throw new Error(`template "${templateName}" not found in metadata root`);
|
|
@@ -305,16 +319,19 @@ export function renderExtractor(root: MetaData, templateName: string): string {
|
|
|
305
319
|
);
|
|
306
320
|
}
|
|
307
321
|
|
|
308
|
-
|
|
322
|
+
// ADR-0044/#228: the strict payload TYPE name is the entity-domain EMITTED name (Task 3's
|
|
323
|
+
// `valueObjectEmittedName`) — the SAME name entityFile() declared the interface under, so a
|
|
324
|
+
// cross-package short-name collision emits e.g. `AcmeAlphaNote`, matching `./AcmeAlphaNote.js`.
|
|
325
|
+
const strictType = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
|
|
309
326
|
const rootMirror = `${templateName}Extracted`;
|
|
310
327
|
const extractLenientWithName = `extractLenient${templateName}WithLoader`; // the nested-capable lenient extract (output-parser)
|
|
311
328
|
const extractLenientPublic = `extractLenient${templateName}`; // re-exposed never-throws lenient tier name
|
|
312
329
|
const extractName = `extract${templateName}`;
|
|
313
|
-
const rootMapper = mapperName(vo);
|
|
330
|
+
const rootMapper = mapperName(vo, ctx);
|
|
314
331
|
|
|
315
|
-
const payloadGroups = reachablePayloadGroups(vo, root);
|
|
316
|
-
const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror);
|
|
317
|
-
const mappers = emitMappers(vo, root, rootMirror);
|
|
332
|
+
const payloadGroups = reachablePayloadGroups(vo, root, ctx);
|
|
333
|
+
const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror, ctx);
|
|
334
|
+
const mappers = emitMappers(vo, root, rootMirror, ctx);
|
|
318
335
|
|
|
319
336
|
// One type-only import per VO entity module (the VO interface + its own enum
|
|
320
337
|
// union-aliases co-located there). NOT a single non-existent `./payloads.js`.
|
|
@@ -40,7 +40,7 @@ import { enumValues } from "../enum-meta.js";
|
|
|
40
40
|
import { renderDocsFor } from "./jsdoc.js";
|
|
41
41
|
import { sharedEnumForField } from "../enum-shared.js";
|
|
42
42
|
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
|
|
43
|
-
import type
|
|
43
|
+
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
|
|
@@ -132,6 +132,11 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
|
|
|
132
132
|
// De-duplicate by type-alias name — multiple fields can extend the same abstract enum.
|
|
133
133
|
const seen = new Set<string>();
|
|
134
134
|
const lines: string[] = [];
|
|
135
|
+
// ADR-0044/#228 — an inline enum's alias is `<Owner><Field>`; `<Owner>` is this
|
|
136
|
+
// object's EMITTED name so a collision-qualified value object declares (and its
|
|
137
|
+
// interface references) `AcmeAlphaNoteStatus`, not a bare `NoteStatus`. Entities
|
|
138
|
+
// and non-colliding value objects keep their bare name (byte-identical).
|
|
139
|
+
const ownerName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
|
|
135
140
|
|
|
136
141
|
for (const field of entity.fields()) {
|
|
137
142
|
if (field.subType !== FIELD_SUBTYPE_ENUM) continue;
|
|
@@ -139,7 +144,7 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
|
|
|
139
144
|
const values = enumValues(field);
|
|
140
145
|
if (values === undefined) continue;
|
|
141
146
|
|
|
142
|
-
const typeName = enumUnionAliasName(
|
|
147
|
+
const typeName = enumUnionAliasName(ownerName, field);
|
|
143
148
|
if (seen.has(typeName)) continue;
|
|
144
149
|
seen.add(typeName);
|
|
145
150
|
|
|
@@ -214,6 +219,12 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
|
|
|
214
219
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
215
220
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
216
221
|
if (typeof ref === "string" && ref.length > 0) {
|
|
222
|
+
// #228: docs-tier bare name under collision — this is the deprecated `meta docs`
|
|
223
|
+
// TEXT-shape helper (no ctx/root in scope; callers api-field-shape run under
|
|
224
|
+
// api-model's `{ pkMap } as RenderContext` shim), so it can't resolve the ADR-0044
|
|
225
|
+
// emitted name. Byte-identical to codegen in every non-colliding model; on a
|
|
226
|
+
// cross-package collision it documents the bare `Note` while codegen emits
|
|
227
|
+
// `AcmeAlphaNote`. Threading a real RenderContext into api-docs is out of scope.
|
|
217
228
|
const base = stripPackage(ref);
|
|
218
229
|
return field.resolvedIsArray() ? `${base}[]` : base;
|
|
219
230
|
}
|
|
@@ -241,6 +252,10 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
|
|
|
241
252
|
* ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
|
|
242
253
|
*/
|
|
243
254
|
function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: RenderContext): Code {
|
|
255
|
+
// ADR-0044/#228 — the owning value-object's EMITTED name (bare when unique in
|
|
256
|
+
// the run, package-qualified on a cross-package short-name collision). Drives
|
|
257
|
+
// the inline enum-union alias so it matches the alias declared for this object.
|
|
258
|
+
const ownerName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
|
|
244
259
|
// `@dbColumnType: jsonb` (open JSON bag) → `unknown`, in lock-step with
|
|
245
260
|
// fieldTsTypeString above and the `z.unknown()` Zod emission.
|
|
246
261
|
if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
|
|
@@ -252,16 +267,19 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
252
267
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
253
268
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
254
269
|
if (typeof ref === "string" && ref.length > 0) {
|
|
255
|
-
// @objectRef may be authored fully-qualified (acme::sales::Brief) or bare
|
|
256
|
-
// referenced interface is named by
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
270
|
+
// @objectRef may be authored fully-qualified (acme::sales::Brief) or bare.
|
|
271
|
+
// ADR-0044/#228 — the referenced interface is named by its EMITTED name
|
|
272
|
+
// (bare when unique in the run, package-qualified on a cross-package
|
|
273
|
+
// short-name collision), resolved package-locally from the FIELD's declaring
|
|
274
|
+
// package. The import MODULE is resolved through the shared
|
|
275
|
+
// layout/package/extStyle-aware helper (the SAME one the Zod schema +
|
|
276
|
+
// Drizzle .$type<> use) so all three agree. Without a ctx (bare unit-test
|
|
277
|
+
// calls) fall back to the bare name + flat same-dir specifier.
|
|
278
|
+
const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
|
|
261
279
|
const moduleSpec = ctx
|
|
262
|
-
? valueObjectModuleSpecifier(
|
|
263
|
-
: `./${
|
|
264
|
-
const refImp = imp(`${
|
|
280
|
+
? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
|
|
281
|
+
: `./${refName}.js`;
|
|
282
|
+
const refImp = imp(`${refName}@${moduleSpec}`);
|
|
265
283
|
return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
|
|
266
284
|
}
|
|
267
285
|
return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
|
|
@@ -271,11 +289,11 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
271
289
|
if (field.subType === FIELD_SUBTYPE_MAP) {
|
|
272
290
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
273
291
|
if (typeof ref === "string" && ref.length > 0) {
|
|
274
|
-
const
|
|
292
|
+
const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
|
|
275
293
|
const moduleSpec = ctx
|
|
276
|
-
? valueObjectModuleSpecifier(
|
|
277
|
-
: `./${
|
|
278
|
-
const refImp = imp(`${
|
|
294
|
+
? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
|
|
295
|
+
: `./${refName}.js`;
|
|
296
|
+
const refImp = imp(`${refName}@${moduleSpec}`);
|
|
279
297
|
return code`Record<string, ${refImp}>`;
|
|
280
298
|
}
|
|
281
299
|
const vt = field.attr(FIELD_ATTR_VALUE_TYPE);
|
|
@@ -287,7 +305,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
287
305
|
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
288
306
|
const values = enumValues(field);
|
|
289
307
|
if (values !== undefined) {
|
|
290
|
-
const alias = enumUnionAliasName(
|
|
308
|
+
const alias = enumUnionAliasName(ownerName, field);
|
|
291
309
|
// FR-019: a shared/provided enum's type lives in another module (./enums or
|
|
292
310
|
// the provided module). Use imp() so ts-poet hoists `import { type E }` —
|
|
293
311
|
// the local interface can then reference E. Inline enums reference the
|
|
@@ -322,6 +340,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
322
340
|
export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderContext): Code {
|
|
323
341
|
const docs = renderDocsFor(entity);
|
|
324
342
|
const docsPrefix = docs ? `${docs}\n` : "";
|
|
343
|
+
// ADR-0044/#228 — the declared interface name is this value object's EMITTED
|
|
344
|
+
// name (bare when unique in the run, package-qualified on a cross-package
|
|
345
|
+
// short-name collision). Byte-identical (bare) when there is no collision.
|
|
346
|
+
const objName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
|
|
325
347
|
|
|
326
348
|
const lines: Code[] = [];
|
|
327
349
|
for (const field of entity.fields()) {
|
|
@@ -333,7 +355,7 @@ export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderConte
|
|
|
333
355
|
|
|
334
356
|
// joinCode with "\n" interpolates each Code segment on its own line and
|
|
335
357
|
// keeps the imp() registrations intact so ts-poet hoists the imports.
|
|
336
|
-
return code`${docsPrefix}export interface ${
|
|
358
|
+
return code`${docsPrefix}export interface ${objName} {
|
|
337
359
|
${joinCode(lines, { on: "\n" })}
|
|
338
360
|
}
|
|
339
361
|
`;
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
usedHelpers,
|
|
29
29
|
hasNested,
|
|
30
30
|
} from "./extract-delegate-emitter.js";
|
|
31
|
+
import type { RenderContext } from "../render-context.js";
|
|
31
32
|
|
|
32
33
|
const SCALAR_ZOD: Record<string, string> = {
|
|
33
34
|
string: "z.string()",
|
|
@@ -93,7 +94,7 @@ function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<stri
|
|
|
93
94
|
* Throws if the template isn't found, isn't a template.output, or its
|
|
94
95
|
* @payloadRef doesn't resolve to an object.value.
|
|
95
96
|
*/
|
|
96
|
-
export function renderOutputParser(root: MetaData, templateName: string): string {
|
|
97
|
+
export function renderOutputParser(root: MetaData, templateName: string, ctx?: RenderContext): string {
|
|
97
98
|
const tmpl = findTemplate(root, templateName);
|
|
98
99
|
if (!tmpl) {
|
|
99
100
|
throw new Error(`template "${templateName}" not found in metadata root`);
|
|
@@ -176,8 +177,10 @@ export function ${safeParseName}(
|
|
|
176
177
|
// The nullable mirror is the return shape of the delegating extract. Use the nested-aware
|
|
177
178
|
// emitter so the payload mirror's nested-object / array-of-object components are typed (not
|
|
178
179
|
// `unknown`), and so a mirror interface is emitted for every reachable nested value-object.
|
|
179
|
-
// The payload mirror keeps the canonical `<Template>Extracted` name.
|
|
180
|
-
|
|
180
|
+
// The payload mirror keeps the canonical `<Template>Extracted` name. ADR-0044/#228: `ctx`
|
|
181
|
+
// qualifies a nested mirror's name/dedupe when its VO's bare short name collides across
|
|
182
|
+
// packages, matching Task 3's entity-domain emitted name (e.g. `AcmeAlphaNoteExtracted`).
|
|
183
|
+
const mirrorDecls = nestedMirrorInterfaces(vo, root, extractedName, ctx);
|
|
181
184
|
|
|
182
185
|
// Render-package imports the (single, loader-delegating) extract block needs. Kept minimal so
|
|
183
186
|
// the file has no unused imports (tsc noUnusedLocals-safe).
|
|
@@ -190,19 +193,41 @@ export function ${safeParseName}(
|
|
|
190
193
|
// graph is then mapped into the typed nullable mirror graph by the generated from<VO>Extracted
|
|
191
194
|
// mappers. Codegen-wrapping-runtime (a generated DAO calling the dynamic-metadata runtime).
|
|
192
195
|
//
|
|
193
|
-
// The baked PAYLOAD_NAME is the resolved payload VO's SIMPLE name (root.findObject
|
|
194
|
-
// the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so
|
|
195
|
-
// the canonically-named `<Template>Extracted` mirror); nested mappers use their VO
|
|
196
|
+
// The baked PAYLOAD_NAME is normally the resolved payload VO's SIMPLE name (root.findObject
|
|
197
|
+
// matches on the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so
|
|
198
|
+
// it returns the canonically-named `<Template>Extracted` mirror); nested mappers use their VO
|
|
199
|
+
// names (via the entity-domain name map, so they agree with the imported mirror types above).
|
|
200
|
+
//
|
|
201
|
+
// ADR-0044/#228 — `root.findObject()` (MetaRoot's public runtime API) is a BARE-name-only,
|
|
202
|
+
// first-match lookup with no package awareness. If THIS PAYLOAD's own bare name collides with
|
|
203
|
+
// a same-short-name value-object elsewhere in the run (the identical signal Option A already
|
|
204
|
+
// computes: `ctx.valueObjectEmittedName(vo)` diverges from the bare name), a bare lookup could
|
|
205
|
+
// silently resolve to the WRONG package's object at runtime (load-order-dependent — the exact
|
|
206
|
+
// hazard class ADR-0042 closed everywhere else). When it does collide, bake the FQN
|
|
207
|
+
// (`resolutionKey()`) instead and resolve it via the SAME canonical ADR-0042 `resolveObjectRef`
|
|
208
|
+
// this file's own build-time `findObject()` wraps (FQN-exact, load-order-independent). A
|
|
209
|
+
// non-colliding payload keeps the bare name + `root.findObject()` path — byte-identical to
|
|
210
|
+
// pre-#228 output.
|
|
196
211
|
const payloadName = vo.name;
|
|
212
|
+
const emittedPayloadName = ctx ? ctx.valueObjectEmittedName(vo) : payloadName;
|
|
213
|
+
const payloadNameCollides = emittedPayloadName !== payloadName;
|
|
214
|
+
const bakedPayloadName = payloadNameCollides ? vo.resolutionKey() : payloadName;
|
|
197
215
|
const rootMapper = rootMapperName(templateName);
|
|
198
216
|
void hasNested;
|
|
217
|
+
const lookupExpr = payloadNameCollides
|
|
218
|
+
? `resolveObjectRef(root, ${payloadFqnConst}, "").node`
|
|
219
|
+
: `root.findObject(${payloadFqnConst})`;
|
|
199
220
|
const delegating = `
|
|
200
|
-
/** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime
|
|
201
|
-
|
|
221
|
+
/** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime.${
|
|
222
|
+
payloadNameCollides
|
|
223
|
+
? " ADR-0042 FQN (this payload's bare name collides with a same-short-name value object elsewhere in the run)."
|
|
224
|
+
: ""
|
|
225
|
+
} */
|
|
226
|
+
export const ${payloadFqnConst} = ${JSON.stringify(bakedPayloadName)};
|
|
202
227
|
|
|
203
228
|
${mirrorDecls}
|
|
204
229
|
|
|
205
|
-
${nestedMappers(vo, root, rootMapper, extractedName)}
|
|
230
|
+
${nestedMappers(vo, root, rootMapper, extractedName, ctx)}
|
|
206
231
|
|
|
207
232
|
${delegateHelpers(usedHelpers(vo, root))}
|
|
208
233
|
|
|
@@ -221,7 +246,7 @@ export function ${extractLenientWithName}(
|
|
|
221
246
|
text: string,
|
|
222
247
|
opts?: Partial<ExtractOptions> | null,
|
|
223
248
|
): ExtractionResult<${extractedName}> {
|
|
224
|
-
const mo =
|
|
249
|
+
const mo = ${lookupExpr};
|
|
225
250
|
if (mo === undefined) {
|
|
226
251
|
throw new Error(\`${extractLenientWithName}: payload "\${${payloadFqnConst}}" not found in the supplied MetaRoot\`);
|
|
227
252
|
}
|
|
@@ -230,8 +255,11 @@ export function ${extractLenientWithName}(
|
|
|
230
255
|
}
|
|
231
256
|
`;
|
|
232
257
|
|
|
233
|
-
// The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata
|
|
234
|
-
|
|
258
|
+
// The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata
|
|
259
|
+
// (+ resolveObjectRef, ADR-0044/#228, only when this payload's own bare name collides).
|
|
260
|
+
const metadataImport = payloadNameCollides
|
|
261
|
+
? `import type { MetaRoot } from "@metaobjectsdev/metadata";\nimport { resolveObjectRef } from "@metaobjectsdev/metadata";\n`
|
|
262
|
+
: `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
|
|
235
263
|
const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
|
|
236
264
|
|
|
237
265
|
return (
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
11
11
|
import {
|
|
12
12
|
MetaField, MetaObject, type MetaRoot,
|
|
13
|
+
FIELD_ATTR_OBJECT_REF, stripPackage,
|
|
13
14
|
} from "@metaobjectsdev/metadata";
|
|
14
15
|
import { projectionViewName } from "../projection/extract-view-spec.js";
|
|
15
16
|
import { columnNameFromField, toSnakeCase, pluralize } from "../naming.js";
|
|
16
17
|
import { GENERATED_HEADER } from "../constants.js";
|
|
17
18
|
import type { ColumnNamingStrategy } from "../metaobjects-config.js";
|
|
18
|
-
import type
|
|
19
|
+
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
19
20
|
import { valueObjectModuleSpecifier } from "../import-path.js";
|
|
20
21
|
import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
|
|
21
22
|
import { renderFilterType } from "./filter-type.js";
|
|
@@ -92,13 +93,22 @@ export function renderProjectionDecl(
|
|
|
92
93
|
): string {
|
|
93
94
|
const { dialect, columnNamingStrategy, apiPrefix = "", timestampMode = "string", allowlists = true, ctx, includeViewDecl = true } = opts;
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
// ADR-0044/#228 — resolve a projection field's `@objectRef` to the value object's
|
|
97
|
+
// EMITTED name + module TOGETHER (lock-step): bare when unique in the run,
|
|
98
|
+
// package-qualified on a cross-package short-name collision, so the projection's
|
|
99
|
+
// VO import matches the entity's. Layout/package/extStyle-aware when a render
|
|
100
|
+
// context is present, else a flat same-dir import (zodFieldExpr's fallback).
|
|
101
|
+
const voRef = (field: MetaField): { name: string; module: string } => {
|
|
102
|
+
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
103
|
+
const rawRef = typeof ref === "string" ? ref : "";
|
|
104
|
+
const name = ctx
|
|
105
|
+
? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field, projection.package))
|
|
106
|
+
: stripPackage(rawRef);
|
|
107
|
+
const module = ctx
|
|
108
|
+
? valueObjectModuleSpecifier(name, ctx.packageOf, projection.package, ctx.outputLayout, ctx.extStyle)
|
|
109
|
+
: `./${name}.js`;
|
|
110
|
+
return { name, module };
|
|
111
|
+
};
|
|
102
112
|
|
|
103
113
|
const z = imp("z@zod");
|
|
104
114
|
|
|
@@ -145,12 +155,12 @@ export function renderProjectionDecl(
|
|
|
145
155
|
const sections: Code[] = [
|
|
146
156
|
...(includeViewDecl
|
|
147
157
|
? [renderExistingViewDecl(allFields, viewName, `${camelName}View`, {
|
|
148
|
-
dialect, columnNamingStrategy, timestampMode,
|
|
158
|
+
dialect, columnNamingStrategy, timestampMode, voRef,
|
|
149
159
|
})]
|
|
150
160
|
: []),
|
|
151
161
|
code`
|
|
152
162
|
export const ${projName}Schema = ${renderViewReadZodObject(allFields, {
|
|
153
|
-
dialect, columnNamingStrategy, timestampMode,
|
|
163
|
+
dialect, columnNamingStrategy, timestampMode, voRef,
|
|
154
164
|
})};
|
|
155
165
|
`,
|
|
156
166
|
code`
|