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.
- package/cli/commands/index.ts +140 -0
- package/cli/generate.ts +149 -0
- package/cli/index.ts +56 -447
- package/cli/load-config.ts +182 -0
- package/cli/schema-compiler.ts +657 -0
- package/cli/templates/auth/README.md +156 -0
- package/cli/templates/auth/config.ts +61 -0
- package/cli/templates/auth/route-config.ts +18 -0
- package/cli/templates/auth/route-handler.ts +18 -0
- package/cli/templates/auth/route-request-utils.ts +55 -0
- package/cli/templates/auth/route.ts +14 -0
- package/cli/templates/core/README.md +266 -0
- package/cli/templates/core/app-creation.ts +19 -0
- package/cli/templates/core/client/appflare.ts +37 -0
- package/cli/templates/core/client/index.ts +6 -0
- package/cli/templates/core/client/storage.ts +100 -0
- package/cli/templates/core/client/types.ts +54 -0
- package/cli/templates/core/client-modules/appflare.ts +112 -0
- package/cli/templates/core/client-modules/handlers/index.ts +740 -0
- package/cli/templates/core/client-modules/handlers.ts +1 -0
- package/cli/templates/core/client-modules/index.ts +7 -0
- package/cli/templates/core/client-modules/storage.ts +180 -0
- package/cli/templates/core/client-modules/types.ts +145 -0
- package/cli/templates/core/client.ts +39 -0
- package/cli/templates/core/drizzle.ts +15 -0
- package/cli/templates/core/export.ts +14 -0
- package/cli/templates/core/handlers-route.ts +23 -0
- package/cli/templates/core/handlers.ts +1 -0
- package/cli/templates/core/imports.ts +8 -0
- package/cli/templates/core/server.ts +38 -0
- package/cli/templates/core/types.ts +6 -0
- package/cli/templates/core/wrangler.ts +109 -0
- package/cli/templates/handlers/README.md +265 -0
- package/cli/templates/handlers/auth.ts +36 -0
- package/cli/templates/handlers/execution.ts +39 -0
- package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
- package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
- package/cli/templates/handlers/generators/context/types.ts +18 -0
- package/cli/templates/handlers/generators/context.ts +43 -0
- package/cli/templates/handlers/generators/execution.ts +15 -0
- package/cli/templates/handlers/generators/handlers.ts +13 -0
- package/cli/templates/handlers/index.ts +43 -0
- package/cli/templates/handlers/operations.ts +116 -0
- package/cli/templates/handlers/registration.ts +1114 -0
- package/cli/templates/handlers/types.ts +960 -0
- package/cli/templates/handlers/utils.ts +48 -0
- package/cli/types.ts +108 -0
- package/cli/utils/handler-discovery.ts +366 -0
- package/cli/utils/json-utils.ts +24 -0
- package/cli/utils/path-utils.ts +19 -0
- package/cli/utils/schema-discovery.ts +390 -0
- package/index.ts +27 -4
- package/package.json +23 -20
- package/react/index.ts +5 -3
- package/react/use-infinite-query.ts +190 -0
- package/react/use-mutation.ts +54 -0
- package/react/use-query.ts +158 -0
- package/schema.ts +262 -0
- package/tsconfig.json +2 -4
- package/cli/README.md +0 -108
- package/cli/core/build.ts +0 -187
- package/cli/core/config.ts +0 -92
- package/cli/core/discover-handlers.ts +0 -143
- package/cli/core/handlers.ts +0 -7
- package/cli/core/index.ts +0 -205
- package/cli/generators/generate-api-client/client.ts +0 -163
- package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
- package/cli/generators/generate-api-client/index.ts +0 -973
- package/cli/generators/generate-api-client/types.ts +0 -164
- package/cli/generators/generate-api-client/utils.ts +0 -22
- package/cli/generators/generate-api-client.ts +0 -1
- package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
- package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
- package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
- package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
- package/cli/generators/generate-cloudflare-worker.ts +0 -4
- package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
- package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
- package/cli/generators/generate-cron-handlers/index.ts +0 -61
- package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
- package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
- package/cli/generators/generate-db-handlers/index.ts +0 -33
- package/cli/generators/generate-db-handlers/prepare.ts +0 -24
- package/cli/generators/generate-db-handlers/templates.ts +0 -189
- package/cli/generators/generate-db-handlers.ts +0 -1
- package/cli/generators/generate-hono-server/auth.ts +0 -97
- package/cli/generators/generate-hono-server/imports.ts +0 -55
- package/cli/generators/generate-hono-server/index.ts +0 -52
- package/cli/generators/generate-hono-server/routes.ts +0 -115
- package/cli/generators/generate-hono-server/template.ts +0 -371
- package/cli/generators/generate-hono-server.ts +0 -1
- package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
- package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
- package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
- package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
- package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
- package/cli/generators/generate-scheduler-handlers.ts +0 -1
- package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
- package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
- package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
- package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
- package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
- package/cli/generators/generate-websocket-durable-object.ts +0 -1
- package/cli/schema/schema-static-types.ts +0 -702
- package/cli/schema/schema.ts +0 -151
- package/cli/utils/tsc.ts +0 -54
- package/cli/utils/utils.ts +0 -190
- package/cli/utils/zod-utils.ts +0 -121
- package/lib/README.md +0 -50
- package/lib/db.ts +0 -19
- package/lib/location.ts +0 -110
- package/lib/values.ts +0 -27
- package/react/README.md +0 -67
- package/react/hooks/useMutation.ts +0 -89
- package/react/hooks/usePaginatedQuery.ts +0 -213
- package/react/hooks/useQuery.ts +0 -106
- package/react/shared/queryShared.ts +0 -174
- package/server/README.md +0 -218
- package/server/auth.ts +0 -107
- package/server/database/builders.ts +0 -83
- package/server/database/context.ts +0 -327
- package/server/database/populate.ts +0 -234
- package/server/database/query-builder.ts +0 -161
- package/server/database/query-utils.ts +0 -25
- package/server/db.ts +0 -2
- package/server/storage/auth.ts +0 -16
- package/server/storage/bucket.ts +0 -22
- package/server/storage/context.ts +0 -34
- package/server/storage/index.ts +0 -38
- package/server/storage/operations.ts +0 -149
- package/server/storage/route-handler.ts +0 -60
- package/server/storage/types.ts +0 -55
- package/server/storage/utils.ts +0 -47
- package/server/storage.ts +0 -6
- package/server/types/schema-refs.ts +0 -66
- package/server/types/types.ts +0 -633
- 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";
|