appflare 0.2.46 → 0.2.48

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 (141) hide show
  1. package/Documentation.md +898 -898
  2. package/cli/commands/index.ts +247 -247
  3. package/cli/generate.ts +360 -360
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1366 -1366
  7. package/cli/templates/auth/README.md +156 -156
  8. package/cli/templates/auth/config.ts +61 -61
  9. package/cli/templates/auth/route-config.ts +1 -1
  10. package/cli/templates/auth/route-handler.ts +1 -1
  11. package/cli/templates/auth/route-request-utils.ts +5 -5
  12. package/cli/templates/auth/route.config.ts +18 -18
  13. package/cli/templates/auth/route.handler.ts +18 -18
  14. package/cli/templates/auth/route.request-utils.ts +55 -55
  15. package/cli/templates/auth/route.ts +14 -14
  16. package/cli/templates/core/README.md +266 -266
  17. package/cli/templates/core/app-creation.ts +19 -19
  18. package/cli/templates/core/client/appflare.ts +112 -112
  19. package/cli/templates/core/client/handlers/index.ts +763 -763
  20. package/cli/templates/core/client/handlers.ts +1 -1
  21. package/cli/templates/core/client/index.ts +7 -7
  22. package/cli/templates/core/client/storage.ts +195 -195
  23. package/cli/templates/core/client/types.ts +187 -187
  24. package/cli/templates/core/client-modules/appflare.ts +1 -1
  25. package/cli/templates/core/client-modules/handlers.ts +1 -1
  26. package/cli/templates/core/client-modules/index.ts +1 -1
  27. package/cli/templates/core/client-modules/storage.ts +1 -1
  28. package/cli/templates/core/client-modules/types.ts +1 -1
  29. package/cli/templates/core/client.artifacts.ts +39 -39
  30. package/cli/templates/core/client.ts +4 -4
  31. package/cli/templates/core/drizzle.ts +15 -15
  32. package/cli/templates/core/export.ts +14 -14
  33. package/cli/templates/core/handlers.route.ts +24 -24
  34. package/cli/templates/core/handlers.ts +1 -1
  35. package/cli/templates/core/imports.ts +9 -9
  36. package/cli/templates/core/server.ts +38 -38
  37. package/cli/templates/core/types.ts +6 -6
  38. package/cli/templates/core/wrangler.ts +109 -109
  39. package/cli/templates/dashboard/builders/functions/index.ts +17 -17
  40. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
  41. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
  42. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +271 -271
  43. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
  44. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +703 -703
  45. package/cli/templates/dashboard/builders/functions/tree-builder.ts +47 -47
  46. package/cli/templates/dashboard/builders/navigation.ts +155 -155
  47. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  48. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  49. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  50. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  51. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  52. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  53. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  55. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  56. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  57. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  58. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  59. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  60. package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
  61. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  62. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  63. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  64. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  65. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  66. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  67. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  68. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  69. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  70. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  71. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  72. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  73. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  74. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  75. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  76. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  77. package/cli/templates/dashboard/components/layout.ts +420 -420
  78. package/cli/templates/dashboard/components/login-page.ts +65 -65
  79. package/cli/templates/dashboard/index.ts +61 -61
  80. package/cli/templates/dashboard/types.ts +9 -9
  81. package/cli/templates/handlers/README.md +353 -353
  82. package/cli/templates/handlers/auth.ts +37 -37
  83. package/cli/templates/handlers/execution.ts +42 -42
  84. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  85. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  86. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  87. package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
  88. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  89. package/cli/templates/handlers/generators/context/types.ts +40 -40
  90. package/cli/templates/handlers/generators/context.ts +43 -43
  91. package/cli/templates/handlers/generators/execution.ts +15 -15
  92. package/cli/templates/handlers/generators/handlers.ts +14 -14
  93. package/cli/templates/handlers/generators/registration/modules/cron.ts +35 -26
  94. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  95. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  96. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  97. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  98. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  99. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  100. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +510 -510
  101. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +65 -56
  102. package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
  103. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  104. package/cli/templates/handlers/generators/types/context.ts +121 -121
  105. package/cli/templates/handlers/generators/types/core.ts +108 -106
  106. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  107. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +291 -291
  108. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  109. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1382 -1382
  110. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -278
  111. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +158 -157
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  116. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +958 -958
  117. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  118. package/cli/templates/handlers/index.ts +47 -47
  119. package/cli/templates/handlers/operations.ts +116 -116
  120. package/cli/templates/handlers/registration.ts +91 -91
  121. package/cli/templates/handlers/types.ts +17 -17
  122. package/cli/templates/handlers/utils.ts +48 -48
  123. package/cli/types.ts +110 -110
  124. package/cli/utils/handler-discovery.ts +501 -501
  125. package/cli/utils/json-utils.ts +24 -24
  126. package/cli/utils/path-utils.ts +19 -19
  127. package/cli/utils/schema-discovery.ts +399 -399
  128. package/dist/cli/index.js +43 -23
  129. package/dist/cli/index.mjs +43 -23
  130. package/dist/react/index.js +1 -1
  131. package/dist/react/index.mjs +1 -1
  132. package/index.ts +18 -18
  133. package/package.json +58 -58
  134. package/react/index.ts +5 -5
  135. package/react/use-infinite-query.ts +255 -255
  136. package/react/use-mutation.ts +89 -89
  137. package/react/use-query.ts +210 -210
  138. package/schema.ts +641 -641
  139. package/test-better-auth-hash.ts +2 -2
  140. package/tsconfig.json +6 -6
  141. package/tsup.config.ts +82 -82
