@metaobjectsdev/codegen-ts 0.5.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 (198) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +101 -0
  3. package/dist/column-mapper.d.ts +38 -0
  4. package/dist/column-mapper.d.ts.map +1 -0
  5. package/dist/column-mapper.js +205 -0
  6. package/dist/column-mapper.js.map +1 -0
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +8 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +7 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +11 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/format.d.ts +2 -0
  16. package/dist/format.d.ts.map +1 -0
  17. package/dist/format.js +47 -0
  18. package/dist/format.js.map +1 -0
  19. package/dist/generator.d.ts +44 -0
  20. package/dist/generator.d.ts.map +1 -0
  21. package/dist/generator.js +17 -0
  22. package/dist/generator.js.map +1 -0
  23. package/dist/generators/barrel.d.ts +6 -0
  24. package/dist/generators/barrel.d.ts.map +1 -0
  25. package/dist/generators/barrel.js +17 -0
  26. package/dist/generators/barrel.js.map +1 -0
  27. package/dist/generators/entity-file.d.ts +8 -0
  28. package/dist/generators/entity-file.d.ts.map +1 -0
  29. package/dist/generators/entity-file.js +27 -0
  30. package/dist/generators/entity-file.js.map +1 -0
  31. package/dist/generators/index.d.ts +5 -0
  32. package/dist/generators/index.d.ts.map +1 -0
  33. package/dist/generators/index.js +5 -0
  34. package/dist/generators/index.js.map +1 -0
  35. package/dist/generators/queries-file.d.ts +8 -0
  36. package/dist/generators/queries-file.d.ts.map +1 -0
  37. package/dist/generators/queries-file.js +26 -0
  38. package/dist/generators/queries-file.js.map +1 -0
  39. package/dist/generators/routes-file.d.ts +12 -0
  40. package/dist/generators/routes-file.d.ts.map +1 -0
  41. package/dist/generators/routes-file.js +30 -0
  42. package/dist/generators/routes-file.js.map +1 -0
  43. package/dist/import-path.d.ts +41 -0
  44. package/dist/import-path.d.ts.map +1 -0
  45. package/dist/import-path.js +95 -0
  46. package/dist/import-path.js.map +1 -0
  47. package/dist/index.d.ts +29 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/metaobjects-config.d.ts +56 -0
  52. package/dist/metaobjects-config.d.ts.map +1 -0
  53. package/dist/metaobjects-config.js +42 -0
  54. package/dist/metaobjects-config.js.map +1 -0
  55. package/dist/naming.d.ts +29 -0
  56. package/dist/naming.d.ts.map +1 -0
  57. package/dist/naming.js +67 -0
  58. package/dist/naming.js.map +1 -0
  59. package/dist/overwrite-policy.d.ts +8 -0
  60. package/dist/overwrite-policy.d.ts.map +1 -0
  61. package/dist/overwrite-policy.js +23 -0
  62. package/dist/overwrite-policy.js.map +1 -0
  63. package/dist/pk-resolver.d.ts +18 -0
  64. package/dist/pk-resolver.d.ts.map +1 -0
  65. package/dist/pk-resolver.js +36 -0
  66. package/dist/pk-resolver.js.map +1 -0
  67. package/dist/projection/extract-view-spec.d.ts +18 -0
  68. package/dist/projection/extract-view-spec.d.ts.map +1 -0
  69. package/dist/projection/extract-view-spec.js +272 -0
  70. package/dist/projection/extract-view-spec.js.map +1 -0
  71. package/dist/projection/index.d.ts +5 -0
  72. package/dist/projection/index.d.ts.map +1 -0
  73. package/dist/projection/index.js +5 -0
  74. package/dist/projection/index.js.map +1 -0
  75. package/dist/projection/projection-detector.d.ts +4 -0
  76. package/dist/projection/projection-detector.d.ts.map +1 -0
  77. package/dist/projection/projection-detector.js +13 -0
  78. package/dist/projection/projection-detector.js.map +1 -0
  79. package/dist/projection/view-ddl-emit.d.ts +10 -0
  80. package/dist/projection/view-ddl-emit.d.ts.map +1 -0
  81. package/dist/projection/view-ddl-emit.js +47 -0
  82. package/dist/projection/view-ddl-emit.js.map +1 -0
  83. package/dist/projection/view-spec.d.ts +56 -0
  84. package/dist/projection/view-spec.d.ts.map +1 -0
  85. package/dist/projection/view-spec.js +2 -0
  86. package/dist/projection/view-spec.js.map +1 -0
  87. package/dist/relation-resolver.d.ts +21 -0
  88. package/dist/relation-resolver.d.ts.map +1 -0
  89. package/dist/relation-resolver.js +62 -0
  90. package/dist/relation-resolver.js.map +1 -0
  91. package/dist/render-context.d.ts +65 -0
  92. package/dist/render-context.d.ts.map +1 -0
  93. package/dist/render-context.js +28 -0
  94. package/dist/render-context.js.map +1 -0
  95. package/dist/runner.d.ts +17 -0
  96. package/dist/runner.d.ts.map +1 -0
  97. package/dist/runner.js +135 -0
  98. package/dist/runner.js.map +1 -0
  99. package/dist/templates/barrel.d.ts +8 -0
  100. package/dist/templates/barrel.d.ts.map +1 -0
  101. package/dist/templates/barrel.js +12 -0
  102. package/dist/templates/barrel.js.map +1 -0
  103. package/dist/templates/drizzle-schema.d.ts +13 -0
  104. package/dist/templates/drizzle-schema.d.ts.map +1 -0
  105. package/dist/templates/drizzle-schema.js +251 -0
  106. package/dist/templates/drizzle-schema.js.map +1 -0
  107. package/dist/templates/entity-constants.d.ts +4 -0
  108. package/dist/templates/entity-constants.d.ts.map +1 -0
  109. package/dist/templates/entity-constants.js +215 -0
  110. package/dist/templates/entity-constants.js.map +1 -0
  111. package/dist/templates/entity-file.d.ts +4 -0
  112. package/dist/templates/entity-file.d.ts.map +1 -0
  113. package/dist/templates/entity-file.js +45 -0
  114. package/dist/templates/entity-file.js.map +1 -0
  115. package/dist/templates/field-meta.d.ts +24 -0
  116. package/dist/templates/field-meta.d.ts.map +1 -0
  117. package/dist/templates/field-meta.js +117 -0
  118. package/dist/templates/field-meta.js.map +1 -0
  119. package/dist/templates/filter-allowlist.d.ts +5 -0
  120. package/dist/templates/filter-allowlist.d.ts.map +1 -0
  121. package/dist/templates/filter-allowlist.js +86 -0
  122. package/dist/templates/filter-allowlist.js.map +1 -0
  123. package/dist/templates/filter-shared.d.ts +15 -0
  124. package/dist/templates/filter-shared.d.ts.map +1 -0
  125. package/dist/templates/filter-shared.js +30 -0
  126. package/dist/templates/filter-shared.js.map +1 -0
  127. package/dist/templates/filter-type.d.ts +4 -0
  128. package/dist/templates/filter-type.d.ts.map +1 -0
  129. package/dist/templates/filter-type.js +78 -0
  130. package/dist/templates/filter-type.js.map +1 -0
  131. package/dist/templates/inferred-types.d.ts +4 -0
  132. package/dist/templates/inferred-types.d.ts.map +1 -0
  133. package/dist/templates/inferred-types.js +14 -0
  134. package/dist/templates/inferred-types.js.map +1 -0
  135. package/dist/templates/projection-decl.d.ts +21 -0
  136. package/dist/templates/projection-decl.d.ts.map +1 -0
  137. package/dist/templates/projection-decl.js +116 -0
  138. package/dist/templates/projection-decl.js.map +1 -0
  139. package/dist/templates/queries-file.d.ts +4 -0
  140. package/dist/templates/queries-file.d.ts.map +1 -0
  141. package/dist/templates/queries-file.js +39 -0
  142. package/dist/templates/queries-file.js.map +1 -0
  143. package/dist/templates/queries.d.ts +9 -0
  144. package/dist/templates/queries.d.ts.map +1 -0
  145. package/dist/templates/queries.js +115 -0
  146. package/dist/templates/queries.js.map +1 -0
  147. package/dist/templates/relations-block.d.ts +9 -0
  148. package/dist/templates/relations-block.d.ts.map +1 -0
  149. package/dist/templates/relations-block.js +45 -0
  150. package/dist/templates/relations-block.js.map +1 -0
  151. package/dist/templates/routes-file.d.ts +4 -0
  152. package/dist/templates/routes-file.d.ts.map +1 -0
  153. package/dist/templates/routes-file.js +158 -0
  154. package/dist/templates/routes-file.js.map +1 -0
  155. package/dist/templates/zod-validators.d.ts +4 -0
  156. package/dist/templates/zod-validators.d.ts.map +1 -0
  157. package/dist/templates/zod-validators.js +129 -0
  158. package/dist/templates/zod-validators.js.map +1 -0
  159. package/package.json +59 -0
  160. package/src/column-mapper.ts +266 -0
  161. package/src/constants.ts +10 -0
  162. package/src/errors.ts +10 -0
  163. package/src/format.ts +50 -0
  164. package/src/generator.ts +73 -0
  165. package/src/generators/barrel.ts +28 -0
  166. package/src/generators/entity-file.ts +33 -0
  167. package/src/generators/index.ts +4 -0
  168. package/src/generators/queries-file.ts +32 -0
  169. package/src/generators/routes-file.ts +36 -0
  170. package/src/import-path.ts +153 -0
  171. package/src/index.ts +45 -0
  172. package/src/metaobjects-config.ts +95 -0
  173. package/src/naming.ts +84 -0
  174. package/src/overwrite-policy.ts +39 -0
  175. package/src/pk-resolver.ts +47 -0
  176. package/src/projection/extract-view-spec.ts +372 -0
  177. package/src/projection/index.ts +4 -0
  178. package/src/projection/projection-detector.ts +26 -0
  179. package/src/projection/view-ddl-emit.ts +66 -0
  180. package/src/projection/view-spec.ts +62 -0
  181. package/src/relation-resolver.ts +87 -0
  182. package/src/render-context.ts +93 -0
  183. package/src/runner.ts +178 -0
  184. package/src/templates/barrel.ts +23 -0
  185. package/src/templates/drizzle-schema.ts +286 -0
  186. package/src/templates/entity-constants.ts +248 -0
  187. package/src/templates/entity-file.ts +51 -0
  188. package/src/templates/field-meta.ts +150 -0
  189. package/src/templates/filter-allowlist.ts +104 -0
  190. package/src/templates/filter-shared.ts +30 -0
  191. package/src/templates/filter-type.ts +93 -0
  192. package/src/templates/inferred-types.ts +16 -0
  193. package/src/templates/projection-decl.ts +146 -0
  194. package/src/templates/queries-file.ts +56 -0
  195. package/src/templates/queries.ts +132 -0
  196. package/src/templates/relations-block.ts +65 -0
  197. package/src/templates/routes-file.ts +179 -0
  198. package/src/templates/zod-validators.ts +140 -0
