appflare 0.2.25 → 0.2.26
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 +758 -758
- package/cli/commands/index.ts +238 -238
- package/cli/generate.ts +178 -178
- package/cli/index.ts +120 -120
- package/cli/load-config.ts +184 -184
- package/cli/schema-compiler.ts +1183 -1183
- 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 +748 -748
- 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 +180 -180
- package/cli/templates/core/client/types.ts +184 -184
- 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 +171 -171
- package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
- package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +554 -554
- package/cli/templates/dashboard/builders/navigation.ts +122 -122
- 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 +388 -388
- 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 +134 -112
- package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
- package/cli/templates/handlers/generators/context/types.ts +18 -18
- 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 +13 -13
- package/cli/templates/handlers/generators/registration/modules/cron.ts +26 -26
- 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 +516 -516
- package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -56
- package/cli/templates/handlers/generators/registration/modules/storage.ts +196 -194
- package/cli/templates/handlers/generators/registration/sections.ts +210 -210
- package/cli/templates/handlers/generators/types/context.ts +68 -66
- package/cli/templates/handlers/generators/types/core.ts +106 -106
- package/cli/templates/handlers/generators/types/operations.ts +135 -135
- package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +259 -259
- 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 +1031 -1031
- package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +246 -246
- 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 +121 -121
- package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
- package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +676 -676
- package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
- package/cli/templates/handlers/index.ts +43 -43
- package/cli/templates/handlers/operations.ts +116 -116
- package/cli/templates/handlers/registration.ts +91 -91
- package/cli/templates/handlers/types.ts +15 -15
- package/cli/templates/handlers/utils.ts +48 -48
- package/cli/types.ts +110 -110
- package/cli/utils/handler-discovery.ts +466 -466
- 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 +43 -17
- package/dist/cli/index.mjs +43 -17
- package/index.ts +18 -18
- package/package.json +58 -58
- package/react/index.ts +5 -5
- package/react/use-infinite-query.ts +252 -252
- package/react/use-mutation.ts +89 -89
- package/react/use-query.ts +207 -207
- package/schema.ts +415 -415
- package/test-better-auth-hash.ts +2 -2
- package/tsconfig.json +6 -6
- package/tsup.config.ts +82 -82
- package/dist/cli/index.d.mts +0 -2
- package/dist/cli/index.d.ts +0 -2
|
@@ -1,353 +1,353 @@
|
|
|
1
|
-
# Handlers Template System (with Realtime Durable Objects)
|
|
2
|
-
|
|
3
|
-
This directory contains code-generation templates used by the Appflare CLI to build runtime handler files in `_generated/`.
|
|
4
|
-
|
|
5
|
-
The generated runtime now supports **realtime updates** using a **single global Cloudflare Durable Object** and websocket fanout.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## What this template set generates
|
|
10
|
-
|
|
11
|
-
From this directory, the CLI generates:
|
|
12
|
-
|
|
13
|
-
- `handlers.ts`
|
|
14
|
-
- `handlers.context.ts`
|
|
15
|
-
- `handlers.execution.ts`
|
|
16
|
-
- `handlers.routes.ts`
|
|
17
|
-
|
|
18
|
-
Key behavior added for realtime:
|
|
19
|
-
|
|
20
|
-
1. Endpoint-first subscriptions (`POST /realtime/subscribe`)
|
|
21
|
-
2. WebSocket connect endpoint (`GET /realtime/ws`)
|
|
22
|
-
3. Per-subscription `token` + `signature`
|
|
23
|
-
4. Query identity contract: `[dirName]/[fileName]/[functionName]`
|
|
24
|
-
5. Args validation against discovered query schema (args-derived keys only)
|
|
25
|
-
6. Mutation-to-query invalidation with **partial filter overlap** matching
|
|
26
|
-
7. Push updated query result to matching subscribers only
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Realtime architecture
|
|
31
|
-
|
|
32
|
-
### Components
|
|
33
|
-
|
|
34
|
-
- **Generated query routes** (`GET /queries/...`)
|
|
35
|
-
- **Generated mutation routes** (`POST /mutations/...`)
|
|
36
|
-
- **Realtime subscription endpoint** (`POST /realtime/subscribe`)
|
|
37
|
-
- **Realtime websocket endpoint** (`GET /realtime/ws?token=...&authToken=...`)
|
|
38
|
-
- **Global Durable Object class**: `AppflareRealtimeDurableObject`
|
|
39
|
-
|
|
40
|
-
### Data flow
|
|
41
|
-
|
|
42
|
-
1. Client calls `POST /realtime/subscribe` with:
|
|
43
|
-
- `queryName`: `[dir]/[file]/[function]`
|
|
44
|
-
- `args`: query arguments
|
|
45
|
-
- `authToken`: bearer token
|
|
46
|
-
2. Server validates:
|
|
47
|
-
- query exists
|
|
48
|
-
- args parse against that query Zod schema
|
|
49
|
-
- auth token resolves to a user session
|
|
50
|
-
3. Server stores subscription inside the global DO and returns:
|
|
51
|
-
- `token`
|
|
52
|
-
- `signature`
|
|
53
|
-
- websocket URL/protocol metadata
|
|
54
|
-
4. Client opens websocket at `GET /realtime/ws?token=...&authToken=...`.
|
|
55
|
-
5. On any mutation (`insert`, `update`, `delete`, `upsert`), generated `db` wrappers collect mutation events.
|
|
56
|
-
6. Matching subscriptions are re-executed (same query + same validated args) and pushed over websocket as `query:update`.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Identity and signatures
|
|
61
|
-
|
|
62
|
-
### Query identity
|
|
63
|
-
|
|
64
|
-
Query names are generated from discovered source layout:
|
|
65
|
-
|
|
66
|
-
`[dirName]/[fileName]/[functionName]`
|
|
67
|
-
|
|
68
|
-
Examples:
|
|
69
|
-
|
|
70
|
-
- `users/profile/getProfile`
|
|
71
|
-
- `root/test/getTest`
|
|
72
|
-
|
|
73
|
-
### Signature
|
|
74
|
-
|
|
75
|
-
Signature is generated from:
|
|
76
|
-
|
|
77
|
-
- `queryName`
|
|
78
|
-
- normalized `args`
|
|
79
|
-
|
|
80
|
-
The generated runtime normalizes/sorts object keys before stringifying, so logically equivalent payloads produce a stable signature.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Partial filter overlap matching
|
|
85
|
-
|
|
86
|
-
When mutations run, subscribers are selected using overlap logic (not strict equality).
|
|
87
|
-
|
|
88
|
-
High-level rules:
|
|
89
|
-
|
|
90
|
-
- Scalars: equal values overlap
|
|
91
|
-
- Arrays: overlap if any element overlaps
|
|
92
|
-
- Objects: overlap if any shared key overlaps recursively
|
|
93
|
-
- Matching checks both:
|
|
94
|
-
- mutation input args (`set`, `where`, `values`, etc.)
|
|
95
|
-
- returned mutation rows
|
|
96
|
-
|
|
97
|
-
This means subscriptions react when filters intersect mutation impact, without requiring exact filter identity.
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
## Authentication model
|
|
102
|
-
|
|
103
|
-
### Subscribe
|
|
104
|
-
|
|
105
|
-
`POST /realtime/subscribe` requires `authToken` in body.
|
|
106
|
-
|
|
107
|
-
The runtime creates a request with `Authorization: Bearer <authToken>` and calls `resolveSession(...)`.
|
|
108
|
-
|
|
109
|
-
If no user resolves, response is `401`.
|
|
110
|
-
|
|
111
|
-
### WebSocket connect
|
|
112
|
-
|
|
113
|
-
`GET /realtime/ws` requires both query params:
|
|
114
|
-
|
|
115
|
-
- `token`
|
|
116
|
-
- `authToken`
|
|
117
|
-
|
|
118
|
-
Server validates token ownership and auth token before websocket upgrade.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Endpoint contracts
|
|
123
|
-
|
|
124
|
-
### 1) Subscribe
|
|
125
|
-
|
|
126
|
-
`POST /realtime/subscribe`
|
|
127
|
-
|
|
128
|
-
Request:
|
|
129
|
-
|
|
130
|
-
```json
|
|
131
|
-
{
|
|
132
|
-
"queryName": "users/profile/getProfile",
|
|
133
|
-
"args": { "userId": "u_123" },
|
|
134
|
-
"authToken": "<token>"
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Success response:
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{
|
|
142
|
-
"token": "<subscription-token>",
|
|
143
|
-
"signature": "users/profile/getProfile::{\"userId\":\"u_123\"}",
|
|
144
|
-
"websocket": {
|
|
145
|
-
"url": "wss://api.example.com/realtime/ws",
|
|
146
|
-
"protocol": "appflare.realtime.v1",
|
|
147
|
-
"params": {
|
|
148
|
-
"tokenParam": "token",
|
|
149
|
-
"authTokenParam": "authToken"
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### 2) WebSocket connect
|
|
156
|
-
|
|
157
|
-
`GET /realtime/ws?token=<token>&authToken=<authToken>`
|
|
158
|
-
|
|
159
|
-
Server pushes messages like:
|
|
160
|
-
|
|
161
|
-
```json
|
|
162
|
-
{
|
|
163
|
-
"event": "query:update",
|
|
164
|
-
"payload": {
|
|
165
|
-
"queryName": "users/profile/getProfile",
|
|
166
|
-
"signature": "...",
|
|
167
|
-
"data": { "id": "u_123", "name": "Ada" }
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Heartbeat support:
|
|
173
|
-
|
|
174
|
-
- client sends: `ping`
|
|
175
|
-
- server replies: `{"event":"pong"}`
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## Mutation event capture
|
|
180
|
-
|
|
181
|
-
`createQueryDb(...)` now accepts options with `onMutation`.
|
|
182
|
-
|
|
183
|
-
The generated wrappers for `insert`, `update`, `upsert`, and `delete` emit:
|
|
184
|
-
|
|
185
|
-
- operation kind
|
|
186
|
-
- table name
|
|
187
|
-
- mutation args
|
|
188
|
-
- returned rows
|
|
189
|
-
|
|
190
|
-
Execution contexts store these as `ctx.mutationEvents`, and mutation routes call `publishMutationEvents(...)` after successful execution.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## DB aggregate helpers
|
|
195
|
-
|
|
196
|
-
Generated `ctx.db.<table>` wrappers now include aggregate helpers:
|
|
197
|
-
|
|
198
|
-
- `count(args?)`
|
|
199
|
-
- `where?: WhereInput<TModel>`
|
|
200
|
-
- `field?: keyof TModel | string` (supports relation paths, e.g. `comments.id`)
|
|
201
|
-
- `distinct?: boolean`
|
|
202
|
-
- `with?: QueryWithInput<...>`
|
|
203
|
-
- returns `Promise<number>`
|
|
204
|
-
- `avg(args)`
|
|
205
|
-
- `where?: WhereInput<TModel>`
|
|
206
|
-
- `field: NumericFieldKey<TModel> | string` (supports relation paths, e.g. `comments.id`)
|
|
207
|
-
- `distinct?: boolean`
|
|
208
|
-
- `with?: QueryWithInput<...>`
|
|
209
|
-
- returns `Promise<number | null>`
|
|
210
|
-
|
|
211
|
-
Aggregate behavior with `with`:
|
|
212
|
-
|
|
213
|
-
- Relation `with.where` and nested `with` filters are treated as parent-row constraints for aggregates (EXISTS-style).
|
|
214
|
-
- For `count` with `with`, `distinct` defaults to `true` when `field` is provided.
|
|
215
|
-
- Nested relation paths are supported recursively for both `count` and `avg`.
|
|
216
|
-
|
|
217
|
-
Relation `with` aggregates on `findMany`/`findFirst`:
|
|
218
|
-
|
|
219
|
-
- You can request per-parent relation aggregates directly inside `with` using `_count` and `_avg`.
|
|
220
|
-
- Result rows include a sibling `<relationName>Aggregate` object.
|
|
221
|
-
- `_avg` returns `0` for parents with no related rows.
|
|
222
|
-
|
|
223
|
-
Example:
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
const total = await ctx.db.posts.count({
|
|
227
|
-
where: { ownerId: user.id },
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const uniqueOwners = await ctx.db.posts.count({
|
|
231
|
-
field: "ownerId",
|
|
232
|
-
distinct: true,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const averageId = await ctx.db.posts.avg({
|
|
236
|
-
field: "id",
|
|
237
|
-
where: { ownerId: user.id },
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const postsWithMatchingComments = await ctx.db.posts.count({
|
|
241
|
-
with: {
|
|
242
|
-
comments: {
|
|
243
|
-
where: {
|
|
244
|
-
id: { gte: 10000 },
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const averageCommentId = await ctx.db.posts.avg({
|
|
251
|
-
field: "comments.id",
|
|
252
|
-
with: {
|
|
253
|
-
comments: {
|
|
254
|
-
where: {
|
|
255
|
-
id: { gte: 10000 },
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const postsWithCommentStats = await ctx.db.posts.findMany({
|
|
262
|
-
with: {
|
|
263
|
-
comments: {
|
|
264
|
-
_count: true,
|
|
265
|
-
_avg: {
|
|
266
|
-
id: true,
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const firstPostCommentCount =
|
|
273
|
-
postsWithCommentStats[0]?.commentsAggregate.count ?? 0;
|
|
274
|
-
const firstPostAverageCommentId =
|
|
275
|
-
postsWithCommentStats[0]?.commentsAggregate.avg.id ?? 0;
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
`geoWithin.latitudeField` and `geoWithin.longitudeField` are now typed to table keys (instead of free-form strings). Invalid field names still no-op the geo filter at runtime, but now emit a warning.
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## Durable Object responsibilities
|
|
283
|
-
|
|
284
|
-
`AppflareRealtimeDurableObject` keeps in-memory maps for:
|
|
285
|
-
|
|
286
|
-
- `subscriptions` (`token -> metadata`)
|
|
287
|
-
- `sockets` (`token -> websocket`)
|
|
288
|
-
|
|
289
|
-
Supported internal routes:
|
|
290
|
-
|
|
291
|
-
- `POST /subscribe`
|
|
292
|
-
- `POST /subscriptions`
|
|
293
|
-
- `POST /emit`
|
|
294
|
-
- `GET /ws`
|
|
295
|
-
|
|
296
|
-
This design centralizes fanout in one global app DO instance.
|
|
297
|
-
|
|
298
|
-
---
|
|
299
|
-
|
|
300
|
-
## Generated client support
|
|
301
|
-
|
|
302
|
-
The client generator exposes realtime helper APIs:
|
|
303
|
-
|
|
304
|
-
- `appflare.realtime.subscribe(...)`
|
|
305
|
-
|
|
306
|
-
Types include:
|
|
307
|
-
|
|
308
|
-
- `RealtimeSubscriptionRequest`
|
|
309
|
-
- `RealtimeSubscriptionResponse`
|
|
310
|
-
|
|
311
|
-
The client performs endpoint-first subscription; websocket connection is then established using returned metadata.
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
|
-
## Configuration knobs (from app config)
|
|
316
|
-
|
|
317
|
-
Realtime defaults are normalized from `realtime` config:
|
|
318
|
-
|
|
319
|
-
- `enabled` (default `true`)
|
|
320
|
-
- `binding` (default `APPFLARE_REALTIME`)
|
|
321
|
-
- `className` (default `AppflareRealtimeDurableObject`)
|
|
322
|
-
- `objectName` (default `global`)
|
|
323
|
-
- `subscribePath` (default `/realtime/subscribe`)
|
|
324
|
-
- `websocketPath` (default `/realtime/ws`)
|
|
325
|
-
- `protocol` (default `appflare.realtime.v1`)
|
|
326
|
-
|
|
327
|
-
Wrangler generation automatically emits:
|
|
328
|
-
|
|
329
|
-
- `durable_objects.bindings`
|
|
330
|
-
- `migrations` with DO class
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
## Notes and limitations
|
|
335
|
-
|
|
336
|
-
1. Current DO storage is in-memory; restarts drop live subscriptions.
|
|
337
|
-
2. Matching is overlap-based and intentionally permissive for realtime invalidation.
|
|
338
|
-
3. Query re-execution occurs on matching mutation events and can be tuned later for batching/debouncing.
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
342
|
-
## Safe extension points
|
|
343
|
-
|
|
344
|
-
- `registration.ts`:
|
|
345
|
-
- realtime routes, token/session policy, protocol envelopes
|
|
346
|
-
- `types.ts`:
|
|
347
|
-
- mutation event payload contracts
|
|
348
|
-
- `generators/context/context-creation.ts`:
|
|
349
|
-
- context-level mutation event tracking
|
|
350
|
-
- `utils/handler-discovery.ts`:
|
|
351
|
-
- query identity strategy
|
|
352
|
-
|
|
353
|
-
After template changes, regenerate and validate `_generated` output.
|
|
1
|
+
# Handlers Template System (with Realtime Durable Objects)
|
|
2
|
+
|
|
3
|
+
This directory contains code-generation templates used by the Appflare CLI to build runtime handler files in `_generated/`.
|
|
4
|
+
|
|
5
|
+
The generated runtime now supports **realtime updates** using a **single global Cloudflare Durable Object** and websocket fanout.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What this template set generates
|
|
10
|
+
|
|
11
|
+
From this directory, the CLI generates:
|
|
12
|
+
|
|
13
|
+
- `handlers.ts`
|
|
14
|
+
- `handlers.context.ts`
|
|
15
|
+
- `handlers.execution.ts`
|
|
16
|
+
- `handlers.routes.ts`
|
|
17
|
+
|
|
18
|
+
Key behavior added for realtime:
|
|
19
|
+
|
|
20
|
+
1. Endpoint-first subscriptions (`POST /realtime/subscribe`)
|
|
21
|
+
2. WebSocket connect endpoint (`GET /realtime/ws`)
|
|
22
|
+
3. Per-subscription `token` + `signature`
|
|
23
|
+
4. Query identity contract: `[dirName]/[fileName]/[functionName]`
|
|
24
|
+
5. Args validation against discovered query schema (args-derived keys only)
|
|
25
|
+
6. Mutation-to-query invalidation with **partial filter overlap** matching
|
|
26
|
+
7. Push updated query result to matching subscribers only
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Realtime architecture
|
|
31
|
+
|
|
32
|
+
### Components
|
|
33
|
+
|
|
34
|
+
- **Generated query routes** (`GET /queries/...`)
|
|
35
|
+
- **Generated mutation routes** (`POST /mutations/...`)
|
|
36
|
+
- **Realtime subscription endpoint** (`POST /realtime/subscribe`)
|
|
37
|
+
- **Realtime websocket endpoint** (`GET /realtime/ws?token=...&authToken=...`)
|
|
38
|
+
- **Global Durable Object class**: `AppflareRealtimeDurableObject`
|
|
39
|
+
|
|
40
|
+
### Data flow
|
|
41
|
+
|
|
42
|
+
1. Client calls `POST /realtime/subscribe` with:
|
|
43
|
+
- `queryName`: `[dir]/[file]/[function]`
|
|
44
|
+
- `args`: query arguments
|
|
45
|
+
- `authToken`: bearer token
|
|
46
|
+
2. Server validates:
|
|
47
|
+
- query exists
|
|
48
|
+
- args parse against that query Zod schema
|
|
49
|
+
- auth token resolves to a user session
|
|
50
|
+
3. Server stores subscription inside the global DO and returns:
|
|
51
|
+
- `token`
|
|
52
|
+
- `signature`
|
|
53
|
+
- websocket URL/protocol metadata
|
|
54
|
+
4. Client opens websocket at `GET /realtime/ws?token=...&authToken=...`.
|
|
55
|
+
5. On any mutation (`insert`, `update`, `delete`, `upsert`), generated `db` wrappers collect mutation events.
|
|
56
|
+
6. Matching subscriptions are re-executed (same query + same validated args) and pushed over websocket as `query:update`.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Identity and signatures
|
|
61
|
+
|
|
62
|
+
### Query identity
|
|
63
|
+
|
|
64
|
+
Query names are generated from discovered source layout:
|
|
65
|
+
|
|
66
|
+
`[dirName]/[fileName]/[functionName]`
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
|
|
70
|
+
- `users/profile/getProfile`
|
|
71
|
+
- `root/test/getTest`
|
|
72
|
+
|
|
73
|
+
### Signature
|
|
74
|
+
|
|
75
|
+
Signature is generated from:
|
|
76
|
+
|
|
77
|
+
- `queryName`
|
|
78
|
+
- normalized `args`
|
|
79
|
+
|
|
80
|
+
The generated runtime normalizes/sorts object keys before stringifying, so logically equivalent payloads produce a stable signature.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Partial filter overlap matching
|
|
85
|
+
|
|
86
|
+
When mutations run, subscribers are selected using overlap logic (not strict equality).
|
|
87
|
+
|
|
88
|
+
High-level rules:
|
|
89
|
+
|
|
90
|
+
- Scalars: equal values overlap
|
|
91
|
+
- Arrays: overlap if any element overlaps
|
|
92
|
+
- Objects: overlap if any shared key overlaps recursively
|
|
93
|
+
- Matching checks both:
|
|
94
|
+
- mutation input args (`set`, `where`, `values`, etc.)
|
|
95
|
+
- returned mutation rows
|
|
96
|
+
|
|
97
|
+
This means subscriptions react when filters intersect mutation impact, without requiring exact filter identity.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Authentication model
|
|
102
|
+
|
|
103
|
+
### Subscribe
|
|
104
|
+
|
|
105
|
+
`POST /realtime/subscribe` requires `authToken` in body.
|
|
106
|
+
|
|
107
|
+
The runtime creates a request with `Authorization: Bearer <authToken>` and calls `resolveSession(...)`.
|
|
108
|
+
|
|
109
|
+
If no user resolves, response is `401`.
|
|
110
|
+
|
|
111
|
+
### WebSocket connect
|
|
112
|
+
|
|
113
|
+
`GET /realtime/ws` requires both query params:
|
|
114
|
+
|
|
115
|
+
- `token`
|
|
116
|
+
- `authToken`
|
|
117
|
+
|
|
118
|
+
Server validates token ownership and auth token before websocket upgrade.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Endpoint contracts
|
|
123
|
+
|
|
124
|
+
### 1) Subscribe
|
|
125
|
+
|
|
126
|
+
`POST /realtime/subscribe`
|
|
127
|
+
|
|
128
|
+
Request:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"queryName": "users/profile/getProfile",
|
|
133
|
+
"args": { "userId": "u_123" },
|
|
134
|
+
"authToken": "<token>"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Success response:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"token": "<subscription-token>",
|
|
143
|
+
"signature": "users/profile/getProfile::{\"userId\":\"u_123\"}",
|
|
144
|
+
"websocket": {
|
|
145
|
+
"url": "wss://api.example.com/realtime/ws",
|
|
146
|
+
"protocol": "appflare.realtime.v1",
|
|
147
|
+
"params": {
|
|
148
|
+
"tokenParam": "token",
|
|
149
|
+
"authTokenParam": "authToken"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 2) WebSocket connect
|
|
156
|
+
|
|
157
|
+
`GET /realtime/ws?token=<token>&authToken=<authToken>`
|
|
158
|
+
|
|
159
|
+
Server pushes messages like:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"event": "query:update",
|
|
164
|
+
"payload": {
|
|
165
|
+
"queryName": "users/profile/getProfile",
|
|
166
|
+
"signature": "...",
|
|
167
|
+
"data": { "id": "u_123", "name": "Ada" }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Heartbeat support:
|
|
173
|
+
|
|
174
|
+
- client sends: `ping`
|
|
175
|
+
- server replies: `{"event":"pong"}`
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Mutation event capture
|
|
180
|
+
|
|
181
|
+
`createQueryDb(...)` now accepts options with `onMutation`.
|
|
182
|
+
|
|
183
|
+
The generated wrappers for `insert`, `update`, `upsert`, and `delete` emit:
|
|
184
|
+
|
|
185
|
+
- operation kind
|
|
186
|
+
- table name
|
|
187
|
+
- mutation args
|
|
188
|
+
- returned rows
|
|
189
|
+
|
|
190
|
+
Execution contexts store these as `ctx.mutationEvents`, and mutation routes call `publishMutationEvents(...)` after successful execution.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## DB aggregate helpers
|
|
195
|
+
|
|
196
|
+
Generated `ctx.db.<table>` wrappers now include aggregate helpers:
|
|
197
|
+
|
|
198
|
+
- `count(args?)`
|
|
199
|
+
- `where?: WhereInput<TModel>`
|
|
200
|
+
- `field?: keyof TModel | string` (supports relation paths, e.g. `comments.id`)
|
|
201
|
+
- `distinct?: boolean`
|
|
202
|
+
- `with?: QueryWithInput<...>`
|
|
203
|
+
- returns `Promise<number>`
|
|
204
|
+
- `avg(args)`
|
|
205
|
+
- `where?: WhereInput<TModel>`
|
|
206
|
+
- `field: NumericFieldKey<TModel> | string` (supports relation paths, e.g. `comments.id`)
|
|
207
|
+
- `distinct?: boolean`
|
|
208
|
+
- `with?: QueryWithInput<...>`
|
|
209
|
+
- returns `Promise<number | null>`
|
|
210
|
+
|
|
211
|
+
Aggregate behavior with `with`:
|
|
212
|
+
|
|
213
|
+
- Relation `with.where` and nested `with` filters are treated as parent-row constraints for aggregates (EXISTS-style).
|
|
214
|
+
- For `count` with `with`, `distinct` defaults to `true` when `field` is provided.
|
|
215
|
+
- Nested relation paths are supported recursively for both `count` and `avg`.
|
|
216
|
+
|
|
217
|
+
Relation `with` aggregates on `findMany`/`findFirst`:
|
|
218
|
+
|
|
219
|
+
- You can request per-parent relation aggregates directly inside `with` using `_count` and `_avg`.
|
|
220
|
+
- Result rows include a sibling `<relationName>Aggregate` object.
|
|
221
|
+
- `_avg` returns `0` for parents with no related rows.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const total = await ctx.db.posts.count({
|
|
227
|
+
where: { ownerId: user.id },
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const uniqueOwners = await ctx.db.posts.count({
|
|
231
|
+
field: "ownerId",
|
|
232
|
+
distinct: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const averageId = await ctx.db.posts.avg({
|
|
236
|
+
field: "id",
|
|
237
|
+
where: { ownerId: user.id },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const postsWithMatchingComments = await ctx.db.posts.count({
|
|
241
|
+
with: {
|
|
242
|
+
comments: {
|
|
243
|
+
where: {
|
|
244
|
+
id: { gte: 10000 },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const averageCommentId = await ctx.db.posts.avg({
|
|
251
|
+
field: "comments.id",
|
|
252
|
+
with: {
|
|
253
|
+
comments: {
|
|
254
|
+
where: {
|
|
255
|
+
id: { gte: 10000 },
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const postsWithCommentStats = await ctx.db.posts.findMany({
|
|
262
|
+
with: {
|
|
263
|
+
comments: {
|
|
264
|
+
_count: true,
|
|
265
|
+
_avg: {
|
|
266
|
+
id: true,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const firstPostCommentCount =
|
|
273
|
+
postsWithCommentStats[0]?.commentsAggregate.count ?? 0;
|
|
274
|
+
const firstPostAverageCommentId =
|
|
275
|
+
postsWithCommentStats[0]?.commentsAggregate.avg.id ?? 0;
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`geoWithin.latitudeField` and `geoWithin.longitudeField` are now typed to table keys (instead of free-form strings). Invalid field names still no-op the geo filter at runtime, but now emit a warning.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Durable Object responsibilities
|
|
283
|
+
|
|
284
|
+
`AppflareRealtimeDurableObject` keeps in-memory maps for:
|
|
285
|
+
|
|
286
|
+
- `subscriptions` (`token -> metadata`)
|
|
287
|
+
- `sockets` (`token -> websocket`)
|
|
288
|
+
|
|
289
|
+
Supported internal routes:
|
|
290
|
+
|
|
291
|
+
- `POST /subscribe`
|
|
292
|
+
- `POST /subscriptions`
|
|
293
|
+
- `POST /emit`
|
|
294
|
+
- `GET /ws`
|
|
295
|
+
|
|
296
|
+
This design centralizes fanout in one global app DO instance.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Generated client support
|
|
301
|
+
|
|
302
|
+
The client generator exposes realtime helper APIs:
|
|
303
|
+
|
|
304
|
+
- `appflare.realtime.subscribe(...)`
|
|
305
|
+
|
|
306
|
+
Types include:
|
|
307
|
+
|
|
308
|
+
- `RealtimeSubscriptionRequest`
|
|
309
|
+
- `RealtimeSubscriptionResponse`
|
|
310
|
+
|
|
311
|
+
The client performs endpoint-first subscription; websocket connection is then established using returned metadata.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Configuration knobs (from app config)
|
|
316
|
+
|
|
317
|
+
Realtime defaults are normalized from `realtime` config:
|
|
318
|
+
|
|
319
|
+
- `enabled` (default `true`)
|
|
320
|
+
- `binding` (default `APPFLARE_REALTIME`)
|
|
321
|
+
- `className` (default `AppflareRealtimeDurableObject`)
|
|
322
|
+
- `objectName` (default `global`)
|
|
323
|
+
- `subscribePath` (default `/realtime/subscribe`)
|
|
324
|
+
- `websocketPath` (default `/realtime/ws`)
|
|
325
|
+
- `protocol` (default `appflare.realtime.v1`)
|
|
326
|
+
|
|
327
|
+
Wrangler generation automatically emits:
|
|
328
|
+
|
|
329
|
+
- `durable_objects.bindings`
|
|
330
|
+
- `migrations` with DO class
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Notes and limitations
|
|
335
|
+
|
|
336
|
+
1. Current DO storage is in-memory; restarts drop live subscriptions.
|
|
337
|
+
2. Matching is overlap-based and intentionally permissive for realtime invalidation.
|
|
338
|
+
3. Query re-execution occurs on matching mutation events and can be tuned later for batching/debouncing.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Safe extension points
|
|
343
|
+
|
|
344
|
+
- `registration.ts`:
|
|
345
|
+
- realtime routes, token/session policy, protocol envelopes
|
|
346
|
+
- `types.ts`:
|
|
347
|
+
- mutation event payload contracts
|
|
348
|
+
- `generators/context/context-creation.ts`:
|
|
349
|
+
- context-level mutation event tracking
|
|
350
|
+
- `utils/handler-discovery.ts`:
|
|
351
|
+
- query identity strategy
|
|
352
|
+
|
|
353
|
+
After template changes, regenerate and validate `_generated` output.
|