appflare 0.0.28 → 0.1.0

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/cli/commands/index.ts +140 -0
  2. package/cli/generate.ts +149 -0
  3. package/cli/index.ts +56 -447
  4. package/cli/load-config.ts +182 -0
  5. package/cli/schema-compiler.ts +657 -0
  6. package/cli/templates/auth/README.md +156 -0
  7. package/cli/templates/auth/config.ts +61 -0
  8. package/cli/templates/auth/route-config.ts +18 -0
  9. package/cli/templates/auth/route-handler.ts +18 -0
  10. package/cli/templates/auth/route-request-utils.ts +55 -0
  11. package/cli/templates/auth/route.ts +14 -0
  12. package/cli/templates/core/README.md +266 -0
  13. package/cli/templates/core/app-creation.ts +19 -0
  14. package/cli/templates/core/client/appflare.ts +37 -0
  15. package/cli/templates/core/client/index.ts +6 -0
  16. package/cli/templates/core/client/storage.ts +100 -0
  17. package/cli/templates/core/client/types.ts +54 -0
  18. package/cli/templates/core/client-modules/appflare.ts +112 -0
  19. package/cli/templates/core/client-modules/handlers/index.ts +740 -0
  20. package/cli/templates/core/client-modules/handlers.ts +1 -0
  21. package/cli/templates/core/client-modules/index.ts +7 -0
  22. package/cli/templates/core/client-modules/storage.ts +180 -0
  23. package/cli/templates/core/client-modules/types.ts +145 -0
  24. package/cli/templates/core/client.ts +39 -0
  25. package/cli/templates/core/drizzle.ts +15 -0
  26. package/cli/templates/core/export.ts +14 -0
  27. package/cli/templates/core/handlers-route.ts +23 -0
  28. package/cli/templates/core/handlers.ts +1 -0
  29. package/cli/templates/core/imports.ts +8 -0
  30. package/cli/templates/core/server.ts +38 -0
  31. package/cli/templates/core/types.ts +6 -0
  32. package/cli/templates/core/wrangler.ts +109 -0
  33. package/cli/templates/handlers/README.md +265 -0
  34. package/cli/templates/handlers/auth.ts +36 -0
  35. package/cli/templates/handlers/execution.ts +39 -0
  36. package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
  37. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
  38. package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
  39. package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
  40. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
  41. package/cli/templates/handlers/generators/context/types.ts +18 -0
  42. package/cli/templates/handlers/generators/context.ts +43 -0
  43. package/cli/templates/handlers/generators/execution.ts +15 -0
  44. package/cli/templates/handlers/generators/handlers.ts +13 -0
  45. package/cli/templates/handlers/index.ts +43 -0
  46. package/cli/templates/handlers/operations.ts +116 -0
  47. package/cli/templates/handlers/registration.ts +1114 -0
  48. package/cli/templates/handlers/types.ts +960 -0
  49. package/cli/templates/handlers/utils.ts +48 -0
  50. package/cli/types.ts +108 -0
  51. package/cli/utils/handler-discovery.ts +366 -0
  52. package/cli/utils/json-utils.ts +24 -0
  53. package/cli/utils/path-utils.ts +19 -0
  54. package/cli/utils/schema-discovery.ts +390 -0
  55. package/index.ts +27 -4
  56. package/package.json +23 -20
  57. package/react/index.ts +5 -3
  58. package/react/use-infinite-query.ts +190 -0
  59. package/react/use-mutation.ts +54 -0
  60. package/react/use-query.ts +158 -0
  61. package/schema.ts +262 -0
  62. package/tsconfig.json +2 -4
  63. package/cli/README.md +0 -108
  64. package/cli/core/build.ts +0 -187
  65. package/cli/core/config.ts +0 -92
  66. package/cli/core/discover-handlers.ts +0 -143
  67. package/cli/core/handlers.ts +0 -7
  68. package/cli/core/index.ts +0 -205
  69. package/cli/generators/generate-api-client/client.ts +0 -163
  70. package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
  71. package/cli/generators/generate-api-client/index.ts +0 -973
  72. package/cli/generators/generate-api-client/types.ts +0 -164
  73. package/cli/generators/generate-api-client/utils.ts +0 -22
  74. package/cli/generators/generate-api-client.ts +0 -1
  75. package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
  76. package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
  77. package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
  78. package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
  79. package/cli/generators/generate-cloudflare-worker.ts +0 -4
  80. package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
  81. package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
  82. package/cli/generators/generate-cron-handlers/index.ts +0 -61
  83. package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
  84. package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
  85. package/cli/generators/generate-db-handlers/index.ts +0 -33
  86. package/cli/generators/generate-db-handlers/prepare.ts +0 -24
  87. package/cli/generators/generate-db-handlers/templates.ts +0 -189
  88. package/cli/generators/generate-db-handlers.ts +0 -1
  89. package/cli/generators/generate-hono-server/auth.ts +0 -97
  90. package/cli/generators/generate-hono-server/imports.ts +0 -55
  91. package/cli/generators/generate-hono-server/index.ts +0 -52
  92. package/cli/generators/generate-hono-server/routes.ts +0 -115
  93. package/cli/generators/generate-hono-server/template.ts +0 -371
  94. package/cli/generators/generate-hono-server.ts +0 -1
  95. package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
  96. package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
  97. package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
  98. package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
  99. package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
  100. package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
  101. package/cli/generators/generate-scheduler-handlers.ts +0 -1
  102. package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
  103. package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
  104. package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
  105. package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
  106. package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
  107. package/cli/generators/generate-websocket-durable-object.ts +0 -1
  108. package/cli/schema/schema-static-types.ts +0 -702
  109. package/cli/schema/schema.ts +0 -151
  110. package/cli/utils/tsc.ts +0 -54
  111. package/cli/utils/utils.ts +0 -190
  112. package/cli/utils/zod-utils.ts +0 -121
  113. package/lib/README.md +0 -50
  114. package/lib/db.ts +0 -19
  115. package/lib/location.ts +0 -110
  116. package/lib/values.ts +0 -27
  117. package/react/README.md +0 -67
  118. package/react/hooks/useMutation.ts +0 -89
  119. package/react/hooks/usePaginatedQuery.ts +0 -213
  120. package/react/hooks/useQuery.ts +0 -106
  121. package/react/shared/queryShared.ts +0 -174
  122. package/server/README.md +0 -218
  123. package/server/auth.ts +0 -107
  124. package/server/database/builders.ts +0 -83
  125. package/server/database/context.ts +0 -327
  126. package/server/database/populate.ts +0 -234
  127. package/server/database/query-builder.ts +0 -161
  128. package/server/database/query-utils.ts +0 -25
  129. package/server/db.ts +0 -2
  130. package/server/storage/auth.ts +0 -16
  131. package/server/storage/bucket.ts +0 -22
  132. package/server/storage/context.ts +0 -34
  133. package/server/storage/index.ts +0 -38
  134. package/server/storage/operations.ts +0 -149
  135. package/server/storage/route-handler.ts +0 -60
  136. package/server/storage/types.ts +0 -55
  137. package/server/storage/utils.ts +0 -47
  138. package/server/storage.ts +0 -6
  139. package/server/types/schema-refs.ts +0 -66
  140. package/server/types/types.ts +0 -633
  141. package/server/utils/id-utils.ts +0 -230
