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 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: `./${outDirRel}/src`,
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 { assertDirExists, assertFileExists } from "../utils/utils";
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
- params.schemaPathAbs
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 ${local}.handler(ctx as any, query as any);\n` +
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 ${local}.handler(ctx as any, body as any);\n` +
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
- ({ db, scheduler, ...auth } as AppflareServerContext));
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 ctx = await createContext(c, db, auth, scheduler);
254
- return ctx ?? ({ db, scheduler, ...auth } as AppflareServerContext);
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
- \tprivate async fetchData(sub: Subscription): Promise<unknown> {
544
- \t\tconst subWithAuth = await this.withAuth(sub);
545
- \t\tconst query = resolveQueryHandler(subWithAuth.handler);
546
- \t\tif (query) {
547
- \t\t\tconst ctx = this.createHandlerContext(subWithAuth.auth);
548
- \t\t\tconst parsedArgs = this.parseHandlerArgs(query, subWithAuth.where);
549
- \t\t\treturn await query.handler(ctx, parsedArgs);
550
- \t\t}
551
-
552
- \t\tconst db = this.getDb();
553
- \t\tconst table = db[subWithAuth.table] as any;
554
- \t\tif (!table || typeof table.findMany !== "function") {
555
- \t\t\tthrow new Error("Unknown table: " + subWithAuth.table);
556
- \t\t}
557
- \t\tconst data = await table.findMany({
558
- \t\t\twhere: subWithAuth.where as any,
559
- \t\t\torderBy: subWithAuth.orderBy as any,
560
- \t\t\tskip: subWithAuth.skip,
561
- \t\t\ttake: subWithAuth.take,
562
- \t\t\tselect: subWithAuth.select as any,
563
- \t\t\tinclude: subWithAuth.include as any,
564
- \t\t});
565
- \t\treturn data ?? [];
566
- \t}
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
- \t\t\tuser: auth?.user ?? null,
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
- TableDocMap[TableName]
49
- > & Record<string, unknown>;
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: `./${outDirRel}/src`,
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
  };
@@ -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
- const table = description.slice("ref:".length);
67
- return { tsType: `Id<${JSON.stringify(table)}>`, optional: false };
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 function defineTable(shape: Record<string, z.ZodTypeAny>) {
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(tables: Record<string, z.ZodTypeAny>) {
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: z.ZodTypeAny) => z.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: z.ZodTypeAny) => schema.optional(),
19
- nullable: (schema: z.ZodTypeAny) => schema.nullable(),
20
- union: (...schemas: z.ZodTypeAny[]) => z.union(schemas),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appflare",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "bin": {
5
5
  "appflare": "./cli/index.ts"
6
6
  },
@@ -260,11 +260,13 @@ function createAppflareTableClient<
260
260
 
261
261
  const normalizedWhere = args.where
262
262
  ? normalizeIdFilter(
263
- normalizeRefFields(
264
- params.table as string,
265
- args.where as Record<string, unknown>,
266
- params.refs
267
- ) as any
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 { normalizeIdFilter, stringifyIdField } from "../utils/id-utils";
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 = next as unknown as Filter<Document>;
64
+ const nextFilter = normalizeIdFilter(
65
+ toMongoFilter(next as unknown as Filter<Document>)
66
+ );
61
67
  if (!filter) {
62
68
  filter = nextFilter;
63
69
  } else {
@@ -4,11 +4,13 @@ import type {
4
4
  StorageRule,
5
5
  } from "./types";
6
6
 
7
- const allowAnonymous: () => StorageAuthResult = () => ({ allow: true });
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
  }
@@ -140,7 +140,57 @@ export type QuerySort<TKey extends string> =
140
140
  | Record<string, SortDirection>
141
141
  | Array<[string, SortDirection]>;
142
142
 
143
- export type QueryWhere<TDoc extends Record<string, unknown>> = Partial<TDoc> &
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;
@@ -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
- return where as Filter<Document>;
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
  }