okengine 0.19.8 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +8 -8
- package/site/content/docs/client/index.mdx +4 -4
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +24 -8
- package/site/content/docs/elements/flow/index.mdx +15 -12
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/store/index.mdx +11 -3
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +91 -33
- package/site/content/docs/elements/vault/index.mdx +3 -5
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +13 -4
- package/site/content/docs/reference/configuration.mdx +7 -7
- package/site/content/docs/reference/errors.mdx +30 -30
- package/site/content/docs/reference/fx.mdx +16 -8
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/try-it.mdx +2 -0
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/ask-vault-gaps.test.ts +60 -4
- package/src/cli/ask-vault-gaps.ts +59 -3
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +12 -4
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +18 -12
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/config/index.ts +6 -4
- package/src/console/ui-next/dist/assets/{access-page-C5yG4aS2.js → access-page-DFLu0wTA.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-D2ToVK86.js → agent-disclosure-DGscxaF5.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-7HUR0kCz.js → cache-glyph-BGmRZk7d.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-D0ky3aXt.js → call-pii-button--feUYxvG.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BLUHH2dB.js → collapsible-JWvpaiGY.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-BgHEFMtm.js → duration-tone-D9yCJG4n.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DDybqshQ.js → flows-page-Bs6MD9GB.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BFiJKYV4.js → highlighted-json-xH8MrEnv.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BVAcB0bI.js → http-method-C4vB6ZIw.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-BID6LSYI.js → index-yTCY4AcS.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CANo_mMK.js → observability-page-BxJ3R6dU.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CqQGCshp.js → replica-lag-QRKB_IE8.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DyUDkEt4.js → request-meta-DqZ-fMu5.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CA0rJ_Ow.js → store-page-Dixb6L7a.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DjH_fMzI.js → trace-detail-sheet-CazhjtiU.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWSomIeQ.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Cz9TIViw.js → units-page-BXTLjU2-.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Bb1o1O9p.js → vault-page-39KR__bc.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-session.ts +44 -2
- package/src/elements/store/table.ts +8 -6
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +26 -32
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.ts +12 -2
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- package/src/runs/duckdb.test.ts +2 -2
|
@@ -6,8 +6,8 @@ source: "docs/spec/unified-theory.md"
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
HTTP routes are inferred from where a Flow file lives. Put `http.get()` in
|
|
9
|
-
`src/flows/
|
|
10
|
-
Flow `
|
|
9
|
+
`src/flows/links/[code]/get.ts` and the compiler stamps `GET /links/:code`, names the
|
|
10
|
+
Flow `links.get`, and the client calls `api.links.get({ code })`.
|
|
11
11
|
|
|
12
12
|
<Callout title="The one rule">
|
|
13
13
|
On a tree file, omit path and name: `on(http.get(), flow({ do }))`. The file
|
|
@@ -22,8 +22,9 @@ Flow `notes.get`, and the client calls `api.notes.get({ id })`.
|
|
|
22
22
|
| **HTTP path** | Tree file under `src/flows/<unit>/` — `http.get()`, `http.post()`, … | Barrel `index.ts`; a public URL that must not match the folder; `http.resource(path, ops)`; custom live SSE path |
|
|
23
23
|
| **Flow name** | Same tree file — `flow({ do })` stamps `unit.export` | Barrel (optional — export still stamps); stable name across moves; call-only Flow you `fx.call` by name |
|
|
24
24
|
|
|
25
|
-
**Consequence:**
|
|
26
|
-
|
|
25
|
+
**Consequence:** create-oke `shorter` is this tree — `http.get()` + `flow({ … })` with no
|
|
26
|
+
string path and no string name, except where the public URL must not follow the folder
|
|
27
|
+
(`GET /:code`).
|
|
27
28
|
|
|
28
29
|
## Smallest Example
|
|
29
30
|
|
|
@@ -32,14 +33,15 @@ Flow `notes.get`, and the client calls `api.notes.get({ id })`.
|
|
|
32
33
|
<Step>
|
|
33
34
|
### Place the file in the tree
|
|
34
35
|
|
|
35
|
-
`oke dev` / `oke build` regenerate `src/flows/
|
|
36
|
+
`oke dev` / `oke build` regenerate `src/flows/index.ts`. Import it before
|
|
36
37
|
`oke()` so pathless triggers receive their stamp:
|
|
37
38
|
|
|
38
39
|
```typescript title="src/app.ts"
|
|
39
|
-
import "@/
|
|
40
|
-
import
|
|
40
|
+
import "@/core";
|
|
41
|
+
import "@/flows";
|
|
42
|
+
import { oke } from "okengine/http";
|
|
41
43
|
|
|
42
|
-
export const app = oke({ name: "
|
|
44
|
+
export const app = oke({ name: "app" });
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
</Step>
|
|
@@ -47,20 +49,20 @@ export const app = oke({ name: "notes" });
|
|
|
47
49
|
<Step>
|
|
48
50
|
### Use pathless `http.get()`
|
|
49
51
|
|
|
50
|
-
The tree stamps `GET /
|
|
51
|
-
client method (`api.
|
|
52
|
+
The tree stamps `GET /links/:code`. `:code` merges into `in`. The export name is the
|
|
53
|
+
client method (`api.links.get`):
|
|
52
54
|
|
|
53
|
-
```typescript title="src/flows/
|
|
55
|
+
```typescript title="src/flows/links/[code]/get.ts"
|
|
54
56
|
import { on, flow, http } from "okengine";
|
|
55
57
|
import { z } from "zod";
|
|
56
58
|
|
|
57
59
|
export const get = on(
|
|
58
60
|
http.get({
|
|
59
|
-
in: z.object({
|
|
60
|
-
out: z.object({
|
|
61
|
+
in: z.object({ code: z.string() }),
|
|
62
|
+
out: z.object({ code: z.string(), url: z.string() }),
|
|
61
63
|
}),
|
|
62
64
|
flow({
|
|
63
|
-
do: async ({
|
|
65
|
+
do: async ({ code }) => ({ code, url: "https://example.com" }),
|
|
64
66
|
}),
|
|
65
67
|
);
|
|
66
68
|
```
|
|
@@ -71,14 +73,14 @@ export const get = on(
|
|
|
71
73
|
### Call the endpoint
|
|
72
74
|
|
|
73
75
|
```bash
|
|
74
|
-
curl -X GET http://localhost:6530/
|
|
76
|
+
curl -X GET http://localhost:6530/links/ok -H "accept: application/json"
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
Response:
|
|
78
80
|
|
|
79
81
|
```json
|
|
80
82
|
{
|
|
81
|
-
"data": { "
|
|
83
|
+
"data": { "code": "ok", "url": "https://example.com" },
|
|
82
84
|
"error": null
|
|
83
85
|
}
|
|
84
86
|
```
|
|
@@ -95,16 +97,17 @@ Response:
|
|
|
95
97
|
|
|
96
98
|
## Progressive Patterns
|
|
97
99
|
|
|
98
|
-
From a pathless tree file to an explicit barrel, an action leaf, and a
|
|
100
|
+
From a pathless tree file to an explicit barrel, an action leaf, and a public URL that must
|
|
101
|
+
not follow the folder:
|
|
99
102
|
|
|
100
|
-
<Tabs items={["Tree", "Barrel", "Action", "
|
|
103
|
+
<Tabs items={["Tree", "Barrel", "Action", "Override"]}>
|
|
101
104
|
|
|
102
105
|
<Tab value="Tree">
|
|
103
106
|
|
|
104
107
|
One file per route. Skip the path argument. The generated barrel calls
|
|
105
108
|
`stampHttpPath` / `stampFlowName` after import:
|
|
106
109
|
|
|
107
|
-
```typescript title="src/flows/
|
|
110
|
+
```typescript title="src/flows/links/list.ts"
|
|
108
111
|
import { on, flow, http } from "okengine";
|
|
109
112
|
|
|
110
113
|
export const list = on(
|
|
@@ -115,7 +118,7 @@ export const list = on(
|
|
|
115
118
|
);
|
|
116
119
|
```
|
|
117
120
|
|
|
118
|
-
Stamped to `GET /
|
|
121
|
+
Stamped to `GET /links`, Flow `links.list`, client `api.links.list()`.
|
|
119
122
|
|
|
120
123
|
</Tab>
|
|
121
124
|
|
|
@@ -124,21 +127,21 @@ Stamped to `GET /notes`, Flow `notes.list`, client `api.notes.list()`.
|
|
|
124
127
|
A unit that is **only** `index.ts` (plus skip-list files) is a barrel. The
|
|
125
128
|
generated file re-exports it **without** stamping — pass explicit paths:
|
|
126
129
|
|
|
127
|
-
```typescript title="src/flows/
|
|
130
|
+
```typescript title="src/flows/links/index.ts"
|
|
128
131
|
import { on, flow, http } from "okengine";
|
|
129
132
|
import { z } from "zod";
|
|
130
133
|
|
|
131
134
|
export const list = on(
|
|
132
|
-
http.get("/
|
|
133
|
-
flow("
|
|
135
|
+
http.get("/links").public(),
|
|
136
|
+
flow("links.list", {
|
|
134
137
|
do: () => [],
|
|
135
138
|
}),
|
|
136
139
|
);
|
|
137
140
|
|
|
138
141
|
export const create = on(
|
|
139
|
-
http.post("/
|
|
140
|
-
flow("
|
|
141
|
-
do: async ({
|
|
142
|
+
http.post("/links", { in: z.object({ url: z.string().url() }) }),
|
|
143
|
+
flow("links.create", {
|
|
144
|
+
do: async ({ url }, fx) => ({ id: fx.id(), url }),
|
|
142
145
|
}),
|
|
143
146
|
);
|
|
144
147
|
```
|
|
@@ -151,25 +154,48 @@ Pathless `http.get()` inside a barrel stays unresolved and fails boot
|
|
|
151
154
|
<Tab value="Action">
|
|
152
155
|
|
|
153
156
|
A leaf that is not reserved **adds** a segment. `archive.ts` is
|
|
154
|
-
`POST /
|
|
157
|
+
`POST /links/:code/archive`, not `POST /links/:code`:
|
|
155
158
|
|
|
156
|
-
```typescript title="src/flows/
|
|
159
|
+
```typescript title="src/flows/links/[code]/archive.ts"
|
|
157
160
|
import { on, flow, http } from "okengine";
|
|
158
161
|
import { z } from "zod";
|
|
159
162
|
|
|
160
163
|
export const archive = on(
|
|
161
|
-
http.post({ in: z.object({
|
|
164
|
+
http.post({ in: z.object({ code: z.string() }) }),
|
|
162
165
|
flow({
|
|
163
|
-
do: async ({
|
|
166
|
+
do: async ({ code }) => ({ code, archived: true }),
|
|
164
167
|
}),
|
|
165
168
|
);
|
|
166
169
|
```
|
|
167
170
|
|
|
168
171
|
</Tab>
|
|
169
172
|
|
|
170
|
-
<Tab value="
|
|
173
|
+
<Tab value="Override">
|
|
171
174
|
|
|
172
|
-
|
|
175
|
+
Pass the path when the folder would stamp the wrong URL. `redirect.ts` would be
|
|
176
|
+
`GET /links/redirect`. Short URLs must be root-level:
|
|
177
|
+
|
|
178
|
+
```typescript title="src/flows/links/redirect.ts"
|
|
179
|
+
import { on, flow, http } from "okengine";
|
|
180
|
+
import { z } from "zod";
|
|
181
|
+
|
|
182
|
+
export const redirect = on(
|
|
183
|
+
http.get("/:code", { in: z.object({ code: z.string() }) }).public(),
|
|
184
|
+
flow({
|
|
185
|
+
do: async ({ code }) =>
|
|
186
|
+
new Response(null, { status: 302, headers: { Location: `https://example.com/${code}` } }),
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The tree never overwrites an explicit path. Static `/health` · `/links` · `/auth`
|
|
192
|
+
still win over `/:code`.
|
|
193
|
+
|
|
194
|
+
</Tab>
|
|
195
|
+
|
|
196
|
+
</Tabs>
|
|
197
|
+
|
|
198
|
+
Catch-all `[...slug]` becomes `*` on the URL. The request param is always `"*"`, never
|
|
173
199
|
`slug`:
|
|
174
200
|
|
|
175
201
|
```typescript title="src/flows/docs/[...slug]/get.ts"
|
|
@@ -192,56 +218,59 @@ curl -X GET http://localhost:6530/docs/getting-started/install \
|
|
|
192
218
|
`do` receives `{ "*": "getting-started/install" }`. Call
|
|
193
219
|
`api.docs.get({ "*": "a/b/c" })` — not `{ slug }`.
|
|
194
220
|
|
|
195
|
-
</Tab>
|
|
196
|
-
|
|
197
|
-
</Tabs>
|
|
198
|
-
|
|
199
221
|
## Convention Reference
|
|
200
222
|
|
|
201
|
-
| Convention | File
|
|
202
|
-
| ------------- |
|
|
203
|
-
| Dynamic param | `
|
|
204
|
-
| Reserved leaf | `
|
|
205
|
-
| Action leaf | `
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
223
|
+
| Convention | File | Stamped route | Client |
|
|
224
|
+
| ------------- | ------------------------------------------ | --------------------------- | ------------------------------ |
|
|
225
|
+
| Dynamic param | `links/[code]/get.ts` + `http.get()` | `GET /links/:code` | `api.links.get({ code })` |
|
|
226
|
+
| Reserved leaf | `links/list.ts` + `http.get()` | `GET /links` | `api.links.list()` |
|
|
227
|
+
| Action leaf | `links/[code]/archive.ts` + `http.post()` | `POST /links/:code/archive` | `api.links.archive({ code })` |
|
|
228
|
+
| Explicit path | `links/redirect.ts` + `http.get("/:code")` | `GET /:code` | `api.links.redirect({ code })` |
|
|
229
|
+
| Catch-all | `docs/[...slug]/get.ts` + `http.get()` | `GET /docs/*` | `api.docs.get({ "*": "a/b" })` |
|
|
230
|
+
| Route group | `links/(ops)/archive.ts` | `/links/archive` | `api.links.archive` |
|
|
231
|
+
| Root unit | `main/health.ts` + `http.get()` | `GET /health` | `api.main.health()` |
|
|
232
|
+
| Folder root | `main/route.ts` + `http.get()` | `GET /` | `api.main.root()` |
|
|
210
233
|
|
|
211
234
|
## Units
|
|
212
235
|
|
|
213
236
|
The first folder under `src/flows/` is the **client unit**. Nested folders and
|
|
214
237
|
the leaf file build the URL. The `export const` name is the method on that unit.
|
|
215
238
|
|
|
239
|
+
create-oke `-t shorter` ships this tree (`-t blank` is `main/` only):
|
|
240
|
+
|
|
216
241
|
```text
|
|
217
242
|
src/flows/
|
|
218
|
-
├──
|
|
219
|
-
│ ├── list.ts # GET /
|
|
220
|
-
│ ├── create.ts # POST /
|
|
221
|
-
│ ├──
|
|
222
|
-
│
|
|
223
|
-
│
|
|
224
|
-
│
|
|
225
|
-
├──
|
|
226
|
-
│
|
|
227
|
-
│
|
|
243
|
+
├── links/ # Unit: links → api.links.*
|
|
244
|
+
│ ├── list.ts # GET /links (reserved leaf)
|
|
245
|
+
│ ├── create.ts # POST /links (reserved leaf)
|
|
246
|
+
│ ├── redirect.ts # explicit GET /:code
|
|
247
|
+
│ ├── expire.ts # Clock — not HTTP
|
|
248
|
+
│ ├── reach.ts # Clock — not HTTP
|
|
249
|
+
│ ├── shapes.ts # skipped (contracts)
|
|
250
|
+
│ ├── signals.ts # skipped as a route; on() still joins the unit
|
|
251
|
+
│ ├── _shared.ts # skipped (_ prefix)
|
|
252
|
+
│ └── [code]/
|
|
253
|
+
│ ├── get.ts # GET /links/:code
|
|
254
|
+
│ ├── archive.ts # POST /links/:code/archive
|
|
255
|
+
│ └── report.ts # GET /links/:code/report
|
|
228
256
|
└── main/ # Unit: main — prefix omitted from the URL
|
|
229
257
|
├── health.ts # GET /health
|
|
230
|
-
|
|
258
|
+
├── route.ts # GET /
|
|
259
|
+
└── shapes.ts
|
|
231
260
|
```
|
|
232
261
|
|
|
233
262
|
A file sitting directly in `src/flows/` (no unit folder) is not a route.
|
|
234
263
|
|
|
235
|
-
Unit folder names must be valid JS identifiers (`
|
|
236
|
-
inside; `my-
|
|
264
|
+
Unit folder names must be valid JS identifiers (`links`, `main`, `_` allowed
|
|
265
|
+
inside; `my-links` is skipped). Folders starting with `_`, `[`, or `(` are not
|
|
237
266
|
units.
|
|
238
267
|
|
|
239
|
-
**Consequence:** `export const
|
|
240
|
-
`api.
|
|
268
|
+
**Consequence:** `export const getLink` from `get.ts` is `api.links.getLink`, not
|
|
269
|
+
`api.links.get`. Match the export to the name you want on the client.
|
|
241
270
|
|
|
242
271
|
## Path Conventions
|
|
243
272
|
|
|
244
|
-
**Default — pathless.** Omit the path so `
|
|
273
|
+
**Default — pathless.** Omit the path so `src/flows/index.ts` stamps the URL from disk:
|
|
245
274
|
|
|
246
275
|
```typescript
|
|
247
276
|
http.get(); // pending until stampHttpPath runs
|
|
@@ -252,7 +281,7 @@ own the route (barrel, public API shape, resource mount). The tree never
|
|
|
252
281
|
overwrites an explicit path:
|
|
253
282
|
|
|
254
283
|
```typescript
|
|
255
|
-
http.get("
|
|
284
|
+
http.get("/:code");
|
|
256
285
|
```
|
|
257
286
|
|
|
258
287
|
Path params, query string, and JSON body still merge into one object checked by
|
|
@@ -260,7 +289,7 @@ Path params, query string, and JSON body still merge into one object checked by
|
|
|
260
289
|
|
|
261
290
|
### Dynamic parameters
|
|
262
291
|
|
|
263
|
-
`[
|
|
292
|
+
`[code]` → `:code`. The folder name is the param key. Declare the same key on `in`:
|
|
264
293
|
|
|
265
294
|
```typescript title="src/flows/orgs/[orgId]/members/[memberId]/get.ts"
|
|
266
295
|
import { on, flow, http } from "okengine";
|
|
@@ -290,10 +319,10 @@ A folder `(ops)` is omitted from the URL. Use it to group files without adding a
|
|
|
290
319
|
segment:
|
|
291
320
|
|
|
292
321
|
```text
|
|
293
|
-
src/flows/
|
|
322
|
+
src/flows/links/(ops)/archive.ts → /links/archive
|
|
294
323
|
```
|
|
295
324
|
|
|
296
|
-
`(ops)` never enters the Flow name either (`
|
|
325
|
+
`(ops)` never enters the Flow name either (`links.archive`).
|
|
297
326
|
|
|
298
327
|
### The `main` unit
|
|
299
328
|
|
|
@@ -307,35 +336,39 @@ src/flows/notes/(ops)/archive.ts → /notes/archive
|
|
|
307
336
|
|
|
308
337
|
### Skip list
|
|
309
338
|
|
|
310
|
-
These files are never routes (
|
|
339
|
+
These files are never HTTP routes (path inference returns nothing). Walk skips
|
|
340
|
+
them except `signals.ts`, which still contributes `on()` consumers:
|
|
311
341
|
|
|
312
|
-
| Pattern | Why
|
|
313
|
-
| --------------------------- |
|
|
314
|
-
| `
|
|
315
|
-
| `shapes.ts` | Shared Zod / contracts
|
|
316
|
-
| `signals.ts` |
|
|
317
|
-
| `*.test.ts` / `*.test.tsx` | Tests
|
|
318
|
-
| `_` prefix (file or folder) | Private helpers (`
|
|
342
|
+
| Pattern | Why |
|
|
343
|
+
| --------------------------- | --------------------------------------------------------- |
|
|
344
|
+
| `src/flows/index.ts` | Adopt barrel (`oke dev` / `oke build` writes it) |
|
|
345
|
+
| `shapes.ts` | Shared Zod / contracts |
|
|
346
|
+
| `signals.ts` | Declarations. `on()` in the same file still join the unit |
|
|
347
|
+
| `*.test.ts` / `*.test.tsx` | Tests |
|
|
348
|
+
| `_` prefix (file or folder) | Private helpers (`links/_shared.ts`) |
|
|
319
349
|
|
|
320
350
|
Skip-list files may sit next to tree routes. They do **not** turn a tree into a
|
|
321
|
-
barrel.
|
|
351
|
+
barrel. Clock files (`expire.ts`, `reach.ts`) still join the unit — they are not
|
|
352
|
+
HTTP unless they bind `http.*`.
|
|
322
353
|
|
|
323
354
|
## Reserved Leaves
|
|
324
355
|
|
|
325
|
-
To avoid `/
|
|
356
|
+
To avoid `/links/get` and `/orders/list`, these filenames add **no** URL
|
|
326
357
|
segment — the same five CRUD names as `http.resource`, plus folder roots:
|
|
327
358
|
|
|
328
359
|
| Leaf | Typical trigger | Example file | Stamped path |
|
|
329
360
|
| -------- | --------------- | ----------------------------------- | --------------- |
|
|
330
|
-
| `list` | `http.get()` | `
|
|
331
|
-
| `create` | `http.post()` | `
|
|
332
|
-
| `get` | `http.get()` | `
|
|
333
|
-
| `update` | `http.patch()` | `
|
|
334
|
-
| `remove` | `http.delete()` | `
|
|
335
|
-
| `index` | _(barrel only)_ | `
|
|
336
|
-
| `route` | any | `
|
|
337
|
-
|
|
338
|
-
Any other leaf **is** a segment: `query.ts` → `/
|
|
361
|
+
| `list` | `http.get()` | `links/list.ts` | `/links` |
|
|
362
|
+
| `create` | `http.post()` | `links/create.ts` | `/links` |
|
|
363
|
+
| `get` | `http.get()` | `links/[code]/get.ts` | `/links/:code` |
|
|
364
|
+
| `update` | `http.patch()` | `links/[code]/update.ts` | `/links/:code` |
|
|
365
|
+
| `remove` | `http.delete()` | `links/[code]/remove.ts` | `/links/:code` |
|
|
366
|
+
| `index` | _(barrel only)_ | `links/index.ts` | `/links` |
|
|
367
|
+
| `route` | any | `links/route.ts` or `main/route.ts` | `/links` or `/` |
|
|
368
|
+
|
|
369
|
+
Any other leaf **is** a segment: `query.ts` → `/links/query`. `redirect.ts` with
|
|
370
|
+
pathless `http.get()` would stamp `/links/redirect` — pass `http.get("/:code")`
|
|
371
|
+
instead.
|
|
339
372
|
|
|
340
373
|
In a tree unit, do **not** add `index.ts` beside other route files — that is a
|
|
341
374
|
generate error. Use `route.ts` (or `list.ts` / `create.ts`) for the collection
|
|
@@ -344,7 +377,7 @@ root.
|
|
|
344
377
|
## Barrel vs Tree
|
|
345
378
|
|
|
346
379
|
`oke dev` / `oke build` scans each `src/flows/<unit>/` folder and writes
|
|
347
|
-
`
|
|
380
|
+
`src/flows/index.ts`. Two shapes, never mixed:
|
|
348
381
|
|
|
349
382
|
<Tabs items={["Tree", "Barrel", "App entry"]}>
|
|
350
383
|
|
|
@@ -354,15 +387,15 @@ root.
|
|
|
354
387
|
each file and stamps path + name:
|
|
355
388
|
|
|
356
389
|
```typescript
|
|
357
|
-
const
|
|
358
|
-
get: stampHttpPath(stampFlowName(
|
|
359
|
-
list: stampHttpPath(stampFlowName(
|
|
390
|
+
const links = {
|
|
391
|
+
get: stampHttpPath(stampFlowName(links_$code$_get.get, "links.get"), "/links/:code"),
|
|
392
|
+
list: stampHttpPath(stampFlowName(links_list.list, "links.list"), "/links"),
|
|
360
393
|
};
|
|
361
|
-
export {
|
|
362
|
-
registerFlowUnits({
|
|
394
|
+
export { links };
|
|
395
|
+
registerFlowUnits({ links });
|
|
363
396
|
```
|
|
364
397
|
|
|
365
|
-
`oke()` drains `registerFlowUnits` into `$routes`. `.adopt({
|
|
398
|
+
`oke()` drains `registerFlowUnits` into `$routes`. `.adopt({ links })` is
|
|
366
399
|
optional and additive.
|
|
367
400
|
|
|
368
401
|
</Tab>
|
|
@@ -372,12 +405,12 @@ optional and additive.
|
|
|
372
405
|
Only `index.ts` (+ skip-list). Re-export, no stamp:
|
|
373
406
|
|
|
374
407
|
```typescript
|
|
375
|
-
import * as
|
|
376
|
-
export {
|
|
377
|
-
registerFlowUnits({
|
|
408
|
+
import * as links from "./links/index.ts";
|
|
409
|
+
export { links };
|
|
410
|
+
registerFlowUnits({ links });
|
|
378
411
|
```
|
|
379
412
|
|
|
380
|
-
Declare `http.get("/
|
|
413
|
+
Declare `http.get("/links")` and `flow("links.list", {…})` (or rely on adopt to
|
|
381
414
|
stamp the name from the export). Pathless HTTP fails **OKE1040**.
|
|
382
415
|
|
|
383
416
|
</Tab>
|
|
@@ -385,14 +418,15 @@ stamp the name from the export). Pathless HTTP fails **OKE1040**.
|
|
|
385
418
|
<Tab value="App entry">
|
|
386
419
|
|
|
387
420
|
```typescript title="src/app.ts"
|
|
388
|
-
import "@/
|
|
389
|
-
import
|
|
421
|
+
import "@/core";
|
|
422
|
+
import "@/flows";
|
|
423
|
+
import { oke } from "okengine/http";
|
|
390
424
|
|
|
391
|
-
export const app = oke({ name: "
|
|
425
|
+
export const app = oke({ name: "app" });
|
|
392
426
|
export type App = typeof app;
|
|
393
427
|
```
|
|
394
428
|
|
|
395
|
-
Do not edit `
|
|
429
|
+
Do not edit `src/flows/index.ts` by hand. Adding a unit folder without regenerating
|
|
396
430
|
leaves a stale barrel — **OKE1030** in prod / `oke dev` with Compose.
|
|
397
431
|
|
|
398
432
|
</Tab>
|
|
@@ -402,10 +436,10 @@ leaves a stale barrel — **OKE1030** in prod / `oke dev` with Compose.
|
|
|
402
436
|
<Accordions>
|
|
403
437
|
|
|
404
438
|
<Accordion title="Mixed barrel + tree">
|
|
405
|
-
`index.ts` plus `[
|
|
439
|
+
`index.ts` plus `[code]/get.ts` (or any other route file) throws at generate:
|
|
406
440
|
|
|
407
441
|
```text
|
|
408
|
-
Unit "
|
|
442
|
+
Unit "links" mixes a barrel index.ts with tree route files. Use only index.ts (barrel), or move the collection path to route.ts and keep [id]/ beside it.
|
|
409
443
|
```
|
|
410
444
|
|
|
411
445
|
Fix: delete `index.ts` and use `list.ts` / `route.ts`, or fold every route into
|
|
@@ -417,7 +451,7 @@ Fix: delete `index.ts` and use `list.ts` / `route.ts`, or fold every route into
|
|
|
417
451
|
Two files in the same unit cannot share an `export const` name:
|
|
418
452
|
|
|
419
453
|
```text
|
|
420
|
-
Unit "
|
|
454
|
+
Unit "links" exports "get" from both list.ts and route.ts.
|
|
421
455
|
```
|
|
422
456
|
|
|
423
457
|
Rename one export. The client method is the export name, not the filename.
|
|
@@ -425,13 +459,13 @@ Rename one export. The client method is the export name, not the filename.
|
|
|
425
459
|
</Accordion>
|
|
426
460
|
|
|
427
461
|
<Accordion title="Unit-prefix drift">
|
|
428
|
-
`flow("tasks.get", {…})` living under `src/flows/
|
|
462
|
+
`flow("tasks.get", {…})` living under `src/flows/links/` throws:
|
|
429
463
|
|
|
430
464
|
```text
|
|
431
|
-
flow("tasks.…") in
|
|
465
|
+
flow("tasks.…") in links/get.ts does not match the folder "links".
|
|
432
466
|
```
|
|
433
467
|
|
|
434
|
-
Use `flow("
|
|
468
|
+
Use `flow("links.get", {…})`, a nameless `flow({ do })` (stamped `links.get`
|
|
435
469
|
from the export), or move the file.
|
|
436
470
|
|
|
437
471
|
</Accordion>
|
|
@@ -442,18 +476,18 @@ from the export), or move the file.
|
|
|
442
476
|
|
|
443
477
|
Three names, one file:
|
|
444
478
|
|
|
445
|
-
| Surface | Source | Example
|
|
446
|
-
| --------- | ------------------------------------------------------------------- |
|
|
447
|
-
| HTTP path | File tree (default) or explicit `http.get("/x")` | `/
|
|
448
|
-
| Flow name | `unit.export` from `flow({ do })` (default), or `flow("
|
|
449
|
-
| Client | Unit folder + `export const` | `api.
|
|
479
|
+
| Surface | Source | Example |
|
|
480
|
+
| --------- | ------------------------------------------------------------------- | ------------------------- |
|
|
481
|
+
| HTTP path | File tree (default) or explicit `http.get("/x")` | `/links/:code` |
|
|
482
|
+
| Flow name | `unit.export` from `flow({ do })` (default), or `flow("links.get")` | `links.get` |
|
|
483
|
+
| Client | Unit folder + `export const` | `api.links.get({ code })` |
|
|
450
484
|
|
|
451
485
|
Nameless `flow({ do })` is the tree default — same rule as pathless HTTP. Pass
|
|
452
|
-
`flow("
|
|
486
|
+
`flow("links.get")` only for control (stable name, barrel, or matching unit
|
|
453
487
|
prefix). Wrong-unit prefixes fail generate.
|
|
454
488
|
|
|
455
|
-
Non-HTTP files still join the unit. A signal consumer in `
|
|
456
|
-
|
|
489
|
+
Non-HTTP files still join the unit. A signal consumer in `links/signals.ts` is
|
|
490
|
+
`api.links.onCreated` over RPC (`POST /_oke/links/onCreated`), not HTTP.
|
|
457
491
|
|
|
458
492
|
Signal / Clock consumers pass an explicit `flow("…")` name (**OKE1072** if nameless
|
|
459
493
|
outside `src/flows/<unit>/`; **OKE1070** on collision). Clock may write `clock.every(…)`
|
|
@@ -469,11 +503,15 @@ All adopted HTTP bindings go into one matcher. Wrong method on a known path is
|
|
|
469
503
|
| `"default"` | yes | Compiled RegExp (O(1) static map + per-bucket regex for `:id`). Falls back to Trie when a path includes `*` |
|
|
470
504
|
| `"edge"` | | Linear scan, then Trie. No RegExp compile — cold-start / isolates |
|
|
471
505
|
|
|
506
|
+
Static paths win over `:param` on the same method (`GET /health` beats
|
|
507
|
+
`GET /:code`), including the edge linear scan. `okengine/http` defaults to
|
|
508
|
+
`"edge"`.
|
|
509
|
+
|
|
472
510
|
p99 match stays under **1 ms** on the compiled matcher. You do not pick buckets
|
|
473
511
|
by hand — a catch-all in the table selects Trie for the whole app.
|
|
474
512
|
|
|
475
513
|
Duplicate `METHOD + path` fails boot (**OKE1041**), including a resource mount
|
|
476
|
-
plus a handwritten `http.get("/
|
|
514
|
+
plus a handwritten `http.get("/links")`.
|
|
477
515
|
|
|
478
516
|
## Troubleshooting
|
|
479
517
|
|
|
@@ -481,8 +519,8 @@ plus a handwritten `http.get("/notes")`.
|
|
|
481
519
|
|
|
482
520
|
<Accordion title="404 Not Found — route missing">
|
|
483
521
|
No Flow is bound to that method + path. Check the explicit path, or for pathless routes the
|
|
484
|
-
file-tree stamp (`
|
|
485
|
-
the router found no match.
|
|
522
|
+
file-tree stamp (`links/[code]/get.ts` → `GET /links/:code`). A bare `404` with body `Not Found`
|
|
523
|
+
means the router found no match.
|
|
486
524
|
</Accordion>
|
|
487
525
|
|
|
488
526
|
<Accordion title="405 Method Not Allowed on valid route">
|
|
@@ -492,8 +530,8 @@ plus a handwritten `http.get("/notes")`.
|
|
|
492
530
|
|
|
493
531
|
<Accordion title="OKE1040 — pathless trigger never stamped">
|
|
494
532
|
Cause: `Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.` Import
|
|
495
|
-
`@/flows
|
|
496
|
-
|
|
533
|
+
`@/flows`, run `oke dev` / `oke build`, or pass `http.get("/…")`. Barrels do not stamp — they need
|
|
534
|
+
the explicit path.
|
|
497
535
|
</Accordion>
|
|
498
536
|
|
|
499
537
|
<Accordion title="OKE1030 — adopt barrel stale">
|
|
@@ -525,8 +563,8 @@ plus a handwritten `http.get("/notes")`.
|
|
|
525
563
|
</Accordion>
|
|
526
564
|
|
|
527
565
|
<Accordion title="422 — path param missing from in">
|
|
528
|
-
`[
|
|
529
|
-
path is `:
|
|
566
|
+
`[code]` stamps `:code`. `in` must declare `code` (same key). A schema that expects `id` while the
|
|
567
|
+
path is `:code` fails validation before `do`.
|
|
530
568
|
</Accordion>
|
|
531
569
|
|
|
532
570
|
<Accordion title="Catch-all input is empty / wrong key">
|
|
@@ -537,7 +575,13 @@ plus a handwritten `http.get("/notes")`.
|
|
|
537
575
|
|
|
538
576
|
<Accordion title="Unit mixes index.ts with tree files">
|
|
539
577
|
Generate: `Unit "…" mixes a barrel index.ts with tree route files.` Use only `index.ts` (explicit
|
|
540
|
-
paths), or move the collection path to `route.ts` and keep `[
|
|
578
|
+
paths), or move the collection path to `route.ts` and keep `[code]/` beside it.
|
|
579
|
+
</Accordion>
|
|
580
|
+
|
|
581
|
+
<Accordion title="GET /:code swallowed /health">
|
|
582
|
+
Static paths win. If `/health` 404s, a catch-all `/:code` registered first on the edge matcher
|
|
583
|
+
used to win — upgrade. Shorter binds `GET /:code` as an explicit override; `GET /health` still
|
|
584
|
+
matches `main.health`.
|
|
541
585
|
</Accordion>
|
|
542
586
|
|
|
543
587
|
</Accordions>
|
|
@@ -545,7 +589,7 @@ plus a handwritten `http.get("/notes")`.
|
|
|
545
589
|
## Learn more
|
|
546
590
|
|
|
547
591
|
- [HTTP](/docs/elements/flow/http) — verbs, envelopes, `http.resource`, live SSE
|
|
548
|
-
- [Client](/docs/client/calling) — `api.
|
|
592
|
+
- [Client](/docs/client/calling) — `api.links.get`, REST vs RPC, `$routes`
|
|
549
593
|
- [Errors](/docs/reference/errors) — OKE1040 · OKE1030 · OKE1041 · OKE1045 · OKE1070 · OKE1072
|
|
550
594
|
- [The Architecture](/docs/understand/the-architecture) — derived routes, no hand-written table
|
|
551
595
|
- [Gate](/docs/elements/gate) — `.gate(...)` / `.public()` on the same trigger
|
|
@@ -34,7 +34,7 @@ import { gate } from "okengine";
|
|
|
34
34
|
export const member = gate.policy("member", ({ auth }) => !!auth.verified);
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
```typescript title="src/db/schema.
|
|
37
|
+
```typescript title="src/db/schema.ts"
|
|
38
38
|
import { store, field } from "okengine";
|
|
39
39
|
|
|
40
40
|
export const tasks = store.schema.table(
|
|
@@ -193,7 +193,7 @@ Extras are the **third argument** of `store.schema.table(name, cols, extras)`:
|
|
|
193
193
|
`for` accepts `select` · `insert` · `update` · `delete` · `all`. Optional `as`:
|
|
194
194
|
`"permissive"` (default) or `"restrictive"`. Optional `to` limits Postgres roles.
|
|
195
195
|
|
|
196
|
-
```typescript title="src/db/schema.
|
|
196
|
+
```typescript title="src/db/schema.ts"
|
|
197
197
|
import { store, field } from "okengine";
|
|
198
198
|
import { member, bookingCreate } from "@/core/gate";
|
|
199
199
|
|
|
@@ -24,7 +24,7 @@ For developers persisting domain data on okengine — one handle shape, drivers
|
|
|
24
24
|
<Step>
|
|
25
25
|
### Declare a SQL store
|
|
26
26
|
|
|
27
|
-
```typescript title="src/db/schema.
|
|
27
|
+
```typescript title="src/db/schema.ts"
|
|
28
28
|
import { store, field } from "okengine";
|
|
29
29
|
|
|
30
30
|
export const notes = store.schema.table("notes", {
|
|
@@ -49,17 +49,20 @@ import { db, notes } from "@/schema";
|
|
|
49
49
|
export const create = on(
|
|
50
50
|
http.post({
|
|
51
51
|
in: z.object({ title: z.string().min(1) }),
|
|
52
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
52
53
|
}),
|
|
53
54
|
flow({
|
|
54
55
|
do: async ({ title }, fx) => {
|
|
55
56
|
const id = fx.id();
|
|
56
|
-
await fx.store(db).insert(notes).values({ id, title });
|
|
57
|
-
return fx.json.create(
|
|
57
|
+
const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
|
|
58
|
+
return fx.json.create(row);
|
|
58
59
|
},
|
|
59
60
|
}),
|
|
60
61
|
);
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
`out` projects the insert row — `createdAt` strips; Date timestamps become ISO-8601.
|
|
65
|
+
|
|
63
66
|
</Step>
|
|
64
67
|
|
|
65
68
|
<Step>
|
|
@@ -84,6 +87,11 @@ Response:
|
|
|
84
87
|
|
|
85
88
|
</Steps>
|
|
86
89
|
|
|
90
|
+
<Callout title="Two declare layouts">
|
|
91
|
+
One file (`schema.ts`) or a folder (`schema/`, one table per file). Both import as `@/db/schema`.
|
|
92
|
+
Pick one — see [SQL](/docs/elements/store/sql#declaring-stores).
|
|
93
|
+
</Callout>
|
|
94
|
+
|
|
87
95
|
<Callout title="Effects are inferred">
|
|
88
96
|
Every `fx.store` touch is recorded on the Flow as `reads` / `writes` (`sql:app`, `kv:sessions`,
|
|
89
97
|
`files:uploads`, …). That powers the Manifest, Console, cache invalidation, and least privilege —
|