@metaobjectsdev/codegen-ts 0.6.0 → 0.7.0-rc.10

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 (119) hide show
  1. package/README.md +161 -4
  2. package/dist/column-mapper.d.ts +16 -0
  3. package/dist/column-mapper.d.ts.map +1 -1
  4. package/dist/column-mapper.js +73 -2
  5. package/dist/column-mapper.js.map +1 -1
  6. package/dist/generators/docs-file.d.ts +8 -0
  7. package/dist/generators/docs-file.d.ts.map +1 -0
  8. package/dist/generators/docs-file.js +52 -0
  9. package/dist/generators/docs-file.js.map +1 -0
  10. package/dist/generators/entity-file.d.ts +15 -0
  11. package/dist/generators/entity-file.d.ts.map +1 -1
  12. package/dist/generators/entity-file.js +2 -1
  13. package/dist/generators/entity-file.js.map +1 -1
  14. package/dist/generators/index.d.ts +4 -0
  15. package/dist/generators/index.d.ts.map +1 -1
  16. package/dist/generators/index.js +4 -0
  17. package/dist/generators/index.js.map +1 -1
  18. package/dist/generators/output-parser-file.d.ts +9 -0
  19. package/dist/generators/output-parser-file.d.ts.map +1 -0
  20. package/dist/generators/output-parser-file.js +37 -0
  21. package/dist/generators/output-parser-file.js.map +1 -0
  22. package/dist/generators/prompt-render-file.d.ts +9 -0
  23. package/dist/generators/prompt-render-file.d.ts.map +1 -0
  24. package/dist/generators/prompt-render-file.js +70 -0
  25. package/dist/generators/prompt-render-file.js.map +1 -0
  26. package/dist/generators/queries-file.d.ts +1 -1
  27. package/dist/generators/queries-file.d.ts.map +1 -1
  28. package/dist/generators/queries-file.js +11 -3
  29. package/dist/generators/queries-file.js.map +1 -1
  30. package/dist/generators/routes-file-hono.d.ts +21 -0
  31. package/dist/generators/routes-file-hono.d.ts.map +1 -0
  32. package/dist/generators/routes-file-hono.js +38 -0
  33. package/dist/generators/routes-file-hono.js.map +1 -0
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/metaobjects-config.d.ts +10 -1
  39. package/dist/metaobjects-config.d.ts.map +1 -1
  40. package/dist/metaobjects-config.js +2 -1
  41. package/dist/metaobjects-config.js.map +1 -1
  42. package/dist/naming.d.ts +3 -12
  43. package/dist/naming.d.ts.map +1 -1
  44. package/dist/naming.js +14 -44
  45. package/dist/naming.js.map +1 -1
  46. package/dist/payload-codegen.d.ts +8 -0
  47. package/dist/payload-codegen.d.ts.map +1 -1
  48. package/dist/payload-codegen.js +33 -3
  49. package/dist/payload-codegen.js.map +1 -1
  50. package/dist/projection/extract-view-spec.d.ts +1 -1
  51. package/dist/projection/extract-view-spec.js +1 -1
  52. package/dist/source-detect.d.ts +10 -0
  53. package/dist/source-detect.d.ts.map +1 -0
  54. package/dist/source-detect.js +30 -0
  55. package/dist/source-detect.js.map +1 -0
  56. package/dist/templates/docs-file.d.ts +48 -0
  57. package/dist/templates/docs-file.d.ts.map +1 -0
  58. package/dist/templates/docs-file.js +445 -0
  59. package/dist/templates/docs-file.js.map +1 -0
  60. package/dist/templates/drizzle-schema.js +27 -3
  61. package/dist/templates/drizzle-schema.js.map +1 -1
  62. package/dist/templates/entity-file.d.ts +15 -1
  63. package/dist/templates/entity-file.d.ts.map +1 -1
  64. package/dist/templates/entity-file.js +15 -5
  65. package/dist/templates/entity-file.js.map +1 -1
  66. package/dist/templates/inferred-types.d.ts +9 -0
  67. package/dist/templates/inferred-types.d.ts.map +1 -1
  68. package/dist/templates/inferred-types.js +88 -2
  69. package/dist/templates/inferred-types.js.map +1 -1
  70. package/dist/templates/output-parser.d.ts +8 -0
  71. package/dist/templates/output-parser.d.ts.map +1 -0
  72. package/dist/templates/output-parser.js +129 -0
  73. package/dist/templates/output-parser.js.map +1 -0
  74. package/dist/templates/projection-decl.d.ts +1 -1
  75. package/dist/templates/projection-decl.js +1 -1
  76. package/dist/templates/queries-file.d.ts.map +1 -1
  77. package/dist/templates/queries-file.js +15 -4
  78. package/dist/templates/queries-file.js.map +1 -1
  79. package/dist/templates/queries.d.ts.map +1 -1
  80. package/dist/templates/queries.js +11 -30
  81. package/dist/templates/queries.js.map +1 -1
  82. package/dist/templates/routes-file-hono.d.ts +4 -0
  83. package/dist/templates/routes-file-hono.d.ts.map +1 -0
  84. package/dist/templates/routes-file-hono.js +119 -0
  85. package/dist/templates/routes-file-hono.js.map +1 -0
  86. package/dist/templates/value-object-file.d.ts +3 -0
  87. package/dist/templates/value-object-file.d.ts.map +1 -0
  88. package/dist/templates/value-object-file.js +27 -0
  89. package/dist/templates/value-object-file.js.map +1 -0
  90. package/dist/templates/zod-validators.d.ts +10 -0
  91. package/dist/templates/zod-validators.d.ts.map +1 -1
  92. package/dist/templates/zod-validators.js +108 -30
  93. package/dist/templates/zod-validators.js.map +1 -1
  94. package/package.json +5 -4
  95. package/src/column-mapper.ts +86 -1
  96. package/src/generators/docs-file.ts +64 -0
  97. package/src/generators/entity-file.ts +17 -1
  98. package/src/generators/index.ts +4 -0
  99. package/src/generators/output-parser-file.ts +50 -0
  100. package/src/generators/prompt-render-file.ts +95 -0
  101. package/src/generators/queries-file.ts +13 -4
  102. package/src/generators/routes-file-hono.ts +48 -0
  103. package/src/index.ts +2 -2
  104. package/src/metaobjects-config.ts +11 -2
  105. package/src/naming.ts +22 -46
  106. package/src/payload-codegen.ts +34 -2
  107. package/src/projection/extract-view-spec.ts +1 -1
  108. package/src/source-detect.ts +28 -0
  109. package/src/templates/docs-file.ts +545 -0
  110. package/src/templates/drizzle-schema.ts +27 -3
  111. package/src/templates/entity-file.ts +36 -5
  112. package/src/templates/inferred-types.ts +117 -3
  113. package/src/templates/output-parser.ts +143 -0
  114. package/src/templates/projection-decl.ts +1 -1
  115. package/src/templates/queries-file.ts +18 -4
  116. package/src/templates/queries.ts +11 -33
  117. package/src/templates/routes-file-hono.ts +142 -0
  118. package/src/templates/value-object-file.ts +30 -0
  119. package/src/templates/zod-validators.ts +121 -35