@@ -1,510 +1,510 @@
1
- export const realtimeUtilsModule = `
2
- function isRecord(value: unknown): value is Record<string, unknown> {
3
- return typeof value === "object" && value !== null;
4
- }
5
-
6
- function stableStringify(value: unknown): string {
7
- if (Array.isArray(value)) {
8
- return "[" + value.map((entry) => stableStringify(entry)).join(",") + "]";
9
- }
10
-
11
- if (value instanceof Date) {
12
- return JSON.stringify(value.toISOString());
13
- }
14
-
15
- if (isRecord(value)) {
16
- const keys = Object.keys(value).sort((a, b) => a.localeCompare(b));
17
- return (
18
- "{" +
19
- keys
20
- .map((key) => JSON.stringify(key) + ":" + stableStringify(value[key]))
21
- .join(",") +
22
- "}"
23
- );
24
- }
25
-
26
- return JSON.stringify(value ?? null);
27
- }
28
-
29
- function normalizeComparableValue(value: unknown): unknown {
30
- if (Array.isArray(value)) {
31
- return value.map((entry) => normalizeComparableValue(entry));
32
- }
33
-
34
- if (value instanceof Date) {
35
- return value.toISOString();
36
- }
37
-
38
- if (isRecord(value)) {
39
- return Object.entries(value)
40
- .sort(([a], [b]) => a.localeCompare(b))
41
- .reduce<Record<string, unknown>>((accumulator, [key, entry]) => {
42
- accumulator[key] = normalizeComparableValue(entry);
43
- return accumulator;
44
- }, {});
45
- }
46
-
47
- return value;
48
- }
49
-
50
- function createSubscriptionSignature(
51
- queryName: string,
52
- args: Record<string, unknown>,
53
- ): string {
54
- const normalizedArgs = normalizeComparableValue(args);
55
- return queryName + "::" + stableStringify(normalizedArgs);
56
- }
57
-
58
- type RealtimeQueryMethod = "findMany" | "findFirst" | "count" | "avg";
59
-
60
- type RealtimeQueryMatchDescriptor = {
61
- table: string;
62
- method: RealtimeQueryMethod;
63
- where: Record<string, unknown> | null;
64
- };
65
-
66
- type RealtimeQueryMatchPlan = {
67
- descriptors: RealtimeQueryMatchDescriptor[];
68
- unsupported: boolean;
69
- };
70
-
71
- type RealtimePlanContextSeed = {
72
- user: unknown;
73
- session: unknown;
74
- };
75
-
76
- function normalizeQueryWhereArg(args: unknown): Record<string, unknown> | null {
77
- if (!isRecord(args)) {
78
- return null;
79
- }
80
-
81
- const where = args.where;
82
- if (isRecord(where)) {
83
- return where;
84
- }
85
-
86
- return null;
87
- }
88
-
89
- function normalizeQueryWithArg(args: unknown): Record<string, unknown> | null {
90
- if (!isRecord(args)) {
91
- return null;
92
- }
93
-
94
- const withArg = args.with;
95
- if (isRecord(withArg)) {
96
- return withArg;
97
- }
98
-
99
- return null;
100
- }
101
-
102
- function appendWithWhereDescriptors(
103
- descriptors: RealtimeQueryMatchDescriptor[],
104
- withInput: unknown,
105
- method: RealtimeQueryMethod,
106
- ): void {
107
- if (!isRecord(withInput)) {
108
- return;
109
- }
110
-
111
- for (const [relationName, relationValue] of Object.entries(withInput)) {
112
- if (!isRecord(relationValue)) {
113
- continue;
114
- }
115
-
116
- const relationWhere = relationValue.where;
117
- if (isRecord(relationWhere)) {
118
- descriptors.push({
119
- table: relationName,
120
- method,
121
- where: relationWhere,
122
- });
123
- }
124
-
125
- appendWithWhereDescriptors(descriptors, relationValue.with, method);
126
- }
127
- }
128
-
129
- function readOperatorValue(record: Record<string, unknown>, key: string): unknown {
130
- if (record[key] !== undefined) {
131
- return record[key];
132
- }
133
- const prefixed = "$" + key;
134
- return record[prefixed];
135
- }
136
-
137
- function toDateTimestamp(value: unknown): number | null {
138
- if (value instanceof Date) {
139
- return value.getTime();
140
- }
141
-
142
- if (typeof value === "string" || typeof value === "number") {
143
- const date = new Date(value);
144
- const timestamp = date.getTime();
145
- if (!Number.isNaN(timestamp)) {
146
- return timestamp;
147
- }
148
- }
149
-
150
- return null;
151
- }
152
-
153
- function compareScalarValues(left: unknown, right: unknown): boolean {
154
- if (left === right) {
155
- return true;
156
- }
157
-
158
- const leftDate = toDateTimestamp(left);
159
- const rightDate = toDateTimestamp(right);
160
- if (leftDate !== null && rightDate !== null) {
161
- return leftDate === rightDate;
162
- }
163
-
164
- return false;
165
- }
166
-
167
- function compareOrderValues(
168
- left: unknown,
169
- right: unknown,
170
- ): { left: number | string; right: number | string } | null {
171
- if (typeof left === "number" && typeof right === "number") {
172
- return { left, right };
173
- }
174
-
175
- const leftDate = toDateTimestamp(left);
176
- const rightDate = toDateTimestamp(right);
177
- if (leftDate !== null && rightDate !== null) {
178
- return { left: leftDate, right: rightDate };
179
- }
180
-
181
- if (typeof left === "string" && typeof right === "string") {
182
- return { left, right };
183
- }
184
-
185
- return null;
186
- }
187
-
188
- function hasKnownOperator(condition: Record<string, unknown>): boolean {
189
- return [
190
- "eq",
191
- "ne",
192
- "in",
193
- "nin",
194
- "gt",
195
- "gte",
196
- "lt",
197
- "lte",
198
- "exists",
199
- "regex",
200
- "options",
201
- "geoWithin",
202
- "includes",
203
- "includesAny",
204
- "length",
205
- ].some((key) => key in condition);
206
- }
207
-
208
- function hasPartialOverlap(left: unknown, right: unknown): boolean {
209
- if (left === null || left === undefined || right === null || right === undefined) {
210
- return false;
211
- }
212
-
213
- if (Array.isArray(left) && Array.isArray(right)) {
214
- return left.some((leftValue) => {
215
- return right.some((rightValue) => hasPartialOverlap(leftValue, rightValue));
216
- });
217
- }
218
-
219
- if (Array.isArray(left)) {
220
- return left.some((leftValue) => hasPartialOverlap(leftValue, right));
221
- }
222
-
223
- if (Array.isArray(right)) {
224
- return right.some((rightValue) => hasPartialOverlap(left, rightValue));
225
- }
226
-
227
- if (isRecord(left) && isRecord(right)) {
228
- const keys = Object.keys(left);
229
- for (const key of keys) {
230
- if (!(key in right)) {
231
- continue;
232
- }
233
-
234
- if (hasPartialOverlap(left[key], right[key])) {
235
- return true;
236
- }
237
- }
238
-
239
- return false;
240
- }
241
-
242
- return left === right;
243
- }
244
-
245
- function matchesWhereFieldCondition(
246
- condition: unknown,
247
- fieldValue: unknown,
248
- ): boolean {
249
- if (!isRecord(condition) || condition instanceof Date || Array.isArray(condition)) {
250
- return compareScalarValues(fieldValue, condition);
251
- }
252
-
253
- if (!hasKnownOperator(condition)) {
254
- return hasPartialOverlap(condition, fieldValue);
255
- }
256
-
257
- const eqValue = readOperatorValue(condition, "eq");
258
- if (eqValue !== undefined && !compareScalarValues(fieldValue, eqValue)) {
259
- return false;
260
- }
261
-
262
- const neValue = readOperatorValue(condition, "ne");
263
- if (neValue !== undefined && compareScalarValues(fieldValue, neValue)) {
264
- return false;
265
- }
266
-
267
- const inValue = readOperatorValue(condition, "in");
268
- if (Array.isArray(inValue) && inValue.length > 0) {
269
- if (!inValue.some((entry) => compareScalarValues(fieldValue, entry))) {
270
- return false;
271
- }
272
- }
273
-
274
- const ninValue = readOperatorValue(condition, "nin");
275
- if (Array.isArray(ninValue) && ninValue.length > 0) {
276
- if (ninValue.some((entry) => compareScalarValues(fieldValue, entry))) {
277
- return false;
278
- }
279
- }
280
-
281
- const gtValue = readOperatorValue(condition, "gt");
282
- if (gtValue !== undefined) {
283
- const comparable = compareOrderValues(fieldValue, gtValue);
284
- if (!comparable || !(comparable.left > comparable.right)) {
285
- return false;
286
- }
287
- }
288
-
289
- const gteValue = readOperatorValue(condition, "gte");
290
- if (gteValue !== undefined) {
291
- const comparable = compareOrderValues(fieldValue, gteValue);
292
- if (!comparable || !(comparable.left >= comparable.right)) {
293
- return false;
294
- }
295
- }
296
-
297
- const ltValue = readOperatorValue(condition, "lt");
298
- if (ltValue !== undefined) {
299
- const comparable = compareOrderValues(fieldValue, ltValue);
300
- if (!comparable || !(comparable.left < comparable.right)) {
301
- return false;
302
- }
303
- }
304
-
305
- const lteValue = readOperatorValue(condition, "lte");
306
- if (lteValue !== undefined) {
307
- const comparable = compareOrderValues(fieldValue, lteValue);
308
- if (!comparable || !(comparable.left <= comparable.right)) {
309
- return false;
310
- }
311
- }
312
-
313
- if (typeof condition.exists === "boolean") {
314
- const exists = fieldValue !== null && fieldValue !== undefined;
315
- if (exists !== condition.exists) {
316
- return false;
317
- }
318
- }
319
-
320
- if (typeof condition.regex === "string") {
321
- if (typeof fieldValue !== "string") {
322
- return false;
323
- }
324
-
325
- const caseInsensitive =
326
- typeof condition.$options === "string" && condition.$options.includes("i");
327
- const source = caseInsensitive ? fieldValue.toLowerCase() : fieldValue;
328
- const pattern = caseInsensitive
329
- ? condition.regex.toLowerCase()
330
- : condition.regex;
331
- if (!source.includes(pattern)) {
332
- return false;
333
- }
334
- }
335
-
336
- return true;
337
- }
338
-
339
- function doesWhereMatchCandidate(
340
- where: Record<string, unknown>,
341
- candidate: unknown,
342
- ): boolean {
343
- if (!isRecord(candidate)) {
344
- return false;
345
- }
346
-
347
- for (const [fieldName, condition] of Object.entries(where)) {
348
- if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
349
- continue;
350
- }
351
-
352
- if (!matchesWhereFieldCondition(condition, candidate[fieldName])) {
353
- return false;
354
- }
355
- }
356
-
357
- return true;
358
- }
359
-
360
- function collectMutationCandidates(event: DbMutationEvent): unknown[] {
361
- const candidates: unknown[] = [...event.rows];
362
- const values = event.args.values;
363
- if (Array.isArray(values)) {
364
- candidates.push(...values);
365
- } else if (values !== undefined) {
366
- candidates.push(values);
367
- }
368
-
369
- if (event.args.set !== undefined) {
370
- candidates.push(event.args.set);
371
- }
372
-
373
- if (event.args.where !== undefined) {
374
- candidates.push(event.args.where);
375
- }
376
-
377
- return candidates;
378
- }
379
-
380
- async function buildRealtimeQueryMatchPlan(
381
- handler: (ctx: AppflareContext, args: unknown) => Promise<unknown> | unknown,
382
- args: unknown,
383
- ctxSeed: RealtimePlanContextSeed,
384
- ): Promise<RealtimeQueryMatchPlan> {
385
- const descriptors: RealtimeQueryMatchDescriptor[] = [];
386
- let unsupported = false;
387
-
388
- const unsupportedError = (message: string): Error =>
389
- new Error("Realtime matcher unsupported: " + message);
390
-
391
- const dbProxy = new Proxy({} as Record<string, unknown>, {
392
- get(_target, tableProperty) {
393
- if (typeof tableProperty !== "string") {
394
- unsupported = true;
395
- throw unsupportedError("invalid table access");
396
- }
397
-
398
- const tableName = tableProperty;
399
- return new Proxy({} as Record<string, unknown>, {
400
- get(_tableTarget, methodProperty) {
401
- if (typeof methodProperty !== "string") {
402
- unsupported = true;
403
- throw unsupportedError("invalid method access");
404
- }
405
-
406
- if (
407
- methodProperty === "findMany" ||
408
- methodProperty === "findFirst" ||
409
- methodProperty === "count" ||
410
- methodProperty === "avg"
411
- ) {
412
- return async (queryArgs?: unknown) => {
413
- const methodName = methodProperty as RealtimeQueryMethod;
414
- descriptors.push({
415
- table: tableName,
416
- method: methodName,
417
- where: normalizeQueryWhereArg(queryArgs),
418
- });
419
- appendWithWhereDescriptors(
420
- descriptors,
421
- normalizeQueryWithArg(queryArgs),
422
- methodName,
423
- );
424
-
425
- if (methodProperty === "findMany") {
426
- return [];
427
- }
428
- if (methodProperty === "findFirst") {
429
- return null;
430
- }
431
- if (methodProperty === "count") {
432
- return 0;
433
- }
434
-
435
- return null;
436
- };
437
- }
438
-
439
- unsupported = true;
440
- throw unsupportedError("ctx.db." + tableName + "." + methodProperty);
441
- },
442
- });
443
- },
444
- });
445
-
446
- const planCtx = new Proxy({} as Record<string, unknown>, {
447
- get(_target, property) {
448
- if (property === "db") {
449
- return dbProxy;
450
- }
451
- if (property === "user") {
452
- return ctxSeed.user;
453
- }
454
- if (property === "session") {
455
- return ctxSeed.session;
456
- }
457
-
458
- unsupported = true;
459
- throw unsupportedError("ctx." + String(property));
460
- },
461
- });
462
-
463
- try {
464
- await handler(planCtx as AppflareContext, args);
465
- } catch (_error) {
466
- unsupported = true;
467
- }
468
-
469
- if (descriptors.length === 0) {
470
- unsupported = true;
471
- }
472
-
473
- return {
474
- descriptors,
475
- unsupported,
476
- };
477
- }
478
-
479
- function doesSubscriptionMatchMutation(
480
- plan: RealtimeQueryMatchPlan,
481
- event: DbMutationEvent,
482
- ): boolean {
483
- if (plan.unsupported) {
484
- return false;
485
- }
486
-
487
- const descriptors = plan.descriptors.filter(
488
- (descriptor) => descriptor.table === event.table,
489
- );
490
- if (descriptors.length === 0) {
491
- return false;
492
- }
493
-
494
- const candidates = collectMutationCandidates(event);
495
- for (const descriptor of descriptors) {
496
- if (!descriptor.where) {
497
- if (candidates.length > 0) {
498
- return true;
499
- }
500
- continue;
501
- }
502
-
503
- if (candidates.some((candidate) => doesWhereMatchCandidate(descriptor.where as Record<string, unknown>, candidate))) {
504
- return true;
505
- }
506
- }
507
-
508
- return false;
509
- }
510
- `;
1
+ export const realtimeUtilsModule = `
2
+ function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+
6
+ function stableStringify(value: unknown): string {
7
+ if (Array.isArray(value)) {
8
+ return "[" + value.map((entry) => stableStringify(entry)).join(",") + "]";
9
+ }
10
+
11
+ if (value instanceof Date) {
12
+ return JSON.stringify(value.toISOString());
13
+ }
14
+
15
+ if (isRecord(value)) {
16
+ const keys = Object.keys(value).sort((a, b) => a.localeCompare(b));
17
+ return (
18
+ "{" +
19
+ keys
20
+ .map((key) => JSON.stringify(key) + ":" + stableStringify(value[key]))
21
+ .join(",") +
22
+ "}"
23
+ );
24
+ }
25
+
26
+ return JSON.stringify(value ?? null);
27
+ }
28
+
29
+ function normalizeComparableValue(value: unknown): unknown {
30
+ if (Array.isArray(value)) {
31
+ return value.map((entry) => normalizeComparableValue(entry));
32
+ }
33
+
34
+ if (value instanceof Date) {
35
+ return value.toISOString();
36
+ }
37
+
38
+ if (isRecord(value)) {
39
+ return Object.entries(value)
40
+ .sort(([a], [b]) => a.localeCompare(b))
41
+ .reduce<Record<string, unknown>>((accumulator, [key, entry]) => {
42
+ accumulator[key] = normalizeComparableValue(entry);
43
+ return accumulator;
44
+ }, {});
45
+ }
46
+
47
+ return value;
48
+ }
49
+
50
+ function createSubscriptionSignature(
51
+ queryName: string,
52
+ args: Record<string, unknown>,
53
+ ): string {
54
+ const normalizedArgs = normalizeComparableValue(args);
55
+ return queryName + "::" + stableStringify(normalizedArgs);
56
+ }
57
+
58
+ type RealtimeQueryMethod = "findMany" | "findFirst" | "count" | "avg";
59
+
60
+ type RealtimeQueryMatchDescriptor = {
61
+ table: string;
62
+ method: RealtimeQueryMethod;
63
+ where: Record<string, unknown> | null;
64
+ };
65
+
66
+ type RealtimeQueryMatchPlan = {
67
+ descriptors: RealtimeQueryMatchDescriptor[];
68
+ unsupported: boolean;
69
+ };
70
+
71
+ type RealtimePlanContextSeed = {
72
+ user: unknown;
73
+ session: unknown;
74
+ };
75
+
76
+ function normalizeQueryWhereArg(args: unknown): Record<string, unknown> | null {
77
+ if (!isRecord(args)) {
78
+ return null;
79
+ }
80
+
81
+ const where = args.where;
82
+ if (isRecord(where)) {
83
+ return where;
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function normalizeQueryWithArg(args: unknown): Record<string, unknown> | null {
90
+ if (!isRecord(args)) {
91
+ return null;
92
+ }
93
+
94
+ const withArg = args.with;
95
+ if (isRecord(withArg)) {
96
+ return withArg;
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ function appendWithWhereDescriptors(
103
+ descriptors: RealtimeQueryMatchDescriptor[],
104
+ withInput: unknown,
105
+ method: RealtimeQueryMethod,
106
+ ): void {
107
+ if (!isRecord(withInput)) {
108
+ return;
109
+ }
110
+
111
+ for (const [relationName, relationValue] of Object.entries(withInput)) {
112
+ if (!isRecord(relationValue)) {
113
+ continue;
114
+ }
115
+
116
+ const relationWhere = relationValue.where;
117
+ if (isRecord(relationWhere)) {
118
+ descriptors.push({
119
+ table: relationName,
120
+ method,
121
+ where: relationWhere,
122
+ });
123
+ }
124
+
125
+ appendWithWhereDescriptors(descriptors, relationValue.with, method);
126
+ }
127
+ }
128
+
129
+ function readOperatorValue(record: Record<string, unknown>, key: string): unknown {
130
+ if (record[key] !== undefined) {
131
+ return record[key];
132
+ }
133
+ const prefixed = "$" + key;
134
+ return record[prefixed];
135
+ }
136
+
137
+ function toDateTimestamp(value: unknown): number | null {
138
+ if (value instanceof Date) {
139
+ return value.getTime();
140
+ }
141
+
142
+ if (typeof value === "string" || typeof value === "number") {
143
+ const date = new Date(value);
144
+ const timestamp = date.getTime();
145
+ if (!Number.isNaN(timestamp)) {
146
+ return timestamp;
147
+ }
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ function compareScalarValues(left: unknown, right: unknown): boolean {
154
+ if (left === right) {
155
+ return true;
156
+ }
157
+
158
+ const leftDate = toDateTimestamp(left);
159
+ const rightDate = toDateTimestamp(right);
160
+ if (leftDate !== null && rightDate !== null) {
161
+ return leftDate === rightDate;
162
+ }
163
+
164
+ return false;
165
+ }
166
+
167
+ function compareOrderValues(
168
+ left: unknown,
169
+ right: unknown,
170
+ ): { left: number | string; right: number | string } | null {
171
+ if (typeof left === "number" && typeof right === "number") {
172
+ return { left, right };
173
+ }
174
+
175
+ const leftDate = toDateTimestamp(left);
176
+ const rightDate = toDateTimestamp(right);
177
+ if (leftDate !== null && rightDate !== null) {
178
+ return { left: leftDate, right: rightDate };
179
+ }
180
+
181
+ if (typeof left === "string" && typeof right === "string") {
182
+ return { left, right };
183
+ }
184
+
185
+ return null;
186
+ }
187
+
188
+ function hasKnownOperator(condition: Record<string, unknown>): boolean {
189
+ return [
190
+ "eq",
191
+ "ne",
192
+ "in",
193
+ "nin",
194
+ "gt",
195
+ "gte",
196
+ "lt",
197
+ "lte",
198
+ "exists",
199
+ "regex",
200
+ "options",
201
+ "geoWithin",
202
+ "includes",
203
+ "includesAny",
204
+ "length",
205
+ ].some((key) => key in condition);
206
+ }
207
+
208
+ function hasPartialOverlap(left: unknown, right: unknown): boolean {
209
+ if (left === null || left === undefined || right === null || right === undefined) {
210
+ return false;
211
+ }
212
+
213
+ if (Array.isArray(left) && Array.isArray(right)) {
214
+ return left.some((leftValue) => {
215
+ return right.some((rightValue) => hasPartialOverlap(leftValue, rightValue));
216
+ });
217
+ }
218
+
219
+ if (Array.isArray(left)) {
220
+ return left.some((leftValue) => hasPartialOverlap(leftValue, right));
221
+ }
222
+
223
+ if (Array.isArray(right)) {
224
+ return right.some((rightValue) => hasPartialOverlap(left, rightValue));
225
+ }
226
+
227
+ if (isRecord(left) && isRecord(right)) {
228
+ const keys = Object.keys(left);
229
+ for (const key of keys) {
230
+ if (!(key in right)) {
231
+ continue;
232
+ }
233
+
234
+ if (hasPartialOverlap(left[key], right[key])) {
235
+ return true;
236
+ }
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ return left === right;
243
+ }
244
+
245
+ function matchesWhereFieldCondition(
246
+ condition: unknown,
247
+ fieldValue: unknown,
248
+ ): boolean {
249
+ if (!isRecord(condition) || condition instanceof Date || Array.isArray(condition)) {
250
+ return compareScalarValues(fieldValue, condition);
251
+ }
252
+
253
+ if (!hasKnownOperator(condition)) {
254
+ return hasPartialOverlap(condition, fieldValue);
255
+ }
256
+
257
+ const eqValue = readOperatorValue(condition, "eq");
258
+ if (eqValue !== undefined && !compareScalarValues(fieldValue, eqValue)) {
259
+ return false;
260
+ }
261
+
262
+ const neValue = readOperatorValue(condition, "ne");
263
+ if (neValue !== undefined && compareScalarValues(fieldValue, neValue)) {
264
+ return false;
265
+ }
266
+
267
+ const inValue = readOperatorValue(condition, "in");
268
+ if (Array.isArray(inValue) && inValue.length > 0) {
269
+ if (!inValue.some((entry) => compareScalarValues(fieldValue, entry))) {
270
+ return false;
271
+ }
272
+ }
273
+
274
+ const ninValue = readOperatorValue(condition, "nin");
275
+ if (Array.isArray(ninValue) && ninValue.length > 0) {
276
+ if (ninValue.some((entry) => compareScalarValues(fieldValue, entry))) {
277
+ return false;
278
+ }
279
+ }
280
+
281
+ const gtValue = readOperatorValue(condition, "gt");
282
+ if (gtValue !== undefined) {
283
+ const comparable = compareOrderValues(fieldValue, gtValue);
284
+ if (!comparable || !(comparable.left > comparable.right)) {
285
+ return false;
286
+ }
287
+ }
288
+
289
+ const gteValue = readOperatorValue(condition, "gte");
290
+ if (gteValue !== undefined) {
291
+ const comparable = compareOrderValues(fieldValue, gteValue);
292
+ if (!comparable || !(comparable.left >= comparable.right)) {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ const ltValue = readOperatorValue(condition, "lt");
298
+ if (ltValue !== undefined) {
299
+ const comparable = compareOrderValues(fieldValue, ltValue);
300
+ if (!comparable || !(comparable.left < comparable.right)) {
301
+ return false;
302
+ }
303
+ }
304
+
305
+ const lteValue = readOperatorValue(condition, "lte");
306
+ if (lteValue !== undefined) {
307
+ const comparable = compareOrderValues(fieldValue, lteValue);
308
+ if (!comparable || !(comparable.left <= comparable.right)) {
309
+ return false;
310
+ }
311
+ }
312
+
313
+ if (typeof condition.exists === "boolean") {
314
+ const exists = fieldValue !== null && fieldValue !== undefined;
315
+ if (exists !== condition.exists) {
316
+ return false;
317
+ }
318
+ }
319
+
320
+ if (typeof condition.regex === "string") {
321
+ if (typeof fieldValue !== "string") {
322
+ return false;
323
+ }
324
+
325
+ const caseInsensitive =
326
+ typeof condition.$options === "string" && condition.$options.includes("i");
327
+ const source = caseInsensitive ? fieldValue.toLowerCase() : fieldValue;
328
+ const pattern = caseInsensitive
329
+ ? condition.regex.toLowerCase()
330
+ : condition.regex;
331
+ if (!source.includes(pattern)) {
332
+ return false;
333
+ }
334
+ }
335
+
336
+ return true;
337
+ }
338
+
339
+ function doesWhereMatchCandidate(
340
+ where: Record<string, unknown>,
341
+ candidate: unknown,
342
+ ): boolean {
343
+ if (!isRecord(candidate)) {
344
+ return false;
345
+ }
346
+
347
+ for (const [fieldName, condition] of Object.entries(where)) {
348
+ if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
349
+ continue;
350
+ }
351
+
352
+ if (!matchesWhereFieldCondition(condition, candidate[fieldName])) {
353
+ return false;
354
+ }
355
+ }
356
+
357
+ return true;
358
+ }
359
+
360
+ function collectMutationCandidates(event: DbMutationEvent): unknown[] {
361
+ const candidates: unknown[] = [...event.rows];
362
+ const values = event.args.values;
363
+ if (Array.isArray(values)) {
364
+ candidates.push(...values);
365
+ } else if (values !== undefined) {
366
+ candidates.push(values);
367
+ }
368
+
369
+ if (event.args.set !== undefined) {
370
+ candidates.push(event.args.set);
371
+ }
372
+
373
+ if (event.args.where !== undefined) {
374
+ candidates.push(event.args.where);
375
+ }
376
+
377
+ return candidates;
378
+ }
379
+
380
+ async function buildRealtimeQueryMatchPlan(
381
+ handler: (ctx: AppflareContext, args: unknown) => Promise<unknown> | unknown,
382
+ args: unknown,
383
+ ctxSeed: RealtimePlanContextSeed,
384
+ ): Promise<RealtimeQueryMatchPlan> {
385
+ const descriptors: RealtimeQueryMatchDescriptor[] = [];
386
+ let unsupported = false;
387
+
388
+ const unsupportedError = (message: string): Error =>
389
+ new Error("Realtime matcher unsupported: " + message);
390
+
391
+ const dbProxy = new Proxy({} as Record<string, unknown>, {
392
+ get(_target, tableProperty) {
393
+ if (typeof tableProperty !== "string") {
394
+ unsupported = true;
395
+ throw unsupportedError("invalid table access");
396
+ }
397
+
398
+ const tableName = tableProperty;
399
+ return new Proxy({} as Record<string, unknown>, {
400
+ get(_tableTarget, methodProperty) {
401
+ if (typeof methodProperty !== "string") {
402
+ unsupported = true;
403
+ throw unsupportedError("invalid method access");
404
+ }
405
+
406
+ if (
407
+ methodProperty === "findMany" ||
408
+ methodProperty === "findFirst" ||
409
+ methodProperty === "count" ||
410
+ methodProperty === "avg"
411
+ ) {
412
+ return async (queryArgs?: unknown) => {
413
+ const methodName = methodProperty as RealtimeQueryMethod;
414
+ descriptors.push({
415
+ table: tableName,
416
+ method: methodName,
417
+ where: normalizeQueryWhereArg(queryArgs),
418
+ });
419
+ appendWithWhereDescriptors(
420
+ descriptors,
421
+ normalizeQueryWithArg(queryArgs),
422
+ methodName,
423
+ );
424
+
425
+ if (methodProperty === "findMany") {
426
+ return [];
427
+ }
428
+ if (methodProperty === "findFirst") {
429
+ return null;
430
+ }
431
+ if (methodProperty === "count") {
432
+ return 0;
433
+ }
434
+
435
+ return null;
436
+ };
437
+ }
438
+
439
+ unsupported = true;
440
+ throw unsupportedError("ctx.db." + tableName + "." + methodProperty);
441
+ },
442
+ });
443
+ },
444
+ });
445
+
446
+ const planCtx = new Proxy({} as Record<string, unknown>, {
447
+ get(_target, property) {
448
+ if (property === "db") {
449
+ return dbProxy;
450
+ }
451
+ if (property === "user") {
452
+ return ctxSeed.user;
453
+ }
454
+ if (property === "session") {
455
+ return ctxSeed.session;
456
+ }
457
+
458
+ unsupported = true;
459
+ throw unsupportedError("ctx." + String(property));
460
+ },
461
+ });
462
+
463
+ try {
464
+ await handler(planCtx as AppflareContext, args);
465
+ } catch (_error) {
466
+ unsupported = true;
467
+ }
468
+
469
+ if (descriptors.length === 0) {
470
+ unsupported = true;
471
+ }
472
+
473
+ return {
474
+ descriptors,
475
+ unsupported,
476
+ };
477
+ }
478
+
479
+ function doesSubscriptionMatchMutation(
480
+ plan: RealtimeQueryMatchPlan,
481
+ event: DbMutationEvent,
482
+ ): boolean {
483
+ if (plan.unsupported) {
484
+ return false;
485
+ }
486
+
487
+ const descriptors = plan.descriptors.filter(
488
+ (descriptor) => descriptor.table === event.table,
489
+ );
490
+ if (descriptors.length === 0) {
491
+ return false;
492
+ }
493
+
494
+ const candidates = collectMutationCandidates(event);
495
+ for (const descriptor of descriptors) {
496
+ if (!descriptor.where) {
497
+ if (candidates.length > 0) {
498
+ return true;
499
+ }
500
+ continue;
501
+ }
502
+
503
+ if (candidates.some((candidate) => doesWhereMatchCandidate(descriptor.where as Record<string, unknown>, candidate))) {
504
+ return true;
505
+ }
506
+ }
507
+
508
+ return false;
509
+ }
510
+ `;