@metaobjectsdev/codegen-ts 0.8.1-rc.1 → 0.9.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.
Files changed (97) hide show
  1. package/dist/column-mapper.d.ts.map +1 -1
  2. package/dist/column-mapper.js +123 -46
  3. package/dist/column-mapper.js.map +1 -1
  4. package/dist/generators/callable-file.d.ts +8 -0
  5. package/dist/generators/callable-file.d.ts.map +1 -0
  6. package/dist/generators/callable-file.js +32 -0
  7. package/dist/generators/callable-file.js.map +1 -0
  8. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  9. package/dist/generators/docs-data-builder.js +5 -2
  10. package/dist/generators/docs-data-builder.js.map +1 -1
  11. package/dist/generators/extractor-file.d.ts +9 -0
  12. package/dist/generators/extractor-file.d.ts.map +1 -0
  13. package/dist/generators/extractor-file.js +45 -0
  14. package/dist/generators/extractor-file.js.map +1 -0
  15. package/dist/generators/index.d.ts +3 -0
  16. package/dist/generators/index.d.ts.map +1 -1
  17. package/dist/generators/index.js +3 -0
  18. package/dist/generators/index.js.map +1 -1
  19. package/dist/generators/render-helper-file.d.ts +9 -0
  20. package/dist/generators/render-helper-file.d.ts.map +1 -0
  21. package/dist/generators/render-helper-file.js +58 -0
  22. package/dist/generators/render-helper-file.js.map +1 -0
  23. package/dist/payload-codegen.d.ts.map +1 -1
  24. package/dist/payload-codegen.js +42 -8
  25. package/dist/payload-codegen.js.map +1 -1
  26. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  27. package/dist/projection/extract-view-spec.js +11 -3
  28. package/dist/projection/extract-view-spec.js.map +1 -1
  29. package/dist/render-engine/framework-provider.d.ts +6 -5
  30. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  31. package/dist/render-engine/framework-provider.js +53 -11
  32. package/dist/render-engine/framework-provider.js.map +1 -1
  33. package/dist/templates/callable-file.d.ts +8 -0
  34. package/dist/templates/callable-file.d.ts.map +1 -0
  35. package/dist/templates/callable-file.js +98 -0
  36. package/dist/templates/callable-file.js.map +1 -0
  37. package/dist/templates/extract-delegate-emitter.d.ts +42 -0
  38. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -0
  39. package/dist/templates/extract-delegate-emitter.js +339 -0
  40. package/dist/templates/extract-delegate-emitter.js.map +1 -0
  41. package/dist/templates/{recover-schema-emitter.d.ts → extract-schema-emitter.d.ts} +2 -2
  42. package/dist/templates/extract-schema-emitter.d.ts.map +1 -0
  43. package/dist/templates/{recover-schema-emitter.js → extract-schema-emitter.js} +37 -20
  44. package/dist/templates/extract-schema-emitter.js.map +1 -0
  45. package/dist/templates/extractor.d.ts +9 -0
  46. package/dist/templates/extractor.d.ts.map +1 -0
  47. package/dist/templates/extractor.js +296 -0
  48. package/dist/templates/extractor.js.map +1 -0
  49. package/dist/templates/field-meta.d.ts.map +1 -1
  50. package/dist/templates/field-meta.js +2 -1
  51. package/dist/templates/field-meta.js.map +1 -1
  52. package/dist/templates/filter-type.d.ts.map +1 -1
  53. package/dist/templates/filter-type.js +8 -5
  54. package/dist/templates/filter-type.js.map +1 -1
  55. package/dist/templates/fr010-field-mapping.d.ts +22 -6
  56. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  57. package/dist/templates/fr010-field-mapping.js +66 -21
  58. package/dist/templates/fr010-field-mapping.js.map +1 -1
  59. package/dist/templates/inferred-types.d.ts +15 -1
  60. package/dist/templates/inferred-types.d.ts.map +1 -1
  61. package/dist/templates/inferred-types.js +30 -17
  62. package/dist/templates/inferred-types.js.map +1 -1
  63. package/dist/templates/output-parser.d.ts.map +1 -1
  64. package/dist/templates/output-parser.js +98 -34
  65. package/dist/templates/output-parser.js.map +1 -1
  66. package/dist/templates/output-prompt.js +2 -2
  67. package/dist/templates/render-helper.d.ts +14 -0
  68. package/dist/templates/render-helper.d.ts.map +1 -0
  69. package/dist/templates/render-helper.js +180 -0
  70. package/dist/templates/render-helper.js.map +1 -0
  71. package/dist/templates/zod-validators.d.ts.map +1 -1
  72. package/dist/templates/zod-validators.js +59 -3
  73. package/dist/templates/zod-validators.js.map +1 -1
  74. package/package.json +10 -4
  75. package/src/column-mapper.ts +128 -45
  76. package/src/generators/callable-file.ts +44 -0
  77. package/src/generators/docs-data-builder.ts +5 -1
  78. package/src/generators/extractor-file.ts +57 -0
  79. package/src/generators/index.ts +3 -0
  80. package/src/generators/render-helper-file.ts +74 -0
  81. package/src/payload-codegen.ts +52 -7
  82. package/src/projection/extract-view-spec.ts +11 -3
  83. package/src/render-engine/framework-provider.ts +53 -16
  84. package/src/templates/callable-file.ts +122 -0
  85. package/src/templates/extract-delegate-emitter.ts +370 -0
  86. package/src/templates/{recover-schema-emitter.ts → extract-schema-emitter.ts} +39 -19
  87. package/src/templates/extractor.ts +333 -0
  88. package/src/templates/field-meta.ts +2 -0
  89. package/src/templates/filter-type.ts +7 -5
  90. package/src/templates/fr010-field-mapping.ts +71 -18
  91. package/src/templates/inferred-types.ts +32 -18
  92. package/src/templates/output-parser.ts +108 -35
  93. package/src/templates/output-prompt.ts +2 -2
  94. package/src/templates/render-helper.ts +244 -0
  95. package/src/templates/zod-validators.ts +51 -4
  96. package/dist/templates/recover-schema-emitter.d.ts.map +0 -1
  97. package/dist/templates/recover-schema-emitter.js.map +0 -1
