appflare 0.0.28 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/cli/commands/index.ts +140 -0
  2. package/cli/generate.ts +149 -0
  3. package/cli/index.ts +56 -447
  4. package/cli/load-config.ts +182 -0
  5. package/cli/schema-compiler.ts +657 -0
  6. package/cli/templates/auth/README.md +156 -0
  7. package/cli/templates/auth/config.ts +61 -0
  8. package/cli/templates/auth/route-config.ts +18 -0
  9. package/cli/templates/auth/route-handler.ts +18 -0
  10. package/cli/templates/auth/route-request-utils.ts +55 -0
  11. package/cli/templates/auth/route.ts +14 -0
  12. package/cli/templates/core/README.md +266 -0
  13. package/cli/templates/core/app-creation.ts +19 -0
  14. package/cli/templates/core/client/appflare.ts +37 -0
  15. package/cli/templates/core/client/index.ts +6 -0
  16. package/cli/templates/core/client/storage.ts +100 -0
  17. package/cli/templates/core/client/types.ts +54 -0
  18. package/cli/templates/core/client-modules/appflare.ts +112 -0
  19. package/cli/templates/core/client-modules/handlers/index.ts +740 -0
  20. package/cli/templates/core/client-modules/handlers.ts +1 -0
  21. package/cli/templates/core/client-modules/index.ts +7 -0
  22. package/cli/templates/core/client-modules/storage.ts +180 -0
  23. package/cli/templates/core/client-modules/types.ts +145 -0
  24. package/cli/templates/core/client.ts +39 -0
  25. package/cli/templates/core/drizzle.ts +15 -0
  26. package/cli/templates/core/export.ts +14 -0
  27. package/cli/templates/core/handlers-route.ts +23 -0
  28. package/cli/templates/core/handlers.ts +1 -0
  29. package/cli/templates/core/imports.ts +8 -0
  30. package/cli/templates/core/server.ts +38 -0
  31. package/cli/templates/core/types.ts +6 -0
  32. package/cli/templates/core/wrangler.ts +109 -0
  33. package/cli/templates/handlers/README.md +265 -0
  34. package/cli/templates/handlers/auth.ts +36 -0
  35. package/cli/templates/handlers/execution.ts +39 -0
  36. package/cli/templates/handlers/generators/context/context-creation.ts +80 -0
  37. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -0
  38. package/cli/templates/handlers/generators/context/scheduler.ts +24 -0
  39. package/cli/templates/handlers/generators/context/storage-api.ts +112 -0
  40. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -0
  41. package/cli/templates/handlers/generators/context/types.ts +18 -0
  42. package/cli/templates/handlers/generators/context.ts +43 -0
  43. package/cli/templates/handlers/generators/execution.ts +15 -0
  44. package/cli/templates/handlers/generators/handlers.ts +13 -0
  45. package/cli/templates/handlers/index.ts +43 -0
  46. package/cli/templates/handlers/operations.ts +116 -0
  47. package/cli/templates/handlers/registration.ts +1114 -0
  48. package/cli/templates/handlers/types.ts +960 -0
  49. package/cli/templates/handlers/utils.ts +48 -0
  50. package/cli/types.ts +108 -0
  51. package/cli/utils/handler-discovery.ts +366 -0
  52. package/cli/utils/json-utils.ts +24 -0
  53. package/cli/utils/path-utils.ts +19 -0
  54. package/cli/utils/schema-discovery.ts +390 -0
  55. package/index.ts +27 -4
  56. package/package.json +23 -20
  57. package/react/index.ts +5 -3
  58. package/react/use-infinite-query.ts +190 -0
  59. package/react/use-mutation.ts +54 -0
  60. package/react/use-query.ts +158 -0
  61. package/schema.ts +262 -0
  62. package/tsconfig.json +2 -4
  63. package/cli/README.md +0 -108
  64. package/cli/core/build.ts +0 -187
  65. package/cli/core/config.ts +0 -92
  66. package/cli/core/discover-handlers.ts +0 -143
  67. package/cli/core/handlers.ts +0 -7
  68. package/cli/core/index.ts +0 -205
  69. package/cli/generators/generate-api-client/client.ts +0 -163
  70. package/cli/generators/generate-api-client/extract-configuration.ts +0 -121
  71. package/cli/generators/generate-api-client/index.ts +0 -973
  72. package/cli/generators/generate-api-client/types.ts +0 -164
  73. package/cli/generators/generate-api-client/utils.ts +0 -22
  74. package/cli/generators/generate-api-client.ts +0 -1
  75. package/cli/generators/generate-cloudflare-worker/helpers.ts +0 -24
  76. package/cli/generators/generate-cloudflare-worker/index.ts +0 -2
  77. package/cli/generators/generate-cloudflare-worker/worker.ts +0 -148
  78. package/cli/generators/generate-cloudflare-worker/wrangler.ts +0 -108
  79. package/cli/generators/generate-cloudflare-worker.ts +0 -4
  80. package/cli/generators/generate-cron-handlers/cron-handlers-block.ts +0 -2
  81. package/cli/generators/generate-cron-handlers/handler-entries.ts +0 -29
  82. package/cli/generators/generate-cron-handlers/index.ts +0 -61
  83. package/cli/generators/generate-cron-handlers/runtime-block.ts +0 -49
  84. package/cli/generators/generate-cron-handlers/type-helpers-block.ts +0 -60
  85. package/cli/generators/generate-db-handlers/index.ts +0 -33
  86. package/cli/generators/generate-db-handlers/prepare.ts +0 -24
  87. package/cli/generators/generate-db-handlers/templates.ts +0 -189
  88. package/cli/generators/generate-db-handlers.ts +0 -1
  89. package/cli/generators/generate-hono-server/auth.ts +0 -97
  90. package/cli/generators/generate-hono-server/imports.ts +0 -55
  91. package/cli/generators/generate-hono-server/index.ts +0 -52
  92. package/cli/generators/generate-hono-server/routes.ts +0 -115
  93. package/cli/generators/generate-hono-server/template.ts +0 -371
  94. package/cli/generators/generate-hono-server.ts +0 -1
  95. package/cli/generators/generate-scheduler-handlers/constants.ts +0 -8
  96. package/cli/generators/generate-scheduler-handlers/handler-entries.ts +0 -22
  97. package/cli/generators/generate-scheduler-handlers/index.ts +0 -51
  98. package/cli/generators/generate-scheduler-handlers/runtime-block.ts +0 -68
  99. package/cli/generators/generate-scheduler-handlers/scheduler-handlers-block.ts +0 -2
  100. package/cli/generators/generate-scheduler-handlers/type-helpers-block.ts +0 -68
  101. package/cli/generators/generate-scheduler-handlers.ts +0 -1
  102. package/cli/generators/generate-websocket-durable-object/auth.ts +0 -30
  103. package/cli/generators/generate-websocket-durable-object/imports.ts +0 -55
  104. package/cli/generators/generate-websocket-durable-object/index.ts +0 -41
  105. package/cli/generators/generate-websocket-durable-object/query-handlers.ts +0 -18
  106. package/cli/generators/generate-websocket-durable-object/template.ts +0 -714
  107. package/cli/generators/generate-websocket-durable-object.ts +0 -1
  108. package/cli/schema/schema-static-types.ts +0 -702
  109. package/cli/schema/schema.ts +0 -151
  110. package/cli/utils/tsc.ts +0 -54
  111. package/cli/utils/utils.ts +0 -190
  112. package/cli/utils/zod-utils.ts +0 -121
  113. package/lib/README.md +0 -50
  114. package/lib/db.ts +0 -19
  115. package/lib/location.ts +0 -110
  116. package/lib/values.ts +0 -27
  117. package/react/README.md +0 -67
  118. package/react/hooks/useMutation.ts +0 -89
  119. package/react/hooks/usePaginatedQuery.ts +0 -213
  120. package/react/hooks/useQuery.ts +0 -106
  121. package/react/shared/queryShared.ts +0 -174
  122. package/server/README.md +0 -218
  123. package/server/auth.ts +0 -107
  124. package/server/database/builders.ts +0 -83
  125. package/server/database/context.ts +0 -327
  126. package/server/database/populate.ts +0 -234
  127. package/server/database/query-builder.ts +0 -161
  128. package/server/database/query-utils.ts +0 -25
  129. package/server/db.ts +0 -2
  130. package/server/storage/auth.ts +0 -16
  131. package/server/storage/bucket.ts +0 -22
  132. package/server/storage/context.ts +0 -34
  133. package/server/storage/index.ts +0 -38
  134. package/server/storage/operations.ts +0 -149
  135. package/server/storage/route-handler.ts +0 -60
  136. package/server/storage/types.ts +0 -55
  137. package/server/storage/utils.ts +0 -47
  138. package/server/storage.ts +0 -6
  139. package/server/types/schema-refs.ts +0 -66
  140. package/server/types/types.ts +0 -633
  141. package/server/utils/id-utils.ts +0 -230
