@metaobjectsdev/codegen-ts 1.0.0-rc.3 → 1.0.0-rc.5

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 (138) hide show
  1. package/dist/column-mapper.d.ts +12 -0
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +16 -0
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/generator-registry.d.ts.map +1 -1
  6. package/dist/generator-registry.js +1 -0
  7. package/dist/generator-registry.js.map +1 -1
  8. package/dist/generators/agent-ui-page.d.ts.map +1 -1
  9. package/dist/generators/agent-ui-page.js +11 -9
  10. package/dist/generators/agent-ui-page.js.map +1 -1
  11. package/dist/generators/api-model.d.ts +2 -1
  12. package/dist/generators/api-model.d.ts.map +1 -1
  13. package/dist/generators/api-model.js +12 -6
  14. package/dist/generators/api-model.js.map +1 -1
  15. package/dist/generators/barrel.d.ts.map +1 -1
  16. package/dist/generators/barrel.js +2 -1
  17. package/dist/generators/barrel.js.map +1 -1
  18. package/dist/generators/callable-file.d.ts.map +1 -1
  19. package/dist/generators/callable-file.js +2 -1
  20. package/dist/generators/callable-file.js.map +1 -1
  21. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  22. package/dist/generators/docs-data-builder.js +6 -5
  23. package/dist/generators/docs-data-builder.js.map +1 -1
  24. package/dist/generators/entity-file.d.ts.map +1 -1
  25. package/dist/generators/entity-file.js +2 -1
  26. package/dist/generators/entity-file.js.map +1 -1
  27. package/dist/generators/names-file.d.ts +22 -1
  28. package/dist/generators/names-file.d.ts.map +1 -1
  29. package/dist/generators/names-file.js +12 -18
  30. package/dist/generators/names-file.js.map +1 -1
  31. package/dist/generators/queries-file.d.ts.map +1 -1
  32. package/dist/generators/queries-file.js +2 -1
  33. package/dist/generators/queries-file.js.map +1 -1
  34. package/dist/generators/render-helper-file.js +1 -1
  35. package/dist/generators/render-helper-file.js.map +1 -1
  36. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  37. package/dist/generators/requirements-markdown.js +26 -1
  38. package/dist/generators/requirements-markdown.js.map +1 -1
  39. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  40. package/dist/generators/routes-file-hono.js +2 -1
  41. package/dist/generators/routes-file-hono.js.map +1 -1
  42. package/dist/generators/routes-file.d.ts.map +1 -1
  43. package/dist/generators/routes-file.js +2 -1
  44. package/dist/generators/routes-file.js.map +1 -1
  45. package/dist/index.d.ts +6 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +10 -5
  48. package/dist/index.js.map +1 -1
  49. package/dist/names.d.ts.map +1 -1
  50. package/dist/names.js +5 -4
  51. package/dist/names.js.map +1 -1
  52. package/dist/overwrite-policy.d.ts +9 -7
  53. package/dist/overwrite-policy.d.ts.map +1 -1
  54. package/dist/overwrite-policy.js +22 -0
  55. package/dist/overwrite-policy.js.map +1 -1
  56. package/dist/runner.d.ts +24 -0
  57. package/dist/runner.d.ts.map +1 -1
  58. package/dist/runner.js +96 -8
  59. package/dist/runner.js.map +1 -1
  60. package/dist/templates/callable-file.d.ts.map +1 -1
  61. package/dist/templates/callable-file.js +2 -1
  62. package/dist/templates/callable-file.js.map +1 -1
  63. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  64. package/dist/templates/drizzle-schema.js +39 -14
  65. package/dist/templates/drizzle-schema.js.map +1 -1
  66. package/dist/templates/entity-file.d.ts.map +1 -1
  67. package/dist/templates/entity-file.js +4 -2
  68. package/dist/templates/entity-file.js.map +1 -1
  69. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  70. package/dist/templates/filter-allowlist.js +10 -6
  71. package/dist/templates/filter-allowlist.js.map +1 -1
  72. package/dist/templates/filter-shared.d.ts +42 -1
  73. package/dist/templates/filter-shared.d.ts.map +1 -1
  74. package/dist/templates/filter-shared.js +68 -5
  75. package/dist/templates/filter-shared.js.map +1 -1
  76. package/dist/templates/inferred-types.d.ts.map +1 -1
  77. package/dist/templates/inferred-types.js +9 -6
  78. package/dist/templates/inferred-types.js.map +1 -1
  79. package/dist/templates/projection-decl.d.ts.map +1 -1
  80. package/dist/templates/projection-decl.js +4 -2
  81. package/dist/templates/projection-decl.js.map +1 -1
  82. package/dist/templates/queries-file.d.ts.map +1 -1
  83. package/dist/templates/queries-file.js +6 -5
  84. package/dist/templates/queries-file.js.map +1 -1
  85. package/dist/templates/relations-block.d.ts.map +1 -1
  86. package/dist/templates/relations-block.js +2 -1
  87. package/dist/templates/relations-block.js.map +1 -1
  88. package/dist/templates/render-helper.d.ts +12 -1
  89. package/dist/templates/render-helper.d.ts.map +1 -1
  90. package/dist/templates/render-helper.js +30 -7
  91. package/dist/templates/render-helper.js.map +1 -1
  92. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  93. package/dist/templates/routes-file-hono.js +2 -1
  94. package/dist/templates/routes-file-hono.js.map +1 -1
  95. package/dist/templates/routes-file.d.ts.map +1 -1
  96. package/dist/templates/routes-file.js +18 -10
  97. package/dist/templates/routes-file.js.map +1 -1
  98. package/dist/templates/zod-validators.d.ts.map +1 -1
  99. package/dist/templates/zod-validators.js +7 -4
  100. package/dist/templates/zod-validators.js.map +1 -1
  101. package/package.json +6 -6
  102. package/src/column-mapper.ts +17 -0
  103. package/src/generator-registry.ts +1 -0
  104. package/src/generators/agent-ui-page.ts +10 -10
  105. package/src/generators/api-model.ts +14 -7
  106. package/src/generators/barrel.ts +2 -1
  107. package/src/generators/callable-file.ts +2 -1
  108. package/src/generators/docs-data-builder.ts +6 -5
  109. package/src/generators/entity-file.ts +2 -1
  110. package/src/generators/names-file.ts +33 -4
  111. package/src/generators/queries-file.ts +2 -1
  112. package/src/generators/render-helper-file.ts +1 -1
  113. package/src/generators/requirements-markdown.ts +28 -1
  114. package/src/generators/routes-file-hono.ts +2 -1
  115. package/src/generators/routes-file.ts +2 -1
  116. package/src/index.ts +17 -5
  117. package/src/names.ts +5 -4
  118. package/src/overwrite-policy.ts +19 -9
  119. package/src/reference/barrel.ts +2 -1
  120. package/src/reference/entity.ts +2 -2
  121. package/src/reference/names.ts +26 -4
  122. package/src/reference/queries.ts +3 -3
  123. package/src/reference/routes-hono.ts +2 -1
  124. package/src/reference/routes.ts +2 -1
  125. package/src/runner.ts +112 -7
  126. package/src/templates/callable-file.ts +2 -1
  127. package/src/templates/drizzle-schema.ts +43 -13
  128. package/src/templates/entity-file.ts +4 -2
  129. package/src/templates/filter-allowlist.ts +9 -6
  130. package/src/templates/filter-shared.ts +91 -6
  131. package/src/templates/inferred-types.ts +9 -6
  132. package/src/templates/projection-decl.ts +4 -2
  133. package/src/templates/queries-file.ts +6 -5
  134. package/src/templates/relations-block.ts +2 -1
  135. package/src/templates/render-helper.ts +31 -6
  136. package/src/templates/routes-file-hono.ts +2 -1
  137. package/src/templates/routes-file.ts +18 -10
  138. package/src/templates/zod-validators.ts +7 -4
