okengine 0.19.9 → 0.21.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 +2 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +125 -37
- package/site/content/docs/client/index.mdx +7 -7
- 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 +42 -23
- package/site/content/docs/elements/flow/index.mdx +41 -30
- 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/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +13 -6
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +100 -39
- package/site/content/docs/elements/vault/index.mdx +14 -13
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- 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/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +7 -4
- package/site/content/docs/reference/configuration.mdx +8 -8
- package/site/content/docs/reference/errors.mdx +229 -55
- package/site/content/docs/reference/fx.mdx +22 -9
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +759 -24
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- 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 +2 -1
- 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 +13 -11
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +3 -3
- 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/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +6 -2
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +24 -40
- package/src/client/types.ts +17 -6
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/live-resource.ts +6 -2
- 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/compiler/response.ts +17 -27
- package/src/config/index.ts +6 -4
- 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-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.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/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/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/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/schema-decl.test.ts +178 -0
- 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 +50 -2
- package/src/elements/store/store-errors.ts +47 -0
- package/src/elements/store/table.ts +8 -6
- 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/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +40 -34
- 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/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/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +97 -24
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +24 -8
- package/src/kernel/index.ts +12 -1
- 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
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
|
@@ -24,8 +24,8 @@ writes.
|
|
|
24
24
|
### Scaffold an app
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
bunx create-oke@latest
|
|
28
|
-
cd
|
|
27
|
+
bunx create-oke@latest my-app --yes
|
|
28
|
+
cd my-app
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
</Step>
|
|
@@ -98,7 +98,7 @@ oke start # production entry (Docker CMD)
|
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
bunx create-oke@latest my-app --yes
|
|
101
|
-
bunx create-oke@latest my-app -t
|
|
101
|
+
bunx create-oke@latest my-app -t shorter --locales ar --proxy caddy
|
|
102
102
|
bunx create-oke@latest # interactive (TTY only)
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -164,7 +164,7 @@ oke db search-backfill notes --batch 500
|
|
|
164
164
|
|
|
165
165
|
| Flag | Meaning |
|
|
166
166
|
| -------------------------------- | --------------------------------------------------------- |
|
|
167
|
-
| `-t, --template <id>` | `
|
|
167
|
+
| `-t, --template <id>` | `blank` (default) · `shorter` |
|
|
168
168
|
| `--sql <id>` | Store SQL dialect — only `postgres` (test stays `pglite`) |
|
|
169
169
|
| `-y, --yes` | No prompts; defaults + bun install (no `oke dev`) |
|
|
170
170
|
| `--install` / `--no-install` | Run or skip `bun install` after scaffold |
|
|
@@ -175,6 +175,9 @@ oke db search-backfill notes --batch 500
|
|
|
175
175
|
| `--proxy <id>` / `--no-proxy` | `none` · `caddy` · `traefik` · `nginx` |
|
|
176
176
|
| `-h, --help` | Show help |
|
|
177
177
|
|
|
178
|
+
`blank` is an empty app (`main.health` + Store/Vault). `shorter` is a URL shortener
|
|
179
|
+
(`POST /links`, public `GET /:code`).
|
|
180
|
+
|
|
178
181
|
## Troubleshooting
|
|
179
182
|
|
|
180
183
|
<Accordions>
|
|
@@ -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",
|
|
@@ -212,13 +212,13 @@ Runs retention and redaction — **not** the same key as `drivers.runs`.
|
|
|
212
212
|
|
|
213
213
|
Domain schema sync for `oke db push | generate | migrate` (Drizzle). Unrelated to `oke schema generate`.
|
|
214
214
|
|
|
215
|
-
| Option | Default
|
|
216
|
-
| ----------- |
|
|
217
|
-
| `autoPush` | `true`
|
|
218
|
-
| `config` | `"drizzle.config.ts"`
|
|
219
|
-
| `declare` | `
|
|
220
|
-
| `generated` | `
|
|
221
|
-
| `entry` | `src/app.ts`
|
|
215
|
+
| Option | Default | Meaning |
|
|
216
|
+
| ----------- | --------------------- | -------------------------------------------------------------------------------- |
|
|
217
|
+
| `autoPush` | `true` | Auto-run `db push` on schema change under `oke dev`; forced off in `prod` |
|
|
218
|
+
| `config` | `"drizzle.config.ts"` | Path to the drizzle-kit config |
|
|
219
|
+
| `declare` | auto | `schema.ts` (file) if present, else `schema/index.ts` (folder). Set to override. |
|
|
220
|
+
| `generated` | auto | `src/db/drizzle/index.ts`. Set to override. |
|
|
221
|
+
| `entry` | `src/app.ts` | App entry for collecting plugin table contributions |
|
|
222
222
|
|
|
223
223
|
## topology
|
|
224
224
|
|
|
@@ -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
|
|
@@ -78,35 +224,36 @@ string. Custom app codes stay message-less until registered. Full catalogs:
|
|
|
78
224
|
|
|
79
225
|
## OKE numeric codes
|
|
80
226
|
|
|
81
|
-
| Code | Name | Cause | Fix
|
|
82
|
-
| ------ | ---------------------- | ------------------------------------------------------ |
|
|
83
|
-
| `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads`
|
|
84
|
-
| `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes`
|
|
85
|
-
| `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits`
|
|
86
|
-
| `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends`
|
|
87
|
-
| `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks`
|
|
88
|
-
| `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets`
|
|
89
|
-
| `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls`
|
|
90
|
-
| `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches`
|
|
91
|
-
| `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds`
|
|
92
|
-
| `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects
|
|
93
|
-
| `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `
|
|
94
|
-
| `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows
|
|
95
|
-
| `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path
|
|
96
|
-
| `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow`
|
|
97
|
-
| `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter
|
|
98
|
-
| `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name
|
|
99
|
-
| `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export
|
|
100
|
-
| `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow
|
|
101
|
-
| `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow`
|
|
102
|
-
| `1110` | schema missing | Domain table absent in `prod` — no auto-DDL | Run `oke db migrate` against this environment
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
227
|
+
| Code | Name | Cause | Fix |
|
|
228
|
+
| ------ | ---------------------- | ------------------------------------------------------ | --------------------------------------------------------------- |
|
|
229
|
+
| `1001` | undeclared read | Flow reads a resource not in `effects.reads` | Add it to the flow's `effects.reads` |
|
|
230
|
+
| `1002` | undeclared write | Flow writes a resource not in `effects.writes` | Add it to the flow's `effects.writes` |
|
|
231
|
+
| `1003` | undeclared emit | Flow emits a signal not in `effects.emits` | Add it to the flow's `effects.emits` |
|
|
232
|
+
| `1004` | undeclared send | Flow sends a template not in `effects.sends` | Add it to the flow's `effects.sends` |
|
|
233
|
+
| `1005` | undeclared ask | Flow asks a prompt not in `effects.asks` | Add it to the flow's `effects.asks` |
|
|
234
|
+
| `1006` | undeclared secret | Flow reads a secret not in `effects.secrets` | Add it to the flow's `effects.secrets` |
|
|
235
|
+
| `1007` | undeclared call | Flow calls a flow not in `effects.calls` | Add it to the flow's `effects.calls` |
|
|
236
|
+
| `1008` | undeclared fetch | Flow fetches a host not in `effects.fetches` | Add the hostname to the flow's `effects.fetches` |
|
|
237
|
+
| `1009` | undeclared embed | Flow embeds with a model not in `effects.embeds` | Add it to the flow's `effects.embeds` |
|
|
238
|
+
| `1020` | no effects declared | Flow has no `effects` and no Manifest to infer from | Run `oke build` / `oke dev`, or declare effects |
|
|
239
|
+
| `1030` | adopt barrel stale | A `src/flows/<unit>` folder was not adopted | Run `oke dev` or `oke build` to regenerate `src/flows/index.ts` |
|
|
240
|
+
| `1040` | HTTP path unresolved | Pathless `http.get()` never received a file-tree stamp | Import `@/flows`, or pass `http.get("/…")` |
|
|
241
|
+
| `1041` | HTTP route clash | Two HTTP flows share the same method + path | Give each flow a unique method + path |
|
|
242
|
+
| `1045` | HTTP flow unnamed | Adopted HTTP flow still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
243
|
+
| `1050` | live exposure dup | Same signal, gates, and match on two GET routes | Change the gate or path-param filter |
|
|
244
|
+
| `1060` | MCP tool duplicate | Two MCP tool bindings share the same tool name | Give each MCP tool exposure a unique name |
|
|
245
|
+
| `1070` | flow name duplicate | Two Flows share the same Manifest / `fx.call` name | Give at least one an explicit `flow("…")` or tree export |
|
|
246
|
+
| `1071` | once-signal multi-flow | Two different Flows bound to the same `signal.once` | Use `signal.broadcast`, or bind only one Flow |
|
|
247
|
+
| `1072` | flow unnamed | Signal / Clock consumer still has no `unit.export` | Export from `flows/<unit>/` or pass a named `flow` |
|
|
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 |
|
|
250
|
+
| `1210` | live resume gap | `Last-Event-ID` is not on the retained tape | Reconnect without the cursor; remaining tape replays |
|
|
251
|
+
| `1240` | orphan emit | Emit with zero subscribers and `optional` false | Add `on(signal, …)` or declare `optional: true` |
|
|
252
|
+
| `1250` | signal schema | Emit payload failed the signal's `schema` | Pass a payload that matches `schema`, or remove it |
|
|
253
|
+
| `1605` | channel schema | Send payload failed the template's `schema` | Fix template `data` payload or the template `schema` |
|
|
254
|
+
| `1810` | tenant required | Tenant-scoped op with no `fx.tenant.id` | `switchTenant`, signed `tid`, or tenant header |
|
|
255
|
+
| `1820` | tenant not member | Client-supplied tenant id is not a membership | Pick from `listTenants` or add the user as a member |
|
|
256
|
+
| `1830` | tenant unknown scope | Tenant role used an invented or `console:*` scope | Use a declared application scope |
|
|
110
257
|
|
|
111
258
|
<Callout title="Effects are usually inferred">
|
|
112
259
|
The 1001–1007 · 1008 · 1009 family exists for flows that declare effects explicitly. Most apps
|
|
@@ -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` |
|
|
@@ -167,7 +306,7 @@ Thrown by specific subsystems — each names its own cause:
|
|
|
167
306
|
</Accordion>
|
|
168
307
|
|
|
169
308
|
<Accordion title="OKE1040 pathless HTTP never stamped">
|
|
170
|
-
Import `@/flows
|
|
309
|
+
Import `@/flows`, or pass an explicit path: `http.get("/users/:id")`.
|
|
171
310
|
</Accordion>
|
|
172
311
|
|
|
173
312
|
<Accordion title="OKE1041 method + path bound twice">
|
|
@@ -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
|
|
|
@@ -211,7 +216,8 @@ const rows = await fx.using(
|
|
|
211
216
|
| `fx.send(template, { to?, data?, via?, … })` | `send` | `via` orders fallback; `locale` / `profileLocale` / `acceptLanguage` feed the locale chain |
|
|
212
217
|
|
|
213
218
|
Omit locale opts and the send uses `fx.locale`. Dry runs record _would have fired_ and never
|
|
214
|
-
contact a provider.
|
|
219
|
+
contact a provider. Bodies live on `.template({ catalog })` (`{{field}}`, not ICU — see
|
|
220
|
+
[Channel](/docs/elements/channel)).
|
|
215
221
|
|
|
216
222
|
## Outbound HTTP
|
|
217
223
|
|
|
@@ -255,13 +261,13 @@ host tooltip).
|
|
|
255
261
|
|
|
256
262
|
## Clock
|
|
257
263
|
|
|
258
|
-
| Signature | Notes
|
|
259
|
-
| --------------------------------- |
|
|
260
|
-
| `fx.clock.now()` | Epoch-ms, injectable — the only legal "now"
|
|
261
|
-
| `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days)
|
|
262
|
-
| `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days)
|
|
263
|
-
| `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`)
|
|
264
|
-
| `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise
|
|
264
|
+
| Signature | Notes |
|
|
265
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
+
| `fx.clock.now()` | Epoch-ms, injectable — the only legal "now"; pass into SQL timestamps as-is (store coerces numbers and ISO strings to `Date`) |
|
|
267
|
+
| `fx.clock.ago(duration)` | Instant before now (`"30d"` → now − 30 days) |
|
|
268
|
+
| `fx.clock.fromNow(duration)` | Instant after now (`"14d"` → now + 14 days) |
|
|
269
|
+
| `fx.clock.duration(duration)` | Span in ms — offset a stored instant (`createdAt + duration("7d")`) |
|
|
270
|
+
| `fx.clock.sleep(label, duration)` | Durable sleep in `durable` flows; immediate otherwise |
|
|
265
271
|
|
|
266
272
|
Durations: `"200ms"` · `"30s"` · `"2m"` · `"1h"` · `"7d"`. A `"d"` is 86_400_000 ms, not a calendar day. Unknown strings parse as `0`.
|
|
267
273
|
|
|
@@ -293,6 +299,8 @@ keys. Use `cache: false` to opt out, or `cache: "30s"` for a TTL.
|
|
|
293
299
|
|
|
294
300
|
Returning a plain value instead answers 200 with `{ data: value, error: null }` — the helpers exist for status and `meta` control. Pass `fx.stream(...)` into `fx.json.stream` to reach the HTTP client token-by-token.
|
|
295
301
|
|
|
302
|
+
When `out` is set, success values are projected onto it (`fx.json.create(row)`, `return row`, `fx.json.withQuery(rows, input)`). Date timestamps and temporal epoch-ms (`*At` / `*_at` / `at`) become ISO-8601 and extra keys strip. A parse miss is not a client error.
|
|
303
|
+
|
|
296
304
|
## Logging, i18n, ids
|
|
297
305
|
|
|
298
306
|
| Signature | Notes |
|
|
@@ -355,6 +363,11 @@ to the key — see [Gate](/docs/elements/gate#api-keys).
|
|
|
355
363
|
Type the parameter as `Fx` from `okengine`, not a hand-rolled structural type.
|
|
356
364
|
</Accordion>
|
|
357
365
|
|
|
366
|
+
<Accordion title="I mapped Date columns to ISO by hand">
|
|
367
|
+
Declared `out` projects success values. `fx.json.create(row)`, `return row`, and
|
|
368
|
+
`fx.json.withQuery(rows, input)` are enough — extra columns strip. A miss is not a client error.
|
|
369
|
+
</Accordion>
|
|
370
|
+
|
|
358
371
|
</Accordions>
|
|
359
372
|
|
|
360
373
|
## Learn more
|
|
@@ -257,8 +257,8 @@ until registered. Full tables: [Errors](/docs/reference/errors).
|
|
|
257
257
|
|
|
258
258
|
## Channel catalogs are separate
|
|
259
259
|
|
|
260
|
-
`fx.send`
|
|
261
|
-
|
|
260
|
+
`fx.send` interpolates `{{field}}` from `.template({ catalog })` — not ICU, not
|
|
261
|
+
`fx.t`. Omit `locale` / `profileLocale` / `acceptLanguage` on `fx.send` and the
|
|
262
262
|
send uses `fx.locale`. Details: [Channel](/docs/elements/channel).
|
|
263
263
|
|
|
264
264
|
## Troubleshooting
|
|
@@ -280,8 +280,8 @@ the app (proxies sometimes strip it).
|
|
|
280
280
|
</Accordion>
|
|
281
281
|
<Accordion title="Email body is still English while fx.t is Arabic">
|
|
282
282
|
|
|
283
|
-
Channel catalogs are separate `{{field}}` strings. Add an `ar`
|
|
284
|
-
template
|
|
283
|
+
Channel catalogs are separate `{{field}}` strings. Add an `ar` key on
|
|
284
|
+
`.template({ catalog })`; `fx.t` does not translate Channel templates.
|
|
285
285
|
|
|
286
286
|
</Accordion>
|
|
287
287
|
<Accordion title="TypeScript rejects a key that exists at runtime">
|
|
@@ -54,7 +54,7 @@ export const app = oke({ name: "shop", env: "dev" }).plug(audit);
|
|
|
54
54
|
<Step>
|
|
55
55
|
### Everything derives as usual
|
|
56
56
|
|
|
57
|
-
Plugin flows appear in the Manifest, plugin tables land in `
|
|
57
|
+
Plugin flows appear in the Manifest, plugin tables land in `src/db/drizzle/` on the next `oke db push`, plugin panels show up in the Console. No extra wiring — a contribution is ordinary OKE, just authored elsewhere.
|
|
58
58
|
|
|
59
59
|
</Step>
|
|
60
60
|
|
|
@@ -76,8 +76,8 @@ Every method below exists on both the fluent definition and the boot-time builde
|
|
|
76
76
|
| `.clock(decl)` | A named clock schedule — merged into boot clocks |
|
|
77
77
|
| `.signal(decl)` | A signal declaration — merged into boot signals |
|
|
78
78
|
| `.gate(decl)` | A gate declaration — merged into boot gates |
|
|
79
|
-
| `.channelTemplate(decl)` |
|
|
80
|
-
| `.channelCatalog(catalog)` |
|
|
79
|
+
| `.channelTemplate(decl)` | `channel.<medium>().template(…)` — `catalog` on the decl drains into boot |
|
|
80
|
+
| `.channelCatalog(catalog)` | Overlay bodies after `.template({ catalog })` (`{{field}}`); later locale wins |
|
|
81
81
|
| `.driver(id, impl)` | A protocol-named driver for an existing element |
|
|
82
82
|
| `.image(role, recipe)` | An image recipe for a docker role |
|
|
83
83
|
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
|
|
@@ -226,7 +226,7 @@ Extending an existing **app-owned** table with plugin columns is not supported i
|
|
|
226
226
|
two configurations of one plugin would silently diverge. Pass identical config, or rename one
|
|
227
227
|
instance.
|
|
228
228
|
</Accordion>
|
|
229
|
-
<Accordion title="My plugin table is missing from
|
|
229
|
+
<Accordion title="My plugin table is missing from drizzle/">
|
|
230
230
|
The CLI reads table contributions from the live app entry. Make sure the plugin is actually
|
|
231
231
|
`.plug()`ed in `src/app.ts` (or `db.entry` if overridden), then re-run `oke db push`.
|
|
232
232
|
</Accordion>
|
|
@@ -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
|