@metaobjectsdev/codegen-ts 0.6.0-rc.1 → 0.7.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 (50) hide show
  1. package/README.md +131 -4
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +2 -1
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/generators/index.d.ts +2 -0
  6. package/dist/generators/index.d.ts.map +1 -1
  7. package/dist/generators/index.js +2 -0
  8. package/dist/generators/index.js.map +1 -1
  9. package/dist/generators/output-parser-file.d.ts +9 -0
  10. package/dist/generators/output-parser-file.d.ts.map +1 -0
  11. package/dist/generators/output-parser-file.js +37 -0
  12. package/dist/generators/output-parser-file.js.map +1 -0
  13. package/dist/generators/prompt-render-file.d.ts +9 -0
  14. package/dist/generators/prompt-render-file.d.ts.map +1 -0
  15. package/dist/generators/prompt-render-file.js +52 -0
  16. package/dist/generators/prompt-render-file.js.map +1 -0
  17. package/dist/metaobjects-config.d.ts +3 -1
  18. package/dist/metaobjects-config.d.ts.map +1 -1
  19. package/dist/metaobjects-config.js +2 -1
  20. package/dist/metaobjects-config.js.map +1 -1
  21. package/dist/naming.d.ts +3 -12
  22. package/dist/naming.d.ts.map +1 -1
  23. package/dist/naming.js +14 -44
  24. package/dist/naming.js.map +1 -1
  25. package/dist/projection/extract-view-spec.d.ts +1 -1
  26. package/dist/projection/extract-view-spec.js +1 -1
  27. package/dist/templates/output-parser.d.ts +8 -0
  28. package/dist/templates/output-parser.d.ts.map +1 -0
  29. package/dist/templates/output-parser.js +129 -0
  30. package/dist/templates/output-parser.js.map +1 -0
  31. package/dist/templates/projection-decl.d.ts +1 -1
  32. package/dist/templates/projection-decl.js +1 -1
  33. package/dist/templates/queries-file.d.ts.map +1 -1
  34. package/dist/templates/queries-file.js +15 -4
  35. package/dist/templates/queries-file.js.map +1 -1
  36. package/dist/templates/queries.d.ts.map +1 -1
  37. package/dist/templates/queries.js +11 -30
  38. package/dist/templates/queries.js.map +1 -1
  39. package/package.json +4 -4
  40. package/src/column-mapper.ts +2 -1
  41. package/src/generators/index.ts +2 -0
  42. package/src/generators/output-parser-file.ts +50 -0
  43. package/src/generators/prompt-render-file.ts +69 -0
  44. package/src/metaobjects-config.ts +4 -2
  45. package/src/naming.ts +22 -46
  46. package/src/projection/extract-view-spec.ts +1 -1
  47. package/src/templates/output-parser.ts +143 -0
  48. package/src/templates/projection-decl.ts +1 -1
  49. package/src/templates/queries-file.ts +18 -4
  50. package/src/templates/queries.ts +11 -33
