appflare 0.2.47 → 0.2.48
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 +898 -898
- package/cli/commands/index.ts +247 -247
- package/cli/generate.ts +360 -360
- package/cli/index.ts +120 -120
- package/cli/load-config.ts +184 -184
- package/cli/schema-compiler.ts +1366 -1366
- package/cli/templates/auth/README.md +156 -156
- package/cli/templates/auth/config.ts +61 -61
- package/cli/templates/auth/route-config.ts +1 -1
- package/cli/templates/auth/route-handler.ts +1 -1
- package/cli/templates/auth/route-request-utils.ts +5 -5
- package/cli/templates/auth/route.config.ts +18 -18
- package/cli/templates/auth/route.handler.ts +18 -18
- package/cli/templates/auth/route.request-utils.ts +55 -55
- package/cli/templates/auth/route.ts +14 -14
- package/cli/templates/core/README.md +266 -266
- package/cli/templates/core/app-creation.ts +19 -19
- package/cli/templates/core/client/appflare.ts +112 -112
- package/cli/templates/core/client/handlers/index.ts +763 -763
- package/cli/templates/core/client/handlers.ts +1 -1
- package/cli/templates/core/client/index.ts +7 -7
- package/cli/templates/core/client/storage.ts +195 -195
- package/cli/templates/core/client/types.ts +187 -187
- package/cli/templates/core/client-modules/appflare.ts +1 -1
- package/cli/templates/core/client-modules/handlers.ts +1 -1
- package/cli/templates/core/client-modules/index.ts +1 -1
- package/cli/templates/core/client-modules/storage.ts +1 -1
- package/cli/templates/core/client-modules/types.ts +1 -1
- package/cli/templates/core/client.artifacts.ts +39 -39
- package/cli/templates/core/client.ts +4 -4
- package/cli/templates/core/drizzle.ts +15 -15
- package/cli/templates/core/export.ts +14 -14
- package/cli/templates/core/handlers.route.ts +24 -24
- package/cli/templates/core/handlers.ts +1 -1
- package/cli/templates/core/imports.ts +9 -9
- package/cli/templates/core/server.ts +38 -38
- package/cli/templates/core/types.ts +6 -6
- package/cli/templates/core/wrangler.ts +109 -109
- package/cli/templates/dashboard/builders/functions/index.ts +17 -17
- package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
- package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
- package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +271 -271
- package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
- package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +703 -703
- package/cli/templates/dashboard/builders/functions/tree-builder.ts +47 -47
- package/cli/templates/dashboard/builders/navigation.ts +155 -155
- package/cli/templates/dashboard/builders/storage/index.ts +13 -13
- package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
- package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
- package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
- package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
- package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
- package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
- package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
- package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
- package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
- package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
- package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
- package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
- package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
- package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
- package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
- package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
- package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
- package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
- package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
- package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
- package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
- package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
- package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
- package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
- package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
- package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
- package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
- package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
- package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
- package/cli/templates/dashboard/components/layout.ts +420 -420
- package/cli/templates/dashboard/components/login-page.ts +65 -65
- package/cli/templates/dashboard/index.ts +61 -61
- package/cli/templates/dashboard/types.ts +9 -9
- package/cli/templates/handlers/README.md +353 -353
- package/cli/templates/handlers/auth.ts +37 -37
- package/cli/templates/handlers/execution.ts +42 -42
- package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
- package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
- package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
- package/cli/templates/handlers/generators/context/storage-api.ts +82 -82
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
- package/cli/templates/handlers/generators/context/types.ts +40 -40
- package/cli/templates/handlers/generators/context.ts +43 -43
- package/cli/templates/handlers/generators/execution.ts +15 -15
- package/cli/templates/handlers/generators/handlers.ts +14 -14
- package/cli/templates/handlers/generators/registration/modules/cron.ts +35 -35
- package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
- package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
- package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
- package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
- package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
- package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
- package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +510 -510
- package/cli/templates/handlers/generators/registration/modules/scheduler.ts +65 -65
- package/cli/templates/handlers/generators/registration/modules/storage.ts +199 -199
- package/cli/templates/handlers/generators/registration/sections.ts +210 -210
- package/cli/templates/handlers/generators/types/context.ts +121 -121
- package/cli/templates/handlers/generators/types/core.ts +108 -106
- package/cli/templates/handlers/generators/types/operations.ts +135 -135
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +291 -291
- package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
- package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1382 -1382
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +278 -278
- package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
- package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
- package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
- package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +158 -157
- package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +958 -958
- package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
- package/cli/templates/handlers/index.ts +47 -47
- package/cli/templates/handlers/operations.ts +116 -116
- package/cli/templates/handlers/registration.ts +91 -91
- package/cli/templates/handlers/types.ts +17 -17
- package/cli/templates/handlers/utils.ts +48 -48
- package/cli/types.ts +110 -110
- package/cli/utils/handler-discovery.ts +501 -501
- package/cli/utils/json-utils.ts +24 -24
- package/cli/utils/path-utils.ts +19 -19
- package/cli/utils/schema-discovery.ts +399 -399
- package/dist/cli/index.js +6 -4
- package/dist/cli/index.mjs +6 -4
- package/index.ts +18 -18
- package/package.json +58 -58
- package/react/index.ts +5 -5
- package/react/use-infinite-query.ts +255 -255
- package/react/use-mutation.ts +89 -89
- package/react/use-query.ts +210 -210
- package/schema.ts +641 -641
- package/test-better-auth-hash.ts +2 -2
- package/tsconfig.json +6 -6
- package/tsup.config.ts +82 -82
package/Documentation.md
CHANGED
|
@@ -1,898 +1,898 @@
|
|
|
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
|
-
### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
|
|
49
|
-
|
|
50
|
-
- `v.one("target")` creates a single-reference relation and infers a local FK field.
|
|
51
|
-
- `v.many("target")` is inverse one-to-many and infers an FK on the target table.
|
|
52
|
-
- `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
|
|
53
|
-
|
|
54
|
-
Many-to-many example:
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
export const schemas = schema({
|
|
58
|
-
pets: table({
|
|
59
|
-
id: v.uuid(),
|
|
60
|
-
trips: v.manyToMany("trips"),
|
|
61
|
-
}),
|
|
62
|
-
trips: table({
|
|
63
|
-
id: v.uuid(),
|
|
64
|
-
pets: v.manyToMany("pets"),
|
|
65
|
-
}),
|
|
66
|
-
});
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Default behavior for `v.manyToMany`:
|
|
70
|
-
|
|
71
|
-
- Generates one deterministic junction table per pair.
|
|
72
|
-
- Junction rows are pure links (two FK columns, no payload columns).
|
|
73
|
-
- Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 3) How to create handlers
|
|
78
|
-
|
|
79
|
-
Handlers are created with generated helpers:
|
|
80
|
-
|
|
81
|
-
- `query(...)` for read endpoints
|
|
82
|
-
- `mutation(...)` for write endpoints
|
|
83
|
-
|
|
84
|
-
Import them from your generated handlers module:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { query, mutation } from "../_generated/handlers";
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 3.1 Query handler example
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
import { query } from "../../_generated/handlers";
|
|
94
|
-
import * as z from "zod";
|
|
95
|
-
|
|
96
|
-
export const getUserProfile = query({
|
|
97
|
-
args: {
|
|
98
|
-
userId: z.string(),
|
|
99
|
-
},
|
|
100
|
-
handler: async (ctx, args) => {
|
|
101
|
-
const user = await ctx.db.users.findFirst({
|
|
102
|
-
where: { id: args.userId },
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (!user) {
|
|
106
|
-
ctx.error(404, "User not found", { userId: args.userId });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return user;
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### 3.1.1 More query examples (basic to advanced)
|
|
115
|
-
|
|
116
|
-
#### A) Search + relation include + limit
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
import { query } from "../../_generated/handlers";
|
|
120
|
-
import * as z from "zod";
|
|
121
|
-
|
|
122
|
-
export const searchPosts = query({
|
|
123
|
-
args: {
|
|
124
|
-
search: z.string().optional(),
|
|
125
|
-
ownerId: z.string().optional(),
|
|
126
|
-
limit: z.number().int().min(1).max(100).default(25),
|
|
127
|
-
},
|
|
128
|
-
handler: async (ctx, args) => {
|
|
129
|
-
return ctx.db.posts.findMany({
|
|
130
|
-
where: {
|
|
131
|
-
title: {
|
|
132
|
-
regex: args.search ?? "",
|
|
133
|
-
$options: "i",
|
|
134
|
-
},
|
|
135
|
-
...(args.ownerId ? { ownerId: args.ownerId } : {}),
|
|
136
|
-
},
|
|
137
|
-
with: {
|
|
138
|
-
owner: true,
|
|
139
|
-
comments: true,
|
|
140
|
-
},
|
|
141
|
-
limit: args.limit,
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### B) Cursor/pagination-style query
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import { query } from "../../_generated/handlers";
|
|
151
|
-
import * as z from "zod";
|
|
152
|
-
|
|
153
|
-
export const listPostsPage = query({
|
|
154
|
-
args: {
|
|
155
|
-
cursor: z.number().int().optional(),
|
|
156
|
-
pageSize: z.number().int().min(1).max(50).default(20),
|
|
157
|
-
},
|
|
158
|
-
handler: async (ctx, args) => {
|
|
159
|
-
const rows = await ctx.db.posts.findMany({
|
|
160
|
-
where: args.cursor
|
|
161
|
-
? {
|
|
162
|
-
id: {
|
|
163
|
-
gt: args.cursor,
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
: {},
|
|
167
|
-
orderBy: { column: "id", direction: "asc" },
|
|
168
|
-
limit: args.pageSize,
|
|
169
|
-
with: {
|
|
170
|
-
owner: true,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const nextCursor = rows.length > 0 ? rows[rows.length - 1]?.id : undefined;
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
rows,
|
|
178
|
-
nextCursor,
|
|
179
|
-
hasMore: rows.length === args.pageSize,
|
|
180
|
-
};
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### C) Aggregate-heavy query (`count`, `avg`, relation path)
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { query } from "../../_generated/handlers";
|
|
189
|
-
import * as z from "zod";
|
|
190
|
-
|
|
191
|
-
export const getPostStats = query({
|
|
192
|
-
args: {
|
|
193
|
-
ownerId: z.string().optional(),
|
|
194
|
-
},
|
|
195
|
-
handler: async (ctx, args) => {
|
|
196
|
-
const totalPosts = await ctx.db.posts.count({
|
|
197
|
-
where: args.ownerId ? { ownerId: args.ownerId } : {},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const uniqueOwners = await ctx.db.posts.count({
|
|
201
|
-
field: "ownerId",
|
|
202
|
-
distinct: true,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
const averagePostId = await ctx.db.posts.avg({
|
|
206
|
-
field: "id",
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const averageCommentId = await ctx.db.posts.avg({
|
|
210
|
-
field: "comments.id",
|
|
211
|
-
with: {
|
|
212
|
-
comments: {
|
|
213
|
-
where: {
|
|
214
|
-
id: {
|
|
215
|
-
gte: 10000,
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
totalPosts,
|
|
224
|
-
uniqueOwners,
|
|
225
|
-
averagePostId,
|
|
226
|
-
averageCommentId,
|
|
227
|
-
};
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
#### D) Geo query (`geoWithin`) + filter composition
|
|
233
|
-
|
|
234
|
-
```ts
|
|
235
|
-
import { query } from "../../_generated/handlers";
|
|
236
|
-
import * as z from "zod";
|
|
237
|
-
|
|
238
|
-
export const nearbyPlaygroundItems = query({
|
|
239
|
-
args: {
|
|
240
|
-
latitude: z.number(),
|
|
241
|
-
longitude: z.number(),
|
|
242
|
-
radiusMeters: z.number().positive().default(5000),
|
|
243
|
-
},
|
|
244
|
-
handler: async (ctx, args) => {
|
|
245
|
-
const rows = await ctx.db.queryPlayground.findMany({
|
|
246
|
-
where: {
|
|
247
|
-
geoWithin: {
|
|
248
|
-
$geometry: {
|
|
249
|
-
latitude: args.latitude,
|
|
250
|
-
longitude: args.longitude,
|
|
251
|
-
},
|
|
252
|
-
latitudeField: "latitude",
|
|
253
|
-
longitudeField: "longitude",
|
|
254
|
-
gte: 0,
|
|
255
|
-
lt: args.radiusMeters,
|
|
256
|
-
},
|
|
257
|
-
isActive: {
|
|
258
|
-
eq: true,
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
limit: 100,
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
return {
|
|
265
|
-
count: rows.length,
|
|
266
|
-
rows,
|
|
267
|
-
};
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
#### F) Query with `orderBy`
|
|
273
|
-
|
|
274
|
-
The `orderBy` field accepts an object or array of objects with `column` and optional `direction`:
|
|
275
|
-
|
|
276
|
-
```ts
|
|
277
|
-
import { query } from "../../_generated/handlers";
|
|
278
|
-
import * as z from "zod";
|
|
279
|
-
|
|
280
|
-
export const getTopUsers = query({
|
|
281
|
-
args: {
|
|
282
|
-
minScore: z.number().optional(),
|
|
283
|
-
limit: z.number().int().min(1).max(100).default(10),
|
|
284
|
-
},
|
|
285
|
-
handler: async (ctx, args) => {
|
|
286
|
-
return ctx.db.users.findMany({
|
|
287
|
-
where: args.minScore ? { score: { gte: args.minScore } } : {},
|
|
288
|
-
orderBy: { column: "score", direction: "desc" },
|
|
289
|
-
limit: args.limit,
|
|
290
|
-
});
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Multiple sort keys are supported with an array:
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
orderBy: [
|
|
299
|
-
{ column: "score", direction: "desc" },
|
|
300
|
-
{ column: "name", direction: "asc" },
|
|
301
|
-
],
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
#### G) Array column operators (`includes`, `includesAny`, `length`)
|
|
305
|
-
|
|
306
|
-
For JSON array columns, use array-specific operators:
|
|
307
|
-
|
|
308
|
-
```ts
|
|
309
|
-
import { query } from "../../_generated/handlers";
|
|
310
|
-
import * as z from "zod";
|
|
311
|
-
|
|
312
|
-
export const findProducts = query({
|
|
313
|
-
args: {
|
|
314
|
-
color: z.string().optional(),
|
|
315
|
-
tags: z.array(z.string()).optional(),
|
|
316
|
-
minTagCount: z.number().int().optional(),
|
|
317
|
-
},
|
|
318
|
-
handler: async (ctx, args) => {
|
|
319
|
-
return ctx.db.products.findMany({
|
|
320
|
-
where: {
|
|
321
|
-
...(args.tags ? { tags: { includes: args.tags } } : {}),
|
|
322
|
-
...(args.minTagCount ? { tags: { length: args.minTagCount } } : {}),
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
- `includes` — row's array must contain **all** specified values
|
|
330
|
-
- `includesAny` — row's array must contain **at least one** of the specified values
|
|
331
|
-
- `length` — matches the array length exactly
|
|
332
|
-
- `eq` / `ne` — exact match on the whole JSON array
|
|
333
|
-
|
|
334
|
-
#### H) Complex production-style query (similar to `db-features`)
|
|
335
|
-
|
|
336
|
-
```ts
|
|
337
|
-
import { query } from "../../_generated/handlers";
|
|
338
|
-
import * as z from "zod";
|
|
339
|
-
|
|
340
|
-
export const queryDashboardData = query({
|
|
341
|
-
args: {
|
|
342
|
-
userId: z.string().optional(),
|
|
343
|
-
search: z.string().optional(),
|
|
344
|
-
},
|
|
345
|
-
handler: async (ctx, args) => {
|
|
346
|
-
const posts = await ctx.db.posts.findMany({
|
|
347
|
-
where: {
|
|
348
|
-
ownerId: args.userId,
|
|
349
|
-
title: {
|
|
350
|
-
regex: args.search ?? "test",
|
|
351
|
-
$options: "i",
|
|
352
|
-
},
|
|
353
|
-
id: { gt: 0 },
|
|
354
|
-
},
|
|
355
|
-
with: {
|
|
356
|
-
comments: true,
|
|
357
|
-
owner: true,
|
|
358
|
-
},
|
|
359
|
-
limit: 25,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const postsWithCommentStats = await ctx.db.posts.findMany({
|
|
363
|
-
with: {
|
|
364
|
-
comments: {
|
|
365
|
-
_count: true,
|
|
366
|
-
_avg: { id: true },
|
|
367
|
-
},
|
|
368
|
-
},
|
|
369
|
-
limit: 10,
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const postsTotal = await ctx.db.posts.count({
|
|
373
|
-
where: { id: { $gte: 1 } },
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
return {
|
|
377
|
-
posts,
|
|
378
|
-
postsTotal,
|
|
379
|
-
postsWithCommentStats,
|
|
380
|
-
};
|
|
381
|
-
},
|
|
382
|
-
});
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### 3.1.2 Query design tips for complex handlers
|
|
386
|
-
|
|
387
|
-
- Keep args schema strict (defaults, min/max, optional fields).
|
|
388
|
-
- Return stable shapes (avoid switching response shape by condition).
|
|
389
|
-
- Start with one root query and compose aggregates/relations progressively.
|
|
390
|
-
- Prefer server-side filtering in `where` instead of filtering on frontend.
|
|
391
|
-
- For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
|
|
392
|
-
- Use `orderBy` with cursor-based pagination to ensure deterministic ordering across pages.
|
|
393
|
-
|
|
394
|
-
### 3.2 Mutation handler examples
|
|
395
|
-
|
|
396
|
-
#### Insert
|
|
397
|
-
|
|
398
|
-
```ts
|
|
399
|
-
import { mutation } from "../../_generated/handlers";
|
|
400
|
-
import * as z from "zod";
|
|
401
|
-
|
|
402
|
-
export const createPost = mutation({
|
|
403
|
-
args: {
|
|
404
|
-
title: z.string().min(1),
|
|
405
|
-
slug: z.string().min(1),
|
|
406
|
-
},
|
|
407
|
-
handler: async (ctx, args) => {
|
|
408
|
-
const inserted = await ctx.db.posts.insert({
|
|
409
|
-
values: {
|
|
410
|
-
title: args.title,
|
|
411
|
-
slug: args.slug,
|
|
412
|
-
ownerId: "some-user-id",
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
return { created: inserted.length };
|
|
417
|
-
},
|
|
418
|
-
});
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
#### Upsert
|
|
422
|
-
|
|
423
|
-
```ts
|
|
424
|
-
import { mutation } from "../../_generated/handlers";
|
|
425
|
-
import * as z from "zod";
|
|
426
|
-
|
|
427
|
-
export const upsertPost = mutation({
|
|
428
|
-
args: {
|
|
429
|
-
slug: z.string().min(1),
|
|
430
|
-
title: z.string().min(1),
|
|
431
|
-
},
|
|
432
|
-
handler: async (ctx, args) => {
|
|
433
|
-
const result = await ctx.db.posts.upsert({
|
|
434
|
-
values: {
|
|
435
|
-
slug: args.slug,
|
|
436
|
-
title: args.title,
|
|
437
|
-
ownerId: "some-user-id",
|
|
438
|
-
},
|
|
439
|
-
target: "slug",
|
|
440
|
-
set: { title: args.title },
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
return { updated: result.length };
|
|
444
|
-
},
|
|
445
|
-
});
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
- `target` — conflict column(s) to detect existing rows
|
|
449
|
-
- `set` — columns to update on conflict (omit to keep existing values)
|
|
450
|
-
- Supports single or array of values
|
|
451
|
-
|
|
452
|
-
### 3.3 Handler file placement
|
|
453
|
-
|
|
454
|
-
Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
|
|
455
|
-
|
|
456
|
-
- `packages/backend/src/test.ts`
|
|
457
|
-
- `packages/backend/src/queries/db-features.ts`
|
|
458
|
-
- `packages/backend/src/mutations/db-features.ts`
|
|
459
|
-
- `packages/backend/src/bun/test.ts`
|
|
460
|
-
|
|
461
|
-
Generated client route names follow directory + file + export naming. For example:
|
|
462
|
-
|
|
463
|
-
- query from `src/test.ts` export `getTest` becomes `appflare.queries.test.getTest`
|
|
464
|
-
- mutation from `src/mutations/db-features.ts` export `testMutationFeatures` becomes `appflare.mutations["db-features"].testMutationFeatures`
|
|
465
|
-
|
|
466
|
-
### 3.4 Context utilities available in handlers
|
|
467
|
-
|
|
468
|
-
Inside handlers, you commonly use:
|
|
469
|
-
|
|
470
|
-
- `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
|
|
471
|
-
- aggregate helpers like `count` and `avg`
|
|
472
|
-
- `count` supports `where`, `field`, `distinct`, and `with` for filtered relation counts
|
|
473
|
-
- `avg` supports `where`, `field`, `distinct`, and `with` for filtered relation averages
|
|
474
|
-
- `where` supports shorthand operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `exists`, `regex`, `$options`, `includes`, `includesAny`, `length`
|
|
475
|
-
- `with` supports `_count` and `_avg` for relation-level aggregate results (returned as `XxxAggregate`)
|
|
476
|
-
- `orderBy` accepts `{ column, direction }` or array thereof
|
|
477
|
-
- `geoWithin` for geospatial distance queries (Haversine formula)
|
|
478
|
-
- `upsert` supports `target` (conflict columns) and `set` (on-conflict update columns)
|
|
479
|
-
- `ctx.error(status, message, details)` for typed failures
|
|
480
|
-
|
|
481
|
-
#### Updating manyToMany relations
|
|
482
|
-
|
|
483
|
-
The `update` operation supports managing manyToMany relations via the `set` payload. Each manyToMany relation field accepts an object with `items` and optional `mode`:
|
|
484
|
-
|
|
485
|
-
```ts
|
|
486
|
-
// Merge mode (default) — adds new links, keeps existing ones
|
|
487
|
-
await ctx.db.family.update({
|
|
488
|
-
where: { primaryUserId: userId },
|
|
489
|
-
set: {
|
|
490
|
-
members: {
|
|
491
|
-
items: [
|
|
492
|
-
"existing-user-id", // link by ID
|
|
493
|
-
{ id: "another-user-id" }, // link by ID (object form)
|
|
494
|
-
{ name: "New User", email: "..." }, // create new user, then link
|
|
495
|
-
],
|
|
496
|
-
mode: "merge",
|
|
497
|
-
},
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
// Overwrite mode — deletes all existing links, keeps only new ones
|
|
502
|
-
await ctx.db.family.update({
|
|
503
|
-
where: { primaryUserId: userId },
|
|
504
|
-
set: {
|
|
505
|
-
members: {
|
|
506
|
-
items: ["user-id-1", "user-id-2"],
|
|
507
|
-
mode: "overwrite",
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
});
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
- `items` — array of IDs (string/number) or partial objects. Objects without an `id` field are created in the target table before linking.
|
|
514
|
-
- `mode` — `"merge"` (default) keeps existing links and adds new ones; `"overwrite"` deletes all existing links for the parent record before inserting new ones.
|
|
515
|
-
- All relation updates run inside a transaction when present.
|
|
516
|
-
|
|
517
|
-
See real examples in:
|
|
518
|
-
|
|
519
|
-
- `packages/backend/src/queries/db-features.ts`
|
|
520
|
-
- `packages/backend/src/mutations/db-features.ts`
|
|
521
|
-
|
|
522
|
-
---
|
|
523
|
-
|
|
524
|
-
## 4) Generate artifacts
|
|
525
|
-
|
|
526
|
-
From backend package:
|
|
527
|
-
|
|
528
|
-
```bash
|
|
529
|
-
cd packages/backend
|
|
530
|
-
bun ../appflare/cli dev
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
Or via scripts in `packages/backend/package.json`:
|
|
534
|
-
|
|
535
|
-
```bash
|
|
536
|
-
bun run build
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
What gets generated (core set):
|
|
540
|
-
|
|
541
|
-
- `_generated/server.js`
|
|
542
|
-
- `_generated/client.js`
|
|
543
|
-
- `_generated/auth.config.js`
|
|
544
|
-
- `_generated/drizzle.config.js`
|
|
545
|
-
- `_generated/handlers.js`
|
|
546
|
-
- `_generated/handlers.context.js`
|
|
547
|
-
- `_generated/handlers.execution.js`
|
|
548
|
-
- `_generated/handlers.routes.js`
|
|
549
|
-
- `_generated/client/**`
|
|
550
|
-
|
|
551
|
-
### Watch mode
|
|
552
|
-
|
|
553
|
-
To regenerate on file changes:
|
|
554
|
-
|
|
555
|
-
```bash
|
|
556
|
-
cd packages/backend
|
|
557
|
-
bun ../appflare/cli dev --watch
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
---
|
|
561
|
-
|
|
562
|
-
## 5) How to migrate database schema
|
|
563
|
-
|
|
564
|
-
Appflare migration flow wraps two steps:
|
|
565
|
-
|
|
566
|
-
1. Generate drizzle migrations
|
|
567
|
-
2. Apply to D1 via Wrangler
|
|
568
|
-
|
|
569
|
-
### 5.1 Standard migrate
|
|
570
|
-
|
|
571
|
-
```bash
|
|
572
|
-
cd packages/backend
|
|
573
|
-
bun ../appflare/cli migrate
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
Or script:
|
|
577
|
-
|
|
578
|
-
```bash
|
|
579
|
-
bun run migrate
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
### 5.2 Choose target environment
|
|
583
|
-
|
|
584
|
-
Use exactly one of these flags:
|
|
585
|
-
|
|
586
|
-
- `--local`
|
|
587
|
-
- `--remote`
|
|
588
|
-
- `--preview`
|
|
589
|
-
|
|
590
|
-
Examples:
|
|
591
|
-
|
|
592
|
-
```bash
|
|
593
|
-
bun ../appflare/cli migrate --local
|
|
594
|
-
bun ../appflare/cli migrate --remote
|
|
595
|
-
bun ../appflare/cli migrate --preview
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### 5.3 Typical change workflow
|
|
599
|
-
|
|
600
|
-
1. Update `schema.ts`.
|
|
601
|
-
2. Regenerate artifacts:
|
|
602
|
-
- `bun ../appflare/cli dev`
|
|
603
|
-
3. Run migration:
|
|
604
|
-
- `bun ../appflare/cli migrate --local` (or remote/preview)
|
|
605
|
-
4. Verify app behavior in `wrangler dev`.
|
|
606
|
-
|
|
607
|
-
---
|
|
608
|
-
|
|
609
|
-
## 6) Frontend usage (plain TypeScript/JavaScript)
|
|
610
|
-
|
|
611
|
-
Use the generated backend client directly.
|
|
612
|
-
|
|
613
|
-
### 6.1 Create client instance
|
|
614
|
-
|
|
615
|
-
```ts
|
|
616
|
-
import { Appflare } from "appflare-backend/_generated/client";
|
|
617
|
-
|
|
618
|
-
const appflare = new Appflare({
|
|
619
|
-
endpoint: "http://127.0.0.1:8787",
|
|
620
|
-
wsEndpoint: "ws://127.0.0.1:8787",
|
|
621
|
-
onGetAuthToken: async () => localStorage.getItem("appflare-auth-token") ?? "",
|
|
622
|
-
onSetAuthToken: async (token) => {
|
|
623
|
-
localStorage.setItem("appflare-auth-token", token);
|
|
624
|
-
},
|
|
625
|
-
});
|
|
626
|
-
```
|
|
627
|
-
|
|
628
|
-
### 6.2 Run a query
|
|
629
|
-
|
|
630
|
-
```ts
|
|
631
|
-
const result = await appflare.queries.test.getTest.run({ id: "test" });
|
|
632
|
-
|
|
633
|
-
if (result.error) {
|
|
634
|
-
console.error(result.error.status, result.error.message);
|
|
635
|
-
} else {
|
|
636
|
-
console.log(result.data);
|
|
637
|
-
}
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### 6.2.1 More frontend query call examples
|
|
641
|
-
|
|
642
|
-
#### A) Query with filters
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
const result = await appflare.queries["db-features"].testQueryFeatures.run({
|
|
646
|
-
search: "test",
|
|
647
|
-
userId: "as3xNgfPVzrooSuSwn1ZSEKNA92Cjp4V",
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
if (!result.error) {
|
|
651
|
-
console.log(result.data.postsCount, result.data.uniqueOwnerCount);
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
#### B) Query with request options
|
|
656
|
-
|
|
657
|
-
```ts
|
|
658
|
-
const result = await appflare.queries.test.getTest.run(
|
|
659
|
-
{ id: "test" },
|
|
660
|
-
{
|
|
661
|
-
headers: {
|
|
662
|
-
"x-trace-id": crypto.randomUUID(),
|
|
663
|
-
},
|
|
664
|
-
},
|
|
665
|
-
);
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
#### C) Query with realtime and explicit auth token
|
|
669
|
-
|
|
670
|
-
```ts
|
|
671
|
-
const sub = appflare.queries.test.getTest.subscribe({
|
|
672
|
-
args: { id: "test" },
|
|
673
|
-
authToken: "token-from-auth-flow",
|
|
674
|
-
onChange: (data) => {
|
|
675
|
-
console.log("fresh data", data);
|
|
676
|
-
},
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
setTimeout(() => sub.remove(), 30000);
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### 6.3 Run a mutation
|
|
683
|
-
|
|
684
|
-
```ts
|
|
685
|
-
const result = await appflare.mutations.test.newTest.run({});
|
|
686
|
-
|
|
687
|
-
if (result.error) {
|
|
688
|
-
console.error(result.error.message);
|
|
689
|
-
} else {
|
|
690
|
-
console.log(result.data);
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
### 6.4 Realtime subscribe to a query
|
|
695
|
-
|
|
696
|
-
```ts
|
|
697
|
-
const sub = appflare.queries.test.getTest.subscribe({
|
|
698
|
-
args: { id: "test" },
|
|
699
|
-
onChange: (data, event) => {
|
|
700
|
-
console.log("update", event.payload.queryName, data);
|
|
701
|
-
},
|
|
702
|
-
onError: (error) => {
|
|
703
|
-
console.error("subscription error", error);
|
|
704
|
-
},
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
// later
|
|
708
|
-
sub.remove();
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
---
|
|
712
|
-
|
|
713
|
-
## 7) How to use with React
|
|
714
|
-
|
|
715
|
-
Appflare ships React hooks in `appflare/react`:
|
|
716
|
-
|
|
717
|
-
- `useQuery`
|
|
718
|
-
- `useInfiniteQuery`
|
|
719
|
-
- `useMutation`
|
|
720
|
-
|
|
721
|
-
These are thin wrappers around TanStack Query.
|
|
722
|
-
|
|
723
|
-
### 7.1 Setup requirements
|
|
724
|
-
|
|
725
|
-
Install peer requirements in your frontend app:
|
|
726
|
-
|
|
727
|
-
```bash
|
|
728
|
-
bun add @tanstack/react-query react
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
Wrap app with `QueryClientProvider`.
|
|
732
|
-
|
|
733
|
-
```tsx
|
|
734
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
735
|
-
|
|
736
|
-
const queryClient = new QueryClient();
|
|
737
|
-
|
|
738
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
739
|
-
return (
|
|
740
|
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
### 7.2 React query usage
|
|
746
|
-
|
|
747
|
-
```tsx
|
|
748
|
-
import { useQuery } from "appflare/react";
|
|
749
|
-
import { appflare } from "./appflare-client";
|
|
750
|
-
|
|
751
|
-
export function TestScreen() {
|
|
752
|
-
const query = useQuery(
|
|
753
|
-
appflare.queries.test.getTest,
|
|
754
|
-
{ id: "test" },
|
|
755
|
-
{
|
|
756
|
-
realtime: { enabled: true },
|
|
757
|
-
},
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
if (query.isLoading) return <div>Loading...</div>;
|
|
761
|
-
if (query.error) return <div>{query.error.message}</div>;
|
|
762
|
-
|
|
763
|
-
return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
|
|
764
|
-
}
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
### 7.3 React mutation usage
|
|
768
|
-
|
|
769
|
-
```tsx
|
|
770
|
-
import { useMutation } from "appflare/react";
|
|
771
|
-
import { appflare } from "./appflare-client";
|
|
772
|
-
|
|
773
|
-
export function CreatePostButton() {
|
|
774
|
-
const mutation = useMutation(appflare.mutations.test.newTest, {
|
|
775
|
-
onSuccess: (data) => console.log("created", data),
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
return (
|
|
779
|
-
<button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
|
780
|
-
Create
|
|
781
|
-
</button>
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
```
|
|
785
|
-
|
|
786
|
-
### 7.4 React infinite query usage
|
|
787
|
-
|
|
788
|
-
```tsx
|
|
789
|
-
import { useInfiniteQuery } from "appflare/react";
|
|
790
|
-
import { appflare } from "./appflare-client";
|
|
791
|
-
|
|
792
|
-
const result = useInfiniteQuery(
|
|
793
|
-
appflare.queries.test.getTest,
|
|
794
|
-
{ id: "test" },
|
|
795
|
-
{
|
|
796
|
-
pageParamToArgs: (baseArgs, page) => ({ ...baseArgs, page }),
|
|
797
|
-
queryOptions: {
|
|
798
|
-
initialPageParam: 1,
|
|
799
|
-
getNextPageParam: (lastPage, pages) => pages.length + 1,
|
|
800
|
-
},
|
|
801
|
-
},
|
|
802
|
-
);
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
### 7.5 Realtime with React hooks
|
|
806
|
-
|
|
807
|
-
Both `useQuery` and `useInfiniteQuery` support:
|
|
808
|
-
|
|
809
|
-
```ts
|
|
810
|
-
realtime: {
|
|
811
|
-
enabled: true,
|
|
812
|
-
authToken: "optional-token",
|
|
813
|
-
requestOptions: { headers: { "x-custom": "1" } },
|
|
814
|
-
onChange: (data, update) => {},
|
|
815
|
-
onError: (error) => {},
|
|
816
|
-
}
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
When enabled, hooks subscribe via generated query `.subscribe(...)` and keep query cache updated automatically.
|
|
820
|
-
|
|
821
|
-
---
|
|
822
|
-
|
|
823
|
-
## 8) Frontend app helper pattern
|
|
824
|
-
|
|
825
|
-
A good pattern is to keep one shared client factory in a single file.
|
|
826
|
-
|
|
827
|
-
Example in this workspace:
|
|
828
|
-
|
|
829
|
-
- `apps/app/lib/appflare.ts`
|
|
830
|
-
|
|
831
|
-
This file centralizes:
|
|
832
|
-
|
|
833
|
-
- endpoint/wsEndpoint selection (web/mobile)
|
|
834
|
-
- token storage and retrieval
|
|
835
|
-
- exported hook wrappers
|
|
836
|
-
|
|
837
|
-
---
|
|
838
|
-
|
|
839
|
-
## 9) Common commands cheat sheet
|
|
840
|
-
|
|
841
|
-
From `packages/backend`:
|
|
842
|
-
|
|
843
|
-
```bash
|
|
844
|
-
# Generate once
|
|
845
|
-
bun ../appflare/cli build -c appflare.config.ts
|
|
846
|
-
|
|
847
|
-
# Generate in dev mode
|
|
848
|
-
bun ../appflare/cli dev -c appflare.config.ts
|
|
849
|
-
|
|
850
|
-
# Generate + watch
|
|
851
|
-
bun ../appflare/cli dev -c appflare.config.ts --watch
|
|
852
|
-
|
|
853
|
-
# Migrate local D1
|
|
854
|
-
bun ../appflare/cli migrate -c appflare.config.ts --local
|
|
855
|
-
|
|
856
|
-
# Migrate remote D1
|
|
857
|
-
bun ../appflare/cli migrate -c appflare.config.ts --remote
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
Or use backend scripts:
|
|
861
|
-
|
|
862
|
-
```bash
|
|
863
|
-
bun run build
|
|
864
|
-
bun run dev
|
|
865
|
-
bun run migrate
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
---
|
|
869
|
-
|
|
870
|
-
## 10) Troubleshooting
|
|
871
|
-
|
|
872
|
-
### Generated client has missing routes
|
|
873
|
-
|
|
874
|
-
- Ensure handler file is under `scanDir`.
|
|
875
|
-
- Ensure export uses `query(...)` or `mutation(...)`.
|
|
876
|
-
- Run `bun ../appflare/cli dev` again.
|
|
877
|
-
|
|
878
|
-
### Realtime not receiving updates
|
|
879
|
-
|
|
880
|
-
- Ensure query has `.subscribe` in generated client.
|
|
881
|
-
- Ensure `wsEndpoint` is set correctly.
|
|
882
|
-
- Ensure valid auth token is available if your runtime requires auth.
|
|
883
|
-
|
|
884
|
-
### Migration fails
|
|
885
|
-
|
|
886
|
-
- Confirm database values in `appflare.config.ts`.
|
|
887
|
-
- Use one environment flag only (`--local`, `--remote`, or `--preview`).
|
|
888
|
-
- Re-run generation before migration if schema changed.
|
|
889
|
-
|
|
890
|
-
---
|
|
891
|
-
|
|
892
|
-
## 11) Recommended development loop
|
|
893
|
-
|
|
894
|
-
1. Edit schema and handlers.
|
|
895
|
-
2. Run generator (`dev` or `dev --watch`).
|
|
896
|
-
3. Run migrations.
|
|
897
|
-
4. Start backend (`wrangler dev`).
|
|
898
|
-
5. Use generated client in frontend and iterate.
|
|
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
|
+
### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
|
|
49
|
+
|
|
50
|
+
- `v.one("target")` creates a single-reference relation and infers a local FK field.
|
|
51
|
+
- `v.many("target")` is inverse one-to-many and infers an FK on the target table.
|
|
52
|
+
- `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
|
|
53
|
+
|
|
54
|
+
Many-to-many example:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
export const schemas = schema({
|
|
58
|
+
pets: table({
|
|
59
|
+
id: v.uuid(),
|
|
60
|
+
trips: v.manyToMany("trips"),
|
|
61
|
+
}),
|
|
62
|
+
trips: table({
|
|
63
|
+
id: v.uuid(),
|
|
64
|
+
pets: v.manyToMany("pets"),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Default behavior for `v.manyToMany`:
|
|
70
|
+
|
|
71
|
+
- Generates one deterministic junction table per pair.
|
|
72
|
+
- Junction rows are pure links (two FK columns, no payload columns).
|
|
73
|
+
- Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 3) How to create handlers
|
|
78
|
+
|
|
79
|
+
Handlers are created with generated helpers:
|
|
80
|
+
|
|
81
|
+
- `query(...)` for read endpoints
|
|
82
|
+
- `mutation(...)` for write endpoints
|
|
83
|
+
|
|
84
|
+
Import them from your generated handlers module:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { query, mutation } from "../_generated/handlers";
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3.1 Query handler example
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { query } from "../../_generated/handlers";
|
|
94
|
+
import * as z from "zod";
|
|
95
|
+
|
|
96
|
+
export const getUserProfile = query({
|
|
97
|
+
args: {
|
|
98
|
+
userId: z.string(),
|
|
99
|
+
},
|
|
100
|
+
handler: async (ctx, args) => {
|
|
101
|
+
const user = await ctx.db.users.findFirst({
|
|
102
|
+
where: { id: args.userId },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!user) {
|
|
106
|
+
ctx.error(404, "User not found", { userId: args.userId });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return user;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3.1.1 More query examples (basic to advanced)
|
|
115
|
+
|
|
116
|
+
#### A) Search + relation include + limit
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { query } from "../../_generated/handlers";
|
|
120
|
+
import * as z from "zod";
|
|
121
|
+
|
|
122
|
+
export const searchPosts = query({
|
|
123
|
+
args: {
|
|
124
|
+
search: z.string().optional(),
|
|
125
|
+
ownerId: z.string().optional(),
|
|
126
|
+
limit: z.number().int().min(1).max(100).default(25),
|
|
127
|
+
},
|
|
128
|
+
handler: async (ctx, args) => {
|
|
129
|
+
return ctx.db.posts.findMany({
|
|
130
|
+
where: {
|
|
131
|
+
title: {
|
|
132
|
+
regex: args.search ?? "",
|
|
133
|
+
$options: "i",
|
|
134
|
+
},
|
|
135
|
+
...(args.ownerId ? { ownerId: args.ownerId } : {}),
|
|
136
|
+
},
|
|
137
|
+
with: {
|
|
138
|
+
owner: true,
|
|
139
|
+
comments: true,
|
|
140
|
+
},
|
|
141
|
+
limit: args.limit,
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### B) Cursor/pagination-style query
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { query } from "../../_generated/handlers";
|
|
151
|
+
import * as z from "zod";
|
|
152
|
+
|
|
153
|
+
export const listPostsPage = query({
|
|
154
|
+
args: {
|
|
155
|
+
cursor: z.number().int().optional(),
|
|
156
|
+
pageSize: z.number().int().min(1).max(50).default(20),
|
|
157
|
+
},
|
|
158
|
+
handler: async (ctx, args) => {
|
|
159
|
+
const rows = await ctx.db.posts.findMany({
|
|
160
|
+
where: args.cursor
|
|
161
|
+
? {
|
|
162
|
+
id: {
|
|
163
|
+
gt: args.cursor,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
: {},
|
|
167
|
+
orderBy: { column: "id", direction: "asc" },
|
|
168
|
+
limit: args.pageSize,
|
|
169
|
+
with: {
|
|
170
|
+
owner: true,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const nextCursor = rows.length > 0 ? rows[rows.length - 1]?.id : undefined;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
rows,
|
|
178
|
+
nextCursor,
|
|
179
|
+
hasMore: rows.length === args.pageSize,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### C) Aggregate-heavy query (`count`, `avg`, relation path)
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { query } from "../../_generated/handlers";
|
|
189
|
+
import * as z from "zod";
|
|
190
|
+
|
|
191
|
+
export const getPostStats = query({
|
|
192
|
+
args: {
|
|
193
|
+
ownerId: z.string().optional(),
|
|
194
|
+
},
|
|
195
|
+
handler: async (ctx, args) => {
|
|
196
|
+
const totalPosts = await ctx.db.posts.count({
|
|
197
|
+
where: args.ownerId ? { ownerId: args.ownerId } : {},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const uniqueOwners = await ctx.db.posts.count({
|
|
201
|
+
field: "ownerId",
|
|
202
|
+
distinct: true,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const averagePostId = await ctx.db.posts.avg({
|
|
206
|
+
field: "id",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const averageCommentId = await ctx.db.posts.avg({
|
|
210
|
+
field: "comments.id",
|
|
211
|
+
with: {
|
|
212
|
+
comments: {
|
|
213
|
+
where: {
|
|
214
|
+
id: {
|
|
215
|
+
gte: 10000,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
totalPosts,
|
|
224
|
+
uniqueOwners,
|
|
225
|
+
averagePostId,
|
|
226
|
+
averageCommentId,
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### D) Geo query (`geoWithin`) + filter composition
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { query } from "../../_generated/handlers";
|
|
236
|
+
import * as z from "zod";
|
|
237
|
+
|
|
238
|
+
export const nearbyPlaygroundItems = query({
|
|
239
|
+
args: {
|
|
240
|
+
latitude: z.number(),
|
|
241
|
+
longitude: z.number(),
|
|
242
|
+
radiusMeters: z.number().positive().default(5000),
|
|
243
|
+
},
|
|
244
|
+
handler: async (ctx, args) => {
|
|
245
|
+
const rows = await ctx.db.queryPlayground.findMany({
|
|
246
|
+
where: {
|
|
247
|
+
geoWithin: {
|
|
248
|
+
$geometry: {
|
|
249
|
+
latitude: args.latitude,
|
|
250
|
+
longitude: args.longitude,
|
|
251
|
+
},
|
|
252
|
+
latitudeField: "latitude",
|
|
253
|
+
longitudeField: "longitude",
|
|
254
|
+
gte: 0,
|
|
255
|
+
lt: args.radiusMeters,
|
|
256
|
+
},
|
|
257
|
+
isActive: {
|
|
258
|
+
eq: true,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
limit: 100,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
count: rows.length,
|
|
266
|
+
rows,
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### F) Query with `orderBy`
|
|
273
|
+
|
|
274
|
+
The `orderBy` field accepts an object or array of objects with `column` and optional `direction`:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { query } from "../../_generated/handlers";
|
|
278
|
+
import * as z from "zod";
|
|
279
|
+
|
|
280
|
+
export const getTopUsers = query({
|
|
281
|
+
args: {
|
|
282
|
+
minScore: z.number().optional(),
|
|
283
|
+
limit: z.number().int().min(1).max(100).default(10),
|
|
284
|
+
},
|
|
285
|
+
handler: async (ctx, args) => {
|
|
286
|
+
return ctx.db.users.findMany({
|
|
287
|
+
where: args.minScore ? { score: { gte: args.minScore } } : {},
|
|
288
|
+
orderBy: { column: "score", direction: "desc" },
|
|
289
|
+
limit: args.limit,
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Multiple sort keys are supported with an array:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
orderBy: [
|
|
299
|
+
{ column: "score", direction: "desc" },
|
|
300
|
+
{ column: "name", direction: "asc" },
|
|
301
|
+
],
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### G) Array column operators (`includes`, `includesAny`, `length`)
|
|
305
|
+
|
|
306
|
+
For JSON array columns, use array-specific operators:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { query } from "../../_generated/handlers";
|
|
310
|
+
import * as z from "zod";
|
|
311
|
+
|
|
312
|
+
export const findProducts = query({
|
|
313
|
+
args: {
|
|
314
|
+
color: z.string().optional(),
|
|
315
|
+
tags: z.array(z.string()).optional(),
|
|
316
|
+
minTagCount: z.number().int().optional(),
|
|
317
|
+
},
|
|
318
|
+
handler: async (ctx, args) => {
|
|
319
|
+
return ctx.db.products.findMany({
|
|
320
|
+
where: {
|
|
321
|
+
...(args.tags ? { tags: { includes: args.tags } } : {}),
|
|
322
|
+
...(args.minTagCount ? { tags: { length: args.minTagCount } } : {}),
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `includes` — row's array must contain **all** specified values
|
|
330
|
+
- `includesAny` — row's array must contain **at least one** of the specified values
|
|
331
|
+
- `length` — matches the array length exactly
|
|
332
|
+
- `eq` / `ne` — exact match on the whole JSON array
|
|
333
|
+
|
|
334
|
+
#### H) Complex production-style query (similar to `db-features`)
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { query } from "../../_generated/handlers";
|
|
338
|
+
import * as z from "zod";
|
|
339
|
+
|
|
340
|
+
export const queryDashboardData = query({
|
|
341
|
+
args: {
|
|
342
|
+
userId: z.string().optional(),
|
|
343
|
+
search: z.string().optional(),
|
|
344
|
+
},
|
|
345
|
+
handler: async (ctx, args) => {
|
|
346
|
+
const posts = await ctx.db.posts.findMany({
|
|
347
|
+
where: {
|
|
348
|
+
ownerId: args.userId,
|
|
349
|
+
title: {
|
|
350
|
+
regex: args.search ?? "test",
|
|
351
|
+
$options: "i",
|
|
352
|
+
},
|
|
353
|
+
id: { gt: 0 },
|
|
354
|
+
},
|
|
355
|
+
with: {
|
|
356
|
+
comments: true,
|
|
357
|
+
owner: true,
|
|
358
|
+
},
|
|
359
|
+
limit: 25,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const postsWithCommentStats = await ctx.db.posts.findMany({
|
|
363
|
+
with: {
|
|
364
|
+
comments: {
|
|
365
|
+
_count: true,
|
|
366
|
+
_avg: { id: true },
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
limit: 10,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const postsTotal = await ctx.db.posts.count({
|
|
373
|
+
where: { id: { $gte: 1 } },
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
posts,
|
|
378
|
+
postsTotal,
|
|
379
|
+
postsWithCommentStats,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 3.1.2 Query design tips for complex handlers
|
|
386
|
+
|
|
387
|
+
- Keep args schema strict (defaults, min/max, optional fields).
|
|
388
|
+
- Return stable shapes (avoid switching response shape by condition).
|
|
389
|
+
- Start with one root query and compose aggregates/relations progressively.
|
|
390
|
+
- Prefer server-side filtering in `where` instead of filtering on frontend.
|
|
391
|
+
- For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
|
|
392
|
+
- Use `orderBy` with cursor-based pagination to ensure deterministic ordering across pages.
|
|
393
|
+
|
|
394
|
+
### 3.2 Mutation handler examples
|
|
395
|
+
|
|
396
|
+
#### Insert
|
|
397
|
+
|
|
398
|
+
```ts
|
|
399
|
+
import { mutation } from "../../_generated/handlers";
|
|
400
|
+
import * as z from "zod";
|
|
401
|
+
|
|
402
|
+
export const createPost = mutation({
|
|
403
|
+
args: {
|
|
404
|
+
title: z.string().min(1),
|
|
405
|
+
slug: z.string().min(1),
|
|
406
|
+
},
|
|
407
|
+
handler: async (ctx, args) => {
|
|
408
|
+
const inserted = await ctx.db.posts.insert({
|
|
409
|
+
values: {
|
|
410
|
+
title: args.title,
|
|
411
|
+
slug: args.slug,
|
|
412
|
+
ownerId: "some-user-id",
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return { created: inserted.length };
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Upsert
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
import { mutation } from "../../_generated/handlers";
|
|
425
|
+
import * as z from "zod";
|
|
426
|
+
|
|
427
|
+
export const upsertPost = mutation({
|
|
428
|
+
args: {
|
|
429
|
+
slug: z.string().min(1),
|
|
430
|
+
title: z.string().min(1),
|
|
431
|
+
},
|
|
432
|
+
handler: async (ctx, args) => {
|
|
433
|
+
const result = await ctx.db.posts.upsert({
|
|
434
|
+
values: {
|
|
435
|
+
slug: args.slug,
|
|
436
|
+
title: args.title,
|
|
437
|
+
ownerId: "some-user-id",
|
|
438
|
+
},
|
|
439
|
+
target: "slug",
|
|
440
|
+
set: { title: args.title },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return { updated: result.length };
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
- `target` — conflict column(s) to detect existing rows
|
|
449
|
+
- `set` — columns to update on conflict (omit to keep existing values)
|
|
450
|
+
- Supports single or array of values
|
|
451
|
+
|
|
452
|
+
### 3.3 Handler file placement
|
|
453
|
+
|
|
454
|
+
Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
|
|
455
|
+
|
|
456
|
+
- `packages/backend/src/test.ts`
|
|
457
|
+
- `packages/backend/src/queries/db-features.ts`
|
|
458
|
+
- `packages/backend/src/mutations/db-features.ts`
|
|
459
|
+
- `packages/backend/src/bun/test.ts`
|
|
460
|
+
|
|
461
|
+
Generated client route names follow directory + file + export naming. For example:
|
|
462
|
+
|
|
463
|
+
- query from `src/test.ts` export `getTest` becomes `appflare.queries.test.getTest`
|
|
464
|
+
- mutation from `src/mutations/db-features.ts` export `testMutationFeatures` becomes `appflare.mutations["db-features"].testMutationFeatures`
|
|
465
|
+
|
|
466
|
+
### 3.4 Context utilities available in handlers
|
|
467
|
+
|
|
468
|
+
Inside handlers, you commonly use:
|
|
469
|
+
|
|
470
|
+
- `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
|
|
471
|
+
- aggregate helpers like `count` and `avg`
|
|
472
|
+
- `count` supports `where`, `field`, `distinct`, and `with` for filtered relation counts
|
|
473
|
+
- `avg` supports `where`, `field`, `distinct`, and `with` for filtered relation averages
|
|
474
|
+
- `where` supports shorthand operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `exists`, `regex`, `$options`, `includes`, `includesAny`, `length`
|
|
475
|
+
- `with` supports `_count` and `_avg` for relation-level aggregate results (returned as `XxxAggregate`)
|
|
476
|
+
- `orderBy` accepts `{ column, direction }` or array thereof
|
|
477
|
+
- `geoWithin` for geospatial distance queries (Haversine formula)
|
|
478
|
+
- `upsert` supports `target` (conflict columns) and `set` (on-conflict update columns)
|
|
479
|
+
- `ctx.error(status, message, details)` for typed failures
|
|
480
|
+
|
|
481
|
+
#### Updating manyToMany relations
|
|
482
|
+
|
|
483
|
+
The `update` operation supports managing manyToMany relations via the `set` payload. Each manyToMany relation field accepts an object with `items` and optional `mode`:
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
// Merge mode (default) — adds new links, keeps existing ones
|
|
487
|
+
await ctx.db.family.update({
|
|
488
|
+
where: { primaryUserId: userId },
|
|
489
|
+
set: {
|
|
490
|
+
members: {
|
|
491
|
+
items: [
|
|
492
|
+
"existing-user-id", // link by ID
|
|
493
|
+
{ id: "another-user-id" }, // link by ID (object form)
|
|
494
|
+
{ name: "New User", email: "..." }, // create new user, then link
|
|
495
|
+
],
|
|
496
|
+
mode: "merge",
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Overwrite mode — deletes all existing links, keeps only new ones
|
|
502
|
+
await ctx.db.family.update({
|
|
503
|
+
where: { primaryUserId: userId },
|
|
504
|
+
set: {
|
|
505
|
+
members: {
|
|
506
|
+
items: ["user-id-1", "user-id-2"],
|
|
507
|
+
mode: "overwrite",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
- `items` — array of IDs (string/number) or partial objects. Objects without an `id` field are created in the target table before linking.
|
|
514
|
+
- `mode` — `"merge"` (default) keeps existing links and adds new ones; `"overwrite"` deletes all existing links for the parent record before inserting new ones.
|
|
515
|
+
- All relation updates run inside a transaction when present.
|
|
516
|
+
|
|
517
|
+
See real examples in:
|
|
518
|
+
|
|
519
|
+
- `packages/backend/src/queries/db-features.ts`
|
|
520
|
+
- `packages/backend/src/mutations/db-features.ts`
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 4) Generate artifacts
|
|
525
|
+
|
|
526
|
+
From backend package:
|
|
527
|
+
|
|
528
|
+
```bash
|
|
529
|
+
cd packages/backend
|
|
530
|
+
bun ../appflare/cli dev
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Or via scripts in `packages/backend/package.json`:
|
|
534
|
+
|
|
535
|
+
```bash
|
|
536
|
+
bun run build
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
What gets generated (core set):
|
|
540
|
+
|
|
541
|
+
- `_generated/server.js`
|
|
542
|
+
- `_generated/client.js`
|
|
543
|
+
- `_generated/auth.config.js`
|
|
544
|
+
- `_generated/drizzle.config.js`
|
|
545
|
+
- `_generated/handlers.js`
|
|
546
|
+
- `_generated/handlers.context.js`
|
|
547
|
+
- `_generated/handlers.execution.js`
|
|
548
|
+
- `_generated/handlers.routes.js`
|
|
549
|
+
- `_generated/client/**`
|
|
550
|
+
|
|
551
|
+
### Watch mode
|
|
552
|
+
|
|
553
|
+
To regenerate on file changes:
|
|
554
|
+
|
|
555
|
+
```bash
|
|
556
|
+
cd packages/backend
|
|
557
|
+
bun ../appflare/cli dev --watch
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 5) How to migrate database schema
|
|
563
|
+
|
|
564
|
+
Appflare migration flow wraps two steps:
|
|
565
|
+
|
|
566
|
+
1. Generate drizzle migrations
|
|
567
|
+
2. Apply to D1 via Wrangler
|
|
568
|
+
|
|
569
|
+
### 5.1 Standard migrate
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
cd packages/backend
|
|
573
|
+
bun ../appflare/cli migrate
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Or script:
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
bun run migrate
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### 5.2 Choose target environment
|
|
583
|
+
|
|
584
|
+
Use exactly one of these flags:
|
|
585
|
+
|
|
586
|
+
- `--local`
|
|
587
|
+
- `--remote`
|
|
588
|
+
- `--preview`
|
|
589
|
+
|
|
590
|
+
Examples:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
bun ../appflare/cli migrate --local
|
|
594
|
+
bun ../appflare/cli migrate --remote
|
|
595
|
+
bun ../appflare/cli migrate --preview
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 5.3 Typical change workflow
|
|
599
|
+
|
|
600
|
+
1. Update `schema.ts`.
|
|
601
|
+
2. Regenerate artifacts:
|
|
602
|
+
- `bun ../appflare/cli dev`
|
|
603
|
+
3. Run migration:
|
|
604
|
+
- `bun ../appflare/cli migrate --local` (or remote/preview)
|
|
605
|
+
4. Verify app behavior in `wrangler dev`.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## 6) Frontend usage (plain TypeScript/JavaScript)
|
|
610
|
+
|
|
611
|
+
Use the generated backend client directly.
|
|
612
|
+
|
|
613
|
+
### 6.1 Create client instance
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
import { Appflare } from "appflare-backend/_generated/client";
|
|
617
|
+
|
|
618
|
+
const appflare = new Appflare({
|
|
619
|
+
endpoint: "http://127.0.0.1:8787",
|
|
620
|
+
wsEndpoint: "ws://127.0.0.1:8787",
|
|
621
|
+
onGetAuthToken: async () => localStorage.getItem("appflare-auth-token") ?? "",
|
|
622
|
+
onSetAuthToken: async (token) => {
|
|
623
|
+
localStorage.setItem("appflare-auth-token", token);
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 6.2 Run a query
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
const result = await appflare.queries.test.getTest.run({ id: "test" });
|
|
632
|
+
|
|
633
|
+
if (result.error) {
|
|
634
|
+
console.error(result.error.status, result.error.message);
|
|
635
|
+
} else {
|
|
636
|
+
console.log(result.data);
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### 6.2.1 More frontend query call examples
|
|
641
|
+
|
|
642
|
+
#### A) Query with filters
|
|
643
|
+
|
|
644
|
+
```ts
|
|
645
|
+
const result = await appflare.queries["db-features"].testQueryFeatures.run({
|
|
646
|
+
search: "test",
|
|
647
|
+
userId: "as3xNgfPVzrooSuSwn1ZSEKNA92Cjp4V",
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
if (!result.error) {
|
|
651
|
+
console.log(result.data.postsCount, result.data.uniqueOwnerCount);
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
#### B) Query with request options
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
const result = await appflare.queries.test.getTest.run(
|
|
659
|
+
{ id: "test" },
|
|
660
|
+
{
|
|
661
|
+
headers: {
|
|
662
|
+
"x-trace-id": crypto.randomUUID(),
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
#### C) Query with realtime and explicit auth token
|
|
669
|
+
|
|
670
|
+
```ts
|
|
671
|
+
const sub = appflare.queries.test.getTest.subscribe({
|
|
672
|
+
args: { id: "test" },
|
|
673
|
+
authToken: "token-from-auth-flow",
|
|
674
|
+
onChange: (data) => {
|
|
675
|
+
console.log("fresh data", data);
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
setTimeout(() => sub.remove(), 30000);
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### 6.3 Run a mutation
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
const result = await appflare.mutations.test.newTest.run({});
|
|
686
|
+
|
|
687
|
+
if (result.error) {
|
|
688
|
+
console.error(result.error.message);
|
|
689
|
+
} else {
|
|
690
|
+
console.log(result.data);
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### 6.4 Realtime subscribe to a query
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
const sub = appflare.queries.test.getTest.subscribe({
|
|
698
|
+
args: { id: "test" },
|
|
699
|
+
onChange: (data, event) => {
|
|
700
|
+
console.log("update", event.payload.queryName, data);
|
|
701
|
+
},
|
|
702
|
+
onError: (error) => {
|
|
703
|
+
console.error("subscription error", error);
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// later
|
|
708
|
+
sub.remove();
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 7) How to use with React
|
|
714
|
+
|
|
715
|
+
Appflare ships React hooks in `appflare/react`:
|
|
716
|
+
|
|
717
|
+
- `useQuery`
|
|
718
|
+
- `useInfiniteQuery`
|
|
719
|
+
- `useMutation`
|
|
720
|
+
|
|
721
|
+
These are thin wrappers around TanStack Query.
|
|
722
|
+
|
|
723
|
+
### 7.1 Setup requirements
|
|
724
|
+
|
|
725
|
+
Install peer requirements in your frontend app:
|
|
726
|
+
|
|
727
|
+
```bash
|
|
728
|
+
bun add @tanstack/react-query react
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
Wrap app with `QueryClientProvider`.
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
735
|
+
|
|
736
|
+
const queryClient = new QueryClient();
|
|
737
|
+
|
|
738
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
739
|
+
return (
|
|
740
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 7.2 React query usage
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
import { useQuery } from "appflare/react";
|
|
749
|
+
import { appflare } from "./appflare-client";
|
|
750
|
+
|
|
751
|
+
export function TestScreen() {
|
|
752
|
+
const query = useQuery(
|
|
753
|
+
appflare.queries.test.getTest,
|
|
754
|
+
{ id: "test" },
|
|
755
|
+
{
|
|
756
|
+
realtime: { enabled: true },
|
|
757
|
+
},
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
if (query.isLoading) return <div>Loading...</div>;
|
|
761
|
+
if (query.error) return <div>{query.error.message}</div>;
|
|
762
|
+
|
|
763
|
+
return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### 7.3 React mutation usage
|
|
768
|
+
|
|
769
|
+
```tsx
|
|
770
|
+
import { useMutation } from "appflare/react";
|
|
771
|
+
import { appflare } from "./appflare-client";
|
|
772
|
+
|
|
773
|
+
export function CreatePostButton() {
|
|
774
|
+
const mutation = useMutation(appflare.mutations.test.newTest, {
|
|
775
|
+
onSuccess: (data) => console.log("created", data),
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
|
780
|
+
Create
|
|
781
|
+
</button>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### 7.4 React infinite query usage
|
|
787
|
+
|
|
788
|
+
```tsx
|
|
789
|
+
import { useInfiniteQuery } from "appflare/react";
|
|
790
|
+
import { appflare } from "./appflare-client";
|
|
791
|
+
|
|
792
|
+
const result = useInfiniteQuery(
|
|
793
|
+
appflare.queries.test.getTest,
|
|
794
|
+
{ id: "test" },
|
|
795
|
+
{
|
|
796
|
+
pageParamToArgs: (baseArgs, page) => ({ ...baseArgs, page }),
|
|
797
|
+
queryOptions: {
|
|
798
|
+
initialPageParam: 1,
|
|
799
|
+
getNextPageParam: (lastPage, pages) => pages.length + 1,
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
);
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
### 7.5 Realtime with React hooks
|
|
806
|
+
|
|
807
|
+
Both `useQuery` and `useInfiniteQuery` support:
|
|
808
|
+
|
|
809
|
+
```ts
|
|
810
|
+
realtime: {
|
|
811
|
+
enabled: true,
|
|
812
|
+
authToken: "optional-token",
|
|
813
|
+
requestOptions: { headers: { "x-custom": "1" } },
|
|
814
|
+
onChange: (data, update) => {},
|
|
815
|
+
onError: (error) => {},
|
|
816
|
+
}
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
When enabled, hooks subscribe via generated query `.subscribe(...)` and keep query cache updated automatically.
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## 8) Frontend app helper pattern
|
|
824
|
+
|
|
825
|
+
A good pattern is to keep one shared client factory in a single file.
|
|
826
|
+
|
|
827
|
+
Example in this workspace:
|
|
828
|
+
|
|
829
|
+
- `apps/app/lib/appflare.ts`
|
|
830
|
+
|
|
831
|
+
This file centralizes:
|
|
832
|
+
|
|
833
|
+
- endpoint/wsEndpoint selection (web/mobile)
|
|
834
|
+
- token storage and retrieval
|
|
835
|
+
- exported hook wrappers
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
## 9) Common commands cheat sheet
|
|
840
|
+
|
|
841
|
+
From `packages/backend`:
|
|
842
|
+
|
|
843
|
+
```bash
|
|
844
|
+
# Generate once
|
|
845
|
+
bun ../appflare/cli build -c appflare.config.ts
|
|
846
|
+
|
|
847
|
+
# Generate in dev mode
|
|
848
|
+
bun ../appflare/cli dev -c appflare.config.ts
|
|
849
|
+
|
|
850
|
+
# Generate + watch
|
|
851
|
+
bun ../appflare/cli dev -c appflare.config.ts --watch
|
|
852
|
+
|
|
853
|
+
# Migrate local D1
|
|
854
|
+
bun ../appflare/cli migrate -c appflare.config.ts --local
|
|
855
|
+
|
|
856
|
+
# Migrate remote D1
|
|
857
|
+
bun ../appflare/cli migrate -c appflare.config.ts --remote
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Or use backend scripts:
|
|
861
|
+
|
|
862
|
+
```bash
|
|
863
|
+
bun run build
|
|
864
|
+
bun run dev
|
|
865
|
+
bun run migrate
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## 10) Troubleshooting
|
|
871
|
+
|
|
872
|
+
### Generated client has missing routes
|
|
873
|
+
|
|
874
|
+
- Ensure handler file is under `scanDir`.
|
|
875
|
+
- Ensure export uses `query(...)` or `mutation(...)`.
|
|
876
|
+
- Run `bun ../appflare/cli dev` again.
|
|
877
|
+
|
|
878
|
+
### Realtime not receiving updates
|
|
879
|
+
|
|
880
|
+
- Ensure query has `.subscribe` in generated client.
|
|
881
|
+
- Ensure `wsEndpoint` is set correctly.
|
|
882
|
+
- Ensure valid auth token is available if your runtime requires auth.
|
|
883
|
+
|
|
884
|
+
### Migration fails
|
|
885
|
+
|
|
886
|
+
- Confirm database values in `appflare.config.ts`.
|
|
887
|
+
- Use one environment flag only (`--local`, `--remote`, or `--preview`).
|
|
888
|
+
- Re-run generation before migration if schema changed.
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## 11) Recommended development loop
|
|
893
|
+
|
|
894
|
+
1. Edit schema and handlers.
|
|
895
|
+
2. Run generator (`dev` or `dev --watch`).
|
|
896
|
+
3. Run migrations.
|
|
897
|
+
4. Start backend (`wrangler dev`).
|
|
898
|
+
5. Use generated client in frontend and iterate.
|