@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.
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +123 -46
- package/dist/column-mapper.js.map +1 -1
- package/dist/generators/callable-file.d.ts +8 -0
- package/dist/generators/callable-file.d.ts.map +1 -0
- package/dist/generators/callable-file.js +32 -0
- package/dist/generators/callable-file.js.map +1 -0
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +5 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/extractor-file.d.ts +9 -0
- package/dist/generators/extractor-file.d.ts.map +1 -0
- package/dist/generators/extractor-file.js +45 -0
- package/dist/generators/extractor-file.js.map +1 -0
- package/dist/generators/index.d.ts +3 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/render-helper-file.d.ts +9 -0
- package/dist/generators/render-helper-file.d.ts.map +1 -0
- package/dist/generators/render-helper-file.js +58 -0
- package/dist/generators/render-helper-file.js.map +1 -0
- package/dist/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +42 -8
- 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 +11 -3
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/render-engine/framework-provider.d.ts +6 -5
- package/dist/render-engine/framework-provider.d.ts.map +1 -1
- package/dist/render-engine/framework-provider.js +53 -11
- package/dist/render-engine/framework-provider.js.map +1 -1
- package/dist/templates/callable-file.d.ts +8 -0
- package/dist/templates/callable-file.d.ts.map +1 -0
- package/dist/templates/callable-file.js +98 -0
- package/dist/templates/callable-file.js.map +1 -0
- package/dist/templates/extract-delegate-emitter.d.ts +42 -0
- package/dist/templates/extract-delegate-emitter.d.ts.map +1 -0
- package/dist/templates/extract-delegate-emitter.js +339 -0
- package/dist/templates/extract-delegate-emitter.js.map +1 -0
- package/dist/templates/{recover-schema-emitter.d.ts → extract-schema-emitter.d.ts} +2 -2
- package/dist/templates/extract-schema-emitter.d.ts.map +1 -0
- package/dist/templates/{recover-schema-emitter.js → extract-schema-emitter.js} +37 -20
- package/dist/templates/extract-schema-emitter.js.map +1 -0
- package/dist/templates/extractor.d.ts +9 -0
- package/dist/templates/extractor.d.ts.map +1 -0
- package/dist/templates/extractor.js +296 -0
- package/dist/templates/extractor.js.map +1 -0
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +2 -1
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/filter-type.d.ts.map +1 -1
- package/dist/templates/filter-type.js +8 -5
- package/dist/templates/filter-type.js.map +1 -1
- package/dist/templates/fr010-field-mapping.d.ts +22 -6
- package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
- package/dist/templates/fr010-field-mapping.js +66 -21
- package/dist/templates/fr010-field-mapping.js.map +1 -1
- package/dist/templates/inferred-types.d.ts +15 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +30 -17
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +98 -34
- package/dist/templates/output-parser.js.map +1 -1
- package/dist/templates/output-prompt.js +2 -2
- package/dist/templates/render-helper.d.ts +14 -0
- package/dist/templates/render-helper.d.ts.map +1 -0
- package/dist/templates/render-helper.js +180 -0
- package/dist/templates/render-helper.js.map +1 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +59 -3
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +10 -4
- package/src/column-mapper.ts +128 -45
- package/src/generators/callable-file.ts +44 -0
- package/src/generators/docs-data-builder.ts +5 -1
- package/src/generators/extractor-file.ts +57 -0
- package/src/generators/index.ts +3 -0
- package/src/generators/render-helper-file.ts +74 -0
- package/src/payload-codegen.ts +52 -7
- package/src/projection/extract-view-spec.ts +11 -3
- package/src/render-engine/framework-provider.ts +53 -16
- package/src/templates/callable-file.ts +122 -0
- package/src/templates/extract-delegate-emitter.ts +370 -0
- package/src/templates/{recover-schema-emitter.ts → extract-schema-emitter.ts} +39 -19
- package/src/templates/extractor.ts +333 -0
- package/src/templates/field-meta.ts +2 -0
- package/src/templates/filter-type.ts +7 -5
- package/src/templates/fr010-field-mapping.ts +71 -18
- package/src/templates/inferred-types.ts +32 -18
- package/src/templates/output-parser.ts +108 -35
- package/src/templates/output-prompt.ts +2 -2
- package/src/templates/render-helper.ts +244 -0
- package/src/templates/zod-validators.ts +51 -4
- package/dist/templates/recover-schema-emitter.d.ts.map +0 -1
- package/dist/templates/recover-schema-emitter.js.map +0 -1
package/src/payload-codegen.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
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?.
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
106
|
-
*
|
|
107
|
-
|
|
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
|
+
}
|