@@ -0,0 +1,143 @@
1
+ // server/typescript/packages/codegen-ts/src/templates/output-parser.ts
2
+ //
3
+ // Per-template renderer for template.output codegen. Walks the @payloadRef's
4
+ // value-object into a Zod schema and emits a dual-API parser (parse + safeParse)
5
+ // alongside the schema. The emitted file is self-contained: it derives a
6
+ // local data type via `z.infer<typeof Schema>` and exports it as
7
+ // `<TemplateName>Data`. Consumers wiring `promptRender()` get a structurally
8
+ // identical payload-VO interface in `prompts.ts`; either type can be used
9
+ // interchangeably with parse results.
10
+
11
+ import {
12
+ type MetaData,
13
+ TYPE_OBJECT,
14
+ TYPE_FIELD,
15
+ TYPE_TEMPLATE,
16
+ TEMPLATE_SUBTYPE_OUTPUT,
17
+ FIELD_SUBTYPE_OBJECT,
18
+ FIELD_ATTR_OBJECT_REF,
19
+ TEMPLATE_ATTR_PAYLOAD_REF,
20
+ } from "@metaobjectsdev/metadata";
21
+
22
+ const SCALAR_ZOD: Record<string, string> = {
23
+ string: "z.string()",
24
+ class: "z.string()",
25
+ int: "z.number().int()",
26
+ short: "z.number().int()",
27
+ byte: "z.number().int()",
28
+ long: "z.number().int()",
29
+ double: "z.number()",
30
+ float: "z.number()",
31
+ boolean: "z.boolean()",
32
+ };
33
+
34
+ function findObject(root: MetaData, name: string): MetaData | undefined {
35
+ return root.ownChildren().find((c) => c.type === TYPE_OBJECT && c.name === name);
36
+ }
37
+
38
+ function findTemplate(root: MetaData, name: string): MetaData | undefined {
39
+ return root.ownChildren().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
40
+ }
41
+
42
+ /** Render the Zod expression for a single field; recurses on @objectRef. */
43
+ function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
44
+ // isArray is a native (reserved) property on MetaData, not an attr.
45
+ const isArray = field.isArray === true;
46
+ let base: string;
47
+ if (field.subType === FIELD_SUBTYPE_OBJECT) {
48
+ const refName = field.ownAttr(FIELD_ATTR_OBJECT_REF);
49
+ if (typeof refName !== "string") {
50
+ base = "z.unknown()";
51
+ } else if (seen.has(refName)) {
52
+ // Cycle guard — emit unknown for self-references (rare; lazy schemas not in scope for v1).
53
+ base = "z.unknown()";
54
+ } else {
55
+ const inner = findObject(root, refName);
56
+ base = inner ? renderObjectSchema(inner, root, new Set(seen).add(refName), depth + 1) : "z.unknown()";
57
+ }
58
+ } else {
59
+ base = SCALAR_ZOD[field.subType] ?? "z.unknown()";
60
+ }
61
+ return isArray ? `z.array(${base})` : base;
62
+ }
63
+
64
+ /** Render a `z.object({ ... })` for an object.value node.
65
+ * At depth 0 the schema starts at column 0 (consumer's `const Foo = z.object({`),
66
+ * so fields sit at 2 spaces and the closing `})` at 0 spaces — matching the
67
+ * surrounding `const NameSchema = ...` statement's indent. Nested schemas
68
+ * step in two spaces per depth level. */
69
+ function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
70
+ const fields = vo.children().filter((c) => c.type === TYPE_FIELD);
71
+ const fieldIndent = " ".repeat(depth + 1);
72
+ const closeIndent = " ".repeat(depth);
73
+ const lines = fields.map((f) => `${fieldIndent}${f.name}: ${fieldZod(f, root, seen, depth)},`);
74
+ return `z.object({\n${lines.join("\n")}\n${closeIndent}})`;
75
+ }
76
+
77
+ /**
78
+ * Render the full output-parser file for one `template.output` node.
79
+ * Throws if the template isn't found, isn't a template.output, or its
80
+ * @payloadRef doesn't resolve to an object.value.
81
+ */
82
+ export function renderOutputParser(root: MetaData, templateName: string): string {
83
+ const tmpl = findTemplate(root, templateName);
84
+ if (!tmpl) {
85
+ throw new Error(`template "${templateName}" not found in metadata root`);
86
+ }
87
+ if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
88
+ throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
89
+ }
90
+ const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
91
+ if (typeof payloadRef !== "string") {
92
+ throw new Error(`template "${templateName}" missing @payloadRef`);
93
+ }
94
+ const vo = findObject(root, payloadRef);
95
+ if (!vo) {
96
+ throw new Error(`template "${templateName}" @payloadRef "${payloadRef}" not found in metadata root`);
97
+ }
98
+
99
+ const schema = renderObjectSchema(vo, root, new Set([payloadRef]), 0);
100
+ const schemaName = `${templateName}Schema`;
101
+ const dataName = `${templateName}Data`;
102
+ const errorName = `${templateName}ValidationError`;
103
+ const parseName = `parse${templateName}`;
104
+ const safeParseName = `safeParse${templateName}`;
105
+
106
+ return `import { z } from "zod";
107
+
108
+ const ${schemaName} = ${schema};
109
+
110
+ export type ${dataName} = z.infer<typeof ${schemaName}>;
111
+ export type ${errorName} = z.ZodError;
112
+
113
+ /**
114
+ * Parse an LLM response into a typed ${dataName}.
115
+ * @throws ZodError on validation failure.
116
+ */
117
+ export function ${parseName}(text: string): ${dataName} {
118
+ return ${schemaName}.parse(JSON.parse(text));
119
+ }
120
+
121
+ /**
122
+ * Parse an LLM response with explicit error handling (Result-style).
123
+ * Does not throw on validation failure.
124
+ */
125
+ export function ${safeParseName}(
126
+ text: string,
127
+ ): { success: true; data: ${dataName} } | { success: false; error: ${errorName} } {
128
+ let parsed: unknown;
129
+ try {
130
+ parsed = JSON.parse(text);
131
+ } catch (err) {
132
+ return {
133
+ success: false,
134
+ error: new z.ZodError([{ code: "custom", path: [], message: \`invalid JSON: \${(err as Error).message}\` }]),
135
+ };
136
+ }
137
+ const result = ${schemaName}.safeParse(parsed);
138
+ return result.success
139
+ ? { success: true, data: result.data }
140
+ : { success: false, error: result.error };
141
+ }
142
+ `;
143
+ }
@@ -56,7 +56,7 @@ function pathFromProjectionName(name: string): string {
56
56
  *
57
57
  * @param projection The projection entity (has a source[dbView] child).
58
58
  * @param root The loader's root (all top-level objects as direct children,
59
- * from `MetaDataLoader.load()` / `FileMetaDataLoader.loadFiles()` as `result.root`).
59
+ * from `MetaDataLoader.load()` or `MetaDataLoader.fromDirectory()` as `result.root`).
60
60
  * @param opts Column naming strategy + dialect.