@@ -0,0 +1,265 @@
1
+ # Handlers Template System (with Realtime Durable Objects)
2
+
3
+ This directory contains code-generation templates used by the Appflare CLI to build runtime handler files in `_generated/`.
4
+
5
+ The generated runtime now supports **realtime updates** using a **single global Cloudflare Durable Object** and websocket fanout.
6
+
7
+ ---
8
+
9
+ ## What this template set generates
10
+
11
+ From this directory, the CLI generates:
12
+
13
+ - `handlers.ts`
14
+ - `handlers.context.ts`
15
+ - `handlers.execution.ts`
16
+ - `handlers.routes.ts`
17
+
18
+ Key behavior added for realtime:
19
+
20
+ 1. Endpoint-first subscriptions (`POST /realtime/subscribe`)
21
+ 2. WebSocket connect endpoint (`GET /realtime/ws`)
22
+ 3. Per-subscription `token` + `signature`
23
+ 4. Query identity contract: `[dirName]/[fileName]/[functionName]`
24
+ 5. Args validation against discovered query schema (args-derived keys only)
25
+ 6. Mutation-to-query invalidation with **partial filter overlap** matching
26
+ 7. Push updated query result to matching subscribers only
27
+
28
+ ---
29
+
30
+ ## Realtime architecture
31
+
32
+ ### Components
33
+
34
+ - **Generated query routes** (`GET /queries/...`)
35
+ - **Generated mutation routes** (`POST /mutations/...`)
36
+ - **Realtime subscription endpoint** (`POST /realtime/subscribe`)
37
+ - **Realtime websocket endpoint** (`GET /realtime/ws?token=...&authToken=...`)
38
+ - **Global Durable Object class**: `AppflareRealtimeDurableObject`
39
+
40
+ ### Data flow
41
+
42
+ 1. Client calls `POST /realtime/subscribe` with:
43
+ - `queryName`: `[dir]/[file]/[function]`
44
+ - `args`: query arguments
45
+ - `authToken`: bearer token
46
+ 2. Server validates:
47
+ - query exists
48
+ - args parse against that query Zod schema
49
+ - auth token resolves to a user session
50
+ 3. Server stores subscription inside the global DO and returns:
51
+ - `token`
52
+ - `signature`
53
+ - websocket URL/protocol metadata
54
+ 4. Client opens websocket at `GET /realtime/ws?token=...&authToken=...`.
55
+ 5. On any mutation (`insert`, `update`, `delete`, `upsert`), generated `db` wrappers collect mutation events.
56
+ 6. Matching subscriptions are re-executed (same query + same validated args) and pushed over websocket as `query:update`.
57
+
58
+ ---
59
+
60
+ ## Identity and signatures
61
+
62
+ ### Query identity
63
+
64
+ Query names are generated from discovered source layout:
65
+
66
+ `[dirName]/[fileName]/[functionName]`
67
+
68
+ Examples:
69
+
70
+ - `users/profile/getProfile`
71
+ - `root/test/getTest`
72
+
73
+ ### Signature
74
+
75
+ Signature is generated from:
76
+
77
+ - `queryName`
78
+ - normalized `args`
79
+
80
+ The generated runtime normalizes/sorts object keys before stringifying, so logically equivalent payloads produce a stable signature.
81
+
82
+ ---
83
+
84
+ ## Partial filter overlap matching
85
+
86
+ When mutations run, subscribers are selected using overlap logic (not strict equality).
87
+
88
+ High-level rules:
89
+
90
+ - Scalars: equal values overlap
91
+ - Arrays: overlap if any element overlaps
92
+ - Objects: overlap if any shared key overlaps recursively
93
+ - Matching checks both:
94
+ - mutation input args (`set`, `where`, `values`, etc.)
95
+ - returned mutation rows
96
+
97
+ This means subscriptions react when filters intersect mutation impact, without requiring exact filter identity.
98
+
99
+ ---
100
+
101
+ ## Authentication model
102
+
103
+ ### Subscribe
104
+
105
+ `POST /realtime/subscribe` requires `authToken` in body.
106
+
107
+ The runtime creates a request with `Authorization: Bearer <authToken>` and calls `resolveSession(...)`.
108
+
109
+ If no user resolves, response is `401`.
110
+
111
+ ### WebSocket connect
112
+
113
+ `GET /realtime/ws` requires both query params:
114
+
115
+ - `token`
116
+ - `authToken`
117
+
118
+ Server validates token ownership and auth token before websocket upgrade.
119
+
120
+ ---
121
+
122
+ ## Endpoint contracts
123
+
124
+ ### 1) Subscribe
125
+
126
+ `POST /realtime/subscribe`
127
+
128
+ Request:
129
+
130
+ ```json
131
+ {
132
+ "queryName": "users/profile/getProfile",
133
+ "args": { "userId": "u_123" },
134
+ "authToken": "<token>"
135
+ }
136
+ ```
137
+
138
+ Success response:
139
+
140
+ ```json
141
+ {
142
+ "token": "<subscription-token>",
143
+ "signature": "users/profile/getProfile::{\"userId\":\"u_123\"}",
144
+ "websocket": {
145
+ "url": "wss://api.example.com/realtime/ws",
146
+ "protocol": "appflare.realtime.v1",
147
+ "params": {
148
+ "tokenParam": "token",
149
+ "authTokenParam": "authToken"
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### 2) WebSocket connect
156
+
157
+ `GET /realtime/ws?token=<token>&authToken=<authToken>`
158
+
159
+ Server pushes messages like:
160
+
161
+ ```json
162
+ {
163
+ "event": "query:update",
164
+ "payload": {
165
+ "queryName": "users/profile/getProfile",
166
+ "signature": "...",
167
+ "data": { "id": "u_123", "name": "Ada" }
168
+ }
169
+ }
170
+ ```
171
+
172
+ Heartbeat support:
173
+
174
+ - client sends: `ping`
175
+ - server replies: `{"event":"pong"}`
176
+
177
+ ---
178
+
179
+ ## Mutation event capture
180
+
181
+ `createQueryDb(...)` now accepts options with `onMutation`.
182
+
183
+ The generated wrappers for `insert`, `update`, `upsert`, and `delete` emit:
184
+
185
+ - operation kind
186
+ - table name
187
+ - mutation args
188
+ - returned rows
189
+
190
+ Execution contexts store these as `ctx.mutationEvents`, and mutation routes call `publishMutationEvents(...)` after successful execution.
191
+
192
+ ---
193
+
194
+ ## Durable Object responsibilities
195
+
196
+ `AppflareRealtimeDurableObject` keeps in-memory maps for:
197
+
198
+ - `subscriptions` (`token -> metadata`)
199
+ - `sockets` (`token -> websocket`)
200
+
201
+ Supported internal routes:
202
+
203
+ - `POST /subscribe`
204
+ - `POST /subscriptions`
205
+ - `POST /emit`
206
+ - `GET /ws`
207
+
208
+ This design centralizes fanout in one global app DO instance.
209
+
210
+ ---
211
+
212
+ ## Generated client support
213
+
214
+ The client generator exposes realtime helper APIs:
215
+
216
+ - `appflare.realtime.subscribe(...)`
217
+
218
+ Types include:
219
+
220
+ - `RealtimeSubscriptionRequest`
221
+ - `RealtimeSubscriptionResponse`
222
+
223
+ The client performs endpoint-first subscription; websocket connection is then established using returned metadata.
224
+
225
+ ---
226
+
227
+ ## Configuration knobs (from app config)
228
+
229
+ Realtime defaults are normalized from `realtime` config:
230
+
231
+ - `enabled` (default `true`)
232
+ - `binding` (default `APPFLARE_REALTIME`)
233
+ - `className` (default `AppflareRealtimeDurableObject`)
234
+ - `objectName` (default `global`)
235
+ - `subscribePath` (default `/realtime/subscribe`)
236
+ - `websocketPath` (default `/realtime/ws`)
237
+ - `protocol` (default `appflare.realtime.v1`)
238
+
239
+ Wrangler generation automatically emits:
240
+
241
+ - `durable_objects.bindings`
242
+ - `migrations` with DO class
243
+
244
+ ---
245
+
246
+ ## Notes and limitations
247
+
248
+ 1. Current DO storage is in-memory; restarts drop live subscriptions.
249
+ 2. Matching is overlap-based and intentionally permissive for realtime invalidation.
250
+ 3. Query re-execution occurs on matching mutation events and can be tuned later for batching/debouncing.
251
+
252
+ ---
253
+
254
+ ## Safe extension points
255
+
256
+ - `registration.ts`:
257
+ - realtime routes, token/session policy, protocol envelopes
258
+ - `types.ts`:
259
+ - mutation event payload contracts
260
+ - `generators/context/context-creation.ts`:
261
+ - context-level mutation event tracking
262
+ - `utils/handler-discovery.ts`:
263
+ - query identity strategy
264
+
265
+ After template changes, regenerate and validate `_generated` output.
@@ -0,0 +1,36 @@
1
+ export function generateAuth(): string {
2
+ return `
3
+
4
+ export async function resolveSession(
5
+ \trequest: Request,
6
+ \tdatabase: D1Database,
7
+ \tkvNamespace?: KVNamespace,
8
+ \tcf?: IncomingRequestCfProperties,
9
+ ): Promise<{ user: unknown; session: unknown }> {
10
+ \tconst auth = createAuth(
11
+ \t\t{
12
+ \t\t\tDATABASE: database,
13
+ \t\t\tKV: kvNamespace,
14
+ \t\t},
15
+ \t\tcf,
16
+ \t);
17
+
18
+ \ttry {
19
+ \t\tconst session = await auth.api.getSession({
20
+ \t\t\theaders: request.headers,
21
+ \t\t});
22
+
23
+ \t\treturn {
24
+ \t\t\tuser: (session as any)?.user ?? null,
25
+ \t\t\tsession: (session as any)?.session ?? null,
26
+ \t\t};
27
+ \t} catch {
28
+ \t\treturn {
29
+ \t\t\tuser: null,
30
+ \t\t\tsession: null,
31
+ \t\t};
32
+ \t}
33
+ }
34
+
35
+ `;
36
+ }
@@ -0,0 +1,39 @@
1
+ export function generateExecution(): string {
2
+ return `
3
+ export async function executeOperation(
4
+ c: Context<WorkerEnv>,
5
+ \toperation: RegisteredOperation<ZodRawShape, unknown>,
6
+ args: unknown,
7
+ \tctx: AppflareContext,
8
+ ): Promise<Response> {
9
+ if (operation.definition.authRequired && !ctx.user) {
10
+ ctx.error(401, "Unauthorized");
11
+ }
12
+
13
+ if (operation.definition.middleware) {
14
+ await operation.definition.middleware(ctx, args as never, c.req.raw);
15
+ }
16
+
17
+ const result = await operation.definition.handler(ctx, args as never);
18
+
19
+ \treturn c.json(result, 200);
20
+ }
21
+
22
+ export function handleOperationError(
23
+ c: Context<WorkerEnv>,
24
+ error: unknown,
25
+ validationMessage: string,
26
+ ): Response {
27
+ if (error instanceof AppflareHandledError) {
28
+ return c.json(error.payload, error.status);
29
+ }
30
+
31
+ if (error instanceof ZodError) {
32
+ return c.json({ message: validationMessage, issues: error.issues }, 400);
33
+ }
34
+
35
+ return c.json({ message: (error as Error).message ?? "Unknown error" }, 500);
36
+ }
37
+
38
+ `;
39
+ }
@@ -0,0 +1,80 @@
1
+ export function generateContextCreation(defaultR2Binding?: string): string {
2
+ return `
3
+ export function createSchedulerExecutionContext(
4
+ env: Record<string, unknown>,
5
+ options: RegisterHandlersOptions,
6
+ ): AppflareContext {
7
+ const database = env[options.databaseBinding] as D1Database;
8
+ const r2Binding = options.r2Binding ?? ${JSON.stringify(defaultR2Binding ?? "")};
9
+ const storageBucket = r2Binding
10
+ ? (env[r2Binding] as R2BucketBinding | undefined)
11
+ : undefined;
12
+ const db = createDb(database);
13
+ const mutationEvents = [] as AppflareContext["mutationEvents"];
14
+ const schedulerBinding = options.schedulerBinding ?? "APPFLARE_SCHEDULER_QUEUE";
15
+ const schedulerQueue = env[schedulerBinding] as SchedulerQueueBinding | undefined;
16
+ const helpers = createContextErrorHelpers();
17
+ const ctx = {
18
+ $db: db,
19
+ db: createQueryDb(db, {
20
+ onMutation: (event) => {
21
+ mutationEvents.push(event);
22
+ },
23
+ }),
24
+ mutationEvents,
25
+ user: null as never,
26
+ session: null as never,
27
+ context: null as never,
28
+ scheduler: createScheduler(schedulerQueue),
29
+ storage: null as never,
30
+ ...helpers,
31
+ } as AppflareContext;
32
+
33
+ ctx.storage = createStorageApi(ctx, storageBucket);
34
+ return ctx;
35
+ }
36
+
37
+ export async function createExecutionContext(
38
+ c: Context<WorkerEnv>,
39
+ options: RegisterHandlersOptions,
40
+ ): Promise<AppflareContext> {
41
+ const database = c.env[options.databaseBinding] as D1Database;
42
+ const r2Binding = options.r2Binding ?? ${JSON.stringify(defaultR2Binding ?? "")};
43
+ const storageBucket = r2Binding
44
+ ? (c.env[r2Binding] as R2BucketBinding | undefined)
45
+ : undefined;
46
+ const kvNamespace = options.kvBinding
47
+ ? (c.env[options.kvBinding] as KVNamespace)
48
+ : undefined;
49
+ const db = createDb(database);
50
+ const mutationEvents = [] as AppflareContext["mutationEvents"];
51
+ const { user, session } = await resolveSession(
52
+ c.req.raw,
53
+ database,
54
+ kvNamespace,
55
+ c.req.raw.cf as IncomingRequestCfProperties | undefined,
56
+ );
57
+ const schedulerBinding = options.schedulerBinding ?? "APPFLARE_SCHEDULER_QUEUE";
58
+ const schedulerQueue = c.env[schedulerBinding] as SchedulerQueueBinding | undefined;
59
+ const helpers = createContextErrorHelpers();
60
+ const ctx = {
61
+ $db: db,
62
+ db: createQueryDb(db, {
63
+ onMutation: (event) => {
64
+ mutationEvents.push(event);
65
+ },
66
+ }),
67
+ mutationEvents,
68
+ user,
69
+ session,
70
+ context: c,
71
+ scheduler: createScheduler(schedulerQueue),
72
+ storage: null as never,
73
+ ...helpers,
74
+ } as AppflareContext;
75
+
76
+ ctx.storage = createStorageApi(ctx, storageBucket);
77
+ return ctx;
78
+ }
79
+ `;
80
+ }
@@ -0,0 +1,11 @@
1
+ export function generateErrorHelpers(): string {
2
+ return `
3
+ function createContextErrorHelpers() {
4
+ return {
5
+ error: (status: number, message: string, details?: unknown) => {
6
+ throw new AppflareHandledError(status, { message, details });
7
+ },
8
+ };
9
+ }
10
+ `;
11
+ }
@@ -0,0 +1,24 @@
1
+ export function generateSchedulerFunctions(): string {
2
+ return `
3
+ export function createScheduler(
4
+ queue?: SchedulerQueueBinding,
5
+ ): Scheduler {
6
+ return {
7
+ enqueue: async (task, ...args) => {
8
+ const [payload, options] = args as [unknown, SchedulerEnqueueOptions | undefined];
9
+ if (!queue) {
10
+ throw new Error("Scheduler queue binding is not configured");
11
+ }
12
+
13
+ await queue.send(
14
+ {
15
+ task,
16
+ payload,
17
+ },
18
+ options,
19
+ );
20
+ },
21
+ };
22
+ }
23
+ `;
24
+ }
@@ -0,0 +1,112 @@
1
+ export function generateStorageApi(): string {
2
+ return `
3
+ function createStorageApi(
4
+ ctx: AppflareContext,
5
+ bucket: R2BucketBinding | undefined,
6
+ ): AppflareStorage {
7
+ const assertAuthorized = async (args: StorageAuthorizationArgs): Promise<void> => {
8
+ const allowed = await isStorageAllowed(ctx, args);
9
+ if (!allowed) {
10
+ ctx.error(403, "Storage access denied", {
11
+ path: args.path,
12
+ method: args.method,
13
+ });
14
+ }
15
+ };
16
+
17
+ const requireBucket = (): R2BucketBinding => {
18
+ if (!bucket) {
19
+ throw new Error(
20
+ "R2 binding is not configured. Set r2 in appflare.config.ts and regenerate artifacts.",
21
+ );
22
+ }
23
+ return bucket;
24
+ };
25
+
26
+ return {
27
+ put: async (args) => {
28
+ const path = normalizeStoragePath(args.path);
29
+ await assertAuthorized({
30
+ path: "/" + path,
31
+ method: "put",
32
+ contentType: args.contentType,
33
+ });
34
+
35
+ return requireBucket().put(path, args.body, {
36
+ httpMetadata: {
37
+ ...(args.httpMetadata ?? {}),
38
+ ...(args.contentType ? { contentType: args.contentType } : {}),
39
+ },
40
+ customMetadata: args.customMetadata,
41
+ });
42
+ },
43
+ get: async (args) => {
44
+ const path = normalizeStoragePath(args.path);
45
+ const method = normalizeStorageMethod(args.method);
46
+ await assertAuthorized({
47
+ path: "/" + path,
48
+ method,
49
+ });
50
+
51
+ return requireBucket().get(path, {
52
+ onlyIf: args.onlyIf,
53
+ range: args.range,
54
+ });
55
+ },
56
+ delete: async (args) => {
57
+ const path = normalizeStoragePath(args.path);
58
+ await assertAuthorized({
59
+ path: "/" + path,
60
+ method: "delete",
61
+ });
62
+
63
+ await requireBucket().delete(path);
64
+ },
65
+ list: async (args = {}) => {
66
+ await assertAuthorized({
67
+ path: "/" + (args.prefix ?? ""),
68
+ method: args.method ?? "list",
69
+ });
70
+
71
+ return requireBucket().list({
72
+ prefix: args.prefix,
73
+ cursor: args.cursor,
74
+ limit: args.limit,
75
+ delimiter: args.delimiter,
76
+ include: args.include,
77
+ });
78
+ },
79
+ signedUrl: async (args) => {
80
+ const path = normalizeStoragePath(args.path);
81
+ const requestMethod = args.method ?? "GET";
82
+ const method: StorageMethod =
83
+ requestMethod === "PUT"
84
+ ? "put"
85
+ : requestMethod === "DELETE"
86
+ ? "delete"
87
+ : args.downloadAsAttachment === false
88
+ ? "preview"
89
+ : "download";
90
+
91
+ await assertAuthorized({
92
+ path: "/" + path,
93
+ method,
94
+ contentType: args.contentType,
95
+ });
96
+
97
+ const currentBucket = requireBucket();
98
+ if (typeof currentBucket.createPresignedUrl !== "function") {
99
+ throw new Error("R2 createPresignedUrl is unavailable for this runtime binding");
100
+ }
101
+
102
+ const signedRequest = buildSignedRequest(args, path);
103
+ const signedUrl = await currentBucket.createPresignedUrl(signedRequest, {
104
+ expiresIn: args.expiresIn ?? 60 * 5,
105
+ });
106
+
107
+ return signedUrl.toString();
108
+ },
109
+ };
110
+ }
111
+ `;
112
+ }
@@ -0,0 +1,59 @@
1
+ export function generateStorageHelpers(): string {
2
+ return `
3
+ function normalizeStoragePath(path: string): string {
4
+ const trimmed = path.trim();
5
+ if (trimmed.length === 0) {
6
+ throw new Error("Storage path is required");
7
+ }
8
+
9
+ const withoutLeadingSlash = trimmed.replace(/^\\/+/, "");
10
+ if (withoutLeadingSlash.length === 0) {
11
+ throw new Error("Storage path is required");
12
+ }
13
+
14
+ return withoutLeadingSlash;
15
+ }
16
+
17
+ function normalizeStorageMethod(method: StorageMethod | undefined): StorageMethod {
18
+ return method ?? "get";
19
+ }
20
+
21
+ function sanitizeSignedFileName(fileName: string | undefined): string | null {
22
+ if (!fileName) {
23
+ return null;
24
+ }
25
+
26
+ const normalized = fileName.replace(/[\\r\\n\\"]/g, "").trim();
27
+ return normalized.length > 0 ? normalized : null;
28
+ }
29
+
30
+ function buildSignedRequest(
31
+ args: StorageSignedUrlArgs,
32
+ path: string,
33
+ ): Request {
34
+ const method = args.method ?? "GET";
35
+ const endpoint = new URL("https://r2.appflare.local/" + encodeURI(path));
36
+ const headers = new Headers();
37
+
38
+ if (args.contentType) {
39
+ headers.set("content-type", args.contentType);
40
+ }
41
+
42
+ if (method === "GET") {
43
+ const disposition = args.downloadAsAttachment === false ? "inline" : "attachment";
44
+ const fileName = sanitizeSignedFileName(args.fileName);
45
+ headers.set(
46
+ "response-content-disposition",
47
+ fileName
48
+ ? disposition + '; filename="' + fileName + '"'
49
+ : disposition,
50
+ );
51
+ }
52
+
53
+ return new Request(endpoint.toString(), {
54
+ method,
55
+ headers,
56
+ });
57
+ }
58
+ `;
59
+ }
@@ -0,0 +1,18 @@
1
+ export function generateContextTypes(): string {
2
+ return `
3
+ type SchedulerQueueBinding = {
4
+ send: (body: unknown, options?: SchedulerEnqueueOptions) => Promise<void>;
5
+ };
6
+
7
+ type R2BucketBinding = {
8
+ put: (key: string, value: unknown, options?: Record<string, unknown>) => Promise<unknown>;
9
+ get: (key: string, options?: Record<string, unknown>) => Promise<unknown | null>;
10
+ delete: (key: string | string[]) => Promise<void>;
11
+ list: (options?: Record<string, unknown>) => Promise<unknown>;
12
+ createPresignedUrl?: (
13
+ request: Request,
14
+ options?: { expiresIn?: number },
15
+ ) => Promise<URL>;
16
+ };
17
+ `;
18
+ }
@@ -0,0 +1,43 @@
1
+ import { generateAuth } from "../auth";
2
+ import { generateContextTypes } from "./context/types";
3
+ import { generateSchedulerFunctions } from "./context/scheduler";
4
+ import { generateErrorHelpers } from "./context/error-helpers";
5
+ import { generateStorageHelpers } from "./context/storage-helpers";
6
+ import { generateStorageApi } from "./context/storage-api";
7
+ import { generateContextCreation } from "./context/context-creation";
8
+
9
+ export function generateContextSource(defaultR2Binding?: string): string {
10
+ return `import type { Context } from "hono";
11
+ import type { D1Database, IncomingRequestCfProperties, KVNamespace } from "@cloudflare/workers-types";
12
+ import { createAuth } from "./auth.config";
13
+ import {
14
+ type AppflareContext,
15
+ type AppflareStorage,
16
+ AppflareHandledError,
17
+ type Scheduler,
18
+ type SchedulerEnqueueOptions,
19
+ type RegisterHandlersOptions,
20
+ type StorageAuthorizationArgs,
21
+ type StorageMethod,
22
+ type StorageSignedUrlArgs,
23
+ type WorkerEnv,
24
+ isStorageAllowed,
25
+ createDb,
26
+ createQueryDb,
27
+ } from "./handlers";
28
+
29
+ ${generateContextTypes()}
30
+
31
+ ${generateAuth()}
32
+
33
+ ${generateSchedulerFunctions()}
34
+
35
+ ${generateErrorHelpers()}
36
+
37
+ ${generateStorageHelpers()}
38
+
39
+ ${generateStorageApi()}
40
+
41
+ ${generateContextCreation(defaultR2Binding)}
42
+ `;
43
+ }
@@ -0,0 +1,15 @@
1
+ import { generateExecution } from "../execution";
2
+
3
+ export function generateExecutionSource(): string {
4
+ return `import type { Context } from "hono";
5
+ import { ZodError, type ZodRawShape } from "zod";
6
+ import {
7
+ type AppflareContext,
8
+ AppflareHandledError,
9
+ type RegisteredOperation,
10
+ type WorkerEnv,
11
+ } from "./handlers";
12
+
13
+ ${generateExecution()}
14
+ `;
15
+ }