okengine 0.20.0 → 0.21.1
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/package.json +3 -7
- package/site/content/docs/client/calling.mdx +117 -29
- package/site/content/docs/client/index.mdx +3 -3
- package/site/content/docs/elements/flow/http.mdx +18 -15
- package/site/content/docs/elements/flow/index.mdx +26 -18
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +2 -3
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/sql.mdx +9 -6
- package/site/content/docs/elements/vault/index.mdx +11 -8
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +199 -25
- package/site/content/docs/reference/fx.mdx +6 -1
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +758 -25
- package/src/cli/dev-app-runner.ts +2 -1
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +14 -27
- package/src/client/explain.test.ts +252 -0
- package/src/client/explain.ts +272 -0
- package/src/client/live.test.ts +44 -0
- package/src/client/live.ts +44 -101
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +26 -68
- package/src/client/stream.ts +25 -67
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +51 -112
- package/src/client/types.ts +17 -6
- package/src/client/wire.ts +119 -0
- package/src/client-react/live-resource.ts +6 -2
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/interpret.ts +45 -0
- package/src/compiler/response.ts +17 -27
- package/src/console/server/invoke-user-flow.test.ts +8 -2
- package/src/console/server/invoke-user-flow.ts +12 -18
- package/src/console/server/security.gate.test.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-DFLu0wTA.js → access-page-Bgt9bq2r.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-B43CXZZR.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-92uM5MO7.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-DtGVPtcs.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DN7l6zmC.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-BmIR9FV8.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-BMHs-IzK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-BlAEVgNW.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BDf7OAHv.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-BKpaes3n.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-WnVLI-0j.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-B3GLNVfF.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-DMbnAe3f.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-KvFDingJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-C4NdNuxP.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-CBYl0LW_.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/docker/docker.test.ts +3 -3
- package/src/docker/images-config.test.ts +4 -4
- package/src/docker/stack-id.test.ts +1 -1
- package/src/elements/store/files-errors.test.ts +149 -0
- package/src/elements/store/files-errors.ts +189 -0
- package/src/elements/store/kv-errors.test.ts +98 -0
- package/src/elements/store/kv-errors.ts +139 -0
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/sql-errors.test.ts +197 -0
- package/src/elements/store/sql-errors.ts +294 -0
- package/src/elements/store/sql-session.test.ts +52 -0
- package/src/elements/store/sql-session.ts +26 -4
- package/src/elements/store/store-errors.ts +47 -0
- package/src/http.ts +9 -1
- package/src/i18n/catalogs/ar.ts +18 -0
- package/src/i18n/catalogs/en.ts +18 -0
- package/src/index.ts +9 -1
- package/src/kernel/app.ts +23 -4
- package/src/kernel/builtin-errors.test.ts +117 -0
- package/src/kernel/builtin-errors.ts +129 -0
- package/src/kernel/call.test.ts +182 -0
- package/src/kernel/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +23 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +184 -159
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +49 -335
- package/src/kernel/index.ts +12 -1
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/release/build-lib.ts +9 -0
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
|
@@ -344,12 +344,11 @@ export const get = on(
|
|
|
344
344
|
http.get({
|
|
345
345
|
in: z.object({ id: z.string() }),
|
|
346
346
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
347
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
348
347
|
}),
|
|
349
348
|
flow({
|
|
350
349
|
do: async ({ id }, fx) => {
|
|
351
350
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
352
|
-
if (!note) return fx.fail(
|
|
351
|
+
if (!note) return fx.fail.notFound({ id });
|
|
353
352
|
return note;
|
|
354
353
|
},
|
|
355
354
|
}),
|
|
@@ -404,7 +403,6 @@ export const update = on(
|
|
|
404
403
|
id: z.string(),
|
|
405
404
|
title: z.string().min(1).optional(),
|
|
406
405
|
}),
|
|
407
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
408
406
|
}),
|
|
409
407
|
flow({
|
|
410
408
|
do: async ({ id, title }, fx) => {
|
|
@@ -412,7 +410,7 @@ export const update = on(
|
|
|
412
410
|
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
413
411
|
}
|
|
414
412
|
const row = await fx.store(db).findById(notes, id);
|
|
415
|
-
if (!row) return fx.fail(
|
|
413
|
+
if (!row) return fx.fail.notFound({ id });
|
|
416
414
|
return row;
|
|
417
415
|
},
|
|
418
416
|
}),
|
|
@@ -591,7 +589,7 @@ The URL id segment is always `:id`. Update is **PATCH**, not PUT.
|
|
|
591
589
|
| `out` | Schema | _(required)_ | Item shape (get / list / update return) |
|
|
592
590
|
| `update` | Schema | `in` | Patch fields. Wire body is `{ id, …patch }` |
|
|
593
591
|
| `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
|
|
594
|
-
| `errors` | error map |
|
|
592
|
+
| `errors` | error map | — | Extra domain codes. Built-in `NotFound` is always on |
|
|
595
593
|
| `id` | column | table PK | Column bound to `:id` |
|
|
596
594
|
| `list` | object | see List Options | List query grammar (`GET /notes`) |
|
|
597
595
|
| `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
|
|
@@ -917,6 +915,11 @@ export default defineConfig({
|
|
|
917
915
|
`oke db migrate` in that environment.
|
|
918
916
|
</Accordion>
|
|
919
917
|
|
|
918
|
+
<Accordion title="Unique insert is 500 InternalError">
|
|
919
|
+
Unique / exclusion maps to `Conflict` **409**; missing FK to `ForeignKey` **409**. See [Errors ·
|
|
920
|
+
SQL auto-map](/docs/reference/errors#sql-auto-map).
|
|
921
|
+
</Accordion>
|
|
922
|
+
|
|
920
923
|
<Accordion title="update().set().where(): condition required">
|
|
921
924
|
Updates and deletes without a `where` are rejected (`delete().where(): condition required`). Pass
|
|
922
925
|
a Drizzle condition or equality map — or use `delete(table, id)` for PK deletes.
|
|
@@ -976,7 +979,7 @@ export default defineConfig({
|
|
|
976
979
|
- [HTTP · Resources](/docs/elements/flow/http#resources) — mount, live SSE, verb table
|
|
977
980
|
- [Consumers · CDC](/docs/elements/flow/consumers#cdc) — `db.table(…).changed()`
|
|
978
981
|
- [fx](/docs/reference/fx) — `fx.store` session
|
|
979
|
-
- [Errors](/docs/reference/errors) — OKE1110 · OKE1041
|
|
982
|
+
- [Errors](/docs/reference/errors) — OKE1110 · OKE1041 · SQL auto-map
|
|
980
983
|
|
|
981
984
|
## Next
|
|
982
985
|
|
|
@@ -196,13 +196,16 @@ Order (Console labels match these source ids):
|
|
|
196
196
|
3. **`.env.local`** — local overrides (gitignored)
|
|
197
197
|
4. **dev-fallback** — `dev:` on the contract (dev boot only)
|
|
198
198
|
|
|
199
|
-
Miss every layer → `VaultBootError` listing **all** gaps in one pass
|
|
200
|
-
`vault.env.required` names):
|
|
199
|
+
Miss every layer → `VaultBootError` (TTY title **OKE1510**) listing **all** gaps in one pass
|
|
200
|
+
(including `vault.env.required` names):
|
|
201
201
|
|
|
202
202
|
```text
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
-
|
|
203
|
+
◇ OKE1510
|
|
204
|
+
│ 2 secrets have no value in any resolution layer.
|
|
205
|
+
│ - STRIPE_KEY
|
|
206
|
+
│ - DATABASE_URL
|
|
207
|
+
│ → Set each name (`oke vault set <name>`, or `.env.local`).
|
|
208
|
+
│ https://oke.omqkhafi.dev/e/1510
|
|
206
209
|
```
|
|
207
210
|
|
|
208
211
|
**Consequence:** fix every listed name before traffic — boot does not take a half-configured app.
|
|
@@ -297,9 +300,9 @@ Managed provider ids: `aws-secrets-manager` · `azure-key-vault` · `gcp-secret-
|
|
|
297
300
|
<Accordions>
|
|
298
301
|
|
|
299
302
|
<Accordion title="VaultBootError — N missing secret(s)">
|
|
300
|
-
|
|
301
|
-
prompts each into `.env.local`; otherwise use `oke vault set` / env /
|
|
302
|
-
fallback. `vault.env.required` joins the same list.
|
|
303
|
+
TTY title **OKE1510**. Cause: N secrets have no value in any resolution layer, with every gap
|
|
304
|
+
listed. On a TTY, `oke dev` prompts each into `.env.local`; otherwise use `oke vault set` / env /
|
|
305
|
+
managed, or a `dev:` fallback. `vault.env.required` joins the same list.
|
|
303
306
|
</Accordion>
|
|
304
307
|
|
|
305
308
|
<Accordion title='vault: secret "…" is not loaded'>
|
|
@@ -209,9 +209,12 @@ capability check.
|
|
|
209
209
|
Missing non-tenant contracts fail boot with every hole listed once:
|
|
210
210
|
|
|
211
211
|
```text
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
212
|
+
◇ OKE1510
|
|
213
|
+
│ 2 secrets have no value in any resolution layer.
|
|
214
|
+
│ - STRIPE_KEY
|
|
215
|
+
│ - APP_WEBHOOK_SECRET
|
|
216
|
+
│ → Set each name (`oke vault set <name>`, or `.env.local`).
|
|
217
|
+
│ https://oke.omqkhafi.dev/e/1510
|
|
215
218
|
```
|
|
216
219
|
|
|
217
220
|
Fill gaps with:
|
|
@@ -29,7 +29,7 @@ Master the mental model before exploring features:
|
|
|
29
29
|
/>
|
|
30
30
|
<Card
|
|
31
31
|
title="Try It"
|
|
32
|
-
description="
|
|
32
|
+
description="Scaffold, see the files, add GET and POST /users, save a row — then email if you want."
|
|
33
33
|
href="/docs/understand/try-it"
|
|
34
34
|
/>
|
|
35
35
|
</Cards>
|
|
@@ -151,7 +151,7 @@ images: {
|
|
|
151
151
|
store: {
|
|
152
152
|
sql: "postgres:18-alpine",
|
|
153
153
|
kv: "redis:8-alpine",
|
|
154
|
-
files: "rustfs/rustfs:1.0.0
|
|
154
|
+
files: "rustfs/rustfs:1.0.0",
|
|
155
155
|
},
|
|
156
156
|
channel: { email: "axllent/mailpit:v1.31.1" },
|
|
157
157
|
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Errors"
|
|
3
|
-
description: "
|
|
3
|
+
description: "fx.fail helpers, built-in HTTP statuses, OKE codes, and subsystem errors — values vs thrown invariants."
|
|
4
4
|
icon: "OctagonAlert"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
@@ -19,8 +19,8 @@ Codes are stable after the domain-range renumber — match them safely across up
|
|
|
19
19
|
(see the Unreleased Breaking Changes mapping if you still have a pre-renumber note).
|
|
20
20
|
|
|
21
21
|
<Callout title="The one rule">
|
|
22
|
-
Switch on `error.code`
|
|
23
|
-
|
|
22
|
+
Switch on `error.code` (or a `match` named arm) for typed `data`. Use kind arms / `explain` for
|
|
23
|
+
chrome when the code is unknown. Catch `OKE####` only for thrown framework invariants.
|
|
24
24
|
</Callout>
|
|
25
25
|
|
|
26
26
|
## Smallest Example
|
|
@@ -34,12 +34,11 @@ Codes are stable after the domain-range renumber — match them safely across up
|
|
|
34
34
|
on(
|
|
35
35
|
http.get({
|
|
36
36
|
in: z.object({ id: z.string() }),
|
|
37
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
38
37
|
}),
|
|
39
38
|
flow({
|
|
40
39
|
do: async ({ id }, fx) => {
|
|
41
40
|
const [row] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
42
|
-
if (!row) return fx.fail(
|
|
41
|
+
if (!row) return fx.fail.notFound({ id });
|
|
43
42
|
return row;
|
|
44
43
|
},
|
|
45
44
|
}),
|
|
@@ -52,16 +51,163 @@ on(
|
|
|
52
51
|
### Handle it on the client
|
|
53
52
|
|
|
54
53
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
import { match } from "okengine/client/explain";
|
|
55
|
+
|
|
56
|
+
const result = await api.notes.get({ id });
|
|
57
|
+
match(result, {
|
|
58
|
+
ok: (note) => render(note),
|
|
59
|
+
NotFound: () => showMissing(),
|
|
60
|
+
_: (e) => toast(e.message),
|
|
61
|
+
});
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
</Step>
|
|
62
65
|
|
|
63
66
|
</Steps>
|
|
64
67
|
|
|
68
|
+
## `fx.fail`
|
|
69
|
+
|
|
70
|
+
Same function as `import { fail } from "okengine"`. Returns `{ data: null, error }` — it does **not**
|
|
71
|
+
throw. Built-in codes need no `errors:` bag. Domain codes (`OutOfStock`, `FlightFull`, `Duplicate`)
|
|
72
|
+
still go on the exposure:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
on(
|
|
76
|
+
http.post({
|
|
77
|
+
in: z.object({ sku: z.string() }),
|
|
78
|
+
errors: { OutOfStock: z.object({ available: z.number() }) },
|
|
79
|
+
}),
|
|
80
|
+
flow({
|
|
81
|
+
do: async ({ sku }, fx) => {
|
|
82
|
+
const available = 0;
|
|
83
|
+
if (available === 0) return fx.fail("OutOfStock", { available });
|
|
84
|
+
return { sku };
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Optional `{ message }` overrides the catalog: `fx.fail("OutOfStock", data, { message })`.
|
|
91
|
+
|
|
92
|
+
### Helpers
|
|
93
|
+
|
|
94
|
+
| Call | Code | Status | Payload |
|
|
95
|
+
| ----------------------------------- | -------------------- | ---------------- | --------------------------------------------------------- |
|
|
96
|
+
| `fx.fail.notFound(data?)` | `NotFound` | **404** | `id?`, `key?`, … |
|
|
97
|
+
| `fx.fail.unauthorized(data?)` | `Unauthorized` | **401** | `reason?` |
|
|
98
|
+
| `fx.fail.forbidden(data?)` | `Forbidden` | **403** | `gate?`, `reason?` |
|
|
99
|
+
| `fx.fail.conflict(data?)` | `Conflict` | **409** | `constraint?`, `table?`, `column?`, `id?` |
|
|
100
|
+
| `fx.fail.foreignKey(data?)` | `ForeignKey` | **409** | `constraint?`, `table?`, `column?` |
|
|
101
|
+
| `fx.fail.rateLimited(data?)` | `RateLimited` | **429** | `retryAfterMs?` |
|
|
102
|
+
| `fx.fail.serviceUnavailable(data?)` | `ServiceUnavailable` | **503** | `retryAfter?` |
|
|
103
|
+
| `fx.fail.database(data)` | `DatabaseError` | by `data.reason` | `reason`, `sqlstate?`, `constraint?`, `table?`, `column?` |
|
|
104
|
+
| `fx.fail.internal(data?)` | `InternalError` | **500** | `{}` — catalog message only |
|
|
105
|
+
|
|
106
|
+
`DatabaseError` status: `not_null` / `check` / `invalid` / `too_long` / `out_of_range` → **422**,
|
|
107
|
+
`retryable` → **503**, `unknown` / missing → **500**.
|
|
108
|
+
|
|
109
|
+
Declare a built-in on `errors:` **only to tighten** the payload. Shorter keeps
|
|
110
|
+
`NotFound: z.object({ code: z.string() })` because `{ code }` is not the built-in `{ id? }`.
|
|
111
|
+
Declared schema wins on the Manifest and the typed client.
|
|
112
|
+
|
|
113
|
+
The typed client always includes built-in codes. A declared key still wins.
|
|
114
|
+
|
|
115
|
+
### HTTP status
|
|
116
|
+
|
|
117
|
+
A bare `404` with body `Not Found` means **no route matched**. `fx.fail.notFound` is a JSON envelope
|
|
118
|
+
at **404**. Clients distinguish by envelope, not status. Domain codes stay **400**.
|
|
119
|
+
|
|
120
|
+
| Code | Status | Helper |
|
|
121
|
+
| ------------------------------------------------ | ----------- | ------------------------------------------------------ |
|
|
122
|
+
| `ValidationError` | `422` | none — failed `in`, `do` never runs |
|
|
123
|
+
| `Unauthorized` | `401` | `unauthorized` · also a gate denial |
|
|
124
|
+
| `Forbidden` | `403` | `forbidden` · also a gate denial |
|
|
125
|
+
| `NotFound` | `404` | `notFound` |
|
|
126
|
+
| `Conflict` / `ForeignKey` | `409` | `conflict` / `foreignKey` |
|
|
127
|
+
| `UnsupportedMediaType` | `415` | none — QUERY `Content-Type` is not `application/json` |
|
|
128
|
+
| `RateLimited` / `AuthRateLimited` | `429` | `rateLimited` · `AuthRateLimited` has none |
|
|
129
|
+
| `InvalidQuery` | `400` | none — QUERY missing `Content-Type` or body isn't JSON |
|
|
130
|
+
| `AuthFailed` | `400` | none — credentials / policy, not “no session” |
|
|
131
|
+
| `DatabaseError` | by `reason` | `database` |
|
|
132
|
+
| `ServiceUnavailable` | `503` | `serviceUnavailable` |
|
|
133
|
+
| `InternalError` | `500` | `internal` — never copies a thrown `message` |
|
|
134
|
+
| Domain (`OutOfStock`, `FlightFull`, `Duplicate`) | `400` | `fx.fail("OutOfStock", data)` |
|
|
135
|
+
|
|
136
|
+
### Client chrome
|
|
137
|
+
|
|
138
|
+
`import { match, matchError, explain } from "okengine/client/explain"`. Do not switch on every
|
|
139
|
+
code — a `create` can return store auto-map, gates, transport, and domain codes.
|
|
140
|
+
|
|
141
|
+
| `kind` | Codes |
|
|
142
|
+
| ------------- | -------------------------------------------------------------------------------------- |
|
|
143
|
+
| `auth` | `Unauthorized`, `AuthFailed` |
|
|
144
|
+
| `permission` | `Forbidden` |
|
|
145
|
+
| `missing` | `NotFound`; `TransportError` with HTTP 404 |
|
|
146
|
+
| `conflict` | `Conflict`, `ForeignKey` |
|
|
147
|
+
| `invalid` | `ValidationError`, `InvalidQuery`, `UnsupportedMediaType`; `DatabaseError` 422 reasons |
|
|
148
|
+
| `limited` | `RateLimited`, `AuthRateLimited` |
|
|
149
|
+
| `unavailable` | `ServiceUnavailable`; `DatabaseError` `retryable`; other `TransportError` |
|
|
150
|
+
| `failed` | `InternalError`, unknown `DatabaseError`, domain codes |
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
match(result, {
|
|
154
|
+
ok: (data) => show(data),
|
|
155
|
+
FlightFull: (data) => offerWaitlist(data.seatsLeft),
|
|
156
|
+
auth: () => goSignIn(),
|
|
157
|
+
invalid: (e) => setForm(e.fields),
|
|
158
|
+
_: (e) => toast(e.message),
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Dispatch: `ok` → named code → kind → required `_`. `explain(error).fields` maps
|
|
163
|
+
`ValidationError` issues to `{ email: "Required" }`. Full loop: [Calling](/docs/client/calling).
|
|
164
|
+
|
|
165
|
+
`TransportError` always sets both `error.message` and `error.data.message` to the same string.
|
|
166
|
+
Prefer `error.message` in UI code.
|
|
167
|
+
|
|
168
|
+
### Store auto-map
|
|
169
|
+
|
|
170
|
+
`fx.store` constraint and connection failures become typed `fail` values (same family as
|
|
171
|
+
`ValidationError`). Public `data` is names only — never `message` / `detail` / `hint`.
|
|
172
|
+
`get` returning `null` is **not** auto-`NotFound` — return `fx.fail.notFound` yourself.
|
|
173
|
+
|
|
174
|
+
#### SQL auto-map
|
|
175
|
+
|
|
176
|
+
| Signal | Result |
|
|
177
|
+
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
178
|
+
| Postgres `23505` / `23P01` · SQLite UNIQUE / PRIMARY KEY | `Conflict` **409** |
|
|
179
|
+
| Postgres `23503` / `23001` · SQLite FOREIGN KEY | `ForeignKey` **409** |
|
|
180
|
+
| Postgres `23502` / `23514` · SQLite NOT NULL / CHECK | `DatabaseError` `not_null` / `check` **422** |
|
|
181
|
+
| Postgres `22P02` / `22007` / `22023` / `42804` | `DatabaseError` `invalid` **422** |
|
|
182
|
+
| Postgres `22001` | `DatabaseError` `too_long` **422** |
|
|
183
|
+
| Postgres `22003` / `22008` | `DatabaseError` `out_of_range` **422** |
|
|
184
|
+
| Postgres `40001` / `40P01` / `55P03` · SQLite BUSY | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
|
|
185
|
+
| `08xxx` / `53xxx` / `57P0x` / `57014` / `25P03` / connection / paused | `ServiceUnavailable` **503** |
|
|
186
|
+
| `42P01` / `42703` with `domainDdl: "off"` | **OKE1110** (unchanged) |
|
|
187
|
+
| other SQLSTATE | `DatabaseError` `unknown` **500** |
|
|
188
|
+
|
|
189
|
+
#### KV auto-map
|
|
190
|
+
|
|
191
|
+
Redis-wire `store.kv` only (not Gate / Signal clients). Memory KV does not throw these.
|
|
192
|
+
|
|
193
|
+
| Signal | Result |
|
|
194
|
+
| ----------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
195
|
+
| Connection / `CLUSTERDOWN` / `READONLY` / `OOM` / `MISCONF` | `ServiceUnavailable` **503** |
|
|
196
|
+
| `BUSY` / `TRYAGAIN` / `LOADING` | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
|
|
197
|
+
| `WRONGTYPE` / `NOSCRIPT` | unmapped — `InternalError` **500** |
|
|
198
|
+
|
|
199
|
+
#### Files auto-map
|
|
200
|
+
|
|
201
|
+
`fs` and `s3` (`fx.store` files handle). Missing objects stay `null`.
|
|
202
|
+
|
|
203
|
+
| Signal | Result |
|
|
204
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
205
|
+
| `AccessDenied` / `EACCES` / `EPERM` / HTTP **403** | `Forbidden` **403** |
|
|
206
|
+
| `fs` key starts with `/` or contains `..` | `Forbidden` **403** (`reason: invalid_key`) |
|
|
207
|
+
| `NoSuchBucket` / `ENOSPC` / connection | `ServiceUnavailable` **503** |
|
|
208
|
+
| `SlowDown` / `EBUSY` / HTTP **429** | left thrown so `flow.retry` can run; after exhaust → `ServiceUnavailable` **503** |
|
|
209
|
+
| `NoSuchKey` (thrown) | unmapped — `InternalError` **500** |
|
|
210
|
+
|
|
65
211
|
## Localized messages
|
|
66
212
|
|
|
67
213
|
Typed failures and OKE codes ship English and Arabic ICU catalogs. Locale comes
|
|
@@ -100,6 +246,7 @@ string. Custom app codes stay message-less until registered. Full catalogs:
|
|
|
100
246
|
| `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
|
|
101
247
|
| `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
102
248
|
| `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment |
|
|
249
|
+
| `1510` | vault secret missing | Contract has no value in any resolution layer | `oke vault set <name>`, `.env.local`, or a `dev:` fallback |
|
|
103
250
|
| `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
|
|
104
251
|
| `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
|
|
105
252
|
| `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
|
|
@@ -116,7 +263,8 @@ string. Custom app codes stay message-less until registered. Full catalogs:
|
|
|
116
263
|
|
|
117
264
|
## Gate denials (typed failures)
|
|
118
265
|
|
|
119
|
-
Returned
|
|
266
|
+
Returned before `do` — not thrown. Same codes as `fx.fail.unauthorized` / `.forbidden` /
|
|
267
|
+
`.rateLimited`:
|
|
120
268
|
|
|
121
269
|
| Code | When | Payload |
|
|
122
270
|
| -------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
@@ -124,22 +272,13 @@ Returned, not thrown — the request never reached `do`:
|
|
|
124
272
|
| `Forbidden` | Policy denied, authenticated but not allowed | `gate`, `reason` (`tenant_required` · `not_member` · `unknown_scope` · `session_only` · …) |
|
|
125
273
|
| `RateLimited` | Rate gate budget exhausted | `retryAfterMs` |
|
|
126
274
|
|
|
127
|
-
## Framework validation failures
|
|
128
|
-
|
|
129
|
-
| Code | When |
|
|
130
|
-
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
-
| `ValidationError` | Input failed the `in` schema, or a list param isn't whitelisted (`unknown list param "x"`) |
|
|
132
|
-
| `NotFound` | A `store.resource` get/update/remove hit a missing row |
|
|
133
|
-
| `InvalidQuery` | QUERY missing `Content-Type` (`reason: missing_content_type`) or body isn't JSON (`inconsistent_content`) — **400** |
|
|
134
|
-
| `UnsupportedMediaType` | QUERY `Content-Type` is present but not `application/json` — **415**, `Accept-Query` lists JSON |
|
|
135
|
-
|
|
136
275
|
## Subsystem errors
|
|
137
276
|
|
|
138
277
|
Thrown by specific subsystems — each names its own cause:
|
|
139
278
|
|
|
140
279
|
| Error | Thrown when | What to do |
|
|
141
280
|
| ----------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
|
142
|
-
| `VaultBootError` | A vault contract has no value in any resolution layer |
|
|
281
|
+
| `VaultBootError` | A vault contract has no value in any resolution layer | TTY **OKE1510** — set each listed name |
|
|
143
282
|
| `VaultError` (`UNSUPPORTED`) | Managed provider unknown | Use an official id, built-in `vault`, or `env` |
|
|
144
283
|
| `VaultSealed` | Console rotate-master while this process holds no master key | Export `OKE_VAULT_MASTER_KEY` or run `oke vault unseal` |
|
|
145
284
|
| `VaultRotateBusy` | Master-rotation lease held or a batch is already in flight | Wait and continue, or resume with `oke vault rotate-master --new-key` |
|
|
@@ -196,20 +335,55 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
196
335
|
membership, or rate budget. See [Gate](/docs/elements/gate).
|
|
197
336
|
</Accordion>
|
|
198
337
|
|
|
338
|
+
<Accordion title="Do I need errors: { NotFound }?">
|
|
339
|
+
No. Built-in codes are always on — `return fx.fail.notFound({ id })`. Put `errors:` only for
|
|
340
|
+
domain codes, or to tighten a built-in payload (`{ code }` instead of `{ id? }`).
|
|
341
|
+
</Accordion>
|
|
342
|
+
|
|
343
|
+
<Accordion title="Unique insert is 500 InternalError">
|
|
344
|
+
Unique / exclusion → `Conflict` **409**; missing FK → `ForeignKey` **409**; invalid →
|
|
345
|
+
`DatabaseError` **422**. Serialization (`40001` / `40P01`) stays thrown until retries exhaust,
|
|
346
|
+
then **503**. See [SQL auto-map](#sql-auto-map).
|
|
347
|
+
</Accordion>
|
|
348
|
+
|
|
349
|
+
<Accordion title="Redis down returns InternalError">
|
|
350
|
+
Connection / `CLUSTERDOWN` / `READONLY` map to `ServiceUnavailable` **503**. `BUSY` / `TRYAGAIN`
|
|
351
|
+
stay thrown until `flow.retry` exhausts, then **503**. `WRONGTYPE` stays `InternalError` — see [KV
|
|
352
|
+
auto-map](#kv-auto-map).
|
|
353
|
+
</Accordion>
|
|
354
|
+
|
|
355
|
+
<Accordion title="S3 AccessDenied / disk full is InternalError">
|
|
356
|
+
`AccessDenied` and `fs` path-escape keys → `Forbidden` **403**. `SlowDown` / `EBUSY` stay thrown
|
|
357
|
+
until retries exhaust, then **503**. `ENOSPC` / `NoSuchBucket` → **503**. Missing `get` is still
|
|
358
|
+
`null`, not `NotFound` — see [Files auto-map](#files-auto-map).
|
|
359
|
+
</Accordion>
|
|
360
|
+
|
|
361
|
+
<Accordion title="404 Not Found — route missing or domain miss?">
|
|
362
|
+
Bare body `Not Found` means no HTTP route. JSON `{ error: { code: "NotFound" } }` is
|
|
363
|
+
`fx.fail.notFound`. Clients distinguish by envelope.
|
|
364
|
+
</Accordion>
|
|
365
|
+
|
|
366
|
+
<Accordion title="InternalError leaked a stack / thrown message">
|
|
367
|
+
Unhandled throws use the catalog string only. The thrown `message` is not copied onto the
|
|
368
|
+
envelope. Expected misses belong in `fx.fail.notFound` or a domain `fx.fail("OutOfStock", data)`.
|
|
369
|
+
</Accordion>
|
|
370
|
+
|
|
199
371
|
<Accordion title="VaultBootError at startup">
|
|
200
|
-
A vault contract has no value in any resolution layer. The
|
|
201
|
-
names ([Vault](/docs/elements/vault)).
|
|
372
|
+
TTY title **OKE1510**. A vault contract has no value in any resolution layer. The printer lists
|
|
373
|
+
every gap — set the missing names ([Vault](/docs/elements/vault)).
|
|
202
374
|
</Accordion>
|
|
203
375
|
|
|
204
376
|
</Accordions>
|
|
205
377
|
|
|
206
378
|
## Learn more
|
|
207
379
|
|
|
380
|
+
- [fx](/docs/reference/fx) — `fx.fail` helpers on the door
|
|
208
381
|
- [i18n](/docs/reference/i18n) — catalogs, `fx.t`, locale matching
|
|
209
|
-
- [Flow](/docs/elements/flow) —
|
|
382
|
+
- [Flow](/docs/elements/flow) — envelope and domain `errors:`
|
|
383
|
+
- [HTTP](/docs/elements/flow/http) — response envelopes
|
|
384
|
+
- [Calling](/docs/client/calling) — `match` / `explain` on the client
|
|
210
385
|
- [Gate](/docs/elements/gate) — where the three denials come from
|
|
211
386
|
- [CLI](/docs/reference/cli) — `oke db migrate` and friends
|
|
212
|
-
- [fx](/docs/reference/fx) — how failures and effects surface
|
|
213
387
|
|
|
214
388
|
## Next
|
|
215
389
|
|
|
@@ -220,5 +394,5 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
220
394
|
description="Where Unauthorized and Forbidden come from."
|
|
221
395
|
href="/docs/elements/gate"
|
|
222
396
|
/>
|
|
223
|
-
<Card title="Client" description="
|
|
397
|
+
<Card title="Client" description="match unknown errors in the browser." href="/docs/client" />
|
|
224
398
|
</Cards>
|
|
@@ -141,7 +141,10 @@ on(
|
|
|
141
141
|
| `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
|
|
142
142
|
| `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
|
|
143
143
|
| `fx.signal` | — | Ambient `AbortSignal` for the current branch |
|
|
144
|
-
| `fx.fail(code, data, opts?)` | — |
|
|
144
|
+
| `fx.fail(code, data, opts?)` | — | Domain failure value (`opts.message` overrides) |
|
|
145
|
+
| `fx.fail.notFound(data?)` | — | Built-in `NotFound` — HTTP 404. No `errors:` bag required |
|
|
146
|
+
| `fx.fail.forbidden(data?)` / `.unauthorized` / `.conflict` / `.foreignKey` | — | Built-in 403 / 401 / 409 |
|
|
147
|
+
| `fx.fail.rateLimited` / `.serviceUnavailable` / `.database` / `.internal` | — | Built-in 429 / 503 / DatabaseError / 500 |
|
|
145
148
|
| `fx.auth.createApiKey({ name, scopes, expiresIn?, ipAllowlist?, rateLimit? })` | `write` `auth:api-keys` | Secret once. Creator is live `userId` / `scopes`. Session only. `ipAllowlist` is IPs or hostnames. |
|
|
146
149
|
| `fx.auth.listApiKeys()` | `read` `auth:api-keys` | Keys this session minted |
|
|
147
150
|
| `fx.auth.revokeApiKey(id)` | `write` `auth:api-keys` | Owner only |
|
|
@@ -155,6 +158,8 @@ on(
|
|
|
155
158
|
`fx.call` starts the callee with an **empty** `fx.auth` (fail-closed for authorization) and
|
|
156
159
|
propagates `fx.tenant.id`. For audit/attribution only, read `fx.principal` — it propagates the
|
|
157
160
|
originating identity without copying into `fx.auth`. Gates never consult `fx.principal`.
|
|
161
|
+
Controlled `return fx.fail(...)` returns a `FlowFailure` value; an unhandled `throw` rethrows
|
|
162
|
+
(never silent `undefined`). The same rules apply to `app.call`.
|
|
158
163
|
|
|
159
164
|
## Concurrency and retry
|
|
160
165
|
|
|
@@ -248,12 +248,12 @@ The Elements section walks through each element in depth — this is just enough
|
|
|
248
248
|
|
|
249
249
|
## Where this goes next
|
|
250
250
|
|
|
251
|
-
You now hold the whole model: the drift, the rule, the eight elements, the five-piece anatomy. Don't read more — run it. From an empty folder to
|
|
251
|
+
You now hold the whole model: the drift, the rule, the eight elements, the five-piece anatomy. Don't read more — run it. From an empty folder to `GET` and `POST /users` answering a real request:
|
|
252
252
|
|
|
253
253
|
<Cards>
|
|
254
254
|
<Card
|
|
255
255
|
title="Try It"
|
|
256
|
-
description="
|
|
256
|
+
description="Scaffold, see the files, add GET and POST /users, save a row — then email if you want."
|
|
257
257
|
href="/docs/understand/try-it"
|
|
258
258
|
/>
|
|
259
259
|
<Card
|