61
61
  */
62
62
  export function renderProjectionDecl(
@@ -4,7 +4,7 @@
4
4
  import { code, joinCode, type Code } from "ts-poet";
5
5
  import { MetaObject } from "@metaobjectsdev/metadata";
6
6
  import { type RenderContext } from "../render-context.js";
7
- import { entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
7
+ import { entityModuleSpecifier } from "../import-path.js";
8
8
  import {
9
9
  renderFindByIdFn,
10
10
  renderListFn,
@@ -26,13 +26,27 @@ export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
26
26
  entityName,
27
27
  ctx.extStyle,
28
28
  );
29
- const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, obj.package, ctx.dbImport);
30
29
  const varName = variableNameFromEntity(entityName);
31
30
 
32
- // Literal imports (db + entity types) live in a code block so they sort
31
+ // The persistence-context `db` is parameter-passed into every generated CRUD
32
+ // helper (ADR-0008). Emit the dialect-correct Drizzle type alias so the
33
+ // signatures `findXxx(db: Db, ...)` typecheck without the consumer importing
34
+ // anything to construct one. Consumers pass any compatible Drizzle instance.
35
+ const dbTypeImport =
36
+ ctx.dialect === "postgres"
37
+ ? `import type { NodePgDatabase } from "drizzle-orm/node-postgres";`
38
+ : `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
39
+ const dbTypeAlias =
40
+ ctx.dialect === "postgres"
41
+ ? `type Db = NodePgDatabase<Record<string, never>>;`
42
+ : `type Db = BaseSQLiteDatabase<"async", Record<string, never>>;`;
43
+
44
+ // Literal imports (Db type + entity types) live in a code block so they sort
33
45
  // alongside ts-poet's hoisted imp() imports at the top of the body.
34
46
  const literalImports = code`
35
- import { db } from ${JSON.stringify(dbImportSpec)};
47
+ ${dbTypeImport}
48
+ ${dbTypeAlias}
49
+
36
50
  import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON.stringify(entityFileName)};
37
51
  `;
38
52
 
@@ -5,12 +5,7 @@ import { code, imp, type Code } from "ts-poet";
5
5
  import type { MetaObject } from "@metaobjectsdev/metadata";
6
6
  import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
7
7
  import type { RenderContext } from "../render-context.js";
8
- import { variableNameFromEntity, toSnakeCase, pluralize } from "../naming.js";
9
-
10
- /** Derive a stable prepared statement name. Deterministic from entity + field names. */
11
- function prepareName(prefix: string, entitySnakeName: string, fieldDbName: string): string {
12
- return `${prefix}_${entitySnakeName}_by_${fieldDbName}`;
13
- }
8
+ import { variableNameFromEntity, pluralize } from "../naming.js";
14
9
 
15
10
  /** Get the PK field name and its TS type for a given entity. */
16
11
  function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
@@ -34,31 +29,13 @@ export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
34
29
  const varName = variableNameFromEntity(entity.name);
35
30
  const entityName = entity.name;
36
31
  const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
37
- const entitySnakeName = toSnakeCase(entityName);
38
32
  const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
39
- const pkSnakeName = toSnakeCase(pkField);
40
- const prepName = prepareName("find", entitySnakeName, pkSnakeName);
41
33
  const fnName = `find${entityName}ById`;
42
- const prepVarName = `${fnName}Prepared`;
43
34
  const eqSym = imp("eq@drizzle-orm");
44
- const sqlSym = imp("sql@drizzle-orm");
45
-
46
- const baseVarName = `${fnName}Base`;
47
-
48
- // Drizzle's `.prepare()` signature differs by dialect:
49
- // - Postgres: prepare(name) — the name is used by the pg driver to cache the plan
50
- // - SQLite: prepare() — no name; the name arg was removed in drizzle-orm 0.41+
51
- const prepArg = ctx.dialect === "postgres" ? `"${prepName}"` : "";
52
35
 
53
36
  return code`
