@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.
Files changed (44) hide show
  1. package/dist/generators/api-model.d.ts.map +1 -1
  2. package/dist/generators/api-model.js +6 -6
  3. package/dist/generators/api-model.js.map +1 -1
  4. package/dist/generators/queries-file.d.ts.map +1 -1
  5. package/dist/generators/queries-file.js +7 -2
  6. package/dist/generators/queries-file.js.map +1 -1
  7. package/dist/generators/routes-file-hono.d.ts +2 -1
  8. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  9. package/dist/generators/routes-file-hono.js +4 -3
  10. package/dist/generators/routes-file-hono.js.map +1 -1
  11. package/dist/generators/routes-file.d.ts +2 -1
  12. package/dist/generators/routes-file.d.ts.map +1 -1
  13. package/dist/generators/routes-file.js +4 -3
  14. package/dist/generators/routes-file.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  20. package/dist/templates/drizzle-schema.js +23 -4
  21. package/dist/templates/drizzle-schema.js.map +1 -1
  22. package/dist/templates/relations-block.d.ts.map +1 -1
  23. package/dist/templates/relations-block.js +13 -6
  24. package/dist/templates/relations-block.js.map +1 -1
  25. package/dist/templates/routes-file.d.ts.map +1 -1
  26. package/dist/templates/routes-file.js +21 -4
  27. package/dist/templates/routes-file.js.map +1 -1
  28. package/dist/templates/zod-validators.d.ts +15 -1
  29. package/dist/templates/zod-validators.d.ts.map +1 -1
  30. package/dist/templates/zod-validators.js +21 -0
  31. package/dist/templates/zod-validators.js.map +1 -1
  32. package/package.json +6 -6
  33. package/src/generators/api-model.ts +6 -6
  34. package/src/generators/queries-file.ts +7 -2
  35. package/src/generators/routes-file-hono.ts +4 -3
  36. package/src/generators/routes-file.ts +4 -3
  37. package/src/index.ts +1 -1
  38. package/src/reference/queries.ts +4 -2
  39. package/src/reference/routes-hono.ts +2 -2
  40. package/src/reference/routes.ts +4 -3
  41. package/src/templates/drizzle-schema.ts +21 -4
  42. package/src/templates/relations-block.ts +19 -11
  43. package/src/templates/routes-file.ts +29 -12
  44. 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
- for (const ref of obj.referenceIdentities()) {
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(targetObj.name),
433
- targetEntityName: targetObj.name,
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
- // Use imp() for cross-entity references so ts-poet tracks and emits the import.
67
- const targetSpec = crossEntitySpecifier(
68
- ctx.outputLayout,
69
- thisEntityPackage,
70
- ctx.packageOf.get(entry.targetEntity),
71
- entry.targetEntity,
72
- ctx.extStyle,
73
- );
74
- const targetVarSym = imp(`${ctx.collectionName(entry.targetEntity)}@${targetSpec}`);
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(entry.targetEntity);
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
- const targetVarSym = imp(
292
- `${ctx.collectionName(entry.targetEntity)}@${crossEntitySpecifier(
293
- ctx.outputLayout,
294
- sourcePkg,
295
- ctx.packageOf.get(entry.targetEntity),
296
- entry.targetEntity,
297
- ctx.extStyle,
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(entry.targetEntity)?.fieldName ?? "id", ctx, sourcePkg)
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.