@@ -1,9 +1,34 @@
1
1
  // Inferred types template — emits Drizzle's InferSelectModel / InferInsertModel type aliases,
2
2
  // plus named union types for field.enum fields.
3
+ //
4
+ // Also emits the structural TS interface for value-only objects (metaobjects
5
+ // with no writable source.rdb). That path side-steps Drizzle entirely: the
6
+ // interface is computed directly from the field tree, with `name?: T` for
7
+ // optional fields (matching Zod's `.optional()` inference — `T | undefined` —
8
+ // without the superfluous `| null` that Drizzle nullable columns introduce).
3
9
 
4
- import { code, imp, type Code } from "ts-poet";
5
- import type { MetaObject } from "@metaobjectsdev/metadata";
6
- import { FIELD_SUBTYPE_ENUM } from "@metaobjectsdev/metadata";
10
+ import { code, imp, joinCode, type Code } from "ts-poet";
11
+ import type { MetaObject, MetaField } from "@metaobjectsdev/metadata";
12
+ import {
13
+ FIELD_SUBTYPE_ENUM,
14
+ FIELD_SUBTYPE_OBJECT,
15
+ FIELD_SUBTYPE_STRING,
16
+ FIELD_SUBTYPE_INT,
17
+ FIELD_SUBTYPE_SHORT,
18
+ FIELD_SUBTYPE_BYTE,
19
+ FIELD_SUBTYPE_LONG,
20
+ FIELD_SUBTYPE_DOUBLE,
21
+ FIELD_SUBTYPE_FLOAT,
22
+ FIELD_SUBTYPE_DECIMAL,
23
+ FIELD_SUBTYPE_CURRENCY,
24
+ FIELD_SUBTYPE_BOOLEAN,
25
+ FIELD_SUBTYPE_DATE,
26
+ FIELD_SUBTYPE_TIME,
27
+ FIELD_SUBTYPE_TIMESTAMP,
28
+ FIELD_SUBTYPE_CLASS,
29
+ FIELD_ATTR_REQUIRED,
30
+ FIELD_ATTR_OBJECT_REF,
31
+ } from "@metaobjectsdev/metadata";
7
32
  import { variableNameFromEntity, toPascalCase } from "../naming.js";