54
- const ${baseVarName} = db
55
- .select()
56
- .from(${varName})
57
- .where(${eqSym}(${varName}.${pkField}, ${sqlSym}.placeholder(${JSON.stringify(pkField)})));
58
- const ${prepVarName} = ${baseVarName}.prepare(${prepArg});
59
-
60
- export async function ${fnName}(${pkField}: ${pkType}): Promise<${entityName} | null> {
61
- const [${singularVar}] = await ${prepVarName}.execute({ ${pkField} });
37
+ export async function ${fnName}(db: Db, ${pkField}: ${pkType}): Promise<${entityName} | null> {
38
+ const [${singularVar}] = await db.select().from(${varName}).where(${eqSym}(${varName}.${pkField}, ${pkField})).limit(1);
62
39
  return ${singularVar} ?? null;
63
40
  }
64
41
  `;
@@ -72,7 +49,7 @@ export function renderListFn(entity: MetaObject, _ctx: RenderContext): Code {
72
49
  const fnName = `list${pluralize(entityName)}`;
73
50
 
74
51
  return code`
75
- export async function ${fnName}(opts?: { limit?: number; offset?: number }): Promise<${entityName}[]> {
52
+ export async function ${fnName}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${entityName}[]> {
76
53
  let q = db.select().from(${varName}).$dynamic();
77
54
  if (opts?.limit !== undefined) q = q.limit(opts.limit);
78
55
  if (opts?.offset !== undefined) q = q.offset(opts.offset);
@@ -89,7 +66,7 @@ export function renderCreateFn(entity: MetaObject, _ctx: RenderContext): Code {
89
66
  const schemaName = `${entityName}InsertSchema`;
90
67
 
91
68
  return code`
92
- export async function ${fnName}(data: unknown): Promise<${entityName}> {
69
+ export async function ${fnName}(db: Db, data: unknown): Promise<${entityName}> {
93
70
  const validated = ${schemaName}.parse(data);
94
71
  const [${singularVar}] = await db.insert(${varName}).values(validated).returning();
95
72
  return ${singularVar}!;
@@ -107,7 +84,7 @@ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
107
84
  const eqSym = imp("eq@drizzle-orm");
108
85
 
109
86
  return code`
110
- export async function ${fnName}(${pkField}: ${pkType}, data: unknown): Promise<${entityName} | null> {
87
+ export async function ${fnName}(db: Db, ${pkField}: ${pkType}, data: unknown): Promise<${entityName} | null> {
111
88
  const validated = ${schemaName}.partial().parse(data);
112
89
  const [${singularVar}] = await db.update(${varName}).set(validated).where(${eqSym}(${varName}.${pkField}, ${pkField})).returning();
113
90
  return ${singularVar} ?? null;
@@ -123,10 +100,11 @@ export function renderDeleteByIdFn(entity: MetaObject, ctx: RenderContext): Code
123
100
  const eqSym = imp("eq@drizzle-orm");
124
101
 
125
102
  return code`
126
- export async function ${fnName}(${pkField}: ${pkType}): Promise<boolean> {
127
- const result = await db.delete(${varName}).where(${eqSym}(${varName}.${pkField}, ${pkField}));
128
- // SQLite (libsql/Turso) returns { rowsAffected }; postgres returns array from .returning()
129
- return ('rowsAffected' in result ? result.rowsAffected : (result as unknown as unknown[]).length) > 0;
103
+ export async function ${fnName}(db: Db, ${pkField}: ${pkType}): Promise<boolean> {
104
+ // Use .returning() unconditionally — supported on SQLite ≥3.35 (covers D1, libsql/Turso)
105
+ // and Postgres. Result is an array of deleted rows; presence implies success.
106
+ const deleted = await db.delete(${varName}).where(${eqSym}(${varName}.${pkField}, ${pkField})).returning();
107
+ return deleted.length > 0;
130
108
  }
131
109
  `;
132
110
  }