@@ -1,10 +1,31 @@
1
- // Shared predicate for "is this field sortable?" — used by both filter-allowlist.ts
2
- // (SortAllowlist generation) and filter-type.ts (sort union generation).
3
- // Both must agree on which fields are sortable; keeping them in sync via this shared
4
- // helper prevents client/server mismatches.
1
+ // Shared answers to "how does this entity sort?" — used by every tier that has to
2
+ // answer it, so none of them can answer it differently.
3
+ //
4
+ // Two questions live here:
5
+ // WHICH fields sort — isSortableField / sortableFields, read by the generated
6
+ // <Entity>SortAllowlist (filter-allowlist.ts), the client
7
+ // <Entity>Filter sort union (filter-type.ts) and the agent
8
+ // UI page.
9
+ // WHICH WAY one sorts — declaredSortDefaultOrder / sortDefaultOrder /
10
+ // resolveGridDefaultSort, read by the same allowlist, the
11
+ // generated grid const + grid hook, and the agent UI page.
12
+ //
13
+ // Keeping both here is what prevents a client/server mismatch: a header the grid
14
+ // renders clickable for a field the endpoint rejects, or a grid whose initial sort
15
+ // runs the opposite way from the endpoint it queries.
5
16
 
6
- import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
7
- import { FIELD_ATTR_FILTERABLE, FIELD_ATTR_SORTABLE } from "@metaobjectsdev/metadata";
17
+ import { MetaField, MetaObject, type MetaData } from "@metaobjectsdev/metadata";
18
+ import {
19
+ FIELD_ATTR_FILTERABLE,
20
+ FIELD_ATTR_SORTABLE,
21
+ FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
22
+ LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
23
+ LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER,
24
+ } from "@metaobjectsdev/metadata";
25
+
26
+ /** A resolved sort direction. Never widened to `string`: every door below either
27
+ * returns one of these two or says it has no answer. */
28
+ export type SortOrder = "asc" | "desc";
8
29
 