@@ -0,0 +1,16 @@
1
+ // Inferred types template — emits Drizzle's InferSelectModel / InferInsertModel type aliases.
2
+
3
+ import { code, imp, type Code } from "ts-poet";
4
+ import type { MetaObject } from "@metaobjectsdev/metadata";
5
+ import { variableNameFromEntity } from "../naming.js";
6
+
7
+ export function renderInferredTypes(entity: MetaObject): Code {
8
+ const varName = variableNameFromEntity(entity.name);
9
+ const selectSym = imp("InferSelectModel@drizzle-orm");
10
+ const insertSym = imp("InferInsertModel@drizzle-orm");
11
+ return code`
12
+ export type ${entity.name} = ${selectSym}<typeof ${varName}>;
13
+ export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
14
+ export type ${entity.name}Update = Partial<${entity.name}Insert>;
15
+ `;
16
+ }
@@ -0,0 +1,146 @@
1
+ // Projection declaration template — emits a Drizzle view declaration,
2
+ // a Zod read schema, the TS type via z.infer, and a constants block.
3
+ //
4
+ // This is the read-only counterpart to entity-file.ts.
5
+ // It does NOT emit:
6
+ // - Drizzle table declaration (view-only)
7
+ // - Zod Insert/Update schemas
8
+ // - Insert/Update types
9
+
10
+ import { code, imp, joinCode, type Code } from "ts-poet";
11
+ import { MetaField, MetaObject, type MetaRoot } from "@metaobjectsdev/metadata";
12
+ import { extractViewSpec } from "../projection/extract-view-spec.js";
13
+ import { columnNameFromField, toSnakeCase, pluralize } from "../naming.js";
14
+ import { GENERATED_HEADER } from "../constants.js";
15
+ import type { ColumnNamingStrategy } from "../metaobjects-config.js";
16
+ import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
17
+ import { renderFilterType } from "./filter-type.js";
18
+ import { inferViewKind, zodTypeFor, currencyMetaFor, labelFor } from "./field-meta.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public interface
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface ProjectionDeclOpts {
25
+ readonly columnNamingStrategy: ColumnNamingStrategy;
26
+ readonly dialect: "postgres" | "sqlite";
27
+ readonly apiPrefix?: string;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Convert a PascalCase projection name to a kebab-pluralized URL path.
36
+ * "ProgramSummary" → "/program-summaries"
37
+ * "CustomerSummary" → "/customer-summaries"
38
+ * "Box" → "/boxes"
39
+ * "Wish" → "/wishes"
40
+ */
41
+ function pathFromProjectionName(name: string): string {
42
+ const kebab = toSnakeCase(pluralize(name)).replace(/_/g, "-");
43
+ return `/${kebab}`;
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Public API
48
+ // ---------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Render a TypeScript module for a projection entity. Emits:
52
+ * - `<camel>View` — Drizzle `pgView` / `sqliteView` with `.existing()`
53
+ * - `<Projection>Schema` — Zod read schema (no insert/update)
54
+ * - `<Projection>` — `z.infer` type alias
55
+ * - `<Projection>` const — constants block ($entity, $view, $path, per-field metadata)
56
+ *
57
+ * @param projection The projection entity (has a source[dbView] child).
58
+ * @param root The loader's root (all top-level objects as direct children,
59
+ * from `MetaDataLoader.load()` / `FileMetaDataLoader.loadFiles()` as `result.root`).
60
+ * @param opts Column naming strategy + dialect.
61
+ */
62
+ export function renderProjectionDecl(
63
+ projection: MetaObject,
64
+ root: MetaRoot,
65
+ opts: ProjectionDeclOpts,
66
+ ): string {
67
+ const { dialect, columnNamingStrategy, apiPrefix = "" } = opts;
68
+
69
+ const viewFn = dialect === "postgres" ? "pgView" : "sqliteView";
70
+ const viewModule =
71
+ dialect === "postgres" ? "drizzle-orm/pg-core" : "drizzle-orm/sqlite-core";
72
+ const viewSym = imp(`${viewFn}@${viewModule}`);
73
+ const z = imp("z@zod");
74
+
75
+ const spec = extractViewSpec(projection, root, { columnNamingStrategy });
76
+
77
+ // Collect fields: inherited from extends parent first, then projection-declared.
78
+ const allFields: MetaField[] = [];
79
+ const superModel = projection.superResolved;
80
+ const superName = superModel?.name ?? projection.superRef;
81
+ if (superName) {
82
+ const baseObj =
83
+ superModel instanceof MetaObject ? superModel : root.findObject(superName);
84
+ if (baseObj) {
85
+ // fields() returns effective fields, so inherited fields (from extends:/super:) are included.
86
+ for (const f of baseObj.fields()) allFields.push(f);
87
+ }
88
+ }
89
+ for (const f of projection.ownFields()) allFields.push(f);
90
+
91
+ const zodLines: Code[] = allFields.map(
92
+ (f) => code` ${f.name}: ${z}.${zodTypeFor(f).replace(/^z\./, "")}`,
93
+ );
94
+
95
+ const constFieldLines: string[] = allFields.map((f) => {
96
+ const dbCol = columnNameFromField(f.name, columnNamingStrategy);
97
+ const view = inferViewKind(f);
98
+ const label = labelFor(f);
99
+ const baseEntry = `name: ${JSON.stringify(f.name)}, label: ${JSON.stringify(label)}, view: ${JSON.stringify(view)}, dbCol: ${JSON.stringify(dbCol)}`;
100
+ const currencyMeta = currencyMetaFor(f);
101
+ if (currencyMeta !== null) {
102
+ return ` ${f.name}: { ${baseEntry}, currency: ${JSON.stringify(currencyMeta.currency)}, locale: ${JSON.stringify(currencyMeta.locale)} },`;
103
+ }
104
+ return ` ${f.name}: { ${baseEntry} },`;
105
+ });
106
+
107
+ const projName = projection.name;
108
+ const camelName = projName.charAt(0).toLowerCase() + projName.slice(1);
109
+ const path = pathFromProjectionName(projName);
110
+ const viewName = spec.viewName;
111
+
112
+ const sections: Code[] = [
113
+ code`
114
+ // View declaration — Drizzle uses this for typed SELECT queries.
115
+ // The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
116
+ // not to attempt DDL for this declaration.
117
+ export const ${camelName}View = ${viewSym}(${JSON.stringify(viewName)}, {}).existing();
118
+ `,
119
+ code`
120
+ export const ${projName}Schema = ${z}.object({
121
+ ${joinCode(zodLines, { on: ",\n" })}
122
+ });
123
+ `,
124
+ code`
125
+ export type ${projName} = ${z}.infer<typeof ${projName}Schema>;
126
+ `,
127
+ code`
128
+ export const ${projName} = {
129
+ $entity: ${JSON.stringify(projName)},
130
+ $view: ${JSON.stringify(viewName)},
131
+ $path: ${JSON.stringify(path)},
132
+ $apiPrefix: ${JSON.stringify(apiPrefix)},
133
+ ${constFieldLines.join("\n")}
134
+ } as const;
135
+ `,
136
+ renderFilterAllowlist(projection),
137
+ renderSortAllowlist(projection),
138
+ renderFilterType(projection),
139
+ ];
140
+
141
+ const body = joinCode(sections, { on: "\n" }).toString();
142
+ const header =
143
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
144
+ `// Source metadata: ${projName} (${projection.fqn()})\n`;
145
+ return header + body;
146
+ }
@@ -0,0 +1,56 @@
1
+ // Queries file composer — composes all CRUD function renderers (from queries.ts) into
2
+ // a complete <Entity>.queries.ts file with @generated header and correct imports.
3
+
4
+ import { code, joinCode, type Code } from "ts-poet";
5
+ import { MetaObject } from "@metaobjectsdev/metadata";
6
+ import { type RenderContext } from "../render-context.js";
7
+ import { entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
8
+ import {
9
+ renderFindByIdFn,
10
+ renderListFn,
11
+ renderCreateFn,
12
+ renderUpdateFn,
13
+ renderDeleteByIdFn,
14
+ } from "./queries.js";
15
+ import { variableNameFromEntity } from "../naming.js";
16
+ import { GENERATED_HEADER } from "../constants.js";
17
+
18
+ export function renderQueriesFile(obj: MetaObject, ctx: RenderContext): string {
19
+ const entityName = obj.name;
20
+ // Import the entity's own file. Same target → relative "./Entity"; cross
21
+ // target → importBase-qualified package path.
22
+ const entityFileName = entityModuleSpecifier(
23
+ ctx.selfTarget,
24
+ ctx.entityModuleTarget,
25
+ obj.package,
26
+ entityName,
27
+ ctx.extStyle,
28
+ );
29
+ const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, obj.package, ctx.dbImport);
30
+ const varName = variableNameFromEntity(entityName);
31
+
32
+ // Literal imports (db + entity types) live in a code block so they sort
33
+ // alongside ts-poet's hoisted imp() imports at the top of the body.
34
+ const literalImports = code`
35
+ import { db } from ${JSON.stringify(dbImportSpec)};
36
+ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON.stringify(entityFileName)};
37
+ `;
38
+
39
+ const sections: Code[] = [
40
+ literalImports,
41
+ renderFindByIdFn(obj, ctx),
42
+ renderListFn(obj, ctx),
43
+ renderCreateFn(obj, ctx),
44
+ renderUpdateFn(obj, ctx),
45
+ renderDeleteByIdFn(obj, ctx),
46
+ ];
47
+
48
+ // Render ts-poet body first, then prepend the @generated header so it lands
49
+ // at line 1 ahead of any imports.
50
+ const body = joinCode(sections, { on: "\n" }).toString();
51
+ const header =
52
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
53
+ `// Source metadata: ${entityName} (${obj.fqn()})\n` +
54
+ `// Customize via ${entityName}.extra.ts in this directory (additional queries, custom logic).\n`;
55
+ return header + body;
56
+ }
@@ -0,0 +1,132 @@
1
+ // Queries template — individual CRUD function renderers.
2
+ // Each returns a ts-poet Code block; composed into a file by queries-file.ts.
3
+
4
+ import { code, imp, type Code } from "ts-poet";
5
+ import type { MetaObject } from "@metaobjectsdev/metadata";
6
+ import { IDENTITY_ATTR_FIELDS } from "@metaobjectsdev/metadata";
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
+ }
14
+
15
+ /** Get the PK field name and its TS type for a given entity. */
16
+ function getPkInfo(entity: MetaObject, ctx: RenderContext): { fieldName: string; tsType: string } {
17
+ // Use primaryIdentity() to find the primary identity (may be inherited from extends:/super:).
18
+ const primary = entity.primaryIdentity();
19
+ const rawFields = primary?.ownAttr(IDENTITY_ATTR_FIELDS);
20
+ const fields = Array.isArray(rawFields) ? rawFields : (typeof rawFields === "string" ? [rawFields] : undefined);
21
+ const pkFieldName = fields?.[0] ?? "id";
22
+ const pkInfo = ctx.pkMap.get(entity.name);
23
+ const subType = pkInfo?.fieldSubType ?? "long";
24
+ const tsType =
25
+ subType === "long" || subType === "int" || subType === "short" || subType === "byte"
26
+ ? "number"
27
+ : subType === "boolean"
28
+ ? "boolean"
29
+ : "string";
30
+ return { fieldName: pkFieldName, tsType };
31
+ }
32
+
33
+ export function renderFindByIdFn(entity: MetaObject, ctx: RenderContext): Code {
34
+ const varName = variableNameFromEntity(entity.name);
35
+ const entityName = entity.name;
36
+ const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
37
+ const entitySnakeName = toSnakeCase(entityName);
38
+ const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
39
+ const pkSnakeName = toSnakeCase(pkField);
40
+ const prepName = prepareName("find", entitySnakeName, pkSnakeName);
41
+ const fnName = `find${entityName}ById`;
42
+ const prepVarName = `${fnName}Prepared`;
43
+ 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
+
53
+ 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} });
62
+ return ${singularVar} ?? null;
63
+ }
64
+ `;
65
+ }
66
+
67
+ export function renderListFn(entity: MetaObject, _ctx: RenderContext): Code {
68
+ const varName = variableNameFromEntity(entity.name);
69
+ const entityName = entity.name;
70
+ // Pluralize the PascalCase entity name, preserving capitalization
71
+ // (e.g., "Category" -> "Categories", not "Categorys").
72
+ const fnName = `list${pluralize(entityName)}`;
73
+
74
+ return code`
75
+ export async function ${fnName}(opts?: { limit?: number; offset?: number }): Promise<${entityName}[]> {
76
+ let q = db.select().from(${varName}).$dynamic();
77
+ if (opts?.limit !== undefined) q = q.limit(opts.limit);
78
+ if (opts?.offset !== undefined) q = q.offset(opts.offset);
79
+ return q;
80
+ }
81
+ `;
82
+ }
83
+
84
+ export function renderCreateFn(entity: MetaObject, _ctx: RenderContext): Code {
85
+ const varName = variableNameFromEntity(entity.name);
86
+ const entityName = entity.name;
87
+ const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
88
+ const fnName = `create${entityName}`;
89
+ const schemaName = `${entityName}InsertSchema`;
90
+
91
+ return code`
92
+ export async function ${fnName}(data: unknown): Promise<${entityName}> {
93
+ const validated = ${schemaName}.parse(data);
94
+ const [${singularVar}] = await db.insert(${varName}).values(validated).returning();
95
+ return ${singularVar}!;
96
+ }
97
+ `;
98
+ }
99
+
100
+ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
101
+ const varName = variableNameFromEntity(entity.name);
102
+ const entityName = entity.name;
103
+ const singularVar = entityName.charAt(0).toLowerCase() + entityName.slice(1);
104
+ const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
105
+ const fnName = `update${entityName}`;
106
+ const schemaName = `${entityName}InsertSchema`;
107
+ const eqSym = imp("eq@drizzle-orm");
108
+
109
+ return code`
110
+ export async function ${fnName}(${pkField}: ${pkType}, data: unknown): Promise<${entityName} | null> {
111
+ const validated = ${schemaName}.partial().parse(data);
112
+ const [${singularVar}] = await db.update(${varName}).set(validated).where(${eqSym}(${varName}.${pkField}, ${pkField})).returning();
113
+ return ${singularVar} ?? null;
114
+ }
115
+ `;
116
+ }
117
+
118
+ export function renderDeleteByIdFn(entity: MetaObject, ctx: RenderContext): Code {
119
+ const varName = variableNameFromEntity(entity.name);
120
+ const entityName = entity.name;
121
+ const { fieldName: pkField, tsType: pkType } = getPkInfo(entity, ctx);
122
+ const fnName = `delete${entityName}ById`;
123
+ const eqSym = imp("eq@drizzle-orm");
124
+
125
+ 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;
130
+ }
131
+ `;
132
+ }
@@ -0,0 +1,65 @@
1
+ // Relations block template — emits the Drizzle relations() block for one entity.
2
+ // Derived from the RelationMap pre-pass (relation-resolver.ts).
3
+
4
+ import { code, imp, joinCode, type Code } from "ts-poet";
5
+ import type { MetaObject } from "@metaobjectsdev/metadata";
6
+ import { CARDINALITY_ONE, CARDINALITY_MANY } from "@metaobjectsdev/metadata";
7
+ import { type RenderContext } from "../render-context.js";
8
+ import { crossEntitySpecifier } from "../import-path.js";
9
+ import { variableNameFromEntity } from "../naming.js";
10
+ import type { RelationEntry } from "../relation-resolver.js";
11
+
12
+ /**
13
+ * Render the relations() block for one entity.
14
+ * Returns null if the entity has no relations to emit.
15
+ */
16
+ export function renderRelationsBlock(entity: MetaObject, ctx: RenderContext): Code | null {
17
+ const entries = ctx.relationMap.get(entity.name);
18
+ if (!entries || entries.length === 0) return null;
19
+
20
+ const varName = variableNameFromEntity(entity.name);
21
+ const relationsFn = imp("relations@drizzle-orm");
22
+ const relationsVarName = `${varName}Relations`;
23
+
24
+ const hasOne = entries.some((e) => e.cardinality === CARDINALITY_ONE);
25
+ const hasMany = entries.some((e) => e.cardinality === CARDINALITY_MANY);
26
+
27
+ const paramParts: string[] = [];
28
+ if (hasOne) paramParts.push("one");
29
+ if (hasMany) paramParts.push("many");
30
+ const params = `{ ${paramParts.join(", ")} }`;
31
+
32
+ const thisEntityPackage = entity.package;
33
+ const lines: Code[] = entries.map((entry) =>
34
+ renderRelationEntry(entry, ctx, varName, thisEntityPackage),
35
+ );
36
+
37
+ return code`export const ${relationsVarName} = ${relationsFn}(${varName}, (${params}) => ({
38
+ ${joinCode(lines, { on: ",\n", trim: false })}
39
+ }));
40
+ `;
41
+ }
42
+
43
+ function renderRelationEntry(
44
+ entry: RelationEntry,
45
+ ctx: RenderContext,
46
+ thisVarName: string,
47
+ thisEntityPackage: string | undefined,
48
+ ): Code {
49
+ // Use imp() for cross-entity references so ts-poet tracks and emits the import.
50
+ const targetSpec = crossEntitySpecifier(
51
+ ctx.outputLayout,
52
+ thisEntityPackage,
53
+ ctx.packageOf.get(entry.targetEntity),
54
+ entry.targetEntity,
55
+ ctx.extStyle,
56
+ );
57
+ const targetVarSym = imp(`${variableNameFromEntity(entry.targetEntity)}@${targetSpec}`);
58
+
59
+ if (entry.cardinality === CARDINALITY_ONE) {
60
+ const pkInfo = ctx.pkMap.get(entry.targetEntity);
61
+ const targetPkField = pkInfo?.fieldName ?? "id";
62
+ return code` ${entry.name}: one(${targetVarSym}, { fields: [${thisVarName}.${entry.fkField ?? "id"}], references: [${targetVarSym}.${targetPkField}] })`;
63
+ }
64
+ return code` ${entry.name}: many(${targetVarSym})`;
65
+ }
@@ -0,0 +1,179 @@
1
+ // Fastify route template — emits a per-entity routes file that delegates
2
+ // CRUD verbs to helpers from @metaobjectsdev/runtime-ts/drizzle-fastify.
3
+ //
4
+ // Dispatch logic:
5
+ // isProjection(entity) → mountReadOnlyCrudRoutes (GET list + GET :id only)
6
+ // vanilla / write-through entity → mountCrudRoutes (all 5 CRUD verbs)
7
+ //
8
+ // apiPrefix behaviour:
9
+ // "" → flat mount: mountCrudRoutes({ fastify, ... })
10
+ // "/api" → wrapped: fastify.register(async (instance) => {
11
+ // mountCrudRoutes({ fastify: instance, ... });
12
+ // }, { prefix: "/api" });
13
+ //
14
+ // The user's Drizzle `db` instance is imported from ctx.dbImport (matching
15
+ // the existing queries-file template). The entity's Drizzle table const is
16
+ // imported alongside the Zod schemas + constants from the sibling Entity.ts.
17
+
18
+ import { code, imp } from "ts-poet";
19
+ import type { MetaObject } from "@metaobjectsdev/metadata";
20
+ import { type RenderContext } from "../render-context.js";
21
+ import { entityModuleSpecifier, relativeModuleSpecifier } from "../import-path.js";
22
+ import { GENERATED_HEADER } from "../constants.js";
23
+ import { variableNameFromEntity } from "../naming.js";
24
+ import { isProjection } from "../projection/projection-detector.js";
25
+
26
+ export function renderRoutesFile(entity: MetaObject, ctx: RenderContext): string {
27
+ const entityName = entity.name;
28
+ const handlerName = `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Routes`;
29
+ // Import the entity's own file. Same target → relative "./Entity"; cross
30
+ // target → importBase-qualified package path.
31
+ const entityFileSpec = entityModuleSpecifier(
32
+ ctx.selfTarget,
33
+ ctx.entityModuleTarget,
34
+ entity.package,
35
+ entityName,
36
+ ctx.extStyle,
37
+ );
38
+ const dbImportSpec = relativeModuleSpecifier(ctx.outputLayout, entity.package, ctx.dbImport);
39
+
40
+ const header =
41
+ `// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
42
+ `// Source metadata: ${entityName} (${entity.fqn()})\n` +
43
+ `// Customize via ${entityName}.extra.ts in this directory (e.g., auth, additional handlers).\n`;
44
+
45
+ // --- Projection path: read-only routes (GET list + GET :id) ---
46
+ if (isProjection(entity)) {
47
+ const camelName = entityName.charAt(0).toLowerCase() + entityName.slice(1);
48
+ const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
49
+ const mountReadOnlyCrudRoutesSym = imp(
50
+ "mountReadOnlyCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify",
51
+ );
52
+
53
+ const literalImports = code`
54
+ import { db } from ${JSON.stringify(dbImportSpec)};
55
+ import {
56
+ ${entityName},
57
+ ${camelName}View,
58
+ ${entityName}FilterAllowlist,
59
+ ${entityName}SortAllowlist,
60
+ } from ${JSON.stringify(entityFileSpec)};
61
+ `;
62
+
63
+ const body = ctx.apiPrefix
64
+ ? code`
65
+ /**
66
+ * Mount read-only REST endpoints for ${entityName} (projection — view-backed, no writes).
67
+ *
68
+ * Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
69
+ * Customize: register this as-is, or import individual route helpers from
70
+ * @metaobjectsdev/runtime-ts/drizzle-fastify.
71
+ */
72
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
73
+ await fastify.register(async (instance) => {
74
+ ${mountReadOnlyCrudRoutesSym}({
75
+ fastify: instance,
76
+ path: ${entityName}.$path,
77
+ db,
78
+ view: ${camelName}View,
79
+ filterAllowlist: ${entityName}FilterAllowlist,
80
+ sortAllowlist: ${entityName}SortAllowlist,
81
+ dialect: ${JSON.stringify(ctx.dialect)},
82
+ });
83
+ }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
84
+ }
85
+ `
86
+ : code`
87
+ /**
88
+ * Mount read-only REST endpoints for ${entityName} (projection — view-backed, no writes).
89
+ *
90
+ * Exposes GET list + GET :id only. POST/PATCH/DELETE return 405.
91
+ * Customize: register this as-is, or import individual route helpers from
92
+ * @metaobjectsdev/runtime-ts/drizzle-fastify.
93
+ */
94
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
95
+ ${mountReadOnlyCrudRoutesSym}({
96
+ fastify,
97
+ path: ${entityName}.$path,
98
+ db,
99
+ view: ${camelName}View,
100
+ filterAllowlist: ${entityName}FilterAllowlist,
101
+ sortAllowlist: ${entityName}SortAllowlist,
102
+ dialect: ${JSON.stringify(ctx.dialect)},
103
+ });
104
+ }
105
+ `;
106
+
107
+ return header + literalImports.toString() + body.toString();
108
+ }
109
+
110
+ // --- Vanilla / write-through entity path: full CRUD routes ---
111
+ const tableVar = variableNameFromEntity(entityName);
112
+
113
+ const FastifyInstanceSym = imp("t:FastifyInstance@fastify");
114
+ const mountCrudRoutesSym = imp("mountCrudRoutes@@metaobjectsdev/runtime-ts/drizzle-fastify");
115
+
116
+ const literalImports = code`
117
+ import { db } from ${JSON.stringify(dbImportSpec)};
118
+ import {
119
+ ${entityName},
120
+ ${tableVar},
121
+ ${entityName}InsertSchema,
122
+ ${entityName}UpdateSchema,
123
+ ${entityName}FilterAllowlist,
124
+ ${entityName}SortAllowlist,
125
+ } from ${JSON.stringify(entityFileSpec)};
126
+ `;
127
+
128
+ const body = ctx.apiPrefix
129
+ ? code`
130
+ /**
131
+ * Mount the 5 standard REST endpoints for ${entityName} using Drizzle directly.
132
+ *
133
+ * Customize: register this as-is for stock CRUD, OR import the per-verb
134
+ * helpers (mountListRoute, mountGetRoute, ...) from
135
+ * @metaobjectsdev/runtime-ts/drizzle-fastify and mix with your own handlers
136
+ * (auth, side effects, etc.).
137
+ */
138
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
139
+ await fastify.register(async (instance) => {
140
+ ${mountCrudRoutesSym}({
141
+ fastify: instance,
142
+ path: ${entityName}.$path,
143
+ db,
144
+ table: ${tableVar},
145
+ insertSchema: ${entityName}InsertSchema,
146
+ updateSchema: ${entityName}UpdateSchema,
147
+ filterAllowlist: ${entityName}FilterAllowlist,
148
+ sortAllowlist: ${entityName}SortAllowlist,
149
+ dialect: ${JSON.stringify(ctx.dialect)},
150
+ });
151
+ }, { prefix: ${JSON.stringify(ctx.apiPrefix)} });
152
+ }
153
+ `
154
+ : code`
155
+ /**
156
+ * Mount the 5 standard REST endpoints for ${entityName} using Drizzle directly.
157
+ *
158
+ * Customize: register this as-is for stock CRUD, OR import the per-verb
159
+ * helpers (mountListRoute, mountGetRoute, ...) from
160
+ * @metaobjectsdev/runtime-ts/drizzle-fastify and mix with your own handlers
161
+ * (auth, side effects, etc.).
162
+ */
163
+ export async function ${handlerName}(fastify: ${FastifyInstanceSym}) {
164
+ ${mountCrudRoutesSym}({
165
+ fastify,
166
+ path: ${entityName}.$path,
167
+ db,
168
+ table: ${tableVar},
169
+ insertSchema: ${entityName}InsertSchema,
170
+ updateSchema: ${entityName}UpdateSchema,
171
+ filterAllowlist: ${entityName}FilterAllowlist,
172
+ sortAllowlist: ${entityName}SortAllowlist,
173
+ dialect: ${JSON.stringify(ctx.dialect)},
174
+ });
175
+ }
176
+ `;
177
+
178
+ return header + literalImports.toString() + body.toString();
179
+ }