@metaobjectsdev/codegen-ts 1.0.5-rc.1 → 1.0.5-rc.2
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 +6 -6
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +7 -2
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +2 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +4 -3
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +2 -1
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +4 -3
- 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/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +23 -4
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +13 -6
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +21 -4
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +15 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +21 -0
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/generators/api-model.ts +6 -6
- package/src/generators/queries-file.ts +7 -2
- package/src/generators/routes-file-hono.ts +4 -3
- package/src/generators/routes-file.ts +4 -3
- package/src/index.ts +1 -1
- package/src/reference/queries.ts +4 -2
- package/src/reference/routes-hono.ts +2 -2
- package/src/reference/routes.ts +4 -3
- package/src/templates/drizzle-schema.ts +21 -4
- package/src/templates/relations-block.ts +19 -11
- package/src/templates/routes-file.ts +29 -12
- package/src/templates/zod-validators.ts +23 -1
|
@@ -25,7 +25,8 @@ import { resolveTableSchema } from "@metaobjectsdev/metadata";
|
|
|
25
25
|
import { renderEnumIntCustomType } from "./enum-int-codec.js";
|
|
26
26
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
27
27
|
import { renderDocsFor } from "./jsdoc.js";
|
|
28
|
-
import { collectTphSubtypeFields } from "./tph-discriminator.js";
|
|
28
|
+
import { collectTphSubtypeFields, tphConcreteSubtypes } from "./tph-discriminator.js";
|
|
29
|
+
import { tphStorageObject } from "./zod-validators.js";
|
|
29
30
|
import { effectivePackage } from "../docs-paths.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -411,7 +412,19 @@ interface FkInfo {
|
|
|
411
412
|
/** Pre-pass: map fkFieldName → FkInfo for this entity's effective (own + inherited) identity.reference children. */
|
|
412
413
|
function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, FkInfo> {
|
|
413
414
|
const result = new Map<string, FkInfo>();
|
|
414
|
-
|
|
415
|
+
// FR-017 TPH: a discriminator base's table carries every concrete subtype's folded
|
|
416
|
+
// columns (collectTphSubtypeFields), so it carries their FKs too — otherwise a reference
|
|
417
|
+
// declared on a subtype, or on an abstract level between it and the base, lands as a
|
|
418
|
+
// plain column. Same effective, not-already-on-the-base rule as the column fold.
|
|
419
|
+
// A copy: referenceIdentities() is the node's cached array, and loaded metadata is read-only.
|
|
420
|
+
const refs = [...obj.referenceIdentities()];
|
|
421
|
+
const baseRefNames = new Set(refs.map((r) => r.name));
|
|
422
|
+
for (const sub of tphConcreteSubtypes(obj, ctx.loadedRoot)) {
|
|
423
|
+
for (const ref of sub.referenceIdentities()) {
|
|
424
|
+
if (!baseRefNames.has(ref.name)) refs.push(ref);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const ref of refs) {
|
|
415
428
|
// @enforce: false → logical-only reference. Skip the .references() emission;
|
|
416
429
|
// the column stays plain. Drizzle's relations() block (driven by
|
|
417
430
|
// relation-resolver) still includes the relationship for query navigation.
|
|
@@ -419,6 +432,8 @@ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, F
|
|
|
419
432
|
const fkFieldNames = ref.fields;
|
|
420
433
|
if (fkFieldNames.length === 0) continue;
|
|
421
434
|
const fkField = fkFieldNames[0]!;
|
|
435
|
+
// Two subtypes declaring the same reference fold to one column with one FK.
|
|
436
|
+
if (result.has(fkField)) continue;
|
|
422
437
|
const targetName = ref.targetEntity;
|
|
423
438
|
if (!targetName) continue;
|
|
424
439
|
// @references may be authored bare OR package-qualified, and the loader can
|
|
@@ -427,10 +442,12 @@ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, F
|
|
|
427
442
|
// relation-resolver, which already does this.
|
|
428
443
|
const targetObj = ctx.loadedRoot.findObject(stripPackage(targetName));
|
|
429
444
|
if (!targetObj) continue;
|
|
445
|
+
// A TPH subtype has no table const of its own; its rows live in the base's table.
|
|
446
|
+
const storage = tphStorageObject(targetObj);
|
|
430
447
|
const targetPkField = ref.resolvedTargetPkField(ctx.loadedRoot) ?? "id";
|
|
431
448
|
result.set(fkField, {
|
|
432
|
-
targetVarName: ctx.collectionName(
|
|
433
|
-
targetEntityName:
|
|
449
|
+
targetVarName: ctx.collectionName(storage.name),
|
|
450
|
+
targetEntityName: storage.name,
|
|
434
451
|
targetPkField,
|
|
435
452
|
});
|
|
436
453
|
}
|
|
@@ -8,6 +8,7 @@ import { type RenderContext } from "../render-context.js";
|
|
|
8
8
|
import { crossEntitySpecifier } from "../import-path.js";
|
|
9
9
|
import type { RelationEntry } from "../relation-resolver.js";
|
|
10
10
|
import { effectivePackage } from "../docs-paths.js";
|
|
11
|
+
import { tphStorageName } from "./zod-validators.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Render the relations() block for one entity.
|
|
@@ -31,7 +32,7 @@ export function renderRelationsBlock(entity: MetaObject, ctx: RenderContext): Co
|
|
|
31
32
|
|
|
32
33
|
const thisEntityPackage = effectivePackage(entity);
|
|
33
34
|
const lines: Code[] = entries.map((entry) =>
|
|
34
|
-
renderRelationEntry(entry, ctx, varName, thisEntityPackage),
|
|
35
|
+
renderRelationEntry(entry, ctx, entity.name, varName, thisEntityPackage),
|
|
35
36
|
);
|
|
36
37
|
|
|
37
38
|
return code`export const ${relationsVarName} = ${relationsFn}(${varName}, (${params}) => ({
|
|
@@ -43,6 +44,7 @@ ${joinCode(lines, { on: ",\n", trim: false })}
|
|
|
43
44
|
function renderRelationEntry(
|
|
44
45
|
entry: RelationEntry,
|
|
45
46
|
ctx: RenderContext,
|
|
47
|
+
thisEntityName: string,
|
|
46
48
|
thisVarName: string,
|
|
47
49
|
thisEntityPackage: string | undefined,
|
|
48
50
|
): Code {
|
|
@@ -63,18 +65,24 @@ function renderRelationEntry(
|
|
|
63
65
|
return code` ${entry.name}: many(${junctionVarSym})`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
// Bind to the table that stores the target's rows: a TPH subtype's module has no
|
|
69
|
+
// table const, so a navigation onto `Carrier` binds `parties` from `Party`.
|
|
70
|
+
const targetTableEntity = tphStorageName(entry.targetEntity, ctx.loadedRoot);
|
|
71
|
+
// A navigation onto this entity's OWN table (`Order.parent`, or a TPH base onto one of
|
|
72
|
+
// its subtypes) names the local const: importing it from this very module clashes with
|
|
73
|
+
// its declaration (TS2440). Otherwise imp() tracks and emits the cross-entity import.
|
|
74
|
+
const targetVarSym = targetTableEntity === thisEntityName
|
|
75
|
+
? thisVarName
|
|
76
|
+
: imp(`${ctx.collectionName(targetTableEntity)}@${crossEntitySpecifier(
|
|
77
|
+
ctx.outputLayout,
|
|
78
|
+
thisEntityPackage,
|
|
79
|
+
ctx.packageOf.get(targetTableEntity),
|
|
80
|
+
targetTableEntity,
|
|
81
|
+
ctx.extStyle,
|
|
82
|
+
)}`);
|
|
75
83
|
|
|
76
84
|
if (entry.cardinality === CARDINALITY_ONE) {
|
|
77
|
-
const pkInfo = ctx.pkMap.get(
|
|
85
|
+
const pkInfo = ctx.pkMap.get(targetTableEntity);
|
|
78
86
|
const targetPkField = pkInfo?.fieldName ?? "id";
|
|
79
87
|
return code` ${entry.name}: one(${targetVarSym}, { fields: [${thisVarName}.${entry.fkField ?? "id"}], references: [${targetVarSym}.${targetPkField}] })`;
|
|
80
88
|
}
|
|
@@ -31,6 +31,7 @@ import type { RelationEntry } from "../relation-resolver.js";
|
|
|
31
31
|
import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
|
|
32
32
|
import { authSeamJsDoc, type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
|
|
33
33
|
import { effectivePackage } from "../docs-paths.js";
|
|
34
|
+
import { tphDiscriminatorPin, tphStorageObject } from "./zod-validators.js";
|
|
34
35
|
|
|
35
36
|
export function renderRoutesFile(
|
|
36
37
|
entity: MetaObject,
|
|
@@ -288,18 +289,30 @@ function renderM2mMount(
|
|
|
288
289
|
ctx.extStyle,
|
|
289
290
|
)}`,
|
|
290
291
|
);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
292
|
+
// An M:N onto a TPH subtype traverses into its discriminator BASE's table — the
|
|
293
|
+
// subtype has no table const, and the junction FK can only point at the base table —
|
|
294
|
+
// and filters the rows to the subtype, because a Broker id in that FK column is not a
|
|
295
|
+
// Carrier. Every other target binds to itself, and emits no filter.
|
|
296
|
+
const declaredTarget = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
297
|
+
const target = declaredTarget === undefined ? undefined : tphStorageObject(declaredTarget);
|
|
298
|
+
const targetTableEntity = target?.name ?? entry.targetEntity;
|
|
299
|
+
const pin = declaredTarget === undefined ? undefined : tphDiscriminatorPin(declaredTarget);
|
|
300
|
+
// A self-join's target table IS the source table, which this file already imports from
|
|
301
|
+
// the entity module; a second import of the same binding is TS2300, and a SyntaxError
|
|
302
|
+
// when Node loads the module.
|
|
303
|
+
const targetVarSym = targetTableEntity === source.name
|
|
304
|
+
? ctx.collectionName(source.name)
|
|
305
|
+
: imp(
|
|
306
|
+
`${ctx.collectionName(targetTableEntity)}@${crossEntitySpecifier(
|
|
307
|
+
ctx.outputLayout,
|
|
308
|
+
sourcePkg,
|
|
309
|
+
ctx.packageOf.get(targetTableEntity),
|
|
310
|
+
targetTableEntity,
|
|
311
|
+
ctx.extStyle,
|
|
312
|
+
)}`,
|
|
313
|
+
);
|
|
300
314
|
const mountM2mRouteSym = imp("mountM2mRoute@@metaobjectsdev/runtime-ts/drizzle-fastify");
|
|
301
315
|
const junction = ctx.loadedRoot.findObject(entry.junctionEntity);
|
|
302
|
-
const target = ctx.loadedRoot.findObject(entry.targetEntity);
|
|
303
316
|
// fromPackage = source.package: this routes file is SOURCE's own module, never the
|
|
304
317
|
// junction's or the target's — see resolveJunctionColumn's doc comment (B1).
|
|
305
318
|
const sourceColumn: Code = junction
|
|
@@ -309,8 +322,12 @@ function renderM2mMount(
|
|
|
309
322
|
? resolveJunctionColumn(junction, entry.targetJoinField!, ctx, sourcePkg)
|
|
310
323
|
: code`${JSON.stringify(entry.targetJoinField!)}`;
|
|
311
324
|
const targetPkColumn: Code = target
|
|
312
|
-
? resolveJunctionColumn(target, ctx.pkMap.get(
|
|
325
|
+
? resolveJunctionColumn(target, ctx.pkMap.get(targetTableEntity)?.fieldName ?? "id", ctx, sourcePkg)
|
|
313
326
|
: code`${JSON.stringify("id")}`;
|
|
327
|
+
const discriminatorLine: Code | string = pin !== undefined && target !== undefined
|
|
328
|
+
? code`
|
|
329
|
+
targetDiscriminator: { column: ${resolveJunctionColumn(target, pin.fieldName, ctx, sourcePkg)}, value: ${JSON.stringify(pin.value)} },`
|
|
330
|
+
: "";
|
|
314
331
|
|
|
315
332
|
return code` ${mountM2mRouteSym}({
|
|
316
333
|
fastify: ${fastifyVar},
|
|
@@ -322,7 +339,7 @@ function renderM2mMount(
|
|
|
322
339
|
sourceColumn: ${sourceColumn},
|
|
323
340
|
targetColumn: ${targetColumn},
|
|
324
341
|
targetPkColumn: ${targetPkColumn},
|
|
325
|
-
symmetric: ${entry.symmetric ? "true" : "false"}
|
|
342
|
+
symmetric: ${entry.symmetric ? "true" : "false"},${discriminatorLine}
|
|
326
343
|
});`;
|
|
327
344
|
}
|
|
328
345
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
|
|
11
11
|
|
|
12
12
|
import { code, joinCode, imp, type Code } from "ts-poet";
|
|
13
|
-
import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
|
|
13
|
+
import { MetaObject, MetaField, type MetaRoot, stripPackage } from "@metaobjectsdev/metadata";
|
|
14
14
|
import {
|
|
15
15
|
FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
|
|
16
16
|
FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
|
|
@@ -75,6 +75,28 @@ export function tphDiscriminatorBase(obj: MetaObject): MetaObject | undefined {
|
|
|
75
75
|
return tphDiscriminatorLevel(obj)?.base;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The object whose TABLE stores `obj`'s rows: the discriminator base for anything beneath
|
|
80
|
+
* a TPH base — a concrete subtype or an abstract level in between — otherwise `obj`.
|
|
81
|
+
*
|
|
82
|
+
* Neither a subtype's module nor an abstract level's emits a table const, so anything
|
|
83
|
+
* that binds to "the other side" of a reference or relationship — an FK's `.references()`,
|
|
84
|
+
* a `relations()` entry, an M:N traversal's target table — must bind here and never to the
|
|
85
|
+
* target's own name. Binding to the subtype imported `carriers` from a `Carrier.ts` that
|
|
86
|
+
* does not export it.
|
|
87
|
+
*/
|
|
88
|
+
export function tphStorageObject(obj: MetaObject): MetaObject {
|
|
89
|
+
if (declaresTphDiscriminator(obj)) return obj;
|
|
90
|
+
return tphDiscriminatorBase(obj) ?? obj;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** {@link tphStorageObject} by entity name, for the name-keyed relation map. A name the
|
|
94
|
+
* root does not resolve is returned unchanged. */
|
|
95
|
+
export function tphStorageName(entityName: string, root: MetaRoot): string {
|
|
96
|
+
const obj = root.findObject(entityName);
|
|
97
|
+
return obj === undefined ? entityName : tphStorageObject(obj).name;
|
|
98
|
+
}
|
|
99
|
+
|
|
78
100
|
/**
|
|
79
101
|
* True when this object itself DECLARES `@discriminator` — the level of a TPH hierarchy
|
|
80
102
|
* that owns the discriminator, whether or not any concrete subtype extends it yet.
|