appflare 0.2.24 → 0.2.26

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 (138) 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 -749
  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 +180 -180
  23. package/cli/templates/core/client/types.ts +184 -184
  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 +134 -112
  87. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  88. package/cli/templates/handlers/generators/context/types.ts +18 -18
  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 +196 -194
  102. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  103. package/cli/templates/handlers/generators/types/context.ts +68 -66
  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 +259 -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 +1031 -1031
  109. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +246 -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 +121 -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 +676 -676
  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 -83
  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.js +61 -28
  128. package/dist/cli/index.mjs +61 -28
  129. package/index.ts +18 -18
  130. package/package.json +58 -58
  131. package/react/index.ts +5 -5
  132. package/react/use-infinite-query.ts +252 -252
  133. package/react/use-mutation.ts +89 -89
  134. package/react/use-query.ts +207 -207
  135. package/schema.ts +415 -415
  136. package/test-better-auth-hash.ts +2 -2
  137. package/tsconfig.json +6 -6
  138. package/tsup.config.ts +82 -82
@@ -1,516 +1,516 @@
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
- "$eq",
192
- "ne",
193
- "$ne",
194
- "in",
195
- "$in",
196
- "nin",
197
- "$nin",
198
- "gt",
199
- "$gt",
200
- "gte",
201
- "$gte",
202
- "lt",
203
- "$lt",
204
- "lte",
205
- "$lte",
206
- "exists",
207
- "regex",
208
- "$options",
209
- "geoWithin",
210
- "$geoWithin",
211
- ].some((key) => key in condition);
212
- }
213
-
214
- function hasPartialOverlap(left: unknown, right: unknown): boolean {
215
- if (left === null || left === undefined || right === null || right === undefined) {
216
- return false;
217
- }
218
-
219
- if (Array.isArray(left) && Array.isArray(right)) {
220
- return left.some((leftValue) => {
221
- return right.some((rightValue) => hasPartialOverlap(leftValue, rightValue));
222
- });
223
- }
224
-
225
- if (Array.isArray(left)) {
226
- return left.some((leftValue) => hasPartialOverlap(leftValue, right));
227
- }
228
-
229
- if (Array.isArray(right)) {
230
- return right.some((rightValue) => hasPartialOverlap(left, rightValue));
231
- }
232
-
233
- if (isRecord(left) && isRecord(right)) {
234
- const keys = Object.keys(left);
235
- for (const key of keys) {
236
- if (!(key in right)) {
237
- continue;
238
- }
239
-
240
- if (hasPartialOverlap(left[key], right[key])) {
241
- return true;
242
- }
243
- }
244
-
245
- return false;
246
- }
247
-
248
- return left === right;
249
- }
250
-
251
- function matchesWhereFieldCondition(
252
- condition: unknown,
253
- fieldValue: unknown,
254
- ): boolean {
255
- if (!isRecord(condition) || condition instanceof Date || Array.isArray(condition)) {
256
- return compareScalarValues(fieldValue, condition);
257
- }
258
-
259
- if (!hasKnownOperator(condition)) {
260
- return hasPartialOverlap(condition, fieldValue);
261
- }
262
-
263
- const eqValue = readOperatorValue(condition, "eq");
264
- if (eqValue !== undefined && !compareScalarValues(fieldValue, eqValue)) {
265
- return false;
266
- }
267
-
268
- const neValue = readOperatorValue(condition, "ne");
269
- if (neValue !== undefined && compareScalarValues(fieldValue, neValue)) {
270
- return false;
271
- }
272
-
273
- const inValue = readOperatorValue(condition, "in");
274
- if (Array.isArray(inValue) && inValue.length > 0) {
275
- if (!inValue.some((entry) => compareScalarValues(fieldValue, entry))) {
276
- return false;
277
- }
278
- }
279
-
280
- const ninValue = readOperatorValue(condition, "nin");
281
- if (Array.isArray(ninValue) && ninValue.length > 0) {
282
- if (ninValue.some((entry) => compareScalarValues(fieldValue, entry))) {
283
- return false;
284
- }
285
- }
286
-
287
- const gtValue = readOperatorValue(condition, "gt");
288
- if (gtValue !== undefined) {
289
- const comparable = compareOrderValues(fieldValue, gtValue);
290
- if (!comparable || !(comparable.left > comparable.right)) {
291
- return false;
292
- }
293
- }
294
-
295
- const gteValue = readOperatorValue(condition, "gte");
296
- if (gteValue !== undefined) {
297
- const comparable = compareOrderValues(fieldValue, gteValue);
298
- if (!comparable || !(comparable.left >= comparable.right)) {
299
- return false;
300
- }
301
- }
302
-
303
- const ltValue = readOperatorValue(condition, "lt");
304
- if (ltValue !== undefined) {
305
- const comparable = compareOrderValues(fieldValue, ltValue);
306
- if (!comparable || !(comparable.left < comparable.right)) {
307
- return false;
308
- }
309
- }
310
-
311
- const lteValue = readOperatorValue(condition, "lte");
312
- if (lteValue !== undefined) {
313
- const comparable = compareOrderValues(fieldValue, lteValue);
314
- if (!comparable || !(comparable.left <= comparable.right)) {
315
- return false;
316
- }
317
- }
318
-
319
- if (typeof condition.exists === "boolean") {
320
- const exists = fieldValue !== null && fieldValue !== undefined;
321
- if (exists !== condition.exists) {
322
- return false;
323
- }
324
- }
325
-
326
- if (typeof condition.regex === "string") {
327
- if (typeof fieldValue !== "string") {
328
- return false;
329
- }
330
-
331
- const caseInsensitive =
332
- typeof condition.$options === "string" && condition.$options.includes("i");
333
- const source = caseInsensitive ? fieldValue.toLowerCase() : fieldValue;
334
- const pattern = caseInsensitive
335
- ? condition.regex.toLowerCase()
336
- : condition.regex;
337
- if (!source.includes(pattern)) {
338
- return false;
339
- }
340
- }
341
-
342
- return true;
343
- }
344
-
345
- function doesWhereMatchCandidate(
346
- where: Record<string, unknown>,
347
- candidate: unknown,
348
- ): boolean {
349
- if (!isRecord(candidate)) {
350
- return false;
351
- }
352
-
353
- for (const [fieldName, condition] of Object.entries(where)) {
354
- if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
355
- continue;
356
- }
357
-
358
- if (!matchesWhereFieldCondition(condition, candidate[fieldName])) {
359
- return false;
360
- }
361
- }
362
-
363
- return true;
364
- }
365
-
366
- function collectMutationCandidates(event: DbMutationEvent): unknown[] {
367
- const candidates: unknown[] = [...event.rows];
368
- const values = event.args.values;
369
- if (Array.isArray(values)) {
370
- candidates.push(...values);
371
- } else if (values !== undefined) {
372
- candidates.push(values);
373
- }
374
-
375
- if (event.args.set !== undefined) {
376
- candidates.push(event.args.set);
377
- }
378
-
379
- if (event.args.where !== undefined) {
380
- candidates.push(event.args.where);
381
- }
382
-
383
- return candidates;
384
- }
385
-
386
- async function buildRealtimeQueryMatchPlan(
387
- handler: (ctx: AppflareContext, args: unknown) => Promise<unknown> | unknown,
388
- args: unknown,
389
- ctxSeed: RealtimePlanContextSeed,
390
- ): Promise<RealtimeQueryMatchPlan> {
391
- const descriptors: RealtimeQueryMatchDescriptor[] = [];
392
- let unsupported = false;
393
-
394
- const unsupportedError = (message: string): Error =>
395
- new Error("Realtime matcher unsupported: " + message);
396
-
397
- const dbProxy = new Proxy({} as Record<string, unknown>, {
398
- get(_target, tableProperty) {
399
- if (typeof tableProperty !== "string") {
400
- unsupported = true;
401
- throw unsupportedError("invalid table access");
402
- }
403
-
404
- const tableName = tableProperty;
405
- return new Proxy({} as Record<string, unknown>, {
406
- get(_tableTarget, methodProperty) {
407
- if (typeof methodProperty !== "string") {
408
- unsupported = true;
409
- throw unsupportedError("invalid method access");
410
- }
411
-
412
- if (
413
- methodProperty === "findMany" ||
414
- methodProperty === "findFirst" ||
415
- methodProperty === "count" ||
416
- methodProperty === "avg"
417
- ) {
418
- return async (queryArgs?: unknown) => {
419
- const methodName = methodProperty as RealtimeQueryMethod;
420
- descriptors.push({
421
- table: tableName,
422
- method: methodName,
423
- where: normalizeQueryWhereArg(queryArgs),
424
- });
425
- appendWithWhereDescriptors(
426
- descriptors,
427
- normalizeQueryWithArg(queryArgs),
428
- methodName,
429
- );
430
-
431
- if (methodProperty === "findMany") {
432
- return [];
433
- }
434
- if (methodProperty === "findFirst") {
435
- return null;
436
- }
437
- if (methodProperty === "count") {
438
- return 0;
439
- }
440
-
441
- return null;
442
- };
443
- }
444
-
445
- unsupported = true;
446
- throw unsupportedError("ctx.db." + tableName + "." + methodProperty);
447
- },
448
- });
449
- },
450
- });
451
-
452
- const planCtx = new Proxy({} as Record<string, unknown>, {
453
- get(_target, property) {
454
- if (property === "db") {
455
- return dbProxy;
456
- }
457
- if (property === "user") {
458
- return ctxSeed.user;
459
- }
460
- if (property === "session") {
461
- return ctxSeed.session;
462
- }
463
-
464
- unsupported = true;
465
- throw unsupportedError("ctx." + String(property));
466
- },
467
- });
468
-
469
- try {
470
- await handler(planCtx as AppflareContext, args);
471
- } catch (_error) {
472
- unsupported = true;
473
- }
474
-
475
- if (descriptors.length === 0) {
476
- unsupported = true;
477
- }
478
-
479
- return {
480
- descriptors,
481
- unsupported,
482
- };
483
- }
484
-
485
- function doesSubscriptionMatchMutation(
486
- plan: RealtimeQueryMatchPlan,
487
- event: DbMutationEvent,
488
- ): boolean {
489
- if (plan.unsupported) {
490
- return false;
491
- }
492
-
493
- const descriptors = plan.descriptors.filter(
494
- (descriptor) => descriptor.table === event.table,
495
- );
496
- if (descriptors.length === 0) {
497
- return false;
498
- }
499
-
500
- const candidates = collectMutationCandidates(event);
501
- for (const descriptor of descriptors) {
502
- if (!descriptor.where) {
503
- if (candidates.length > 0) {
504
- return true;
505
- }
506
- continue;
507
- }
508
-
509
- if (candidates.some((candidate) => doesWhereMatchCandidate(descriptor.where as Record<string, unknown>, candidate))) {
510
- return true;
511
- }
512
- }
513
-
514
- return false;
515
- }
516
- `;
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
+ "$eq",
192
+ "ne",
193
+ "$ne",
194
+ "in",
195
+ "$in",
196
+ "nin",
197
+ "$nin",
198
+ "gt",
199
+ "$gt",
200
+ "gte",
201
+ "$gte",
202
+ "lt",
203
+ "$lt",
204
+ "lte",
205
+ "$lte",
206
+ "exists",
207
+ "regex",
208
+ "$options",
209
+ "geoWithin",
210
+ "$geoWithin",
211
+ ].some((key) => key in condition);
212
+ }
213
+
214
+ function hasPartialOverlap(left: unknown, right: unknown): boolean {
215
+ if (left === null || left === undefined || right === null || right === undefined) {
216
+ return false;
217
+ }
218
+
219
+ if (Array.isArray(left) && Array.isArray(right)) {
220
+ return left.some((leftValue) => {
221
+ return right.some((rightValue) => hasPartialOverlap(leftValue, rightValue));
222
+ });
223
+ }
224
+
225
+ if (Array.isArray(left)) {
226
+ return left.some((leftValue) => hasPartialOverlap(leftValue, right));
227
+ }
228
+
229
+ if (Array.isArray(right)) {
230
+ return right.some((rightValue) => hasPartialOverlap(left, rightValue));
231
+ }
232
+
233
+ if (isRecord(left) && isRecord(right)) {
234
+ const keys = Object.keys(left);
235
+ for (const key of keys) {
236
+ if (!(key in right)) {
237
+ continue;
238
+ }
239
+
240
+ if (hasPartialOverlap(left[key], right[key])) {
241
+ return true;
242
+ }
243
+ }
244
+
245
+ return false;
246
+ }
247
+
248
+ return left === right;
249
+ }
250
+
251
+ function matchesWhereFieldCondition(
252
+ condition: unknown,
253
+ fieldValue: unknown,
254
+ ): boolean {
255
+ if (!isRecord(condition) || condition instanceof Date || Array.isArray(condition)) {
256
+ return compareScalarValues(fieldValue, condition);
257
+ }
258
+
259
+ if (!hasKnownOperator(condition)) {
260
+ return hasPartialOverlap(condition, fieldValue);
261
+ }
262
+
263
+ const eqValue = readOperatorValue(condition, "eq");
264
+ if (eqValue !== undefined && !compareScalarValues(fieldValue, eqValue)) {
265
+ return false;
266
+ }
267
+
268
+ const neValue = readOperatorValue(condition, "ne");
269
+ if (neValue !== undefined && compareScalarValues(fieldValue, neValue)) {
270
+ return false;
271
+ }
272
+
273
+ const inValue = readOperatorValue(condition, "in");
274
+ if (Array.isArray(inValue) && inValue.length > 0) {
275
+ if (!inValue.some((entry) => compareScalarValues(fieldValue, entry))) {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ const ninValue = readOperatorValue(condition, "nin");
281
+ if (Array.isArray(ninValue) && ninValue.length > 0) {
282
+ if (ninValue.some((entry) => compareScalarValues(fieldValue, entry))) {
283
+ return false;
284
+ }
285
+ }
286
+
287
+ const gtValue = readOperatorValue(condition, "gt");
288
+ if (gtValue !== undefined) {
289
+ const comparable = compareOrderValues(fieldValue, gtValue);
290
+ if (!comparable || !(comparable.left > comparable.right)) {
291
+ return false;
292
+ }
293
+ }
294
+
295
+ const gteValue = readOperatorValue(condition, "gte");
296
+ if (gteValue !== undefined) {
297
+ const comparable = compareOrderValues(fieldValue, gteValue);
298
+ if (!comparable || !(comparable.left >= comparable.right)) {
299
+ return false;
300
+ }
301
+ }
302
+
303
+ const ltValue = readOperatorValue(condition, "lt");
304
+ if (ltValue !== undefined) {
305
+ const comparable = compareOrderValues(fieldValue, ltValue);
306
+ if (!comparable || !(comparable.left < comparable.right)) {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ const lteValue = readOperatorValue(condition, "lte");
312
+ if (lteValue !== undefined) {
313
+ const comparable = compareOrderValues(fieldValue, lteValue);
314
+ if (!comparable || !(comparable.left <= comparable.right)) {
315
+ return false;
316
+ }
317
+ }
318
+
319
+ if (typeof condition.exists === "boolean") {
320
+ const exists = fieldValue !== null && fieldValue !== undefined;
321
+ if (exists !== condition.exists) {
322
+ return false;
323
+ }
324
+ }
325
+
326
+ if (typeof condition.regex === "string") {
327
+ if (typeof fieldValue !== "string") {
328
+ return false;
329
+ }
330
+
331
+ const caseInsensitive =
332
+ typeof condition.$options === "string" && condition.$options.includes("i");
333
+ const source = caseInsensitive ? fieldValue.toLowerCase() : fieldValue;
334
+ const pattern = caseInsensitive
335
+ ? condition.regex.toLowerCase()
336
+ : condition.regex;
337
+ if (!source.includes(pattern)) {
338
+ return false;
339
+ }
340
+ }
341
+
342
+ return true;
343
+ }
344
+
345
+ function doesWhereMatchCandidate(
346
+ where: Record<string, unknown>,
347
+ candidate: unknown,
348
+ ): boolean {
349
+ if (!isRecord(candidate)) {
350
+ return false;
351
+ }
352
+
353
+ for (const [fieldName, condition] of Object.entries(where)) {
354
+ if (fieldName === "geoWithin" || fieldName === "$geoWithin") {
355
+ continue;
356
+ }
357
+
358
+ if (!matchesWhereFieldCondition(condition, candidate[fieldName])) {
359
+ return false;
360
+ }
361
+ }
362
+
363
+ return true;
364
+ }
365
+
366
+ function collectMutationCandidates(event: DbMutationEvent): unknown[] {
367
+ const candidates: unknown[] = [...event.rows];
368
+ const values = event.args.values;
369
+ if (Array.isArray(values)) {
370
+ candidates.push(...values);
371
+ } else if (values !== undefined) {
372
+ candidates.push(values);
373
+ }
374
+
375
+ if (event.args.set !== undefined) {
376
+ candidates.push(event.args.set);
377
+ }
378
+
379
+ if (event.args.where !== undefined) {
380
+ candidates.push(event.args.where);
381
+ }
382
+
383
+ return candidates;
384
+ }
385
+
386
+ async function buildRealtimeQueryMatchPlan(
387
+ handler: (ctx: AppflareContext, args: unknown) => Promise<unknown> | unknown,
388
+ args: unknown,
389
+ ctxSeed: RealtimePlanContextSeed,
390
+ ): Promise<RealtimeQueryMatchPlan> {
391
+ const descriptors: RealtimeQueryMatchDescriptor[] = [];
392
+ let unsupported = false;
393
+
394
+ const unsupportedError = (message: string): Error =>
395
+ new Error("Realtime matcher unsupported: " + message);
396
+
397
+ const dbProxy = new Proxy({} as Record<string, unknown>, {
398
+ get(_target, tableProperty) {
399
+ if (typeof tableProperty !== "string") {
400
+ unsupported = true;
401
+ throw unsupportedError("invalid table access");
402
+ }
403
+
404
+ const tableName = tableProperty;
405
+ return new Proxy({} as Record<string, unknown>, {
406
+ get(_tableTarget, methodProperty) {
407
+ if (typeof methodProperty !== "string") {
408
+ unsupported = true;
409
+ throw unsupportedError("invalid method access");
410
+ }
411
+
412
+ if (
413
+ methodProperty === "findMany" ||
414
+ methodProperty === "findFirst" ||
415
+ methodProperty === "count" ||
416
+ methodProperty === "avg"
417
+ ) {
418
+ return async (queryArgs?: unknown) => {
419
+ const methodName = methodProperty as RealtimeQueryMethod;
420
+ descriptors.push({
421
+ table: tableName,
422
+ method: methodName,
423
+ where: normalizeQueryWhereArg(queryArgs),
424
+ });
425
+ appendWithWhereDescriptors(
426
+ descriptors,
427
+ normalizeQueryWithArg(queryArgs),
428
+ methodName,
429
+ );
430
+
431
+ if (methodProperty === "findMany") {
432
+ return [];
433
+ }
434
+ if (methodProperty === "findFirst") {
435
+ return null;
436
+ }
437
+ if (methodProperty === "count") {
438
+ return 0;
439
+ }
440
+
441
+ return null;
442
+ };
443
+ }
444
+
445
+ unsupported = true;
446
+ throw unsupportedError("ctx.db." + tableName + "." + methodProperty);
447
+ },
448
+ });
449
+ },
450
+ });
451
+
452
+ const planCtx = new Proxy({} as Record<string, unknown>, {
453
+ get(_target, property) {
454
+ if (property === "db") {
455
+ return dbProxy;
456
+ }
457
+ if (property === "user") {
458
+ return ctxSeed.user;
459
+ }
460
+ if (property === "session") {
461
+ return ctxSeed.session;
462
+ }
463
+
464
+ unsupported = true;
465
+ throw unsupportedError("ctx." + String(property));
466
+ },
467
+ });
468
+
469
+ try {
470
+ await handler(planCtx as AppflareContext, args);
471
+ } catch (_error) {
472
+ unsupported = true;
473
+ }
474
+
475
+ if (descriptors.length === 0) {
476
+ unsupported = true;
477
+ }
478
+
479
+ return {
480
+ descriptors,
481
+ unsupported,
482
+ };
483
+ }
484
+
485
+ function doesSubscriptionMatchMutation(
486
+ plan: RealtimeQueryMatchPlan,
487
+ event: DbMutationEvent,
488
+ ): boolean {
489
+ if (plan.unsupported) {
490
+ return false;
491
+ }
492
+
493
+ const descriptors = plan.descriptors.filter(
494
+ (descriptor) => descriptor.table === event.table,
495
+ );
496
+ if (descriptors.length === 0) {
497
+ return false;
498
+ }
499
+
500
+ const candidates = collectMutationCandidates(event);
501
+ for (const descriptor of descriptors) {
502
+ if (!descriptor.where) {
503
+ if (candidates.length > 0) {
504
+ return true;
505
+ }
506
+ continue;
507
+ }
508
+
509
+ if (candidates.some((candidate) => doesWhereMatchCandidate(descriptor.where as Record<string, unknown>, candidate))) {
510
+ return true;
511
+ }
512
+ }
513
+
514
+ return false;
515
+ }
516
+ `;