appflare 0.1.12 → 0.2.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.
- package/Documentation.md +735 -0
- package/cli/generate.ts +1 -1
- package/cli/templates/auth/README.md +3 -3
- package/cli/templates/auth/route-config.ts +1 -18
- package/cli/templates/auth/route-handler.ts +1 -18
- package/cli/templates/auth/route-request-utils.ts +2 -52
- package/cli/templates/auth/route.config.ts +18 -0
- package/cli/templates/auth/route.handler.ts +18 -0
- package/cli/templates/auth/route.request-utils.ts +55 -0
- package/cli/templates/auth/route.ts +2 -2
- package/cli/templates/core/README.md +2 -2
- package/cli/templates/core/client/appflare.ts +78 -3
- package/cli/templates/core/client/handlers.ts +1 -0
- package/cli/templates/core/client/index.ts +1 -0
- package/cli/templates/core/client/storage.ts +85 -5
- package/cli/templates/core/client/types.ts +91 -0
- package/cli/templates/core/client-modules/appflare.ts +1 -112
- package/cli/templates/core/client-modules/handlers.ts +1 -1
- package/cli/templates/core/client-modules/index.ts +1 -7
- package/cli/templates/core/client-modules/storage.ts +1 -180
- package/cli/templates/core/client-modules/types.ts +1 -145
- package/cli/templates/core/client.artifacts.ts +39 -0
- package/cli/templates/core/client.ts +4 -39
- package/cli/templates/core/server.ts +1 -1
- package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +2 -5
- package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +4 -11
- package/cli/templates/handlers/generators/types/query-definitions.ts +74 -18
- package/cli/templates/handlers/generators/types/query-runtime.ts +20 -4
- package/package.json +1 -1
- /package/cli/templates/core/{client-modules → client}/handlers/index.ts +0 -0
- /package/cli/templates/core/{handlers-route.ts → handlers.route.ts} +0 -0
package/Documentation.md
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# Appflare Documentation
|
|
2
|
+
|
|
3
|
+
This guide explains how to build backend handlers, run schema and database migrations, and consume your generated Appflare client in frontend apps (both plain TypeScript/JavaScript and React).
|
|
4
|
+
|
|
5
|
+
All examples are aligned with the current workspace structure and APIs.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1) How Appflare works (high level)
|
|
10
|
+
|
|
11
|
+
Appflare follows a generate-first workflow:
|
|
12
|
+
|
|
13
|
+
1. You write backend schema and handlers in your backend package.
|
|
14
|
+
2. You run the Appflare CLI.
|
|
15
|
+
3. Appflare generates runtime artifacts under your backend out directory (for example `_generated`).
|
|
16
|
+
4. Frontend imports the generated client and calls typed query/mutation routes.
|
|
17
|
+
|
|
18
|
+
In this workspace, the backend uses:
|
|
19
|
+
|
|
20
|
+
- `packages/backend/appflare.config.ts`
|
|
21
|
+
- `packages/backend/schema.ts`
|
|
22
|
+
- `packages/backend/src/**` for handlers
|
|
23
|
+
- Generated output in `packages/backend/_generated/**`
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2) Required backend files
|
|
28
|
+
|
|
29
|
+
### 2.1 Appflare config
|
|
30
|
+
|
|
31
|
+
Your main config is `packages/backend/appflare.config.ts`.
|
|
32
|
+
|
|
33
|
+
Important fields:
|
|
34
|
+
|
|
35
|
+
- `scanDir`: where handlers are discovered (currently `./src`)
|
|
36
|
+
- `outDir`: where generated artifacts are written (currently `./_generated`)
|
|
37
|
+
- `schemaDsl.entry`: schema entry file (currently `./schema.ts`)
|
|
38
|
+
- `schema`: schema files used by drizzle generation
|
|
39
|
+
- `database`, `kv`, `r2`, `auth`, `scheduler`: runtime binding and feature configuration
|
|
40
|
+
- `wranglerOverrides`: final Worker deployment settings
|
|
41
|
+
|
|
42
|
+
### 2.2 Schema
|
|
43
|
+
|
|
44
|
+
Schema is defined with `schema`, `table`, and `v` from Appflare.
|
|
45
|
+
|
|
46
|
+
Example source: `packages/backend/schema.ts`.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3) How to create handlers
|
|
51
|
+
|
|
52
|
+
Handlers are created with generated helpers:
|
|
53
|
+
|
|
54
|
+
- `query(...)` for read endpoints
|
|
55
|
+
- `mutation(...)` for write endpoints
|
|
56
|
+
|
|
57
|
+
Import them from your generated handlers module:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { query, mutation } from "../_generated/handlers";
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3.1 Query handler example
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { query } from "../../_generated/handlers";
|
|
67
|
+
import * as z from "zod";
|
|
68
|
+
|
|
69
|
+
export const getUserProfile = query({
|
|
70
|
+
args: {
|
|
71
|
+
userId: z.string(),
|
|
72
|
+
},
|
|
73
|
+
handler: async (ctx, args) => {
|
|
74
|
+
const user = await ctx.db.users.findFirst({
|
|
75
|
+
where: { id: args.userId },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!user) {
|
|
79
|
+
ctx.error(404, "User not found", { userId: args.userId });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return user;
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3.1.1 More query examples (basic to advanced)
|
|
88
|
+
|
|
89
|
+
#### A) Search + relation include + limit
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { query } from "../../_generated/handlers";
|
|
93
|
+
import * as z from "zod";
|
|
94
|
+
|
|
95
|
+
export const searchPosts = query({
|
|
96
|
+
args: {
|
|
97
|
+
search: z.string().optional(),
|
|
98
|
+
ownerId: z.string().optional(),
|
|
99
|
+
limit: z.number().int().min(1).max(100).default(25),
|
|
100
|
+
},
|
|
101
|
+
handler: async (ctx, args) => {
|
|
102
|
+
return ctx.db.posts.findMany({
|
|
103
|
+
where: {
|
|
104
|
+
title: {
|
|
105
|
+
regex: args.search ?? "",
|
|
106
|
+
$options: "i",
|
|
107
|
+
},
|
|
108
|
+
...(args.ownerId ? { ownerId: args.ownerId } : {}),
|
|
109
|
+
},
|
|
110
|
+
with: {
|
|
111
|
+
owner: true,
|
|
112
|
+
comments: true,
|
|
113
|
+
},
|
|
114
|
+
limit: args.limit,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### B) Cursor/pagination-style query
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { query } from "../../_generated/handlers";
|
|
124
|
+
import * as z from "zod";
|
|
125
|
+
|
|
126
|
+
export const listPostsPage = query({
|
|
127
|
+
args: {
|
|
128
|
+
cursor: z.number().int().optional(),
|
|
129
|
+
pageSize: z.number().int().min(1).max(50).default(20),
|
|
130
|
+
},
|
|
131
|
+
handler: async (ctx, args) => {
|
|
132
|
+
const rows = await ctx.db.posts.findMany({
|
|
133
|
+
where: args.cursor
|
|
134
|
+
? {
|
|
135
|
+
id: {
|
|
136
|
+
gt: args.cursor,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
: {},
|
|
140
|
+
limit: args.pageSize,
|
|
141
|
+
with: {
|
|
142
|
+
owner: true,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const nextCursor = rows.length > 0 ? rows[rows.length - 1]?.id : undefined;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
rows,
|
|
150
|
+
nextCursor,
|
|
151
|
+
hasMore: rows.length === args.pageSize,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### C) Aggregate-heavy query (`count`, `avg`, relation path)
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { query } from "../../_generated/handlers";
|
|
161
|
+
import * as z from "zod";
|
|
162
|
+
|
|
163
|
+
export const getPostStats = query({
|
|
164
|
+
args: {
|
|
165
|
+
ownerId: z.string().optional(),
|
|
166
|
+
},
|
|
167
|
+
handler: async (ctx, args) => {
|
|
168
|
+
const totalPosts = await ctx.db.posts.count({
|
|
169
|
+
where: args.ownerId ? { ownerId: args.ownerId } : {},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const uniqueOwners = await ctx.db.posts.count({
|
|
173
|
+
field: "ownerId",
|
|
174
|
+
distinct: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const averagePostId = await ctx.db.posts.avg({
|
|
178
|
+
field: "id",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const averageCommentId = await ctx.db.posts.avg({
|
|
182
|
+
field: "comments.id",
|
|
183
|
+
with: {
|
|
184
|
+
comments: {
|
|
185
|
+
where: {
|
|
186
|
+
id: {
|
|
187
|
+
gte: 10000,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
totalPosts,
|
|
196
|
+
uniqueOwners,
|
|
197
|
+
averagePostId,
|
|
198
|
+
averageCommentId,
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### D) Geo query (`geoWithin`) + filter composition
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { query } from "../../_generated/handlers";
|
|
208
|
+
import * as z from "zod";
|
|
209
|
+
|
|
210
|
+
export const nearbyPlaygroundItems = query({
|
|
211
|
+
args: {
|
|
212
|
+
latitude: z.number(),
|
|
213
|
+
longitude: z.number(),
|
|
214
|
+
radiusMeters: z.number().positive().default(5000),
|
|
215
|
+
},
|
|
216
|
+
handler: async (ctx, args) => {
|
|
217
|
+
const rows = await ctx.db.queryPlayground.findMany({
|
|
218
|
+
where: {
|
|
219
|
+
geoWithin: {
|
|
220
|
+
$geometry: {
|
|
221
|
+
latitude: args.latitude,
|
|
222
|
+
longitude: args.longitude,
|
|
223
|
+
},
|
|
224
|
+
latitudeField: "latitude",
|
|
225
|
+
longitudeField: "longitude",
|
|
226
|
+
$gte: 0,
|
|
227
|
+
$lt: args.radiusMeters,
|
|
228
|
+
},
|
|
229
|
+
isActive: {
|
|
230
|
+
eq: true,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
limit: 100,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
count: rows.length,
|
|
238
|
+
rows,
|
|
239
|
+
};
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### E) Complex production-style query (similar to `db-features`)
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { query } from "../../_generated/handlers";
|
|
248
|
+
import * as z from "zod";
|
|
249
|
+
|
|
250
|
+
export const queryDashboardData = query({
|
|
251
|
+
args: {
|
|
252
|
+
userId: z.string().optional(),
|
|
253
|
+
search: z.string().optional(),
|
|
254
|
+
},
|
|
255
|
+
handler: async (ctx, args) => {
|
|
256
|
+
const posts = await ctx.db.posts.findMany({
|
|
257
|
+
where: {
|
|
258
|
+
ownerId: args.userId,
|
|
259
|
+
title: {
|
|
260
|
+
regex: args.search ?? "test",
|
|
261
|
+
$options: "i",
|
|
262
|
+
},
|
|
263
|
+
id: { gt: 0 },
|
|
264
|
+
},
|
|
265
|
+
with: {
|
|
266
|
+
comments: true,
|
|
267
|
+
owner: true,
|
|
268
|
+
},
|
|
269
|
+
limit: 25,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const postsWithCommentStats = await ctx.db.posts.findMany({
|
|
273
|
+
with: {
|
|
274
|
+
comments: {
|
|
275
|
+
_count: true,
|
|
276
|
+
_avg: { id: true },
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
limit: 10,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const postsTotal = await ctx.db.posts.count({
|
|
283
|
+
where: { id: { $gte: 1 } },
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
posts,
|
|
288
|
+
postsTotal,
|
|
289
|
+
postsWithCommentStats,
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 3.1.2 Query design tips for complex handlers
|
|
296
|
+
|
|
297
|
+
- Keep args schema strict (defaults, min/max, optional fields).
|
|
298
|
+
- Return stable shapes (avoid switching response shape by condition).
|
|
299
|
+
- Start with one root query and compose aggregates/relations progressively.
|
|
300
|
+
- Prefer server-side filtering in `where` instead of filtering on frontend.
|
|
301
|
+
- For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
|
|
302
|
+
|
|
303
|
+
### 3.2 Mutation handler example
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
import { mutation } from "../../_generated/handlers";
|
|
307
|
+
import * as z from "zod";
|
|
308
|
+
|
|
309
|
+
export const createPost = mutation({
|
|
310
|
+
args: {
|
|
311
|
+
title: z.string().min(1),
|
|
312
|
+
slug: z.string().min(1),
|
|
313
|
+
},
|
|
314
|
+
handler: async (ctx, args) => {
|
|
315
|
+
const inserted = await ctx.db.posts.insert({
|
|
316
|
+
values: {
|
|
317
|
+
title: args.title,
|
|
318
|
+
slug: args.slug,
|
|
319
|
+
ownerId: "some-user-id",
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return { created: inserted.length };
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### 3.3 Handler file placement
|
|
329
|
+
|
|
330
|
+
Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
|
|
331
|
+
|
|
332
|
+
- `packages/backend/src/test.ts`
|
|
333
|
+
- `packages/backend/src/queries/db-features.ts`
|
|
334
|
+
- `packages/backend/src/mutations/db-features.ts`
|
|
335
|
+
- `packages/backend/src/bun/test.ts`
|
|
336
|
+
|
|
337
|
+
Generated client route names follow directory + file + export naming. For example:
|
|
338
|
+
|
|
339
|
+
- query from `src/test.ts` export `getTest` becomes `appflare.queries.test.getTest`
|
|
340
|
+
- mutation from `src/mutations/db-features.ts` export `testMutationFeatures` becomes `appflare.mutations["db-features"].testMutationFeatures`
|
|
341
|
+
|
|
342
|
+
### 3.4 Context utilities available in handlers
|
|
343
|
+
|
|
344
|
+
Inside handlers, you commonly use:
|
|
345
|
+
|
|
346
|
+
- `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
|
|
347
|
+
- aggregate helpers like `count` and `avg`
|
|
348
|
+
- `ctx.error(status, message, details)` for typed failures
|
|
349
|
+
|
|
350
|
+
See real examples in:
|
|
351
|
+
|
|
352
|
+
- `packages/backend/src/queries/db-features.ts`
|
|
353
|
+
- `packages/backend/src/mutations/db-features.ts`
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 4) Generate artifacts
|
|
358
|
+
|
|
359
|
+
From backend package:
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
cd packages/backend
|
|
363
|
+
bun ../appflare/cli dev
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Or via scripts in `packages/backend/package.json`:
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
bun run build
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
What gets generated (core set):
|
|
373
|
+
|
|
374
|
+
- `_generated/server.ts`
|
|
375
|
+
- `_generated/client.ts`
|
|
376
|
+
- `_generated/auth.config.ts`
|
|
377
|
+
- `_generated/drizzle.config.ts`
|
|
378
|
+
- `_generated/handlers.ts`
|
|
379
|
+
- `_generated/handlers.context.ts`
|
|
380
|
+
- `_generated/handlers.execution.ts`
|
|
381
|
+
- `_generated/handlers.routes.ts`
|
|
382
|
+
- `_generated/client/**`
|
|
383
|
+
|
|
384
|
+
### Watch mode
|
|
385
|
+
|
|
386
|
+
To regenerate on file changes:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
cd packages/backend
|
|
390
|
+
bun ../appflare/cli dev --watch
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## 5) How to migrate database schema
|
|
396
|
+
|
|
397
|
+
Appflare migration flow wraps two steps:
|
|
398
|
+
|
|
399
|
+
1. Generate drizzle migrations
|
|
400
|
+
2. Apply to D1 via Wrangler
|
|
401
|
+
|
|
402
|
+
### 5.1 Standard migrate
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
cd packages/backend
|
|
406
|
+
bun ../appflare/cli migrate
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Or script:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
bun run migrate
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 5.2 Choose target environment
|
|
416
|
+
|
|
417
|
+
Use exactly one of these flags:
|
|
418
|
+
|
|
419
|
+
- `--local`
|
|
420
|
+
- `--remote`
|
|
421
|
+
- `--preview`
|
|
422
|
+
|
|
423
|
+
Examples:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
bun ../appflare/cli migrate --local
|
|
427
|
+
bun ../appflare/cli migrate --remote
|
|
428
|
+
bun ../appflare/cli migrate --preview
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 5.3 Typical change workflow
|
|
432
|
+
|
|
433
|
+
1. Update `schema.ts`.
|
|
434
|
+
2. Regenerate artifacts:
|
|
435
|
+
- `bun ../appflare/cli dev`
|
|
436
|
+
3. Run migration:
|
|
437
|
+
- `bun ../appflare/cli migrate --local` (or remote/preview)
|
|
438
|
+
4. Verify app behavior in `wrangler dev`.
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## 6) Frontend usage (plain TypeScript/JavaScript)
|
|
443
|
+
|
|
444
|
+
Use the generated backend client directly.
|
|
445
|
+
|
|
446
|
+
### 6.1 Create client instance
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { Appflare } from "appflare-backend/_generated/client";
|
|
450
|
+
|
|
451
|
+
const appflare = new Appflare({
|
|
452
|
+
endpoint: "http://127.0.0.1:8787",
|
|
453
|
+
wsEndpoint: "ws://127.0.0.1:8787",
|
|
454
|
+
onGetAuthToken: async () => localStorage.getItem("appflare-auth-token") ?? "",
|
|
455
|
+
onSetAuthToken: async (token) => {
|
|
456
|
+
localStorage.setItem("appflare-auth-token", token);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 6.2 Run a query
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
const result = await appflare.queries.test.getTest.run({ id: "test" });
|
|
465
|
+
|
|
466
|
+
if (result.error) {
|
|
467
|
+
console.error(result.error.status, result.error.message);
|
|
468
|
+
} else {
|
|
469
|
+
console.log(result.data);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### 6.2.1 More frontend query call examples
|
|
474
|
+
|
|
475
|
+
#### A) Query with filters
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
const result = await appflare.queries["db-features"].testQueryFeatures.run({
|
|
479
|
+
search: "test",
|
|
480
|
+
userId: "as3xNgfPVzrooSuSwn1ZSEKNA92Cjp4V",
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (!result.error) {
|
|
484
|
+
console.log(result.data.postsCount, result.data.uniqueOwnerCount);
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### B) Query with request options
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
const result = await appflare.queries.test.getTest.run(
|
|
492
|
+
{ id: "test" },
|
|
493
|
+
{
|
|
494
|
+
headers: {
|
|
495
|
+
"x-trace-id": crypto.randomUUID(),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
#### C) Query with realtime and explicit auth token
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
const sub = appflare.queries.test.getTest.subscribe({
|
|
505
|
+
args: { id: "test" },
|
|
506
|
+
authToken: "token-from-auth-flow",
|
|
507
|
+
onChange: (data) => {
|
|
508
|
+
console.log("fresh data", data);
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
setTimeout(() => sub.remove(), 30000);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### 6.3 Run a mutation
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const result = await appflare.mutations.test.newTest.run({});
|
|
519
|
+
|
|
520
|
+
if (result.error) {
|
|
521
|
+
console.error(result.error.message);
|
|
522
|
+
} else {
|
|
523
|
+
console.log(result.data);
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### 6.4 Realtime subscribe to a query
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
const sub = appflare.queries.test.getTest.subscribe({
|
|
531
|
+
args: { id: "test" },
|
|
532
|
+
onChange: (data, event) => {
|
|
533
|
+
console.log("update", event.payload.queryName, data);
|
|
534
|
+
},
|
|
535
|
+
onError: (error) => {
|
|
536
|
+
console.error("subscription error", error);
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// later
|
|
541
|
+
sub.remove();
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## 7) How to use with React
|
|
547
|
+
|
|
548
|
+
Appflare ships React hooks in `appflare/react`:
|
|
549
|
+
|
|
550
|
+
- `useQuery`
|
|
551
|
+
- `useInfiniteQuery`
|
|
552
|
+
- `useMutation`
|
|
553
|
+
|
|
554
|
+
These are thin wrappers around TanStack Query.
|
|
555
|
+
|
|
556
|
+
### 7.1 Setup requirements
|
|
557
|
+
|
|
558
|
+
Install peer requirements in your frontend app:
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
bun add @tanstack/react-query react
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Wrap app with `QueryClientProvider`.
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
568
|
+
|
|
569
|
+
const queryClient = new QueryClient();
|
|
570
|
+
|
|
571
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
572
|
+
return (
|
|
573
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### 7.2 React query usage
|
|
579
|
+
|
|
580
|
+
```tsx
|
|
581
|
+
import { useQuery } from "appflare/react";
|
|
582
|
+
import { appflare } from "./appflare-client";
|
|
583
|
+
|
|
584
|
+
export function TestScreen() {
|
|
585
|
+
const query = useQuery(
|
|
586
|
+
appflare.queries.test.getTest,
|
|
587
|
+
{ id: "test" },
|
|
588
|
+
{
|
|
589
|
+
realtime: { enabled: true },
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
if (query.isLoading) return <div>Loading...</div>;
|
|
594
|
+
if (query.error) return <div>{query.error.message}</div>;
|
|
595
|
+
|
|
596
|
+
return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 7.3 React mutation usage
|
|
601
|
+
|
|
602
|
+
```tsx
|
|
603
|
+
import { useMutation } from "appflare/react";
|
|
604
|
+
import { appflare } from "./appflare-client";
|
|
605
|
+
|
|
606
|
+
export function CreatePostButton() {
|
|
607
|
+
const mutation = useMutation(
|
|
608
|
+
appflare.mutations.test.newTest,
|
|
609
|
+
{},
|
|
610
|
+
{
|
|
611
|
+
onSuccess: (data) => console.log("created", data),
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
return (
|
|
616
|
+
<button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
|
617
|
+
Create
|
|
618
|
+
</button>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 7.4 React infinite query usage
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import { useInfiniteQuery } from "appflare/react";
|
|
627
|
+
import { appflare } from "./appflare-client";
|
|
628
|
+
|
|
629
|
+
const result = useInfiniteQuery(
|
|
630
|
+
appflare.queries.test.getTest,
|
|
631
|
+
{ id: "test" },
|
|
632
|
+
{
|
|
633
|
+
pageParamToArgs: (baseArgs, page) => ({ ...baseArgs, page }),
|
|
634
|
+
queryOptions: {
|
|
635
|
+
initialPageParam: 1,
|
|
636
|
+
getNextPageParam: (lastPage, pages) => pages.length + 1,
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### 7.5 Realtime with React hooks
|
|
643
|
+
|
|
644
|
+
Both `useQuery` and `useInfiniteQuery` support:
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
realtime: {
|
|
648
|
+
enabled: true,
|
|
649
|
+
authToken: "optional-token",
|
|
650
|
+
requestOptions: { headers: { "x-custom": "1" } },
|
|
651
|
+
onChange: (data, update) => {},
|
|
652
|
+
onError: (error) => {},
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
When enabled, hooks subscribe via generated query `.subscribe(...)` and keep query cache updated automatically.
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## 8) Frontend app helper pattern
|
|
661
|
+
|
|
662
|
+
A good pattern is to keep one shared client factory in a single file.
|
|
663
|
+
|
|
664
|
+
Example in this workspace:
|
|
665
|
+
|
|
666
|
+
- `apps/app/lib/appflare.ts`
|
|
667
|
+
|
|
668
|
+
This file centralizes:
|
|
669
|
+
|
|
670
|
+
- endpoint/wsEndpoint selection (web/mobile)
|
|
671
|
+
- token storage and retrieval
|
|
672
|
+
- exported hook wrappers
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## 9) Common commands cheat sheet
|
|
677
|
+
|
|
678
|
+
From `packages/backend`:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Generate once
|
|
682
|
+
bun ../appflare/cli build -c appflare.config.ts
|
|
683
|
+
|
|
684
|
+
# Generate in dev mode
|
|
685
|
+
bun ../appflare/cli dev -c appflare.config.ts
|
|
686
|
+
|
|
687
|
+
# Generate + watch
|
|
688
|
+
bun ../appflare/cli dev -c appflare.config.ts --watch
|
|
689
|
+
|
|
690
|
+
# Migrate local D1
|
|
691
|
+
bun ../appflare/cli migrate -c appflare.config.ts --local
|
|
692
|
+
|
|
693
|
+
# Migrate remote D1
|
|
694
|
+
bun ../appflare/cli migrate -c appflare.config.ts --remote
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
Or use backend scripts:
|
|
698
|
+
|
|
699
|
+
```bash
|
|
700
|
+
bun run build
|
|
701
|
+
bun run dev
|
|
702
|
+
bun run migrate
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## 10) Troubleshooting
|
|
708
|
+
|
|
709
|
+
### Generated client has missing routes
|
|
710
|
+
|
|
711
|
+
- Ensure handler file is under `scanDir`.
|
|
712
|
+
- Ensure export uses `query(...)` or `mutation(...)`.
|
|
713
|
+
- Run `bun ../appflare/cli dev` again.
|
|
714
|
+
|
|
715
|
+
### Realtime not receiving updates
|
|
716
|
+
|
|
717
|
+
- Ensure query has `.subscribe` in generated client.
|
|
718
|
+
- Ensure `wsEndpoint` is set correctly.
|
|
719
|
+
- Ensure valid auth token is available if your runtime requires auth.
|
|
720
|
+
|
|
721
|
+
### Migration fails
|
|
722
|
+
|
|
723
|
+
- Confirm database values in `appflare.config.ts`.
|
|
724
|
+
- Use one environment flag only (`--local`, `--remote`, or `--preview`).
|
|
725
|
+
- Re-run generation before migration if schema changed.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## 11) Recommended development loop
|
|
730
|
+
|
|
731
|
+
1. Edit schema and handlers.
|
|
732
|
+
2. Run generator (`dev` or `dev --watch`).
|
|
733
|
+
3. Run migrations.
|
|
734
|
+
4. Start backend (`wrangler dev`).
|
|
735
|
+
5. Use generated client in frontend and iterate.
|