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,958 +1,958 @@
1
- export function generateQueryRuntimeWriteSection(): string {
2
- return ` insert: async <TArgs extends QueryInsertArgs<TableName>>(args: TArgs) => {
3
- const transaction = ($db as any).transaction;
4
-
5
- const valuesArray = Array.isArray(args.values)
6
- ? args.values
7
- : [args.values];
8
- const baseValuesArray: Array<Record<string, unknown>> = [];
9
- const postInsertRelations: Array<
10
- Array<{
11
- relationName: string;
12
- relation: RuntimeRelation;
13
- value: unknown;
14
- }>
15
- > = [];
16
-
17
- const toRelationItems = (
18
- relation: RuntimeRelation,
19
- relationName: string,
20
- value: unknown,
21
- ): unknown[] => {
22
- if (value === undefined || value === null) {
23
- return [];
24
- }
25
-
26
- if (relation.kind === "one") {
27
- if (Array.isArray(value)) {
28
- throw new Error(
29
- "Relation '" +
30
- tableName +
31
- "." +
32
- relationName +
33
- "' expects a single value.",
34
- );
35
- }
36
- return [value];
37
- }
38
-
39
- return Array.isArray(value) ? value : [value];
40
- };
41
-
42
- const resolveRelationIdentifier = async (
43
- tx: any,
44
- relation: RuntimeRelation,
45
- relationName: string,
46
- input: unknown,
47
- ): Promise<unknown> => {
48
- if (
49
- typeof input === "string" ||
50
- typeof input === "number" ||
51
- typeof input === "bigint"
52
- ) {
53
- return input;
54
- }
55
-
56
- if (!isRecord(input)) {
57
- throw new Error(
58
- "Relation '" +
59
- tableName +
60
- "." +
61
- relationName +
62
- "' expects an id or object payload.",
63
- );
64
- }
65
-
66
- const referenceField =
67
- relation.kind === "manyToMany"
68
- ? (relation.targetReferenceField ?? "id")
69
- : (relation.referenceField ?? "id");
70
- const existingId = input[referenceField];
71
- if (existingId !== undefined && existingId !== null) {
72
- return existingId;
73
- }
74
-
75
- const targetTable = (mergedSchema as Record<string, unknown>)[
76
- relation.targetTable
77
- ];
78
- if (!targetTable) {
79
- throw new Error(
80
- "Unknown target table '" +
81
- relation.targetTable +
82
- "' for relation '" +
83
- tableName +
84
- "." +
85
- relationName +
86
- "'.",
87
- );
88
- }
89
-
90
- let createQuery: any = tx.insert(targetTable as any).values(input as any);
91
- if (typeof createQuery.returning === "function") {
92
- createQuery = createQuery.returning();
93
- }
94
- const createdRows = (await createQuery) as Array<Record<string, unknown>>;
95
- const created = createdRows[0];
96
- if (!created) {
97
- throw new Error(
98
- "Failed to create relation target for '" +
99
- tableName +
100
- "." +
101
- relationName +
102
- "'.",
103
- );
104
- }
105
-
106
- const createdId = created[referenceField];
107
- if (createdId === undefined || createdId === null) {
108
- throw new Error(
109
- "Created relation target for '" +
110
- tableName +
111
- "." +
112
- relationName +
113
- "' is missing '" +
114
- referenceField +
115
- "'.",
116
- );
117
- }
118
-
119
- return createdId;
120
- };
121
-
122
- const getRelationTargetTable = (
123
- relation: RuntimeRelation,
124
- relationName: string,
125
- ): any => {
126
- const targetTable = (mergedSchema as Record<string, unknown>)[
127
- relation.targetTable
128
- ];
129
- if (!targetTable) {
130
- throw new Error(
131
- "Unknown target table '" +
132
- relation.targetTable +
133
- "' for relation '" +
134
- tableName +
135
- "." +
136
- relationName +
137
- "'.",
138
- );
139
- }
140
- return targetTable;
141
- };
142
-
143
- const getRelationReferenceField = (relation: RuntimeRelation): string => {
144
- if (relation.kind === "manyToMany") {
145
- return relation.targetReferenceField ?? "id";
146
- }
147
- return relation.referenceField ?? "id";
148
- };
149
-
150
- const fetchRelatedRowByIdentifier = async (
151
- tx: any,
152
- relation: RuntimeRelation,
153
- relationName: string,
154
- identifier: unknown,
155
- ): Promise<Record<string, unknown> | null> => {
156
- if (identifier === undefined || identifier === null) {
157
- return null;
158
- }
159
-
160
- const targetTable = getRelationTargetTable(relation, relationName);
161
- const referenceField = getRelationReferenceField(relation);
162
- const referenceColumn = (targetTable as Record<string, unknown>)[
163
- referenceField
164
- ];
165
- if (!referenceColumn) {
166
- throw new Error(
167
- "Target table '" +
168
- relation.targetTable +
169
- "' is missing column '" +
170
- referenceField +
171
- "' required by relation '" +
172
- tableName +
173
- "." +
174
- relationName +
175
- "'.",
176
- );
177
- }
178
-
179
- let query: any = tx
180
- .select()
181
- .from(targetTable as any)
182
- .where(eq(referenceColumn as any, identifier as any));
183
- if (typeof query.limit === "function") {
184
- query = query.limit(1);
185
- }
186
- const rows = (await query) as Array<Record<string, unknown>>;
187
- return rows[0] ?? null;
188
- };
189
-
190
- for (const inputValue of valuesArray) {
191
- const scalarValues: Record<string, unknown> = {};
192
- const relationInputs: Array<{
193
- relationName: string;
194
- relation: RuntimeRelation;
195
- value: unknown;
196
- }> = [];
197
-
198
- const valueRecord = (inputValue ?? {}) as Record<string, unknown>;
199
- for (const [fieldName, fieldValue] of Object.entries(valueRecord)) {
200
- const runtimeRelation = getRuntimeRelation(tableName, fieldName);
201
- if (runtimeRelation) {
202
- relationInputs.push({
203
- relationName: fieldName,
204
- relation: runtimeRelation,
205
- value: fieldValue,
206
- });
207
- continue;
208
- }
209
-
210
- if (fieldValue === undefined) {
211
- continue;
212
- }
213
-
214
- scalarValues[fieldName] = fieldValue;
215
- }
216
-
217
- baseValuesArray.push(scalarValues);
218
- postInsertRelations.push(relationInputs);
219
- }
220
-
221
- const executeInsertGraph = async (
222
- tx: any,
223
- ): Promise<Array<QueryInsertResultRow<TableName, TArgs>>> => {
224
- for (let index = 0; index < baseValuesArray.length; index += 1) {
225
- const relationInputs = postInsertRelations[index] ?? [];
226
- for (const relationInput of relationInputs) {
227
- if (relationInput.relation.kind !== "one") {
228
- continue;
229
- }
230
-
231
- const sourceField = relationInput.relation.sourceField;
232
- if (!sourceField) {
233
- throw new Error(
234
- "Relation '" +
235
- tableName +
236
- "." +
237
- relationInput.relationName +
238
- "' is missing sourceField metadata.",
239
- );
240
- }
241
-
242
- const oneItems = toRelationItems(
243
- relationInput.relation,
244
- relationInput.relationName,
245
- relationInput.value,
246
- );
247
- if (oneItems.length === 0) {
248
- continue;
249
- }
250
-
251
- const relationId = await resolveRelationIdentifier(
252
- tx,
253
- relationInput.relation,
254
- relationInput.relationName,
255
- oneItems[0],
256
- );
257
- const existingValue = baseValuesArray[index][sourceField];
258
- if (
259
- existingValue !== undefined &&
260
- existingValue !== relationId
261
- ) {
262
- throw new Error(
263
- "Insert payload for '" +
264
- tableName +
265
- "' has conflicting values for '" +
266
- sourceField +
267
- "'.",
268
- );
269
- }
270
- baseValuesArray[index][sourceField] = relationId;
271
- }
272
- }
273
-
274
- let insertQuery: any = tx.insert(table as any).values(baseValuesArray as any);
275
- if (typeof insertQuery.returning === "function") {
276
- insertQuery = insertQuery.returning();
277
- }
278
- const insertedRows = (await insertQuery) as Array<TableModel<TableName>>;
279
- const hydratedRows: Array<Record<string, unknown>> = [];
280
-
281
- for (let index = 0; index < insertedRows.length; index += 1) {
282
- const parentRow = insertedRows[index] as unknown as Record<string, unknown>;
283
- const hydratedRow: Record<string, unknown> = {
284
- ...parentRow,
285
- };
286
- const relationInputs = postInsertRelations[index] ?? [];
287
-
288
- for (const relationInput of relationInputs) {
289
- if (relationInput.relation.kind === "one") {
290
- const sourceField = relationInput.relation.sourceField;
291
- if (!sourceField) {
292
- throw new Error(
293
- "Relation '" +
294
- tableName +
295
- "." +
296
- relationInput.relationName +
297
- "' is missing sourceField metadata.",
298
- );
299
- }
300
-
301
- const relationItems = toRelationItems(
302
- relationInput.relation,
303
- relationInput.relationName,
304
- relationInput.value,
305
- );
306
- if (relationItems.length === 0) {
307
- hydratedRow[relationInput.relationName] = null;
308
- continue;
309
- }
310
-
311
- const sourceIdentifier = parentRow[sourceField];
312
- hydratedRow[relationInput.relationName] =
313
- (await fetchRelatedRowByIdentifier(
314
- tx,
315
- relationInput.relation,
316
- relationInput.relationName,
317
- sourceIdentifier,
318
- )) ?? null;
319
- continue;
320
- }
321
-
322
- const sourceReferenceField = relationInput.relation.referenceField ?? "id";
323
- const parentReferenceValue = parentRow[sourceReferenceField];
324
- if (
325
- parentReferenceValue === undefined ||
326
- parentReferenceValue === null
327
- ) {
328
- throw new Error(
329
- "Inserted row for '" +
330
- tableName +
331
- "' is missing '" +
332
- sourceReferenceField +
333
- "' required by relation '" +
334
- relationInput.relationName +
335
- "'.",
336
- );
337
- }
338
-
339
- const relatedRows: Array<Record<string, unknown>> = [];
340
- hydratedRow[relationInput.relationName] = relatedRows;
341
-
342
- const relationItems = toRelationItems(
343
- relationInput.relation,
344
- relationInput.relationName,
345
- relationInput.value,
346
- );
347
- if (relationItems.length === 0) {
348
- continue;
349
- }
350
-
351
- if (relationInput.relation.kind === "many") {
352
- const targetTable = getRelationTargetTable(
353
- relationInput.relation,
354
- relationInput.relationName,
355
- );
356
-
357
- const sourceField = relationInput.relation.sourceField;
358
- const targetReferenceField =
359
- relationInput.relation.referenceField ?? "id";
360
- const targetReferenceColumn =
361
- (targetTable as Record<string, unknown>)[targetReferenceField];
362
- if (!sourceField) {
363
- throw new Error(
364
- "Relation '" +
365
- tableName +
366
- "." +
367
- relationInput.relationName +
368
- "' is missing sourceField metadata.",
369
- );
370
- }
371
- if (!targetReferenceColumn) {
372
- throw new Error(
373
- "Target table '" +
374
- relationInput.relation.targetTable +
375
- "' is missing column '" +
376
- targetReferenceField +
377
- "' required by relation '" +
378
- tableName +
379
- "." +
380
- relationInput.relationName +
381
- "'.",
382
- );
383
- }
384
-
385
- for (const relationItem of relationItems) {
386
- if (
387
- typeof relationItem === "string" ||
388
- typeof relationItem === "number" ||
389
- typeof relationItem === "bigint"
390
- ) {
391
- await tx
392
- .update(targetTable as any)
393
- .set({ [sourceField]: parentReferenceValue } as any)
394
- .where(
395
- eq(targetReferenceColumn as any, relationItem as any),
396
- );
397
- const linkedRow = await fetchRelatedRowByIdentifier(
398
- tx,
399
- relationInput.relation,
400
- relationInput.relationName,
401
- relationItem,
402
- );
403
- if (linkedRow) {
404
- relatedRows.push(linkedRow);
405
- }
406
- continue;
407
- }
408
-
409
- if (!isRecord(relationItem)) {
410
- throw new Error(
411
- "Relation '" +
412
- tableName +
413
- "." +
414
- relationInput.relationName +
415
- "' expects id or object payloads.",
416
- );
417
- }
418
-
419
- const existingId = relationItem[targetReferenceField];
420
- const payload = {
421
- ...relationItem,
422
- [sourceField]: parentReferenceValue,
423
- };
424
-
425
- if (existingId !== undefined && existingId !== null) {
426
- const setPayload: Record<string, unknown> = {
427
- ...payload,
428
- };
429
- delete setPayload[targetReferenceField];
430
- await tx
431
- .update(targetTable as any)
432
- .set(setPayload as any)
433
- .where(
434
- eq(targetReferenceColumn as any, existingId as any),
435
- );
436
- const linkedRow = await fetchRelatedRowByIdentifier(
437
- tx,
438
- relationInput.relation,
439
- relationInput.relationName,
440
- existingId,
441
- );
442
- if (linkedRow) {
443
- relatedRows.push(linkedRow);
444
- }
445
- continue;
446
- }
447
-
448
- let createQuery: any = tx
449
- .insert(targetTable as any)
450
- .values(payload as any);
451
- if (typeof createQuery.returning === "function") {
452
- createQuery = createQuery.returning();
453
- }
454
- const createdRows = (await createQuery) as Array<
455
- Record<string, unknown>
456
- >;
457
- const createdRow = createdRows[0];
458
- if (createdRow) {
459
- relatedRows.push(createdRow);
460
- }
461
- }
462
- continue;
463
- }
464
-
465
- const junctionTable = (mergedSchema as Record<string, unknown>)[
466
- relationInput.relation.junctionTable
467
- ];
468
- if (!junctionTable) {
469
- throw new Error(
470
- "Unknown junction table '" +
471
- relationInput.relation.junctionTable +
472
- "' for relation '" +
473
- tableName +
474
- "." +
475
- relationInput.relationName +
476
- "'.",
477
- );
478
- }
479
-
480
- const sourceField = relationInput.relation.sourceField;
481
- const targetField = relationInput.relation.targetField;
482
- if (!sourceField || !targetField) {
483
- throw new Error(
484
- "Relation '" +
485
- tableName +
486
- "." +
487
- relationInput.relationName +
488
- "' is missing junction metadata fields.",
489
- );
490
- }
491
-
492
- const linkValues: Array<Record<string, unknown>> = [];
493
- const targetIdentifiers: unknown[] = [];
494
- for (const relationItem of relationItems) {
495
- const targetId = await resolveRelationIdentifier(
496
- tx,
497
- relationInput.relation,
498
- relationInput.relationName,
499
- relationItem,
500
- );
501
- targetIdentifiers.push(targetId);
502
-
503
- linkValues.push({
504
- [sourceField]: parentReferenceValue,
505
- [targetField]: targetId,
506
- });
507
- }
508
-
509
- if (linkValues.length > 0) {
510
- await tx.insert(junctionTable as any).values(linkValues as any);
511
- }
512
-
513
- for (const targetId of targetIdentifiers) {
514
- const linkedRow = await fetchRelatedRowByIdentifier(
515
- tx,
516
- relationInput.relation,
517
- relationInput.relationName,
518
- targetId,
519
- );
520
- if (linkedRow) {
521
- relatedRows.push(linkedRow);
522
- }
523
- }
524
- }
525
-
526
- hydratedRows.push(hydratedRow);
527
- }
528
-
529
- return hydratedRows as Array<QueryInsertResultRow<TableName, TArgs>>;
530
- };
531
-
532
- let rows: Array<QueryInsertResultRow<TableName, TArgs>>;
533
- if (typeof transaction === "function") {
534
- try {
535
- rows = await transaction.call($db, (tx: any) =>
536
- executeInsertGraph(tx),
537
- );
538
- } catch (error) {
539
- const message =
540
- error instanceof Error ? error.message : String(error);
541
- const lowered = message.toLowerCase();
542
- if (
543
- lowered.includes("failed query: begin") ||
544
- lowered.includes('near "begin"') ||
545
- lowered.includes("cannot start a transaction")
546
- ) {
547
- rows = await executeInsertGraph($db as any);
548
- } else {
549
- throw error;
550
- }
551
- }
552
- } else {
553
- rows = await executeInsertGraph($db as any);
554
- }
555
-
556
- emitMutation(
557
- "insert",
558
- { values: args.values as unknown as Record<string, unknown> },
559
- rows,
560
- );
561
- return rows;
562
- },
563
- update: async (args: QueryUpdateArgs<TableName>) => {
564
- const transaction = ($db as any).transaction;
565
-
566
- const whereFilter = buildWhereFilter(
567
- table,
568
- args.where as Record<string, unknown> | undefined,
569
- tableName,
570
- );
571
-
572
- const setPayload = args.set as Record<string, unknown>;
573
- const cleanSetPayload: Record<string, unknown> = {};
574
- const relationPayloads: Array<{
575
- relationName: string;
576
- relation: RuntimeRelation;
577
- value: unknown;
578
- }> = [];
579
-
580
- for (const [key, value] of Object.entries(setPayload)) {
581
- if (value === undefined) continue;
582
- const runtimeRelation = getRuntimeRelation(tableName, key);
583
- if (runtimeRelation && runtimeRelation.kind === "manyToMany") {
584
- relationPayloads.push({
585
- relationName: key,
586
- relation: runtimeRelation,
587
- value,
588
- });
589
- continue;
590
- }
591
- cleanSetPayload[key] = value;
592
- }
593
-
594
- const normalizeRelationValue = (
595
- value: unknown,
596
- ): { items: unknown[]; mode: "merge" | "overwrite" } => {
597
- if (Array.isArray(value)) {
598
- return { items: value, mode: "merge" };
599
- }
600
- if (value && typeof value === "object") {
601
- const record = value as Record<string, unknown>;
602
- const items = Array.isArray(record.items) ? record.items : [];
603
- const mode = record.mode === "overwrite" ? "overwrite" : "merge";
604
- return { items, mode };
605
- }
606
- return { items: [], mode: "merge" };
607
- };
608
-
609
- const executeUpdateWithRelations = async (
610
- tx: any,
611
- ): Promise<Array<TableModel<TableName>>> => {
612
- let updateQuery: any = tx
613
- .update(table as any)
614
- .set(cleanSetPayload as any);
615
-
616
- if (whereFilter) {
617
- updateQuery = updateQuery.where(whereFilter);
618
- }
619
- if (
620
- typeof args.limit === "number" &&
621
- typeof updateQuery.limit === "function"
622
- ) {
623
- updateQuery = updateQuery.limit(args.limit);
624
- }
625
- if (typeof updateQuery.returning === "function") {
626
- updateQuery = updateQuery.returning();
627
- }
628
-
629
- const rows = (await updateQuery) as Array<TableModel<TableName>>;
630
- if (rows.length === 0) return rows;
631
-
632
- for (const row of rows) {
633
- const parentId = (row as Record<string, unknown>)["id"];
634
- if (parentId === undefined || parentId === null) continue;
635
-
636
- for (const { relationName, relation, value } of relationPayloads) {
637
- const { items, mode } = normalizeRelationValue(value);
638
-
639
- const junctionTable = (mergedSchema as Record<string, unknown>)[
640
- relation.junctionTable
641
- ];
642
- if (!junctionTable) {
643
- throw new Error(
644
- "Unknown junction table '" +
645
- relation.junctionTable +
646
- "' for relation '" +
647
- tableName +
648
- "." +
649
- relationName +
650
- "'.",
651
- );
652
- }
653
-
654
- const sourceField = relation.sourceField;
655
- const targetField = relation.targetField;
656
- if (!sourceField || !targetField) {
657
- throw new Error(
658
- "Relation '" +
659
- tableName +
660
- "." +
661
- relationName +
662
- "' is missing junction metadata fields.",
663
- );
664
- }
665
-
666
- const parentReferenceField = relation.referenceField ?? "id";
667
- const parentReferenceColumn = (table as Record<string, unknown>)[parentReferenceField];
668
- if (!parentReferenceColumn) {
669
- throw new Error(
670
- "Table '" +
671
- tableName +
672
- "' is missing column '" +
673
- parentReferenceField +
674
- "' for relation '" +
675
- relationName +
676
- "'.",
677
- );
678
- }
679
-
680
- const junctionSourceColumn = (junctionTable as Record<string, unknown>)[sourceField];
681
- if (!junctionSourceColumn) {
682
- throw new Error(
683
- "Junction table '" +
684
- relation.junctionTable +
685
- "' is missing column '" +
686
- sourceField +
687
- "' for relation '" +
688
- relationName +
689
- "'.",
690
- );
691
- }
692
-
693
- if (mode === "overwrite") {
694
- await tx
695
- .delete(junctionTable as any)
696
- .where(eq(junctionSourceColumn as any, (row as Record<string, unknown>)[parentReferenceField] as any));
697
- }
698
-
699
- const referenceField = relation.targetReferenceField ?? "id";
700
- const targetTable = (mergedSchema as Record<string, unknown>)[
701
- relation.targetTable
702
- ];
703
-
704
- const junctionTargetColumn =
705
- (junctionTable as Record<string, unknown>)[targetField];
706
- if (!junctionTargetColumn) {
707
- throw new Error(
708
- "Junction table '" +
709
- relation.junctionTable +
710
- "' is missing column '" +
711
- targetField +
712
- "'.",
713
- );
714
- }
715
-
716
- for (const item of items) {
717
- let targetId: unknown;
718
-
719
- if (
720
- typeof item === "string" ||
721
- typeof item === "number" ||
722
- typeof item === "bigint"
723
- ) {
724
- targetId = item;
725
- } else if (item && typeof item === "object") {
726
- const record = item as Record<string, unknown>;
727
- const existingId = record[referenceField];
728
- if (existingId !== undefined && existingId !== null) {
729
- targetId = existingId;
730
- } else {
731
- if (!targetTable) {
732
- throw new Error(
733
- "Unknown target table '" +
734
- relation.targetTable +
735
- "' for relation '" +
736
- tableName +
737
- "." +
738
- relationName +
739
- "'.",
740
- );
741
- }
742
- let createQuery: any = tx
743
- .insert(targetTable as any)
744
- .values(record as any);
745
- if (typeof createQuery.returning === "function") {
746
- createQuery = createQuery.returning();
747
- }
748
- const createdRows =
749
- (await createQuery) as Array<Record<string, unknown>>;
750
- const created = createdRows[0];
751
- if (!created) {
752
- throw new Error(
753
- "Failed to create relation target for '" +
754
- tableName +
755
- "." +
756
- relationName +
757
- "'.",
758
- );
759
- }
760
- targetId = created[referenceField];
761
- if (targetId === undefined || targetId === null) {
762
- throw new Error(
763
- "Created relation target for '" +
764
- tableName +
765
- "." +
766
- relationName +
767
- "' is missing '" +
768
- referenceField +
769
- "'.",
770
- );
771
- }
772
- }
773
- } else {
774
- throw new Error(
775
- "Relation '" +
776
- tableName +
777
- "." +
778
- relationName +
779
- "' expects an id or object payload.",
780
- );
781
- }
782
-
783
- if (mode === "merge") {
784
- const parentRefValue = (row as Record<string, unknown>)[parentReferenceField];
785
- const existingLinks = await tx
786
- .select()
787
- .from(junctionTable as any)
788
- .where(
789
- and(
790
- eq(junctionSourceColumn as any, parentRefValue as any),
791
- eq(junctionTargetColumn as any, targetId as any),
792
- ),
793
- );
794
- if (existingLinks.length > 0) continue;
795
- }
796
-
797
- const parentRefValue = (row as Record<string, unknown>)[parentReferenceField];
798
- await tx.insert(junctionTable as any).values({
799
- [sourceField]: parentRefValue,
800
- [targetField]: targetId,
801
- } as any);
802
- }
803
- }
804
- }
805
-
806
- return rows;
807
- };
808
-
809
- let rows: Array<TableModel<TableName>>;
810
- if (relationPayloads.length > 0) {
811
- if (typeof transaction === "function") {
812
- try {
813
- rows = await transaction.call($db, (tx: any) =>
814
- executeUpdateWithRelations(tx),
815
- );
816
- } catch (error) {
817
- const message =
818
- error instanceof Error ? error.message : String(error);
819
- const lowered = message.toLowerCase();
820
- if (
821
- lowered.includes("failed query: begin") ||
822
- lowered.includes('near "begin"') ||
823
- lowered.includes("cannot start a transaction")
824
- ) {
825
- rows = await executeUpdateWithRelations($db as any);
826
- } else {
827
- throw error;
828
- }
829
- }
830
- } else {
831
- rows = await executeUpdateWithRelations($db as any);
832
- }
833
- } else {
834
- let updateQuery: any = ($db as any)
835
- .update(table as any)
836
- .set(cleanSetPayload as any);
837
-
838
- if (whereFilter) {
839
- updateQuery = updateQuery.where(whereFilter);
840
- }
841
- if (
842
- typeof args.limit === "number" &&
843
- typeof updateQuery.limit === "function"
844
- ) {
845
- updateQuery = updateQuery.limit(args.limit);
846
- }
847
- if (typeof updateQuery.returning === "function") {
848
- updateQuery = updateQuery.returning();
849
- }
850
-
851
- rows = (await updateQuery) as Array<TableModel<TableName>>;
852
- }
853
-
854
- emitMutation(
855
- "update",
856
- {
857
- set: args.set as unknown as Record<string, unknown>,
858
- where: (args.where ?? undefined) as unknown as Record<string, unknown>,
859
- limit: args.limit,
860
- },
861
- rows,
862
- );
863
- return rows;
864
- },
865
- upsert: async (args: QueryUpsertArgs<TableName>) => {
866
- const valuesArray = Array.isArray(args.values)
867
- ? args.values
868
- : [args.values];
869
- const values = Array.isArray(args.values)
870
- ? args.values
871
- : args.values;
872
-
873
- const targets = args.target
874
- ? Array.isArray(args.target)
875
- ? args.target
876
- : [args.target]
877
- : inferConflictTarget(table);
878
-
879
- if (targets.length === 0) {
880
- throw new Error(
881
- "Unable to infer conflict target for table " + tableName + ". Provide target explicitly.",
882
- );
883
- }
884
-
885
- const tableColumns = getTableColumns(table as never) as Record<string, unknown>;
886
- const targetColumns = targets
887
- .map((target) => tableColumns[target])
888
- .filter(Boolean);
889
-
890
- if (targetColumns.length === 0) {
891
- throw new Error(
892
- "Invalid conflict target for table " + tableName + ".",
893
- );
894
- }
895
-
896
- const rawSetPayload = (args.set ?? valuesArray[0] ?? {}) as Record<string, unknown>;
897
- const setPayload: Record<string, unknown> = {};
898
- for (const [key, value] of Object.entries(rawSetPayload)) {
899
- if (value !== undefined) {
900
- setPayload[key] = value;
901
- }
902
- }
903
-
904
- let upsertQuery: any = ($db as any)
905
- .insert(table as any)
906
- .values(values as any)
907
- .onConflictDoUpdate({
908
- target: targetColumns as any,
909
- set: setPayload as any,
910
- });
911
-
912
- if (typeof upsertQuery.returning === "function") {
913
- upsertQuery = upsertQuery.returning();
914
- }
915
-
916
- const rows = (await upsertQuery) as Array<TableModel<TableName>>;
917
- emitMutation(
918
- "upsert",
919
- {
920
- values: args.values as unknown as Record<string, unknown>,
921
- target: targets as unknown as Record<string, unknown>,
922
- set: (args.set ?? undefined) as unknown as Record<string, unknown>,
923
- },
924
- rows,
925
- );
926
- return rows;
927
- },
928
- delete: async (args?: QueryDeleteArgs<TableName>) => {
929
- const whereFilter = buildWhereFilter(
930
- table,
931
- args?.where as Record<string, unknown> | undefined,
932
- tableName,
933
- );
934
- let deleteQuery: any = ($db as any).delete(table as any);
935
-
936
- if (whereFilter) {
937
- deleteQuery = deleteQuery.where(whereFilter);
938
- }
939
- if (typeof args?.limit === "number" && typeof deleteQuery.limit === "function") {
940
- deleteQuery = deleteQuery.limit(args.limit);
941
- }
942
- if (typeof deleteQuery.returning === "function") {
943
- deleteQuery = deleteQuery.returning();
944
- }
945
-
946
- const rows = (await deleteQuery) as Array<TableModel<TableName>>;
947
- emitMutation(
948
- "delete",
949
- {
950
- where: (args?.where ?? undefined) as unknown as Record<string, unknown>,
951
- limit: args?.limit,
952
- },
953
- rows,
954
- );
955
- return rows;
956
- },
957
- `;
958
- }
1
+ export function generateQueryRuntimeWriteSection(): string {
2
+ return ` insert: async <TArgs extends QueryInsertArgs<TableName>>(args: TArgs) => {
3
+ const transaction = ($db as any).transaction;
4
+
5
+ const valuesArray = Array.isArray(args.values)
6
+ ? args.values
7
+ : [args.values];
8
+ const baseValuesArray: Array<Record<string, unknown>> = [];
9
+ const postInsertRelations: Array<
10
+ Array<{
11
+ relationName: string;
12
+ relation: RuntimeRelation;
13
+ value: unknown;
14
+ }>
15
+ > = [];
16
+
17
+ const toRelationItems = (
18
+ relation: RuntimeRelation,
19
+ relationName: string,
20
+ value: unknown,
21
+ ): unknown[] => {
22
+ if (value === undefined || value === null) {
23
+ return [];
24
+ }
25
+
26
+ if (relation.kind === "one") {
27
+ if (Array.isArray(value)) {
28
+ throw new Error(
29
+ "Relation '" +
30
+ tableName +
31
+ "." +
32
+ relationName +
33
+ "' expects a single value.",
34
+ );
35
+ }
36
+ return [value];
37
+ }
38
+
39
+ return Array.isArray(value) ? value : [value];
40
+ };
41
+
42
+ const resolveRelationIdentifier = async (
43
+ tx: any,
44
+ relation: RuntimeRelation,
45
+ relationName: string,
46
+ input: unknown,
47
+ ): Promise<unknown> => {
48
+ if (
49
+ typeof input === "string" ||
50
+ typeof input === "number" ||
51
+ typeof input === "bigint"
52
+ ) {
53
+ return input;
54
+ }
55
+
56
+ if (!isRecord(input)) {
57
+ throw new Error(
58
+ "Relation '" +
59
+ tableName +
60
+ "." +
61
+ relationName +
62
+ "' expects an id or object payload.",
63
+ );
64
+ }
65
+
66
+ const referenceField =
67
+ relation.kind === "manyToMany"
68
+ ? (relation.targetReferenceField ?? "id")
69
+ : (relation.referenceField ?? "id");
70
+ const existingId = input[referenceField];
71
+ if (existingId !== undefined && existingId !== null) {
72
+ return existingId;
73
+ }
74
+
75
+ const targetTable = (mergedSchema as Record<string, unknown>)[
76
+ relation.targetTable
77
+ ];
78
+ if (!targetTable) {
79
+ throw new Error(
80
+ "Unknown target table '" +
81
+ relation.targetTable +
82
+ "' for relation '" +
83
+ tableName +
84
+ "." +
85
+ relationName +
86
+ "'.",
87
+ );
88
+ }
89
+
90
+ let createQuery: any = tx.insert(targetTable as any).values(input as any);
91
+ if (typeof createQuery.returning === "function") {
92
+ createQuery = createQuery.returning();
93
+ }
94
+ const createdRows = (await createQuery) as Array<Record<string, unknown>>;
95
+ const created = createdRows[0];
96
+ if (!created) {
97
+ throw new Error(
98
+ "Failed to create relation target for '" +
99
+ tableName +
100
+ "." +
101
+ relationName +
102
+ "'.",
103
+ );
104
+ }
105
+
106
+ const createdId = created[referenceField];
107
+ if (createdId === undefined || createdId === null) {
108
+ throw new Error(
109
+ "Created relation target for '" +
110
+ tableName +
111
+ "." +
112
+ relationName +
113
+ "' is missing '" +
114
+ referenceField +
115
+ "'.",
116
+ );
117
+ }
118
+
119
+ return createdId;
120
+ };
121
+
122
+ const getRelationTargetTable = (
123
+ relation: RuntimeRelation,
124
+ relationName: string,
125
+ ): any => {
126
+ const targetTable = (mergedSchema as Record<string, unknown>)[
127
+ relation.targetTable
128
+ ];
129
+ if (!targetTable) {
130
+ throw new Error(
131
+ "Unknown target table '" +
132
+ relation.targetTable +
133
+ "' for relation '" +
134
+ tableName +
135
+ "." +
136
+ relationName +
137
+ "'.",
138
+ );
139
+ }
140
+ return targetTable;
141
+ };
142
+
143
+ const getRelationReferenceField = (relation: RuntimeRelation): string => {
144
+ if (relation.kind === "manyToMany") {
145
+ return relation.targetReferenceField ?? "id";
146
+ }
147
+ return relation.referenceField ?? "id";
148
+ };
149
+
150
+ const fetchRelatedRowByIdentifier = async (
151
+ tx: any,
152
+ relation: RuntimeRelation,
153
+ relationName: string,
154
+ identifier: unknown,
155
+ ): Promise<Record<string, unknown> | null> => {
156
+ if (identifier === undefined || identifier === null) {
157
+ return null;
158
+ }
159
+
160
+ const targetTable = getRelationTargetTable(relation, relationName);
161
+ const referenceField = getRelationReferenceField(relation);
162
+ const referenceColumn = (targetTable as Record<string, unknown>)[
163
+ referenceField
164
+ ];
165
+ if (!referenceColumn) {
166
+ throw new Error(
167
+ "Target table '" +
168
+ relation.targetTable +
169
+ "' is missing column '" +
170
+ referenceField +
171
+ "' required by relation '" +
172
+ tableName +
173
+ "." +
174
+ relationName +
175
+ "'.",
176
+ );
177
+ }
178
+
179
+ let query: any = tx
180
+ .select()
181
+ .from(targetTable as any)
182
+ .where(eq(referenceColumn as any, identifier as any));
183
+ if (typeof query.limit === "function") {
184
+ query = query.limit(1);
185
+ }
186
+ const rows = (await query) as Array<Record<string, unknown>>;
187
+ return rows[0] ?? null;
188
+ };
189
+
190
+ for (const inputValue of valuesArray) {
191
+ const scalarValues: Record<string, unknown> = {};
192
+ const relationInputs: Array<{
193
+ relationName: string;
194
+ relation: RuntimeRelation;
195
+ value: unknown;
196
+ }> = [];
197
+
198
+ const valueRecord = (inputValue ?? {}) as Record<string, unknown>;
199
+ for (const [fieldName, fieldValue] of Object.entries(valueRecord)) {
200
+ const runtimeRelation = getRuntimeRelation(tableName, fieldName);
201
+ if (runtimeRelation) {
202
+ relationInputs.push({
203
+ relationName: fieldName,
204
+ relation: runtimeRelation,
205
+ value: fieldValue,
206
+ });
207
+ continue;
208
+ }
209
+
210
+ if (fieldValue === undefined) {
211
+ continue;
212
+ }
213
+
214
+ scalarValues[fieldName] = fieldValue;
215
+ }
216
+
217
+ baseValuesArray.push(scalarValues);
218
+ postInsertRelations.push(relationInputs);
219
+ }
220
+
221
+ const executeInsertGraph = async (
222
+ tx: any,
223
+ ): Promise<Array<QueryInsertResultRow<TableName, TArgs>>> => {
224
+ for (let index = 0; index < baseValuesArray.length; index += 1) {
225
+ const relationInputs = postInsertRelations[index] ?? [];
226
+ for (const relationInput of relationInputs) {
227
+ if (relationInput.relation.kind !== "one") {
228
+ continue;
229
+ }
230
+
231
+ const sourceField = relationInput.relation.sourceField;
232
+ if (!sourceField) {
233
+ throw new Error(
234
+ "Relation '" +
235
+ tableName +
236
+ "." +
237
+ relationInput.relationName +
238
+ "' is missing sourceField metadata.",
239
+ );
240
+ }
241
+
242
+ const oneItems = toRelationItems(
243
+ relationInput.relation,
244
+ relationInput.relationName,
245
+ relationInput.value,
246
+ );
247
+ if (oneItems.length === 0) {
248
+ continue;
249
+ }
250
+
251
+ const relationId = await resolveRelationIdentifier(
252
+ tx,
253
+ relationInput.relation,
254
+ relationInput.relationName,
255
+ oneItems[0],
256
+ );
257
+ const existingValue = baseValuesArray[index][sourceField];
258
+ if (
259
+ existingValue !== undefined &&
260
+ existingValue !== relationId
261
+ ) {
262
+ throw new Error(
263
+ "Insert payload for '" +
264
+ tableName +
265
+ "' has conflicting values for '" +
266
+ sourceField +
267
+ "'.",
268
+ );
269
+ }
270
+ baseValuesArray[index][sourceField] = relationId;
271
+ }
272
+ }
273
+
274
+ let insertQuery: any = tx.insert(table as any).values(baseValuesArray as any);
275
+ if (typeof insertQuery.returning === "function") {
276
+ insertQuery = insertQuery.returning();
277
+ }
278
+ const insertedRows = (await insertQuery) as Array<TableModel<TableName>>;
279
+ const hydratedRows: Array<Record<string, unknown>> = [];
280
+
281
+ for (let index = 0; index < insertedRows.length; index += 1) {
282
+ const parentRow = insertedRows[index] as unknown as Record<string, unknown>;
283
+ const hydratedRow: Record<string, unknown> = {
284
+ ...parentRow,
285
+ };
286
+ const relationInputs = postInsertRelations[index] ?? [];
287
+
288
+ for (const relationInput of relationInputs) {
289
+ if (relationInput.relation.kind === "one") {
290
+ const sourceField = relationInput.relation.sourceField;
291
+ if (!sourceField) {
292
+ throw new Error(
293
+ "Relation '" +
294
+ tableName +
295
+ "." +
296
+ relationInput.relationName +
297
+ "' is missing sourceField metadata.",
298
+ );
299
+ }
300
+
301
+ const relationItems = toRelationItems(
302
+ relationInput.relation,
303
+ relationInput.relationName,
304
+ relationInput.value,
305
+ );
306
+ if (relationItems.length === 0) {
307
+ hydratedRow[relationInput.relationName] = null;
308
+ continue;
309
+ }
310
+
311
+ const sourceIdentifier = parentRow[sourceField];
312
+ hydratedRow[relationInput.relationName] =
313
+ (await fetchRelatedRowByIdentifier(
314
+ tx,
315
+ relationInput.relation,
316
+ relationInput.relationName,
317
+ sourceIdentifier,
318
+ )) ?? null;
319
+ continue;
320
+ }
321
+
322
+ const sourceReferenceField = relationInput.relation.referenceField ?? "id";
323
+ const parentReferenceValue = parentRow[sourceReferenceField];
324
+ if (
325
+ parentReferenceValue === undefined ||
326
+ parentReferenceValue === null
327
+ ) {
328
+ throw new Error(
329
+ "Inserted row for '" +
330
+ tableName +
331
+ "' is missing '" +
332
+ sourceReferenceField +
333
+ "' required by relation '" +
334
+ relationInput.relationName +
335
+ "'.",
336
+ );
337
+ }
338
+
339
+ const relatedRows: Array<Record<string, unknown>> = [];
340
+ hydratedRow[relationInput.relationName] = relatedRows;
341
+
342
+ const relationItems = toRelationItems(
343
+ relationInput.relation,
344
+ relationInput.relationName,
345
+ relationInput.value,
346
+ );
347
+ if (relationItems.length === 0) {
348
+ continue;
349
+ }
350
+
351
+ if (relationInput.relation.kind === "many") {
352
+ const targetTable = getRelationTargetTable(
353
+ relationInput.relation,
354
+ relationInput.relationName,
355
+ );
356
+
357
+ const sourceField = relationInput.relation.sourceField;
358
+ const targetReferenceField =
359
+ relationInput.relation.referenceField ?? "id";
360
+ const targetReferenceColumn =
361
+ (targetTable as Record<string, unknown>)[targetReferenceField];
362
+ if (!sourceField) {
363
+ throw new Error(
364
+ "Relation '" +
365
+ tableName +
366
+ "." +
367
+ relationInput.relationName +
368
+ "' is missing sourceField metadata.",
369
+ );
370
+ }
371
+ if (!targetReferenceColumn) {
372
+ throw new Error(
373
+ "Target table '" +
374
+ relationInput.relation.targetTable +
375
+ "' is missing column '" +
376
+ targetReferenceField +
377
+ "' required by relation '" +
378
+ tableName +
379
+ "." +
380
+ relationInput.relationName +
381
+ "'.",
382
+ );
383
+ }
384
+
385
+ for (const relationItem of relationItems) {
386
+ if (
387
+ typeof relationItem === "string" ||
388
+ typeof relationItem === "number" ||
389
+ typeof relationItem === "bigint"
390
+ ) {
391
+ await tx
392
+ .update(targetTable as any)
393
+ .set({ [sourceField]: parentReferenceValue } as any)
394
+ .where(
395
+ eq(targetReferenceColumn as any, relationItem as any),
396
+ );
397
+ const linkedRow = await fetchRelatedRowByIdentifier(
398
+ tx,
399
+ relationInput.relation,
400
+ relationInput.relationName,
401
+ relationItem,
402
+ );
403
+ if (linkedRow) {
404
+ relatedRows.push(linkedRow);
405
+ }
406
+ continue;
407
+ }
408
+
409
+ if (!isRecord(relationItem)) {
410
+ throw new Error(
411
+ "Relation '" +
412
+ tableName +
413
+ "." +
414
+ relationInput.relationName +
415
+ "' expects id or object payloads.",
416
+ );
417
+ }
418
+
419
+ const existingId = relationItem[targetReferenceField];
420
+ const payload = {
421
+ ...relationItem,
422
+ [sourceField]: parentReferenceValue,
423
+ };
424
+
425
+ if (existingId !== undefined && existingId !== null) {
426
+ const setPayload: Record<string, unknown> = {
427
+ ...payload,
428
+ };
429
+ delete setPayload[targetReferenceField];
430
+ await tx
431
+ .update(targetTable as any)
432
+ .set(setPayload as any)
433
+ .where(
434
+ eq(targetReferenceColumn as any, existingId as any),
435
+ );
436
+ const linkedRow = await fetchRelatedRowByIdentifier(
437
+ tx,
438
+ relationInput.relation,
439
+ relationInput.relationName,
440
+ existingId,
441
+ );
442
+ if (linkedRow) {
443
+ relatedRows.push(linkedRow);
444
+ }
445
+ continue;
446
+ }
447
+
448
+ let createQuery: any = tx
449
+ .insert(targetTable as any)
450
+ .values(payload as any);
451
+ if (typeof createQuery.returning === "function") {
452
+ createQuery = createQuery.returning();
453
+ }
454
+ const createdRows = (await createQuery) as Array<
455
+ Record<string, unknown>
456
+ >;
457
+ const createdRow = createdRows[0];
458
+ if (createdRow) {
459
+ relatedRows.push(createdRow);
460
+ }
461
+ }
462
+ continue;
463
+ }
464
+
465
+ const junctionTable = (mergedSchema as Record<string, unknown>)[
466
+ relationInput.relation.junctionTable
467
+ ];
468
+ if (!junctionTable) {
469
+ throw new Error(
470
+ "Unknown junction table '" +
471
+ relationInput.relation.junctionTable +
472
+ "' for relation '" +
473
+ tableName +
474
+ "." +
475
+ relationInput.relationName +
476
+ "'.",
477
+ );
478
+ }
479
+
480
+ const sourceField = relationInput.relation.sourceField;
481
+ const targetField = relationInput.relation.targetField;
482
+ if (!sourceField || !targetField) {
483
+ throw new Error(
484
+ "Relation '" +
485
+ tableName +
486
+ "." +
487
+ relationInput.relationName +
488
+ "' is missing junction metadata fields.",
489
+ );
490
+ }
491
+
492
+ const linkValues: Array<Record<string, unknown>> = [];
493
+ const targetIdentifiers: unknown[] = [];
494
+ for (const relationItem of relationItems) {
495
+ const targetId = await resolveRelationIdentifier(
496
+ tx,
497
+ relationInput.relation,
498
+ relationInput.relationName,
499
+ relationItem,
500
+ );
501
+ targetIdentifiers.push(targetId);
502
+
503
+ linkValues.push({
504
+ [sourceField]: parentReferenceValue,
505
+ [targetField]: targetId,
506
+ });
507
+ }
508
+
509
+ if (linkValues.length > 0) {
510
+ await tx.insert(junctionTable as any).values(linkValues as any);
511
+ }
512
+
513
+ for (const targetId of targetIdentifiers) {
514
+ const linkedRow = await fetchRelatedRowByIdentifier(
515
+ tx,
516
+ relationInput.relation,
517
+ relationInput.relationName,
518
+ targetId,
519
+ );
520
+ if (linkedRow) {
521
+ relatedRows.push(linkedRow);
522
+ }
523
+ }
524
+ }
525
+
526
+ hydratedRows.push(hydratedRow);
527
+ }
528
+
529
+ return hydratedRows as Array<QueryInsertResultRow<TableName, TArgs>>;
530
+ };
531
+
532
+ let rows: Array<QueryInsertResultRow<TableName, TArgs>>;
533
+ if (typeof transaction === "function") {
534
+ try {
535
+ rows = await transaction.call($db, (tx: any) =>
536
+ executeInsertGraph(tx),
537
+ );
538
+ } catch (error) {
539
+ const message =
540
+ error instanceof Error ? error.message : String(error);
541
+ const lowered = message.toLowerCase();
542
+ if (
543
+ lowered.includes("failed query: begin") ||
544
+ lowered.includes('near "begin"') ||
545
+ lowered.includes("cannot start a transaction")
546
+ ) {
547
+ rows = await executeInsertGraph($db as any);
548
+ } else {
549
+ throw error;
550
+ }
551
+ }
552
+ } else {
553
+ rows = await executeInsertGraph($db as any);
554
+ }
555
+
556
+ emitMutation(
557
+ "insert",
558
+ { values: args.values as unknown as Record<string, unknown> },
559
+ rows,
560
+ );
561
+ return rows;
562
+ },
563
+ update: async (args: QueryUpdateArgs<TableName>) => {
564
+ const transaction = ($db as any).transaction;
565
+
566
+ const whereFilter = buildWhereFilter(
567
+ table,
568
+ args.where as Record<string, unknown> | undefined,
569
+ tableName,
570
+ );
571
+
572
+ const setPayload = args.set as Record<string, unknown>;
573
+ const cleanSetPayload: Record<string, unknown> = {};
574
+ const relationPayloads: Array<{
575
+ relationName: string;
576
+ relation: RuntimeRelation;
577
+ value: unknown;
578
+ }> = [];
579
+
580
+ for (const [key, value] of Object.entries(setPayload)) {
581
+ if (value === undefined) continue;
582
+ const runtimeRelation = getRuntimeRelation(tableName, key);
583
+ if (runtimeRelation && runtimeRelation.kind === "manyToMany") {
584
+ relationPayloads.push({
585
+ relationName: key,
586
+ relation: runtimeRelation,
587
+ value,
588
+ });
589
+ continue;
590
+ }
591
+ cleanSetPayload[key] = value;
592
+ }
593
+
594
+ const normalizeRelationValue = (
595
+ value: unknown,
596
+ ): { items: unknown[]; mode: "merge" | "overwrite" } => {
597
+ if (Array.isArray(value)) {
598
+ return { items: value, mode: "merge" };
599
+ }
600
+ if (value && typeof value === "object") {
601
+ const record = value as Record<string, unknown>;
602
+ const items = Array.isArray(record.items) ? record.items : [];
603
+ const mode = record.mode === "overwrite" ? "overwrite" : "merge";
604
+ return { items, mode };
605
+ }
606
+ return { items: [], mode: "merge" };
607
+ };
608
+
609
+ const executeUpdateWithRelations = async (
610
+ tx: any,
611
+ ): Promise<Array<TableModel<TableName>>> => {
612
+ let updateQuery: any = tx
613
+ .update(table as any)
614
+ .set(cleanSetPayload as any);
615
+
616
+ if (whereFilter) {
617
+ updateQuery = updateQuery.where(whereFilter);
618
+ }
619
+ if (
620
+ typeof args.limit === "number" &&
621
+ typeof updateQuery.limit === "function"
622
+ ) {
623
+ updateQuery = updateQuery.limit(args.limit);
624
+ }
625
+ if (typeof updateQuery.returning === "function") {
626
+ updateQuery = updateQuery.returning();
627
+ }
628
+
629
+ const rows = (await updateQuery) as Array<TableModel<TableName>>;
630
+ if (rows.length === 0) return rows;
631
+
632
+ for (const row of rows) {
633
+ const parentId = (row as Record<string, unknown>)["id"];
634
+ if (parentId === undefined || parentId === null) continue;
635
+
636
+ for (const { relationName, relation, value } of relationPayloads) {
637
+ const { items, mode } = normalizeRelationValue(value);
638
+
639
+ const junctionTable = (mergedSchema as Record<string, unknown>)[
640
+ relation.junctionTable
641
+ ];
642
+ if (!junctionTable) {
643
+ throw new Error(
644
+ "Unknown junction table '" +
645
+ relation.junctionTable +
646
+ "' for relation '" +
647
+ tableName +
648
+ "." +
649
+ relationName +
650
+ "'.",
651
+ );
652
+ }
653
+
654
+ const sourceField = relation.sourceField;
655
+ const targetField = relation.targetField;
656
+ if (!sourceField || !targetField) {
657
+ throw new Error(
658
+ "Relation '" +
659
+ tableName +
660
+ "." +
661
+ relationName +
662
+ "' is missing junction metadata fields.",
663
+ );
664
+ }
665
+
666
+ const parentReferenceField = relation.referenceField ?? "id";
667
+ const parentReferenceColumn = (table as Record<string, unknown>)[parentReferenceField];
668
+ if (!parentReferenceColumn) {
669
+ throw new Error(
670
+ "Table '" +
671
+ tableName +
672
+ "' is missing column '" +
673
+ parentReferenceField +
674
+ "' for relation '" +
675
+ relationName +
676
+ "'.",
677
+ );
678
+ }
679
+
680
+ const junctionSourceColumn = (junctionTable as Record<string, unknown>)[sourceField];
681
+ if (!junctionSourceColumn) {
682
+ throw new Error(
683
+ "Junction table '" +
684
+ relation.junctionTable +
685
+ "' is missing column '" +
686
+ sourceField +
687
+ "' for relation '" +
688
+ relationName +
689
+ "'.",
690
+ );
691
+ }
692
+
693
+ if (mode === "overwrite") {
694
+ await tx
695
+ .delete(junctionTable as any)
696
+ .where(eq(junctionSourceColumn as any, (row as Record<string, unknown>)[parentReferenceField] as any));
697
+ }
698
+
699
+ const referenceField = relation.targetReferenceField ?? "id";
700
+ const targetTable = (mergedSchema as Record<string, unknown>)[
701
+ relation.targetTable
702
+ ];
703
+
704
+ const junctionTargetColumn =
705
+ (junctionTable as Record<string, unknown>)[targetField];
706
+ if (!junctionTargetColumn) {
707
+ throw new Error(
708
+ "Junction table '" +
709
+ relation.junctionTable +
710
+ "' is missing column '" +
711
+ targetField +
712
+ "'.",
713
+ );
714
+ }
715
+
716
+ for (const item of items) {
717
+ let targetId: unknown;
718
+
719
+ if (
720
+ typeof item === "string" ||
721
+ typeof item === "number" ||
722
+ typeof item === "bigint"
723
+ ) {
724
+ targetId = item;
725
+ } else if (item && typeof item === "object") {
726
+ const record = item as Record<string, unknown>;
727
+ const existingId = record[referenceField];
728
+ if (existingId !== undefined && existingId !== null) {
729
+ targetId = existingId;
730
+ } else {
731
+ if (!targetTable) {
732
+ throw new Error(
733
+ "Unknown target table '" +
734
+ relation.targetTable +
735
+ "' for relation '" +
736
+ tableName +
737
+ "." +
738
+ relationName +
739
+ "'.",
740
+ );
741
+ }
742
+ let createQuery: any = tx
743
+ .insert(targetTable as any)
744
+ .values(record as any);
745
+ if (typeof createQuery.returning === "function") {
746
+ createQuery = createQuery.returning();
747
+ }
748
+ const createdRows =
749
+ (await createQuery) as Array<Record<string, unknown>>;
750
+ const created = createdRows[0];
751
+ if (!created) {
752
+ throw new Error(
753
+ "Failed to create relation target for '" +
754
+ tableName +
755
+ "." +
756
+ relationName +
757
+ "'.",
758
+ );
759
+ }
760
+ targetId = created[referenceField];
761
+ if (targetId === undefined || targetId === null) {
762
+ throw new Error(
763
+ "Created relation target for '" +
764
+ tableName +
765
+ "." +
766
+ relationName +
767
+ "' is missing '" +
768
+ referenceField +
769
+ "'.",
770
+ );
771
+ }
772
+ }
773
+ } else {
774
+ throw new Error(
775
+ "Relation '" +
776
+ tableName +
777
+ "." +
778
+ relationName +
779
+ "' expects an id or object payload.",
780
+ );
781
+ }
782
+
783
+ if (mode === "merge") {
784
+ const parentRefValue = (row as Record<string, unknown>)[parentReferenceField];
785
+ const existingLinks = await tx
786
+ .select()
787
+ .from(junctionTable as any)
788
+ .where(
789
+ and(
790
+ eq(junctionSourceColumn as any, parentRefValue as any),
791
+ eq(junctionTargetColumn as any, targetId as any),
792
+ ),
793
+ );
794
+ if (existingLinks.length > 0) continue;
795
+ }
796
+
797
+ const parentRefValue = (row as Record<string, unknown>)[parentReferenceField];
798
+ await tx.insert(junctionTable as any).values({
799
+ [sourceField]: parentRefValue,
800
+ [targetField]: targetId,
801
+ } as any);
802
+ }
803
+ }
804
+ }
805
+
806
+ return rows;
807
+ };
808
+
809
+ let rows: Array<TableModel<TableName>>;
810
+ if (relationPayloads.length > 0) {
811
+ if (typeof transaction === "function") {
812
+ try {
813
+ rows = await transaction.call($db, (tx: any) =>
814
+ executeUpdateWithRelations(tx),
815
+ );
816
+ } catch (error) {
817
+ const message =
818
+ error instanceof Error ? error.message : String(error);
819
+ const lowered = message.toLowerCase();
820
+ if (
821
+ lowered.includes("failed query: begin") ||
822
+ lowered.includes('near "begin"') ||
823
+ lowered.includes("cannot start a transaction")
824
+ ) {
825
+ rows = await executeUpdateWithRelations($db as any);
826
+ } else {
827
+ throw error;
828
+ }
829
+ }
830
+ } else {
831
+ rows = await executeUpdateWithRelations($db as any);
832
+ }
833
+ } else {
834
+ let updateQuery: any = ($db as any)
835
+ .update(table as any)
836
+ .set(cleanSetPayload as any);
837
+
838
+ if (whereFilter) {
839
+ updateQuery = updateQuery.where(whereFilter);
840
+ }
841
+ if (
842
+ typeof args.limit === "number" &&
843
+ typeof updateQuery.limit === "function"
844
+ ) {
845
+ updateQuery = updateQuery.limit(args.limit);
846
+ }
847
+ if (typeof updateQuery.returning === "function") {
848
+ updateQuery = updateQuery.returning();
849
+ }
850
+
851
+ rows = (await updateQuery) as Array<TableModel<TableName>>;
852
+ }
853
+
854
+ emitMutation(
855
+ "update",
856
+ {
857
+ set: args.set as unknown as Record<string, unknown>,
858
+ where: (args.where ?? undefined) as unknown as Record<string, unknown>,
859
+ limit: args.limit,
860
+ },
861
+ rows,
862
+ );
863
+ return rows;
864
+ },
865
+ upsert: async (args: QueryUpsertArgs<TableName>) => {
866
+ const valuesArray = Array.isArray(args.values)
867
+ ? args.values
868
+ : [args.values];
869
+ const values = Array.isArray(args.values)
870
+ ? args.values
871
+ : args.values;
872
+
873
+ const targets = args.target
874
+ ? Array.isArray(args.target)
875
+ ? args.target
876
+ : [args.target]
877
+ : inferConflictTarget(table);
878
+
879
+ if (targets.length === 0) {
880
+ throw new Error(
881
+ "Unable to infer conflict target for table " + tableName + ". Provide target explicitly.",
882
+ );
883
+ }
884
+
885
+ const tableColumns = getTableColumns(table as never) as Record<string, unknown>;
886
+ const targetColumns = targets
887
+ .map((target) => tableColumns[target])
888
+ .filter(Boolean);
889
+
890
+ if (targetColumns.length === 0) {
891
+ throw new Error(
892
+ "Invalid conflict target for table " + tableName + ".",
893
+ );
894
+ }
895
+
896
+ const rawSetPayload = (args.set ?? valuesArray[0] ?? {}) as Record<string, unknown>;
897
+ const setPayload: Record<string, unknown> = {};
898
+ for (const [key, value] of Object.entries(rawSetPayload)) {
899
+ if (value !== undefined) {
900
+ setPayload[key] = value;
901
+ }
902
+ }
903
+
904
+ let upsertQuery: any = ($db as any)
905
+ .insert(table as any)
906
+ .values(values as any)
907
+ .onConflictDoUpdate({
908
+ target: targetColumns as any,
909
+ set: setPayload as any,
910
+ });
911
+
912
+ if (typeof upsertQuery.returning === "function") {
913
+ upsertQuery = upsertQuery.returning();
914
+ }
915
+
916
+ const rows = (await upsertQuery) as Array<TableModel<TableName>>;
917
+ emitMutation(
918
+ "upsert",
919
+ {
920
+ values: args.values as unknown as Record<string, unknown>,
921
+ target: targets as unknown as Record<string, unknown>,
922
+ set: (args.set ?? undefined) as unknown as Record<string, unknown>,
923
+ },
924
+ rows,
925
+ );
926
+ return rows;
927
+ },
928
+ delete: async (args?: QueryDeleteArgs<TableName>) => {
929
+ const whereFilter = buildWhereFilter(
930
+ table,
931
+ args?.where as Record<string, unknown> | undefined,
932
+ tableName,
933
+ );
934
+ let deleteQuery: any = ($db as any).delete(table as any);
935
+
936
+ if (whereFilter) {
937
+ deleteQuery = deleteQuery.where(whereFilter);
938
+ }
939
+ if (typeof args?.limit === "number" && typeof deleteQuery.limit === "function") {
940
+ deleteQuery = deleteQuery.limit(args.limit);
941
+ }
942
+ if (typeof deleteQuery.returning === "function") {
943
+ deleteQuery = deleteQuery.returning();
944
+ }
945
+
946
+ const rows = (await deleteQuery) as Array<TableModel<TableName>>;
947
+ emitMutation(
948
+ "delete",
949
+ {
950
+ where: (args?.where ?? undefined) as unknown as Record<string, unknown>,
951
+ limit: args?.limit,
952
+ },
953
+ rows,
954
+ );
955
+ return rows;
956
+ },
957
+ `;
958
+ }