@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,93 @@
1
+ // RenderContext — cross-cutting state passed to every template.
2
+
3
+ import type { MetaRoot } from "@metaobjectsdev/metadata";
4
+ import type { Dialect } from "./column-mapper.js";
5
+ import type { PkInfo } from "./pk-resolver.js";
6
+ import type { RelationMap } from "./relation-resolver.js";
7
+ import type { ColumnNamingStrategy } from "./metaobjects-config.js";
8
+ import type { OutputLayout, ResolvedTarget } from "./import-path.js";
9
+
10
+ /**
11
+ * How to format cross-entity import specifiers in generated files.
12
+ * - "none" → emit `"./Foo"`. **Default.** Works for moduleResolution
13
+ * "bundler"/"node", tsx, drizzle-kit's TS loader, Vite, esbuild,
14
+ * and almost every modern TS toolchain.
15
+ * - "js" → emit `"./Foo.js"`. Required for Node ESM strict + TS NodeNext.
16
+ * Opt in via `--ext-style js` or `codegen.extStyle = "js"`.
17
+ */
18
+ export type ExtStyle = "js" | "none";
19
+
20
+ export interface RenderContext {
21
+ dialect: Dialect;
22
+ loadedRoot: MetaRoot;
23
+ outDir: string;
24
+ /**
25
+ * Import path for { db } in generated .queries.ts files.
26
+ * E.g. '~/server/db'. Set via forge.config.ts codegen.dbImport.
27
+ */
28
+ dbImport: string;
29
+ /**
30
+ * Import path for { om } in generated .routes.ts files.
31
+ * E.g. '../index' or '@your-pkg/database'. The module is expected to export
32
+ * an `om()` function returning a Promise<ObjectManager>.
33
+ * Defaults to '../index' (one level up from outDir).
34
+ */
35
+ omImport: string;
36
+ /** Cross-entity import-specifier style. Defaults to "js" (Node-ESM-safe). */
37
+ extStyle: ExtStyle;
38
+ /** Column naming strategy: how field names map to DB column names. Defaults to "snake_case". */
39
+ columnNamingStrategy: ColumnNamingStrategy;
40
+ /** Path prefix applied to generated route registrations + hook fetch URLs. Defaults to "". */
41
+ apiPrefix: string;
42
+ /** Output layout mode: "flat" (default) — all files in outDir; "package" — sub-paths from entity metadata package. */
43
+ outputLayout: OutputLayout;
44
+ /** The target THIS generator emits to (drives path layout + same-target imports). */
45
+ selfTarget: ResolvedTarget;
46
+ /** Where entity files live (drives cross-target entity imports). */
47
+ entityModuleTarget: ResolvedTarget;
48
+ pkMap: Map<string, PkInfo>;
49
+ /** Pre-pass relation map for FK + relations() block emission. */
50
+ relationMap: RelationMap;
51
+ /** Entity name → its metadata package (undefined if the entity has no package). Built once per run. */
52
+ packageOf: Map<string, string | undefined>;
53
+ }
54
+
55
+ /** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). */
56
+ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "apiPrefix" | "outputLayout" | "packageOf" | "selfTarget" | "entityModuleTarget"> & {
57
+ extStyle?: ExtStyle;
58
+ omImport?: string;
59
+ columnNamingStrategy?: ColumnNamingStrategy;
60
+ apiPrefix?: string;
61
+ outputLayout?: OutputLayout;
62
+ packageOf?: Map<string, string | undefined>;
63
+ selfTarget?: ResolvedTarget;
64
+ entityModuleTarget?: ResolvedTarget;
65
+ };
66
+
67
+ /** Append the configured extension to a cross-entity module specifier. */
68
+ export function withExt(spec: string, style: ExtStyle): string {
69
+ return style === "js" ? `${spec}.js` : spec;
70
+ }
71
+
72
+ /** Thin factory; applies sensible defaults for fields the caller may omit. */
73
+ export function makeRenderContext(opts: RenderContextInput): RenderContext {
74
+ const outputLayout = opts.outputLayout ?? "flat";
75
+ const defaultTarget: ResolvedTarget = opts.selfTarget ?? {
76
+ name: "default",
77
+ outDir: opts.outDir,
78
+ importBase: undefined,
79
+ outputLayout,
80
+ dbImport: opts.dbImport,
81
+ };
82
+ return {
83
+ ...opts,
84
+ extStyle: opts.extStyle ?? "none",
85
+ omImport: opts.omImport ?? "../index",
86
+ columnNamingStrategy: opts.columnNamingStrategy ?? "snake_case",
87
+ apiPrefix: opts.apiPrefix ?? "",
88
+ outputLayout,
89
+ packageOf: opts.packageOf ?? new Map(),
90
+ selfTarget: defaultTarget,
91
+ entityModuleTarget: opts.entityModuleTarget ?? defaultTarget,
92
+ };
93
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,178 @@
1
+ import { join } from "node:path";
2
+ import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
3
+ import { MetaRoot } from "@metaobjectsdev/metadata";
4
+ import type { Generator, GenContext, EmittedFile } from "./generator.js";
5
+ import type { MetaobjectsGenConfig } from "./metaobjects-config.js";
6
+ import { normalizeConfig, DEFAULT_TARGET_NAME } from "./metaobjects-config.js";
7
+ import type { ResolvedTarget } from "./import-path.js";
8
+ import { buildPkMap } from "./pk-resolver.js";
9
+ import { buildRelationMap } from "./relation-resolver.js";
10
+ import { makeRenderContext } from "./render-context.js";
11
+ import { decideAndWrite, type WriteResult, type MergeStrategy } from "./overwrite-policy.js";
12
+
13
+ /** JS-identifier-shape only. Prevents filesystem traversal when metadata comes
14
+ * from untrusted sources (e.g. MCP). Mirrors the guard in legacy generate.ts. */
15
+ const VALID_ENTITY_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
+
17
+ export interface RunGenOpts {
18
+ config: MetaobjectsGenConfig;
19
+ metadata: MetaData;
20
+ /** Optional whitelist of entity names. */
21
+ entityFilter?: string[];
22
+ /** Overwrite strategy passed to decideAndWrite. Defaults to "overwrite". */
23
+ mergeStrategy?: MergeStrategy;
24
+ }
25
+
26
+ export interface RunGenResult {
27
+ files: WriteResult[];
28
+ warnings: string[];
29
+ }
30
+
31
+ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
32
+ const warnings: string[] = [];
33
+ const strategy = opts.mergeStrategy ?? "overwrite";
34
+
35
+ // loadMemory now returns MetaRoot; guard here also covers callers that pass a
36
+ // plain MetaData (e.g. test helpers that build trees programmatically).
37
+ if (!(opts.metadata instanceof MetaRoot)) {
38
+ throw new Error("runGen: opts.metadata must be a loaded MetaRoot.");
39
+ }
40
+ const root = opts.metadata;
41
+
42
+ // 1. Resolve entities (filter + safety check).
43
+ const allObjects = root.objects();
44
+ const entityFilter = opts.entityFilter;
45
+ const filtered = entityFilter
46
+ ? allObjects.filter((o) => entityFilter.includes(o.name))
47
+ : allObjects;
48
+ if (filtered.length === 0) {
49
+ const reason = opts.entityFilter
50
+ ? "no object children match the provided entityFilter"
51
+ : "root has no object children";
52
+ warnings.push(`No entities to generate — ${reason}.`);
53
+ return { files: [], warnings };
54
+ }
55
+
56
+ const safeEntities: MetaObject[] = [];
57
+ for (const entity of filtered) {
58
+ if (!VALID_ENTITY_NAME.test(entity.name)) {
59
+ warnings.push(
60
+ `Skipping entity with unsafe name "${entity.name}" — must match /^[A-Za-z_][A-Za-z0-9_]*$/.`,
61
+ );
62
+ continue;
63
+ }
64
+ safeEntities.push(entity);
65
+ }
66
+ if (safeEntities.length === 0) {
67
+ return { files: [], warnings };
68
+ }
69
+
70
+ // 2. Resolve targets + entity-module target.
71
+ const config = normalizeConfig(opts.config);
72
+ const targets = config.targets;
73
+ const targetOf = (g: Generator): ResolvedTarget => {
74
+ const name = g.target ?? DEFAULT_TARGET_NAME;
75
+ const t = targets[name];
76
+ if (!t) {
77
+ throw new Error(
78
+ `Generator "${g.name}" references unknown target "${name}". ` +
79
+ `Valid targets: ${Object.keys(targets).join(", ")}.`,
80
+ );
81
+ }
82
+ return t;
83
+ };
84
+ // Validate all target references up front.
85
+ for (const g of config.generators) targetOf(g);
86
+
87
+ const entityGen = config.generators.find((g) => g.emitsEntityModule);
88
+ const entityModuleTarget = entityGen ? targetOf(entityGen) : targets[DEFAULT_TARGET_NAME]!;
89
+
90
+ const needsCrossTarget = config.generators.some(
91
+ (g) => (g.target ?? DEFAULT_TARGET_NAME) !== entityModuleTarget.name,
92
+ );
93
+ if (needsCrossTarget && entityModuleTarget.importBase === undefined) {
94
+ throw new Error(
95
+ `Target "${entityModuleTarget.name}" holds the entity modules that other ` +
96
+ `targets import, but has no importBase. Set importBase on it (e.g. ` +
97
+ `"@your-pkg/database/generated").`,
98
+ );
99
+ }
100
+
101
+ // 3. Build shared render state once.
102
+ const pkMap = buildPkMap(root);
103
+ const relationMap = buildRelationMap(root);
104
+ const packageOf = new Map<string, string | undefined>(
105
+ root.objects().map((o) => [o.name, o.package]),
106
+ );
107
+
108
+ // 4. Run each generator with a per-target render context; collect with full path.
109
+ const emitted: { fullPath: string; content: string; generatedBy: string }[] = [];
110
+ for (const generator of config.generators) {
111
+ const selfTarget = targetOf(generator);
112
+ const renderContext = makeRenderContext({
113
+ dialect: config.dialect,
114
+ loadedRoot: root,
115
+ outDir: selfTarget.outDir,
116
+ dbImport: selfTarget.dbImport,
117
+ extStyle: config.extStyle,
118
+ columnNamingStrategy: config.columnNamingStrategy,
119
+ apiPrefix: config.apiPrefix,
120
+ outputLayout: selfTarget.outputLayout,
121
+ pkMap,
122
+ relationMap,
123
+ packageOf,
124
+ selfTarget,
125
+ entityModuleTarget,
126
+ });
127
+ const ctx: GenContext = {
128
+ entities: safeEntities,
129
+ loadedRoot: root,
130
+ matches: (e) => generator.filter?.(e) ?? true,
131
+ config: {
132
+ outDir: selfTarget.outDir,
133
+ extStyle: config.extStyle,
134
+ dbImport: selfTarget.dbImport,
135
+ dialect: config.dialect,
136
+ outputLayout: selfTarget.outputLayout,
137
+ },
138
+ renderContext,
139
+ warn: (msg) => warnings.push(`[${generator.name}] ${msg}`),
140
+ };
141
+
142
+ let files: EmittedFile[];
143
+ try {
144
+ files = await generator.generate(ctx);
145
+ } catch (err) {
146
+ const msg = err instanceof Error ? err.message : String(err);
147
+ throw new Error(`[${generator.name}] ${msg}`);
148
+ }
149
+
150
+ for (const file of files) {
151
+ const fullPath = join(selfTarget.outDir, file.path);
152
+ const collision = emitted.find((prev) => prev.fullPath === fullPath);
153
+ if (collision) {
154
+ throw new Error(
155
+ `Output path collision: "${fullPath}" emitted by both ` +
156
+ `"${collision.generatedBy}" and "${generator.name}". ` +
157
+ `Adjust one generator's filter or output path.`,
158
+ );
159
+ }
160
+ emitted.push({ fullPath, content: file.content, generatedBy: generator.name });
161
+ }
162
+ }
163
+
164
+ // 5. Write phase.
165
+ const writes: WriteResult[] = [];
166
+ for (const file of emitted) {
167
+ const result = decideAndWrite(file.fullPath, file.content, strategy);
168
+ writes.push(result);
169
+ if (result.status === "refused") {
170
+ warnings.push(
171
+ `Refused to overwrite ${file.fullPath}: file exists without @generated header. ` +
172
+ `Move to a different outDir, delete the file, or add the header to opt in.`,
173
+ );
174
+ }
175
+ }
176
+
177
+ return { files: writes, warnings };
178
+ }
@@ -0,0 +1,23 @@
1
+ // Barrel template — emits index.ts with one export per entity, alphabetical.
2
+
3
+ import { GENERATED_HEADER } from "../constants.js";
4
+ import { type ExtStyle } from "../render-context.js";
5
+ import { barrelModuleSpecifier, type ResolvedTarget } from "../import-path.js";
6
+
7
+ export interface BarrelEntry {
8
+ name: string;
9
+ package: string | undefined;
10
+ }
11
+
12
+ export function renderBarrel(
13
+ entries: BarrelEntry[],
14
+ extStyle: ExtStyle,
15
+ selfTarget: ResolvedTarget,
16
+ entityModuleTarget: ResolvedTarget,
17
+ ): string {
18
+ const sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name));
19
+ const exports = sorted
20
+ .map((e) => `export * from ${JSON.stringify(barrelModuleSpecifier(selfTarget, entityModuleTarget, e.package, e.name, extStyle))};`)
21
+ .join("\n");
22
+ return `// ${GENERATED_HEADER} — DO NOT EDIT.\n${exports}\n`;
23
+ }
@@ -0,0 +1,286 @@
1
+ // Drizzle schema template — emits a single sqliteTable() or pgTable() definition,
2
+ // with FK .references() on FK columns derived from relationship children,
3
+ // plus the relations() block auto-emitted at the end.
4
+
5
+ import { code, imp, joinCode, type Code } from "ts-poet";
6
+ import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
7
+ import {
8
+ IDENTITY_SUBTYPE_SECONDARY, FIELD_SUBTYPE_LONG,
9
+ IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, IDENTITY_ATTR_UNIQUE,
10
+ GENERATION_INCREMENT, GENERATION_UUID,
11
+ FIELD_ATTR_AUTO_SET,
12
+ } from "@metaobjectsdev/metadata";
13
+ import { type RenderContext } from "../render-context.js";
14
+ import { crossEntitySpecifier } from "../import-path.js";
15
+ import { mapColumnType } from "../column-mapper.js";
16
+ import { tableNameFromEntity, variableNameFromEntity, columnNameFromField } from "../naming.js";
17
+ import { renderRelationsBlock } from "./relations-block.js";
18
+
19
+ /**
20
+ * Render the Drizzle table definition for one entity, including:
21
+ * - FK .references() on FK columns derived from relationship children
22
+ * - relations() block at the end of the section
23
+ *
24
+ * Returns a Code object so ts-poet can deduplicate imports when this composes
25
+ * with the rest of the entity file. Biome formatting runs after composition.
26
+ */
27
+ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
28
+ const dialect = ctx.dialect;
29
+ const tableFn = dialect === "sqlite" ? "sqliteTable" : "pgTable";
30
+ const importModule = dialect === "sqlite" ? "drizzle-orm/sqlite-core" : "drizzle-orm/pg-core";
31
+ const tableFnSym = imp(`${tableFn}@${importModule}`);
32
+
33
+ const tableName = obj.dbTable ?? tableNameFromEntity(obj.name, ctx.columnNamingStrategy);
34
+ const varName = variableNameFromEntity(obj.name);
35
+
36
+ const primary = obj.primaryIdentity();
37
+ const rawPkFields = primary?.ownAttr(IDENTITY_ATTR_FIELDS);
38
+ const pkFieldsList: string[] = Array.isArray(rawPkFields)
39
+ ? rawPkFields as string[]
40
+ : typeof rawPkFields === "string"
41
+ ? rawPkFields.split(",").map((f) => f.trim()).filter(Boolean)
42
+ : [];
43
+ const pkFieldNames = new Set<string>(pkFieldsList);
44
+ const pkGeneration = primary?.ownAttr(IDENTITY_ATTR_GENERATION) as string | undefined;
45
+
46
+ const fkMap = buildFkMapForEntity(obj, ctx);
47
+
48
+ const isComposite = pkFieldNames.size > 1;
49
+
50
+ // Collect secondary identities and the field names that need .unique() on
51
+ // their column. Only single-column UNIQUE identities propagate .unique() to
52
+ // the column — non-unique indexes emit a separate index() callback entry
53
+ // (handled below) and must not stamp .unique() on the underlying column.
54
+ const secondaryIdentities = obj.secondaryIdentities();
55
+ const uniqueFieldNames = new Set<string>();
56
+ for (const sec of secondaryIdentities) {
57
+ const uniqueAttr = sec.ownAttr(IDENTITY_ATTR_UNIQUE);
58
+ if (uniqueAttr === false) continue; // explicit non-unique → don't mark column
59
+ const fields = sec.ownAttr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
60
+ if (!Array.isArray(fields) || fields.length !== 1) continue; // multi-col uniques use a callback index, not a column flag
61
+ uniqueFieldNames.add(fields[0]!);
62
+ }
63
+
64
+ const columnLines: Code[] = [];
65
+ for (const child of obj.fields()) {
66
+ const isPk = pkFieldNames.has(child.name);
67
+ const isUnique = uniqueFieldNames.has(child.name) && !isPk;
68
+ const fkInfo = fkMap.get(child.name);
69
+ columnLines.push(renderColumn(child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package));
70
+ }
71
+
72
+ // Build all table callback entries
73
+ const callbackEntries: Code[] = [];
74
+
75
+ if (isComposite && primary !== undefined) {
76
+ callbackEntries.push(buildCompositeKeyCallback(pkFieldNames, importModule));
77
+ }
78
+
79
+ for (const sec of secondaryIdentities) {
80
+ const fields = sec.ownAttr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
81
+ if (!Array.isArray(fields) || fields.length === 0) continue;
82
+ const indexName = `idx_${tableName}_${fields.map((f) => columnNameFromField(f, ctx.columnNamingStrategy)).join("_")}`;
83
+ // @unique on the identity defaults to true (preserves back-compat with
84
+ // foundations fixtures that assumed secondary identities were always
85
+ // unique). Explicit @unique: false → ordinary non-unique index.
86
+ const uniqueAttr = sec.ownAttr(IDENTITY_ATTR_UNIQUE);
87
+ const isUnique = uniqueAttr !== false;
88
+ const indexFn = isUnique ? "uniqueIndex" : "index";
89
+ const indexSym = imp(`${indexFn}@${importModule}`);
90
+ // Use the callback param `table` (not the outer varName) so TS doesn't see
91
+ // the table referencing itself inside its own initializer (TS7022/TS7024).
92
+ const cols = fields.map((f) => `table.${f}`).join(", ");
93
+ callbackEntries.push(code`${indexSym}(${JSON.stringify(indexName)}).on(${cols})`);
94
+ }
95
+
96
+ let tableBlock: Code;
97
+ if (callbackEntries.length > 0) {
98
+ tableBlock = code`
99
+ export const ${varName} = ${tableFnSym}(${JSON.stringify(tableName)}, {
100
+ ${joinCode(columnLines, { on: ",\n", trim: false })}
101
+ }, (table) => [
102
+ ${joinCode(callbackEntries, { on: ",\n ", trim: false })}
103
+ ]);
104
+ `;
105
+ } else {
106
+ tableBlock = code`
107
+ export const ${varName} = ${tableFnSym}(${JSON.stringify(tableName)}, {
108
+ ${joinCode(columnLines, { on: ",\n", trim: false })}
109
+ });
110
+ `;
111
+ }
112
+
113
+ // Emit the relations() block (returns null if no relations).
114
+ const relationsBlock = renderRelationsBlock(obj, ctx);
115
+
116
+ if (relationsBlock === null) {
117
+ return tableBlock;
118
+ }
119
+
120
+ return joinCode([tableBlock, relationsBlock], { on: "\n" });
121
+ }
122
+
123
+ interface FkInfo {
124
+ targetVarName: string; // e.g., "users"
125
+ targetEntityName: string; // e.g., "User" — used for the import path
126
+ targetPkField: string; // e.g., "id"
127
+ }
128
+
129
+ /** Pre-pass: map fkFieldName → FkInfo for this entity's effective (own + inherited) identity.reference children. */
130
+ function buildFkMapForEntity(obj: MetaObject, ctx: RenderContext): Map<string, FkInfo> {
131
+ const result = new Map<string, FkInfo>();
132
+ for (const ref of obj.referenceIdentities()) {
133
+ // @enforce: false → logical-only reference. Skip the .references() emission;
134
+ // the column stays plain. Drizzle's relations() block (driven by
135
+ // relation-resolver) still includes the relationship for query navigation.
136
+ if (!ref.enforce) continue;
137
+ const fkFieldNames = ref.fields;
138
+ if (fkFieldNames.length === 0) continue;
139
+ const fkField = fkFieldNames[0]!;
140
+ const targetName = ref.targetEntity;
141
+ if (!targetName) continue;
142
+ const targetObj = ctx.loadedRoot.findObject(targetName);
143
+ if (!targetObj) continue;
144
+ const targetPkField = ref.resolvedTargetPkField(ctx.loadedRoot) ?? "id";
145
+ result.set(fkField, {
146
+ targetVarName: variableNameFromEntity(targetObj.name),
147
+ targetEntityName: targetObj.name,
148
+ targetPkField,
149
+ });
150
+ }
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Build the `primaryKey({ columns: [table.f1, table.f2] })` Code expression
156
+ * for composite primary keys. Uses imp() so ts-poet tracks the import.
157
+ */
158
+ function buildCompositeKeyCallback(
159
+ pkFieldNames: Set<string>,
160
+ importModule: string,
161
+ ): Code {
162
+ const primaryKeySym = imp(`primaryKey@${importModule}`);
163
+ const columnRefs = Array.from(pkFieldNames)
164
+ .map((f) => `table.${f}`)
165
+ .join(", ");
166
+ return code`${primaryKeySym}({ columns: [${columnRefs}] })`;
167
+ }
168
+
169
+ /** Build a JS-style object literal string (not JSON.stringify which uses quoted keys). */
170
+ function inlineObjectLiteral(obj: Record<string, unknown>): string {
171
+ const entries = Object.entries(obj).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
172
+ return `{ ${entries.join(", ")} }`;
173
+ }
174
+
175
+ /** Render one column line (field name + Drizzle column expression). */
176
+ function renderColumn(
177
+ field: MetaField,
178
+ ctx: RenderContext,
179
+ isPk: boolean,
180
+ pkGeneration: string | undefined,
181
+ fkInfo: FkInfo | undefined,
182
+ isComposite: boolean,
183
+ isUnique: boolean = false,
184
+ entityPackage: string | undefined = undefined,
185
+ ): Code {
186
+ const spec = mapColumnType(field, ctx.dialect, ctx.columnNamingStrategy);
187
+ const fnSym = imp(`${spec.fnName}@${spec.importModule}`);
188
+
189
+ const dbNameLit = JSON.stringify(spec.dbName);
190
+ let baseCall: Code;
191
+ if (spec.fnOptions !== undefined && Object.keys(spec.fnOptions).length > 0) {
192
+ baseCall = code`${fnSym}(${dbNameLit}, ${inlineObjectLiteral(spec.fnOptions)})`;
193
+ } else {
194
+ baseCall = code`${fnSym}(${dbNameLit})`;
195
+ }
196
+
197
+ let pkSuffix = "";
198
+ if (isPk) {
199
+ if (pkGeneration === GENERATION_INCREMENT) {
200
+ if (ctx.dialect === "sqlite") {
201
+ // Composite PKs don't use .primaryKey() per-column; table callback owns it.
202
+ pkSuffix = isComposite ? "" : ".primaryKey({ autoIncrement: true })";
203
+ } else {
204
+ // Postgres: bigserial for long (8-byte), serial for int (4-byte).
205
+ if (field.subType === FIELD_SUBTYPE_LONG) {
206
+ const bigserialSym = imp(`bigserial@${spec.importModule}`);
207
+ baseCall = code`${bigserialSym}(${dbNameLit}, { mode: "number" })`;
208
+ } else {
209
+ const serialSym = imp(`serial@${spec.importModule}`);
210
+ baseCall = code`${serialSym}(${dbNameLit})`;
211
+ }
212
+ pkSuffix = isComposite ? "" : ".primaryKey()";
213
+ }
214
+ } else if (pkGeneration === GENERATION_UUID) {
215
+ pkSuffix = isComposite
216
+ ? ""
217
+ : ctx.dialect === "sqlite"
218
+ ? ".primaryKey().$defaultFn(() => crypto.randomUUID())"
219
+ : ".primaryKey().defaultRandom()";
220
+ } else {
221
+ // No generation: natural PK. Composite hands off to table callback.
222
+ pkSuffix = isComposite ? "" : ".primaryKey()";
223
+ }
224
+ }
225
+
226
+ let modifiersStr = pkSuffix;
227
+ // Append .unique() for secondary-identity fields (isPk guard already excluded PKs upstream).
228
+ if (isUnique) modifiersStr += ".unique()";
229
+ for (const m of spec.modifiers) {
230
+ // Single-column PKs imply notNull/unique; avoid emitting them twice.
231
+ // Composite-PK columns are NOT declared with .primaryKey(), so they DO need .notNull().
232
+ if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
233
+ // Avoid double-emitting .unique() if it was already appended above.
234
+ if (isUnique && m === ".unique()") continue;
235
+ modifiersStr += m;
236
+ }
237
+
238
+ // sqlDefaultSegment must be a Code segment (not a raw string) so ts-poet tracks
239
+ // the `sql` import via imp(); a raw `.default(sql`...`)` would leave `sql`
240
+ // unresolved in the generated file.
241
+ let sqlDefaultSegment: Code | null = null;
242
+ if (spec.defaultExpr !== undefined && !isPk) {
243
+ if (spec.defaultExpr.kind === "now") {
244
+ if (ctx.dialect === "sqlite") {
245
+ const sqlSym = imp("sql@drizzle-orm");
246
+ sqlDefaultSegment = code`.default(${sqlSym}\`CURRENT_TIMESTAMP\`)`;
247
+ } else {
248
+ modifiersStr += `.defaultNow()`;
249
+ }
250
+ } else if (spec.defaultExpr.kind === "sqlExpr") {
251
+ const sqlSym = imp("sql@drizzle-orm");
252
+ // Raw SQL keyword/expression — emit as sql`<raw>` for both dialects.
253
+ sqlDefaultSegment = code`.default(${sqlSym}\`${spec.defaultExpr.raw}\`)`;
254
+ } else {
255
+ // literal
256
+ modifiersStr += `.default(${JSON.stringify(spec.defaultExpr.value)})`;
257
+ }
258
+ }
259
+
260
+ // FK .references() uses imp() so ts-poet tracks the cross-entity import.
261
+ let fkRefSegment: Code | null = null;
262
+ if (fkInfo !== undefined && !isPk) {
263
+ const targetSpec = crossEntitySpecifier(
264
+ ctx.outputLayout,
265
+ entityPackage,
266
+ ctx.packageOf.get(fkInfo.targetEntityName),
267
+ fkInfo.targetEntityName,
268
+ ctx.extStyle,
269
+ );
270
+ const targetVarSym = imp(`${fkInfo.targetVarName}@${targetSpec}`);
271
+ fkRefSegment = code`.references(() => ${targetVarSym}.${fkInfo.targetPkField})`;
272
+ }
273
+
274
+ // @autoSet fields: emit .$defaultFn(() => new Date().toISOString()) so Drizzle
275
+ // inserts stamp the server-side timestamp automatically. This means callers don't
276
+ // need to supply createdAt / updatedAt in INSERT calls — Drizzle fills them in.
277
+ const autoSet = field.ownAttr(FIELD_ATTR_AUTO_SET);
278
+ const autoSetSuffix = (autoSet === "onCreate" || autoSet === "onUpdate")
279
+ ? `.$defaultFn(() => new Date().toISOString())`
280
+ : "";
281
+
282
+ const columnLine = code` ${field.name}: ${baseCall}${modifiersStr}${autoSetSuffix}${sqlDefaultSegment ?? ""}${fkRefSegment ?? ""}`;
283
+ return spec.leadingComment !== undefined
284
+ ? code` // ${spec.leadingComment}\n${columnLine}`
285
+ : columnLine;
286
+ }