appflare 0.2.30 → 0.2.31

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 (140) hide show
  1. package/Documentation.md +758 -758
  2. package/cli/commands/index.ts +238 -238
  3. package/cli/generate.ts +178 -178
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1183 -1183
  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 +748 -748
  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 +186 -186
  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 +171 -171
  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 +554 -554
  45. package/cli/templates/dashboard/builders/navigation.ts +122 -122
  46. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  47. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  48. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  49. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  50. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  51. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  52. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  53. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  55. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  56. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  57. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  58. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  59. package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
  60. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  61. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  62. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  63. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  64. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  65. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  66. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  67. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  68. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  69. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  70. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  71. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  72. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  73. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  74. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  75. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  76. package/cli/templates/dashboard/components/layout.ts +388 -388
  77. package/cli/templates/dashboard/components/login-page.ts +65 -65
  78. package/cli/templates/dashboard/index.ts +61 -61
  79. package/cli/templates/dashboard/types.ts +9 -9
  80. package/cli/templates/handlers/README.md +353 -353
  81. package/cli/templates/handlers/auth.ts +37 -37
  82. package/cli/templates/handlers/execution.ts +42 -42
  83. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  84. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  85. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  86. package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
  87. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  88. package/cli/templates/handlers/generators/context/types.ts +40 -40
  89. package/cli/templates/handlers/generators/context.ts +43 -43
  90. package/cli/templates/handlers/generators/execution.ts +15 -15
  91. package/cli/templates/handlers/generators/handlers.ts +13 -13
  92. package/cli/templates/handlers/generators/registration/modules/cron.ts +26 -26
  93. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  94. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  95. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  96. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  97. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  98. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  99. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +516 -516
  100. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -56
  101. package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
  102. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  103. package/cli/templates/handlers/generators/types/context.ts +92 -92
  104. package/cli/templates/handlers/generators/types/core.ts +106 -106
  105. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  106. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +281 -259
  107. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  108. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1103 -1031
  109. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -246
  110. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  111. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +157 -121
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +697 -697
  116. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  117. package/cli/templates/handlers/index.ts +43 -43
  118. package/cli/templates/handlers/operations.ts +116 -116
  119. package/cli/templates/handlers/registration.ts +91 -91
  120. package/cli/templates/handlers/types.ts +15 -15
  121. package/cli/templates/handlers/utils.ts +48 -48
  122. package/cli/types.ts +110 -110
  123. package/cli/utils/handler-discovery.ts +466 -466
  124. package/cli/utils/json-utils.ts +24 -24
  125. package/cli/utils/path-utils.ts +19 -19
  126. package/cli/utils/schema-discovery.ts +399 -399
  127. package/dist/cli/index.d.mts +2 -0
  128. package/dist/cli/index.d.ts +2 -0
  129. package/dist/cli/index.js +270 -108
  130. package/dist/cli/index.mjs +270 -108
  131. package/index.ts +18 -18
  132. package/package.json +58 -58
  133. package/react/index.ts +5 -5
  134. package/react/use-infinite-query.ts +252 -252
  135. package/react/use-mutation.ts +89 -89
  136. package/react/use-query.ts +207 -207
  137. package/schema.ts +415 -415
  138. package/test-better-auth-hash.ts +2 -2
  139. package/tsconfig.json +6 -6
  140. package/tsup.config.ts +82 -82
