appflare 0.0.13 → 0.0.14
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/index.ts +19 -1
- package/cli/generators/generate-api-client/index.ts +18 -0
- package/cli/generators/generate-hono-server/imports.ts +3 -1
- package/cli/generators/generate-hono-server/routes.ts +18 -2
- package/cli/generators/generate-hono-server/template.ts +89 -5
- package/cli/generators/generate-websocket-durable-object/template.ts +106 -25
- 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/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
package/cli/core/build.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AppflareConfig,
|
|
6
6
|
assertDirExists,
|
|
7
7
|
assertFileExists,
|
|
8
|
+
toImportPathFromGeneratedSrc,
|
|
8
9
|
} from "../utils/utils";
|
|
9
10
|
import { getSchemaTableNames, generateSchemaTypes } from "../schema/schema";
|
|
10
11
|
import {
|
|
@@ -38,6 +39,17 @@ export async function buildFromConfig(params: {
|
|
|
38
39
|
await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
|
|
39
40
|
await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
|
|
40
41
|
|
|
42
|
+
// Re-export the user schema inside the generated output so downstream code can import it from the build directory.
|
|
43
|
+
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
44
|
+
outDirAbs,
|
|
45
|
+
schemaPathAbs
|
|
46
|
+
);
|
|
47
|
+
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
48
|
+
export type AppflareGeneratedSchema = typeof schema;
|
|
49
|
+
export default schema;
|
|
50
|
+
`;
|
|
51
|
+
await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
|
|
52
|
+
|
|
41
53
|
const schemaTypesTs = await generateSchemaTypes({
|
|
42
54
|
schemaPathAbs,
|
|
43
55
|
configPathAbs,
|
|
@@ -140,17 +152,18 @@ async function writeEmitTsconfig(params: {
|
|
|
140
152
|
declaration: true,
|
|
141
153
|
emitDeclarationOnly: false,
|
|
142
154
|
outDir: `./${outDirRel}/dist`,
|
|
143
|
-
rootDir:
|
|
155
|
+
rootDir: `.`,
|
|
144
156
|
sourceMap: false,
|
|
145
157
|
declarationMap: false,
|
|
146
158
|
skipLibCheck: true,
|
|
147
159
|
target: "ES2022",
|
|
148
160
|
module: "ES2022",
|
|
149
161
|
moduleResolution: "Bundler",
|
|
150
|
-
types: [],
|
|
162
|
+
types: ["node"],
|
|
151
163
|
},
|
|
152
164
|
include: [
|
|
153
165
|
`./${outDirRel}/src/schema-types.ts`,
|
|
166
|
+
`./${outDirRel}/src/schema.ts`,
|
|
154
167
|
`./${outDirRel}/src/handlers/**/*`,
|
|
155
168
|
],
|
|
156
169
|
};
|
package/cli/core/index.ts
CHANGED
|
@@ -14,12 +14,20 @@ import {
|
|
|
14
14
|
} from "./handlers";
|
|
15
15
|
import { generateSchemaTypes, getSchemaTableNames } from "../schema/schema";
|
|
16
16
|
import { runTscEmit, writeEmitTsconfig } from "../utils/tsc";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertDirExists,
|
|
19
|
+
assertFileExists,
|
|
20
|
+
toImportPathFromGeneratedSrc,
|
|
21
|
+
} from "../utils/utils";
|
|
18
22
|
|
|
19
23
|
type AppflareConfig = {
|
|
20
24
|
dir: string;
|
|
21
25
|
schema: string;
|
|
22
26
|
outDir: string;
|
|
27
|
+
auth?: {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
basePath?: string;
|
|
30
|
+
};
|
|
23
31
|
};
|
|
24
32
|
|
|
25
33
|
const program = new Command();
|
|
@@ -106,6 +114,16 @@ async function buildFromConfig(params: {
|
|
|
106
114
|
await fs.mkdir(path.join(outDirAbs, "src"), { recursive: true });
|
|
107
115
|
await fs.mkdir(path.join(outDirAbs, "server"), { recursive: true });
|
|
108
116
|
|
|
117
|
+
const schemaImportPathForGeneratedSrc = toImportPathFromGeneratedSrc(
|
|
118
|
+
outDirAbs,
|
|
119
|
+
schemaPathAbs
|
|
120
|
+
);
|
|
121
|
+
const schemaReexport = `import schema from ${JSON.stringify(schemaImportPathForGeneratedSrc)};
|
|
122
|
+
export type AppflareGeneratedSchema = typeof schema;
|
|
123
|
+
export default schema;
|
|
124
|
+
`;
|
|
125
|
+
await fs.writeFile(path.join(outDirAbs, "src", "schema.ts"), schemaReexport);
|
|
126
|
+
|
|
109
127
|
const schemaTypesTs = await generateSchemaTypes({
|
|
110
128
|
schemaPathAbs,
|
|
111
129
|
configPathAbs,
|
|
@@ -369,6 +369,15 @@ export async function runInternalQuery<
|
|
|
369
369
|
args: HandlerArgsFromShape<TArgs>
|
|
370
370
|
): Promise<TResult> {
|
|
371
371
|
const parsed = parseHandlerArgs(handler as any, args as any);
|
|
372
|
+
if (handler.middleware) {
|
|
373
|
+
const middlewareResult = await handler.middleware(
|
|
374
|
+
ctx as any,
|
|
375
|
+
parsed as any
|
|
376
|
+
);
|
|
377
|
+
if (typeof middlewareResult !== "undefined") {
|
|
378
|
+
return middlewareResult as TResult;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
372
381
|
return handler.handler(ctx as any, parsed as any);
|
|
373
382
|
}
|
|
374
383
|
|
|
@@ -381,6 +390,15 @@ export async function runInternalMutation<
|
|
|
381
390
|
args: HandlerArgsFromShape<TArgs>
|
|
382
391
|
): Promise<TResult> {
|
|
383
392
|
const parsed = parseHandlerArgs(handler as any, args as any);
|
|
393
|
+
if (handler.middleware) {
|
|
394
|
+
const middlewareResult = await handler.middleware(
|
|
395
|
+
ctx as any,
|
|
396
|
+
parsed as any
|
|
397
|
+
);
|
|
398
|
+
if (typeof middlewareResult !== "undefined") {
|
|
399
|
+
return middlewareResult as TResult;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
384
402
|
return handler.handler(ctx as any, parsed as any);
|
|
385
403
|
}
|
|
386
404
|
|
|
@@ -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,
|
|
@@ -16,7 +16,15 @@ export function buildRouteLines(params: {
|
|
|
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` +
|
|
@@ -37,7 +45,15 @@ export function buildRouteLines(params: {
|
|
|
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` +
|
|
@@ -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")}
|
|
@@ -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
|
|
@@ -147,9 +151,77 @@ const resolveDatabase = (env: any) => {
|
|
|
147
151
|
\treturn db;
|
|
148
152
|
};
|
|
149
153
|
|
|
154
|
+
type AppflareHandlerError = Error & {
|
|
155
|
+
status: number;
|
|
156
|
+
details?: unknown;
|
|
157
|
+
__appflareHandlerError: true;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
type AppflareErrorFactory = (
|
|
161
|
+
status: number,
|
|
162
|
+
message?: string,
|
|
163
|
+
details?: unknown
|
|
164
|
+
) => AppflareHandlerError;
|
|
165
|
+
|
|
166
|
+
const createHandlerError: AppflareErrorFactory = (
|
|
167
|
+
status,
|
|
168
|
+
message,
|
|
169
|
+
details
|
|
170
|
+
) => {
|
|
171
|
+
const err = new Error(message ?? \`HTTP \${status}\`);
|
|
172
|
+
err.name = "AppflareHandlerError";
|
|
173
|
+
const typed = err as AppflareHandlerError;
|
|
174
|
+
typed.status = status;
|
|
175
|
+
typed.details = details;
|
|
176
|
+
typed.__appflareHandlerError = true;
|
|
177
|
+
return typed;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const isHandlerError = (value: unknown): value is AppflareHandlerError =>
|
|
181
|
+
!!value &&
|
|
182
|
+
typeof value === "object" &&
|
|
183
|
+
(value as any).__appflareHandlerError === true &&
|
|
184
|
+
Number.isFinite((value as any).status);
|
|
185
|
+
|
|
186
|
+
const handlerErrorBody = (
|
|
187
|
+
err: AppflareHandlerError
|
|
188
|
+
): { error: string; details?: unknown } => {
|
|
189
|
+
const includeDetails =
|
|
190
|
+
err.details !== undefined &&
|
|
191
|
+
(err.details && typeof err.details === "object"
|
|
192
|
+
? !(err.details instanceof Error) && !Array.isArray(err.details)
|
|
193
|
+
: true);
|
|
194
|
+
return includeDetails
|
|
195
|
+
? { error: err.message, details: err.details }
|
|
196
|
+
: { error: err.message };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
async function runHandlerWithMiddleware<TArgs, TResult>(
|
|
200
|
+
handler: {
|
|
201
|
+
handler: (ctx: AppflareServerContext, args: TArgs) => Promise<TResult> | TResult;
|
|
202
|
+
middleware?: (
|
|
203
|
+
ctx: AppflareServerContext,
|
|
204
|
+
args: TArgs
|
|
205
|
+
) => Promise<TResult | void> | TResult | void;
|
|
206
|
+
},
|
|
207
|
+
ctx: AppflareServerContext,
|
|
208
|
+
args: TArgs
|
|
209
|
+
): Promise<TResult> {
|
|
210
|
+
if (handler.middleware) {
|
|
211
|
+
const middlewareResult = await handler.middleware(ctx, args);
|
|
212
|
+
if (typeof middlewareResult !== "undefined") {
|
|
213
|
+
return middlewareResult as TResult;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return handler.handler(ctx, args) as Promise<TResult>;
|
|
217
|
+
}
|
|
218
|
+
|
|
150
219
|
const formatHandlerError = (
|
|
151
220
|
\terr: unknown
|
|
152
221
|
): { error: string; details?: unknown } => {
|
|
222
|
+
if (isHandlerError(err)) {
|
|
223
|
+
return handlerErrorBody(err);
|
|
224
|
+
}
|
|
153
225
|
\tconst message =
|
|
154
226
|
\t\terr instanceof Error
|
|
155
227
|
\t\t\t? err.message
|
|
@@ -540,30 +612,38 @@ export class WebSocketHibernationServer extends DurableObject {
|
|
|
540
612
|
\t\treturn true;
|
|
541
613
|
\t}
|
|
542
614
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
615
|
+
private async fetchData(sub: Subscription): Promise<unknown> {
|
|
616
|
+
const subWithAuth = await this.withAuth(sub);
|
|
617
|
+
const query = resolveQueryHandler(subWithAuth.handler);
|
|
618
|
+
if (query) {
|
|
619
|
+
const ctx = this.createHandlerContext(subWithAuth.auth);
|
|
620
|
+
const parsedArgs = this.parseHandlerArgs(query, subWithAuth.where);
|
|
621
|
+
const result = await runHandlerWithMiddleware(
|
|
622
|
+
query,
|
|
623
|
+
ctx,
|
|
624
|
+
parsedArgs as any
|
|
625
|
+
);
|
|
626
|
+
if (isHandlerError(result)) {
|
|
627
|
+
throw result;
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const db = this.getDb();
|
|
633
|
+
const table = db[subWithAuth.table] as any;
|
|
634
|
+
if (!table || typeof table.findMany !== "function") {
|
|
635
|
+
throw new Error("Unknown table: " + subWithAuth.table);
|
|
636
|
+
}
|
|
637
|
+
const data = await table.findMany({
|
|
638
|
+
where: subWithAuth.where as any,
|
|
639
|
+
orderBy: subWithAuth.orderBy as any,
|
|
640
|
+
skip: subWithAuth.skip,
|
|
641
|
+
take: subWithAuth.take,
|
|
642
|
+
select: subWithAuth.select as any,
|
|
643
|
+
include: subWithAuth.include as any,
|
|
644
|
+
});
|
|
645
|
+
return data ?? [];
|
|
646
|
+
}
|
|
567
647
|
|
|
568
648
|
\tprivate parseHandlerArgs(
|
|
569
649
|
\t\tquery: QueryHandlerDefinition,
|
|
@@ -586,7 +666,8 @@ export class WebSocketHibernationServer extends DurableObject {
|
|
|
586
666
|
\t\treturn {
|
|
587
667
|
\t\t\tdb: this.getDb(),
|
|
588
668
|
\t\t\tsession: auth?.session ?? null,
|
|
589
|
-
|
|
669
|
+
user: auth?.user ?? null,
|
|
670
|
+
error: createHandlerError,
|
|
590
671
|
\t\t} as AppflareServerContext;
|
|
591
672
|
\t}
|
|
592
673
|
|
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/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
|
}
|
package/lib/values.ts
CHANGED
|
@@ -12,12 +12,13 @@ export const v = {
|
|
|
12
12
|
z
|
|
13
13
|
.string()
|
|
14
14
|
.regex(/^[a-f\d]{24}$/i, "Invalid ObjectId")
|
|
15
|
-
.describe(`ref:${table}`),
|
|
16
|
-
array: (item:
|
|
15
|
+
.describe(`ref:${table}`) as z.ZodString,
|
|
16
|
+
array: <T extends z.ZodTypeAny>(item: T) => z.array(item),
|
|
17
17
|
object: (shape: Record<string, z.ZodTypeAny>) => z.object(shape),
|
|
18
|
-
optional: (schema:
|
|
19
|
-
nullable: (schema:
|
|
20
|
-
union:
|
|
18
|
+
optional: <T extends z.ZodTypeAny>(schema: T) => schema.optional(),
|
|
19
|
+
nullable: <T extends z.ZodTypeAny>(schema: T) => schema.nullable(),
|
|
20
|
+
union: <T extends [z.ZodTypeAny, ...z.ZodTypeAny[]]>(...schemas: T) =>
|
|
21
|
+
z.union(schemas),
|
|
21
22
|
literal: (value: any) => z.literal(value),
|
|
22
23
|
buffer: () => z.string(), // or z.instanceof(Buffer) if using Buffer
|
|
23
24
|
any: () => z.any(),
|
package/package.json
CHANGED
|
@@ -260,11 +260,13 @@ function createAppflareTableClient<
|
|
|
260
260
|
|
|
261
261
|
const normalizedWhere = args.where
|
|
262
262
|
? normalizeIdFilter(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
263
|
+
toMongoFilter(
|
|
264
|
+
normalizeRefFields(
|
|
265
|
+
params.table as string,
|
|
266
|
+
args.where as Record<string, unknown>,
|
|
267
|
+
params.refs
|
|
268
|
+
) as any
|
|
269
|
+
)
|
|
268
270
|
)
|
|
269
271
|
: undefined;
|
|
270
272
|
if (normalizedWhere && Object.keys(normalizedWhere).length > 0) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Collection, Document, Filter, FindOptions, Sort } from "mongodb";
|
|
2
2
|
import { applyPopulate } from "./populate";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
normalizeIdFilter,
|
|
5
|
+
stringifyIdField,
|
|
6
|
+
toMongoFilter,
|
|
7
|
+
} from "../utils/id-utils";
|
|
4
8
|
import type { MongoDbQuery, SchemaRefMap } from "../types/types";
|
|
5
9
|
import { buildProjection, normalizeSort } from "./query-utils";
|
|
6
10
|
|
|
@@ -57,7 +61,9 @@ export function createQueryBuilder<
|
|
|
57
61
|
|
|
58
62
|
const api: MongoDbQuery<any, any, any> = {
|
|
59
63
|
where(next) {
|
|
60
|
-
const nextFilter =
|
|
64
|
+
const nextFilter = normalizeIdFilter(
|
|
65
|
+
toMongoFilter(next as unknown as Filter<Document>)
|
|
66
|
+
);
|
|
61
67
|
if (!filter) {
|
|
62
68
|
filter = nextFilter;
|
|
63
69
|
} else {
|
package/server/storage/auth.ts
CHANGED
|
@@ -4,11 +4,13 @@ import type {
|
|
|
4
4
|
StorageRule,
|
|
5
5
|
} from "./types";
|
|
6
6
|
|
|
7
|
-
const allowAnonymous
|
|
7
|
+
const allowAnonymous = <Principal>(): StorageAuthResult<Principal> => ({
|
|
8
|
+
allow: true,
|
|
9
|
+
});
|
|
8
10
|
|
|
9
11
|
export async function authorizeRequest<Env, Principal>(
|
|
10
12
|
ctx: StorageBaseContext<Env, Principal>,
|
|
11
13
|
rule: StorageRule<Env, Principal>
|
|
12
14
|
): Promise<StorageAuthResult<Principal>> {
|
|
13
|
-
return rule.authorize ? rule.authorize(ctx) : allowAnonymous();
|
|
15
|
+
return rule.authorize ? rule.authorize(ctx) : allowAnonymous<Principal>();
|
|
14
16
|
}
|
package/server/types/types.ts
CHANGED
|
@@ -140,7 +140,57 @@ export type QuerySort<TKey extends string> =
|
|
|
140
140
|
| Record<string, SortDirection>
|
|
141
141
|
| Array<[string, SortDirection]>;
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
type Comparable<T> =
|
|
144
|
+
NonNil<T> extends number | bigint | Date ? NonNil<T> : never;
|
|
145
|
+
|
|
146
|
+
type RegexOperand<T> =
|
|
147
|
+
NonNil<T> extends string
|
|
148
|
+
? string | RegExp | { pattern: string; options?: string }
|
|
149
|
+
: never;
|
|
150
|
+
|
|
151
|
+
type ArrayOperand<T> = ReadonlyArray<NonNil<T>>;
|
|
152
|
+
|
|
153
|
+
type QueryWhereField<T> =
|
|
154
|
+
| NonNil<T>
|
|
155
|
+
| {
|
|
156
|
+
eq?: NonNil<T>;
|
|
157
|
+
$eq?: NonNil<T>;
|
|
158
|
+
ne?: NonNil<T>;
|
|
159
|
+
$ne?: NonNil<T>;
|
|
160
|
+
in?: ArrayOperand<T>;
|
|
161
|
+
$in?: ArrayOperand<T>;
|
|
162
|
+
nin?: ArrayOperand<T>;
|
|
163
|
+
$nin?: ArrayOperand<T>;
|
|
164
|
+
gt?: Comparable<T>;
|
|
165
|
+
$gt?: Comparable<T>;
|
|
166
|
+
gte?: Comparable<T>;
|
|
167
|
+
$gte?: Comparable<T>;
|
|
168
|
+
lt?: Comparable<T>;
|
|
169
|
+
$lt?: Comparable<T>;
|
|
170
|
+
lte?: Comparable<T>;
|
|
171
|
+
$lte?: Comparable<T>;
|
|
172
|
+
exists?: boolean;
|
|
173
|
+
$exists?: boolean;
|
|
174
|
+
regex?: RegexOperand<T>;
|
|
175
|
+
$regex?: RegexOperand<T>;
|
|
176
|
+
$options?: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
type LogicalWhere<TDoc extends Record<string, unknown>> = {
|
|
180
|
+
$and?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
181
|
+
and?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
182
|
+
$or?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
183
|
+
or?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
184
|
+
$nor?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
185
|
+
nor?: ReadonlyArray<QueryWhere<TDoc>>;
|
|
186
|
+
$not?: QueryWhere<TDoc>;
|
|
187
|
+
not?: QueryWhere<TDoc>;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type QueryWhere<TDoc extends Record<string, unknown>> = Partial<{
|
|
191
|
+
[K in keyof TDoc]?: QueryWhereField<TDoc[K]>;
|
|
192
|
+
}> &
|
|
193
|
+
LogicalWhere<TDoc> &
|
|
144
194
|
Record<string, unknown>;
|
|
145
195
|
|
|
146
196
|
export type Keys<T> = keyof T;
|
package/server/utils/id-utils.ts
CHANGED
|
@@ -14,6 +14,104 @@ function ensureObjectId(value: string | ObjectId): ObjectId {
|
|
|
14
14
|
return new ObjectId(value);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const OPERATOR_ALIAS_MAP: Record<string, string> = {
|
|
18
|
+
eq: "$eq",
|
|
19
|
+
ne: "$ne",
|
|
20
|
+
gt: "$gt",
|
|
21
|
+
gte: "$gte",
|
|
22
|
+
lt: "$lt",
|
|
23
|
+
lte: "$lte",
|
|
24
|
+
in: "$in",
|
|
25
|
+
nin: "$nin",
|
|
26
|
+
regex: "$regex",
|
|
27
|
+
exists: "$exists",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const LOGICAL_ALIAS_MAP: Record<string, string> = {
|
|
31
|
+
and: "$and",
|
|
32
|
+
or: "$or",
|
|
33
|
+
nor: "$nor",
|
|
34
|
+
not: "$not",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type NormalizedRegex =
|
|
38
|
+
| { $regex: string | RegExp; $options?: string }
|
|
39
|
+
| RegExp
|
|
40
|
+
| string;
|
|
41
|
+
|
|
42
|
+
function normalizeRegexOperand(value: unknown): NormalizedRegex | undefined {
|
|
43
|
+
if (value instanceof RegExp) return value;
|
|
44
|
+
if (typeof value === "string") return value;
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const obj = value as Record<string, unknown>;
|
|
47
|
+
const pattern = obj.pattern ?? obj.regex ?? obj.$regex;
|
|
48
|
+
const options = obj.options ?? obj.$options;
|
|
49
|
+
if (pattern instanceof RegExp) return pattern;
|
|
50
|
+
if (typeof pattern === "string") {
|
|
51
|
+
const normalized: { $regex: string; $options?: string } = {
|
|
52
|
+
$regex: pattern,
|
|
53
|
+
};
|
|
54
|
+
if (typeof options === "string" && options.length > 0) {
|
|
55
|
+
normalized.$options = options;
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeWhereOperators(value: unknown): unknown {
|
|
64
|
+
if (Array.isArray(value)) return value.map(normalizeWhereOperators);
|
|
65
|
+
if (value instanceof RegExp || value instanceof Date) return value;
|
|
66
|
+
if (value && typeof value === "object") {
|
|
67
|
+
const obj = value as Record<string, unknown>;
|
|
68
|
+
const out: Record<string, unknown> = {};
|
|
69
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
70
|
+
const logicalKey = LOGICAL_ALIAS_MAP[key];
|
|
71
|
+
if (logicalKey) {
|
|
72
|
+
const normalizedLogical = Array.isArray(val)
|
|
73
|
+
? (val as unknown[]).map(normalizeWhereOperators)
|
|
74
|
+
: normalizeWhereOperators(val);
|
|
75
|
+
out[logicalKey] = normalizedLogical;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (key.startsWith("$")) {
|
|
80
|
+
out[key] = normalizeWhereOperators(val);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const opKey = OPERATOR_ALIAS_MAP[key];
|
|
85
|
+
if (opKey) {
|
|
86
|
+
if (opKey === "$regex") {
|
|
87
|
+
const normalizedRegex = normalizeRegexOperand(val);
|
|
88
|
+
if (normalizedRegex === undefined) continue;
|
|
89
|
+
if (
|
|
90
|
+
normalizedRegex &&
|
|
91
|
+
typeof normalizedRegex === "object" &&
|
|
92
|
+
!Array.isArray(normalizedRegex) &&
|
|
93
|
+
!(normalizedRegex instanceof RegExp)
|
|
94
|
+
) {
|
|
95
|
+
out.$regex = normalizedRegex.$regex;
|
|
96
|
+
if (normalizedRegex.$options) {
|
|
97
|
+
out.$options = normalizedRegex.$options;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
out[opKey] = normalizeWhereOperators(normalizedRegex);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
out[opKey] = normalizeWhereOperators(val);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
out[key] = normalizeWhereOperators(val);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
17
115
|
export function toMongoFilter(
|
|
18
116
|
where: Id<any> | QueryWhere<any>
|
|
19
117
|
): Filter<Document> {
|
|
@@ -21,7 +119,8 @@ export function toMongoFilter(
|
|
|
21
119
|
return { _id: ensureObjectId(where) } satisfies Filter<Document> as any;
|
|
22
120
|
}
|
|
23
121
|
if (where && typeof where === "object") {
|
|
24
|
-
|
|
122
|
+
const normalized = normalizeWhereOperators(where);
|
|
123
|
+
return normalized as Filter<Document>;
|
|
25
124
|
}
|
|
26
125
|
throw new Error("update/delete requires an id or where filter object");
|
|
27
126
|
}
|