@@ -1,714 +0,0 @@
1
- const schemaTypesImportPath = "../src/schema-types";
2
- const serverImportPath = "./server";
3
-
4
- const indentBlock = (block: string, indent: string): string =>
5
- block
6
- .split("\n")
7
- .map((line) => (line ? indent + line : indent))
8
- .join("\n");
9
-
10
- export type WebsocketDoTemplateParams = {
11
- schemaImportPath: string;
12
- configImportPath: string;
13
- handlerImports: string[];
14
- authImportLine: string;
15
- authSetupBlock: string;
16
- queryHandlerEntries: string;
17
- };
18
-
19
- export function renderWebsocketDurableObjectTemplate(
20
- params: WebsocketDoTemplateParams
21
- ): string {
22
- const authImportBlock = params.authImportLine
23
- ? `${params.authImportLine}\n`
24
- : "";
25
- const handlerImportBlock =
26
- params.handlerImports.length > 0 ? params.handlerImports.join("\n") : "";
27
- const importsBlock =
28
- authImportBlock || handlerImportBlock
29
- ? `${authImportBlock}${handlerImportBlock}\n\n`
30
- : "\n";
31
- const authSetupBlock = indentBlock(params.authSetupBlock, "\t");
32
-
33
- return `/* eslint-disable */
34
- /**
35
- * This file is auto-generated by appflare/handler-build.ts.
36
- * Do not edit directly.
37
- */
38
-
39
- import { DurableObject } from "cloudflare:workers";
40
- import {
41
- \tcreateAppflareDbContext,
42
- \ttype AppflareDbContext,
43
- \ttype AppflareServerContext,
44
- } from ${JSON.stringify(serverImportPath)};
45
- import appflareConfig from ${JSON.stringify(params.configImportPath)};
46
- import schema from ${JSON.stringify(params.schemaImportPath)};
47
- import type {
48
- \tQueryWhere,
49
- \tQuerySort,
50
- \tTableNames,
51
- \tAppflareAuthContext,
52
- \tAppflareAuthSession,
53
- \tAppflareAuthUser,
54
- } from ${JSON.stringify(schemaTypesImportPath)};
55
- import { MongoClient } from "mongodb";
56
- ${importsBlock}const emptyAuthContext: AppflareAuthContext = {
57
- \tsession: null as AppflareAuthSession,
58
- \tuser: null as AppflareAuthUser,
59
- };
60
-
61
- const resolveAuthContextFromToken = async (
62
- authToken?: string | null
63
- ): Promise<AppflareAuthContext> => {
64
- ${authSetupBlock}
65
- if (!__appflareAuth || !authToken) return emptyAuthContext;
66
- try {
67
- const request = new Request("http://appflare-internal/auth", {
68
- headers: { Authorization: \`Bearer \${authToken}\` },
69
- });
70
- const sessionResult = await __appflareAuth.api.getSession(request);
71
- return {
72
- session:
73
- (sessionResult as any)?.session ??
74
- (sessionResult as any) ??
75
- (null as AppflareAuthSession),
76
- user: (sessionResult as any)?.user ?? (null as AppflareAuthUser),
77
- };
78
- } catch (err) {
79
- console.error("Appflare websocket auth failed", err);
80
- return emptyAuthContext;
81
- }
82
- };
83
-
84
- type SubscriptionQueryHandler = { file: string; name: string };
85
-
86
- type Subscription = {
87
- \ttable: TableNames;
88
- \thandler?: SubscriptionQueryHandler;
89
- \targs?: unknown;
90
- \twhere?: QueryWhere<TableNames>;
91
- \torderBy?: QuerySort<TableNames>;
92
- \ttake?: number;
93
- \tskip?: number;
94
- \tselect?: unknown;
95
- \tinclude?: unknown;
96
- \tauthToken?: string | null;
97
- \tauth?: AppflareAuthContext;
98
- };
99
-
100
- type SubscriptionWithAuth = Subscription & { auth?: AppflareAuthContext };
101
-
102
- type ParsedSubscription =
103
- \t| { ok: true; value: Subscription }
104
- \t| { ok: false; error: string };
105
-
106
- type QueryArgsParser = { parse?: (value: unknown) => unknown };
107
-
108
- type QueryHandlerDefinition = {
109
- \targs?: QueryArgsParser | unknown;
110
- middleware?: (
111
- ctx: import("./server").AppflareServerContext,
112
- args: unknown
113
- ) => unknown | Promise<unknown>;
114
- \thandler: (
115
- \t\tctx: import("./server").AppflareServerContext,
116
- \t\targs: unknown
117
- \t) => unknown | Promise<unknown>;
118
- };
119
-
120
- const queryHandlers = {
121
- ${params.queryHandlerEntries}
122
- } satisfies Record<
123
- \tstring,
124
- \t{ file: string; name: string; definition: QueryHandlerDefinition }
125
- >;
126
-
127
- type QueryHandlerKey = keyof typeof queryHandlers;
128
-
129
- const pascalCase = (str: string): string =>
130
- \tstr.replace(/(^\w|_\w)/g, (match) => match.replace("_", "").toUpperCase());
131
- const defaultHandlerForTable = (
132
- \ttable: TableNames
133
- ): SubscriptionQueryHandler | null => {
134
- \tconst tableStr = table.toString();
135
- \tconst possible = [tableStr];
136
- \tif (tableStr.endsWith("s")) {
137
- \t\tpossible.push(tableStr.slice(0, -1));
138
- \t}
139
- \tfor (const candidate of possible) {
140
- const handlerName = "get" + pascalCase(candidate);
141
- const suffix = "/" + handlerName;
142
- const matchKey = Object.keys(queryHandlers).find((key) => {
143
- if (!key.endsWith(suffix)) return false;
144
- const segments = key.split("/");
145
- return segments.length >= 2 && segments[segments.length - 2] === candidate;
146
- });
147
- if (matchKey) {
148
- const file = matchKey.slice(0, matchKey.length - suffix.length);
149
- return { file, name: handlerName };
150
- }
151
- \t}
152
- \treturn null;
153
- };
154
-
155
- const resolveDatabase = (env: any) => {
156
- \tconst client = new MongoClient(env.MONGO_URI);
157
- \tconst db = client.db(env.MONGO_DB);
158
- \treturn db;
159
- };
160
-
161
- type AppflareHandlerError = Error & {
162
- status: number;
163
- details?: unknown;
164
- __appflareHandlerError: true;
165
- };
166
-
167
- type AppflareErrorFactory = (
168
- status: number,
169
- message?: string,
170
- details?: unknown
171
- ) => AppflareHandlerError;
172
-
173
- const createHandlerError: AppflareErrorFactory = (
174
- status,
175
- message,
176
- details
177
- ) => {
178
- const err = new Error(message ?? \`HTTP \${status}\`);
179
- err.name = "AppflareHandlerError";
180
- const typed = err as AppflareHandlerError;
181
- typed.status = status;
182
- typed.details = details;
183
- typed.__appflareHandlerError = true;
184
- return typed;
185
- };
186
-
187
- const isHandlerError = (value: unknown): value is AppflareHandlerError =>
188
- !!value &&
189
- typeof value === "object" &&
190
- (value as any).__appflareHandlerError === true &&
191
- Number.isFinite((value as any).status);
192
-
193
- const handlerErrorBody = (
194
- err: AppflareHandlerError
195
- ): { error: string; details?: unknown } => {
196
- const includeDetails =
197
- err.details !== undefined &&
198
- (err.details && typeof err.details === "object"
199
- ? !(err.details instanceof Error) && !Array.isArray(err.details)
200
- : true);
201
- return includeDetails
202
- ? { error: err.message, details: err.details }
203
- : { error: err.message };
204
- };
205
-
206
- async function runHandlerWithMiddleware<TArgs, TResult>(
207
- handler: {
208
- handler: (ctx: AppflareServerContext, args: TArgs) => Promise<TResult> | TResult;
209
- middleware?: (
210
- ctx: AppflareServerContext,
211
- args: TArgs
212
- ) => Promise<TResult | void> | TResult | void;
213
- },
214
- ctx: AppflareServerContext,
215
- args: TArgs
216
- ): Promise<TResult> {
217
- if (handler.middleware) {
218
- const middlewareResult = await handler.middleware(ctx, args);
219
- if (typeof middlewareResult !== "undefined") {
220
- return middlewareResult as TResult;
221
- }
222
- }
223
- return handler.handler(ctx, args) as Promise<TResult>;
224
- }
225
-
226
- const formatHandlerError = (
227
- \terr: unknown
228
- ): { error: string; details?: unknown } => {
229
- if (isHandlerError(err)) {
230
- return handlerErrorBody(err);
231
- }
232
- \tconst message =
233
- \t\terr instanceof Error
234
- \t\t\t? err.message
235
- \t\t\t: typeof err === "string"
236
- \t\t\t\t? err
237
- \t\t\t\t: "Unknown error";
238
- \tconst includeDetails =
239
- \t\terr && typeof err === "object" && !(err instanceof Error) && !Array.isArray(err);
240
- \treturn includeDetails ? { error: message, details: err } : { error: message };
241
- };
242
-
243
- type ErrorEnvelope = { __appflareError: true; payload: { error: string; details?: unknown } };
244
-
245
- const makeErrorEnvelope = (err: unknown): ErrorEnvelope => ({
246
- \t__appflareError: true,
247
- \tpayload: formatHandlerError(err),
248
- });
249
-
250
- const isErrorEnvelope = (value: unknown): value is ErrorEnvelope => {
251
- \treturn !!value && typeof value === "object" && (value as any).__appflareError === true;
252
- };
253
-
254
- const handlerKey = (handler: SubscriptionQueryHandler): string =>
255
- \thandler.file + "/" + handler.name;
256
-
257
- const resolveQueryHandler = (
258
- \thandler: SubscriptionQueryHandler | undefined
259
- ): QueryHandlerDefinition | null => {
260
- \tif (!handler) return null;
261
- \tconst key = handlerKey(handler) as QueryHandlerKey;
262
- \treturn queryHandlers[key]?.definition ?? null;
263
- };
264
-
265
- const isKnownQueryHandler = (
266
- \thandler: SubscriptionQueryHandler | undefined
267
- ): boolean => {
268
- \tif (!handler) return false;
269
- \treturn handlerKey(handler) in queryHandlers;
270
- };
271
-
272
- export type MutationNotification = {
273
- \ttable: TableNames;
274
- \thandler: { file: string; name: string };
275
- \targs: unknown;
276
- \tresult?: unknown;
277
- };
278
-
279
- export class WebSocketHibernationServer extends DurableObject {
280
- \tprivate subscriptions: Map<WebSocket, SubscriptionWithAuth>;
281
- \tprivate db: AppflareDbContext | null;
282
-
283
- \tconstructor(ctx: DurableObjectState, env: any) {
284
- \t\tsuper(ctx, env);
285
- \t\tthis.env = env;
286
- \t\tthis.subscriptions = new Map();
287
- \t\tthis.db = null;
288
-
289
- \t\tfor (const socket of this.ctx.getWebSockets()) {
290
- \t\t\tconst restored = socket.deserializeAttachment() as
291
- \t\t\t\t| SubscriptionWithAuth
292
- \t\t\t\t| undefined;
293
- \t\t\tif (restored) {
294
- \t\t\t\tthis.subscriptions.set(socket, restored);
295
- \t\t\t}
296
- \t\t}
297
-
298
- \t\tthis.ctx.setWebSocketAutoResponse(
299
- \t\t\tnew WebSocketRequestResponsePair("ping", "pong")
300
- \t\t);
301
- \t}
302
-
303
- \tprivate async withAuth(
304
- \t\tsub: Subscription
305
- \t): Promise<SubscriptionWithAuth> {
306
- \t\tif (!sub.authToken) return sub as SubscriptionWithAuth;
307
- \t\tif (sub.auth && sub.auth.session !== undefined) {
308
- \t\t\treturn sub as SubscriptionWithAuth;
309
- \t\t}
310
- \t\tconst auth = await resolveAuthContextFromToken(sub.authToken);
311
- \t\treturn { ...sub, auth } as SubscriptionWithAuth;
312
- \t}
313
-
314
- \tasync fetch(request: Request): Promise<Response> {
315
- \t\tconst url = new URL(request.url);
316
- \t\tconst pathname = url.pathname;
317
-
318
- \t\tif (request.headers.get("Upgrade") === "websocket" && pathname === "/ws") {
319
- \t\t\tconst parsed = this.parseSubscription(url.searchParams);
320
- \t\t\tif (parsed.ok === false) {
321
- \t\t\t\treturn new Response(parsed.error, { status: 400 });
322
- \t\t\t}
323
- \t\t\treturn this.handleSubscribe(parsed.value);
324
- \t\t}
325
-
326
- \t\tif (pathname === "/notify" && request.method === "POST") {
327
- \t\t\tconst payload = (await request
328
- \t\t\t\t.json()
329
- \t\t\t\t.catch(() => null)) as MutationNotification | null;
330
- \t\t\tif (!payload) {
331
- \t\t\t\treturn new Response("Invalid mutation payload", { status: 400 });
332
- \t\t\t}
333
- \t\t\tawait this.handleNotification(payload);
334
- \t\t\treturn new Response("ok", { status: 200 });
335
- \t\t}
336
-
337
- \t\treturn new Response("Not found", { status: 404 });
338
- \t}
339
-
340
- \tasync webSocketMessage(
341
- \t\tws: WebSocket,
342
- \t\tmessage: ArrayBuffer | string
343
- \t): Promise<void> {
344
- \t\tif (typeof message === "string" && message.toLowerCase() === "ping") {
345
- \t\t\tws.send("pong");
346
- \t\t\treturn;
347
- \t\t}
348
- \t}
349
-
350
- \tasync webSocketClose(ws: WebSocket): Promise<void> {
351
- \t\tthis.subscriptions.delete(ws);
352
- \t}
353
-
354
- \tprivate async handleSubscribe(sub: Subscription): Promise<Response> {
355
- \t\ttry {
356
- \t\t\tconst subWithAuth = await this.withAuth(sub);
357
- \t\t\tconst { 0: client, 1: server } = Object.values(new WebSocketPair());
358
- \t\t\tserver.serializeAttachment(subWithAuth);
359
- \t\t\tthis.ctx.acceptWebSocket(server);
360
- \t\t\tthis.subscriptions.set(server, subWithAuth);
361
- \t\t\tserver.send(
362
- \t\t\t\tJSON.stringify({ type: "subscribed", subscription: subWithAuth })
363
- \t\t\t);
364
-
365
- \t\t\ttry {
366
- \t\t\t\tconst data = await this.fetchData(subWithAuth);
367
- \t\t\t\tserver.send(
368
- \t\t\t\t\tJSON.stringify({
369
- \t\t\t\t\t\ttype: "data",
370
- \t\t\t\t\t\ttable: subWithAuth.table,
371
- \t\t\t\t\t\twhere: subWithAuth.where,
372
- \t\t\t\t\t\tdata,
373
- \t\t\t\t\t})
374
- \t\t\t\t);
375
- \t\t\t} catch (err) {
376
- \t\t\t\tconst formatted = makeErrorEnvelope(err).payload;
377
- \t\t\t\tconsole.error("Failed to send initial payload", err);
378
- \t\t\t\tserver.send(
379
- \t\t\t\t\tJSON.stringify({
380
- \t\t\t\t\t\ttype: "error",
381
- \t\t\t\t\t\t...formatted,
382
- \t\t\t\t\t})
383
- \t\t\t\t);
384
- \t\t\t}
385
-
386
- \t\t\treturn new Response(null, { status: 101, webSocket: client });
387
- \t\t} catch (err) {
388
- \t\t\tconst formatted = formatHandlerError(err);
389
- \t\t\tconsole.error("Websocket subscription setup failed", err);
390
- \t\t\treturn new Response(JSON.stringify(formatted), {
391
- \t\t\t\tstatus: 500,
392
- \t\t\t\theaders: { "content-type": "application/json" },
393
- \t\t\t});
394
- \t\t}
395
- \t}
396
-
397
- \tprivate parseSubscription(params: URLSearchParams): ParsedSubscription {
398
- \t\tconst table = this.normalizeTableName(params.get("table"));
399
- \t\tif (!table) {
400
- \t\t\treturn { ok: false, error: "Missing or invalid table param" };
401
- \t\t}
402
- \t\tconst authToken = params.get("authToken");
403
-
404
- \t\tconst parseJson = <T>(key: string): T | undefined => {
405
- \t\t\tconst raw = params.get(key);
406
- \t\t\tif (!raw) return undefined;
407
- \t\t\ttry {
408
- \t\t\t\treturn JSON.parse(raw) as T;
409
- \t\t\t} catch (err) {
410
- \t\t\t\tconsole.error("Failed to parse " + key + " search param", err);
411
- \t\t\t\treturn undefined;
412
- \t\t\t}
413
- \t\t};
414
-
415
- \t\tconst handler = this.parseHandlerRef(params);
416
- \t\tlet resolvedHandler = handler ?? defaultHandlerForTable(table) ?? undefined;
417
-
418
- \t\tif (resolvedHandler && !isKnownQueryHandler(resolvedHandler)) {
419
- \t\t\treturn {
420
- \t\t\t\tok: false,
421
- \t\t\t\terror:
422
- \t\t\t\t\t"Unknown query handler: " +
423
- \t\t\t\t\tresolvedHandler.file +
424
- \t\t\t\t\t"/" +
425
- \t\t\t\t\tresolvedHandler.name,
426
- \t\t\t};
427
- \t\t}
428
-
429
- \t\tconst where = parseJson<QueryWhere<TableNames>>("where");
430
- \t\tconst orderBy = parseJson<QuerySort<TableNames>>("orderBy");
431
- \t\tconst select = parseJson<unknown>("select");
432
- \t\tconst include = parseJson<unknown>("include");
433
- \t\tconst args = parseJson<unknown>("args");
434
- \t\tconst takeStr = params.get("take") ?? params.get("limit");
435
- \t\tconst skipStr = params.get("skip") ?? params.get("offset");
436
-
437
- \t\treturn {
438
- \t\t\tok: true,
439
- \t\t\tvalue: {
440
- \t\t\t\ttable,
441
- \t\t\t\thandler: resolvedHandler,
442
- \t\t\t\targs,
443
- \t\t\t\twhere,
444
- \t\t\t\torderBy,
445
- \t\t\t\tselect,
446
- \t\t\t\tinclude,
447
- \t\t\t\ttake: takeStr ? Number(takeStr) : undefined,
448
- \t\t\t\tskip: skipStr ? Number(skipStr) : undefined,
449
- \t\t\t\tauthToken,
450
- \t\t\t},
451
- \t\t};
452
- \t}
453
-
454
- \tprivate parseHandlerRef(
455
- \t\tparams: URLSearchParams
456
- \t): SubscriptionQueryHandler | undefined {
457
- \t\tconst handlerCombined = params.get("handler");
458
- \t\tconst handlerFile =
459
- \t\t\tparams.get("handlerFile") ?? params.get("handler_file") ?? undefined;
460
- \t\tconst handlerName =
461
- \t\t\tparams.get("handlerName") ?? params.get("handler_name") ?? undefined;
462
-
463
- \t\tconst fromCombined = this.tryParseHandlerCombined(handlerCombined);
464
- \t\tif (fromCombined) return fromCombined;
465
- \t\tif (handlerFile && handlerName) {
466
- \t\t\treturn { file: handlerFile, name: handlerName };
467
- \t\t}
468
- \t\treturn undefined;
469
- \t}
470
-
471
- \tprivate tryParseHandlerCombined(
472
- \t\tcombined: string | null
473
- \t): SubscriptionQueryHandler | undefined {
474
- \t\tif (!combined) return undefined;
475
-
476
- \t\ttry {
477
- \t\t\tconst parsed = JSON.parse(combined) as SubscriptionQueryHandler;
478
- \t\t\tif (parsed && parsed.file && parsed.name) return parsed;
479
- \t\t} catch (err) {
480
- \t\t\t// ignore JSON parse failures and try other formats
481
- \t\t}
482
-
483
- \t\tif (combined.includes("/")) {
484
- \t\t\tconst [file, name] = combined.split("/");
485
- \t\t\tif (file && name) return { file, name };
486
- \t\t}
487
-
488
- \t\treturn undefined;
489
- \t}
490
-
491
- \tprivate async handleNotification(
492
- \t\tpayload: MutationNotification
493
- \t): Promise<void> {
494
- \t\tconst matches = Array.from(this.subscriptions.entries()).filter(([, sub]) =>
495
- \t\t\tthis.shouldNotify(sub, payload)
496
- \t\t);
497
- \t\tif (matches.length === 0) return;
498
-
499
- \t\tconst cache = new Map<string, unknown>();
500
- \t\tfor (const [, sub] of matches) {
501
- \t\t\tconst key = this.subscriptionKey(sub);
502
- \t\t\tif (!cache.has(key)) {
503
- \t\t\t\ttry {
504
- \t\t\t\t\tcache.set(key, await this.fetchData(sub));
505
- \t\t\t\t} catch (err) {
506
- \t\t\t\t\tcache.set(key, makeErrorEnvelope(err));
507
- \t\t\t\t\tconsole.error("Failed to fetch subscription data for notification", err);
508
- \t\t\t\t}
509
- \t\t\t}
510
- \t\t}
511
-
512
- \t\tfor (const [socket, sub] of matches) {
513
- \t\t\tconst key = this.subscriptionKey(sub);
514
- \t\t\tconst data = cache.get(key);
515
- \t\t\ttry {
516
- \t\t\t\tif (isErrorEnvelope(data)) {
517
- \t\t\t\t\tsocket.send(
518
- \t\t\t\t\t\tJSON.stringify({
519
- \t\t\t\t\t\t\ttype: "error",
520
- \t\t\t\t\t\t\t...data.payload,
521
- \t\t\t\t\t\t})
522
- );
523
- \t\t\t\t\tcontinue;
524
- \t\t\t\t}
525
- \t\t\t\tsocket.send(
526
- \t\t\t\t\tJSON.stringify({
527
- \t\t\t\t\t\ttype: "data",
528
- \t\t\t\t\t\ttable: sub.table,
529
- \t\t\t\t\t\twhere: sub.where,
530
- \t\t\t\t\t\tdata,
531
- \t\t\t\t\t})
532
- \t\t\t\t);
533
- \t\t\t} catch (err) {
534
- \t\t\t\tsocket.close(1011, "send failed");
535
- \t\t\t\tthis.subscriptions.delete(socket);
536
- \t\t\t\tconsole.error("Failed to notify websocket, closing", err);
537
- \t\t\t}
538
- \t\t}
539
- \t}
540
-
541
- \tprivate shouldNotify(
542
- \t\tsub: Subscription,
543
- \t\tpayload: MutationNotification
544
- \t): boolean {
545
- \t\tif (payload.table !== sub.table) return false;
546
-
547
- \t\tconst doc = this.extractDocument(payload);
548
- \t\tif (doc && this.matchesWhere(sub.where, doc)) return true;
549
-
550
- \t\tconst mutationWhere = this.extractWhere(payload.args);
551
- \t\tif (!sub.where) return true;
552
- \t\tif (this.whereIntersects(sub.where, mutationWhere)) return true;
553
- \t\treturn false;
554
- \t}
555
-
556
- \tprivate extractDocument(payload: MutationNotification):
557
- \t\t| Record<string, unknown>
558
- \t\t| null {
559
- \t\tconst result = payload.result as unknown;
560
- \t\tif (result && typeof result === "object" && !Array.isArray(result)) {
561
- \t\t\treturn result as Record<string, unknown>;
562
- \t\t}
563
- \t\tconst args = payload.args as any;
564
- \t\tif (
565
- \t\t\targs &&
566
- \t\t\ttypeof args === "object" &&
567
- \t\t\targs.data &&
568
- \t\t\ttypeof args.data === "object"
569
- \t\t) {
570
- \t\t\treturn args.data as Record<string, unknown>;
571
- \t\t}
572
- \t\treturn null;
573
- \t}
574
-
575
- \tprivate extractWhere(args: unknown): Record<string, unknown> | undefined {
576
- \t\tif (!args || typeof args !== "object") return undefined;
577
- \t\tconst obj = args as Record<string, unknown>;
578
- \t\tif (obj.where && typeof obj.where === "object") {
579
- \t\t\treturn obj.where as Record<string, unknown>;
580
- \t\t}
581
- \t\tif (obj.id) {
582
- \t\t\treturn { _id: obj.id } as Record<string, unknown>;
583
- \t\t}
584
- \t\treturn undefined;
585
- \t}
586
-
587
- \tprivate whereIntersects(
588
- \t\tsubWhere?: Record<string, unknown>,
589
- \t\tmutationWhere?: Record<string, unknown>
590
- \t): boolean {
591
- \t\tif (!subWhere) return true;
592
- \t\tif (!mutationWhere) return false;
593
- \t\tfor (const [key, value] of Object.entries(subWhere)) {
594
- \t\t\tif (value === undefined) continue;
595
- \t\t\tif (key in mutationWhere && mutationWhere[key] !== value) {
596
- \t\t\t\treturn false;
597
- \t\t\t}
598
- \t\t}
599
- \t\treturn true;
600
- \t}
601
-
602
- \tprivate matchesWhere(
603
- \t\twhere: Record<string, unknown> | undefined,
604
- \t\tdoc: Record<string, unknown>
605
- \t): boolean {
606
- \t\tif (!where) return true;
607
- \t\tfor (const [key, expected] of Object.entries(where)) {
608
- \t\t\tif (expected === undefined) continue;
609
- \t\t\tconst value = doc[key];
610
- \t\t\tif (Array.isArray(expected)) {
611
- \t\t\t\tif (!Array.isArray(value)) return false;
612
- \t\t\t\tfor (const entry of expected) {
613
- \t\t\t\t\tif (!value.includes(entry)) return false;
614
- \t\t\t\t}
615
- \t\t\t\tcontinue;
616
- \t\t\t}
617
- \t\t\tif (expected !== value) return false;
618
- \t\t}
619
- \t\treturn true;
620
- \t}
621
-
622
- private async fetchData(sub: Subscription): Promise<unknown> {
623
- const subWithAuth = await this.withAuth(sub);
624
- const query = resolveQueryHandler(subWithAuth.handler);
625
- if (query) {
626
- const ctx = this.createHandlerContext(subWithAuth.auth);
627
- const parsedArgs = this.parseHandlerArgs(query, subWithAuth.where);
628
- const result = await runHandlerWithMiddleware(
629
- query,
630
- ctx,
631
- parsedArgs as any
632
- );
633
- if (isHandlerError(result)) {
634
- throw result;
635
- }
636
- return result;
637
- }
638
-
639
- const db = this.getDb();
640
- const table = db[subWithAuth.table] as any;
641
- if (!table || typeof table.findMany !== "function") {
642
- throw new Error("Unknown table: " + subWithAuth.table);
643
- }
644
- const data = await table.findMany({
645
- where: subWithAuth.where as any,
646
- orderBy: subWithAuth.orderBy as any,
647
- skip: subWithAuth.skip,
648
- take: subWithAuth.take,
649
- select: subWithAuth.select as any,
650
- include: subWithAuth.include as any,
651
- });
652
- return data ?? [];
653
- }
654
-
655
- \tprivate parseHandlerArgs(
656
- \t\tquery: QueryHandlerDefinition,
657
- \t\targs: unknown
658
- \t): unknown {
659
- \t\tif (query.args && typeof query.args === "object") {
660
- \t\t\tconst parser = query.args as QueryArgsParser;
661
- \t\t\tif (parser.parse && typeof parser.parse === "function") {
662
- \t\t\t\ttry {
663
- \t\t\t\t\treturn parser.parse(args ?? {});
664
- \t\t\t\t} catch (err) {
665
- \t\t\t\t\tconsole.error("Failed to parse handler args", err);
666
- \t\t\t\t}
667
- \t\t\t}
668
- \t\t}
669
- \t\treturn args ?? {};
670
- \t}
671
-
672
- \tprivate createHandlerContext(auth?: AppflareAuthContext): AppflareServerContext {
673
- \t\treturn {
674
- \t\t\tdb: this.getDb(),
675
- \t\t\tsession: auth?.session ?? null,
676
- user: auth?.user ?? null,
677
- error: createHandlerError,
678
- \t\t} as AppflareServerContext;
679
- \t}
680
-
681
- \tprivate getDb(): AppflareDbContext {
682
- \t\tif (!this.db) {
683
- \t\t\tthis.db = createAppflareDbContext({
684
- \t\t\t\tdb: resolveDatabase(this.env),
685
- \t\t\t});
686
- \t\t}
687
- \t\treturn this.db;
688
- \t}
689
-
690
- \tprivate subscriptionKey(sub: Subscription): string {
691
- \t\treturn JSON.stringify({
692
- \t\t\ttable: sub.table,
693
- \t\t\thandler: sub.handler ?? null,
694
- \t\t\targs: sub.args ?? null,
695
- \t\t\twhere: sub.where ?? null,
696
- \t\t\torderBy: sub.orderBy ?? null,
697
- \t\t\tselect: sub.select ?? null,
698
- \t\t\tinclude: sub.include ?? null,
699
- \t\t\ttake: sub.take ?? null,
700
- \t\t\tskip: sub.skip ?? null,
701
- \t\t});
702
- \t}
703
-
704
- \tprivate normalizeTableName(table: string | null): TableNames | null {
705
- \t\tif (!table) return null;
706
- \t\tconst schemaTables = schema as Record<string, unknown>;
707
- \t\tif (schemaTables[table]) return table as TableNames;
708
- \t\tconst plural = table + "s";
709
- \t\tif (schemaTables[plural]) return plural as TableNames;
710
- \t\treturn null;
711
- \t}
712
- }
713
- `;
714
- }
@@ -1 +0,0 @@
1
- export { generateWebsocketDurableObject } from "./generate-websocket-durable-object/index";