@@ -1,1031 +1,1103 @@
1
- export function generateQueryHelperFunctionsSection(): string {
2
- return `function isRecord(value: unknown): value is Record<string, unknown> {
3
- return typeof value === "object" && value !== null;
4
- }
5
-
6
- function readOperatorValue(record: Record<string, unknown>, key: string): unknown {
7
- if (record[key] !== undefined) {
8
- return record[key];
9
- }
10
- const prefixed = "$" + key;
11
- return record[prefixed];
12
- }
13
-
14
- function normalizeGeoPoint(
15
- value: GeoPoint | GeoCoordinates,
16
- ): { latitude: number; longitude: number } | null {
17
- if ((value as GeoPoint).latitude !== undefined) {
18
- const point = value as GeoPoint;
19
- if (
20
- typeof point.latitude === "number" &&
21
- typeof point.longitude === "number"
22
- ) {
23
- return {
24
- latitude: point.latitude,
25
- longitude: point.longitude,
26
- };
27
- }
28
- return null;
29
- }
30
-
31
- const coordinates = (value as GeoCoordinates).coordinates;
32
- if (!Array.isArray(coordinates) || coordinates.length < 2) {
33
- return null;
34
- }
35
-
36
- const [longitude, latitude] = coordinates;
37
- if (typeof latitude !== "number" || typeof longitude !== "number") {
38
- return null;
39
- }
40
-
41
- return {
42
- latitude,
43
- longitude,
44
- };
45
- }
46
-
47
- function createGeoDistanceFilter(
48
- operand: GeoWithinOperand,
49
- fields: Record<string, unknown>,
50
- tableName?: string,
51
- fallbackLatitudeField?: string,
52
- ): SQL | undefined {
53
- const geometry = normalizeGeoPoint(operand.$geometry);
54
- if (!geometry) {
55
- return undefined;
56
- }
57
-
58
- const latitudeField = operand.latitudeField ?? fallbackLatitudeField;
59
- const longitudeField = operand.longitudeField;
60
- if (!latitudeField || !longitudeField) {
61
- return undefined;
62
- }
63
-
64
- const latitudeColumn = fields[latitudeField];
65
- if (!latitudeColumn) {
66
- console.warn(
67
- "Invalid geoWithin latitudeField",
68
- tableName ?? "unknown",
69
- latitudeField,
70
- );
71
- return undefined;
72
- }
73
-
74
- const longitudeColumn = fields[longitudeField];
75
- if (!longitudeColumn) {
76
- console.warn(
77
- "Invalid geoWithin longitudeField",
78
- tableName ?? "unknown",
79
- longitudeField,
80
- );
81
- return undefined;
82
- }
83
-
84
- const distanceExpression = sql<number>\`(
85
- 6371000 * acos(
86
- cos(radians(\${geometry.latitude})) * cos(radians(\${latitudeColumn}))
87
- * cos(radians(\${longitudeColumn}) - radians(\${geometry.longitude}))
88
- + sin(radians(\${geometry.latitude})) * sin(radians(\${latitudeColumn}))
89
- )
90
- )\`;
91
-
92
- const minDistance =
93
- operand.$minDistance ??
94
- operand.gte ??
95
- operand.$gte ??
96
- operand.gt ??
97
- operand.$gt;
98
- const maxDistance =
99
- operand.$maxDistance ??
100
- operand.lte ??
101
- operand.$lte ??
102
- operand.lt ??
103
- operand.$lt;
104
-
105
- const filters: SQL[] = [];
106
- if (typeof minDistance === "number") {
107
- filters.push(sql\`\${distanceExpression} >= \${minDistance}\`);
108
- }
109
- if (typeof maxDistance === "number") {
110
- filters.push(sql\`\${distanceExpression} <= \${maxDistance}\`);
111
- }
112
-
113
- if (filters.length === 0) {
114
- return undefined;
115
- }
116
-
117
- if (filters.length === 1) {
118
- return filters[0];
119
- }
120
-
121
- return and(...filters);
122
- }
123
-
124
- function buildFieldFilter(
125
- fieldName: string,
126
- field: unknown,
127
- value: unknown,
128
- fields: Record<string, unknown>,
129
- ): SQL | undefined {
130
- if (!isRecord(value) || value instanceof Date || Array.isArray(value)) {
131
- return eq(field as never, value as never);
132
- }
133
-
134
- const filters: SQL[] = [];
135
- const eqValue = readOperatorValue(value, "eq");
136
- if (eqValue !== undefined) {
137
- filters.push(eq(field as never, eqValue as never));
138
- }
139
-
140
- const neValue = readOperatorValue(value, "ne");
141
- if (neValue !== undefined) {
142
- filters.push(ne(field as never, neValue as never));
143
- }
144
-
145
- const inValue = readOperatorValue(value, "in");
146
- if (Array.isArray(inValue) && inValue.length > 0) {
147
- filters.push(inArray(field as never, inValue as never));
148
- }
149
-
150
- const ninValue = readOperatorValue(value, "nin");
151
- if (Array.isArray(ninValue) && ninValue.length > 0) {
152
- filters.push(notInArray(field as never, ninValue as never));
153
- }
154
-
155
- const gtValue = readOperatorValue(value, "gt");
156
- if (gtValue !== undefined) {
157
- filters.push(gt(field as never, gtValue as never));
158
- }
159
-
160
- const gteValue = readOperatorValue(value, "gte");
161
- if (gteValue !== undefined) {
162
- filters.push(gte(field as never, gteValue as never));
163
- }
164
-
165
- const ltValue = readOperatorValue(value, "lt");
166
- if (ltValue !== undefined) {
167
- filters.push(lt(field as never, ltValue as never));
168
- }
169
-
170
- const lteValue = readOperatorValue(value, "lte");
171
- if (lteValue !== undefined) {
172
- filters.push(lte(field as never, lteValue as never));
173
- }
174
-
175
- if (typeof value.exists === "boolean") {
176
- filters.push(value.exists ? isNotNull(field as never) : isNull(field as never));
177
- }
178
-
179
- if (typeof value.regex === "string") {
180
- const caseInsensitive = typeof value.$options === "string" && value.$options.includes("i");
181
- const pattern = "%" + value.regex + "%";
182
- if (caseInsensitive) {
183
- filters.push(
184
- sql\`lower(\${field as never}) like lower(\${pattern})\`,
185
- );
186
- } else {
187
- filters.push(sql\`\${field as never} like \${pattern}\`);
188
- }
189
- }
190
-
191
- if (isRecord(value.geoWithin)) {
192
- const geoFilter = createGeoDistanceFilter(
193
- value.geoWithin as GeoWithinOperand,
194
- fields,
195
- undefined,
196
- fieldName,
197
- );
198
- if (geoFilter) {
199
- filters.push(geoFilter);
200
- }
201
- }
202
-
203
- if (filters.length === 0) {
204
- return eq(field as never, value as never);
205
- }
206
-
207
- if (filters.length === 1) {
208
- return filters[0];
209
- }
210
-
211
- return and(...filters);
212
- }
213
-
214
- function buildWhereFilter(
215
- table: unknown,
216
- where?: Record<string, unknown>,
217
- tableName?: string,
218
- ): SQL | undefined {
219
- if (!table || !where || Object.keys(where).length === 0) {
220
- return undefined;
221
- }
222
-
223
- const fields = getTableColumns(table as never) as Record<string, unknown>;
224
- return buildWhereFilterFromFields(fields, where, tableName);
225
- }
226
-
227
- function buildWhereFilterFromFields(
228
- fields: Record<string, unknown>,
229
- where?: Record<string, unknown>,
230
- tableName?: string,
231
- ): SQL | undefined {
232
- if (!where || Object.keys(where).length === 0) {
233
- return undefined;
234
- }
235
-
236
- const filters: SQL[] = [];
237
-
238
- const topLevelGeoWithin = isRecord(where.geoWithin)
239
- ? (where.geoWithin as GeoWithinOperand)
240
- : isRecord(where.$geoWithin)
241
- ? (where.$geoWithin as GeoWithinOperand)
242
- : undefined;
243
-
244
- if (topLevelGeoWithin) {
245
- const geoFilter = createGeoDistanceFilter(topLevelGeoWithin, fields, tableName);
246
- if (geoFilter) {
247
- filters.push(geoFilter);
248
- }
249
- }
250
-
251
- for (const [fieldName, fieldValue] of Object.entries(where)) {
252
- if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
253
- continue;
254
- }
255
-
256
- if (fieldValue === undefined) {
257
- continue;
258
- }
259
-
260
- const field = fields[fieldName];
261
- if (!field) {
262
- continue;
263
- }
264
-
265
- const filter = buildFieldFilter(fieldName, field, fieldValue, fields);
266
- if (filter) {
267
- filters.push(filter);
268
- }
269
- }
270
-
271
- if (filters.length === 0) {
272
- return undefined;
273
- }
274
-
275
- if (filters.length === 1) {
276
- return filters[0];
277
- }
278
-
279
- return and(...filters);
280
- }
281
-
282
- type ManyToManyRuntimeRelation = {
283
- targetTable: string;
284
- junctionTable: string;
285
- sourceField?: string;
286
- targetField?: string;
287
- referenceField?: string;
288
- targetReferenceField?: string;
289
- };
290
-
291
- type RuntimeRelationOne = {
292
- kind: "one";
293
- targetTable: string;
294
- sourceField?: string;
295
- referenceField?: string;
296
- };
297
-
298
- type RuntimeRelationMany = {
299
- kind: "many";
300
- targetTable: string;
301
- sourceField?: string;
302
- referenceField?: string;
303
- };
304
-
305
- type RuntimeRelationManyToMany = ManyToManyRuntimeRelation & {
306
- kind: "manyToMany";
307
- };
308
-
309
- type RuntimeRelation =
310
- | RuntimeRelationOne
311
- | RuntimeRelationMany
312
- | RuntimeRelationManyToMany;
313
-
314
- type RuntimeRelationMap = Record<string, Record<string, RuntimeRelation>>;
315
-
316
- function getRuntimeRelationMap(): RuntimeRelationMap {
317
- const rawValue = (mergedSchema as Record<string, unknown>)[
318
- "__appflareRelations"
319
- ];
320
- if (!isRecord(rawValue)) {
321
- return {};
322
- }
323
-
324
- const map: RuntimeRelationMap = {};
325
- for (const [tableName, tableValue] of Object.entries(rawValue)) {
326
- if (!isRecord(tableValue)) {
327
- continue;
328
- }
329
-
330
- const relationMap: Record<string, RuntimeRelation> = {};
331
- for (const [relationName, relationValue] of Object.entries(tableValue)) {
332
- if (!isRecord(relationValue)) {
333
- continue;
334
- }
335
-
336
- const kind = relationValue.kind;
337
- const targetTable = relationValue.targetTable;
338
- if (typeof kind !== "string" || typeof targetTable !== "string") {
339
- continue;
340
- }
341
-
342
- const sourceField =
343
- typeof relationValue.sourceField === "string"
344
- ? relationValue.sourceField
345
- : undefined;
346
- const referenceField =
347
- typeof relationValue.referenceField === "string"
348
- ? relationValue.referenceField
349
- : undefined;
350
-
351
- if (kind === "one") {
352
- relationMap[relationName] = {
353
- kind: "one",
354
- targetTable,
355
- sourceField,
356
- referenceField,
357
- };
358
- continue;
359
- }
360
-
361
- if (kind === "many") {
362
- relationMap[relationName] = {
363
- kind: "many",
364
- targetTable,
365
- sourceField,
366
- referenceField,
367
- };
368
- continue;
369
- }
370
-
371
- if (kind === "manyToMany") {
372
- const junctionTable = relationValue.junctionTable;
373
- if (typeof junctionTable !== "string") {
374
- continue;
375
- }
376
-
377
- relationMap[relationName] = {
378
- kind: "manyToMany",
379
- targetTable,
380
- junctionTable,
381
- sourceField,
382
- targetField:
383
- typeof relationValue.targetField === "string"
384
- ? relationValue.targetField
385
- : undefined,
386
- referenceField,
387
- targetReferenceField:
388
- typeof relationValue.targetReferenceField === "string"
389
- ? relationValue.targetReferenceField
390
- : undefined,
391
- };
392
- }
393
- }
394
-
395
- if (Object.keys(relationMap).length > 0) {
396
- map[tableName] = relationMap;
397
- }
398
- }
399
-
400
- return map;
401
- }
402
-
403
- const runtimeRelationMap = getRuntimeRelationMap();
404
-
405
- function getRuntimeRelation(
406
- tableName: string,
407
- relationName: string,
408
- ): RuntimeRelation | undefined {
409
- return runtimeRelationMap[tableName]?.[relationName];
410
- }
411
-
412
- type ManyToManyRuntimeMap = Record<
413
- string,
414
- Record<string, ManyToManyRuntimeRelation>
415
- >;
416
-
417
- function getManyToManyRuntimeMap(): ManyToManyRuntimeMap {
418
- const rawValue = (mergedSchema as Record<string, unknown>)[
419
- "__appflareManyToMany"
420
- ];
421
- if (!isRecord(rawValue)) {
422
- return {};
423
- }
424
-
425
- const map: ManyToManyRuntimeMap = {};
426
- for (const [tableName, tableValue] of Object.entries(rawValue)) {
427
- if (!isRecord(tableValue)) {
428
- continue;
429
- }
430
-
431
- const relationMap: Record<string, ManyToManyRuntimeRelation> = {};
432
- for (const [relationName, relationValue] of Object.entries(tableValue)) {
433
- const runtimeRelation = getRuntimeRelation(tableName, relationName);
434
- if (!runtimeRelation || runtimeRelation.kind !== "manyToMany") {
435
- continue;
436
- }
437
-
438
- relationMap[relationName] = {
439
- targetTable: runtimeRelation.targetTable,
440
- junctionTable: runtimeRelation.junctionTable,
441
- sourceField: runtimeRelation.sourceField,
442
- targetField: runtimeRelation.targetField,
443
- referenceField: runtimeRelation.referenceField,
444
- targetReferenceField: runtimeRelation.targetReferenceField,
445
- };
446
- }
447
-
448
- if (Object.keys(relationMap).length > 0) {
449
- map[tableName] = relationMap;
450
- }
451
- }
452
-
453
- return map;
454
- }
455
-
456
- const manyToManyRuntimeMap = getManyToManyRuntimeMap();
457
-
458
- function getManyToManyRelation(
459
- tableName: string,
460
- relationName: string,
461
- ): ManyToManyRuntimeRelation | undefined {
462
- return manyToManyRuntimeMap[tableName]?.[relationName];
463
- }
464
-
465
- function hasManyToManyRelationsInWith(
466
- tableName: string,
467
- withValue: unknown,
468
- ): boolean {
469
- if (!isRecord(withValue)) {
470
- return false;
471
- }
472
-
473
- for (const [relationName, relationValue] of Object.entries(withValue)) {
474
- const manyToMany = getManyToManyRelation(tableName, relationName);
475
- if (manyToMany) {
476
- return true;
477
- }
478
-
479
- if (!isRecord(relationValue)) {
480
- continue;
481
- }
482
-
483
- const nestedWith = relationValue.with;
484
- if (hasManyToManyRelationsInWith(relationName, nestedWith)) {
485
- return true;
486
- }
487
- }
488
-
489
- return false;
490
- }
491
-
492
- function flattenManyToManyResult(
493
- tableName: string,
494
- result: unknown,
495
- withValue: unknown,
496
- ): unknown {
497
- if (!isRecord(withValue)) {
498
- return result;
499
- }
500
-
501
- if (Array.isArray(result)) {
502
- return result.map((entry) =>
503
- flattenManyToManyResult(tableName, entry, withValue),
504
- );
505
- }
506
-
507
- if (!isRecord(result)) {
508
- return result;
509
- }
510
-
511
- const nextRow: Record<string, unknown> = {
512
- ...result,
513
- };
514
-
515
- for (const [relationName, relationValue] of Object.entries(withValue)) {
516
- const manyToMany = getManyToManyRelation(tableName, relationName);
517
- const currentValue = nextRow[relationName];
518
-
519
- if (manyToMany) {
520
- const relationEntries = Array.isArray(currentValue)
521
- ? currentValue
522
- : currentValue === null || currentValue === undefined
523
- ? []
524
- : [currentValue];
525
-
526
- const nestedWith =
527
- isRecord(relationValue) && relationValue.with !== undefined
528
- ? relationValue.with
529
- : undefined;
530
-
531
- const flattened = relationEntries
532
- .map((entry) => {
533
- if (!isRecord(entry)) {
534
- return undefined;
535
- }
536
-
537
- const targetValue = entry[manyToMany.targetTable];
538
- return flattenManyToManyResult(
539
- manyToMany.targetTable,
540
- targetValue,
541
- nestedWith,
542
- );
543
- })
544
- .filter((entry) => entry !== undefined && entry !== null);
545
-
546
- nextRow[relationName] = flattened;
547
- continue;
548
- }
549
-
550
- if (isRecord(relationValue) && relationValue.with !== undefined) {
551
- nextRow[relationName] = flattenManyToManyResult(
552
- relationName,
553
- currentValue,
554
- relationValue.with,
555
- );
556
- }
557
- }
558
-
559
- return nextRow;
560
- }
561
-
562
- function transformWithRelations(withValue: unknown, tableName?: string): unknown {
563
- return transformWithRelationsAndExtractAggregates(withValue, tableName).with;
564
- }
565
-
566
- type RelationWithAggregatePlanEntry = {
567
- count: boolean;
568
- avgFields: string[];
569
- nested?: RelationWithAggregatePlan;
570
- };
571
-
572
- type RelationWithAggregatePlan = Record<string, RelationWithAggregatePlanEntry>;
573
-
574
- function hasRelationAggregatePlanEntries(
575
- plan: RelationWithAggregatePlan | undefined,
576
- ): plan is RelationWithAggregatePlan {
577
- return !!plan && Object.keys(plan).length > 0;
578
- }
579
-
580
- function extractRelationAvgFieldNames(value: unknown): string[] {
581
- if (!isRecord(value)) {
582
- return [];
583
- }
584
-
585
- return Object.entries(value)
586
- .filter(([, enabled]) => enabled === true)
587
- .map(([fieldName]) => fieldName);
588
- }
589
-
590
- function transformWithRelationsAndExtractAggregates(
591
- withValue: unknown,
592
- tableName?: string,
593
- ): {
594
- with: unknown;
595
- aggregatePlan: RelationWithAggregatePlan | undefined;
596
- } {
597
- if (!isRecord(withValue)) {
598
- return {
599
- with: withValue,
600
- aggregatePlan: undefined,
601
- };
602
- }
603
-
604
- const transformed: Record<string, unknown> = {};
605
- const aggregatePlan: RelationWithAggregatePlan = {};
606
- for (const [relationName, relationValue] of Object.entries(withValue)) {
607
- const manyToMany =
608
- tableName === undefined
609
- ? undefined
610
- : getManyToManyRelation(tableName, relationName);
611
-
612
- if (typeof relationValue === "boolean") {
613
- if (manyToMany && relationValue) {
614
- transformed[relationName] = {
615
- with: {
616
- [manyToMany.targetTable]: true,
617
- },
618
- };
619
- } else {
620
- transformed[relationName] = relationValue;
621
- }
622
- continue;
623
- }
624
-
625
- if (!isRecord(relationValue)) {
626
- transformed[relationName] = relationValue;
627
- continue;
628
- }
629
-
630
- const relationConfigInput: Record<string, unknown> = {
631
- ...relationValue,
632
- };
633
- const countRequested = relationConfigInput._count === true;
634
- const avgFieldNames = extractRelationAvgFieldNames(relationConfigInput._avg);
635
- delete relationConfigInput._count;
636
- delete relationConfigInput._avg;
637
-
638
- const relationConfig: Record<string, unknown> = manyToMany
639
- ? {
640
- ...relationConfigInput,
641
- }
642
- : relationConfigInput;
643
-
644
- let nestedAggregatePlan: RelationWithAggregatePlan | undefined;
645
-
646
- const relationWhere = relationConfig.where;
647
- if (relationWhere !== undefined && !isRecord(relationWhere)) {
648
- throw new Error(
649
- "Invalid relational with.where for relation " +
650
- relationName +
651
- ": only shorthand object filters are supported.",
652
- );
653
- }
654
-
655
- if (isRecord(relationWhere) && !manyToMany) {
656
- relationConfig.where = (fields: Record<string, unknown>) => {
657
- const filter = buildWhereFilterFromFields(
658
- fields,
659
- relationWhere,
660
- relationName,
661
- );
662
- if (filter) {
663
- return filter;
664
- }
665
-
666
- return sql\`1 = 1\`;
667
- };
668
- }
669
-
670
- if (manyToMany) {
671
- const targetRelationConfig: Record<string, unknown> = {};
672
- for (const [configKey, configValue] of Object.entries(relationConfig)) {
673
- if (configKey === "with" || configKey === "where") {
674
- continue;
675
- }
676
- targetRelationConfig[configKey] = configValue;
677
- }
678
-
679
- if (isRecord(relationWhere)) {
680
- targetRelationConfig.where = (fields: Record<string, unknown>) => {
681
- const filter = buildWhereFilterFromFields(
682
- fields,
683
- relationWhere,
684
- manyToMany.targetTable,
685
- );
686
- if (filter) {
687
- return filter;
688
- }
689
-
690
- return sql\`1 = 1\`;
691
- };
692
- }
693
-
694
- if (relationConfig.with !== undefined) {
695
- const nestedTransform = transformWithRelationsAndExtractAggregates(
696
- relationConfig.with,
697
- manyToMany.targetTable,
698
- );
699
- targetRelationConfig.with = nestedTransform.with;
700
- nestedAggregatePlan = nestedTransform.aggregatePlan;
701
- }
702
-
703
- const targetConfigValue =
704
- Object.keys(targetRelationConfig).length === 0
705
- ? true
706
- : targetRelationConfig;
707
-
708
- relationConfig.with = {
709
- [manyToMany.targetTable]: targetConfigValue,
710
- };
711
- delete relationConfig.where;
712
- } else if (relationConfig.with !== undefined) {
713
- const nestedTransform = transformWithRelationsAndExtractAggregates(
714
- relationConfig.with,
715
- relationName,
716
- );
717
- relationConfig.with = nestedTransform.with;
718
- nestedAggregatePlan = nestedTransform.aggregatePlan;
719
- }
720
-
721
- if (
722
- countRequested ||
723
- avgFieldNames.length > 0 ||
724
- hasRelationAggregatePlanEntries(nestedAggregatePlan)
725
- ) {
726
- aggregatePlan[relationName] = {
727
- count: countRequested,
728
- avgFields: avgFieldNames,
729
- ...(nestedAggregatePlan
730
- ? { nested: nestedAggregatePlan }
731
- : {}),
732
- };
733
- }
734
-
735
- transformed[relationName] = relationConfig;
736
- }
737
-
738
- return {
739
- with: transformed,
740
- aggregatePlan: hasRelationAggregatePlanEntries(aggregatePlan)
741
- ? aggregatePlan
742
- : undefined,
743
- };
744
- }
745
-
746
- function computeAverageOrZero(values: number[]): number {
747
- if (values.length === 0) {
748
- return 0;
749
- }
750
-
751
- const total = values.reduce((sum, current) => sum + current, 0);
752
- return total / values.length;
753
- }
754
-
755
- function applyRelationAggregatePlanToRow(
756
- row: unknown,
757
- plan: RelationWithAggregatePlan,
758
- ): unknown {
759
- if (!isRecord(row)) {
760
- return row;
761
- }
762
-
763
- const nextRow: Record<string, unknown> = {
764
- ...row,
765
- };
766
-
767
- for (const [relationName, relationPlan] of Object.entries(plan)) {
768
- const relationData = nextRow[relationName];
769
- const relationEntries = Array.isArray(relationData)
770
- ? relationData.filter((entry) => entry !== null && entry !== undefined)
771
- : relationData === null || relationData === undefined
772
- ? []
773
- : [relationData];
774
-
775
- if (relationPlan.nested) {
776
- if (Array.isArray(relationData)) {
777
- nextRow[relationName] = relationData.map((entry) =>
778
- applyRelationAggregatePlanToRow(entry, relationPlan.nested as RelationWithAggregatePlan),
779
- );
780
- } else if (isRecord(relationData)) {
781
- nextRow[relationName] = applyRelationAggregatePlanToRow(
782
- relationData,
783
- relationPlan.nested,
784
- );
785
- }
786
- }
787
-
788
- if (!relationPlan.count && relationPlan.avgFields.length === 0) {
789
- continue;
790
- }
791
-
792
- const aggregatePayload: Record<string, unknown> = {};
793
- if (relationPlan.count) {
794
- aggregatePayload.count = relationEntries.length;
795
- }
796
-
797
- if (relationPlan.avgFields.length > 0) {
798
- const averagePayload: Record<string, number> = {};
799
- for (const fieldName of relationPlan.avgFields) {
800
- const numericValues = relationEntries
801
- .map((entry) =>
802
- isRecord(entry) ? toFiniteNumber(entry[fieldName]) : null,
803
- )
804
- .filter((value): value is number => value !== null);
805
- averagePayload[fieldName] = computeAverageOrZero(numericValues);
806
- }
807
- aggregatePayload.avg = averagePayload;
808
- }
809
-
810
- nextRow[relationName + "Aggregate"] = aggregatePayload;
811
- }
812
-
813
- return nextRow;
814
- }
815
-
816
- function applyRelationAggregatePlanToResult(
817
- result: unknown,
818
- plan: RelationWithAggregatePlan | undefined,
819
- ): unknown {
820
- if (!hasRelationAggregatePlanEntries(plan)) {
821
- return result;
822
- }
823
-
824
- if (Array.isArray(result)) {
825
- return result.map((row) => applyRelationAggregatePlanToRow(row, plan));
826
- }
827
-
828
- if (result === null || result === undefined) {
829
- return result;
830
- }
831
-
832
- return applyRelationAggregatePlanToRow(result, plan);
833
- }
834
-
835
- function hasAggregateWithConstraints(withValue: unknown): boolean {
836
- if (!isRecord(withValue)) {
837
- return false;
838
- }
839
-
840
- for (const relationValue of Object.values(withValue)) {
841
- if (!isRecord(relationValue)) {
842
- continue;
843
- }
844
-
845
- if (isRecord(relationValue.where)) {
846
- return true;
847
- }
848
-
849
- if (hasAggregateWithConstraints(relationValue.with)) {
850
- return true;
851
- }
852
- }
853
-
854
- return false;
855
- }
856
-
857
- function rowMatchesAggregateWithConstraints(
858
- row: unknown,
859
- withValue: unknown,
860
- ): boolean {
861
- if (!isRecord(withValue)) {
862
- return true;
863
- }
864
-
865
- if (!isRecord(row)) {
866
- return !hasAggregateWithConstraints(withValue);
867
- }
868
-
869
- for (const [relationName, relationValue] of Object.entries(withValue)) {
870
- if (!isRecord(relationValue)) {
871
- continue;
872
- }
873
-
874
- const nestedWith = relationValue.with;
875
- const requiresPresence =
876
- isRecord(relationValue.where) || hasAggregateWithConstraints(nestedWith);
877
- if (!requiresPresence) {
878
- continue;
879
- }
880
-
881
- const relationData = row[relationName];
882
- if (Array.isArray(relationData)) {
883
- if (relationData.length === 0) {
884
- return false;
885
- }
886
-
887
- if (hasAggregateWithConstraints(nestedWith)) {
888
- const hasNestedMatch = relationData.some((entry) =>
889
- rowMatchesAggregateWithConstraints(entry, nestedWith),
890
- );
891
- if (!hasNestedMatch) {
892
- return false;
893
- }
894
- }
895
-
896
- continue;
897
- }
898
-
899
- if (!isRecord(relationData)) {
900
- return false;
901
- }
902
-
903
- if (
904
- hasAggregateWithConstraints(nestedWith) &&
905
- !rowMatchesAggregateWithConstraints(relationData, nestedWith)
906
- ) {
907
- return false;
908
- }
909
- }
910
-
911
- return true;
912
- }
913
-
914
- function splitAggregateFieldPath(field: string): string[] {
915
- return field
916
- .split(".")
917
- .map((segment) => segment.trim())
918
- .filter((segment) => segment.length > 0);
919
- }
920
-
921
- function collectValuesFromPath(
922
- value: unknown,
923
- pathSegments: string[],
924
- index = 0,
925
- ): unknown[] {
926
- if (index >= pathSegments.length) {
927
- return [value];
928
- }
929
-
930
- if (value === null || value === undefined) {
931
- return [];
932
- }
933
-
934
- if (Array.isArray(value)) {
935
- return value.flatMap((entry) =>
936
- collectValuesFromPath(entry, pathSegments, index),
937
- );
938
- }
939
-
940
- if (!isRecord(value)) {
941
- return [];
942
- }
943
-
944
- const nextValue = value[pathSegments[index]];
945
- return collectValuesFromPath(nextValue, pathSegments, index + 1);
946
- }
947
-
948
- function createDistinctValueKey(value: unknown): string {
949
- if (value instanceof Date) {
950
- return "date:" + value.toISOString();
951
- }
952
-
953
- if (Array.isArray(value)) {
954
- return (
955
- "[" + value.map((entry) => createDistinctValueKey(entry)).join(",") + "]"
956
- );
957
- }
958
-
959
- if (isRecord(value)) {
960
- const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
961
- return (
962
- "{" +
963
- keys
964
- .map((key) => JSON.stringify(key) + ":" + createDistinctValueKey(value[key]))
965
- .join(",") +
966
- "}"
967
- );
968
- }
969
-
970
- if (value === null) {
971
- return "null";
972
- }
973
-
974
- if (value === undefined) {
975
- return "undefined";
976
- }
977
-
978
- if (typeof value === "number" && Number.isNaN(value)) {
979
- return "number:NaN";
980
- }
981
-
982
- return typeof value + ":" + String(value);
983
- }
984
-
985
- function countAggregateValues(values: unknown[], distinct: boolean): number {
986
- const nonNullValues = values.filter(
987
- (value) => value !== null && value !== undefined,
988
- );
989
- if (!distinct) {
990
- return nonNullValues.length;
991
- }
992
-
993
- const seen = new Set<string>();
994
- for (const value of nonNullValues) {
995
- seen.add(createDistinctValueKey(value));
996
- }
997
-
998
- return seen.size;
999
- }
1000
-
1001
- function toFiniteNumber(value: unknown): number | null {
1002
- if (typeof value !== "number" || !Number.isFinite(value)) {
1003
- return null;
1004
- }
1005
-
1006
- return value;
1007
- }
1008
-
1009
- function averageAggregateValues(values: number[]): number | null {
1010
- if (values.length === 0) {
1011
- return null;
1012
- }
1013
-
1014
- const total = values.reduce((sum, current) => sum + current, 0);
1015
- return total / values.length;
1016
- }
1017
-
1018
- function inferConflictTarget(table: unknown): string[] {
1019
- if (!table) {
1020
- return [];
1021
- }
1022
-
1023
- const columns = getTableColumns(table as never) as Record<string, unknown>;
1024
- if (columns.id) {
1025
- return ["id"];
1026
- }
1027
-
1028
- return [];
1029
- }
1030
- `;
1031
- }
1
+ export function generateQueryHelperFunctionsSection(): string {
2
+ return `function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+
6
+ function readOperatorValue(record: Record<string, unknown>, key: string): unknown {
7
+ if (record[key] !== undefined) {
8
+ return record[key];
9
+ }
10
+ const prefixed = "$" + key;
11
+ return record[prefixed];
12
+ }
13
+
14
+ function normalizeGeoPoint(
15
+ value: GeoPoint | GeoCoordinates,
16
+ ): { latitude: number; longitude: number } | null {
17
+ if ((value as GeoPoint).latitude !== undefined) {
18
+ const point = value as GeoPoint;
19
+ if (
20
+ typeof point.latitude === "number" &&
21
+ typeof point.longitude === "number"
22
+ ) {
23
+ return {
24
+ latitude: point.latitude,
25
+ longitude: point.longitude,
26
+ };
27
+ }
28
+ return null;
29
+ }
30
+
31
+ const coordinates = (value as GeoCoordinates).coordinates;
32
+ if (!Array.isArray(coordinates) || coordinates.length < 2) {
33
+ return null;
34
+ }
35
+
36
+ const [longitude, latitude] = coordinates;
37
+ if (typeof latitude !== "number" || typeof longitude !== "number") {
38
+ return null;
39
+ }
40
+
41
+ return {
42
+ latitude,
43
+ longitude,
44
+ };
45
+ }
46
+
47
+ function createGeoDistanceFilter(
48
+ operand: GeoWithinOperand,
49
+ fields: Record<string, unknown>,
50
+ tableName?: string,
51
+ fallbackLatitudeField?: string,
52
+ ): SQL | undefined {
53
+ const geometry = normalizeGeoPoint(operand.$geometry);
54
+ if (!geometry) {
55
+ return undefined;
56
+ }
57
+
58
+ const latitudeField = operand.latitudeField ?? fallbackLatitudeField;
59
+ const longitudeField = operand.longitudeField;
60
+ if (!latitudeField || !longitudeField) {
61
+ return undefined;
62
+ }
63
+
64
+ const latitudeColumn = fields[latitudeField];
65
+ if (!latitudeColumn) {
66
+ console.warn(
67
+ "Invalid geoWithin latitudeField",
68
+ tableName ?? "unknown",
69
+ latitudeField,
70
+ );
71
+ return undefined;
72
+ }
73
+
74
+ const longitudeColumn = fields[longitudeField];
75
+ if (!longitudeColumn) {
76
+ console.warn(
77
+ "Invalid geoWithin longitudeField",
78
+ tableName ?? "unknown",
79
+ longitudeField,
80
+ );
81
+ return undefined;
82
+ }
83
+
84
+ const distanceExpression = sql<number>\`(
85
+ 6371000 * acos(
86
+ cos(radians(\${geometry.latitude})) * cos(radians(\${latitudeColumn}))
87
+ * cos(radians(\${longitudeColumn}) - radians(\${geometry.longitude}))
88
+ + sin(radians(\${geometry.latitude})) * sin(radians(\${latitudeColumn}))
89
+ )
90
+ )\`;
91
+
92
+ const minDistance =
93
+ operand.$minDistance ??
94
+ operand.gte ??
95
+ operand.$gte ??
96
+ operand.gt ??
97
+ operand.$gt;
98
+ const maxDistance =
99
+ operand.$maxDistance ??
100
+ operand.lte ??
101
+ operand.$lte ??
102
+ operand.lt ??
103
+ operand.$lt;
104
+
105
+ const filters: SQL[] = [];
106
+ if (typeof minDistance === "number") {
107
+ filters.push(sql\`\${distanceExpression} >= \${minDistance}\`);
108
+ }
109
+ if (typeof maxDistance === "number") {
110
+ filters.push(sql\`\${distanceExpression} <= \${maxDistance}\`);
111
+ }
112
+
113
+ if (filters.length === 0) {
114
+ return undefined;
115
+ }
116
+
117
+ if (filters.length === 1) {
118
+ return filters[0];
119
+ }
120
+
121
+ return and(...filters);
122
+ }
123
+
124
+ function buildFieldFilter(
125
+ fieldName: string,
126
+ field: unknown,
127
+ value: unknown,
128
+ fields: Record<string, unknown>,
129
+ ): SQL | undefined {
130
+ if (!isRecord(value) || value instanceof Date || Array.isArray(value)) {
131
+ return eq(field as never, value as never);
132
+ }
133
+
134
+ const filters: SQL[] = [];
135
+ const eqValue = readOperatorValue(value, "eq");
136
+ if (eqValue !== undefined) {
137
+ filters.push(eq(field as never, eqValue as never));
138
+ }
139
+
140
+ const neValue = readOperatorValue(value, "ne");
141
+ if (neValue !== undefined) {
142
+ filters.push(ne(field as never, neValue as never));
143
+ }
144
+
145
+ const inValue = readOperatorValue(value, "in");
146
+ if (Array.isArray(inValue) && inValue.length > 0) {
147
+ filters.push(inArray(field as never, inValue as never));
148
+ }
149
+
150
+ const ninValue = readOperatorValue(value, "nin");
151
+ if (Array.isArray(ninValue) && ninValue.length > 0) {
152
+ filters.push(notInArray(field as never, ninValue as never));
153
+ }
154
+
155
+ const gtValue = readOperatorValue(value, "gt");
156
+ if (gtValue !== undefined) {
157
+ filters.push(gt(field as never, gtValue as never));
158
+ }
159
+
160
+ const gteValue = readOperatorValue(value, "gte");
161
+ if (gteValue !== undefined) {
162
+ filters.push(gte(field as never, gteValue as never));
163
+ }
164
+
165
+ const ltValue = readOperatorValue(value, "lt");
166
+ if (ltValue !== undefined) {
167
+ filters.push(lt(field as never, ltValue as never));
168
+ }
169
+
170
+ const lteValue = readOperatorValue(value, "lte");
171
+ if (lteValue !== undefined) {
172
+ filters.push(lte(field as never, lteValue as never));
173
+ }
174
+
175
+ if (typeof value.exists === "boolean") {
176
+ filters.push(value.exists ? isNotNull(field as never) : isNull(field as never));
177
+ }
178
+
179
+ if (typeof value.regex === "string") {
180
+ const caseInsensitive = typeof value.$options === "string" && value.$options.includes("i");
181
+ const pattern = "%" + value.regex + "%";
182
+ if (caseInsensitive) {
183
+ filters.push(
184
+ sql\`lower(\${field as never}) like lower(\${pattern})\`,
185
+ );
186
+ } else {
187
+ filters.push(sql\`\${field as never} like \${pattern}\`);
188
+ }
189
+ }
190
+
191
+ if (isRecord(value.geoWithin)) {
192
+ const geoFilter = createGeoDistanceFilter(
193
+ value.geoWithin as GeoWithinOperand,
194
+ fields,
195
+ undefined,
196
+ fieldName,
197
+ );
198
+ if (geoFilter) {
199
+ filters.push(geoFilter);
200
+ }
201
+ }
202
+
203
+ if (filters.length === 0) {
204
+ return eq(field as never, value as never);
205
+ }
206
+
207
+ if (filters.length === 1) {
208
+ return filters[0];
209
+ }
210
+
211
+ return and(...filters);
212
+ }
213
+
214
+ function buildWhereFilter(
215
+ table: unknown,
216
+ where?: Record<string, unknown>,
217
+ tableName?: string,
218
+ ): SQL | undefined {
219
+ if (!table || !where || Object.keys(where).length === 0) {
220
+ return undefined;
221
+ }
222
+
223
+ const fields = getTableColumns(table as never) as Record<string, unknown>;
224
+ return buildWhereFilterFromFields(fields, where, tableName);
225
+ }
226
+
227
+ function buildWhereFilterFromFields(
228
+ fields: Record<string, unknown>,
229
+ where?: Record<string, unknown>,
230
+ tableName?: string,
231
+ ): SQL | undefined {
232
+ if (!where || Object.keys(where).length === 0) {
233
+ return undefined;
234
+ }
235
+
236
+ const filters: SQL[] = [];
237
+
238
+ const topLevelGeoWithin = isRecord(where.geoWithin)
239
+ ? (where.geoWithin as GeoWithinOperand)
240
+ : isRecord(where.$geoWithin)
241
+ ? (where.$geoWithin as GeoWithinOperand)
242
+ : undefined;
243
+
244
+ if (topLevelGeoWithin) {
245
+ const geoFilter = createGeoDistanceFilter(topLevelGeoWithin, fields, tableName);
246
+ if (geoFilter) {
247
+ filters.push(geoFilter);
248
+ }
249
+ }
250
+
251
+ for (const [fieldName, fieldValue] of Object.entries(where)) {
252
+ if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
253
+ continue;
254
+ }
255
+
256
+ if (fieldValue === undefined) {
257
+ continue;
258
+ }
259
+
260
+ const field = fields[fieldName];
261
+ if (!field) {
262
+ if (isRecord(fieldValue) && tableName) {
263
+ const relation = getRuntimeRelation(tableName, fieldName);
264
+ if (relation) {
265
+ const relationFilter = buildRelationFilter(
266
+ relation,
267
+ fieldValue,
268
+ tableName,
269
+ fields,
270
+ );
271
+ if (relationFilter) {
272
+ filters.push(relationFilter);
273
+ }
274
+ continue;
275
+ }
276
+ }
277
+ continue;
278
+ }
279
+
280
+ const filter = buildFieldFilter(fieldName, field, fieldValue, fields);
281
+ if (filter) {
282
+ filters.push(filter);
283
+ }
284
+ }
285
+
286
+ if (filters.length === 0) {
287
+ return undefined;
288
+ }
289
+
290
+ if (filters.length === 1) {
291
+ return filters[0];
292
+ }
293
+
294
+ return and(...filters);
295
+ }
296
+
297
+ type ManyToManyRuntimeRelation = {
298
+ targetTable: string;
299
+ junctionTable: string;
300
+ sourceField?: string;
301
+ targetField?: string;
302
+ referenceField?: string;
303
+ targetReferenceField?: string;
304
+ };
305
+
306
+ type RuntimeRelationOne = {
307
+ kind: "one";
308
+ targetTable: string;
309
+ sourceField?: string;
310
+ referenceField?: string;
311
+ };
312
+
313
+ type RuntimeRelationMany = {
314
+ kind: "many";
315
+ targetTable: string;
316
+ sourceField?: string;
317
+ referenceField?: string;
318
+ };
319
+
320
+ type RuntimeRelationManyToMany = ManyToManyRuntimeRelation & {
321
+ kind: "manyToMany";
322
+ };
323
+
324
+ type RuntimeRelation =
325
+ | RuntimeRelationOne
326
+ | RuntimeRelationMany
327
+ | RuntimeRelationManyToMany;
328
+
329
+ type RuntimeRelationMap = Record<string, Record<string, RuntimeRelation>>;
330
+
331
+ function getRuntimeRelationMap(): RuntimeRelationMap {
332
+ const rawValue = (mergedSchema as Record<string, unknown>)[
333
+ "__appflareRelations"
334
+ ];
335
+ if (!isRecord(rawValue)) {
336
+ return {};
337
+ }
338
+
339
+ const map: RuntimeRelationMap = {};
340
+ for (const [tableName, tableValue] of Object.entries(rawValue)) {
341
+ if (!isRecord(tableValue)) {
342
+ continue;
343
+ }
344
+
345
+ const relationMap: Record<string, RuntimeRelation> = {};
346
+ for (const [relationName, relationValue] of Object.entries(tableValue)) {
347
+ if (!isRecord(relationValue)) {
348
+ continue;
349
+ }
350
+
351
+ const kind = relationValue.kind;
352
+ const targetTable = relationValue.targetTable;
353
+ if (typeof kind !== "string" || typeof targetTable !== "string") {
354
+ continue;
355
+ }
356
+
357
+ const sourceField =
358
+ typeof relationValue.sourceField === "string"
359
+ ? relationValue.sourceField
360
+ : undefined;
361
+ const referenceField =
362
+ typeof relationValue.referenceField === "string"
363
+ ? relationValue.referenceField
364
+ : undefined;
365
+
366
+ if (kind === "one") {
367
+ relationMap[relationName] = {
368
+ kind: "one",
369
+ targetTable,
370
+ sourceField,
371
+ referenceField,
372
+ };
373
+ continue;
374
+ }
375
+
376
+ if (kind === "many") {
377
+ relationMap[relationName] = {
378
+ kind: "many",
379
+ targetTable,
380
+ sourceField,
381
+ referenceField,
382
+ };
383
+ continue;
384
+ }
385
+
386
+ if (kind === "manyToMany") {
387
+ const junctionTable = relationValue.junctionTable;
388
+ if (typeof junctionTable !== "string") {
389
+ continue;
390
+ }
391
+
392
+ relationMap[relationName] = {
393
+ kind: "manyToMany",
394
+ targetTable,
395
+ junctionTable,
396
+ sourceField,
397
+ targetField:
398
+ typeof relationValue.targetField === "string"
399
+ ? relationValue.targetField
400
+ : undefined,
401
+ referenceField,
402
+ targetReferenceField:
403
+ typeof relationValue.targetReferenceField === "string"
404
+ ? relationValue.targetReferenceField
405
+ : undefined,
406
+ };
407
+ }
408
+ }
409
+
410
+ if (Object.keys(relationMap).length > 0) {
411
+ map[tableName] = relationMap;
412
+ }
413
+ }
414
+
415
+ return map;
416
+ }
417
+
418
+ const runtimeRelationMap = getRuntimeRelationMap();
419
+
420
+ function getRuntimeRelation(
421
+ tableName: string,
422
+ relationName: string,
423
+ ): RuntimeRelation | undefined {
424
+ return runtimeRelationMap[tableName]?.[relationName];
425
+ }
426
+
427
+ function buildRelationFilter(
428
+ relation: RuntimeRelation,
429
+ whereValue: Record<string, unknown>,
430
+ sourceTableName: string,
431
+ sourceFields: Record<string, unknown>
432
+ ): SQL | undefined {
433
+ const rawTargetTable = (mergedSchema as Record<string, unknown>)[relation.targetTable];
434
+ if (!rawTargetTable) {
435
+ return undefined;
436
+ }
437
+
438
+ const targetFields = getTableColumns(rawTargetTable as never) as Record<string, unknown>;
439
+ const relationFilter = buildWhereFilterFromFields(
440
+ targetFields,
441
+ whereValue,
442
+ relation.targetTable,
443
+ );
444
+
445
+ if (!relationFilter) {
446
+ return undefined;
447
+ }
448
+
449
+ if (relation.kind === "one") {
450
+ const sourceField = relation.sourceField || (relation.targetTable + "Id");
451
+ const targetFieldCol = targetFields[relation.referenceField || "id"];
452
+ const sourceFieldCol = sourceFields[sourceField];
453
+ if (!targetFieldCol || !sourceFieldCol) return undefined;
454
+ return sql\`exists (select 1 from \${rawTargetTable as never} where \${eq(targetFieldCol as never, sourceFieldCol as never)} and \${relationFilter})\`;
455
+ }
456
+
457
+ if (relation.kind === "many") {
458
+ const sourceFieldCol = sourceFields[relation.sourceField || "id"];
459
+ const targetFieldCol = targetFields[relation.referenceField || (sourceTableName + "Id")];
460
+ if (!targetFieldCol || !sourceFieldCol) return undefined;
461
+ return sql\`exists (select 1 from \${rawTargetTable as never} where \${eq(targetFieldCol as never, sourceFieldCol as never)} and \${relationFilter})\`;
462
+ }
463
+
464
+ if (relation.kind === "manyToMany") {
465
+ const junctionTableName = (relation as RuntimeRelationManyToMany).junctionTable;
466
+ const junctionTable = (mergedSchema as Record<string, unknown>)[junctionTableName];
467
+ if (!junctionTable) return undefined;
468
+ const junctionFields = getTableColumns(junctionTable as never) as Record<string, unknown>;
469
+
470
+ const sourceFieldCol = sourceFields[(relation as RuntimeRelationManyToMany).sourceField || "id"];
471
+ const targetFieldCol = targetFields[(relation as RuntimeRelationManyToMany).targetField || "id"];
472
+
473
+ const junctionSourceRefCol = junctionFields[(relation as RuntimeRelationManyToMany).referenceField || (sourceTableName + "Id")];
474
+ const junctionTargetRefCol = junctionFields[(relation as RuntimeRelationManyToMany).targetReferenceField || (relation.targetTable + "Id")];
475
+
476
+ if (!sourceFieldCol || !targetFieldCol || !junctionSourceRefCol || !junctionTargetRefCol) return undefined;
477
+
478
+ return sql\`exists (select 1 from \${junctionTable as never} inner join \${rawTargetTable as never} on \${eq(junctionTargetRefCol as never, targetFieldCol as never)} where \${eq(junctionSourceRefCol as never, sourceFieldCol as never)} and \${relationFilter})\`;
479
+ }
480
+
481
+ return undefined;
482
+ }
483
+
484
+ type ManyToManyRuntimeMap = Record<
485
+ string,
486
+ Record<string, ManyToManyRuntimeRelation>
487
+ >;
488
+
489
+ function getManyToManyRuntimeMap(): ManyToManyRuntimeMap {
490
+ const rawValue = (mergedSchema as Record<string, unknown>)[
491
+ "__appflareManyToMany"
492
+ ];
493
+ if (!isRecord(rawValue)) {
494
+ return {};
495
+ }
496
+
497
+ const map: ManyToManyRuntimeMap = {};
498
+ for (const [tableName, tableValue] of Object.entries(rawValue)) {
499
+ if (!isRecord(tableValue)) {
500
+ continue;
501
+ }
502
+
503
+ const relationMap: Record<string, ManyToManyRuntimeRelation> = {};
504
+ for (const [relationName, relationValue] of Object.entries(tableValue)) {
505
+ const runtimeRelation = getRuntimeRelation(tableName, relationName);
506
+ if (!runtimeRelation || runtimeRelation.kind !== "manyToMany") {
507
+ continue;
508
+ }
509
+
510
+ relationMap[relationName] = {
511
+ targetTable: runtimeRelation.targetTable,
512
+ junctionTable: runtimeRelation.junctionTable,
513
+ sourceField: runtimeRelation.sourceField,
514
+ targetField: runtimeRelation.targetField,
515
+ referenceField: runtimeRelation.referenceField,
516
+ targetReferenceField: runtimeRelation.targetReferenceField,
517
+ };
518
+ }
519
+
520
+ if (Object.keys(relationMap).length > 0) {
521
+ map[tableName] = relationMap;
522
+ }
523
+ }
524
+
525
+ return map;
526
+ }
527
+
528
+ const manyToManyRuntimeMap = getManyToManyRuntimeMap();
529
+
530
+ function getManyToManyRelation(
531
+ tableName: string,
532
+ relationName: string,
533
+ ): ManyToManyRuntimeRelation | undefined {
534
+ return manyToManyRuntimeMap[tableName]?.[relationName];
535
+ }
536
+
537
+ function hasManyToManyRelationsInWith(
538
+ tableName: string,
539
+ withValue: unknown,
540
+ ): boolean {
541
+ if (!isRecord(withValue)) {
542
+ return false;
543
+ }
544
+
545
+ for (const [relationName, relationValue] of Object.entries(withValue)) {
546
+ const manyToMany = getManyToManyRelation(tableName, relationName);
547
+ if (manyToMany) {
548
+ return true;
549
+ }
550
+
551
+ if (!isRecord(relationValue)) {
552
+ continue;
553
+ }
554
+
555
+ const nestedWith = relationValue.with;
556
+ if (hasManyToManyRelationsInWith(relationName, nestedWith)) {
557
+ return true;
558
+ }
559
+ }
560
+
561
+ return false;
562
+ }
563
+
564
+ function flattenManyToManyResult(
565
+ tableName: string,
566
+ result: unknown,
567
+ withValue: unknown,
568
+ ): unknown {
569
+ if (!isRecord(withValue)) {
570
+ return result;
571
+ }
572
+
573
+ if (Array.isArray(result)) {
574
+ return result.map((entry) =>
575
+ flattenManyToManyResult(tableName, entry, withValue),
576
+ );
577
+ }
578
+
579
+ if (!isRecord(result)) {
580
+ return result;
581
+ }
582
+
583
+ const nextRow: Record<string, unknown> = {
584
+ ...result,
585
+ };
586
+
587
+ for (const [relationName, relationValue] of Object.entries(withValue)) {
588
+ const manyToMany = getManyToManyRelation(tableName, relationName);
589
+ const currentValue = nextRow[relationName];
590
+
591
+ if (manyToMany) {
592
+ const relationEntries = Array.isArray(currentValue)
593
+ ? currentValue
594
+ : currentValue === null || currentValue === undefined
595
+ ? []
596
+ : [currentValue];
597
+
598
+ const nestedWith =
599
+ isRecord(relationValue) && relationValue.with !== undefined
600
+ ? relationValue.with
601
+ : undefined;
602
+
603
+ const flattened = relationEntries
604
+ .map((entry) => {
605
+ if (!isRecord(entry)) {
606
+ return undefined;
607
+ }
608
+
609
+ const targetValue = entry[manyToMany.targetTable];
610
+ return flattenManyToManyResult(
611
+ manyToMany.targetTable,
612
+ targetValue,
613
+ nestedWith,
614
+ );
615
+ })
616
+ .filter((entry) => entry !== undefined && entry !== null);
617
+
618
+ nextRow[relationName] = flattened;
619
+ continue;
620
+ }
621
+
622
+ if (isRecord(relationValue) && relationValue.with !== undefined) {
623
+ nextRow[relationName] = flattenManyToManyResult(
624
+ relationName,
625
+ currentValue,
626
+ relationValue.with,
627
+ );
628
+ }
629
+ }
630
+
631
+ return nextRow;
632
+ }
633
+
634
+ function transformWithRelations(withValue: unknown, tableName?: string): unknown {
635
+ return transformWithRelationsAndExtractAggregates(withValue, tableName).with;
636
+ }
637
+
638
+ type RelationWithAggregatePlanEntry = {
639
+ count: boolean;
640
+ avgFields: string[];
641
+ nested?: RelationWithAggregatePlan;
642
+ };
643
+
644
+ type RelationWithAggregatePlan = Record<string, RelationWithAggregatePlanEntry>;
645
+
646
+ function hasRelationAggregatePlanEntries(
647
+ plan: RelationWithAggregatePlan | undefined,
648
+ ): plan is RelationWithAggregatePlan {
649
+ return !!plan && Object.keys(plan).length > 0;
650
+ }
651
+
652
+ function extractRelationAvgFieldNames(value: unknown): string[] {
653
+ if (!isRecord(value)) {
654
+ return [];
655
+ }
656
+
657
+ return Object.entries(value)
658
+ .filter(([, enabled]) => enabled === true)
659
+ .map(([fieldName]) => fieldName);
660
+ }
661
+
662
+ function transformWithRelationsAndExtractAggregates(
663
+ withValue: unknown,
664
+ tableName?: string,
665
+ ): {
666
+ with: unknown;
667
+ aggregatePlan: RelationWithAggregatePlan | undefined;
668
+ } {
669
+ if (!isRecord(withValue)) {
670
+ return {
671
+ with: withValue,
672
+ aggregatePlan: undefined,
673
+ };
674
+ }
675
+
676
+ const transformed: Record<string, unknown> = {};
677
+ const aggregatePlan: RelationWithAggregatePlan = {};
678
+ for (const [relationName, relationValue] of Object.entries(withValue)) {
679
+ const manyToMany =
680
+ tableName === undefined
681
+ ? undefined
682
+ : getManyToManyRelation(tableName, relationName);
683
+
684
+ if (typeof relationValue === "boolean") {
685
+ if (manyToMany && relationValue) {
686
+ transformed[relationName] = {
687
+ with: {
688
+ [manyToMany.targetTable]: true,
689
+ },
690
+ };
691
+ } else {
692
+ transformed[relationName] = relationValue;
693
+ }
694
+ continue;
695
+ }
696
+
697
+ if (!isRecord(relationValue)) {
698
+ transformed[relationName] = relationValue;
699
+ continue;
700
+ }
701
+
702
+ const relationConfigInput: Record<string, unknown> = {
703
+ ...relationValue,
704
+ };
705
+ const countRequested = relationConfigInput._count === true;
706
+ const avgFieldNames = extractRelationAvgFieldNames(relationConfigInput._avg);
707
+ delete relationConfigInput._count;
708
+ delete relationConfigInput._avg;
709
+
710
+ const relationConfig: Record<string, unknown> = manyToMany
711
+ ? {
712
+ ...relationConfigInput,
713
+ }
714
+ : relationConfigInput;
715
+
716
+ let nestedAggregatePlan: RelationWithAggregatePlan | undefined;
717
+
718
+ const relationWhere = relationConfig.where;
719
+ if (relationWhere !== undefined && !isRecord(relationWhere)) {
720
+ throw new Error(
721
+ "Invalid relational with.where for relation " +
722
+ relationName +
723
+ ": only shorthand object filters are supported.",
724
+ );
725
+ }
726
+
727
+ if (isRecord(relationWhere) && !manyToMany) {
728
+ relationConfig.where = (fields: Record<string, unknown>) => {
729
+ const filter = buildWhereFilterFromFields(
730
+ fields,
731
+ relationWhere,
732
+ relationName,
733
+ );
734
+ if (filter) {
735
+ return filter;
736
+ }
737
+
738
+ return sql\`1 = 1\`;
739
+ };
740
+ }
741
+
742
+ if (manyToMany) {
743
+ const targetRelationConfig: Record<string, unknown> = {};
744
+ for (const [configKey, configValue] of Object.entries(relationConfig)) {
745
+ if (configKey === "with" || configKey === "where") {
746
+ continue;
747
+ }
748
+ targetRelationConfig[configKey] = configValue;
749
+ }
750
+
751
+ if (isRecord(relationWhere)) {
752
+ targetRelationConfig.where = (fields: Record<string, unknown>) => {
753
+ const filter = buildWhereFilterFromFields(
754
+ fields,
755
+ relationWhere,
756
+ manyToMany.targetTable,
757
+ );
758
+ if (filter) {
759
+ return filter;
760
+ }
761
+
762
+ return sql\`1 = 1\`;
763
+ };
764
+ }
765
+
766
+ if (relationConfig.with !== undefined) {
767
+ const nestedTransform = transformWithRelationsAndExtractAggregates(
768
+ relationConfig.with,
769
+ manyToMany.targetTable,
770
+ );
771
+ targetRelationConfig.with = nestedTransform.with;
772
+ nestedAggregatePlan = nestedTransform.aggregatePlan;
773
+ }
774
+
775
+ const targetConfigValue =
776
+ Object.keys(targetRelationConfig).length === 0
777
+ ? true
778
+ : targetRelationConfig;
779
+
780
+ relationConfig.with = {
781
+ [manyToMany.targetTable]: targetConfigValue,
782
+ };
783
+ delete relationConfig.where;
784
+ } else if (relationConfig.with !== undefined) {
785
+ const nestedTransform = transformWithRelationsAndExtractAggregates(
786
+ relationConfig.with,
787
+ relationName,
788
+ );
789
+ relationConfig.with = nestedTransform.with;
790
+ nestedAggregatePlan = nestedTransform.aggregatePlan;
791
+ }
792
+
793
+ if (
794
+ countRequested ||
795
+ avgFieldNames.length > 0 ||
796
+ hasRelationAggregatePlanEntries(nestedAggregatePlan)
797
+ ) {
798
+ aggregatePlan[relationName] = {
799
+ count: countRequested,
800
+ avgFields: avgFieldNames,
801
+ ...(nestedAggregatePlan
802
+ ? { nested: nestedAggregatePlan }
803
+ : {}),
804
+ };
805
+ }
806
+
807
+ transformed[relationName] = relationConfig;
808
+ }
809
+
810
+ return {
811
+ with: transformed,
812
+ aggregatePlan: hasRelationAggregatePlanEntries(aggregatePlan)
813
+ ? aggregatePlan
814
+ : undefined,
815
+ };
816
+ }
817
+
818
+ function computeAverageOrZero(values: number[]): number {
819
+ if (values.length === 0) {
820
+ return 0;
821
+ }
822
+
823
+ const total = values.reduce((sum, current) => sum + current, 0);
824
+ return total / values.length;
825
+ }
826
+
827
+ function applyRelationAggregatePlanToRow(
828
+ row: unknown,
829
+ plan: RelationWithAggregatePlan,
830
+ ): unknown {
831
+ if (!isRecord(row)) {
832
+ return row;
833
+ }
834
+
835
+ const nextRow: Record<string, unknown> = {
836
+ ...row,
837
+ };
838
+
839
+ for (const [relationName, relationPlan] of Object.entries(plan)) {
840
+ const relationData = nextRow[relationName];
841
+ const relationEntries = Array.isArray(relationData)
842
+ ? relationData.filter((entry) => entry !== null && entry !== undefined)
843
+ : relationData === null || relationData === undefined
844
+ ? []
845
+ : [relationData];
846
+
847
+ if (relationPlan.nested) {
848
+ if (Array.isArray(relationData)) {
849
+ nextRow[relationName] = relationData.map((entry) =>
850
+ applyRelationAggregatePlanToRow(entry, relationPlan.nested as RelationWithAggregatePlan),
851
+ );
852
+ } else if (isRecord(relationData)) {
853
+ nextRow[relationName] = applyRelationAggregatePlanToRow(
854
+ relationData,
855
+ relationPlan.nested,
856
+ );
857
+ }
858
+ }
859
+
860
+ if (!relationPlan.count && relationPlan.avgFields.length === 0) {
861
+ continue;
862
+ }
863
+
864
+ const aggregatePayload: Record<string, unknown> = {};
865
+ if (relationPlan.count) {
866
+ aggregatePayload.count = relationEntries.length;
867
+ }
868
+
869
+ if (relationPlan.avgFields.length > 0) {
870
+ const averagePayload: Record<string, number> = {};
871
+ for (const fieldName of relationPlan.avgFields) {
872
+ const numericValues = relationEntries
873
+ .map((entry) =>
874
+ isRecord(entry) ? toFiniteNumber(entry[fieldName]) : null,
875
+ )
876
+ .filter((value): value is number => value !== null);
877
+ averagePayload[fieldName] = computeAverageOrZero(numericValues);
878
+ }
879
+ aggregatePayload.avg = averagePayload;
880
+ }
881
+
882
+ nextRow[relationName + "Aggregate"] = aggregatePayload;
883
+ }
884
+
885
+ return nextRow;
886
+ }
887
+
888
+ function applyRelationAggregatePlanToResult(
889
+ result: unknown,
890
+ plan: RelationWithAggregatePlan | undefined,
891
+ ): unknown {
892
+ if (!hasRelationAggregatePlanEntries(plan)) {
893
+ return result;
894
+ }
895
+
896
+ if (Array.isArray(result)) {
897
+ return result.map((row) => applyRelationAggregatePlanToRow(row, plan));
898
+ }
899
+
900
+ if (result === null || result === undefined) {
901
+ return result;
902
+ }
903
+
904
+ return applyRelationAggregatePlanToRow(result, plan);
905
+ }
906
+
907
+ function hasAggregateWithConstraints(withValue: unknown): boolean {
908
+ if (!isRecord(withValue)) {
909
+ return false;
910
+ }
911
+
912
+ for (const relationValue of Object.values(withValue)) {
913
+ if (!isRecord(relationValue)) {
914
+ continue;
915
+ }
916
+
917
+ if (isRecord(relationValue.where)) {
918
+ return true;
919
+ }
920
+
921
+ if (hasAggregateWithConstraints(relationValue.with)) {
922
+ return true;
923
+ }
924
+ }
925
+
926
+ return false;
927
+ }
928
+
929
+ function rowMatchesAggregateWithConstraints(
930
+ row: unknown,
931
+ withValue: unknown,
932
+ ): boolean {
933
+ if (!isRecord(withValue)) {
934
+ return true;
935
+ }
936
+
937
+ if (!isRecord(row)) {
938
+ return !hasAggregateWithConstraints(withValue);
939
+ }
940
+
941
+ for (const [relationName, relationValue] of Object.entries(withValue)) {
942
+ if (!isRecord(relationValue)) {
943
+ continue;
944
+ }
945
+
946
+ const nestedWith = relationValue.with;
947
+ const requiresPresence =
948
+ isRecord(relationValue.where) || hasAggregateWithConstraints(nestedWith);
949
+ if (!requiresPresence) {
950
+ continue;
951
+ }
952
+
953
+ const relationData = row[relationName];
954
+ if (Array.isArray(relationData)) {
955
+ if (relationData.length === 0) {
956
+ return false;
957
+ }
958
+
959
+ if (hasAggregateWithConstraints(nestedWith)) {
960
+ const hasNestedMatch = relationData.some((entry) =>
961
+ rowMatchesAggregateWithConstraints(entry, nestedWith),
962
+ );
963
+ if (!hasNestedMatch) {
964
+ return false;
965
+ }
966
+ }
967
+
968
+ continue;
969
+ }
970
+
971
+ if (!isRecord(relationData)) {
972
+ return false;
973
+ }
974
+
975
+ if (
976
+ hasAggregateWithConstraints(nestedWith) &&
977
+ !rowMatchesAggregateWithConstraints(relationData, nestedWith)
978
+ ) {
979
+ return false;
980
+ }
981
+ }
982
+
983
+ return true;
984
+ }
985
+
986
+ function splitAggregateFieldPath(field: string): string[] {
987
+ return field
988
+ .split(".")
989
+ .map((segment) => segment.trim())
990
+ .filter((segment) => segment.length > 0);
991
+ }
992
+
993
+ function collectValuesFromPath(
994
+ value: unknown,
995
+ pathSegments: string[],
996
+ index = 0,
997
+ ): unknown[] {
998
+ if (index >= pathSegments.length) {
999
+ return [value];
1000
+ }
1001
+
1002
+ if (value === null || value === undefined) {
1003
+ return [];
1004
+ }
1005
+
1006
+ if (Array.isArray(value)) {
1007
+ return value.flatMap((entry) =>
1008
+ collectValuesFromPath(entry, pathSegments, index),
1009
+ );
1010
+ }
1011
+
1012
+ if (!isRecord(value)) {
1013
+ return [];
1014
+ }
1015
+
1016
+ const nextValue = value[pathSegments[index]];
1017
+ return collectValuesFromPath(nextValue, pathSegments, index + 1);
1018
+ }
1019
+
1020
+ function createDistinctValueKey(value: unknown): string {
1021
+ if (value instanceof Date) {
1022
+ return "date:" + value.toISOString();
1023
+ }
1024
+
1025
+ if (Array.isArray(value)) {
1026
+ return (
1027
+ "[" + value.map((entry) => createDistinctValueKey(entry)).join(",") + "]"
1028
+ );
1029
+ }
1030
+
1031
+ if (isRecord(value)) {
1032
+ const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
1033
+ return (
1034
+ "{" +
1035
+ keys
1036
+ .map((key) => JSON.stringify(key) + ":" + createDistinctValueKey(value[key]))
1037
+ .join(",") +
1038
+ "}"
1039
+ );
1040
+ }
1041
+
1042
+ if (value === null) {
1043
+ return "null";
1044
+ }
1045
+
1046
+ if (value === undefined) {
1047
+ return "undefined";
1048
+ }
1049
+
1050
+ if (typeof value === "number" && Number.isNaN(value)) {
1051
+ return "number:NaN";
1052
+ }
1053
+
1054
+ return typeof value + ":" + String(value);
1055
+ }
1056
+
1057
+ function countAggregateValues(values: unknown[], distinct: boolean): number {
1058
+ const nonNullValues = values.filter(
1059
+ (value) => value !== null && value !== undefined,
1060
+ );
1061
+ if (!distinct) {
1062
+ return nonNullValues.length;
1063
+ }
1064
+
1065
+ const seen = new Set<string>();
1066
+ for (const value of nonNullValues) {
1067
+ seen.add(createDistinctValueKey(value));
1068
+ }
1069
+
1070
+ return seen.size;
1071
+ }
1072
+
1073
+ function toFiniteNumber(value: unknown): number | null {
1074
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1075
+ return null;
1076
+ }
1077
+
1078
+ return value;
1079
+ }
1080
+
1081
+ function averageAggregateValues(values: number[]): number | null {
1082
+ if (values.length === 0) {
1083
+ return null;
1084
+ }
1085
+
1086
+ const total = values.reduce((sum, current) => sum + current, 0);
1087
+ return total / values.length;
1088
+ }
1089
+
1090
+ function inferConflictTarget(table: unknown): string[] {
1091
+ if (!table) {
1092
+ return [];
1093
+ }
1094
+
1095
+ const columns = getTableColumns(table as never) as Record<string, unknown>;
1096
+ if (columns.id) {
1097
+ return ["id"];
1098
+ }
1099
+
1100
+ return [];
1101
+ }
1102
+ `;
1103
+ }