appflare 0.2.48 → 0.2.49

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 (139) hide show
  1. package/Documentation.md +898 -898
  2. package/cli/commands/index.ts +247 -247
  3. package/cli/generate.ts +360 -360
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1366 -1366
  7. package/cli/templates/auth/README.md +156 -156
  8. package/cli/templates/auth/config.ts +61 -61
  9. package/cli/templates/auth/route-config.ts +1 -1
  10. package/cli/templates/auth/route-handler.ts +1 -1
  11. package/cli/templates/auth/route-request-utils.ts +5 -5
  12. package/cli/templates/auth/route.config.ts +18 -18
  13. package/cli/templates/auth/route.handler.ts +18 -18
  14. package/cli/templates/auth/route.request-utils.ts +55 -55
  15. package/cli/templates/auth/route.ts +14 -14
  16. package/cli/templates/core/README.md +266 -266
  17. package/cli/templates/core/app-creation.ts +19 -19
  18. package/cli/templates/core/client/appflare.ts +112 -112
  19. package/cli/templates/core/client/handlers/index.ts +763 -763
  20. package/cli/templates/core/client/handlers.ts +1 -1
  21. package/cli/templates/core/client/index.ts +7 -7
  22. package/cli/templates/core/client/storage.ts +195 -195
  23. package/cli/templates/core/client/types.ts +187 -187
  24. package/cli/templates/core/client-modules/appflare.ts +1 -1
  25. package/cli/templates/core/client-modules/handlers.ts +1 -1
  26. package/cli/templates/core/client-modules/index.ts +1 -1
  27. package/cli/templates/core/client-modules/storage.ts +1 -1
  28. package/cli/templates/core/client-modules/types.ts +1 -1
  29. package/cli/templates/core/client.artifacts.ts +39 -39
  30. package/cli/templates/core/client.ts +4 -4
  31. package/cli/templates/core/drizzle.ts +15 -15
  32. package/cli/templates/core/export.ts +14 -14
  33. package/cli/templates/core/handlers.route.ts +24 -24
  34. package/cli/templates/core/handlers.ts +1 -1
  35. package/cli/templates/core/imports.ts +9 -9
  36. package/cli/templates/core/server.ts +38 -38
  37. package/cli/templates/core/types.ts +6 -6
  38. package/cli/templates/core/wrangler.ts +109 -109
  39. package/cli/templates/dashboard/builders/functions/index.ts +17 -17
  40. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
  41. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
  42. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +271 -271
  43. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
  44. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +703 -703
  45. package/cli/templates/dashboard/builders/functions/tree-builder.ts +47 -47
  46. package/cli/templates/dashboard/builders/navigation.ts +155 -155
  47. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  48. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  49. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  50. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  51. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  52. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  53. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  55. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  56. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  57. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  58. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  59. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  60. package/cli/templates/dashboard/builders/table-routes/fragments.ts +257 -217
  61. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  62. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  63. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  64. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  65. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  66. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  67. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  68. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  69. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  70. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  71. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  72. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  73. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  74. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  75. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  76. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  77. package/cli/templates/dashboard/components/layout.ts +420 -420
  78. package/cli/templates/dashboard/components/login-page.ts +65 -65
  79. package/cli/templates/dashboard/index.ts +61 -61
  80. package/cli/templates/dashboard/types.ts +9 -9
  81. package/cli/templates/handlers/README.md +353 -353
  82. package/cli/templates/handlers/auth.ts +37 -37
  83. package/cli/templates/handlers/execution.ts +44 -42
  84. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  85. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  86. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  87. package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
  88. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  89. package/cli/templates/handlers/generators/context/types.ts +40 -40
  90. package/cli/templates/handlers/generators/context.ts +43 -43
  91. package/cli/templates/handlers/generators/execution.ts +15 -15
  92. package/cli/templates/handlers/generators/handlers.ts +14 -14
  93. package/cli/templates/handlers/generators/registration/modules/cron.ts +35 -35
  94. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  95. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  96. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  97. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  98. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  99. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  100. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +510 -510
  101. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +65 -65
  102. package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
  103. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  104. package/cli/templates/handlers/generators/types/context.ts +121 -121
  105. package/cli/templates/handlers/generators/types/core.ts +108 -108
  106. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  107. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +291 -291
  108. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  109. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1382 -1382
  110. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -278
  111. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +156 -156
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  116. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +958 -958
  117. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  118. package/cli/templates/handlers/index.ts +47 -47
  119. package/cli/templates/handlers/operations.ts +116 -116
  120. package/cli/templates/handlers/registration.ts +91 -91
  121. package/cli/templates/handlers/types.ts +17 -17
  122. package/cli/templates/handlers/utils.ts +48 -48
  123. package/cli/types.ts +110 -110
  124. package/cli/utils/handler-discovery.ts +501 -501
  125. package/cli/utils/json-utils.ts +24 -24
  126. package/cli/utils/path-utils.ts +19 -19
  127. package/cli/utils/schema-discovery.ts +402 -399
  128. package/dist/cli/index.js +77 -55
  129. package/dist/cli/index.mjs +77 -55
  130. package/index.ts +18 -18
  131. package/package.json +58 -58
  132. package/react/index.ts +5 -5
  133. package/react/use-infinite-query.ts +255 -255
  134. package/react/use-mutation.ts +89 -89
  135. package/react/use-query.ts +210 -210
  136. package/schema.ts +641 -641
  137. package/test-better-auth-hash.ts +2 -2
  138. package/tsconfig.json +6 -6
  139. package/tsup.config.ts +82 -82
