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.
@@ -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,
@@ -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.fileName)}_${handler.name}`;
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
- `\t${JSON.stringify(`/queries/${query.fileName}/${query.name}`)},\n` +
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 ${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` +
@@ -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.fileName}/${mutation.name}`)},\n` +
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 ${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` +
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.fileName)}, name: ${JSON.stringify(mutation.name)} },\n` +
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
- ({ 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")}
@@ -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.fileName}/${handler.name}`;
12
+ const task = `${handler.routePath}/${handler.name}`;
13
13
  return (
14
14
  `\t${JSON.stringify(task)}: {\n` +
15
- `\t\tfile: ${JSON.stringify(handler.fileName)},\n` +
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.fileName)}_${handler.name}`;
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.fileName === b.fileName) return a.name.localeCompare(b.name);
11
- return a.fileName.localeCompare(b.fileName);
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
- `\t${JSON.stringify(`${query.fileName}/${query.name}`)}: { file: ${JSON.stringify(query.fileName)}, name: ${JSON.stringify(query.name)}, definition: ${params.localNameFor(query)} },`
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
- \t\tconst key = candidate + "/get" + pascalCase(candidate);
137
- \t\tif (key in queryHandlers) {
138
- \t\t\treturn { file: candidate, name: "get" + pascalCase(candidate) };
139
- \t\t}
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
- \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}
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
- \t\t\tuser: auth?.user ?? null,
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
- 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
  };
@@ -52,6 +52,8 @@ export type HandlerKind =
52
52
 
53
53
  export type DiscoveredHandler = {
54
54
  fileName: string;
55
+ /** Path relative to project root without .ts extension, using forward slashes. */
56
+ routePath: string;
55
57
  name: string;
56
58
  kind: HandlerKind;
57
59
  sourceFileAbs: string;
@@ -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
  }