appflare 0.0.13 → 0.0.15
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/core/build.ts +15 -2
- package/cli/core/discover-handlers.ts +11 -1
- package/cli/core/index.ts +19 -1
- package/cli/generators/generate-api-client/client.ts +145 -75
- package/cli/generators/generate-api-client/index.ts +26 -5
- package/cli/generators/generate-api-client/types.ts +128 -43
- package/cli/generators/generate-api-client/utils.ts +11 -7
- package/cli/generators/generate-cron-handlers/handler-entries.ts +2 -2
- package/cli/generators/generate-hono-server/imports.ts +4 -2
- package/cli/generators/generate-hono-server/routes.ts +21 -5
- package/cli/generators/generate-hono-server/template.ts +89 -5
- package/cli/generators/generate-scheduler-handlers/handler-entries.ts +2 -2
- package/cli/generators/generate-websocket-durable-object/imports.ts +1 -1
- package/cli/generators/generate-websocket-durable-object/query-handlers.ts +3 -3
- package/cli/generators/generate-websocket-durable-object/template.ts +117 -29
- package/cli/index.ts +12 -0
- package/cli/schema/schema-static-types.ts +84 -3
- package/cli/utils/tsc.ts +3 -2
- package/cli/utils/utils.ts +2 -0
- package/cli/utils/zod-utils.ts +2 -2
- package/lib/db.ts +12 -2
- package/lib/values.ts +6 -5
- package/package.json +1 -1
- package/server/database/context.ts +7 -5
- package/server/database/query-builder.ts +8 -2
- package/server/storage/auth.ts +4 -2
- package/server/types/types.ts +51 -1
- package/server/utils/id-utils.ts +100 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import {
|
|
2
3
|
DiscoveredHandler,
|
|
3
4
|
groupBy,
|
|
@@ -18,9 +19,10 @@ export function buildImportSection(params: {
|
|
|
18
19
|
schemaPathAbs: string;
|
|
19
20
|
configPathAbs: string;
|
|
20
21
|
}): ImportSection {
|
|
22
|
+
const generatedSchemaAbs = path.join(params.outDirAbs, "src", "schema.ts");
|
|
21
23
|
const schemaImportPath = toImportPathFromGeneratedServer(
|
|
22
24
|
params.outDirAbs,
|
|
23
|
-
|
|
25
|
+
generatedSchemaAbs
|
|
24
26
|
);
|
|
25
27
|
const configImportPath = toImportPathFromGeneratedServer(
|
|
26
28
|
params.outDirAbs,
|
|
@@ -28,7 +30,7 @@ export function buildImportSection(params: {
|
|
|
28
30
|
);
|
|
29
31
|
const configImportLine = `import appflareConfig from ${JSON.stringify(configImportPath)};`;
|
|
30
32
|
const localNameFor = (handler: DiscoveredHandler): string =>
|
|
31
|
-
`__appflare_${pascalCase(handler.
|
|
33
|
+
`__appflare_${pascalCase(handler.routePath)}_${handler.name}`;
|
|
32
34
|
const grouped = groupBy(params.handlers, (handler) => handler.sourceFileAbs);
|
|
33
35
|
const handlerImports: string[] = [];
|
|
34
36
|
for (const [fileAbs, list] of Array.from(grouped.entries())) {
|
|
@@ -10,13 +10,21 @@ export function buildRouteLines(params: {
|
|
|
10
10
|
const local = params.localNameFor(query);
|
|
11
11
|
routeLines.push(
|
|
12
12
|
`app.get(\n` +
|
|
13
|
-
|
|
13
|
+
` ${JSON.stringify(`/queries/${query.routePath}/${query.name}`)},\n` +
|
|
14
14
|
`\tsValidator("query", z.object(${local}.args as any)),\n` +
|
|
15
15
|
`\tasync (c) => {\n` +
|
|
16
16
|
`\t\ttry {\n` +
|
|
17
17
|
`\t\t\tconst query = c.req.valid("query");\n` +
|
|
18
18
|
`\t\t\tconst ctx = await resolveContext(c);\n` +
|
|
19
|
-
`\t\t\tconst result = await
|
|
19
|
+
`\t\t\tconst result = await runHandlerWithMiddleware(\n` +
|
|
20
|
+
`\t\t\t\t${local} as any,\n` +
|
|
21
|
+
`\t\t\t\tctx as any,\n` +
|
|
22
|
+
`\t\t\t\tquery as any\n` +
|
|
23
|
+
`\t\t\t);\n` +
|
|
24
|
+
`\t\t\tif (isHandlerError(result)) {\n` +
|
|
25
|
+
`\t\t\t\tconst { status, body } = formatHandlerError(result);\n` +
|
|
26
|
+
`\t\t\t\treturn c.json(body as any, status);\n` +
|
|
27
|
+
`\t\t\t}\n` +
|
|
20
28
|
`\t\t\treturn c.json(result, 200);\n` +
|
|
21
29
|
`\t\t} catch (err) {\n` +
|
|
22
30
|
`\t\t\tconst { status, body } = formatHandlerError(err);\n` +
|
|
@@ -31,18 +39,26 @@ export function buildRouteLines(params: {
|
|
|
31
39
|
const local = params.localNameFor(mutation);
|
|
32
40
|
routeLines.push(
|
|
33
41
|
`app.post(\n` +
|
|
34
|
-
`\t${JSON.stringify(`/mutations/${mutation.
|
|
42
|
+
`\t${JSON.stringify(`/mutations/${mutation.routePath}/${mutation.name}`)},\n` +
|
|
35
43
|
`\tsValidator("json", z.object(${local}.args as any)),\n` +
|
|
36
44
|
`\tasync (c) => {\n` +
|
|
37
45
|
`\t\ttry {\n` +
|
|
38
46
|
`\t\t\tconst body = c.req.valid("json");\n` +
|
|
39
47
|
`\t\t\tconst ctx = await resolveContext(c);\n` +
|
|
40
|
-
`\t\t\tconst result = await
|
|
48
|
+
`\t\t\tconst result = await runHandlerWithMiddleware(\n` +
|
|
49
|
+
`\t\t\t\t${local} as any,\n` +
|
|
50
|
+
`\t\t\t\tctx as any,\n` +
|
|
51
|
+
`\t\t\t\tbody as any\n` +
|
|
52
|
+
`\t\t\t);\n` +
|
|
53
|
+
`\t\t\tif (isHandlerError(result)) {\n` +
|
|
54
|
+
`\t\t\t\tconst { status, body } = formatHandlerError(result);\n` +
|
|
55
|
+
`\t\t\t\treturn c.json(body as any, status);\n` +
|
|
56
|
+
`\t\t\t}\n` +
|
|
41
57
|
`\t\t\tif (notifyMutation) {\n` +
|
|
42
58
|
`\t\t\t\ttry {\n` +
|
|
43
59
|
`\t\t\t\t\tawait notifyMutation({\n` +
|
|
44
60
|
`\t\t\t\t\t table: normalizeTableName(${JSON.stringify(mutation.fileName)}),\n` +
|
|
45
|
-
`\t\t\t\t\t handler: { file: ${JSON.stringify(mutation.
|
|
61
|
+
`\t\t\t\t\t handler: { file: ${JSON.stringify(mutation.routePath)}, name: ${JSON.stringify(mutation.name)} },\n` +
|
|
46
62
|
`\t\t\t\t\t args: body,\n` +
|
|
47
63
|
`\t\t\t\t\t result,\n` +
|
|
48
64
|
`\t\t\t\t});\n` +
|
|
@@ -50,8 +50,74 @@ export type AppflareDbContext = MongoDbContext<TableNames, TableDocMap>;
|
|
|
50
50
|
export type AppflareServerContext = AppflareAuthContext & {
|
|
51
51
|
db: AppflareDbContext;
|
|
52
52
|
scheduler?: Scheduler;
|
|
53
|
+
error: AppflareErrorFactory;
|
|
53
54
|
};
|
|
54
55
|
|
|
56
|
+
export type AppflareHandlerError = Error & {
|
|
57
|
+
status: number;
|
|
58
|
+
details?: unknown;
|
|
59
|
+
__appflareHandlerError: true;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type AppflareErrorFactory = (
|
|
63
|
+
status: number,
|
|
64
|
+
message?: string,
|
|
65
|
+
details?: unknown
|
|
66
|
+
) => AppflareHandlerError;
|
|
67
|
+
|
|
68
|
+
const createHandlerError: AppflareErrorFactory = (
|
|
69
|
+
status,
|
|
70
|
+
message,
|
|
71
|
+
details
|
|
72
|
+
) => {
|
|
73
|
+
const err = new Error(message ?? \`HTTP \${status}\`);
|
|
74
|
+
err.name = "AppflareHandlerError";
|
|
75
|
+
const typed = err as AppflareHandlerError;
|
|
76
|
+
typed.status = status;
|
|
77
|
+
typed.details = details;
|
|
78
|
+
typed.__appflareHandlerError = true;
|
|
79
|
+
return typed;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const isHandlerError = (value: unknown): value is AppflareHandlerError =>
|
|
83
|
+
!!value &&
|
|
84
|
+
typeof value === "object" &&
|
|
85
|
+
(value as any).__appflareHandlerError === true &&
|
|
86
|
+
Number.isFinite((value as any).status);
|
|
87
|
+
|
|
88
|
+
const handlerErrorBody = (
|
|
89
|
+
err: AppflareHandlerError
|
|
90
|
+
): { error: string; details?: unknown } => {
|
|
91
|
+
const includeDetails =
|
|
92
|
+
err.details !== undefined &&
|
|
93
|
+
(err.details && typeof err.details === "object"
|
|
94
|
+
? !(err.details instanceof Error) && !Array.isArray(err.details)
|
|
95
|
+
: true);
|
|
96
|
+
return includeDetails
|
|
97
|
+
? { error: err.message, details: err.details }
|
|
98
|
+
: { error: err.message };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
async function runHandlerWithMiddleware<TArgs, TResult>(
|
|
102
|
+
handler: {
|
|
103
|
+
handler: (ctx: AppflareServerContext, args: TArgs) => Promise<TResult>;
|
|
104
|
+
middleware?: (
|
|
105
|
+
ctx: AppflareServerContext,
|
|
106
|
+
args: TArgs
|
|
107
|
+
) => Promise<TResult | void>;
|
|
108
|
+
},
|
|
109
|
+
ctx: AppflareServerContext,
|
|
110
|
+
args: TArgs
|
|
111
|
+
): Promise<TResult> {
|
|
112
|
+
if (handler.middleware) {
|
|
113
|
+
const middlewareResult = await handler.middleware(ctx, args);
|
|
114
|
+
if (typeof middlewareResult !== "undefined") {
|
|
115
|
+
return middlewareResult as TResult;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return handler.handler(ctx, args);
|
|
119
|
+
}
|
|
120
|
+
|
|
55
121
|
export function createAppflareDbContext(params: {
|
|
56
122
|
\tdb: import("mongodb").Db;
|
|
57
123
|
\tcollectionName?: (table: TableNames) => string;
|
|
@@ -105,7 +171,8 @@ export type AppflareHonoServerOptions = {
|
|
|
105
171
|
c: HonoContext,
|
|
106
172
|
db: AppflareDbContext,
|
|
107
173
|
auth: AppflareAuthContext,
|
|
108
|
-
scheduler?: Scheduler
|
|
174
|
+
scheduler?: Scheduler,
|
|
175
|
+
error?: AppflareErrorFactory
|
|
109
176
|
) => AppflareServerContext | Promise<AppflareServerContext>;
|
|
110
177
|
\tcollectionName?: (table: TableNames) => string;
|
|
111
178
|
\tcorsOrigin?: string | string[];
|
|
@@ -135,6 +202,9 @@ function formatHandlerError(err: unknown): {
|
|
|
135
202
|
\tstatus: number;
|
|
136
203
|
\tbody: { error: string; details?: unknown };
|
|
137
204
|
} {
|
|
205
|
+
if (isHandlerError(err)) {
|
|
206
|
+
return { status: err.status, body: handlerErrorBody(err) };
|
|
207
|
+
}
|
|
138
208
|
\tconst statusCandidate =
|
|
139
209
|
\t\ttypeof err === "object" && err !== null
|
|
140
210
|
\t\t\t? Number((err as any).status ?? (err as any).statusCode)
|
|
@@ -192,8 +262,13 @@ export function createAppflareHonoServer(options: AppflareHonoServerOptions): Ho
|
|
|
192
262
|
|
|
193
263
|
const createContext =
|
|
194
264
|
options.createContext ??
|
|
195
|
-
((_c, db, auth, scheduler) =>
|
|
196
|
-
({
|
|
265
|
+
((_c, db, auth, scheduler, error) =>
|
|
266
|
+
({
|
|
267
|
+
db,
|
|
268
|
+
scheduler,
|
|
269
|
+
error: error ?? createHandlerError,
|
|
270
|
+
...auth,
|
|
271
|
+
} as AppflareServerContext));
|
|
197
272
|
\tconst notifyMutation = createMutationNotifier(options.realtime);
|
|
198
273
|
\tconst app = new Hono();
|
|
199
274
|
\tapp.use(
|
|
@@ -250,8 +325,17 @@ ${params.authSetupBlock}${params.authMountBlock}${params.authResolverBlock}\tcon
|
|
|
250
325
|
const db = await resolveDb(c);
|
|
251
326
|
const auth = await resolveAuthContext(c);
|
|
252
327
|
const scheduler = await resolveScheduler(c);
|
|
253
|
-
const
|
|
254
|
-
|
|
328
|
+
const error = createHandlerError;
|
|
329
|
+
const ctx = await createContext(c, db, auth, scheduler, error);
|
|
330
|
+
const merged = {
|
|
331
|
+
db,
|
|
332
|
+
scheduler,
|
|
333
|
+
error,
|
|
334
|
+
...auth,
|
|
335
|
+
...(ctx ?? {}),
|
|
336
|
+
error: (ctx as any)?.error ?? error,
|
|
337
|
+
};
|
|
338
|
+
return merged as AppflareServerContext;
|
|
255
339
|
};
|
|
256
340
|
|
|
257
341
|
\t${params.routeLines.join("\n\n\t")}
|
|
@@ -9,10 +9,10 @@ export const buildHandlerEntries = (params: {
|
|
|
9
9
|
return params.handlers
|
|
10
10
|
.map((handler) => {
|
|
11
11
|
const local = params.localNameFor(handler);
|
|
12
|
-
const task = `${handler.
|
|
12
|
+
const task = `${handler.routePath}/${handler.name}`;
|
|
13
13
|
return (
|
|
14
14
|
`\t${JSON.stringify(task)}: {\n` +
|
|
15
|
-
|
|
15
|
+
` \tfile: ${JSON.stringify(handler.routePath)},\n` +
|
|
16
16
|
`\t\tname: ${JSON.stringify(handler.name)},\n` +
|
|
17
17
|
`\t\trun: ${local}.handler,\n` +
|
|
18
18
|
`\t},`
|
|
@@ -28,7 +28,7 @@ export function buildImportSection(params: {
|
|
|
28
28
|
);
|
|
29
29
|
|
|
30
30
|
const localNameFor = (handler: DiscoveredHandler): string =>
|
|
31
|
-
`__appflare_${pascalCase(handler.
|
|
31
|
+
`__appflare_${pascalCase(handler.routePath)}_${handler.name}`;
|
|
32
32
|
|
|
33
33
|
const grouped = groupBy(params.queries, (handler) => handler.sourceFileAbs);
|
|
34
34
|
const handlerImports: string[] = [];
|
|
@@ -7,12 +7,12 @@ export function buildQueryHandlerEntries(params: {
|
|
|
7
7
|
return params.queries
|
|
8
8
|
.slice()
|
|
9
9
|
.sort((a, b) => {
|
|
10
|
-
if (a.
|
|
11
|
-
return a.
|
|
10
|
+
if (a.routePath === b.routePath) return a.name.localeCompare(b.name);
|
|
11
|
+
return a.routePath.localeCompare(b.routePath);
|
|
12
12
|
})
|
|
13
13
|
.map(
|
|
14
14
|
(query) =>
|
|
15
|
-
|
|
15
|
+
` ${JSON.stringify(`${query.routePath}/${query.name}`)}: { file: ${JSON.stringify(query.routePath)}, name: ${JSON.stringify(query.name)}, definition: ${params.localNameFor(query)} },`
|
|
16
16
|
)
|
|
17
17
|
.join("\n");
|
|
18
18
|
}
|
|
@@ -107,6 +107,10 @@ type QueryArgsParser = { parse?: (value: unknown) => unknown };
|
|
|
107
107
|
|
|
108
108
|
type QueryHandlerDefinition = {
|
|
109
109
|
\targs?: QueryArgsParser | unknown;
|
|
110
|
+
middleware?: (
|
|
111
|
+
ctx: import("./server").AppflareServerContext,
|
|
112
|
+
args: unknown
|
|
113
|
+
) => unknown | Promise<unknown>;
|
|
110
114
|
\thandler: (
|
|
111
115
|
\t\tctx: import("./server").AppflareServerContext,
|
|
112
116
|
\t\targs: unknown
|
|
@@ -133,10 +137,17 @@ const defaultHandlerForTable = (
|
|
|
133
137
|
\t\tpossible.push(tableStr.slice(0, -1));
|
|
134
138
|
\t}
|
|
135
139
|
\tfor (const candidate of possible) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|
|
140
151
|
\t}
|
|
141
152
|
\treturn null;
|
|
142
153
|
};
|
|
@@ -147,9 +158,77 @@ const resolveDatabase = (env: any) => {
|
|
|
147
158
|
\treturn db;
|
|
148
159
|
};
|
|
149
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
|
+
|
|
150
226
|
const formatHandlerError = (
|
|
151
227
|
\terr: unknown
|
|
152
228
|
): { error: string; details?: unknown } => {
|
|
229
|
+
if (isHandlerError(err)) {
|
|
230
|
+
return handlerErrorBody(err);
|
|
231
|
+
}
|
|
153
232
|
\tconst message =
|
|
154
233
|
\t\terr instanceof Error
|
|
155
234
|
\t\t\t? err.message
|
|
@@ -540,30 +619,38 @@ export class WebSocketHibernationServer extends DurableObject {
|
|
|
540
619
|
\t\treturn true;
|
|
541
620
|
\t}
|
|
542
621
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
+
}
|
|
567
654
|
|
|
568
655
|
\tprivate parseHandlerArgs(
|
|
569
656
|
\t\tquery: QueryHandlerDefinition,
|
|
@@ -586,7 +673,8 @@ export class WebSocketHibernationServer extends DurableObject {
|
|
|
586
673
|
\t\treturn {
|
|
587
674
|
\t\t\tdb: this.getDb(),
|
|
588
675
|
\t\t\tsession: auth?.session ?? null,
|
|
589
|
-
|
|
676
|
+
user: auth?.user ?? null,
|
|
677
|
+
error: createHandlerError,
|
|
590
678
|
\t\t} as AppflareServerContext;
|
|
591
679
|
\t}
|
|
592
680
|
|
package/cli/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
AppflareConfig,
|
|
25
25
|
assertDirExists,
|
|
26
26
|
assertFileExists,
|
|
27
|
+
toImportPathFromGeneratedSrc,
|
|
27
28
|
} from "./utils/utils";
|
|
28
29
|
|
|
29
30
|
type WatchConfig = {
|
|
@@ -139,6 +140,17 @@ async function buildFromConfig(params: {
|
|
|
139
140
|
await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
|
|
140
141
|
await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
|
|
141
142
|
|
|
143
|
+
// Re-export the user schema inside the generated output so downstream code can import it from the build directory.
|
|
144
|
+
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
145
|
+
outDirAbs,
|
|
146
|
+
schemaPathAbs
|
|
147
|
+
);
|
|
148
|
+
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
149
|
+
export type AppflareGeneratedSchema = typeof schema;
|
|
150
|
+
export default schema;
|
|
151
|
+
`;
|
|
152
|
+
await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
|
|
153
|
+
|
|
142
154
|
const schemaTypesTs = await generateSchemaTypes({
|
|
143
155
|
schemaPathAbs,
|
|
144
156
|
configPathAbs,
|
|
@@ -44,9 +44,60 @@ type WithSelected<TDoc, TKeys extends Keys<TDoc>> = Pick<TDoc, TKeys>;
|
|
|
44
44
|
|
|
45
45
|
export type SortDirection = "asc" | "desc";
|
|
46
46
|
|
|
47
|
+
type Comparable<T> = NonNil<T> extends number | bigint | Date ? NonNil<T> : never;
|
|
48
|
+
|
|
49
|
+
type RegexOperand<T> = NonNil<T> extends string
|
|
50
|
+
? string | RegExp | { pattern: string; options?: string }
|
|
51
|
+
: never;
|
|
52
|
+
|
|
53
|
+
type ArrayOperand<T> = ReadonlyArray<NonNil<T>>;
|
|
54
|
+
|
|
55
|
+
type QueryWhereField<T> =
|
|
56
|
+
| NonNil<T>
|
|
57
|
+
| {
|
|
58
|
+
eq?: NonNil<T>;
|
|
59
|
+
$eq?: NonNil<T>;
|
|
60
|
+
ne?: NonNil<T>;
|
|
61
|
+
$ne?: NonNil<T>;
|
|
62
|
+
in?: ArrayOperand<T>;
|
|
63
|
+
$in?: ArrayOperand<T>;
|
|
64
|
+
nin?: ArrayOperand<T>;
|
|
65
|
+
$nin?: ArrayOperand<T>;
|
|
66
|
+
gt?: Comparable<T>;
|
|
67
|
+
$gt?: Comparable<T>;
|
|
68
|
+
gte?: Comparable<T>;
|
|
69
|
+
$gte?: Comparable<T>;
|
|
70
|
+
lt?: Comparable<T>;
|
|
71
|
+
$lt?: Comparable<T>;
|
|
72
|
+
lte?: Comparable<T>;
|
|
73
|
+
$lte?: Comparable<T>;
|
|
74
|
+
exists?: boolean;
|
|
75
|
+
$exists?: boolean;
|
|
76
|
+
regex?: RegexOperand<T>;
|
|
77
|
+
$regex?: RegexOperand<T>;
|
|
78
|
+
$options?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type LogicalWhere<TableName extends TableNames> = {
|
|
82
|
+
$and?: ReadonlyArray<QueryWhere<TableName>>;
|
|
83
|
+
and?: ReadonlyArray<QueryWhere<TableName>>;
|
|
84
|
+
$or?: ReadonlyArray<QueryWhere<TableName>>;
|
|
85
|
+
or?: ReadonlyArray<QueryWhere<TableName>>;
|
|
86
|
+
$nor?: ReadonlyArray<QueryWhere<TableName>>;
|
|
87
|
+
nor?: ReadonlyArray<QueryWhere<TableName>>;
|
|
88
|
+
$not?: QueryWhere<TableName>;
|
|
89
|
+
not?: QueryWhere<TableName>;
|
|
90
|
+
};
|
|
91
|
+
|
|
47
92
|
export type QueryWhere<TableName extends TableNames> = Partial<
|
|
48
|
-
|
|
49
|
-
|
|
93
|
+
{
|
|
94
|
+
[K in keyof TableDocMap[TableName]]?: QueryWhereField<
|
|
95
|
+
TableDocMap[TableName][K]
|
|
96
|
+
>;
|
|
97
|
+
}
|
|
98
|
+
> &
|
|
99
|
+
LogicalWhere<TableName> &
|
|
100
|
+
Record<string, unknown>;
|
|
50
101
|
|
|
51
102
|
export type QuerySortKey<TableName extends TableNames> = keyof TableDocMap[TableName] &
|
|
52
103
|
string;
|
|
@@ -71,7 +122,7 @@ const GEO_EARTH_RADIUS_METERS = 6_378_100;
|
|
|
71
122
|
const __geoNormalizePoint = (point: GeoPointInput): GeoPoint =>
|
|
72
123
|
Array.isArray(point)
|
|
73
124
|
? { type: "Point", coordinates: [point[0], point[1]] }
|
|
74
|
-
: point;
|
|
125
|
+
: (point as GeoPoint);
|
|
75
126
|
|
|
76
127
|
export const geo = {
|
|
77
128
|
point(lng: number, lat: number): GeoPoint {
|
|
@@ -509,9 +560,22 @@ export const cron = <Env = unknown>(
|
|
|
509
560
|
definition: CronDefinition<Env>
|
|
510
561
|
): CronDefinition<Env> => definition;
|
|
511
562
|
|
|
563
|
+
export type AppflareHandlerError = Error & {
|
|
564
|
+
status: number;
|
|
565
|
+
details?: unknown;
|
|
566
|
+
__appflareHandlerError: true;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export type AppflareErrorFactory = (
|
|
570
|
+
status: number,
|
|
571
|
+
message?: string,
|
|
572
|
+
details?: unknown
|
|
573
|
+
) => AppflareHandlerError;
|
|
574
|
+
|
|
512
575
|
export interface QueryContext extends AppflareAuthContext {
|
|
513
576
|
db: DatabaseReader;
|
|
514
577
|
scheduler?: Scheduler;
|
|
578
|
+
error: AppflareErrorFactory;
|
|
515
579
|
}
|
|
516
580
|
|
|
517
581
|
export interface InternalQueryContext {
|
|
@@ -530,6 +594,10 @@ export type InferQueryArgs<TArgs extends QueryArgsShape> = {
|
|
|
530
594
|
|
|
531
595
|
export interface QueryDefinition<TArgs extends QueryArgsShape, TResult> {
|
|
532
596
|
args: TArgs;
|
|
597
|
+
middleware?: (
|
|
598
|
+
ctx: QueryContext,
|
|
599
|
+
args: InferQueryArgs<TArgs>
|
|
600
|
+
) => Promise<TResult | void>;
|
|
533
601
|
handler: (ctx: QueryContext, args: InferQueryArgs<TArgs>) => Promise<TResult>;
|
|
534
602
|
}
|
|
535
603
|
|
|
@@ -547,10 +615,15 @@ export interface DatabaseWriter extends DatabaseReader {}
|
|
|
547
615
|
export interface MutationContext extends AppflareAuthContext {
|
|
548
616
|
db: DatabaseWriter;
|
|
549
617
|
scheduler?: Scheduler;
|
|
618
|
+
error: AppflareErrorFactory;
|
|
550
619
|
}
|
|
551
620
|
|
|
552
621
|
export interface MutationDefinition<TArgs extends QueryArgsShape, TResult> {
|
|
553
622
|
args: TArgs;
|
|
623
|
+
middleware?: (
|
|
624
|
+
ctx: MutationContext,
|
|
625
|
+
args: InferQueryArgs<TArgs>
|
|
626
|
+
) => Promise<TResult | void>;
|
|
554
627
|
handler: (
|
|
555
628
|
ctx: MutationContext,
|
|
556
629
|
args: InferQueryArgs<TArgs>
|
|
@@ -571,6 +644,10 @@ export interface InternalQueryDefinition<
|
|
|
571
644
|
TResult,
|
|
572
645
|
> {
|
|
573
646
|
args: TArgs;
|
|
647
|
+
middleware?: (
|
|
648
|
+
ctx: InternalQueryContext,
|
|
649
|
+
args: InferQueryArgs<TArgs>
|
|
650
|
+
) => Promise<TResult | void>;
|
|
574
651
|
handler: (
|
|
575
652
|
ctx: InternalQueryContext,
|
|
576
653
|
args: InferQueryArgs<TArgs>
|
|
@@ -586,6 +663,10 @@ export interface InternalMutationDefinition<
|
|
|
586
663
|
TResult,
|
|
587
664
|
> {
|
|
588
665
|
args: TArgs;
|
|
666
|
+
middleware?: (
|
|
667
|
+
ctx: InternalMutationContext,
|
|
668
|
+
args: InferQueryArgs<TArgs>
|
|
669
|
+
) => Promise<TResult | void>;
|
|
589
670
|
handler: (
|
|
590
671
|
ctx: InternalMutationContext,
|
|
591
672
|
args: InferQueryArgs<TArgs>
|
package/cli/utils/tsc.ts
CHANGED
|
@@ -34,17 +34,18 @@ export async function writeEmitTsconfig(params: {
|
|
|
34
34
|
declaration: true,
|
|
35
35
|
emitDeclarationOnly: false,
|
|
36
36
|
outDir: `./${outDirRel}/dist`,
|
|
37
|
-
rootDir:
|
|
37
|
+
rootDir: `.`,
|
|
38
38
|
sourceMap: false,
|
|
39
39
|
declarationMap: false,
|
|
40
40
|
skipLibCheck: true,
|
|
41
41
|
target: "ES2022",
|
|
42
42
|
module: "ES2022",
|
|
43
43
|
moduleResolution: "Bundler",
|
|
44
|
-
types: [],
|
|
44
|
+
types: ["node"],
|
|
45
45
|
},
|
|
46
46
|
include: [
|
|
47
47
|
`./${outDirRel}/src/schema-types.ts`,
|
|
48
|
+
`./${outDirRel}/src/schema.ts`,
|
|
48
49
|
`./${outDirRel}/src/handlers/**/*`,
|
|
49
50
|
],
|
|
50
51
|
};
|
package/cli/utils/utils.ts
CHANGED
package/cli/utils/zod-utils.ts
CHANGED
|
@@ -63,8 +63,8 @@ function renderType(schema: any): { tsType: string; optional: boolean } {
|
|
|
63
63
|
const description: string | undefined =
|
|
64
64
|
schema?.description ?? def?.description;
|
|
65
65
|
if (typeof description === "string" && description.startsWith("ref:")) {
|
|
66
|
-
|
|
67
|
-
return { tsType:
|
|
66
|
+
// Treat reference fields as plain strings in generated types for friendlier typing/UX.
|
|
67
|
+
return { tsType: "string", optional: false };
|
|
68
68
|
}
|
|
69
69
|
return { tsType: "string", optional: false };
|
|
70
70
|
}
|
package/lib/db.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export type AppflareTable<TShape extends Record<string, z.ZodTypeAny>> =
|
|
4
|
+
z.ZodObject<TShape>;
|
|
5
|
+
|
|
6
|
+
export type AppflareSchema<TTables extends Record<string, AppflareTable<any>>> =
|
|
7
|
+
TTables;
|
|
8
|
+
|
|
9
|
+
export function defineTable<TShape extends Record<string, z.ZodTypeAny>>(
|
|
10
|
+
shape: TShape
|
|
11
|
+
): AppflareTable<TShape> {
|
|
4
12
|
return z.object(shape);
|
|
5
13
|
}
|
|
6
14
|
|
|
7
|
-
export function defineSchema
|
|
15
|
+
export function defineSchema<
|
|
16
|
+
TTables extends Record<string, AppflareTable<any>>,
|
|
17
|
+
>(tables: TTables): AppflareSchema<TTables> {
|
|
8
18
|
return tables;
|
|
9
19
|
}
|