9
30
  /**
10
31
  * Returns true if the given field should be included in sort operations.
@@ -28,3 +49,67 @@ export function sortableFields(entity: MetaObject): MetaField[] {
28
49
  // fields() returns effective fields, so inherited fields (from extends:/super:) are included in sort ops.
29
50
  return entity.fields().filter(isSortableField);
30
51
  }
52
+
53
+ /**
54
+ * The direction `@sortableDefaultOrder` DECLARES on this field, or undefined when
55
+ * the field declares none.
56
+ *
57
+ * The declared value is what the generated `<Entity>SortAllowlist` carries, so an
58
+ * undeclared field stays `{}` there and the runtime's own `?? "asc"` remains the
59
+ * single place the fallback is spelled. Callers that need a concrete direction
60
+ * rather than a declaration use `sortDefaultOrder`.
61
+ *
62
+ * ADR-0039: resolving — `@sortableDefaultOrder` may be inherited via `extends`.
63
+ */
64
+ export function declaredSortDefaultOrder(field: MetaField): SortOrder | undefined {
65
+ const v = field.attr(FIELD_ATTR_SORTABLE_DEFAULT_ORDER);
66
+ return v === "asc" || v === "desc" ? v : undefined;
67
+ }
68
+
69
+ /** The direction a sort runs when it names this field without giving one. */
70
+ export function sortDefaultOrder(field: MetaField): SortOrder {
71
+ return declaredSortDefaultOrder(field) ?? "asc";
72
+ }
73
+
74
+ /** A grid's initial sort: the field it runs on and the direction it runs. */
75
+ export interface GridDefaultSort {
76
+ field: string;
77
+ order: SortOrder;
78
+ }
79
+
80
+ /**
81
+ * A `layout.dataGrid`'s initial sort, resolved once for every tier that renders it.
82
+ *
83
+ * `@defaultSortOrder` wins when the layout declares one. When it does not, the
84
+ * NAMED FIELD's `@sortableDefaultOrder` applies — that is what the attribute has
85
+ * always been registered to mean ("the direction a sort takes when it names this
86
+ * field but omits the order"), and it is the same rule the runtime applies to
87
+ * `?sort=<field>` with no `:order`. Without this the two ends of one declaration
88
+ * disagree: the endpoint returns the field's declared order while the grid that
89
+ * queries it renders the opposite.
90
+ *
91
+ * Returns undefined when the layout names no field — a grid with no initial sort.
92
+ *
93
+ * The field lookup cannot miss on a LOADED model: `validateDataGridSortFields`
94
+ * refuses `@defaultSortField` naming a field the entity does not have
95
+ * (`ERR_BAD_DEFAULT_SORT_FIELD`). "asc" is the answer for a hand-built node that
96
+ * never went through the loader, so codegen degrades rather than throwing.
97
+ */
98
+ // `layout` is typed as the base MetaData rather than MetaLayout because the three
99
+ // call sites hold the node at different static types (`entity.layouts()` gives
100
+ // MetaLayout, the agent page's own `dataGrids()` gives MetaData) and this reads
101
+ // nothing but `attr()`, which is declared there. Widening beats a cast per caller.
102
+ export function resolveGridDefaultSort(
103
+ entity: MetaObject,
104
+ layout: MetaData,
105
+ ): GridDefaultSort | undefined {
106
+ // ADR-0039: resolving — a layout may inherit its grid attrs via extends.
107
+ const fieldName = layout.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD);
108
+ if (typeof fieldName !== "string" || fieldName === "") return undefined;
109
+
110
+ const declared = layout.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER);
111
+ if (declared === "asc" || declared === "desc") return { field: fieldName, order: declared };
112
+
113
+ const field = entity.fields().find((f) => f.name === fieldName);
114
+ return { field: fieldName, order: field === undefined ? "asc" : sortDefaultOrder(field) };
115
+ }
@@ -42,6 +42,7 @@ import { renderDocsFor } from "./jsdoc.js";
42
42
  import { sharedEnumForField } from "../enum-shared.js";
