appflare 0.2.4 → 0.2.7
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.
- package/cli/commands/index.ts +98 -4
- package/cli/generate.ts +8 -0
- package/cli/index.ts +32 -1
- package/cli/templates/auth/config.ts +0 -2
- package/cli/templates/core/handlers.route.ts +1 -0
- package/cli/templates/core/imports.ts +1 -0
- package/cli/templates/dashboard/builders/functions/execute-handler.ts +124 -0
- package/cli/templates/dashboard/builders/functions/index.ts +22 -0
- package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -0
- package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -0
- package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +67 -0
- package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +19 -0
- package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +17 -0
- package/cli/templates/dashboard/builders/navigation.ts +122 -0
- package/cli/templates/dashboard/builders/storage/index.ts +13 -0
- package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -0
- package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -0
- package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -0
- package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -0
- package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -0
- package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -0
- package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -0
- package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -0
- package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -0
- package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -0
- package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -0
- package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -0
- package/cli/templates/dashboard/builders/table-routes/fragments.ts +214 -0
- package/cli/templates/dashboard/builders/table-routes/helpers.ts +49 -0
- package/cli/templates/dashboard/builders/table-routes/index.ts +8 -0
- package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -0
- package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +166 -0
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +77 -0
- package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +111 -0
- package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -0
- package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -0
- package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +127 -0
- package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -0
- package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -0
- package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -0
- package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -0
- package/cli/templates/dashboard/components/dashboard-home.ts +23 -0
- package/cli/templates/dashboard/components/layout.ts +388 -0
- package/cli/templates/dashboard/components/login-page.ts +65 -0
- package/cli/templates/dashboard/index.ts +61 -0
- package/cli/templates/dashboard/types.ts +9 -0
- package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +1 -1
- package/cli/templates/handlers/generators/types/core.ts +5 -0
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +168 -0
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +133 -0
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +686 -0
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +97 -0
- package/cli/templates/handlers/generators/types/query-definitions.ts +11 -1083
- package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +164 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +85 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -0
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +137 -0
- package/cli/templates/handlers/generators/types/query-runtime.ts +13 -431
- package/cli/utils/schema-discovery.ts +10 -1
- package/package.json +1 -1
- package/test-better-auth-hash.ts +2 -0
|
@@ -0,0 +1,686 @@
|
|
|
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
|
+
function transformWithRelations(withValue: unknown): unknown {
|
|
283
|
+
return transformWithRelationsAndExtractAggregates(withValue).with;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
type RelationWithAggregatePlanEntry = {
|
|
287
|
+
count: boolean;
|
|
288
|
+
avgFields: string[];
|
|
289
|
+
nested?: RelationWithAggregatePlan;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
type RelationWithAggregatePlan = Record<string, RelationWithAggregatePlanEntry>;
|
|
293
|
+
|
|
294
|
+
function hasRelationAggregatePlanEntries(
|
|
295
|
+
plan: RelationWithAggregatePlan | undefined,
|
|
296
|
+
): boolean {
|
|
297
|
+
return !!plan && Object.keys(plan).length > 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function extractRelationAvgFieldNames(value: unknown): string[] {
|
|
301
|
+
if (!isRecord(value)) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return Object.entries(value)
|
|
306
|
+
.filter(([, enabled]) => enabled === true)
|
|
307
|
+
.map(([fieldName]) => fieldName);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function transformWithRelationsAndExtractAggregates(withValue: unknown): {
|
|
311
|
+
with: unknown;
|
|
312
|
+
aggregatePlan: RelationWithAggregatePlan | undefined;
|
|
313
|
+
} {
|
|
314
|
+
if (!isRecord(withValue)) {
|
|
315
|
+
return {
|
|
316
|
+
with: withValue,
|
|
317
|
+
aggregatePlan: undefined,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const transformed: Record<string, unknown> = {};
|
|
322
|
+
const aggregatePlan: RelationWithAggregatePlan = {};
|
|
323
|
+
for (const [relationName, relationValue] of Object.entries(withValue)) {
|
|
324
|
+
if (typeof relationValue === "boolean") {
|
|
325
|
+
transformed[relationName] = relationValue;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!isRecord(relationValue)) {
|
|
330
|
+
transformed[relationName] = relationValue;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const relationConfig: Record<string, unknown> = {
|
|
335
|
+
...relationValue,
|
|
336
|
+
};
|
|
337
|
+
const countRequested = relationConfig._count === true;
|
|
338
|
+
const avgFieldNames = extractRelationAvgFieldNames(relationConfig._avg);
|
|
339
|
+
delete relationConfig._count;
|
|
340
|
+
delete relationConfig._avg;
|
|
341
|
+
|
|
342
|
+
let nestedAggregatePlan: RelationWithAggregatePlan | undefined;
|
|
343
|
+
|
|
344
|
+
const relationWhere = relationConfig.where;
|
|
345
|
+
if (relationWhere !== undefined && !isRecord(relationWhere)) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"Invalid relational with.where for relation " +
|
|
348
|
+
relationName +
|
|
349
|
+
": only shorthand object filters are supported.",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (isRecord(relationWhere)) {
|
|
354
|
+
relationConfig.where = (fields: Record<string, unknown>) => {
|
|
355
|
+
const filter = buildWhereFilterFromFields(
|
|
356
|
+
fields,
|
|
357
|
+
relationWhere,
|
|
358
|
+
relationName,
|
|
359
|
+
);
|
|
360
|
+
if (filter) {
|
|
361
|
+
return filter;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return sql\`1 = 1\`;
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (relationConfig.with !== undefined) {
|
|
369
|
+
const nestedTransform = transformWithRelationsAndExtractAggregates(
|
|
370
|
+
relationConfig.with,
|
|
371
|
+
);
|
|
372
|
+
relationConfig.with = nestedTransform.with;
|
|
373
|
+
nestedAggregatePlan = nestedTransform.aggregatePlan;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
countRequested ||
|
|
378
|
+
avgFieldNames.length > 0 ||
|
|
379
|
+
hasRelationAggregatePlanEntries(nestedAggregatePlan)
|
|
380
|
+
) {
|
|
381
|
+
aggregatePlan[relationName] = {
|
|
382
|
+
count: countRequested,
|
|
383
|
+
avgFields: avgFieldNames,
|
|
384
|
+
...(nestedAggregatePlan
|
|
385
|
+
? { nested: nestedAggregatePlan }
|
|
386
|
+
: {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
transformed[relationName] = relationConfig;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
with: transformed,
|
|
395
|
+
aggregatePlan: hasRelationAggregatePlanEntries(aggregatePlan)
|
|
396
|
+
? aggregatePlan
|
|
397
|
+
: undefined,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function computeAverageOrZero(values: number[]): number {
|
|
402
|
+
if (values.length === 0) {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const total = values.reduce((sum, current) => sum + current, 0);
|
|
407
|
+
return total / values.length;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function applyRelationAggregatePlanToRow(
|
|
411
|
+
row: unknown,
|
|
412
|
+
plan: RelationWithAggregatePlan,
|
|
413
|
+
): unknown {
|
|
414
|
+
if (!isRecord(row)) {
|
|
415
|
+
return row;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const nextRow: Record<string, unknown> = {
|
|
419
|
+
...row,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
for (const [relationName, relationPlan] of Object.entries(plan)) {
|
|
423
|
+
const relationData = nextRow[relationName];
|
|
424
|
+
const relationEntries = Array.isArray(relationData)
|
|
425
|
+
? relationData.filter((entry) => entry !== null && entry !== undefined)
|
|
426
|
+
: relationData === null || relationData === undefined
|
|
427
|
+
? []
|
|
428
|
+
: [relationData];
|
|
429
|
+
|
|
430
|
+
if (relationPlan.nested) {
|
|
431
|
+
if (Array.isArray(relationData)) {
|
|
432
|
+
nextRow[relationName] = relationData.map((entry) =>
|
|
433
|
+
applyRelationAggregatePlanToRow(entry, relationPlan.nested as RelationWithAggregatePlan),
|
|
434
|
+
);
|
|
435
|
+
} else if (isRecord(relationData)) {
|
|
436
|
+
nextRow[relationName] = applyRelationAggregatePlanToRow(
|
|
437
|
+
relationData,
|
|
438
|
+
relationPlan.nested,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!relationPlan.count && relationPlan.avgFields.length === 0) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const aggregatePayload: Record<string, unknown> = {};
|
|
448
|
+
if (relationPlan.count) {
|
|
449
|
+
aggregatePayload.count = relationEntries.length;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (relationPlan.avgFields.length > 0) {
|
|
453
|
+
const averagePayload: Record<string, number> = {};
|
|
454
|
+
for (const fieldName of relationPlan.avgFields) {
|
|
455
|
+
const numericValues = relationEntries
|
|
456
|
+
.map((entry) =>
|
|
457
|
+
isRecord(entry) ? toFiniteNumber(entry[fieldName]) : null,
|
|
458
|
+
)
|
|
459
|
+
.filter((value): value is number => value !== null);
|
|
460
|
+
averagePayload[fieldName] = computeAverageOrZero(numericValues);
|
|
461
|
+
}
|
|
462
|
+
aggregatePayload.avg = averagePayload;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
nextRow[relationName + "Aggregate"] = aggregatePayload;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return nextRow;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function applyRelationAggregatePlanToResult(
|
|
472
|
+
result: unknown,
|
|
473
|
+
plan: RelationWithAggregatePlan | undefined,
|
|
474
|
+
): unknown {
|
|
475
|
+
if (!hasRelationAggregatePlanEntries(plan)) {
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (Array.isArray(result)) {
|
|
480
|
+
return result.map((row) => applyRelationAggregatePlanToRow(row, plan));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (result === null || result === undefined) {
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return applyRelationAggregatePlanToRow(result, plan);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function hasAggregateWithConstraints(withValue: unknown): boolean {
|
|
491
|
+
if (!isRecord(withValue)) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
for (const relationValue of Object.values(withValue)) {
|
|
496
|
+
if (!isRecord(relationValue)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (isRecord(relationValue.where)) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (hasAggregateWithConstraints(relationValue.with)) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function rowMatchesAggregateWithConstraints(
|
|
513
|
+
row: unknown,
|
|
514
|
+
withValue: unknown,
|
|
515
|
+
): boolean {
|
|
516
|
+
if (!isRecord(withValue)) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (!isRecord(row)) {
|
|
521
|
+
return !hasAggregateWithConstraints(withValue);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (const [relationName, relationValue] of Object.entries(withValue)) {
|
|
525
|
+
if (!isRecord(relationValue)) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const nestedWith = relationValue.with;
|
|
530
|
+
const requiresPresence =
|
|
531
|
+
isRecord(relationValue.where) || hasAggregateWithConstraints(nestedWith);
|
|
532
|
+
if (!requiresPresence) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const relationData = row[relationName];
|
|
537
|
+
if (Array.isArray(relationData)) {
|
|
538
|
+
if (relationData.length === 0) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (hasAggregateWithConstraints(nestedWith)) {
|
|
543
|
+
const hasNestedMatch = relationData.some((entry) =>
|
|
544
|
+
rowMatchesAggregateWithConstraints(entry, nestedWith),
|
|
545
|
+
);
|
|
546
|
+
if (!hasNestedMatch) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!isRecord(relationData)) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (
|
|
559
|
+
hasAggregateWithConstraints(nestedWith) &&
|
|
560
|
+
!rowMatchesAggregateWithConstraints(relationData, nestedWith)
|
|
561
|
+
) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function splitAggregateFieldPath(field: string): string[] {
|
|
570
|
+
return field
|
|
571
|
+
.split(".")
|
|
572
|
+
.map((segment) => segment.trim())
|
|
573
|
+
.filter((segment) => segment.length > 0);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function collectValuesFromPath(
|
|
577
|
+
value: unknown,
|
|
578
|
+
pathSegments: string[],
|
|
579
|
+
index = 0,
|
|
580
|
+
): unknown[] {
|
|
581
|
+
if (index >= pathSegments.length) {
|
|
582
|
+
return [value];
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (value === null || value === undefined) {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (Array.isArray(value)) {
|
|
590
|
+
return value.flatMap((entry) =>
|
|
591
|
+
collectValuesFromPath(entry, pathSegments, index),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!isRecord(value)) {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const nextValue = value[pathSegments[index]];
|
|
600
|
+
return collectValuesFromPath(nextValue, pathSegments, index + 1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function createDistinctValueKey(value: unknown): string {
|
|
604
|
+
if (value instanceof Date) {
|
|
605
|
+
return "date:" + value.toISOString();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
return (
|
|
610
|
+
"[" + value.map((entry) => createDistinctValueKey(entry)).join(",") + "]"
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (isRecord(value)) {
|
|
615
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
616
|
+
return (
|
|
617
|
+
"{" +
|
|
618
|
+
keys
|
|
619
|
+
.map((key) => JSON.stringify(key) + ":" + createDistinctValueKey(value[key]))
|
|
620
|
+
.join(",") +
|
|
621
|
+
"}"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (value === null) {
|
|
626
|
+
return "null";
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (value === undefined) {
|
|
630
|
+
return "undefined";
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (typeof value === "number" && Number.isNaN(value)) {
|
|
634
|
+
return "number:NaN";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return typeof value + ":" + String(value);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function countAggregateValues(values: unknown[], distinct: boolean): number {
|
|
641
|
+
const nonNullValues = values.filter(
|
|
642
|
+
(value) => value !== null && value !== undefined,
|
|
643
|
+
);
|
|
644
|
+
if (!distinct) {
|
|
645
|
+
return nonNullValues.length;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const seen = new Set<string>();
|
|
649
|
+
for (const value of nonNullValues) {
|
|
650
|
+
seen.add(createDistinctValueKey(value));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return seen.size;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function toFiniteNumber(value: unknown): number | null {
|
|
657
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return value;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function averageAggregateValues(values: number[]): number | null {
|
|
665
|
+
if (values.length === 0) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const total = values.reduce((sum, current) => sum + current, 0);
|
|
670
|
+
return total / values.length;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function inferConflictTarget(table: unknown): string[] {
|
|
674
|
+
if (!table) {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const columns = getTableColumns(table as never) as Record<string, unknown>;
|
|
679
|
+
if (columns.id) {
|
|
680
|
+
return ["id"];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
`;
|
|
686
|
+
}
|