8
33
  import { enumValues } from "../enum-meta.js";
9
34
  import { renderDocsFor } from "./jsdoc.js";
@@ -53,3 +78,92 @@ export function renderEnumTypeAliases(entity: MetaObject): Code | null {
53
78
 
54
79
  return lines.length > 0 ? code`${lines.join("\n")}` : null;
55
80
  }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Value-object interface emitter
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const SCALAR_TS_BY_SUBTYPE: Record<string, string> = {
87
+ [FIELD_SUBTYPE_STRING]: "string",
88
+ [FIELD_SUBTYPE_CLASS]: "string",
89
+ [FIELD_SUBTYPE_INT]: "number",
90
+ [FIELD_SUBTYPE_SHORT]: "number",
91
+ [FIELD_SUBTYPE_BYTE]: "number",
92
+ [FIELD_SUBTYPE_LONG]: "number",
93
+ [FIELD_SUBTYPE_DOUBLE]: "number",
94
+ [FIELD_SUBTYPE_FLOAT]: "number",
95
+ [FIELD_SUBTYPE_DECIMAL]: "number",
96
+ [FIELD_SUBTYPE_CURRENCY]: "number",
97
+ [FIELD_SUBTYPE_BOOLEAN]: "boolean",
98
+ [FIELD_SUBTYPE_DATE]: "string",
99
+ [FIELD_SUBTYPE_TIME]: "string",
100
+ [FIELD_SUBTYPE_TIMESTAMP]: "string",
101
+ };
102
+
103
+ /** Type-alias name for a field.enum, mirroring renderEnumTypeAliases. */
104
+ function enumTypeAliasName(entity: MetaObject, field: MetaField): string {
105
+ const superField = field.resolveSuper();
106
+ return superField !== undefined
107
+ ? toPascalCase(superField.name)
108
+ : `${entity.name}${toPascalCase(field.name)}`;
109
+ }
110
+
111
+ /**
112
+ * One-line TS type expression for a field on a value-only object.
113
+ * Returns a `Code` so cross-module `field.object` refs can be hoisted via
114
+ * ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
115
+ */
116
+ function valueObjectFieldType(entity: MetaObject, field: MetaField): Code {
117
+ // field.object: import the referenced TS interface from its sibling module
118
+ // so ts-poet hoists the import. Mirrors zod-validators.ts's `<Ref>InsertSchema`
119
+ // import strategy, just for the type alias instead of the schema constant.
120
+ if (field.subType === FIELD_SUBTYPE_OBJECT) {
121
+ const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
122
+ if (typeof ref === "string" && ref.length > 0) {
123
+ const refImp = imp(`${ref}@./${ref}.js`);
124
+ return field.isArray ? code`${refImp}[]` : code`${refImp}`;
125
+ }
126
+ return field.isArray ? code`unknown[]` : code`unknown`;
127
+ }
128
+
129
+ // field.enum: use the same type-alias name as renderEnumTypeAliases emits.
130
+ if (field.subType === FIELD_SUBTYPE_ENUM) {
131
+ const values = enumValues(field);
132
+ if (values !== undefined) {
133
+ const alias = enumTypeAliasName(entity, field);
134
+ return field.isArray ? code`${alias}[]` : code`${alias}`;
135
+ }
136
+ return field.isArray ? code`string[]` : code`string`;
137
+ }
138
+
139
+ const scalar = SCALAR_TS_BY_SUBTYPE[field.subType] ?? "unknown";
140
+ return field.isArray ? code`${scalar}[]` : code`${scalar}`;
141
+ }
142
+
143
+ /**
144
+ * Emit a structural `interface <Name> { ... }` for a value-only object.
145
+ *
146
+ * Optional fields use `name?: T` (matching the Zod `.optional()` inference
147
+ * `T | undefined`) instead of `name?: T | null`. Value objects never round-
148
+ * trip through Drizzle nullable columns, so the null-bridge is unnecessary
149
+ * here — and forces consumers into a residual cast at the call site.
150
+ */
151
+ export function renderValueObjectInterface(entity: MetaObject): Code {
152
+ const docs = renderDocsFor(entity);
153
+ const docsPrefix = docs ? `${docs}\n` : "";
154
+
155
+ const lines: Code[] = [];
156
+ for (const field of entity.fields()) {
157
+ const required = field.ownAttr(FIELD_ATTR_REQUIRED) === true;
158
+ const optional = required ? "" : "?";
159
+ const tsType = valueObjectFieldType(entity, field);
160
+ lines.push(code` ${field.name}${optional}: ${tsType};`);
161
+ }
162
+
163
+ // joinCode with "\n" interpolates each Code segment on its own line and
164
+ // keeps the imp() registrations intact so ts-poet hoists the imports.
165
+ return code`${docsPrefix}export interface ${entity.name} {
166
+ ${joinCode(lines, { on: "\n" })}
167
+ }
168
+ `;
169
+ }
@@ -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
  }