43
43
  import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
44
44
  import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
45
+ import { effectivePackage } from "../docs-paths.js";
45
46
 
46
47
  /**
47
48
  * Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
@@ -179,7 +180,7 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
179
180
  // configured external module; never redeclare the union here.
180
181
  const spec = shared.provided
181
182
  ? providedEnumImportSpecifier(ctx!, shared.name)
182
- : sharedEnumImportSpecifier(ctx!, entity.package);
183
+ : sharedEnumImportSpecifier(ctx!, effectivePackage(entity));
183
184
  // Re-export the runtime constant beside the type for a MATERIALIZED shared enum, so a
184
185
  // consumer reaches its members the same way it reaches an inline enum's, without having
185
186
  // to know which kind it is or which module the shared one landed in.
@@ -305,9 +306,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
305
306
  // layout/package/extStyle-aware helper (the SAME one the Zod schema +
306
307
  // Drizzle .$type<> use) so all three agree. Without a ctx (bare unit-test
307
308
  // calls) fall back to the bare name + flat same-dir specifier.
308
- const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
309
+ const entityPkg = effectivePackage(entity);
310
+ const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entityPkg)) : stripPackage(ref);
309
311
  const moduleSpec = ctx
310
- ? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
312
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle)
311
313
  : `./${refName}.js`;
312
314
  const refImp = imp(`${refName}@${moduleSpec}`);
313
315
  return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
@@ -319,9 +321,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
319
321
  if (field.subType === FIELD_SUBTYPE_MAP) {
320
322
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
321
323
  if (typeof ref === "string" && ref.length > 0) {
322
- const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
324
+ const entityPkg = effectivePackage(entity);
325
+ const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entityPkg)) : stripPackage(ref);
323
326
  const moduleSpec = ctx
324
- ? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
327
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle)
325
328
  : `./${refName}.js`;
326
329
  const refImp = imp(`${refName}@${moduleSpec}`);
327
330
  return code`Record<string, ${refImp}>`;
@@ -349,7 +352,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
349
352
  if (shared !== undefined) {
350
353
  const spec = shared.provided
351
354
  ? providedEnumImportSpecifier(ctx, shared.name)
352
- : sharedEnumImportSpecifier(ctx, entity.package);
355
+ : sharedEnumImportSpecifier(ctx, effectivePackage(entity));
353
356
  const sym = imp(`t:${shared.name}@${spec}`);
354
357
  return field.resolvedIsArray() ? code`${sym}[]` : code`${sym}`;
355
358
  }
@@ -23,6 +23,7 @@ import { renderFilterType } from "./filter-type.js";
23
23
  import { buildUiFieldDescriptor, resourcePath } from "./entity-ui-descriptor.js";
24
24
  import { renderExistingViewDecl, renderViewReadZodObject } from "./view-decl.js";
25
25
  import { primaryIdentityFieldNames } from "./zod-validators.js";
26
+ import { effectivePackage } from "../docs-paths.js";
26
27
 
27
28
  // ---------------------------------------------------------------------------
28
29
  // Public interface
@@ -103,14 +104,15 @@ export function renderProjectionDecl(
103
104
  // package-qualified on a cross-package short-name collision, so the projection's
104
105
  // VO import matches the entity's. Layout/package/extStyle-aware when a render
105
106
  // context is present, else a flat same-dir import (zodFieldExpr's fallback).
107
+ const projectionPkg = effectivePackage(projection);
106
108
  const voRef = (field: MetaField): { name: string; module: string } => {
107
109
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
108
110
  const rawRef = typeof ref === "string" ? ref : "";
109
111
  const name = ctx
110
- ? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field, projection.package))
112
+ ? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field, projectionPkg))
111
113
  : stripPackage(rawRef);
112
114
  const module = ctx
113
- ? valueObjectModuleSpecifier(name, ctx.packageOf, projection.package, ctx.outputLayout, ctx.extStyle)
115
+ ? valueObjectModuleSpecifier(name, ctx.packageOf, projectionPkg, ctx.outputLayout, ctx.extStyle)
114
116
  : `./${name}.js`;
115
117
  return { name, module };
116
118
  };
@@ -26,6 +26,7 @@ import { GENERATED_HEADER } from "../constants.js";
26
26
  import { isTphDiscriminatorBase, tphConcreteSubtypes } from "./tph-discriminator.js";
27
27
  import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
28
28
  import { hasAutoSetFields } from "./zod-validators.js";
29
+ import { effectivePackage } from "../docs-paths.js";
29
30
 
30
31
  /**
31
32
  * The dialect-correct Drizzle `Db` type alias + its import — parameter-passed into every
@@ -94,7 +95,7 @@ export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
94
95
  const entityFileName = entityModuleSpecifier(
95
96
  ctx.selfTarget,
96
97
  ctx.entityModuleTarget,
97
- obj.package,
98
+ effectivePackage(obj),
98
99
  entityName,
99
100
  ctx.extStyle,
100
101
  );
@@ -163,7 +164,7 @@ function renderProjectionQueriesFile(obj: MetaObject, ctx: RenderContext): strin
163
164
  const camelName = entityName.charAt(0).toLowerCase() + entityName.slice(1);
164
165
  const viewVar = `${camelName}View`;
165
166
  const entityFileName = entityModuleSpecifier(
166
- ctx.selfTarget, ctx.entityModuleTarget, obj.package, entityName, ctx.extStyle,
167
+ ctx.selfTarget, ctx.entityModuleTarget, effectivePackage(obj), entityName, ctx.extStyle,
167
168
  );
168
169
  const { fieldName: pkField, tsType: pkType } = getPkInfo(obj, ctx);
169
170
  const eqSym = imp("eq@drizzle-orm");
@@ -215,7 +216,7 @@ function renderWriteThroughQueriesFile(obj: MetaObject, ctx: RenderContext): str
215
216
  const tableVar = ctx.collectionName(entityName);
216
217
  const singularVar = camelName;
217
218
  const entityFileName = entityModuleSpecifier(
218
- ctx.selfTarget, ctx.entityModuleTarget, obj.package, entityName, ctx.extStyle,
219
+ ctx.selfTarget, ctx.entityModuleTarget, effectivePackage(obj), entityName, ctx.extStyle,
219
220
  );
220
221
  const { fieldName: pkField, tsType: pkType } = getPkInfo(obj, ctx);
221
222
  const pkFields = getPkFields(obj);
@@ -309,7 +310,7 @@ function renderTphQueriesFile(base: MetaObject, ctx: RenderContext): string {
309
310
  const { fieldName: pkField, tsType: pkType } = getPkInfo(base, ctx);
310
311
 
311
312
  const baseFileSpec = entityModuleSpecifier(
312
- ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
313
+ ctx.selfTarget, ctx.entityModuleTarget, effectivePackage(base), baseName, ctx.extStyle,
313
314
  );
314
315
  const tableSym = imp(`${tableVar}@${baseFileSpec}`);
315
316
  const baseTypeSym = imp(`t:${baseName}@${baseFileSpec}`);
@@ -342,7 +343,7 @@ export async function list${pluralize(baseName)}(db: Db, opts?: { limit?: number
342
343
  const value = sub.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE) as string;
343
344
  const valueLit = JSON.stringify(value);
344
345
  const subFileSpec = entityModuleSpecifier(
345
- ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
346
+ ctx.selfTarget, ctx.entityModuleTarget, effectivePackage(sub), sub.name, ctx.extStyle,
346
347
  );
347
348
  const subTypeSym = imp(`t:${sub.name}@${subFileSpec}`);
348
349
  const subSchemaSym = imp(`${sub.name}Schema@${subFileSpec}`);
@@ -7,6 +7,7 @@ import { CARDINALITY_ONE, CARDINALITY_MANY } from "@metaobjectsdev/metadata";
7
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
+ import { effectivePackage } from "../docs-paths.js";
10
11
 
11
12
  /**
12
13
  * Render the relations() block for one entity.
@@ -28,7 +29,7 @@ export function renderRelationsBlock(entity: MetaObject, ctx: RenderContext): Co
28
29
  if (hasMany) paramParts.push("many");
29
30
  const params = `{ ${paramParts.join(", ")} }`;
30
31
 
31
- const thisEntityPackage = entity.package;
32
+ const thisEntityPackage = effectivePackage(entity);
32
33
  const lines: Code[] = entries.map((entry) =>
33
34
  renderRelationEntry(entry, ctx, varName, thisEntityPackage),
34
35
  );
@@ -53,6 +53,8 @@ import {
53
53
  type VerifyError,
54
54
  } from "@metaobjectsdev/render";
55
55
  import { templateSymbolBase } from "../naming.js";
56
+ import { GENERATED_HEADER } from "../constants.js";
57
+ import type { ExtStyle } from "../metaobjects-config.js";
56
58
 
57
59
  // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
58
60
  // ADR-0042: package-local — resolveObjectRef binds a bare @objectRef in `referrerPkg` first (else root-level), an FQN exactly.
@@ -65,6 +67,20 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
65
67
  return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
66
68
  }
67
69
 
70
+ /**
71
+ * The three lines every emitted render-helper module opens with: the generated-file
72
+ * marker, the `render()` import, and the typed payload import. Identical between the
73
+ * email and document kinds below — only the `@metaobjectsdev/render` type import
74
+ * differs (email additionally needs `EmailDocument`), so that piece is a parameter
75
+ * rather than a second near-copy of this block.
76
+ */
77
+ function renderHelperHeader(typeImports: string, payloadTypeName: string, ext: string): string {
78
+ return `// ${GENERATED_HEADER} — DO NOT EDIT.
79
+ import { render } from "@metaobjectsdev/render";
80
+ import type { ${typeImports} } from "@metaobjectsdev/render";
81
+ import type { ${payloadTypeName} } from "./${payloadTypeName}${ext}";`;
82
+ }
83
+
68
84
  /**
69
85
  * Walk an `object.value` view-object into a render `PayloadField[]`. Object-ref
70
86
  * fields recurse into their referenced view-object; a `seen` set guards a
@@ -150,6 +166,16 @@ export function renderRenderHelper(
150
166
  root: MetaData,
151
167
  templateName: string,
152
168
  provider: Provider,
169
+ /**
170
+ * The project's import-extension style, as every other emitter honours it.
171
+ * Defaults to "js" — the engine default — so an existing caller is unchanged.
172
+ *
173
+ * This file used to hardcode `.js` regardless. Both spellings happen to resolve
174
+ * under nodenext AND bundler, so nothing broke; it was simply the one emitter
175
+ * that did not read the setting, which is how a project with `extStyle: "none"`
176
+ * ended up with one file spelling its imports differently from every other.
177
+ */
178
+ extStyle: ExtStyle = "js",
153
179
  ): string {
154
180
  const tmpl = findTemplate(root, templateName);
155
181
  if (!tmpl) {
@@ -186,6 +212,9 @@ export function renderRenderHelper(
186
212
  // resolution above (findObject/derivePayloadFieldTree) intentionally keeps
187
213
  // using the raw `payloadRef`.
188
214
  const payloadTypeName = stripPackage(payloadRef);
215
+ // The emitted relative import's extension, from the project's setting rather than
216
+ // hardcoded — see the `extStyle` parameter doc.
217
+ const ext = extStyle === "none" ? "" : ".js";
189
218
 
190
219
  // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
191
220
  const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT)
@@ -215,9 +244,7 @@ export function renderRenderHelper(
215
244
  ? `\n textBody: render({ ref: ${JSON.stringify(textBodyRef)}, payload, format: "text", provider, verify: ${ft} }),`
216
245
  : "";
217
246
 
218
- return `import { render } from "@metaobjectsdev/render";
219
- import type { Provider, EmailDocument } from "@metaobjectsdev/render";
220
- import type { ${payloadTypeName} } from "./${payloadTypeName}.js";
247
+ return `${renderHelperHeader("Provider, EmailDocument", payloadTypeName, ext)}
221
248
 
222
249
  /**
223
250
  * Render the ${templateName} email (subject + html body${typeof textBodyRef === "string" ? " + text body" : ""}) from a
@@ -255,9 +282,7 @@ export function ${fnName}(payload: ${payloadTypeName}, provider: Provider): Emai
255
282
  const maxCharsArg =
256
283
  maxChars !== undefined && Number.isFinite(maxChars) ? `, maxChars: ${maxChars}` : "";
257
284
 
258
- return `import { render } from "@metaobjectsdev/render";
259
- import type { Provider } from "@metaobjectsdev/render";
260
- import type { ${payloadTypeName} } from "./${payloadTypeName}.js";
285
+ return `${renderHelperHeader("Provider", payloadTypeName, ext)}
261
286
 
262
287
  /**
263
288
  * Render the ${templateName} document from a typed ${payloadTypeName} payload. Wraps the
@@ -31,6 +31,7 @@ import { entityModuleSpecifier } from "../import-path.js";
31
31
  import { GENERATED_HEADER } from "../constants.js";
32
32
  import { isProjection, isWriteThrough } from "../projection/projection-detector.js";
33
33
  import { type CrudVerb, exposeLine } from "../routes-expose.js";
34
+ import { effectivePackage } from "../docs-paths.js";
34
35
 
35
36
  export function renderRoutesFileHono(
36
37
  entity: MetaObject,
@@ -49,7 +50,7 @@ export function renderRoutesFileHono(
49
50
  const entityFileSpec = entityModuleSpecifier(
50
51
  ctx.selfTarget,
51
52
  ctx.entityModuleTarget,
52
- entity.package,
53
+ effectivePackage(entity),
53
54
  entityName,
54
55
  ctx.extStyle,
55
56
  );
@@ -30,6 +30,7 @@ import { isProjection, isWriteThrough } from "../projection/projection-detector.
30
30
  import type { RelationEntry } from "../relation-resolver.js";
31
31
  import { isTphDiscriminatorBase, tphPlan } from "./tph-discriminator.js";
32
32
  import { type CrudVerb, exposeLine, intersectExpose, TPH_POLYMORPHIC_VERBS } from "../routes-expose.js";
33
+ import { effectivePackage } from "../docs-paths.js";
33
34
 
34
35
  export function renderRoutesFile(
35
36
  entity: MetaObject,
@@ -49,16 +50,17 @@ export function renderRoutesFile(
49
50
 
50
51
  const entityName = entity.name;
51
52
  const handlerName = routesHandlerName(entityName);
53
+ const entityPkg = effectivePackage(entity);
52
54
  // Import the entity's own file. Same target → relative "./Entity"; cross
53
55
  // target → importBase-qualified package path.
54
56
  const entityFileSpec = entityModuleSpecifier(
55
57
  ctx.selfTarget,
56
58
  ctx.entityModuleTarget,
57
- entity.package,
59
+ entityPkg,
58
60
  entityName,
59
61
  ctx.extStyle,
60
62
  );
61
- const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, entity.package, ctx.dbImport, ctx.extStyle);
63
+ const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, entityPkg, ctx.dbImport, ctx.extStyle);
62
64
 
63
65
  const header =
64
66
  `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
@@ -260,10 +262,15 @@ function renderM2mMount(
260
262
  ctx: RenderContext,
261
263
  fastifyVar: string,
262
264
  ): Code {
265
+ // `source` never changes across this function, so its effective package is computed
266
+ // once and reused below (both crossEntitySpecifier calls, and the three
267
+ // resolveJunctionColumn calls' `fromPackage` — see that comment for why it must be
268
+ // SOURCE's package rather than the junction/target entity's own).
269
+ const sourcePkg = effectivePackage(source);
263
270
  const junctionVarSym = imp(
264
271
  `${ctx.collectionName(entry.junctionEntity)}@${crossEntitySpecifier(
265
272
  ctx.outputLayout,
266
- source.package,
273
+ sourcePkg,
267
274
  ctx.packageOf.get(entry.junctionEntity),
268
275
  entry.junctionEntity,
269
276
  ctx.extStyle,
@@ -272,7 +279,7 @@ function renderM2mMount(
272
279
  const targetVarSym = imp(
273
280
  `${ctx.collectionName(entry.targetEntity)}@${crossEntitySpecifier(
274
281
  ctx.outputLayout,
275
- source.package,
282
+ sourcePkg,
276
283
  ctx.packageOf.get(entry.targetEntity),
277
284
  entry.targetEntity,
278
285
  ctx.extStyle,
@@ -284,13 +291,13 @@ function renderM2mMount(
284
291
  // fromPackage = source.package: this routes file is SOURCE's own module, never the
285
292
  // junction's or the target's — see resolveJunctionColumn's doc comment (B1).
286
293
  const sourceColumn: Code = junction
287
- ? resolveJunctionColumn(junction, entry.sourceJoinField!, ctx, source.package)
294
+ ? resolveJunctionColumn(junction, entry.sourceJoinField!, ctx, sourcePkg)
288
295
  : code`${JSON.stringify(entry.sourceJoinField!)}`;
289
296
  const targetColumn: Code = junction
290
- ? resolveJunctionColumn(junction, entry.targetJoinField!, ctx, source.package)
297
+ ? resolveJunctionColumn(junction, entry.targetJoinField!, ctx, sourcePkg)
291
298
  : code`${JSON.stringify(entry.targetJoinField!)}`;
292
299
  const targetPkColumn: Code = target
293
- ? resolveJunctionColumn(target, ctx.pkMap.get(entry.targetEntity)?.fieldName ?? "id", ctx, source.package)
300
+ ? resolveJunctionColumn(target, ctx.pkMap.get(entry.targetEntity)?.fieldName ?? "id", ctx, sourcePkg)
294
301
  : code`${JSON.stringify("id")}`;
295
302
 
296
303
  return code` ${mountM2mRouteSym}({
@@ -368,10 +375,11 @@ function renderTphRoutesFile(
368
375
  const discField = plan.discriminatorField;
369
376
  const tableVar = ctx.collectionName(baseName);
370
377
 
378
+ const basePkg = effectivePackage(base);
371
379
  const baseFileSpec = entityModuleSpecifier(
372
- ctx.selfTarget, ctx.entityModuleTarget, base.package, baseName, ctx.extStyle,
380
+ ctx.selfTarget, ctx.entityModuleTarget, basePkg, baseName, ctx.extStyle,
373
381
  );
374
- const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, base.package, ctx.dbImport, ctx.extStyle);
382
+ const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, basePkg, ctx.dbImport, ctx.extStyle);
375
383
 
376
384
  const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
377
385
  const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
@@ -405,7 +413,7 @@ function renderTphRoutesFile(
405
413
 
406
414
  const subtypeMounts: Code[] = plan.subtypes.map(({ entity: sub, value, routeSegment: segment }) => {
407
415
  const subFileSpec = entityModuleSpecifier(
408
- ctx.selfTarget, ctx.entityModuleTarget, sub.package, sub.name, ctx.extStyle,
416
+ ctx.selfTarget, ctx.entityModuleTarget, effectivePackage(sub), sub.name, ctx.extStyle,
409
417
  );
410
418
  const subInsertSym = imp(`${sub.name}InsertSchema@${subFileSpec}`);
411
419
  // FR-036 Program B: the per-subtype UPDATE must carry the FR-035 present-key
@@ -42,6 +42,7 @@ import { valueObjectModuleSpecifier } from "../import-path.js";
42
42
  // FR-035: the SAME required-predicate that drives the Drizzle column's .notNull()
43
43
  // drives the UpdateSchema's .nullable() exclusion — shared so they cannot drift.
44
44
  import { isRequired } from "../column-mapper.js";
45
+ import { effectivePackage } from "../docs-paths.js";
45
46
 
46
47
  /**
47
48
  * FR-017 Tier 1 — when this object is a TPH subtype (@discriminatorValue set
@@ -593,9 +594,10 @@ function zodFieldExpr(
593
594
  // layout/package/extStyle-aware helper (the SAME one the field's TS type +
594
595
  // Drizzle .$type<> use) so all three agree. Without owner/ctx (bare
595
596
  // unit-test calls) fall back to the bare name + flat same-dir.
596
- const refName = (ctx && owner) ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, owner.package)) : stripPackage(ref);
597
+ const ownerPkg = (ctx && owner) ? effectivePackage(owner) : undefined;
598
+ const refName = (ctx && owner) ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, ownerPkg)) : stripPackage(ref);
597
599
  const moduleSpec = (ctx && owner)
598
- ? valueObjectModuleSpecifier(refName, ctx.packageOf, owner.package, ctx.outputLayout, ctx.extStyle)
600
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, ownerPkg, ctx.outputLayout, ctx.extStyle)
599
601
  : `./${refName}.js`;
600
602
  const refImp = imp(`${refName}InsertSchema@${moduleSpec}`);
601
603
  let base: Code = code`${refImp}`;
@@ -614,9 +616,10 @@ function zodFieldExpr(
614
616
  if (field.subType === FIELD_SUBTYPE_MAP) {
615
617
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
616
618
  if (typeof ref === "string" && ref.length > 0) {
617
- const refName = (ctx && owner) ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, owner.package)) : stripPackage(ref);
619
+ const ownerPkg = (ctx && owner) ? effectivePackage(owner) : undefined;
620
+ const refName = (ctx && owner) ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, ownerPkg)) : stripPackage(ref);
618
621
  const moduleSpec = (ctx && owner)
619
- ? valueObjectModuleSpecifier(refName, ctx.packageOf, owner.package, ctx.outputLayout, ctx.extStyle)
622
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, ownerPkg, ctx.outputLayout, ctx.extStyle)
620
623
  : `./${refName}.js`;
621
624
  const refImp = imp(`${refName}InsertSchema@${moduleSpec}`);
622
625
  return appendValidatorChain(code`z.record(z.string(), ${refImp})`, field, forceRequired);