@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
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
TYPE_FIELD,
|
|
15
15
|
FIELD_SUBTYPE_STRING,
|
|
16
16
|
FIELD_SUBTYPE_UUID,
|
|
17
|
+
FIELD_SUBTYPE_URI,
|
|
18
|
+
FIELD_SUBTYPE_INET,
|
|
17
19
|
FIELD_SUBTYPE_DATE,
|
|
18
20
|
FIELD_SUBTYPE_TIME,
|
|
19
21
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
@@ -41,6 +43,9 @@ export function scalarKind(subType: string): string | null {
|
|
|
41
43
|
switch (subType) {
|
|
42
44
|
case FIELD_SUBTYPE_STRING:
|
|
43
45
|
case FIELD_SUBTYPE_UUID:
|
|
46
|
+
// ADR-0036/0037 Wave 3: uri/inet are string scalars on the wire.
|
|
47
|
+
case FIELD_SUBTYPE_URI:
|
|
48
|
+
case FIELD_SUBTYPE_INET:
|
|
44
49
|
case FIELD_SUBTYPE_DATE:
|
|
45
50
|
case FIELD_SUBTYPE_TIME:
|
|
46
51
|
case FIELD_SUBTYPE_TIMESTAMP:
|
|
@@ -71,7 +76,7 @@ export function fields(vo: MetaData): MetaData[] {
|
|
|
71
76
|
|
|
72
77
|
/** isArray is a native (reserved) property on MetaData, not an attr. */
|
|
73
78
|
export function isArray(field: MetaData): boolean {
|
|
74
|
-
return field.
|
|
79
|
+
return field.resolvedIsArray();
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
/** True iff the field's @required is explicitly true (or the string "true"). */
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
FIELD_SUBTYPE_TIME,
|
|
26
26
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
27
27
|
FIELD_SUBTYPE_UUID,
|
|
28
|
+
FIELD_SUBTYPE_URI,
|
|
29
|
+
FIELD_SUBTYPE_INET,
|
|
28
30
|
FIELD_ATTR_REQUIRED,
|
|
29
31
|
FIELD_ATTR_OBJECT_REF,
|
|
30
32
|
FIELD_ATTR_VALUE_TYPE,
|
|
@@ -149,6 +151,10 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
|
|
|
149
151
|
const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
|
|
150
152
|
[FIELD_SUBTYPE_STRING]: "string",
|
|
151
153
|
[FIELD_SUBTYPE_UUID]: "string",
|
|
154
|
+
// ADR-0036/0037 Wave 3: uri/inet bind to TS `string` (TS has no native URI/IP
|
|
155
|
+
// type, same as uuid). Other ports bind to their native URI/IP type.
|
|
156
|
+
[FIELD_SUBTYPE_URI]: "string",
|
|
157
|
+
[FIELD_SUBTYPE_INET]: "string",
|
|
152
158
|
[FIELD_SUBTYPE_INT]: "number",
|
|
153
159
|
[FIELD_SUBTYPE_LONG]: "number",
|
|
154
160
|
[FIELD_SUBTYPE_DOUBLE]: "number",
|
|
@@ -184,15 +190,15 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
|
|
|
184
190
|
// the column is a bare `jsonb()` returning any parsed JSON value, so the TS type
|
|
185
191
|
// stays in lock-step with the `z.unknown()` Zod emission (NOT `string`).
|
|
186
192
|
if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
|
|
187
|
-
return field.
|
|
193
|
+
return field.resolvedIsArray() ? "unknown[]" : "unknown";
|
|
188
194
|
}
|
|
189
195
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
190
196
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
191
197
|
if (typeof ref === "string" && ref.length > 0) {
|
|
192
198
|
const base = stripPackage(ref);
|
|
193
|
-
return field.
|
|
199
|
+
return field.resolvedIsArray() ? `${base}[]` : base;
|
|
194
200
|
}
|
|
195
|
-
return field.
|
|
201
|
+
return field.resolvedIsArray() ? "unknown[]" : "unknown";
|
|
196
202
|
}
|
|
197
203
|
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
198
204
|
const values = enumValues(field);
|
|
@@ -202,12 +208,12 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
|
|
|
202
208
|
// self-contained (an agent sees the exact allowed values, not an opaque
|
|
203
209
|
// alias name). Array enums wrap the parenthesized union: `(A | B)[]`.
|
|
204
210
|
const union = enumUnionString(values);
|
|
205
|
-
return field.
|
|
211
|
+
return field.resolvedIsArray() ? `(${union})[]` : union;
|
|
206
212
|
}
|
|
207
|
-
return field.
|
|
213
|
+
return field.resolvedIsArray() ? "string[]" : "string";
|
|
208
214
|
}
|
|
209
215
|
const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
|
|
210
|
-
return field.
|
|
216
|
+
return field.resolvedIsArray() ? `${scalar}[]` : scalar;
|
|
211
217
|
}
|
|
212
218
|
|
|
213
219
|
/**
|
|
@@ -219,7 +225,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
219
225
|
// `@dbColumnType: jsonb` (open JSON bag) → `unknown`, in lock-step with
|
|
220
226
|
// fieldTsTypeString above and the `z.unknown()` Zod emission.
|
|
221
227
|
if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
|
|
222
|
-
return field.
|
|
228
|
+
return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
|
|
223
229
|
}
|
|
224
230
|
// field.object: import the referenced TS interface from its sibling module
|
|
225
231
|
// so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
|
|
@@ -237,9 +243,9 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
237
243
|
? valueObjectModuleSpecifier(base, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
|
|
238
244
|
: `./${base}.js`;
|
|
239
245
|
const refImp = imp(`${base}@${moduleSpec}`);
|
|
240
|
-
return field.
|
|
246
|
+
return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
|
|
241
247
|
}
|
|
242
|
-
return field.
|
|
248
|
+
return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
|
|
243
249
|
}
|
|
244
250
|
|
|
245
251
|
// field.map: Record<string, V> — V is a value-object (@objectRef) or a scalar (@valueType).
|
|
@@ -274,16 +280,16 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
274
280
|
? providedEnumImportSpecifier(ctx, shared.name)
|
|
275
281
|
: sharedEnumImportSpecifier(ctx, entity.package);
|
|
276
282
|
const sym = imp(`${shared.name}@${spec}`);
|
|
277
|
-
return field.
|
|
283
|
+
return field.resolvedIsArray() ? code`${sym}[]` : code`${sym}`;
|
|
278
284
|
}
|
|
279
285
|
}
|
|
280
|
-
return field.
|
|
286
|
+
return field.resolvedIsArray() ? code`${alias}[]` : code`${alias}`;
|
|
281
287
|
}
|
|
282
|
-
return field.
|
|
288
|
+
return field.resolvedIsArray() ? code`string[]` : code`string`;
|
|
283
289
|
}
|
|
284
290
|
|
|
285
291
|
const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
|
|
286
|
-
return field.
|
|
292
|
+
return field.resolvedIsArray() ? code`${scalar}[]` : code`${scalar}`;
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
/**
|
|
@@ -42,12 +42,14 @@ export function specLiteral(vo: MetaData, template: MetaData, rootName: string):
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function resolveFormat(template: MetaData): string {
|
|
45
|
-
|
|
45
|
+
// ADR-0039: resolving — a template may inherit @format via extends.
|
|
46
|
+
const f = template.attr(TEMPLATE_ATTR_FORMAT);
|
|
46
47
|
return typeof f === "string" && f.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function resolvePromptStyle(template: MetaData): string {
|
|
50
|
-
|
|
51
|
+
// ADR-0039: resolving — a template may inherit @promptStyle via extends.
|
|
52
|
+
switch (template.attr(TEMPLATE_ATTR_PROMPT_STYLE)) {
|
|
51
53
|
case PROMPT_STYLE_INLINE:
|
|
52
54
|
return "PromptStyle.INLINE";
|
|
53
55
|
case PROMPT_STYLE_EXAMPLE_ONLY:
|
|
@@ -92,6 +94,8 @@ function promptFieldLiteral(field: MetaData): string {
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
function optStringAttr(field: MetaData, attrName: string): string {
|
|
95
|
-
|
|
97
|
+
// ADR-0039: resolving — a field may inherit @example/@instruction via extends
|
|
98
|
+
// (mirrors the Java OutputFormatSpecEmitter own→resolving flip).
|
|
99
|
+
const v = field.attr(attrName);
|
|
96
100
|
return typeof v === "string" ? jsonStringLiteral(v) : "null";
|
|
97
101
|
}
|
|
@@ -42,20 +42,22 @@ const SCALAR_ZOD: Record<string, string> = {
|
|
|
42
42
|
boolean: "z.boolean()",
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
45
46
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
46
47
|
// FR-032 — @payloadRef is FQN after the desugar/sweep; match on the effective
|
|
47
48
|
// FQN resolution key (with bare back-compat).
|
|
48
|
-
return root.
|
|
49
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
51
53
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
52
|
-
return root.
|
|
54
|
+
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** Render the Zod expression for a single field; recurses on @objectRef. */
|
|
56
58
|
function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
|
|
57
59
|
// isArray is a native (reserved) property on MetaData, not an attr.
|
|
58
|
-
const isArray = field.
|
|
60
|
+
const isArray = field.resolvedIsArray();
|
|
59
61
|
let base: string;
|
|
60
62
|
if (field.subType === FIELD_SUBTYPE_OBJECT) {
|
|
61
63
|
const refName = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
@@ -100,7 +102,8 @@ export function renderOutputParser(root: MetaData, templateName: string): string
|
|
|
100
102
|
if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
|
|
101
103
|
throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
|
|
102
104
|
}
|
|
103
|
-
|
|
105
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
106
|
+
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
104
107
|
if (typeof payloadRef !== "string") {
|
|
105
108
|
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
106
109
|
}
|
|
@@ -119,7 +122,8 @@ export function renderOutputParser(root: MetaData, templateName: string): string
|
|
|
119
122
|
// FR-010: emit the tolerant extract() API alongside the strict Zod parser when the
|
|
120
123
|
// template targets json/xml. The @payloadRef already resolved to a value-object above,
|
|
121
124
|
// so a ExtractSchema can always be baked. text-format outputs get no extract.
|
|
122
|
-
|
|
125
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
126
|
+
const format = (tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
|
|
123
127
|
const lc = format.toLowerCase();
|
|
124
128
|
const emitExtractLenient = lc === "json" || lc === "xml";
|
|
125
129
|
|
|
@@ -21,17 +21,20 @@ import {
|
|
|
21
21
|
} from "@metaobjectsdev/metadata";
|
|
22
22
|
import { specLiteral } from "./output-format-spec-emitter.js";
|
|
23
23
|
|
|
24
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
24
25
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
25
|
-
return root.
|
|
26
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
28
30
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
29
|
-
return root.
|
|
31
|
+
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** True iff the template.output's @format is json or xml (the renderable structured formats). */
|
|
33
35
|
export function templateSupportsPrompt(tmpl: MetaData): boolean {
|
|
34
|
-
|
|
36
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
37
|
+
const f = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
35
38
|
return f === "json" || f === "xml";
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -51,7 +54,8 @@ export function renderOutputPrompt(root: MetaData, templateName: string): string
|
|
|
51
54
|
if (!templateSupportsPrompt(tmpl)) {
|
|
52
55
|
throw new Error(`template "${templateName}" @format is not json/xml — no prompt fragment`);
|
|
53
56
|
}
|
|
54
|
-
|
|
57
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
58
|
+
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
55
59
|
if (typeof payloadRef !== "string") {
|
|
56
60
|
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
57
61
|
}
|
|
@@ -131,6 +131,9 @@ export function renderProjectionDecl(
|
|
|
131
131
|
for (const f of baseObj.fields()) allFields.push(f);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
// ADR-0039: own — category 1 (emit-declared-here): the super's effective fields
|
|
135
|
+
// were already collected above; append only the projection's OWN new fields so
|
|
136
|
+
// inherited fields are not duplicated.
|
|
134
137
|
for (const f of projection.ownFields()) allFields.push(f);
|
|
135
138
|
|
|
136
139
|
// A field.object passthrough carries the value-object's Zod schema (so the
|
|
@@ -149,7 +152,7 @@ export function renderProjectionDecl(
|
|
|
149
152
|
const refBase = objectRefOf(f);
|
|
150
153
|
if (refBase) {
|
|
151
154
|
const schemaSym = imp(`${refBase}InsertSchema@${voModule(refBase)}`);
|
|
152
|
-
const base = f.
|
|
155
|
+
const base = f.resolvedIsArray()
|
|
153
156
|
? code`${z}.array(${schemaSym})`
|
|
154
157
|
: code`${schemaSym}`;
|
|
155
158
|
return code` ${f.name}: ${base}${nullable}`;
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
renderCreateFn,
|
|
16
16
|
renderUpdateFn,
|
|
17
17
|
renderDeleteByIdFn,
|
|
18
|
+
renderReverseFinderFns,
|
|
19
|
+
reverseFksFor,
|
|
18
20
|
getPkInfo,
|
|
19
21
|
} from "./queries.js";
|
|
20
22
|
import { pluralize, findByIdFnName, listFnName } from "../naming.js";
|
|
@@ -90,6 +92,16 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
|
|
|
90
92
|
renderDeleteByIdFn(obj, ctx),
|
|
91
93
|
];
|
|
92
94
|
|
|
95
|
+
// ADR-0038 — reverse-relationship navigation via explicit FK finders. Each FK
|
|
96
|
+
// this entity holds (`identity.reference`) gets `find<Plural>By<FkField>` +
|
|
97
|
+
// its batched `…In` sibling, so the referenced entity can navigate back to its
|
|
98
|
+
// referencing rows with a plain, single-query, framework-free read. FK field
|
|
99
|
+
// names are unique within the entity, so the finder names never collide — the
|
|
100
|
+
// same-pair case (3 FKs to one target) yields 3 distinct finders.
|
|
101
|
+
for (const fk of reverseFksFor(obj)) {
|
|
102
|
+
sections.push(renderReverseFinderFns(obj, fk, ctx));
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
// Render ts-poet body first, then prepend the @generated header so it lands
|
|
94
106
|
// at line 1 ahead of any imports.
|
|
95
107
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
@@ -178,6 +190,7 @@ export async function ${listFnName(entityName)}(db: Db, opts?: { limit?: number;
|
|
|
178
190
|
function renderTphQueriesFile(base: MetaObject, ctx: RenderContext): string {
|
|
179
191
|
const baseName = base.name;
|
|
180
192
|
const tableVar = ctx.collectionName(baseName);
|
|
193
|
+
// ADR-0039: own — TPH discriminator, read own to identify the base level.
|
|
181
194
|
const discField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR) as string;
|
|
182
195
|
const { fieldName: pkField, tsType: pkType } = getPkInfo(base, ctx);
|
|
183
196
|
|
|
@@ -225,6 +238,7 @@ export async function list${pluralize(baseName)}(db: Db, opts?: { limit?: number
|
|
|
225
238
|
// --- Per-subtype CRUD against the single base table ---
|
|
226
239
|
const subtypeSections: Code[] = [];
|
|
227
240
|
for (const sub of tphConcreteSubtypes(base, ctx.loadedRoot)) {
|
|
241
|
+
// ADR-0039: own — each concrete subtype declares its OWN @discriminatorValue.
|
|
228
242
|
const value = sub.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) as string;
|
|
229
243
|
const valueLit = JSON.stringify(value);
|
|
230
244
|
const subFileSpec = entityModuleSpecifier(
|
package/src/templates/queries.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Each returns a ts-poet Code block; composed into a file by queries-file.ts.
|
|
3
3
|
|
|
4
4
|
import { code, imp, type Code } from "ts-poet";
|
|
5
|
-
import type
|
|
5
|
+
import { type MetaObject, stripPackage } from "@metaobjectsdev/metadata";
|
|
6
6
|
import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
|
|
7
7
|
import type { RenderContext } from "../render-context.js";
|
|
8
8
|
import {
|
|
@@ -11,8 +11,19 @@ import {
|
|
|
11
11
|
createFnName,
|
|
12
12
|
updateFnName,
|
|
13
13
|
deleteByIdFnName,
|
|
14
|
+
reverseFinderFnName,
|
|
15
|
+
reverseFinderInFnName,
|
|
14
16
|
} from "../naming.js";
|
|
15
17
|
|
|
18
|
+
/** Map a field subType to the generated TS scalar type for keys/values. */
|
|
19
|
+
function subTypeToTsType(subType: string): "number" | "boolean" | "string" {
|
|
20
|
+
return subType === "long" || subType === "int" || subType === "short" || subType === "byte"
|
|
21
|
+
? "number"
|
|
22
|
+
: subType === "boolean"
|
|
23
|
+
? "boolean"
|
|
24
|
+
: "string";
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
/** Get the PK field name and its TS type for a given entity. */
|
|
17
28
|
export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
|
|
18
29
|
// Use primaryIdentity() to find the primary identity (may be inherited from extends:/super:).
|
|
@@ -22,13 +33,7 @@ export function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName:
|
|
|
22
33
|
const pkFieldName = fields?.[0] ?? "id";
|
|
23
34
|
const pkInfo = ctx.pkMap.get(entity.name);
|
|
24
35
|
const subType = pkInfo?.fieldSubType ?? "long";
|
|
25
|
-
|
|
26
|
-
subType === "long" || subType === "int" || subType === "short" || subType === "byte"
|
|
27
|
-
? "number"
|
|
28
|
-
: subType === "boolean"
|
|
29
|
-
? "boolean"
|
|
30
|
-
: "string";
|
|
31
|
-
return { fieldName: pkFieldName, tsType };
|
|
36
|
+
return { fieldName: pkFieldName, tsType: subTypeToTsType(subType) };
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
|
|
@@ -114,3 +119,58 @@ export async function ${fnName}(db: Db, ${pkField}: ${pkType}): Promise<boolean>
|
|
|
114
119
|
}
|
|
115
120
|
`;
|
|
116
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One reverse FK finder pair (ADR-0038): the entity holding this FK (`E`) gains
|
|
125
|
+
* `find<EPlural>By<FkField>(value)` (single, `WHERE fk = ?`) and
|
|
126
|
+
* `find<EPlural>By<FkField>In(values)` (batched, `WHERE fk IN (…)`, anti-N+1) so
|
|
127
|
+
* the referenced entity `T` can navigate to its referencing `E` rows by calling
|
|
128
|
+
* the finder with a `T` id. Both are plain, framework-free, single-query reads.
|
|
129
|
+
*/
|
|
130
|
+
interface ReverseFk {
|
|
131
|
+
/** FK FIELD name on this entity (logical), e.g. `currentSceneId`. */
|
|
132
|
+
fkField: string;
|
|
133
|
+
/** Target entity (the `T` referenced), e.g. `Scene`. Drives the value TS type. */
|
|
134
|
+
targetEntity: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Collect this entity's OWN reverse FK targets, in declaration order. */
|
|
138
|
+
export function reverseFksFor(entity: MetaObject): ReverseFk[] {
|
|
139
|
+
const out: ReverseFk[] = [];
|
|
140
|
+
for (const ref of entity.referenceIdentities()) {
|
|
141
|
+
const fkField = ref.fields[0];
|
|
142
|
+
const target = ref.targetEntity;
|
|
143
|
+
if (!fkField || !target) continue;
|
|
144
|
+
out.push({ fkField, targetEntity: stripPackage(target) });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Render the single + batched reverse finders for one FK on `entity`. */
|
|
150
|
+
export function renderReverseFinderFns(entity: MetaObject, fk: ReverseFk, ctx: RenderContext): Code {
|
|
151
|
+
const varName = ctx.collectionName(entity.name);
|
|
152
|
+
const entityName = entity.name;
|
|
153
|
+
// The Drizzle table object is keyed by the LOGICAL field name (the DB column
|
|
154
|
+
// name is the argument to integer()/text()), so column access uses fk.fkField.
|
|
155
|
+
// The FK value type is the target entity's PK type (the FK references it). Fall
|
|
156
|
+
// back to the FK field's own subType, then to number (long-shaped key default).
|
|
157
|
+
const targetPk = ctx.pkMap.get(fk.targetEntity);
|
|
158
|
+
const ownField = entity.findField(fk.fkField);
|
|
159
|
+
const valueType = subTypeToTsType(targetPk?.fieldSubType ?? ownField?.subType ?? "long");
|
|
160
|
+
|
|
161
|
+
const singleName = reverseFinderFnName(entityName, fk.fkField);
|
|
162
|
+
const batchName = reverseFinderInFnName(entityName, fk.fkField);
|
|
163
|
+
const eqSym = imp("eq@drizzle-orm");
|
|
164
|
+
const inArraySym = imp("inArray@drizzle-orm");
|
|
165
|
+
|
|
166
|
+
return code`
|
|
167
|
+
export async function ${singleName}(db: Db, ${fk.fkField}: ${valueType}): Promise<${entityName}[]> {
|
|
168
|
+
return db.select().from(${varName}).where(${eqSym}(${varName}.${fk.fkField}, ${fk.fkField}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function ${batchName}(db: Db, ${fk.fkField}s: ${valueType}[]): Promise<${entityName}[]> {
|
|
172
|
+
if (${fk.fkField}s.length === 0) return [];
|
|
173
|
+
return db.select().from(${varName}).where(${inArraySym}(${varName}.${fk.fkField}, ${fk.fkField}s));
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
@@ -53,12 +53,14 @@ import {
|
|
|
53
53
|
type VerifyError,
|
|
54
54
|
} from "@metaobjectsdev/render";
|
|
55
55
|
|
|
56
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
56
57
|
function findObject(root: MetaData, name: string): MetaData | undefined {
|
|
57
|
-
return root.
|
|
58
|
+
return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
|
|
60
62
|
function findTemplate(root: MetaData, name: string): MetaData | undefined {
|
|
61
|
-
return root.
|
|
63
|
+
return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -150,7 +152,8 @@ export function renderRenderHelper(
|
|
|
150
152
|
`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`,
|
|
151
153
|
);
|
|
152
154
|
}
|
|
153
|
-
|
|
155
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
156
|
+
const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
154
157
|
if (typeof payloadRef !== "string") {
|
|
155
158
|
throw new Error(`template "${templateName}" missing @payloadRef`);
|
|
156
159
|
}
|
|
@@ -165,13 +168,15 @@ export function renderRenderHelper(
|
|
|
165
168
|
const ft = fieldTreeLiteral(fields);
|
|
166
169
|
const fnName = `render${templateName}`;
|
|
167
170
|
|
|
168
|
-
|
|
171
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
172
|
+
const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT)
|
|
169
173
|
.toLowerCase();
|
|
170
174
|
|
|
171
175
|
if (kind === TEMPLATE_KIND_EMAIL) {
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
176
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
177
|
+
const subjectRef = tmpl.attr(TEMPLATE_ATTR_SUBJECT_REF);
|
|
178
|
+
const htmlBodyRef = tmpl.attr(TEMPLATE_ATTR_HTML_BODY_REF);
|
|
179
|
+
const textBodyRef = tmpl.attr(TEMPLATE_ATTR_TEXT_BODY_REF);
|
|
175
180
|
if (typeof subjectRef !== "string") {
|
|
176
181
|
throw new Error(`template "${templateName}" (email) missing @subjectRef`);
|
|
177
182
|
}
|
|
@@ -210,12 +215,14 @@ export function ${fnName}(payload: ${payloadRef}, provider: Provider): EmailDocu
|
|
|
210
215
|
}
|
|
211
216
|
|
|
212
217
|
// --- document kind ---
|
|
213
|
-
|
|
218
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
219
|
+
const textRef = tmpl.attr(TEMPLATE_ATTR_TEXT_REF);
|
|
214
220
|
if (typeof textRef !== "string") {
|
|
215
221
|
throw new Error(`template "${templateName}" (document) missing @textRef`);
|
|
216
222
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
223
|
+
// ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
|
|
224
|
+
const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
|
|
225
|
+
const maxCharsAttr = tmpl.attr(TEMPLATE_ATTR_MAX_CHARS);
|
|
219
226
|
const maxChars =
|
|
220
227
|
typeof maxCharsAttr === "number"
|
|
221
228
|
? maxCharsAttr
|
|
@@ -285,7 +285,8 @@ function renderM2mMount(
|
|
|
285
285
|
|
|
286
286
|
/** Resolve a field's physical column name on an entity (defaults if missing). */
|
|
287
287
|
function resolveJunctionColumn(entity: MetaObject, fieldName: string, ctx: RenderContext): string {
|
|
288
|
-
|
|
288
|
+
// ADR-0039: resolving — a junction FK field may be inherited via extends.
|
|
289
|
+
const field = entity.children().find((c) => c.type === TYPE_FIELD && c.name === fieldName);
|
|
289
290
|
if (!field) return fieldName;
|
|
290
291
|
return resolveColumnName(field, ctx.columnNamingStrategy);
|
|
291
292
|
}
|
|
@@ -74,6 +74,9 @@ export function tphRouteSegment(discriminatorValue: string): string {
|
|
|
74
74
|
export function tphPlan(base: MetaObject, root: MetaRoot): TphPlan | null {
|
|
75
75
|
const cached = _tphPlanCache.get(base);
|
|
76
76
|
if (cached !== undefined) return cached;
|
|
77
|
+
// ADR-0039: own — TPH super-resolution walk (mirrors C# TphPlan). @discriminator
|
|
78
|
+
// must be read own to identify WHICH hierarchy level owns it (the discriminator
|
|
79
|
+
// base); a subtype must not be treated as a base by inheriting it.
|
|
77
80
|
const discriminatorField = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
78
81
|
let plan: TphPlan | null = null;
|
|
79
82
|
if (typeof discriminatorField === "string" && discriminatorField !== "") {
|
|
@@ -117,6 +120,7 @@ export function tphConcreteSubtypes(base: MetaObject, root: MetaRoot): MetaObjec
|
|
|
117
120
|
* caller emits each as a nullable column (rows of other subtypes store NULL).
|
|
118
121
|
*/
|
|
119
122
|
export function collectTphSubtypeFields(base: MetaObject, root: MetaRoot): MetaField[] {
|
|
123
|
+
// ADR-0039: own — TPH super-resolution walk; @discriminator identifies the base level.
|
|
120
124
|
const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
121
125
|
if (typeof discFieldName !== "string" || discFieldName === "") return [];
|
|
122
126
|
|
|
@@ -140,6 +144,7 @@ export function renderTphDiscriminatorUnion(
|
|
|
140
144
|
base: MetaObject,
|
|
141
145
|
root: MetaRoot,
|
|
142
146
|
): Code | null {
|
|
147
|
+
// ADR-0039: own — TPH super-resolution walk; @discriminator identifies the base level.
|
|
143
148
|
const discFieldName = base.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
|
|
144
149
|
if (typeof discFieldName !== "string" || discFieldName === "") return null;
|
|
145
150
|
|
|
@@ -209,6 +214,8 @@ function collectConcreteSubtypes(base: MetaObject, root: MetaRoot): SubtypeBindi
|
|
|
209
214
|
if (obj.isAbstract === true) continue;
|
|
210
215
|
if (obj === base) continue;
|
|
211
216
|
|
|
217
|
+
// ADR-0039: own — each concrete subtype declares its OWN @discriminatorValue;
|
|
218
|
+
// it must NOT inherit a parent's value (super-resolution walk, category 3).
|
|
212
219
|
const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
|
|
213
220
|
if (typeof value !== "string" || value === "") continue;
|
|
214
221
|
|