appflare 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Documentation.md +758 -758
- package/cli/commands/index.ts +238 -238
- package/cli/generate.ts +178 -178
- package/cli/index.ts +120 -120
- package/cli/load-config.ts +184 -184
- package/cli/schema-compiler.ts +1183 -1183
- package/cli/templates/auth/README.md +156 -156
- package/cli/templates/auth/config.ts +61 -61
- package/cli/templates/auth/route-config.ts +1 -1
- package/cli/templates/auth/route-handler.ts +1 -1
- package/cli/templates/auth/route-request-utils.ts +5 -5
- package/cli/templates/auth/route.config.ts +18 -18
- package/cli/templates/auth/route.handler.ts +18 -18
- package/cli/templates/auth/route.request-utils.ts +55 -55
- package/cli/templates/auth/route.ts +14 -14
- package/cli/templates/core/README.md +266 -266
- package/cli/templates/core/app-creation.ts +19 -19
- package/cli/templates/core/client/appflare.ts +112 -112
- package/cli/templates/core/client/handlers/index.ts +748 -748
- package/cli/templates/core/client/handlers.ts +1 -1
- package/cli/templates/core/client/index.ts +7 -7
- package/cli/templates/core/client/storage.ts +195 -195
- package/cli/templates/core/client/types.ts +186 -186
- package/cli/templates/core/client-modules/appflare.ts +1 -1
- package/cli/templates/core/client-modules/handlers.ts +1 -1
- package/cli/templates/core/client-modules/index.ts +1 -1
- package/cli/templates/core/client-modules/storage.ts +1 -1
- package/cli/templates/core/client-modules/types.ts +1 -1
- package/cli/templates/core/client.artifacts.ts +39 -39
- package/cli/templates/core/client.ts +4 -4
- package/cli/templates/core/drizzle.ts +15 -15
- package/cli/templates/core/export.ts +14 -14
- package/cli/templates/core/handlers.route.ts +24 -24
- package/cli/templates/core/handlers.ts +1 -1
- package/cli/templates/core/imports.ts +9 -9
- package/cli/templates/core/server.ts +38 -38
- package/cli/templates/core/types.ts +6 -6
- package/cli/templates/core/wrangler.ts +109 -109
- package/cli/templates/dashboard/builders/functions/index.ts +17 -17
- package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
- package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
- package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +171 -171
- package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
- package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +554 -554
- package/cli/templates/dashboard/builders/navigation.ts +122 -122
- package/cli/templates/dashboard/builders/storage/index.ts +13 -13
- package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
- package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
- package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
- package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
- package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
- package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
- package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
- package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
- package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
- package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
- package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
- package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
- package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
- package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
- package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
- package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
- package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
- package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
- package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
- package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
- package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
- package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
- package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
- package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
- package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
- package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
- package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
- package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
- package/cli/templates/dashboard/components/layout.ts +388 -388
- package/cli/templates/dashboard/components/login-page.ts +65 -65
- package/cli/templates/dashboard/index.ts +61 -61
- package/cli/templates/dashboard/types.ts +9 -9
- package/cli/templates/handlers/README.md +353 -353
- package/cli/templates/handlers/auth.ts +37 -37
- package/cli/templates/handlers/execution.ts +42 -42
- package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
- package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
- package/cli/templates/handlers/generators/context/types.ts +40 -40
- package/cli/templates/handlers/generators/context.ts +43 -43
- package/cli/templates/handlers/generators/execution.ts +15 -15
- package/cli/templates/handlers/generators/handlers.ts +13 -13
- package/cli/templates/handlers/generators/registration/modules/cron.ts +26 -26
- package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
- package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
- package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
- package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
- package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
- package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
- package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +516 -516
- package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -56
- package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
- package/cli/templates/handlers/generators/registration/sections.ts +210 -210
- package/cli/templates/handlers/generators/types/context.ts +92 -92
- package/cli/templates/handlers/generators/types/core.ts +106 -106
- package/cli/templates/handlers/generators/types/operations.ts +135 -135
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +281 -259
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1103 -1031
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -246
- package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
- package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +157 -121
- package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +697 -676
- package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
- package/cli/templates/handlers/index.ts +43 -43
- package/cli/templates/handlers/operations.ts +116 -116
- package/cli/templates/handlers/registration.ts +91 -91
- package/cli/templates/handlers/types.ts +15 -15
- package/cli/templates/handlers/utils.ts +48 -48
- package/cli/types.ts +110 -110
- package/cli/utils/handler-discovery.ts +466 -466
- package/cli/utils/json-utils.ts +24 -24
- package/cli/utils/path-utils.ts +19 -19
- package/cli/utils/schema-discovery.ts +399 -399
- package/dist/cli/index.d.mts +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +301 -118
- package/dist/cli/index.mjs +301 -118
- package/index.ts +18 -18
- package/package.json +58 -58
- package/react/index.ts +5 -5
- package/react/use-infinite-query.ts +252 -252
- package/react/use-mutation.ts +89 -89
- package/react/use-query.ts +207 -207
- package/schema.ts +415 -415
- package/test-better-auth-hash.ts +2 -2
- package/tsconfig.json +6 -6
- 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
|
+
`;
|