@@ -12,16 +12,20 @@
12
12
 
13
13
  import {
14
14
  type MetaData,
15
+ type MetaField,
15
16
  TYPE_OBJECT,
16
17
  TYPE_FIELD,
17
18
  TYPE_TEMPLATE,
18
19
  FIELD_SUBTYPE_OBJECT,
20
+ FIELD_SUBTYPE_ENUM,
19
21
  FIELD_ATTR_OBJECT_REF,
20
22
  FIELD_ATTR_REQUIRED,
21
23
  TEMPLATE_ATTR_PAYLOAD_REF,
22
24
  TEMPLATE_ATTR_TEXT_REF,
23
25
  TEMPLATE_ATTR_FORMAT,
24
26
  } from "@metaobjectsdev/metadata";
27
+ import { enumValues } from "./enum-meta.js";
28
+ import { enumUnionAliasName, enumUnionString } from "./templates/inferred-types.js";
25
29
 
26
30
  const SCALAR_TS: Record<string, string> = {
27
31
  string: "string",
@@ -44,7 +48,18 @@ function findObject(root: MetaData, name: string): MetaData | undefined {
44
48
  return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
45
49
  }
46
50
 
47
- function fieldTsType(field: MetaData): { type: string; refVo?: string } {
51
+ /**
52
+ * Map a payload field to its strict TS type.
53
+ * - `refVo`: the nested @objectRef VO to recurse into (object fields only).
54
+ * - `enumAlias`: a `{ name, decl }` for a `field.enum` — `name` is the union-alias
55
+ * type referenced inline; `decl` is the `export type <name> = "A" | "B";` line the
56
+ * caller hoists above the interface (deduped). Reuses the SAME naming + union
57
+ * logic as the entity inferred-types emitter (single source of truth).
58
+ */
59
+ function fieldTsType(
60
+ field: MetaData,
61
+ ownerName: string,
62
+ ): { type: string; refVo?: string; enumAlias?: { name: string; decl: string } } {
48
63
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
49
64
  const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
50
65
  const refName = typeof ref === "string" ? ref : "unknown";
@@ -56,6 +71,21 @@ function fieldTsType(field: MetaData): { type: string; refVo?: string } {
56
71
  if (typeof ref === "string") result.refVo = ref;
57
72
  return result;
58
73
  }
74
+ // field.enum: a value-constrained string-literal union alias (NOT `unknown`).
75
+ // Field nodes are MetaField instances at runtime (MetaField extends MetaData),
76
+ // so the enum helpers (effective @values + super-resolving name) apply.
77
+ if (field.subType === FIELD_SUBTYPE_ENUM) {
78
+ const values = enumValues(field as MetaField);
79
+ if (values !== undefined) {
80
+ const aliasName = enumUnionAliasName(ownerName, field as MetaField);
81
+ return {
82
+ type: field.isArray ? `${aliasName}[]` : aliasName,
83
+ enumAlias: { name: aliasName, decl: `export type ${aliasName} = ${enumUnionString(values)};` },
84
+ };
85
+ }
86
+ // No @values → fall through to the raw-string representation.
87
+ return { type: field.isArray ? "string[]" : "string" };
88
+ }
59
89
  const scalar = SCALAR_TS[field.subType] ?? "unknown";
60
90
  return { type: field.isArray ? `${scalar}[]` : scalar };
61
91
  }
@@ -65,15 +95,28 @@ function isFieldRequired(field: MetaData): boolean {
65
95
  return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
66
96
  }
67
97
 
68
- function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out: string[]): void {
98
+ function emitInterface(
99
+ root: MetaData,
100
+ voName: string,
101
+ emitted: Set<string>,
102
+ out: string[],
103
+ enumAliases: Set<string>,
104
+ ): void {
69
105
  if (emitted.has(voName)) return;
70
106
  const vo = findObject(root, voName);
71
107
  if (!vo) return;
72
108
  emitted.add(voName);
109
+ const aliasLines: string[] = [];
73
110
  const lines: string[] = [`export interface ${voName} {`];
74
111
  const refs: string[] = [];
75
112
  for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
76
- const { type, refVo } = fieldTsType(f);
113
+ const { type, refVo, enumAlias } = fieldTsType(f, voName);
114
+ // Hoist the enum union alias above the interface, deduped across the whole
115
+ // batch (multiple fields/objects can share one abstract enum's alias).
116
+ if (enumAlias && !enumAliases.has(enumAlias.name)) {
117
+ enumAliases.add(enumAlias.name);
118
+ aliasLines.push(enumAlias.decl);
119
+ }
77
120
  // Required fields: `name: T;`
78
121
  // Optional fields: `name?: T | null;` — the `| null` lets values from
79
122
  // Drizzle entity rows (which return `null` for nullable columns) flow
@@ -86,14 +129,15 @@ function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out
86
129
  if (refVo) refs.push(refVo);
87
130
  }
88
131
  lines.push("}");
89
- out.push(lines.join("\n"));
90
- for (const r of refs) emitInterface(root, r, emitted, out);
132
+ const block = aliasLines.length > 0 ? `${aliasLines.join("\n")}\n${lines.join("\n")}` : lines.join("\n");
133
+ out.push(block);
134
+ for (const r of refs) emitInterface(root, r, emitted, out, enumAliases);
91
135
  }
92
136
 
93
137
  /** Emit the payload `interface` (+ nested element interfaces) for an object.value view-object. */
94
138
  export function generatePayloadInterfaces(root: MetaData, voName: string): string {
95
139
  const out: string[] = [];
96
- emitInterface(root, voName, new Set<string>(), out);
140
+ emitInterface(root, voName, new Set<string>(), out, new Set<string>());
97
141
  return out.join("\n\n") + "\n";
98
142
  }
99
143
 
@@ -108,8 +152,9 @@ export function generatePayloadInterfacesBatch(root: MetaData, voNames: readonly
108
152
  if (voNames.length === 0) return "";
109
153
  const out: string[] = [];
110
154
  const emitted = new Set<string>();
155
+ const enumAliases = new Set<string>();
111
156
  for (const name of voNames) {
112
- emitInterface(root, name, emitted, out);
157
+ emitInterface(root, name, emitted, out, enumAliases);
113
158
  }
114
159
  return out.length === 0 ? "" : out.join("\n\n") + "\n";
115
160
  }
@@ -46,12 +46,20 @@ function findRelationship(obj: MetaData, name: string): MetaData | undefined {
46
46
  }
47
47
 
48
48
  function viewName(projection: MetaObject, ctx: ExtractContext): string {
49
- // The read-only source carries the physical view name (@table).
49
+ // The read-only source carries the physical view name. FR-016: physicalName
50
+ // implements the four-step rule (kind-matching alias → legacy @table →
51
+ // source.name → entity-name fallback), so the call below correctly resolves
52
+ // @view / @materializedView / legacy @table for projection sources.
50
53
  const viewSource = projection.ownChildren().find(
51
54
  (c): c is MetaSource => c instanceof MetaSource && c.isReadOnly(),
52
55
  );
53
- const explicit = viewSource?.tableName;
54
- return explicit ?? viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
56
+ const explicit = viewSource?.physicalName;
57
+ // physicalName always returns a string; empty string means the source had
58
+ // neither alias nor a name and the owning entity name was empty (impossible
59
+ // for a real projection). Fall through to the helper anyway for safety.
60
+ return explicit !== undefined && explicit !== ""
61
+ ? explicit
62
+ : viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
55
63
  }
56
64
 
57
65
  /**
@@ -27,8 +27,13 @@ const CANONICAL_TEMPLATE_REL = "docs/entity-page.md.mustache";
27
27
  * `templates/` directory contains our canonical shipped template (i.e., the
28
28
  * codegen-ts package root). Works the same way from
29
29
  * `src/render-engine/framework-provider.ts` (during dev) and
30
- * `dist/render-engine/framework-provider.js` (after `npm install`). */
31
- function findFrameworkTemplatesDir(start: string): string {
30
+ * `dist/render-engine/framework-provider.js` (after `npm install`).
31
+ *
32
+ * Returns `undefined` when no on-disk templates dir can be found — e.g. inside
33
+ * the `bun build --compile` standalone binary, whose `import.meta.url` is a
34
+ * `/$bunfs/root` virtual path with no real `package.json` alongside it. The
35
+ * embedded-template fallback (see `FileSystemProvider`) covers that case. */
36
+ function findFrameworkTemplatesDir(start: string): string | undefined {
32
37
  let dir = start;
33
38
  while (true) {
34
39
  const pkgJson = join(dir, "package.json");
@@ -43,19 +48,25 @@ function findFrameworkTemplatesDir(start: string): string {
43
48
  if (parent === dir) break;
44
49
  dir = parent;
45
50
  }
46
- throw new Error(
47
- `framework templates dir unresolved: walked up from ${start} without finding a package.json ` +
48
- `whose templates/${CANONICAL_TEMPLATE_REL} exists. This usually means codegen-ts was installed ` +
49
- `via a hoisted layout (pnpm/bun workspaces) into an unexpected location, or the published ` +
50
- `tarball is missing the templates/ directory.`,
51
- );
51
+ return undefined;
52
52
  }
53
53
 
54
54
  // In ESM (CLAUDE.md: "ESM only. No CommonJS."), `import.meta.url` is
55
55
  // guaranteed to be a file: URL; no defensive try/catch needed.
56
56
  const SELF_DIR = dirname(fileURLToPath(import.meta.url));
57
57
 
58
- const FRAMEWORK_TEMPLATES_DIR = findFrameworkTemplatesDir(SELF_DIR);
58
+ // Lazy + cached: resolving the on-disk templates dir walks the filesystem, and
59
+ // in the standalone binary it never finds one (the embedded fallback handles
60
+ // it). Deferring the walk keeps merely *importing* this module side-effect-free
61
+ // — critical for the compiled `meta` binary, where eager resolution at import
62
+ // time used to throw before any command (even `--help`) could run.
63
+ let _frameworkTemplatesDir: string | null | undefined;
64
+ function frameworkTemplatesDir(): string | undefined {
65
+ if (_frameworkTemplatesDir === undefined) {
66
+ _frameworkTemplatesDir = findFrameworkTemplatesDir(SELF_DIR) ?? null;
67
+ }
68
+ return _frameworkTemplatesDir ?? undefined;
69
+ }
59
70
 
60
71
  /** Provider backed by an arbitrary on-disk template directory. References
61
72
  * resolve as `<dir>/<ref>.mustache`. Used by both the framework default
@@ -70,10 +81,27 @@ export class FileSystemProvider implements Provider {
70
81
  }
71
82
 
72
83
  /** The framework defaults provider — resolves refs against codegen-ts's own
73
- * `templates/` directory. */
74
- export const frameworkTemplatesProvider: Provider = new FileSystemProvider(
75
- FRAMEWORK_TEMPLATES_DIR,
76
- );
84
+ * on-disk `templates/` directory.
85
+ *
86
+ * Resolution is lazy: the directory is located on first `resolve()`, not at
87
+ * module import. This keeps merely importing this module side-effect-free,
88
+ * which matters for the `bun build --compile` standalone `meta` binary — its
89
+ * `import.meta.url` is a `/$bunfs/root` virtual path with no on-disk
90
+ * `templates/` dir, so eager resolution at import time used to throw before
91
+ * any command (even `--help` or the schema ops `migrate`/`verify --db`) could
92
+ * run. Now non-codegen commands import cleanly; only the codegen doc path
93
+ * (which the standalone binary doesn't target) needs the on-disk dir. */
94
+ class FrameworkTemplatesProvider implements Provider {
95
+ resolve(ref: string): string | undefined {
96
+ const dir = frameworkTemplatesDir();
97
+ if (dir === undefined) return undefined;
98
+ const path = join(dir, `${ref}.mustache`);
99
+ if (!existsSync(path)) return undefined;
100
+ return readFileSync(path, "utf-8");
101
+ }
102
+ }
103
+
104
+ export const frameworkTemplatesProvider: Provider = new FrameworkTemplatesProvider();
77
105
 
78
106
  /** Compose providers: first match wins. Adopters typically chain
79
107
  * `[projectProvider, frameworkTemplatesProvider]` so their own templates
@@ -102,6 +130,15 @@ export function projectProvider(projectRoot?: string): Provider {
102
130
  ]);
103
131
  }
104
132
 
105
- /** Exposed for tests that want to inspect / clear the resolved framework
106
- * templates directory (don't use outside tests). */
107
- export const __frameworkTemplatesDirForTests = FRAMEWORK_TEMPLATES_DIR;
133
+ /** Exposed for tests that want to inspect the resolved framework templates
134
+ * directory (don't use outside tests). Resolved lazily so that merely
135
+ * importing this module never walks the filesystem or throws — tests always
136
+ * run from source where the on-disk dir exists; the standalone binary (where
137
+ * no dir exists) never touches this export. */
138
+ export function frameworkTemplatesDirForTests(): string {
139
+ const dir = frameworkTemplatesDir();
140
+ if (dir === undefined) {
141
+ throw new Error("framework templates dir unresolved (test ran outside a source/install layout)");
142
+ }
143
+ return dir;
144
+ }
@@ -0,0 +1,122 @@
1
+ // FR-015 — render a typed wrapper function for an entity backed by a stored
2
+ // procedure or table function. One generated file per callable entity:
3
+ //
4
+ // import { sql } from "drizzle-orm";
5
+ // import type { NodePgDatabase } from "drizzle-orm/node-postgres";
6
+ // import type { PhaseSummaryArgs } from "./PhaseSummaryArgs.js";
7
+ // import { PhaseSummarySchema, type PhaseSummary } from "./PhaseSummary.js";
8
+ //
9
+ // export async function callPhaseSummary(
10
+ // db: NodePgDatabase,
11
+ // args: PhaseSummaryArgs,
12
+ // ): Promise<PhaseSummary[]> {
13
+ // const r = await db.execute(
14
+ // sql`SELECT * FROM fn_phase_summary(${args.caseId}, ${args.asOfDate})`,
15
+ // );
16
+ // return r.rows.map((row) => PhaseSummarySchema.parse(row));
17
+ // }
18
+ //
19
+ // Zero-argument procs (no @parameterRef) emit a no-args version that drops the
20
+ // `args` parameter and calls `fn_x()`.
21
+
22
+ import {
23
+ type MetaObject,
24
+ MetaSource,
25
+ SOURCE_ATTR_PARAMETER_REF,
26
+ SOURCE_KIND_STORED_PROC,
27
+ SOURCE_KIND_TABLE_FUNCTION,
28
+ TYPE_FIELD,
29
+ TYPE_SOURCE,
30
+ OBJECT_SUBTYPE_VALUE,
31
+ } from "@metaobjectsdev/metadata";
32
+ import { GENERATED_HEADER } from "../constants.js";
33
+
34
+ const CALLABLE_KINDS: ReadonlySet<string> = new Set([
35
+ SOURCE_KIND_STORED_PROC,
36
+ SOURCE_KIND_TABLE_FUNCTION,
37
+ ]);
38
+
39
+ /** Return true when this entity is backed by a callable source (stored
40
+ * procedure or table function). Used by the generator factory to filter. */
41
+ export function isCallableEntity(entity: MetaObject): boolean {
42
+ const src = callableSource(entity);
43
+ return src !== undefined;
44
+ }
45
+
46
+ function callableSource(entity: MetaObject): MetaSource | undefined {
47
+ for (const child of entity.ownChildren()) {
48
+ if (child.type !== TYPE_SOURCE) continue;
49
+ if (!(child instanceof MetaSource)) continue;
50
+ if (CALLABLE_KINDS.has(child.effectiveKind)) return child;
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ /** Render the full file content for an entity's callable wrapper. Caller
56
+ * is responsible for formatting (prettier / biome) and writing to disk. */
57
+ export function renderCallableFile(entity: MetaObject): string {
58
+ const source = callableSource(entity);
59
+ if (source === undefined) {
60
+ throw new Error(
61
+ `renderCallableFile: entity "${entity.name}" has no source.rdb with @kind: "storedProc" or "tableFunction"`,
62
+ );
63
+ }
64
+
65
+ const procName = source.physicalName;
66
+ const argsRef = source.ownAttr(SOURCE_ATTR_PARAMETER_REF);
67
+ const argsObjectName = typeof argsRef === "string" && argsRef !== "" ? argsRef : undefined;
68
+
69
+ // Resolve the parameter value-object (when set) — same root as the entity.
70
+ const root = entity.root();
71
+ const argsObject =
72
+ argsObjectName !== undefined
73
+ ? (root.ownChildren().find(
74
+ (c) =>
75
+ c.subType === OBJECT_SUBTYPE_VALUE && c.name === argsObjectName,
76
+ ) as MetaObject | undefined)
77
+ : undefined;
78
+
79
+ // Build the parameter list for the SQL call site. Declaration order = arg
80
+ // order. Empty when argsObject is undefined (zero-arg proc).
81
+ const paramFieldNames: string[] = argsObject
82
+ ? argsObject.ownChildren()
83
+ .filter((c) => c.type === TYPE_FIELD)
84
+ .map((f) => f.name)
85
+ : [];
86
+
87
+ const sqlArgList = paramFieldNames.length === 0
88
+ ? ""
89
+ : paramFieldNames.map((n) => `\${args.${n}}`).join(", ");
90
+
91
+ const fnName = `call${entity.name}`;
92
+ const projectionType = entity.name;
93
+ const projectionSchemaName = `${entity.name}Schema`;
94
+
95
+ // Imports: entity (Zod schema + type) + drizzle sql + the args value-object
96
+ // when applicable.
97
+ const argsImport = argsObject
98
+ ? `import type { ${argsObjectName} } from "./${argsObjectName}.js";\n`
99
+ : "";
100
+
101
+ const signature = argsObject
102
+ ? `db: NodePgDatabase, args: ${argsObjectName}`
103
+ : `db: NodePgDatabase`;
104
+
105
+ return `// ${GENERATED_HEADER}
106
+ import { sql } from "drizzle-orm";
107
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
108
+ ${argsImport}import { ${projectionSchemaName}, type ${projectionType} } from "./${entity.name}.js";
109
+
110
+ /**
111
+ * FR-015: typed wrapper around the \`${procName}\` ${source.effectiveKind === SOURCE_KIND_STORED_PROC ? "stored procedure" : "table function"}.
112
+ * Drizzle passes a parameterised SELECT — args bind in declaration order from
113
+ * the @parameterRef value-object${argsObject ? `, here ${argsObjectName}` : ""}.
114
+ */
115
+ export async function ${fnName}(${signature}): Promise<${projectionType}[]> {
116
+ const r = await db.execute(
117
+ sql\`SELECT * FROM ${procName}(${sqlArgList})\`,
118
+ );
119
+ return r.rows.map((row) => ${projectionSchemaName}.parse(row as unknown));
120
+ }
121
+ `;
122
+ }