@@ -0,0 +1,142 @@
1
+ // Hono route template — emits a per-entity routes file that delegates
2
+ // CRUD verbs to helpers from @metaobjectsdev/runtime-ts/hono.
3
+ //
4
+ // Hono parallel of templates/routes-file.ts. Two emit-shape differences
5
+ // vs the Fastify flavor, both reflecting Hono idioms rather than contract
6
+ // drift:
7
+ //
8
+ // 1) Exported function shape — `registerXxxRoutes(app, deps)` instead of
9
+ // `xxxRoutes(fastify)`. Workers/Bun consumers typically pass `db`
10
+ // through a per-request deps object (so a Worker can attach the
11
+ // D1 client pulled off bindings) rather than reaching for a
12
+ // module-level singleton. The Fastify flavor uses `import { db }`
13
+ // because Fastify apps are long-lived Node processes where a
14
+ // singleton is the norm. Both flavors talk to mountCrudRoutes /
15
+ // mountReadOnlyCrudRoutes with identical wire behavior.
16
+ //
17
+ // 2) apiPrefix is composed into the resource path (`${apiPrefix}${path}`)
18
+ // rather than wrapping the registration. Hono has no fastify.register
19
+ // / prefix-wrapping primitive; sub-apps via `app.route(prefix, sub)`
20
+ // exist but bloat the generated code. String concat is simpler and
21
+ // produces identical URL grammar.
22
+ //
23
+ // Dispatch logic mirrors Fastify:
24
+ // isProjection(entity) → mountReadOnlyCrudRoutes (GET list + GET :id)
25
+ // vanilla / write-through entity → mountCrudRoutes (all 5 CRUD verbs)
26
+
27
+ import { code, imp } from "ts-poet";
28
+ import type { MetaObject } from "@metaobjectsdev/metadata";
29
+ import { type RenderContext } from "../render-context.js";
30
+ import { entityModuleSpecifier } from "../import-path.js";
31
+ import { GENERATED_HEADER } from "../constants.js";
32
+ import { variableNameFromEntity } from "../naming.js";
33
+ import { isProjection } from "../projection/projection-detector.js";
34
+
35
+ export function renderRoutesFileHono(entity: MetaObject, ctx: RenderContext): string {
36
+ const entityName = entity.name;
37
+ const handlerName = `register${entityName}Routes`;
38
+
39
+ const entityFileSpec = entityModuleSpecifier(
40
+ ctx.selfTarget,
41
+ ctx.entityModuleTarget,
42
+ entity.package,
43
+ entityName,
44
+ ctx.extStyle,
45
+ );
46
+
47
+ const header =
48
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
49
+ `// Source metadata: ${entityName} (${entity.fqn()})\n` +
50
+ `// Customize via ${entityName}.extra.ts in this directory (e.g., auth, additional handlers).\n`;
51
+
52
+ // Path composition: apiPrefix is a literal string in the URL.
53
+ const pathExpr = ctx.apiPrefix
54
+ ? `\`${ctx.apiPrefix}\${${entityName}.$path}\``
55
+ : `${entityName}.$path`;
56
+
57
+ // --- Projection path: read-only routes (GET list + GET :id) ---
58
+ if (isProjection(entity)) {
59
+ const camelName = entityName.charAt(0).toLowerCase() + entityName.slice(1);
60
+ const HonoSym = imp("t:Hono@hono");
61
+ const mountReadOnlyCrudRoutesSym = imp(
62
+ "mountReadOnlyCrudRoutes@@metaobjectsdev/runtime-ts/hono",
63
+ );
64
+
65
+ const literalImports = code`
66
+ import {
67
+ ${entityName},
68
+ ${camelName}View,
69
+ ${entityName}FilterAllowlist,
70
+ ${entityName}SortAllowlist,
71
+ } from ${JSON.stringify(entityFileSpec)};
72
+ `;
73
+
74
+ const body = code`
75
+ /**
76
+ * Mount read-only REST endpoints for ${entityName} (projection — view-backed, no writes).
77
+ *
78
+ * Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
79
+ * Customize: register this as-is, or import individual route helpers from
80
+ * @metaobjectsdev/runtime-ts/hono.
81
+ */
82
+ // biome-ignore lint/suspicious/noExplicitAny: consumer-defined Hono bindings/variables
83
+ export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unknown }): void {
84
+ ${mountReadOnlyCrudRoutesSym}({
85
+ app,
86
+ path: ${pathExpr},
87
+ db: deps.db,
88
+ view: ${camelName}View,
89
+ filterAllowlist: ${entityName}FilterAllowlist,
90
+ sortAllowlist: ${entityName}SortAllowlist,
91
+ dialect: ${JSON.stringify(ctx.dialect)},
92
+ });
93
+ }
94
+ `;
95
+
96
+ return header + literalImports.toString() + body.toString();
97
+ }
98
+
99
+ // --- Vanilla / write-through entity path: full CRUD routes ---
100
+ const tableVar = variableNameFromEntity(entityName);
101
+
102
+ const HonoSym = imp("t:Hono@hono");
103
+ const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/hono");
104
+
105
+ const literalImports = code`
106
+ import {
107
+ ${entityName},
108
+ ${tableVar},
109
+ ${entityName}InsertSchema,
110
+ ${entityName}UpdateSchema,
111
+ ${entityName}FilterAllowlist,
112
+ ${entityName}SortAllowlist,
113
+ } from ${JSON.stringify(entityFileSpec)};
114
+ `;
115
+
116
+ const body = code`
117
+ /**
118
+ * Mount the 5 standard REST endpoints for ${entityName} using Drizzle directly.
119
+ *
120
+ * Customize: register this as-is for stock CRUD, OR import the per-verb
121
+ * helpers (mountListRoute, mountGetRoute, ...) from
122
+ * @metaobjectsdev/runtime-ts/hono and mix with your own handlers
123
+ * (auth, side effects, etc.).
124
+ */
125
+ // biome-ignore lint/suspicious/noExplicitAny: consumer-defined Hono bindings/variables
126
+ export function ${handlerName}(app: ${HonoSym}<any, any, any>, deps: { db: unknown }): void {
127
+ ${mountCrudRoutesSym}({
128
+ app,
129
+ path: ${pathExpr},
130
+ db: deps.db,
131
+ table: ${tableVar},
132
+ insertSchema: ${entityName}InsertSchema,
133
+ updateSchema: ${entityName}UpdateSchema,
134
+ filterAllowlist: ${entityName}FilterAllowlist,
135
+ sortAllowlist: ${entityName}SortAllowlist,
136
+ dialect: ${JSON.stringify(ctx.dialect)},
137
+ });
138
+ }
139
+ `;
140
+
141
+ return header + literalImports.toString() + body.toString();
142
+ }
@@ -0,0 +1,30 @@
1
+ // Value-object file composer — emits a streamlined "value-only" module for
2
+ // metaobjects with no writable source.rdb child. Output is just the TS
3
+ // interface + the Zod schema (and enum type aliases when applicable); no
4
+ // Drizzle table, no Infer*Model aliases, no filter allowlists, no constants
5
+ // object.
6
+ //
7
+ // Dispatch: entity-file.ts routes here when hasWritableRdbSource(entity) is
8
+ // false. The entity may still have read-only source.* children (those are
9
+ // handled by the projection path before this one is reached).
10
+
11
+ import { joinCode, type Code } from "ts-poet";
12
+ import type { MetaObject } from "@metaobjectsdev/metadata";
13
+ import { renderValueObjectInterface, renderEnumTypeAliases } from "./inferred-types.js";
14
+ import { renderInsertSchemaOnly } from "./zod-validators.js";
15
+ import { GENERATED_HEADER } from "../constants.js";
16
+
17
+ export function renderValueObjectFile(obj: MetaObject): string {
18
+ const enumAliases = renderEnumTypeAliases(obj);
19
+ const sections: Code[] = [
20
+ renderValueObjectInterface(obj),
21
+ ...(enumAliases !== null ? [enumAliases] : []),
22
+ renderInsertSchemaOnly(obj),
23
+ ];
24
+ const body = joinCode(sections, { on: "\n" }).toString();
25
+ const header =
26
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
27
+ `// Source metadata: ${obj.name} (${obj.fqn()})\n` +
28
+ `// Customize via ${obj.name}.extra.ts in this directory.\n`;
29
+ return header + body;
30
+ }