@@ -1,1366 +1,1366 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import type {
5
- ColumnDefinition,
6
- ColumnType,
7
- JsonShape,
8
- ManyRelationDefinition,
9
- ManyToManyRelationDefinition,
10
- OneRelationDefinition,
11
- SchemaDefinition,
12
- TableDefinition,
13
- } from "../schema";
14
- import { isSchemaDefinition } from "../schema";
15
- import type { LoadedAppflareConfig } from "./types";
16
-
17
- export type CompiledSchemaArtifacts = {
18
- schemaPath: string;
19
- typesPath: string;
20
- zodPath: string;
21
- tableNames: string[];
22
- };
23
-
24
- function toSnakeCase(value: string): string {
25
- return value
26
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
27
- .replace(/[\s-]+/g, "_")
28
- .toLowerCase();
29
- }
30
-
31
- function toPascalCase(value: string): string {
32
- return value
33
- .replace(/[_-]+/g, " ")
34
- .replace(/\s+(.)/g, (_match, char: string) => char.toUpperCase())
35
- .replace(/\s/g, "")
36
- .replace(/^(.)/, (_match, char: string) => char.toUpperCase());
37
- }
38
-
39
- function singularize(value: string): string {
40
- if (value.endsWith("ies")) {
41
- return `${value.slice(0, -3)}y`;
42
- }
43
- if (value.endsWith("ses")) {
44
- return value.slice(0, -2);
45
- }
46
- if (value.endsWith("s") && value.length > 1) {
47
- return value.slice(0, -1);
48
- }
49
- return value;
50
- }
51
-
52
- function quote(value: string): string {
53
- return JSON.stringify(value);
54
- }
55
-
56
- function toSqlLiteral(value: unknown): string {
57
- if (typeof value === "string") {
58
- return quote(value);
59
- }
60
- if (typeof value === "number" || typeof value === "boolean") {
61
- return String(value);
62
- }
63
- if (value === null) {
64
- return "null";
65
- }
66
- if (value instanceof Date) {
67
- return quote(value.toISOString());
68
- }
69
- if (typeof value === "object" && value !== null) {
70
- return JSON.stringify(value);
71
- }
72
- throw new Error(
73
- `Unsupported SQL default value '${String(value)}'. Use string, number, boolean, null, Date, array, or object.`,
74
- );
75
- }
76
-
77
- function cloneSchemaDefinition(definition: SchemaDefinition): SchemaDefinition {
78
- return {
79
- kind: "schema",
80
- tables: Object.fromEntries(
81
- Object.entries(definition.tables).map(([tableName, table]) => {
82
- return [
83
- tableName,
84
- {
85
- kind: "table",
86
- sqlName: table.sqlName,
87
- columns: Object.fromEntries(
88
- Object.entries(table.columns).map(([columnName, column]) => {
89
- return [columnName, { ...column }];
90
- }),
91
- ),
92
- relations: Object.fromEntries(
93
- Object.entries(table.relations).map(
94
- ([relationName, relation]) => {
95
- return [relationName, { ...relation }];
96
- },
97
- ),
98
- ),
99
- } satisfies TableDefinition,
100
- ];
101
- }),
102
- ),
103
- enums: Object.fromEntries(
104
- Object.entries(definition.enums ?? {}).map(([name, enumDef]) => {
105
- return [name, { ...enumDef }];
106
- }),
107
- ),
108
- };
109
- }
110
-
111
- function getLocalReferenceType(
112
- definition: SchemaDefinition,
113
- targetTable: string,
114
- referenceField: string,
115
- ): ColumnType | undefined {
116
- const table = definition.tables[targetTable];
117
- if (!table) {
118
- return undefined;
119
- }
120
- const targetColumn = table.columns[referenceField];
121
- return targetColumn?.type;
122
- }
123
-
124
- function ensureInferredReferenceColumn(
125
- tableName: string,
126
- table: TableDefinition,
127
- columnName: string,
128
- referenceTable: string,
129
- referenceField: string,
130
- options: {
131
- fkType?: ColumnType;
132
- sqlName?: string;
133
- notNull?: boolean;
134
- onDelete?: OneRelationDefinition["onDelete"];
135
- onUpdate?: OneRelationDefinition["onUpdate"];
136
- },
137
- inferredType: ColumnType,
138
- ): void {
139
- const existing = table.columns[columnName];
140
- if (existing) {
141
- if (
142
- existing.references &&
143
- (existing.references.table !== referenceTable ||
144
- existing.references.column !== referenceField)
145
- ) {
146
- throw new Error(
147
- `Inferred relation '${tableName}.${columnName}' conflicts with explicit references(${existing.references.table}.${existing.references.column}).`,
148
- );
149
- }
150
-
151
- table.columns[columnName] = {
152
- ...existing,
153
- notNull:
154
- existing.notNull ??
155
- (existing.nullable === true ? false : options.notNull),
156
- nullable:
157
- existing.nullable ?? (options.notNull === false ? true : undefined),
158
- references: {
159
- table: referenceTable,
160
- column: referenceField,
161
- onDelete: existing.references?.onDelete ?? options.onDelete,
162
- onUpdate: existing.references?.onUpdate ?? options.onUpdate,
163
- },
164
- };
165
- return;
166
- }
167
-
168
- table.columns[columnName] = {
169
- kind: "column",
170
- type: options.fkType ?? inferredType,
171
- sqlName: options.sqlName,
172
- notNull: options.notNull ?? true,
173
- nullable: options.notNull === false ? true : undefined,
174
- references: {
175
- table: referenceTable,
176
- column: referenceField,
177
- onDelete: options.onDelete,
178
- onUpdate: options.onUpdate,
179
- },
180
- };
181
- }
182
-
183
- function resolveNotNullFromNullableFlags(
184
- fieldPath: string,
185
- flags: { notNull?: boolean; nullable?: boolean },
186
- defaultNotNull: boolean,
187
- ): boolean {
188
- if (flags.notNull === true && flags.nullable === true) {
189
- throw new Error(
190
- `Invalid nullable configuration on '${fieldPath}': cannot set both notNull and nullable to true.`,
191
- );
192
- }
193
-
194
- if (flags.notNull === true) {
195
- return true;
196
- }
197
-
198
- if (flags.nullable === true) {
199
- return false;
200
- }
201
-
202
- if (flags.notNull === false) {
203
- return false;
204
- }
205
-
206
- return defaultNotNull;
207
- }
208
-
209
- type ManyToManyPair = {
210
- leftTable: string;
211
- leftReferenceField: string;
212
- rightTable: string;
213
- rightReferenceField: string;
214
- junctionTable: string;
215
- leftField: string;
216
- rightField: string;
217
- leftSqlName?: string;
218
- rightSqlName?: string;
219
- onDelete?: OneRelationDefinition["onDelete"];
220
- onUpdate?: OneRelationDefinition["onUpdate"];
221
- };
222
-
223
- function endpointKey(tableName: string, referenceField: string): string {
224
- return `${tableName}:${referenceField}`;
225
- }
226
-
227
- function normalizeManyToManyPairKey(
228
- sourceTable: string,
229
- sourceReferenceField: string,
230
- targetTable: string,
231
- targetReferenceField: string,
232
- ): {
233
- key: string;
234
- leftTable: string;
235
- leftReferenceField: string;
236
- rightTable: string;
237
- rightReferenceField: string;
238
- sourceIsLeft: boolean;
239
- } {
240
- const source = endpointKey(sourceTable, sourceReferenceField);
241
- const target = endpointKey(targetTable, targetReferenceField);
242
- const sourceIsLeft = source <= target;
243
-
244
- if (sourceIsLeft) {
245
- return {
246
- key: `${source}|${target}`,
247
- leftTable: sourceTable,
248
- leftReferenceField: sourceReferenceField,
249
- rightTable: targetTable,
250
- rightReferenceField: targetReferenceField,
251
- sourceIsLeft: true,
252
- };
253
- }
254
-
255
- return {
256
- key: `${target}|${source}`,
257
- leftTable: targetTable,
258
- leftReferenceField: targetReferenceField,
259
- rightTable: sourceTable,
260
- rightReferenceField: sourceReferenceField,
261
- sourceIsLeft: false,
262
- };
263
- }
264
-
265
- function defaultManyToManyJunctionTable(
266
- leftTable: string,
267
- rightTable: string,
268
- ): string {
269
- return `${leftTable}${toPascalCase(rightTable)}Links`;
270
- }
271
-
272
- function defaultManyToManyFieldName(
273
- ownerTable: string,
274
- otherTable: string,
275
- suffix: "source" | "target",
276
- ): string {
277
- const ownerSingular = singularize(ownerTable);
278
- const otherSingular = singularize(otherTable);
279
-
280
- if (ownerSingular === otherSingular) {
281
- return `${suffix}${toPascalCase(ownerSingular)}Id`;
282
- }
283
-
284
- return `${ownerSingular}Id`;
285
- }
286
-
287
- function mergeManyToManyPairConfig(
288
- existing: ManyToManyPair,
289
- incoming: ManyToManyPair,
290
- pairKey: string,
291
- ): ManyToManyPair {
292
- if (existing.junctionTable !== incoming.junctionTable) {
293
- throw new Error(
294
- `manyToMany pair '${pairKey}' has conflicting junctionTable values ('${existing.junctionTable}' vs '${incoming.junctionTable}').`,
295
- );
296
- }
297
-
298
- if (existing.leftField !== incoming.leftField) {
299
- throw new Error(
300
- `manyToMany pair '${pairKey}' has conflicting left field values ('${existing.leftField}' vs '${incoming.leftField}').`,
301
- );
302
- }
303
-
304
- if (existing.rightField !== incoming.rightField) {
305
- throw new Error(
306
- `manyToMany pair '${pairKey}' has conflicting right field values ('${existing.rightField}' vs '${incoming.rightField}').`,
307
- );
308
- }
309
-
310
- if (existing.leftSqlName !== incoming.leftSqlName) {
311
- throw new Error(
312
- `manyToMany pair '${pairKey}' has conflicting left sql name values ('${existing.leftSqlName}' vs '${incoming.leftSqlName}').`,
313
- );
314
- }
315
-
316
- if (existing.rightSqlName !== incoming.rightSqlName) {
317
- throw new Error(
318
- `manyToMany pair '${pairKey}' has conflicting right sql name values ('${existing.rightSqlName}' vs '${incoming.rightSqlName}').`,
319
- );
320
- }
321
-
322
- if (existing.onDelete !== incoming.onDelete) {
323
- throw new Error(
324
- `manyToMany pair '${pairKey}' has conflicting onDelete values ('${existing.onDelete}' vs '${incoming.onDelete}').`,
325
- );
326
- }
327
-
328
- if (existing.onUpdate !== incoming.onUpdate) {
329
- throw new Error(
330
- `manyToMany pair '${pairKey}' has conflicting onUpdate values ('${existing.onUpdate}' vs '${incoming.onUpdate}').`,
331
- );
332
- }
333
-
334
- return existing;
335
- }
336
-
337
- function normalizeManyToManyRelations(definition: SchemaDefinition): void {
338
- const pairMap = new Map<string, ManyToManyPair>();
339
-
340
- for (const [sourceTableName, sourceTable] of Object.entries(
341
- definition.tables,
342
- )) {
343
- for (const relation of Object.values(sourceTable.relations)) {
344
- if (relation.relation !== "manyToMany") {
345
- continue;
346
- }
347
-
348
- const sourceReferenceField = relation.referenceField ?? "id";
349
- const targetReferenceField = relation.targetReferenceField ?? "id";
350
-
351
- const normalizedPair = normalizeManyToManyPairKey(
352
- sourceTableName,
353
- sourceReferenceField,
354
- relation.targetTable,
355
- targetReferenceField,
356
- );
357
-
358
- const defaultLeftField = defaultManyToManyFieldName(
359
- normalizedPair.leftTable,
360
- normalizedPair.rightTable,
361
- "source",
362
- );
363
- const defaultRightField = defaultManyToManyFieldName(
364
- normalizedPair.rightTable,
365
- normalizedPair.leftTable,
366
- "target",
367
- );
368
-
369
- const pair: ManyToManyPair = {
370
- leftTable: normalizedPair.leftTable,
371
- leftReferenceField: normalizedPair.leftReferenceField,
372
- rightTable: normalizedPair.rightTable,
373
- rightReferenceField: normalizedPair.rightReferenceField,
374
- junctionTable:
375
- relation.junctionTable ??
376
- defaultManyToManyJunctionTable(
377
- normalizedPair.leftTable,
378
- normalizedPair.rightTable,
379
- ),
380
- leftField: normalizedPair.sourceIsLeft
381
- ? (relation.sourceField ?? defaultLeftField)
382
- : (relation.targetField ?? defaultLeftField),
383
- rightField: normalizedPair.sourceIsLeft
384
- ? (relation.targetField ?? defaultRightField)
385
- : (relation.sourceField ?? defaultRightField),
386
- leftSqlName: normalizedPair.sourceIsLeft
387
- ? relation.sourceSqlName
388
- : relation.targetSqlName,
389
- rightSqlName: normalizedPair.sourceIsLeft
390
- ? relation.targetSqlName
391
- : relation.sourceSqlName,
392
- onDelete: relation.onDelete,
393
- onUpdate: relation.onUpdate,
394
- };
395
-
396
- if (pair.leftField === pair.rightField) {
397
- throw new Error(
398
- `manyToMany pair '${normalizedPair.key}' resolves to duplicate junction fields '${pair.leftField}'. Set sourceField/targetField explicitly.`,
399
- );
400
- }
401
-
402
- const existing = pairMap.get(normalizedPair.key);
403
- if (existing) {
404
- mergeManyToManyPairConfig(existing, pair, normalizedPair.key);
405
- } else {
406
- pairMap.set(normalizedPair.key, pair);
407
- }
408
-
409
- relation.referenceField = sourceReferenceField;
410
- relation.targetReferenceField = targetReferenceField;
411
- relation.junctionTable = pair.junctionTable;
412
- relation.sourceField = normalizedPair.sourceIsLeft
413
- ? pair.leftField
414
- : pair.rightField;
415
- relation.targetField = normalizedPair.sourceIsLeft
416
- ? pair.rightField
417
- : pair.leftField;
418
- relation.sourceSqlName = normalizedPair.sourceIsLeft
419
- ? pair.leftSqlName
420
- : pair.rightSqlName;
421
- relation.targetSqlName = normalizedPair.sourceIsLeft
422
- ? pair.rightSqlName
423
- : pair.leftSqlName;
424
- }
425
- }
426
-
427
- for (const pair of pairMap.values()) {
428
- if (pair.junctionTable in definition.tables) {
429
- throw new Error(
430
- `manyToMany auto junction table '${pair.junctionTable}' conflicts with an existing table. Set a different junctionTable name.`,
431
- );
432
- }
433
-
434
- const leftType =
435
- getLocalReferenceType(
436
- definition,
437
- pair.leftTable,
438
- pair.leftReferenceField,
439
- ) ?? "string";
440
- const rightType =
441
- getLocalReferenceType(
442
- definition,
443
- pair.rightTable,
444
- pair.rightReferenceField,
445
- ) ?? "string";
446
-
447
- definition.tables[pair.junctionTable] = {
448
- kind: "table",
449
- columns: {
450
- [pair.leftField]: {
451
- kind: "column",
452
- type: leftType,
453
- sqlName: pair.leftSqlName,
454
- notNull: true,
455
- nullable: false,
456
- references: {
457
- table: pair.leftTable,
458
- column: pair.leftReferenceField,
459
- onDelete: pair.onDelete,
460
- onUpdate: pair.onUpdate,
461
- },
462
- index: true,
463
- },
464
- [pair.rightField]: {
465
- kind: "column",
466
- type: rightType,
467
- sqlName: pair.rightSqlName,
468
- notNull: true,
469
- nullable: false,
470
- references: {
471
- table: pair.rightTable,
472
- column: pair.rightReferenceField,
473
- onDelete: pair.onDelete,
474
- onUpdate: pair.onUpdate,
475
- },
476
- index: true,
477
- },
478
- },
479
- relations: {
480
- [pair.leftTable]: {
481
- kind: "relation",
482
- relation: "one",
483
- targetTable: pair.leftTable,
484
- field: pair.leftField,
485
- referenceField: pair.leftReferenceField,
486
- },
487
- [pair.rightTable]: {
488
- kind: "relation",
489
- relation: "one",
490
- targetTable: pair.rightTable,
491
- field: pair.rightField,
492
- referenceField: pair.rightReferenceField,
493
- },
494
- },
495
- };
496
- }
497
- }
498
-
499
- function validateNullableFlags(definition: SchemaDefinition): void {
500
- for (const [tableName, table] of Object.entries(definition.tables)) {
501
- for (const [columnName, column] of Object.entries(table.columns)) {
502
- if (column.notNull === true && column.nullable === true) {
503
- throw new Error(
504
- `Invalid nullable configuration on '${tableName}.${columnName}': cannot set both notNull and nullable to true.`,
505
- );
506
- }
507
- }
508
-
509
- for (const [relationName, relation] of Object.entries(table.relations)) {
510
- if (
511
- relation.relation !== "manyToMany" &&
512
- relation.notNull === true &&
513
- relation.nullable === true
514
- ) {
515
- throw new Error(
516
- `Invalid nullable configuration on '${tableName}.${relationName}': cannot set both notNull and nullable to true.`,
517
- );
518
- }
519
- }
520
- }
521
- }
522
-
523
- function normalizeSchemaDefinition(
524
- definition: SchemaDefinition,
525
- ): SchemaDefinition {
526
- validateNullableFlags(definition);
527
- const normalized = cloneSchemaDefinition(definition);
528
-
529
- for (const [tableName, table] of Object.entries(normalized.tables)) {
530
- for (const [relationName, relation] of Object.entries(table.relations)) {
531
- if (relation.relation !== "one") {
532
- continue;
533
- }
534
-
535
- const referenceField = relation.referenceField ?? "id";
536
- const inferredField = relation.field ?? `${relationName}Id`;
537
- relation.field = inferredField;
538
-
539
- const inferredType =
540
- getLocalReferenceType(
541
- normalized,
542
- relation.targetTable,
543
- referenceField,
544
- ) ??
545
- relation.fkType ??
546
- "string";
547
-
548
- ensureInferredReferenceColumn(
549
- tableName,
550
- table,
551
- inferredField,
552
- relation.targetTable,
553
- referenceField,
554
- {
555
- fkType: relation.fkType,
556
- sqlName: relation.sqlName,
557
- notNull: resolveNotNullFromNullableFlags(
558
- `${tableName}.${relationName}`,
559
- relation,
560
- true,
561
- ),
562
- onDelete: relation.onDelete,
563
- onUpdate: relation.onUpdate,
564
- },
565
- inferredType,
566
- );
567
- }
568
- }
569
-
570
- for (const [sourceTableName, sourceTable] of Object.entries(
571
- normalized.tables,
572
- )) {
573
- for (const relation of Object.values(sourceTable.relations)) {
574
- if (relation.relation !== "many") {
575
- continue;
576
- }
577
-
578
- const targetTable = normalized.tables[relation.targetTable];
579
- if (!targetTable) {
580
- continue;
581
- }
582
-
583
- const referenceField = relation.referenceField ?? "id";
584
- const inferredField =
585
- relation.field ?? `${singularize(sourceTableName)}Id`;
586
- relation.field = inferredField;
587
- const inferredType =
588
- getLocalReferenceType(normalized, sourceTableName, referenceField) ??
589
- relation.fkType ??
590
- "string";
591
-
592
- ensureInferredReferenceColumn(
593
- relation.targetTable,
594
- targetTable,
595
- inferredField,
596
- sourceTableName,
597
- referenceField,
598
- {
599
- fkType: relation.fkType,
600
- sqlName: relation.sqlName,
601
- notNull: resolveNotNullFromNullableFlags(
602
- `${sourceTableName}.${relation.targetTable}`,
603
- relation,
604
- true,
605
- ),
606
- onDelete: relation.onDelete,
607
- onUpdate: relation.onUpdate,
608
- },
609
- inferredType,
610
- );
611
- }
612
- }
613
-
614
- normalizeManyToManyRelations(normalized);
615
-
616
- return normalized;
617
- }
618
-
619
- function isOptionalInsertColumn(column: ColumnDefinition): boolean {
620
- return (
621
- column.primaryKey === true ||
622
- column.notNull !== true ||
623
- column.autoIncrement === true ||
624
- column.sqlDefault !== undefined ||
625
- column.runtimeDefaultFn !== undefined
626
- );
627
- }
628
-
629
- function isNullableSelectColumn(column: ColumnDefinition): boolean {
630
- return column.notNull !== true;
631
- }
632
-
633
- function drizzleBaseColumn(
634
- fieldName: string,
635
- column: ColumnDefinition,
636
- strategy: "camelToSnake",
637
- ): string {
638
- const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
639
- const needsExplicitName = resolvedSqlName !== fieldName;
640
-
641
- if (column.type === "int") {
642
- return needsExplicitName ? `t.int(${quote(resolvedSqlName)})` : "t.int()";
643
- }
644
-
645
- if (column.type === "string") {
646
- if (column.length !== undefined) {
647
- if (needsExplicitName) {
648
- return `t.text(${quote(resolvedSqlName)}, { length: ${column.length} })`;
649
- }
650
- return `t.text({ length: ${column.length} })`;
651
- }
652
-
653
- return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
654
- }
655
-
656
- if (column.type === "boolean") {
657
- if (needsExplicitName) {
658
- return `t.int(${quote(resolvedSqlName)}, { mode: "boolean" })`;
659
- }
660
- return 't.int({ mode: "boolean" })';
661
- }
662
-
663
- if (column.type === "date") {
664
- if (needsExplicitName) {
665
- return `t.int(${quote(resolvedSqlName)}, { mode: "timestamp_ms" })`;
666
- }
667
- return 't.int({ mode: "timestamp_ms" })';
668
- }
669
-
670
- if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
671
- if (column.isArray) {
672
- return needsExplicitName
673
- ? `t.text(${quote(resolvedSqlName)}).array()`
674
- : "t.text().array()";
675
- }
676
- return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
677
- }
678
-
679
- if (column.type === "json" && column.jsonShape) {
680
- const tsType = jsonShapeToTypeScript(column.jsonShape);
681
- const base = needsExplicitName
682
- ? `t.text(${quote(resolvedSqlName)}, { mode: "json" })`
683
- : `t.text({ mode: "json" })`;
684
- return `${base}.$type<${tsType}>()`;
685
- }
686
-
687
- if (strategy === "camelToSnake") {
688
- return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
689
- }
690
-
691
- return "t.text()";
692
- }
693
-
694
- function resolveOneRelationReference(
695
- tableName: string,
696
- table: TableDefinition,
697
- relation: OneRelationDefinition,
698
- ): { sourceField: string; targetField: string } {
699
- const sourceField = relation.field;
700
- const targetField = relation.referenceField ?? "id";
701
-
702
- if (!sourceField) {
703
- throw new Error(
704
- `Relation on '${tableName}' targeting '${relation.targetTable}' is missing a local field.`,
705
- );
706
- }
707
-
708
- if (!(sourceField in table.columns)) {
709
- throw new Error(
710
- `Relation '${tableName}.${sourceField}' references missing local field '${sourceField}'.`,
711
- );
712
- }
713
-
714
- return {
715
- sourceField,
716
- targetField,
717
- };
718
- }
719
-
720
- function buildExternalTableImportLines(externals: Set<string>): string {
721
- if (externals.size === 0) {
722
- return "";
723
- }
724
- return `import { ${Array.from(externals).sort().join(", ")} } from \"./auth.schema\";\n`;
725
- }
726
-
727
- function getRelationImports(table: TableDefinition): string[] {
728
- return Object.values(table.relations)
729
- .filter((relation) => relation.relation === "one")
730
- .map((relation) => relation.targetTable);
731
- }
732
-
733
- function resolveColumnReference(
734
- fieldName: string,
735
- column: ColumnDefinition,
736
- table: TableDefinition,
737
- ): { tableName: string; fieldName: string } | undefined {
738
- if (column.references) {
739
- return {
740
- tableName: column.references.table,
741
- fieldName: column.references.column,
742
- };
743
- }
744
-
745
- const matchingOne = Object.values(table.relations).find((relation) => {
746
- return relation.relation === "one" && relation.field === fieldName;
747
- }) as OneRelationDefinition | undefined;
748
-
749
- if (!matchingOne) {
750
- return undefined;
751
- }
752
-
753
- return {
754
- tableName: matchingOne.targetTable,
755
- fieldName: matchingOne.referenceField ?? "id",
756
- };
757
- }
758
-
759
- function emitJsonColumnsMetadata(definition: SchemaDefinition): string {
760
- const tableEntries: string[] = [];
761
-
762
- for (const [tableName, table] of Object.entries(definition.tables)) {
763
- const columnEntries: string[] = [];
764
-
765
- for (const [fieldName, column] of Object.entries(table.columns)) {
766
- if (column.type !== "json" || !column.jsonShape) continue;
767
-
768
- const shapeStr = jsonShapeToRuntimeLiteral(column.jsonShape);
769
- columnEntries.push(
770
- `${quote(fieldName)}: { shape: ${shapeStr} },`,
771
- );
772
- }
773
-
774
- if (columnEntries.length === 0) continue;
775
-
776
- tableEntries.push(
777
- `${quote(tableName)}: {
778
- ${columnEntries.map((entry) => `\t\t${entry}`).join("\n")}
779
- },`,
780
- );
781
- }
782
-
783
- if (tableEntries.length === 0) {
784
- return "export const __appflareJsonColumns = {} as const;\n";
785
- }
786
-
787
- return `export const __appflareJsonColumns = {
788
- ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
789
- } as const;
790
- `;
791
- }
792
-
793
- function jsonShapeToRuntimeLiteral(shape: JsonShape): string {
794
- if (shape.kind === "array") {
795
- return `{ kind: "array", element: ${jsonShapeToRuntimeLiteral(shape.element)} }`;
796
- }
797
- if (shape.kind === "object") {
798
- const fields = Object.entries(shape.shape)
799
- .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToRuntimeLiteral(fieldShape)}`)
800
- .join(", ");
801
- return `{ kind: "object", shape: { ${fields} } }`;
802
- }
803
- return `{ kind: ${quote(shape.kind)} }`;
804
- }
805
-
806
- function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
807
- const tableEntries: string[] = [];
808
-
809
- for (const [tableName, table] of Object.entries(definition.tables)) {
810
- const relationEntries: string[] = [];
811
-
812
- for (const [relationName, relation] of Object.entries(table.relations)) {
813
- if (relation.relation !== "manyToMany") {
814
- continue;
815
- }
816
-
817
- if (!relation.junctionTable) {
818
- continue;
819
- }
820
-
821
- relationEntries.push(
822
- `${quote(relationName)}: {
823
- targetTable: ${quote(relation.targetTable)},
824
- junctionTable: ${quote(relation.junctionTable)},
825
- sourceField: ${quote(relation.sourceField ?? "")},
826
- targetField: ${quote(relation.targetField ?? "")},
827
- referenceField: ${quote(relation.referenceField ?? "id")},
828
- targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
829
- },`,
830
- );
831
- }
832
-
833
- if (relationEntries.length === 0) {
834
- continue;
835
- }
836
-
837
- tableEntries.push(
838
- `${quote(tableName)}: {
839
- ${relationEntries.map((entry) => `\t${entry}`).join("\n")}
840
- },`,
841
- );
842
- }
843
-
844
- if (tableEntries.length === 0) {
845
- return "export const __appflareManyToMany = {} as const;\n";
846
- }
847
-
848
- return `export const __appflareManyToMany = {
849
- ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
850
- } as const;
851
- `;
852
- }
853
-
854
- function emitRuntimeRelationMetadata(definition: SchemaDefinition): string {
855
- const tableEntries: string[] = [];
856
-
857
- for (const [tableName, table] of Object.entries(definition.tables)) {
858
- const relationEntries: string[] = [];
859
-
860
- for (const [relationName, relation] of Object.entries(table.relations)) {
861
- if (relation.relation === "one") {
862
- relationEntries.push(
863
- `${quote(relationName)}: {
864
- kind: "one",
865
- targetTable: ${quote(relation.targetTable)},
866
- sourceField: ${quote(relation.field ?? "")},
867
- referenceField: ${quote(relation.referenceField ?? "id")},
868
- },`,
869
- );
870
- continue;
871
- }
872
-
873
- if (relation.relation === "many") {
874
- relationEntries.push(
875
- `${quote(relationName)}: {
876
- kind: "many",
877
- targetTable: ${quote(relation.targetTable)},
878
- sourceField: ${quote(relation.field ?? "")},
879
- referenceField: ${quote(relation.referenceField ?? "id")},
880
- },`,
881
- );
882
- continue;
883
- }
884
-
885
- relationEntries.push(
886
- `${quote(relationName)}: {
887
- kind: "manyToMany",
888
- targetTable: ${quote(relation.targetTable)},
889
- junctionTable: ${quote(relation.junctionTable ?? "")},
890
- sourceField: ${quote(relation.sourceField ?? "")},
891
- targetField: ${quote(relation.targetField ?? "")},
892
- referenceField: ${quote(relation.referenceField ?? "id")},
893
- targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
894
- },`,
895
- );
896
- }
897
-
898
- if (relationEntries.length === 0) {
899
- continue;
900
- }
901
-
902
- tableEntries.push(
903
- `${quote(tableName)}: {
904
- ${relationEntries.map((entry) => `\t${entry}`).join("\n")}
905
- },`,
906
- );
907
- }
908
-
909
- if (tableEntries.length === 0) {
910
- return "export const __appflareRelations = {} as const;\n";
911
- }
912
-
913
- return `export const __appflareRelations = {
914
- ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
915
- } as const;
916
- `;
917
- }
918
-
919
- function emitDrizzleSchema(
920
- definition: SchemaDefinition,
921
- strategy: "camelToSnake",
922
- ): string {
923
- const localTables = new Set(Object.keys(definition.tables));
924
- const externalTables = new Set<string>();
925
-
926
- for (const table of Object.values(definition.tables)) {
927
- for (const relationTarget of getRelationImports(table)) {
928
- if (!localTables.has(relationTarget)) {
929
- externalTables.add(relationTarget);
930
- }
931
- }
932
- for (const column of Object.values(table.columns)) {
933
- if (column.references && !localTables.has(column.references.table)) {
934
- externalTables.add(column.references.table);
935
- }
936
- }
937
- }
938
-
939
- const enumColumns = new Map<string, { column: ColumnDefinition; tableName: string; fieldName: string }>();
940
- for (const [tableName, table] of Object.entries(definition.tables)) {
941
- for (const [fieldName, column] of Object.entries(table.columns)) {
942
- if (
943
- column.type === "enum" &&
944
- column.enumValues &&
945
- column.enumValues.length > 0
946
- ) {
947
- const key = column.enumRef ?? `${tableName}_${fieldName}`;
948
- if (!enumColumns.has(key)) {
949
- enumColumns.set(key, { column, tableName, fieldName });
950
- }
951
- }
952
- }
953
- }
954
-
955
- const enumTypeLines: string[] = [];
956
- const enumCustomTypeLines: string[] = [];
957
- for (const [key, info] of enumColumns.entries()) {
958
- const typeName = toPascalCase(key);
959
- const valuesStr = info.column.enumValues!.map((v) => `"${v}"`).join(" | ");
960
- enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
961
- enumCustomTypeLines.push(
962
- `export const ${typeName}Column = t.customType<{ data: ${typeName}; dataNotNull: ${typeName} }>({ dataType: () => "text" });`,
963
- );
964
- }
965
-
966
- const tableBlocks: string[] = [];
967
- const relationBlocks: string[] = [];
968
-
969
- for (const [tableName, table] of Object.entries(definition.tables)) {
970
- const sqlTableName = table.sqlName ?? toSnakeCase(tableName);
971
- const columnLines: string[] = [];
972
- const indexes: string[] = [];
973
-
974
- for (const [fieldName, column] of Object.entries(table.columns)) {
975
- let expr: string;
976
- if (
977
- column.type === "enum" &&
978
- column.enumValues &&
979
- column.enumValues.length > 0
980
- ) {
981
- const enumKey = column.enumRef ?? `${tableName}_${fieldName}`;
982
- const typeName = toPascalCase(enumKey);
983
- const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
984
- const needsExplicitName = resolvedSqlName !== fieldName;
985
- expr = needsExplicitName
986
- ? `${typeName}Column(${quote(resolvedSqlName)})`
987
- : `${typeName}Column()`;
988
- if (column.isArray) {
989
- expr += ".array()";
990
- }
991
- } else {
992
- expr = drizzleBaseColumn(fieldName, column, strategy);
993
- }
994
-
995
- if (column.uuidPrimaryKey) {
996
- expr += ".$defaultFn(() => crypto.randomUUID())";
997
- }
998
-
999
- if (column.primaryKey) {
1000
- expr += column.autoIncrement
1001
- ? ".primaryKey({ autoIncrement: true })"
1002
- : ".primaryKey()";
1003
- }
1004
- if (column.notNull) {
1005
- expr += ".notNull()";
1006
- }
1007
- if (column.sqlDefault !== undefined) {
1008
- expr += `.default(${toSqlLiteral(column.sqlDefault)})`;
1009
- }
1010
-
1011
- const reference = resolveColumnReference(fieldName, column, table);
1012
- if (reference) {
1013
- if (column.references?.onDelete || column.references?.onUpdate) {
1014
- const actions: string[] = [];
1015
- if (column.references.onDelete) {
1016
- actions.push(`onDelete: ${quote(column.references.onDelete)}`);
1017
- }
1018
- if (column.references.onUpdate) {
1019
- actions.push(`onUpdate: ${quote(column.references.onUpdate)}`);
1020
- }
1021
- expr += `.references(() => ${reference.tableName}.${reference.fieldName}, { ${actions.join(", ")} })`;
1022
- } else {
1023
- expr += `.references(() => ${reference.tableName}.${reference.fieldName})`;
1024
- }
1025
- }
1026
-
1027
- if (column.unique) {
1028
- const uniqueName =
1029
- typeof column.unique === "object" && column.unique.name
1030
- ? column.unique.name
1031
- : `${sqlTableName}_${toSnakeCase(fieldName)}_unique_idx`;
1032
- indexes.push(
1033
- `\t\tt.uniqueIndex(${quote(uniqueName)}).on(table.${fieldName})`,
1034
- );
1035
- }
1036
-
1037
- if (column.index) {
1038
- const indexName =
1039
- typeof column.index === "object" && column.index.name
1040
- ? column.index.name
1041
- : `${sqlTableName}_${toSnakeCase(fieldName)}_idx`;
1042
- indexes.push(`\t\tt.index(${quote(indexName)}).on(table.${fieldName})`);
1043
- }
1044
-
1045
- columnLines.push(`\t\t${fieldName}: ${expr},`);
1046
- }
1047
-
1048
- if (indexes.length > 0) {
1049
- tableBlocks.push(
1050
- `export const ${tableName} = table(\n\t${quote(sqlTableName)},\n\t{\n${columnLines.join("\n")}\n\t},\n\t(table) => [\n${indexes.join(",\n")}\n\t],\n);`,
1051
- );
1052
- } else {
1053
- tableBlocks.push(
1054
- `export const ${tableName} = table(${quote(sqlTableName)}, {\n${columnLines.join("\n")}\n\t});`,
1055
- );
1056
- }
1057
-
1058
- const oneRelations = Object.entries(table.relations).filter(
1059
- ([, relation]) => {
1060
- return relation.relation === "one";
1061
- },
1062
- ) as Array<[string, OneRelationDefinition]>;
1063
- const manyRelations = Object.entries(table.relations).filter(
1064
- ([, relation]) => {
1065
- return relation.relation === "many";
1066
- },
1067
- ) as Array<[string, ManyRelationDefinition]>;
1068
- const manyToManyRelations = Object.entries(table.relations).filter(
1069
- ([, relation]) => {
1070
- return relation.relation === "manyToMany";
1071
- },
1072
- ) as Array<[string, ManyToManyRelationDefinition]>;
1073
-
1074
- if (
1075
- oneRelations.length === 0 &&
1076
- manyRelations.length === 0 &&
1077
- manyToManyRelations.length === 0
1078
- ) {
1079
- continue;
1080
- }
1081
-
1082
- const relationLines: string[] = [];
1083
- for (const [relationName, relation] of oneRelations) {
1084
- const resolved = resolveOneRelationReference(tableName, table, relation);
1085
- relationLines.push(
1086
- `\t${relationName}: one(${relation.targetTable}, {\n\t\tfields: [${tableName}.${resolved.sourceField}],\n\t\treferences: [${relation.targetTable}.${resolved.targetField}],\n\t}),`,
1087
- );
1088
- }
1089
- for (const [relationName, relation] of manyRelations) {
1090
- relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
1091
- }
1092
- for (const [relationName, relation] of manyToManyRelations) {
1093
- if (!relation.junctionTable) {
1094
- throw new Error(
1095
- `manyToMany relation '${tableName}.${relationName}' is missing junctionTable after normalization.`,
1096
- );
1097
- }
1098
- relationLines.push(`\t${relationName}: many(${relation.junctionTable}),`);
1099
- }
1100
-
1101
- relationBlocks.push(
1102
- `export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
1103
- );
1104
- }
1105
-
1106
- return `import * as t from "drizzle-orm/sqlite-core";
1107
- import { sqliteTable as table } from "drizzle-orm/sqlite-core";
1108
- import { relations } from "drizzle-orm";
1109
- ${buildExternalTableImportLines(externalTables)}
1110
- ${enumTypeLines.join("\n")}
1111
- ${enumCustomTypeLines.join("\n")}
1112
-
1113
- ${tableBlocks.join("\n\n")}
1114
-
1115
- ${relationBlocks.join("\n\n")}
1116
-
1117
- ${emitJsonColumnsMetadata(definition)}
1118
-
1119
- ${emitManyToManyRuntimeMetadata(definition)}
1120
-
1121
- ${emitRuntimeRelationMetadata(definition)}
1122
- `;
1123
- }
1124
-
1125
- function jsonShapeToZod(shape: JsonShape): string {
1126
- if (shape.kind === "array") {
1127
- return `z.array(${jsonShapeToZod(shape.element)})`;
1128
- }
1129
- if (shape.kind === "object") {
1130
- const fields = Object.entries(shape.shape)
1131
- .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToZod(fieldShape)}`)
1132
- .join(", ");
1133
- return `z.object({ ${fields} })`;
1134
- }
1135
- if (shape.kind === "string") return "z.string()";
1136
- if (shape.kind === "number") return "z.number()";
1137
- if (shape.kind === "boolean") return "z.boolean()";
1138
- if (shape.kind === "date") return "z.date()";
1139
- return "z.unknown()";
1140
- }
1141
-
1142
- function zodSchemaExpression(
1143
- column: ColumnDefinition,
1144
- optional: boolean,
1145
- nullable: boolean,
1146
- ): string {
1147
- let expr = "z.unknown()";
1148
- if (column.type === "int") {
1149
- expr = "z.number().int()";
1150
- } else if (column.type === "string") {
1151
- expr = "z.string()";
1152
- if (column.length !== undefined) {
1153
- expr += `.max(${column.length})`;
1154
- }
1155
- } else if (column.type === "boolean") {
1156
- expr = "z.boolean()";
1157
- } else if (column.type === "date") {
1158
- expr = "z.date()";
1159
- } else if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1160
- const valuesStr = column.enumValues.map((v) => `"${v}"`).join(", ");
1161
- const enumZod = `z.enum([${valuesStr}])`;
1162
- expr = column.isArray ? `z.array(${enumZod})` : enumZod;
1163
- } else if (column.type === "json" && column.jsonShape) {
1164
- expr = jsonShapeToZod(column.jsonShape);
1165
- }
1166
-
1167
- if (optional) {
1168
- expr += ".optional()";
1169
- }
1170
-
1171
- if (nullable) {
1172
- expr += ".nullable()";
1173
- }
1174
-
1175
- return expr;
1176
- }
1177
-
1178
- function emitZodSchemas(definition: SchemaDefinition): string {
1179
- const blocks: string[] = [];
1180
-
1181
- for (const [tableName, table] of Object.entries(definition.tables)) {
1182
- const pascal = toPascalCase(tableName);
1183
- const insertLines: string[] = [];
1184
- const selectLines: string[] = [];
1185
-
1186
- for (const [fieldName, column] of Object.entries(table.columns)) {
1187
- insertLines.push(
1188
- `\t${fieldName}: ${zodSchemaExpression(column, isOptionalInsertColumn(column), isNullableSelectColumn(column))},`,
1189
- );
1190
- selectLines.push(
1191
- `\t${fieldName}: ${zodSchemaExpression(column, isNullableSelectColumn(column), isNullableSelectColumn(column))},`,
1192
- );
1193
- }
1194
-
1195
- blocks.push(`export const ${tableName}InsertSchema = z.object({\n${insertLines.join("\n")}\n});
1196
- export const ${tableName}SelectSchema = z.object({\n${selectLines.join("\n")}\n});
1197
-
1198
- export type ${pascal}Insert = z.infer<typeof ${tableName}InsertSchema>;
1199
- export type ${pascal}Select = z.infer<typeof ${tableName}SelectSchema>;
1200
- `);
1201
- }
1202
-
1203
- return `import { z } from "zod";
1204
-
1205
- ${blocks.join("\n")}`;
1206
- }
1207
-
1208
- function jsonShapeToTypeScript(shape: JsonShape): string {
1209
- if (shape.kind === "array") {
1210
- return `Array<${jsonShapeToTypeScript(shape.element)}>`;
1211
- }
1212
- if (shape.kind === "object") {
1213
- const fields = Object.entries(shape.shape)
1214
- .map(([key, fieldShape]) => `${key}: ${jsonShapeToTypeScript(fieldShape)}`)
1215
- .join("; ");
1216
- return `{ ${fields} }`;
1217
- }
1218
- if (shape.kind === "string") return "string";
1219
- if (shape.kind === "number") return "number";
1220
- if (shape.kind === "boolean") return "boolean";
1221
- if (shape.kind === "date") return "Date";
1222
- return "unknown";
1223
- }
1224
-
1225
- function toTypeScriptType(column: ColumnDefinition): string {
1226
- if (column.type === "int") {
1227
- return "number";
1228
- }
1229
- if (column.type === "string") {
1230
- return "string";
1231
- }
1232
- if (column.type === "boolean") {
1233
- return "boolean";
1234
- }
1235
- if (column.type === "date") {
1236
- return "Date";
1237
- }
1238
- if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1239
- const union = column.enumValues.map((v) => `"${v}"`).join(" | ");
1240
- if (column.isArray) {
1241
- return `Array<${union}>`;
1242
- }
1243
- return union;
1244
- }
1245
- if (column.type === "json" && column.jsonShape) {
1246
- return jsonShapeToTypeScript(column.jsonShape);
1247
- }
1248
- return "unknown";
1249
- }
1250
-
1251
- function emitTypes(definition: SchemaDefinition): string {
1252
- const enumTypeLines: string[] = [];
1253
- for (const [name, enumDef] of Object.entries(definition.enums ?? {})) {
1254
- const typeName = toPascalCase(name);
1255
- const valuesStr = enumDef.values.map((v) => `"${v}"`).join(" | ");
1256
- enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
1257
- }
1258
-
1259
- const lines: string[] = [];
1260
-
1261
- for (const [tableName, table] of Object.entries(definition.tables)) {
1262
- const pascal = toPascalCase(tableName);
1263
- const selectFields: string[] = [];
1264
- const insertFields: string[] = [];
1265
-
1266
- for (const [fieldName, column] of Object.entries(table.columns)) {
1267
- const tsType = toTypeScriptType(column);
1268
- const nullableSuffix = isNullableSelectColumn(column) ? " | null" : "";
1269
- selectFields.push(
1270
- `\t${fieldName}${isNullableSelectColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
1271
- );
1272
- insertFields.push(
1273
- `\t${fieldName}${isOptionalInsertColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
1274
- );
1275
- }
1276
-
1277
- lines.push(`export type ${pascal} = {\n${selectFields.join("\n")}\n};
1278
-
1279
- export type New${pascal} = {\n${insertFields.join("\n")}\n};`);
1280
- }
1281
-
1282
- return `${enumTypeLines.join("\n")}
1283
- ${lines.join("\n\n")}
1284
- `;
1285
- }
1286
-
1287
- function findSchemaExport(
1288
- moduleExports: Record<string, unknown>,
1289
- exportName?: string,
1290
- ): SchemaDefinition {
1291
- if (exportName) {
1292
- const explicit = moduleExports[exportName];
1293
- if (!isSchemaDefinition(explicit)) {
1294
- throw new Error(
1295
- `schemaDsl.exportName '${exportName}' does not point to a schema() export.`,
1296
- );
1297
- }
1298
- return explicit;
1299
- }
1300
-
1301
- for (const candidate of Object.values(moduleExports)) {
1302
- if (isSchemaDefinition(candidate)) {
1303
- return candidate;
1304
- }
1305
- }
1306
-
1307
- throw new Error(
1308
- "No schema() export found in schemaDsl entry module. Set schemaDsl.exportName to the correct export.",
1309
- );
1310
- }
1311
-
1312
- export async function compileSchemaDsl(
1313
- loadedConfig: LoadedAppflareConfig,
1314
- ): Promise<CompiledSchemaArtifacts | undefined> {
1315
- const schemaDsl = loadedConfig.config.schemaDsl;
1316
- if (!schemaDsl) {
1317
- return undefined;
1318
- }
1319
-
1320
- const namingStrategy = schemaDsl.namingStrategy ?? "camelToSnake";
1321
- const entryPath = resolve(loadedConfig.configDir, schemaDsl.entry);
1322
- const outSchemaPath = resolve(
1323
- loadedConfig.configDir,
1324
- schemaDsl.outFile ?? resolve(loadedConfig.outDirAbs, "schema.compiled.ts"),
1325
- );
1326
- const outTypesPath = resolve(
1327
- loadedConfig.configDir,
1328
- schemaDsl.typesOutFile ??
1329
- resolve(loadedConfig.outDirAbs, "schema.types.ts"),
1330
- );
1331
- const outZodPath = resolve(
1332
- loadedConfig.configDir,
1333
- schemaDsl.zodOutFile ?? resolve(loadedConfig.outDirAbs, "schema.zod.ts"),
1334
- );
1335
-
1336
- const moduleUrl = `${pathToFileURL(entryPath).href}?t=${Date.now()}`;
1337
- const schemaModule = (await import(moduleUrl)) as Record<string, unknown>;
1338
- const schemaDefinition = findSchemaExport(schemaModule, schemaDsl.exportName);
1339
- const normalizedSchema = normalizeSchemaDefinition(schemaDefinition);
1340
-
1341
- await Promise.all([
1342
- mkdir(dirname(outSchemaPath), { recursive: true }),
1343
- mkdir(dirname(outTypesPath), { recursive: true }),
1344
- mkdir(dirname(outZodPath), { recursive: true }),
1345
- ]);
1346
-
1347
- const drizzleSchemaSource = emitDrizzleSchema(
1348
- normalizedSchema,
1349
- namingStrategy,
1350
- );
1351
- const typesSource = emitTypes(normalizedSchema);
1352
- const zodSource = emitZodSchemas(normalizedSchema);
1353
-
1354
- await Promise.all([
1355
- Bun.write(outSchemaPath, drizzleSchemaSource),
1356
- Bun.write(outTypesPath, typesSource),
1357
- Bun.write(outZodPath, zodSource),
1358
- ]);
1359
-
1360
- return {
1361
- schemaPath: outSchemaPath,
1362
- typesPath: outTypesPath,
1363
- zodPath: outZodPath,
1364
- tableNames: Object.keys(normalizedSchema.tables),
1365
- };
1366
- }
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import type {
5
+ ColumnDefinition,
6
+ ColumnType,
7
+ JsonShape,
8
+ ManyRelationDefinition,
9
+ ManyToManyRelationDefinition,
10
+ OneRelationDefinition,
11
+ SchemaDefinition,
12
+ TableDefinition,
13
+ } from "../schema";
14
+ import { isSchemaDefinition } from "../schema";
15
+ import type { LoadedAppflareConfig } from "./types";
16
+
17
+ export type CompiledSchemaArtifacts = {
18
+ schemaPath: string;
19
+ typesPath: string;
20
+ zodPath: string;
21
+ tableNames: string[];
22
+ };
23
+
24
+ function toSnakeCase(value: string): string {
25
+ return value
26
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
27
+ .replace(/[\s-]+/g, "_")
28
+ .toLowerCase();
29
+ }
30
+
31
+ function toPascalCase(value: string): string {
32
+ return value
33
+ .replace(/[_-]+/g, " ")
34
+ .replace(/\s+(.)/g, (_match, char: string) => char.toUpperCase())
35
+ .replace(/\s/g, "")
36
+ .replace(/^(.)/, (_match, char: string) => char.toUpperCase());
37
+ }
38
+
39
+ function singularize(value: string): string {
40
+ if (value.endsWith("ies")) {
41
+ return `${value.slice(0, -3)}y`;
42
+ }
43
+ if (value.endsWith("ses")) {
44
+ return value.slice(0, -2);
45
+ }
46
+ if (value.endsWith("s") && value.length > 1) {
47
+ return value.slice(0, -1);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ function quote(value: string): string {
53
+ return JSON.stringify(value);
54
+ }
55
+
56
+ function toSqlLiteral(value: unknown): string {
57
+ if (typeof value === "string") {
58
+ return quote(value);
59
+ }
60
+ if (typeof value === "number" || typeof value === "boolean") {
61
+ return String(value);
62
+ }
63
+ if (value === null) {
64
+ return "null";
65
+ }
66
+ if (value instanceof Date) {
67
+ return quote(value.toISOString());
68
+ }
69
+ if (typeof value === "object" && value !== null) {
70
+ return JSON.stringify(value);
71
+ }
72
+ throw new Error(
73
+ `Unsupported SQL default value '${String(value)}'. Use string, number, boolean, null, Date, array, or object.`,
74
+ );
75
+ }
76
+
77
+ function cloneSchemaDefinition(definition: SchemaDefinition): SchemaDefinition {
78
+ return {
79
+ kind: "schema",
80
+ tables: Object.fromEntries(
81
+ Object.entries(definition.tables).map(([tableName, table]) => {
82
+ return [
83
+ tableName,
84
+ {
85
+ kind: "table",
86
+ sqlName: table.sqlName,
87
+ columns: Object.fromEntries(
88
+ Object.entries(table.columns).map(([columnName, column]) => {
89
+ return [columnName, { ...column }];
90
+ }),
91
+ ),
92
+ relations: Object.fromEntries(
93
+ Object.entries(table.relations).map(
94
+ ([relationName, relation]) => {
95
+ return [relationName, { ...relation }];
96
+ },
97
+ ),
98
+ ),
99
+ } satisfies TableDefinition,
100
+ ];
101
+ }),
102
+ ),
103
+ enums: Object.fromEntries(
104
+ Object.entries(definition.enums ?? {}).map(([name, enumDef]) => {
105
+ return [name, { ...enumDef }];
106
+ }),
107
+ ),
108
+ };
109
+ }
110
+
111
+ function getLocalReferenceType(
112
+ definition: SchemaDefinition,
113
+ targetTable: string,
114
+ referenceField: string,
115
+ ): ColumnType | undefined {
116
+ const table = definition.tables[targetTable];
117
+ if (!table) {
118
+ return undefined;
119
+ }
120
+ const targetColumn = table.columns[referenceField];
121
+ return targetColumn?.type;
122
+ }
123
+
124
+ function ensureInferredReferenceColumn(
125
+ tableName: string,
126
+ table: TableDefinition,
127
+ columnName: string,
128
+ referenceTable: string,
129
+ referenceField: string,
130
+ options: {
131
+ fkType?: ColumnType;
132
+ sqlName?: string;
133
+ notNull?: boolean;
134
+ onDelete?: OneRelationDefinition["onDelete"];
135
+ onUpdate?: OneRelationDefinition["onUpdate"];
136
+ },
137
+ inferredType: ColumnType,
138
+ ): void {
139
+ const existing = table.columns[columnName];
140
+ if (existing) {
141
+ if (
142
+ existing.references &&
143
+ (existing.references.table !== referenceTable ||
144
+ existing.references.column !== referenceField)
145
+ ) {
146
+ throw new Error(
147
+ `Inferred relation '${tableName}.${columnName}' conflicts with explicit references(${existing.references.table}.${existing.references.column}).`,
148
+ );
149
+ }
150
+
151
+ table.columns[columnName] = {
152
+ ...existing,
153
+ notNull:
154
+ existing.notNull ??
155
+ (existing.nullable === true ? false : options.notNull),
156
+ nullable:
157
+ existing.nullable ?? (options.notNull === false ? true : undefined),
158
+ references: {
159
+ table: referenceTable,
160
+ column: referenceField,
161
+ onDelete: existing.references?.onDelete ?? options.onDelete,
162
+ onUpdate: existing.references?.onUpdate ?? options.onUpdate,
163
+ },
164
+ };
165
+ return;
166
+ }
167
+
168
+ table.columns[columnName] = {
169
+ kind: "column",
170
+ type: options.fkType ?? inferredType,
171
+ sqlName: options.sqlName,
172
+ notNull: options.notNull ?? true,
173
+ nullable: options.notNull === false ? true : undefined,
174
+ references: {
175
+ table: referenceTable,
176
+ column: referenceField,
177
+ onDelete: options.onDelete,
178
+ onUpdate: options.onUpdate,
179
+ },
180
+ };
181
+ }
182
+
183
+ function resolveNotNullFromNullableFlags(
184
+ fieldPath: string,
185
+ flags: { notNull?: boolean; nullable?: boolean },
186
+ defaultNotNull: boolean,
187
+ ): boolean {
188
+ if (flags.notNull === true && flags.nullable === true) {
189
+ throw new Error(
190
+ `Invalid nullable configuration on '${fieldPath}': cannot set both notNull and nullable to true.`,
191
+ );
192
+ }
193
+
194
+ if (flags.notNull === true) {
195
+ return true;
196
+ }
197
+
198
+ if (flags.nullable === true) {
199
+ return false;
200
+ }
201
+
202
+ if (flags.notNull === false) {
203
+ return false;
204
+ }
205
+
206
+ return defaultNotNull;
207
+ }
208
+
209
+ type ManyToManyPair = {
210
+ leftTable: string;
211
+ leftReferenceField: string;
212
+ rightTable: string;
213
+ rightReferenceField: string;
214
+ junctionTable: string;
215
+ leftField: string;
216
+ rightField: string;
217
+ leftSqlName?: string;
218
+ rightSqlName?: string;
219
+ onDelete?: OneRelationDefinition["onDelete"];
220
+ onUpdate?: OneRelationDefinition["onUpdate"];
221
+ };
222
+
223
+ function endpointKey(tableName: string, referenceField: string): string {
224
+ return `${tableName}:${referenceField}`;
225
+ }
226
+
227
+ function normalizeManyToManyPairKey(
228
+ sourceTable: string,
229
+ sourceReferenceField: string,
230
+ targetTable: string,
231
+ targetReferenceField: string,
232
+ ): {
233
+ key: string;
234
+ leftTable: string;
235
+ leftReferenceField: string;
236
+ rightTable: string;
237
+ rightReferenceField: string;
238
+ sourceIsLeft: boolean;
239
+ } {
240
+ const source = endpointKey(sourceTable, sourceReferenceField);
241
+ const target = endpointKey(targetTable, targetReferenceField);
242
+ const sourceIsLeft = source <= target;
243
+
244
+ if (sourceIsLeft) {
245
+ return {
246
+ key: `${source}|${target}`,
247
+ leftTable: sourceTable,
248
+ leftReferenceField: sourceReferenceField,
249
+ rightTable: targetTable,
250
+ rightReferenceField: targetReferenceField,
251
+ sourceIsLeft: true,
252
+ };
253
+ }
254
+
255
+ return {
256
+ key: `${target}|${source}`,
257
+ leftTable: targetTable,
258
+ leftReferenceField: targetReferenceField,
259
+ rightTable: sourceTable,
260
+ rightReferenceField: sourceReferenceField,
261
+ sourceIsLeft: false,
262
+ };
263
+ }
264
+
265
+ function defaultManyToManyJunctionTable(
266
+ leftTable: string,
267
+ rightTable: string,
268
+ ): string {
269
+ return `${leftTable}${toPascalCase(rightTable)}Links`;
270
+ }
271
+
272
+ function defaultManyToManyFieldName(
273
+ ownerTable: string,
274
+ otherTable: string,
275
+ suffix: "source" | "target",
276
+ ): string {
277
+ const ownerSingular = singularize(ownerTable);
278
+ const otherSingular = singularize(otherTable);
279
+
280
+ if (ownerSingular === otherSingular) {
281
+ return `${suffix}${toPascalCase(ownerSingular)}Id`;
282
+ }
283
+
284
+ return `${ownerSingular}Id`;
285
+ }
286
+
287
+ function mergeManyToManyPairConfig(
288
+ existing: ManyToManyPair,
289
+ incoming: ManyToManyPair,
290
+ pairKey: string,
291
+ ): ManyToManyPair {
292
+ if (existing.junctionTable !== incoming.junctionTable) {
293
+ throw new Error(
294
+ `manyToMany pair '${pairKey}' has conflicting junctionTable values ('${existing.junctionTable}' vs '${incoming.junctionTable}').`,
295
+ );
296
+ }
297
+
298
+ if (existing.leftField !== incoming.leftField) {
299
+ throw new Error(
300
+ `manyToMany pair '${pairKey}' has conflicting left field values ('${existing.leftField}' vs '${incoming.leftField}').`,
301
+ );
302
+ }
303
+
304
+ if (existing.rightField !== incoming.rightField) {
305
+ throw new Error(
306
+ `manyToMany pair '${pairKey}' has conflicting right field values ('${existing.rightField}' vs '${incoming.rightField}').`,
307
+ );
308
+ }
309
+
310
+ if (existing.leftSqlName !== incoming.leftSqlName) {
311
+ throw new Error(
312
+ `manyToMany pair '${pairKey}' has conflicting left sql name values ('${existing.leftSqlName}' vs '${incoming.leftSqlName}').`,
313
+ );
314
+ }
315
+
316
+ if (existing.rightSqlName !== incoming.rightSqlName) {
317
+ throw new Error(
318
+ `manyToMany pair '${pairKey}' has conflicting right sql name values ('${existing.rightSqlName}' vs '${incoming.rightSqlName}').`,
319
+ );
320
+ }
321
+
322
+ if (existing.onDelete !== incoming.onDelete) {
323
+ throw new Error(
324
+ `manyToMany pair '${pairKey}' has conflicting onDelete values ('${existing.onDelete}' vs '${incoming.onDelete}').`,
325
+ );
326
+ }
327
+
328
+ if (existing.onUpdate !== incoming.onUpdate) {
329
+ throw new Error(
330
+ `manyToMany pair '${pairKey}' has conflicting onUpdate values ('${existing.onUpdate}' vs '${incoming.onUpdate}').`,
331
+ );
332
+ }
333
+
334
+ return existing;
335
+ }
336
+
337
+ function normalizeManyToManyRelations(definition: SchemaDefinition): void {
338
+ const pairMap = new Map<string, ManyToManyPair>();
339
+
340
+ for (const [sourceTableName, sourceTable] of Object.entries(
341
+ definition.tables,
342
+ )) {
343
+ for (const relation of Object.values(sourceTable.relations)) {
344
+ if (relation.relation !== "manyToMany") {
345
+ continue;
346
+ }
347
+
348
+ const sourceReferenceField = relation.referenceField ?? "id";
349
+ const targetReferenceField = relation.targetReferenceField ?? "id";
350
+
351
+ const normalizedPair = normalizeManyToManyPairKey(
352
+ sourceTableName,
353
+ sourceReferenceField,
354
+ relation.targetTable,
355
+ targetReferenceField,
356
+ );
357
+
358
+ const defaultLeftField = defaultManyToManyFieldName(
359
+ normalizedPair.leftTable,
360
+ normalizedPair.rightTable,
361
+ "source",
362
+ );
363
+ const defaultRightField = defaultManyToManyFieldName(
364
+ normalizedPair.rightTable,
365
+ normalizedPair.leftTable,
366
+ "target",
367
+ );
368
+
369
+ const pair: ManyToManyPair = {
370
+ leftTable: normalizedPair.leftTable,
371
+ leftReferenceField: normalizedPair.leftReferenceField,
372
+ rightTable: normalizedPair.rightTable,
373
+ rightReferenceField: normalizedPair.rightReferenceField,
374
+ junctionTable:
375
+ relation.junctionTable ??
376
+ defaultManyToManyJunctionTable(
377
+ normalizedPair.leftTable,
378
+ normalizedPair.rightTable,
379
+ ),
380
+ leftField: normalizedPair.sourceIsLeft
381
+ ? (relation.sourceField ?? defaultLeftField)
382
+ : (relation.targetField ?? defaultLeftField),
383
+ rightField: normalizedPair.sourceIsLeft
384
+ ? (relation.targetField ?? defaultRightField)
385
+ : (relation.sourceField ?? defaultRightField),
386
+ leftSqlName: normalizedPair.sourceIsLeft
387
+ ? relation.sourceSqlName
388
+ : relation.targetSqlName,
389
+ rightSqlName: normalizedPair.sourceIsLeft
390
+ ? relation.targetSqlName
391
+ : relation.sourceSqlName,
392
+ onDelete: relation.onDelete,
393
+ onUpdate: relation.onUpdate,
394
+ };
395
+
396
+ if (pair.leftField === pair.rightField) {
397
+ throw new Error(
398
+ `manyToMany pair '${normalizedPair.key}' resolves to duplicate junction fields '${pair.leftField}'. Set sourceField/targetField explicitly.`,
399
+ );
400
+ }
401
+
402
+ const existing = pairMap.get(normalizedPair.key);
403
+ if (existing) {
404
+ mergeManyToManyPairConfig(existing, pair, normalizedPair.key);
405
+ } else {
406
+ pairMap.set(normalizedPair.key, pair);
407
+ }
408
+
409
+ relation.referenceField = sourceReferenceField;
410
+ relation.targetReferenceField = targetReferenceField;
411
+ relation.junctionTable = pair.junctionTable;
412
+ relation.sourceField = normalizedPair.sourceIsLeft
413
+ ? pair.leftField
414
+ : pair.rightField;
415
+ relation.targetField = normalizedPair.sourceIsLeft
416
+ ? pair.rightField
417
+ : pair.leftField;
418
+ relation.sourceSqlName = normalizedPair.sourceIsLeft
419
+ ? pair.leftSqlName
420
+ : pair.rightSqlName;
421
+ relation.targetSqlName = normalizedPair.sourceIsLeft
422
+ ? pair.rightSqlName
423
+ : pair.leftSqlName;
424
+ }
425
+ }
426
+
427
+ for (const pair of pairMap.values()) {
428
+ if (pair.junctionTable in definition.tables) {
429
+ throw new Error(
430
+ `manyToMany auto junction table '${pair.junctionTable}' conflicts with an existing table. Set a different junctionTable name.`,
431
+ );
432
+ }
433
+
434
+ const leftType =
435
+ getLocalReferenceType(
436
+ definition,
437
+ pair.leftTable,
438
+ pair.leftReferenceField,
439
+ ) ?? "string";
440
+ const rightType =
441
+ getLocalReferenceType(
442
+ definition,
443
+ pair.rightTable,
444
+ pair.rightReferenceField,
445
+ ) ?? "string";
446
+
447
+ definition.tables[pair.junctionTable] = {
448
+ kind: "table",
449
+ columns: {
450
+ [pair.leftField]: {
451
+ kind: "column",
452
+ type: leftType,
453
+ sqlName: pair.leftSqlName,
454
+ notNull: true,
455
+ nullable: false,
456
+ references: {
457
+ table: pair.leftTable,
458
+ column: pair.leftReferenceField,
459
+ onDelete: pair.onDelete,
460
+ onUpdate: pair.onUpdate,
461
+ },
462
+ index: true,
463
+ },
464
+ [pair.rightField]: {
465
+ kind: "column",
466
+ type: rightType,
467
+ sqlName: pair.rightSqlName,
468
+ notNull: true,
469
+ nullable: false,
470
+ references: {
471
+ table: pair.rightTable,
472
+ column: pair.rightReferenceField,
473
+ onDelete: pair.onDelete,
474
+ onUpdate: pair.onUpdate,
475
+ },
476
+ index: true,
477
+ },
478
+ },
479
+ relations: {
480
+ [pair.leftTable]: {
481
+ kind: "relation",
482
+ relation: "one",
483
+ targetTable: pair.leftTable,
484
+ field: pair.leftField,
485
+ referenceField: pair.leftReferenceField,
486
+ },
487
+ [pair.rightTable]: {
488
+ kind: "relation",
489
+ relation: "one",
490
+ targetTable: pair.rightTable,
491
+ field: pair.rightField,
492
+ referenceField: pair.rightReferenceField,
493
+ },
494
+ },
495
+ };
496
+ }
497
+ }
498
+
499
+ function validateNullableFlags(definition: SchemaDefinition): void {
500
+ for (const [tableName, table] of Object.entries(definition.tables)) {
501
+ for (const [columnName, column] of Object.entries(table.columns)) {
502
+ if (column.notNull === true && column.nullable === true) {
503
+ throw new Error(
504
+ `Invalid nullable configuration on '${tableName}.${columnName}': cannot set both notNull and nullable to true.`,
505
+ );
506
+ }
507
+ }
508
+
509
+ for (const [relationName, relation] of Object.entries(table.relations)) {
510
+ if (
511
+ relation.relation !== "manyToMany" &&
512
+ relation.notNull === true &&
513
+ relation.nullable === true
514
+ ) {
515
+ throw new Error(
516
+ `Invalid nullable configuration on '${tableName}.${relationName}': cannot set both notNull and nullable to true.`,
517
+ );
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ function normalizeSchemaDefinition(
524
+ definition: SchemaDefinition,
525
+ ): SchemaDefinition {
526
+ validateNullableFlags(definition);
527
+ const normalized = cloneSchemaDefinition(definition);
528
+
529
+ for (const [tableName, table] of Object.entries(normalized.tables)) {
530
+ for (const [relationName, relation] of Object.entries(table.relations)) {
531
+ if (relation.relation !== "one") {
532
+ continue;
533
+ }
534
+
535
+ const referenceField = relation.referenceField ?? "id";
536
+ const inferredField = relation.field ?? `${relationName}Id`;
537
+ relation.field = inferredField;
538
+
539
+ const inferredType =
540
+ getLocalReferenceType(
541
+ normalized,
542
+ relation.targetTable,
543
+ referenceField,
544
+ ) ??
545
+ relation.fkType ??
546
+ "string";
547
+
548
+ ensureInferredReferenceColumn(
549
+ tableName,
550
+ table,
551
+ inferredField,
552
+ relation.targetTable,
553
+ referenceField,
554
+ {
555
+ fkType: relation.fkType,
556
+ sqlName: relation.sqlName,
557
+ notNull: resolveNotNullFromNullableFlags(
558
+ `${tableName}.${relationName}`,
559
+ relation,
560
+ true,
561
+ ),
562
+ onDelete: relation.onDelete,
563
+ onUpdate: relation.onUpdate,
564
+ },
565
+ inferredType,
566
+ );
567
+ }
568
+ }
569
+
570
+ for (const [sourceTableName, sourceTable] of Object.entries(
571
+ normalized.tables,
572
+ )) {
573
+ for (const relation of Object.values(sourceTable.relations)) {
574
+ if (relation.relation !== "many") {
575
+ continue;
576
+ }
577
+
578
+ const targetTable = normalized.tables[relation.targetTable];
579
+ if (!targetTable) {
580
+ continue;
581
+ }
582
+
583
+ const referenceField = relation.referenceField ?? "id";
584
+ const inferredField =
585
+ relation.field ?? `${singularize(sourceTableName)}Id`;
586
+ relation.field = inferredField;
587
+ const inferredType =
588
+ getLocalReferenceType(normalized, sourceTableName, referenceField) ??
589
+ relation.fkType ??
590
+ "string";
591
+
592
+ ensureInferredReferenceColumn(
593
+ relation.targetTable,
594
+ targetTable,
595
+ inferredField,
596
+ sourceTableName,
597
+ referenceField,
598
+ {
599
+ fkType: relation.fkType,
600
+ sqlName: relation.sqlName,
601
+ notNull: resolveNotNullFromNullableFlags(
602
+ `${sourceTableName}.${relation.targetTable}`,
603
+ relation,
604
+ true,
605
+ ),
606
+ onDelete: relation.onDelete,
607
+ onUpdate: relation.onUpdate,
608
+ },
609
+ inferredType,
610
+ );
611
+ }
612
+ }
613
+
614
+ normalizeManyToManyRelations(normalized);
615
+
616
+ return normalized;
617
+ }
618
+
619
+ function isOptionalInsertColumn(column: ColumnDefinition): boolean {
620
+ return (
621
+ column.primaryKey === true ||
622
+ column.notNull !== true ||
623
+ column.autoIncrement === true ||
624
+ column.sqlDefault !== undefined ||
625
+ column.runtimeDefaultFn !== undefined
626
+ );
627
+ }
628
+
629
+ function isNullableSelectColumn(column: ColumnDefinition): boolean {
630
+ return column.notNull !== true;
631
+ }
632
+
633
+ function drizzleBaseColumn(
634
+ fieldName: string,
635
+ column: ColumnDefinition,
636
+ strategy: "camelToSnake",
637
+ ): string {
638
+ const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
639
+ const needsExplicitName = resolvedSqlName !== fieldName;
640
+
641
+ if (column.type === "int") {
642
+ return needsExplicitName ? `t.int(${quote(resolvedSqlName)})` : "t.int()";
643
+ }
644
+
645
+ if (column.type === "string") {
646
+ if (column.length !== undefined) {
647
+ if (needsExplicitName) {
648
+ return `t.text(${quote(resolvedSqlName)}, { length: ${column.length} })`;
649
+ }
650
+ return `t.text({ length: ${column.length} })`;
651
+ }
652
+
653
+ return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
654
+ }
655
+
656
+ if (column.type === "boolean") {
657
+ if (needsExplicitName) {
658
+ return `t.int(${quote(resolvedSqlName)}, { mode: "boolean" })`;
659
+ }
660
+ return 't.int({ mode: "boolean" })';
661
+ }
662
+
663
+ if (column.type === "date") {
664
+ if (needsExplicitName) {
665
+ return `t.int(${quote(resolvedSqlName)}, { mode: "timestamp_ms" })`;
666
+ }
667
+ return 't.int({ mode: "timestamp_ms" })';
668
+ }
669
+
670
+ if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
671
+ if (column.isArray) {
672
+ return needsExplicitName
673
+ ? `t.text(${quote(resolvedSqlName)}).array()`
674
+ : "t.text().array()";
675
+ }
676
+ return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
677
+ }
678
+
679
+ if (column.type === "json" && column.jsonShape) {
680
+ const tsType = jsonShapeToTypeScript(column.jsonShape);
681
+ const base = needsExplicitName
682
+ ? `t.text(${quote(resolvedSqlName)}, { mode: "json" })`
683
+ : `t.text({ mode: "json" })`;
684
+ return `${base}.$type<${tsType}>()`;
685
+ }
686
+
687
+ if (strategy === "camelToSnake") {
688
+ return needsExplicitName ? `t.text(${quote(resolvedSqlName)})` : "t.text()";
689
+ }
690
+
691
+ return "t.text()";
692
+ }
693
+
694
+ function resolveOneRelationReference(
695
+ tableName: string,
696
+ table: TableDefinition,
697
+ relation: OneRelationDefinition,
698
+ ): { sourceField: string; targetField: string } {
699
+ const sourceField = relation.field;
700
+ const targetField = relation.referenceField ?? "id";
701
+
702
+ if (!sourceField) {
703
+ throw new Error(
704
+ `Relation on '${tableName}' targeting '${relation.targetTable}' is missing a local field.`,
705
+ );
706
+ }
707
+
708
+ if (!(sourceField in table.columns)) {
709
+ throw new Error(
710
+ `Relation '${tableName}.${sourceField}' references missing local field '${sourceField}'.`,
711
+ );
712
+ }
713
+
714
+ return {
715
+ sourceField,
716
+ targetField,
717
+ };
718
+ }
719
+
720
+ function buildExternalTableImportLines(externals: Set<string>): string {
721
+ if (externals.size === 0) {
722
+ return "";
723
+ }
724
+ return `import { ${Array.from(externals).sort().join(", ")} } from \"./auth.schema\";\n`;
725
+ }
726
+
727
+ function getRelationImports(table: TableDefinition): string[] {
728
+ return Object.values(table.relations)
729
+ .filter((relation) => relation.relation === "one")
730
+ .map((relation) => relation.targetTable);
731
+ }
732
+
733
+ function resolveColumnReference(
734
+ fieldName: string,
735
+ column: ColumnDefinition,
736
+ table: TableDefinition,
737
+ ): { tableName: string; fieldName: string } | undefined {
738
+ if (column.references) {
739
+ return {
740
+ tableName: column.references.table,
741
+ fieldName: column.references.column,
742
+ };
743
+ }
744
+
745
+ const matchingOne = Object.values(table.relations).find((relation) => {
746
+ return relation.relation === "one" && relation.field === fieldName;
747
+ }) as OneRelationDefinition | undefined;
748
+
749
+ if (!matchingOne) {
750
+ return undefined;
751
+ }
752
+
753
+ return {
754
+ tableName: matchingOne.targetTable,
755
+ fieldName: matchingOne.referenceField ?? "id",
756
+ };
757
+ }
758
+
759
+ function emitJsonColumnsMetadata(definition: SchemaDefinition): string {
760
+ const tableEntries: string[] = [];
761
+
762
+ for (const [tableName, table] of Object.entries(definition.tables)) {
763
+ const columnEntries: string[] = [];
764
+
765
+ for (const [fieldName, column] of Object.entries(table.columns)) {
766
+ if (column.type !== "json" || !column.jsonShape) continue;
767
+
768
+ const shapeStr = jsonShapeToRuntimeLiteral(column.jsonShape);
769
+ columnEntries.push(
770
+ `${quote(fieldName)}: { shape: ${shapeStr} },`,
771
+ );
772
+ }
773
+
774
+ if (columnEntries.length === 0) continue;
775
+
776
+ tableEntries.push(
777
+ `${quote(tableName)}: {
778
+ ${columnEntries.map((entry) => `\t\t${entry}`).join("\n")}
779
+ },`,
780
+ );
781
+ }
782
+
783
+ if (tableEntries.length === 0) {
784
+ return "export const __appflareJsonColumns = {} as const;\n";
785
+ }
786
+
787
+ return `export const __appflareJsonColumns = {
788
+ ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
789
+ } as const;
790
+ `;
791
+ }
792
+
793
+ function jsonShapeToRuntimeLiteral(shape: JsonShape): string {
794
+ if (shape.kind === "array") {
795
+ return `{ kind: "array", element: ${jsonShapeToRuntimeLiteral(shape.element)} }`;
796
+ }
797
+ if (shape.kind === "object") {
798
+ const fields = Object.entries(shape.shape)
799
+ .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToRuntimeLiteral(fieldShape)}`)
800
+ .join(", ");
801
+ return `{ kind: "object", shape: { ${fields} } }`;
802
+ }
803
+ return `{ kind: ${quote(shape.kind)} }`;
804
+ }
805
+
806
+ function emitManyToManyRuntimeMetadata(definition: SchemaDefinition): string {
807
+ const tableEntries: string[] = [];
808
+
809
+ for (const [tableName, table] of Object.entries(definition.tables)) {
810
+ const relationEntries: string[] = [];
811
+
812
+ for (const [relationName, relation] of Object.entries(table.relations)) {
813
+ if (relation.relation !== "manyToMany") {
814
+ continue;
815
+ }
816
+
817
+ if (!relation.junctionTable) {
818
+ continue;
819
+ }
820
+
821
+ relationEntries.push(
822
+ `${quote(relationName)}: {
823
+ targetTable: ${quote(relation.targetTable)},
824
+ junctionTable: ${quote(relation.junctionTable)},
825
+ sourceField: ${quote(relation.sourceField ?? "")},
826
+ targetField: ${quote(relation.targetField ?? "")},
827
+ referenceField: ${quote(relation.referenceField ?? "id")},
828
+ targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
829
+ },`,
830
+ );
831
+ }
832
+
833
+ if (relationEntries.length === 0) {
834
+ continue;
835
+ }
836
+
837
+ tableEntries.push(
838
+ `${quote(tableName)}: {
839
+ ${relationEntries.map((entry) => `\t${entry}`).join("\n")}
840
+ },`,
841
+ );
842
+ }
843
+
844
+ if (tableEntries.length === 0) {
845
+ return "export const __appflareManyToMany = {} as const;\n";
846
+ }
847
+
848
+ return `export const __appflareManyToMany = {
849
+ ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
850
+ } as const;
851
+ `;
852
+ }
853
+
854
+ function emitRuntimeRelationMetadata(definition: SchemaDefinition): string {
855
+ const tableEntries: string[] = [];
856
+
857
+ for (const [tableName, table] of Object.entries(definition.tables)) {
858
+ const relationEntries: string[] = [];
859
+
860
+ for (const [relationName, relation] of Object.entries(table.relations)) {
861
+ if (relation.relation === "one") {
862
+ relationEntries.push(
863
+ `${quote(relationName)}: {
864
+ kind: "one",
865
+ targetTable: ${quote(relation.targetTable)},
866
+ sourceField: ${quote(relation.field ?? "")},
867
+ referenceField: ${quote(relation.referenceField ?? "id")},
868
+ },`,
869
+ );
870
+ continue;
871
+ }
872
+
873
+ if (relation.relation === "many") {
874
+ relationEntries.push(
875
+ `${quote(relationName)}: {
876
+ kind: "many",
877
+ targetTable: ${quote(relation.targetTable)},
878
+ sourceField: ${quote(relation.field ?? "")},
879
+ referenceField: ${quote(relation.referenceField ?? "id")},
880
+ },`,
881
+ );
882
+ continue;
883
+ }
884
+
885
+ relationEntries.push(
886
+ `${quote(relationName)}: {
887
+ kind: "manyToMany",
888
+ targetTable: ${quote(relation.targetTable)},
889
+ junctionTable: ${quote(relation.junctionTable ?? "")},
890
+ sourceField: ${quote(relation.sourceField ?? "")},
891
+ targetField: ${quote(relation.targetField ?? "")},
892
+ referenceField: ${quote(relation.referenceField ?? "id")},
893
+ targetReferenceField: ${quote(relation.targetReferenceField ?? "id")},
894
+ },`,
895
+ );
896
+ }
897
+
898
+ if (relationEntries.length === 0) {
899
+ continue;
900
+ }
901
+
902
+ tableEntries.push(
903
+ `${quote(tableName)}: {
904
+ ${relationEntries.map((entry) => `\t${entry}`).join("\n")}
905
+ },`,
906
+ );
907
+ }
908
+
909
+ if (tableEntries.length === 0) {
910
+ return "export const __appflareRelations = {} as const;\n";
911
+ }
912
+
913
+ return `export const __appflareRelations = {
914
+ ${tableEntries.map((entry) => `\t${entry}`).join("\n")}
915
+ } as const;
916
+ `;
917
+ }
918
+
919
+ function emitDrizzleSchema(
920
+ definition: SchemaDefinition,
921
+ strategy: "camelToSnake",
922
+ ): string {
923
+ const localTables = new Set(Object.keys(definition.tables));
924
+ const externalTables = new Set<string>();
925
+
926
+ for (const table of Object.values(definition.tables)) {
927
+ for (const relationTarget of getRelationImports(table)) {
928
+ if (!localTables.has(relationTarget)) {
929
+ externalTables.add(relationTarget);
930
+ }
931
+ }
932
+ for (const column of Object.values(table.columns)) {
933
+ if (column.references && !localTables.has(column.references.table)) {
934
+ externalTables.add(column.references.table);
935
+ }
936
+ }
937
+ }
938
+
939
+ const enumColumns = new Map<string, { column: ColumnDefinition; tableName: string; fieldName: string }>();
940
+ for (const [tableName, table] of Object.entries(definition.tables)) {
941
+ for (const [fieldName, column] of Object.entries(table.columns)) {
942
+ if (
943
+ column.type === "enum" &&
944
+ column.enumValues &&
945
+ column.enumValues.length > 0
946
+ ) {
947
+ const key = column.enumRef ?? `${tableName}_${fieldName}`;
948
+ if (!enumColumns.has(key)) {
949
+ enumColumns.set(key, { column, tableName, fieldName });
950
+ }
951
+ }
952
+ }
953
+ }
954
+
955
+ const enumTypeLines: string[] = [];
956
+ const enumCustomTypeLines: string[] = [];
957
+ for (const [key, info] of enumColumns.entries()) {
958
+ const typeName = toPascalCase(key);
959
+ const valuesStr = info.column.enumValues!.map((v) => `"${v}"`).join(" | ");
960
+ enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
961
+ enumCustomTypeLines.push(
962
+ `export const ${typeName}Column = t.customType<{ data: ${typeName}; dataNotNull: ${typeName} }>({ dataType: () => "text" });`,
963
+ );
964
+ }
965
+
966
+ const tableBlocks: string[] = [];
967
+ const relationBlocks: string[] = [];
968
+
969
+ for (const [tableName, table] of Object.entries(definition.tables)) {
970
+ const sqlTableName = table.sqlName ?? toSnakeCase(tableName);
971
+ const columnLines: string[] = [];
972
+ const indexes: string[] = [];
973
+
974
+ for (const [fieldName, column] of Object.entries(table.columns)) {
975
+ let expr: string;
976
+ if (
977
+ column.type === "enum" &&
978
+ column.enumValues &&
979
+ column.enumValues.length > 0
980
+ ) {
981
+ const enumKey = column.enumRef ?? `${tableName}_${fieldName}`;
982
+ const typeName = toPascalCase(enumKey);
983
+ const resolvedSqlName = column.sqlName ?? toSnakeCase(fieldName);
984
+ const needsExplicitName = resolvedSqlName !== fieldName;
985
+ expr = needsExplicitName
986
+ ? `${typeName}Column(${quote(resolvedSqlName)})`
987
+ : `${typeName}Column()`;
988
+ if (column.isArray) {
989
+ expr += ".array()";
990
+ }
991
+ } else {
992
+ expr = drizzleBaseColumn(fieldName, column, strategy);
993
+ }
994
+
995
+ if (column.uuidPrimaryKey) {
996
+ expr += ".$defaultFn(() => crypto.randomUUID())";
997
+ }
998
+
999
+ if (column.primaryKey) {
1000
+ expr += column.autoIncrement
1001
+ ? ".primaryKey({ autoIncrement: true })"
1002
+ : ".primaryKey()";
1003
+ }
1004
+ if (column.notNull) {
1005
+ expr += ".notNull()";
1006
+ }
1007
+ if (column.sqlDefault !== undefined) {
1008
+ expr += `.default(${toSqlLiteral(column.sqlDefault)})`;
1009
+ }
1010
+
1011
+ const reference = resolveColumnReference(fieldName, column, table);
1012
+ if (reference) {
1013
+ if (column.references?.onDelete || column.references?.onUpdate) {
1014
+ const actions: string[] = [];
1015
+ if (column.references.onDelete) {
1016
+ actions.push(`onDelete: ${quote(column.references.onDelete)}`);
1017
+ }
1018
+ if (column.references.onUpdate) {
1019
+ actions.push(`onUpdate: ${quote(column.references.onUpdate)}`);
1020
+ }
1021
+ expr += `.references(() => ${reference.tableName}.${reference.fieldName}, { ${actions.join(", ")} })`;
1022
+ } else {
1023
+ expr += `.references(() => ${reference.tableName}.${reference.fieldName})`;
1024
+ }
1025
+ }
1026
+
1027
+ if (column.unique) {
1028
+ const uniqueName =
1029
+ typeof column.unique === "object" && column.unique.name
1030
+ ? column.unique.name
1031
+ : `${sqlTableName}_${toSnakeCase(fieldName)}_unique_idx`;
1032
+ indexes.push(
1033
+ `\t\tt.uniqueIndex(${quote(uniqueName)}).on(table.${fieldName})`,
1034
+ );
1035
+ }
1036
+
1037
+ if (column.index) {
1038
+ const indexName =
1039
+ typeof column.index === "object" && column.index.name
1040
+ ? column.index.name
1041
+ : `${sqlTableName}_${toSnakeCase(fieldName)}_idx`;
1042
+ indexes.push(`\t\tt.index(${quote(indexName)}).on(table.${fieldName})`);
1043
+ }
1044
+
1045
+ columnLines.push(`\t\t${fieldName}: ${expr},`);
1046
+ }
1047
+
1048
+ if (indexes.length > 0) {
1049
+ tableBlocks.push(
1050
+ `export const ${tableName} = table(\n\t${quote(sqlTableName)},\n\t{\n${columnLines.join("\n")}\n\t},\n\t(table) => [\n${indexes.join(",\n")}\n\t],\n);`,
1051
+ );
1052
+ } else {
1053
+ tableBlocks.push(
1054
+ `export const ${tableName} = table(${quote(sqlTableName)}, {\n${columnLines.join("\n")}\n\t});`,
1055
+ );
1056
+ }
1057
+
1058
+ const oneRelations = Object.entries(table.relations).filter(
1059
+ ([, relation]) => {
1060
+ return relation.relation === "one";
1061
+ },
1062
+ ) as Array<[string, OneRelationDefinition]>;
1063
+ const manyRelations = Object.entries(table.relations).filter(
1064
+ ([, relation]) => {
1065
+ return relation.relation === "many";
1066
+ },
1067
+ ) as Array<[string, ManyRelationDefinition]>;
1068
+ const manyToManyRelations = Object.entries(table.relations).filter(
1069
+ ([, relation]) => {
1070
+ return relation.relation === "manyToMany";
1071
+ },
1072
+ ) as Array<[string, ManyToManyRelationDefinition]>;
1073
+
1074
+ if (
1075
+ oneRelations.length === 0 &&
1076
+ manyRelations.length === 0 &&
1077
+ manyToManyRelations.length === 0
1078
+ ) {
1079
+ continue;
1080
+ }
1081
+
1082
+ const relationLines: string[] = [];
1083
+ for (const [relationName, relation] of oneRelations) {
1084
+ const resolved = resolveOneRelationReference(tableName, table, relation);
1085
+ relationLines.push(
1086
+ `\t${relationName}: one(${relation.targetTable}, {\n\t\tfields: [${tableName}.${resolved.sourceField}],\n\t\treferences: [${relation.targetTable}.${resolved.targetField}],\n\t}),`,
1087
+ );
1088
+ }
1089
+ for (const [relationName, relation] of manyRelations) {
1090
+ relationLines.push(`\t${relationName}: many(${relation.targetTable}),`);
1091
+ }
1092
+ for (const [relationName, relation] of manyToManyRelations) {
1093
+ if (!relation.junctionTable) {
1094
+ throw new Error(
1095
+ `manyToMany relation '${tableName}.${relationName}' is missing junctionTable after normalization.`,
1096
+ );
1097
+ }
1098
+ relationLines.push(`\t${relationName}: many(${relation.junctionTable}),`);
1099
+ }
1100
+
1101
+ relationBlocks.push(
1102
+ `export const ${tableName}Relations = relations(${tableName}, ({ one, many }) => ({\n${relationLines.join("\n")}\n}));`,
1103
+ );
1104
+ }
1105
+
1106
+ return `import * as t from "drizzle-orm/sqlite-core";
1107
+ import { sqliteTable as table } from "drizzle-orm/sqlite-core";
1108
+ import { relations } from "drizzle-orm";
1109
+ ${buildExternalTableImportLines(externalTables)}
1110
+ ${enumTypeLines.join("\n")}
1111
+ ${enumCustomTypeLines.join("\n")}
1112
+
1113
+ ${tableBlocks.join("\n\n")}
1114
+
1115
+ ${relationBlocks.join("\n\n")}
1116
+
1117
+ ${emitJsonColumnsMetadata(definition)}
1118
+
1119
+ ${emitManyToManyRuntimeMetadata(definition)}
1120
+
1121
+ ${emitRuntimeRelationMetadata(definition)}
1122
+ `;
1123
+ }
1124
+
1125
+ function jsonShapeToZod(shape: JsonShape): string {
1126
+ if (shape.kind === "array") {
1127
+ return `z.array(${jsonShapeToZod(shape.element)})`;
1128
+ }
1129
+ if (shape.kind === "object") {
1130
+ const fields = Object.entries(shape.shape)
1131
+ .map(([key, fieldShape]) => `${quote(key)}: ${jsonShapeToZod(fieldShape)}`)
1132
+ .join(", ");
1133
+ return `z.object({ ${fields} })`;
1134
+ }
1135
+ if (shape.kind === "string") return "z.string()";
1136
+ if (shape.kind === "number") return "z.number()";
1137
+ if (shape.kind === "boolean") return "z.boolean()";
1138
+ if (shape.kind === "date") return "z.date()";
1139
+ return "z.unknown()";
1140
+ }
1141
+
1142
+ function zodSchemaExpression(
1143
+ column: ColumnDefinition,
1144
+ optional: boolean,
1145
+ nullable: boolean,
1146
+ ): string {
1147
+ let expr = "z.unknown()";
1148
+ if (column.type === "int") {
1149
+ expr = "z.number().int()";
1150
+ } else if (column.type === "string") {
1151
+ expr = "z.string()";
1152
+ if (column.length !== undefined) {
1153
+ expr += `.max(${column.length})`;
1154
+ }
1155
+ } else if (column.type === "boolean") {
1156
+ expr = "z.boolean()";
1157
+ } else if (column.type === "date") {
1158
+ expr = "z.date()";
1159
+ } else if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1160
+ const valuesStr = column.enumValues.map((v) => `"${v}"`).join(", ");
1161
+ const enumZod = `z.enum([${valuesStr}])`;
1162
+ expr = column.isArray ? `z.array(${enumZod})` : enumZod;
1163
+ } else if (column.type === "json" && column.jsonShape) {
1164
+ expr = jsonShapeToZod(column.jsonShape);
1165
+ }
1166
+
1167
+ if (optional) {
1168
+ expr += ".optional()";
1169
+ }
1170
+
1171
+ if (nullable) {
1172
+ expr += ".nullable()";
1173
+ }
1174
+
1175
+ return expr;
1176
+ }
1177
+
1178
+ function emitZodSchemas(definition: SchemaDefinition): string {
1179
+ const blocks: string[] = [];
1180
+
1181
+ for (const [tableName, table] of Object.entries(definition.tables)) {
1182
+ const pascal = toPascalCase(tableName);
1183
+ const insertLines: string[] = [];
1184
+ const selectLines: string[] = [];
1185
+
1186
+ for (const [fieldName, column] of Object.entries(table.columns)) {
1187
+ insertLines.push(
1188
+ `\t${fieldName}: ${zodSchemaExpression(column, isOptionalInsertColumn(column), isNullableSelectColumn(column))},`,
1189
+ );
1190
+ selectLines.push(
1191
+ `\t${fieldName}: ${zodSchemaExpression(column, isNullableSelectColumn(column), isNullableSelectColumn(column))},`,
1192
+ );
1193
+ }
1194
+
1195
+ blocks.push(`export const ${tableName}InsertSchema = z.object({\n${insertLines.join("\n")}\n});
1196
+ export const ${tableName}SelectSchema = z.object({\n${selectLines.join("\n")}\n});
1197
+
1198
+ export type ${pascal}Insert = z.infer<typeof ${tableName}InsertSchema>;
1199
+ export type ${pascal}Select = z.infer<typeof ${tableName}SelectSchema>;
1200
+ `);
1201
+ }
1202
+
1203
+ return `import { z } from "zod";
1204
+
1205
+ ${blocks.join("\n")}`;
1206
+ }
1207
+
1208
+ function jsonShapeToTypeScript(shape: JsonShape): string {
1209
+ if (shape.kind === "array") {
1210
+ return `Array<${jsonShapeToTypeScript(shape.element)}>`;
1211
+ }
1212
+ if (shape.kind === "object") {
1213
+ const fields = Object.entries(shape.shape)
1214
+ .map(([key, fieldShape]) => `${key}: ${jsonShapeToTypeScript(fieldShape)}`)
1215
+ .join("; ");
1216
+ return `{ ${fields} }`;
1217
+ }
1218
+ if (shape.kind === "string") return "string";
1219
+ if (shape.kind === "number") return "number";
1220
+ if (shape.kind === "boolean") return "boolean";
1221
+ if (shape.kind === "date") return "Date";
1222
+ return "unknown";
1223
+ }
1224
+
1225
+ function toTypeScriptType(column: ColumnDefinition): string {
1226
+ if (column.type === "int") {
1227
+ return "number";
1228
+ }
1229
+ if (column.type === "string") {
1230
+ return "string";
1231
+ }
1232
+ if (column.type === "boolean") {
1233
+ return "boolean";
1234
+ }
1235
+ if (column.type === "date") {
1236
+ return "Date";
1237
+ }
1238
+ if (column.type === "enum" && column.enumValues && column.enumValues.length > 0) {
1239
+ const union = column.enumValues.map((v) => `"${v}"`).join(" | ");
1240
+ if (column.isArray) {
1241
+ return `Array<${union}>`;
1242
+ }
1243
+ return union;
1244
+ }
1245
+ if (column.type === "json" && column.jsonShape) {
1246
+ return jsonShapeToTypeScript(column.jsonShape);
1247
+ }
1248
+ return "unknown";
1249
+ }
1250
+
1251
+ function emitTypes(definition: SchemaDefinition): string {
1252
+ const enumTypeLines: string[] = [];
1253
+ for (const [name, enumDef] of Object.entries(definition.enums ?? {})) {
1254
+ const typeName = toPascalCase(name);
1255
+ const valuesStr = enumDef.values.map((v) => `"${v}"`).join(" | ");
1256
+ enumTypeLines.push(`export type ${typeName} = ${valuesStr};`);
1257
+ }
1258
+
1259
+ const lines: string[] = [];
1260
+
1261
+ for (const [tableName, table] of Object.entries(definition.tables)) {
1262
+ const pascal = toPascalCase(tableName);
1263
+ const selectFields: string[] = [];
1264
+ const insertFields: string[] = [];
1265
+
1266
+ for (const [fieldName, column] of Object.entries(table.columns)) {
1267
+ const tsType = toTypeScriptType(column);
1268
+ const nullableSuffix = isNullableSelectColumn(column) ? " | null" : "";
1269
+ selectFields.push(
1270
+ `\t${fieldName}${isNullableSelectColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
1271
+ );
1272
+ insertFields.push(
1273
+ `\t${fieldName}${isOptionalInsertColumn(column) ? "?" : ""}: ${tsType}${nullableSuffix};`,
1274
+ );
1275
+ }
1276
+
1277
+ lines.push(`export type ${pascal} = {\n${selectFields.join("\n")}\n};
1278
+
1279
+ export type New${pascal} = {\n${insertFields.join("\n")}\n};`);
1280
+ }
1281
+
1282
+ return `${enumTypeLines.join("\n")}
1283
+ ${lines.join("\n\n")}
1284
+ `;
1285
+ }
1286
+
1287
+ function findSchemaExport(
1288
+ moduleExports: Record<string, unknown>,
1289
+ exportName?: string,
1290
+ ): SchemaDefinition {
1291
+ if (exportName) {
1292
+ const explicit = moduleExports[exportName];
1293
+ if (!isSchemaDefinition(explicit)) {
1294
+ throw new Error(
1295
+ `schemaDsl.exportName '${exportName}' does not point to a schema() export.`,
1296
+ );
1297
+ }
1298
+ return explicit;
1299
+ }
1300
+
1301
+ for (const candidate of Object.values(moduleExports)) {
1302
+ if (isSchemaDefinition(candidate)) {
1303
+ return candidate;
1304
+ }
1305
+ }
1306
+
1307
+ throw new Error(
1308
+ "No schema() export found in schemaDsl entry module. Set schemaDsl.exportName to the correct export.",
1309
+ );
1310
+ }
1311
+
1312
+ export async function compileSchemaDsl(
1313
+ loadedConfig: LoadedAppflareConfig,
1314
+ ): Promise<CompiledSchemaArtifacts | undefined> {
1315
+ const schemaDsl = loadedConfig.config.schemaDsl;
1316
+ if (!schemaDsl) {
1317
+ return undefined;
1318
+ }
1319
+
1320
+ const namingStrategy = schemaDsl.namingStrategy ?? "camelToSnake";
1321
+ const entryPath = resolve(loadedConfig.configDir, schemaDsl.entry);
1322
+ const outSchemaPath = resolve(
1323
+ loadedConfig.configDir,
1324
+ schemaDsl.outFile ?? resolve(loadedConfig.outDirAbs, "schema.compiled.ts"),
1325
+ );
1326
+ const outTypesPath = resolve(
1327
+ loadedConfig.configDir,
1328
+ schemaDsl.typesOutFile ??
1329
+ resolve(loadedConfig.outDirAbs, "schema.types.ts"),
1330
+ );
1331
+ const outZodPath = resolve(
1332
+ loadedConfig.configDir,
1333
+ schemaDsl.zodOutFile ?? resolve(loadedConfig.outDirAbs, "schema.zod.ts"),
1334
+ );
1335
+
1336
+ const moduleUrl = `${pathToFileURL(entryPath).href}?t=${Date.now()}`;
1337
+ const schemaModule = (await import(moduleUrl)) as Record<string, unknown>;
1338
+ const schemaDefinition = findSchemaExport(schemaModule, schemaDsl.exportName);
1339
+ const normalizedSchema = normalizeSchemaDefinition(schemaDefinition);
1340
+
1341
+ await Promise.all([
1342
+ mkdir(dirname(outSchemaPath), { recursive: true }),
1343
+ mkdir(dirname(outTypesPath), { recursive: true }),
1344
+ mkdir(dirname(outZodPath), { recursive: true }),
1345
+ ]);
1346
+
1347
+ const drizzleSchemaSource = emitDrizzleSchema(
1348
+ normalizedSchema,
1349
+ namingStrategy,
1350
+ );
1351
+ const typesSource = emitTypes(normalizedSchema);
1352
+ const zodSource = emitZodSchemas(normalizedSchema);
1353
+
1354
+ await Promise.all([
1355
+ Bun.write(outSchemaPath, drizzleSchemaSource),
1356
+ Bun.write(outTypesPath, typesSource),
1357
+ Bun.write(outZodPath, zodSource),
1358
+ ]);
1359
+
1360
+ return {
1361
+ schemaPath: outSchemaPath,
1362
+ typesPath: outTypesPath,
1363
+ zodPath: outZodPath,
1364
+ tableNames: Object.keys(normalizedSchema.tables),
1365
+ };
1366
+ }