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
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,7 @@ After changing `src/kernel/`, `src/client/`, `src/compiler/`, `src/validation/`,
|
|
|
84
84
|
Published packages:
|
|
85
85
|
|
|
86
86
|
- `okengine` — framework. Subpath exports: `.`, `./client`, `./test`, `./config`, `./auth`, `./plugins`, `./drivers/*`. `"sideEffects": false`. CLI binary: `oke`.
|
|
87
|
-
- `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships
|
|
87
|
+
- `create-oke` — scaffold CLI (`bunx create-oke@latest <name>`). Lives in `packages/create-oke` and ships starters from `packages/create-oke/templates/{blank,shorter}`.
|
|
88
88
|
|
|
89
89
|
Engine: Bun `>=1.4.2`.
|
|
90
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "One law. Eight elements. One contract. The backend model stays small; operational surfaces are derived from it instead of maintained separately.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"./client": "./src/client/index.ts",
|
|
72
72
|
"./client/auth": "./src/client/auth.ts",
|
|
73
|
+
"./client/explain": "./src/client/explain.ts",
|
|
73
74
|
"./client-react": "./src/client-react/index.ts",
|
|
74
75
|
"./test": "./src/test/index.ts",
|
|
75
76
|
"./testing": {
|
|
@@ -171,8 +171,7 @@ auth.bind(api);
|
|
|
171
171
|
| `Forbidden` | 403 | Wrong scopes / CSRF — read `error.data.reason` |
|
|
172
172
|
| `RateLimited` | 429 | Wait `retryAfterMs` |
|
|
173
173
|
|
|
174
|
-
Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
|
|
175
|
-
demos cookie + passkey + `<Can>`.
|
|
174
|
+
Also see [CORS](/docs/plugins/cors) and [CSRF](/docs/plugins/csrf).
|
|
176
175
|
|
|
177
176
|
## Troubleshooting
|
|
178
177
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Calling"
|
|
3
|
-
description: "createClient forms, REST vs RPC,
|
|
3
|
+
description: "createClient forms, REST vs RPC, match/explain for unknown errors, options, envelopes, and remote types."
|
|
4
4
|
icon: "Braces"
|
|
5
5
|
source: "docs/spec/unified-theory.md"
|
|
6
6
|
---
|
|
@@ -13,8 +13,8 @@ For developers wiring a storefront, mobile app, or partner service to an okengin
|
|
|
13
13
|
**6530**).
|
|
14
14
|
|
|
15
15
|
<Callout title="The one rule">
|
|
16
|
-
Treat every call as
|
|
17
|
-
|
|
16
|
+
Treat every call as `{ data, error }`. Use `match` from `okengine/client/explain` for success plus
|
|
17
|
+
unknown failures. Name a code only when this screen has special recovery.
|
|
18
18
|
</Callout>
|
|
19
19
|
|
|
20
20
|
## Smallest Example
|
|
@@ -81,11 +81,11 @@ export const publicApiUrl = vault.config("PUBLIC_API_URL", {
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
| Runtime | Base URL for `createClient`
|
|
85
|
-
| ------------------------------------------ |
|
|
86
|
-
| Browser / Bun
|
|
87
|
-
| Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")`
|
|
88
|
-
| Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""`
|
|
84
|
+
| Runtime | Base URL for `createClient` |
|
|
85
|
+
| ------------------------------------------ | --------------------------------------------------------- |
|
|
86
|
+
| Browser / Bun | `vault.env("PUBLIC_API_URL") ?? ""` — empty = same origin |
|
|
87
|
+
| Node / worker / CLI | `vault.env.required("PUBLIC_API_URL")` |
|
|
88
|
+
| Separate storefront after `oke client add` | Same `vault.env("PUBLIC_API_URL") ?? ""` |
|
|
89
89
|
|
|
90
90
|
Set `PUBLIC_API_URL` via environment or `oke vault set`. Prefer `vault.env` over raw
|
|
91
91
|
`process.env` — same helpers as the rest of the measure. See
|
|
@@ -125,7 +125,7 @@ if (isOk(result)) {
|
|
|
125
125
|
<Tab value="Failures">
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
|
-
import {
|
|
128
|
+
import { match } from "okengine/client/explain";
|
|
129
129
|
|
|
130
130
|
const result = await api.bookings.create({
|
|
131
131
|
flightId: "SK481",
|
|
@@ -133,16 +133,18 @@ const result = await api.bookings.create({
|
|
|
133
133
|
cabin: "economy",
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
136
|
+
match(result, {
|
|
137
|
+
ok: (data) => show(data.confirmationCode),
|
|
138
|
+
FlightFull: (data) => offerWaitlist(data.seatsLeft),
|
|
139
|
+
auth: () => goSignIn(),
|
|
140
|
+
invalid: (e) => setForm(e.fields),
|
|
141
|
+
_: (e) => toast(e.message),
|
|
142
|
+
});
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
One import. Named codes narrow `data`. Kind arms (`auth`, `invalid`) cover chrome. `_` is required
|
|
146
|
+
— store auto-map, transport, and unknown domain codes land there. Full table: [`match` /
|
|
147
|
+
`explain`](#match--explain).
|
|
146
148
|
|
|
147
149
|
</Tab>
|
|
148
150
|
|
|
@@ -154,13 +156,13 @@ import { isTransportError } from "okengine/client";
|
|
|
154
156
|
const result = await api.orders.list({ limit: 20, status: "shipped" });
|
|
155
157
|
|
|
156
158
|
if (result.error && isTransportError(result.error)) {
|
|
157
|
-
showOfflineBanner(result.error.data.message
|
|
159
|
+
showOfflineBanner(result.error.message); // === error.data.message
|
|
158
160
|
// optional: result.error.data.status
|
|
159
161
|
}
|
|
160
162
|
```
|
|
161
163
|
|
|
162
164
|
Network failure, abort (`timeout`), non-JSON body, or HTTP status without a `{ data, error }`
|
|
163
|
-
envelope — never a declared Flow code.
|
|
165
|
+
envelope — never a declared Flow code. Prefer `error.message`; it always matches `error.data.message`.
|
|
164
166
|
|
|
165
167
|
</Tab>
|
|
166
168
|
|
|
@@ -180,6 +182,80 @@ JSON envelopes.
|
|
|
180
182
|
|
|
181
183
|
</Tabs>
|
|
182
184
|
|
|
185
|
+
## `match` / `explain`
|
|
186
|
+
|
|
187
|
+
A `create` can return `ValidationError`, store auto-map (`Conflict`, `DatabaseError`), gates,
|
|
188
|
+
`TransportError`, plus any domain code. Do not switch on every `error.code`.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { match } from "okengine/client/explain";
|
|
192
|
+
|
|
193
|
+
const result = await api.bookings.create({ flightId: "SK481", seats: 2 });
|
|
194
|
+
|
|
195
|
+
match(result, {
|
|
196
|
+
ok: (data) => show(data.confirmationCode),
|
|
197
|
+
FlightFull: (data) => offerWaitlist(data.seatsLeft),
|
|
198
|
+
auth: () => goSignIn(),
|
|
199
|
+
invalid: (e) => setForm(e.fields),
|
|
200
|
+
_: (e) => toast(e.message),
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
<Callout title="Dispatch order">
|
|
205
|
+
`ok` → named code (`FlightFull`) → UX kind (`auth`, `invalid`) → required `_`. Kind names are
|
|
206
|
+
reserved — do not use `auth` or `failed` as domain codes.
|
|
207
|
+
</Callout>
|
|
208
|
+
|
|
209
|
+
Import from `okengine/client/explain` so the 5 kB `okengine/client` graph stays small. `isOk` /
|
|
210
|
+
`isFail` stay on `okengine/client` when you only need a predicate.
|
|
211
|
+
|
|
212
|
+
| Helper | When |
|
|
213
|
+
| ----------------------------- | ------------------------- |
|
|
214
|
+
| `match(result, { ok, …, _ })` | Whole envelope — one call |
|
|
215
|
+
| `matchError(error, { …, _ })` | You already have `error` |
|
|
216
|
+
| `explain(error)` | Chrome only — no matcher |
|
|
217
|
+
|
|
218
|
+
### Kinds
|
|
219
|
+
|
|
220
|
+
| `kind` | Codes | Typical UI |
|
|
221
|
+
| ------------- | -------------------------------------------------------------------------------------- | --------------------------- |
|
|
222
|
+
| `auth` | `Unauthorized`, `AuthFailed` | Sign-in |
|
|
223
|
+
| `permission` | `Forbidden` | “You can’t do that” |
|
|
224
|
+
| `missing` | `NotFound`; `TransportError` HTTP 404 | Empty state |
|
|
225
|
+
| `conflict` | `Conflict`, `ForeignKey` | Retry or pick another value |
|
|
226
|
+
| `invalid` | `ValidationError`, `InvalidQuery`, `UnsupportedMediaType`; `DatabaseError` 422 reasons | Form fields |
|
|
227
|
+
| `limited` | `RateLimited`, `AuthRateLimited` | Wait / retry (`retryable`) |
|
|
228
|
+
| `unavailable` | `ServiceUnavailable`; `DatabaseError` `retryable`; other `TransportError` | Offline / retry |
|
|
229
|
+
| `failed` | `InternalError`, unknown `DatabaseError`, unnamed domain codes | Toast `message` |
|
|
230
|
+
|
|
231
|
+
Named code arms receive typed `data`. Kind arms and `_` receive `{ kind, code, message, retryable,
|
|
232
|
+
fields? }`. `_` is required even if you listed every kind.
|
|
233
|
+
|
|
234
|
+
`explain(error).fields` maps `ValidationError` issues to `{ email: "Required" }` (`.`-joined
|
|
235
|
+
path; empty path is `_`). `retryable` is true for `limited` and `unavailable`; `retryAfterMs` is
|
|
236
|
+
set when the payload has `retryAfterMs` or `retryAfter` (seconds).
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
import { explain, matchError } from "okengine/client/explain";
|
|
240
|
+
|
|
241
|
+
if (result.error) {
|
|
242
|
+
matchError(result.error, {
|
|
243
|
+
FlightFull: (data) => offerWaitlist(data.seatsLeft),
|
|
244
|
+
_: (e) => toast(e.message),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
explain(result.error!); // { kind, code, message, retryable, fields? }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`error.code === "FlightFull"` still narrows `error.data` if you prefer an `if`. HTTP status is on
|
|
252
|
+
the response, not on `error`.
|
|
253
|
+
|
|
254
|
+
Domain codes with no `{ message }` toast the code name (`OutOfStock`). Pass
|
|
255
|
+
`fx.fail("OutOfStock", data, { message })` on the Flow, or name that code in `match`.
|
|
256
|
+
|
|
257
|
+
See [Errors](/docs/reference/errors) for `fx.fail` helpers and store auto-map.
|
|
258
|
+
|
|
183
259
|
## `createClient` forms
|
|
184
260
|
|
|
185
261
|
| Form | Types from | Wire |
|
|
@@ -324,24 +400,19 @@ See [Store](/docs/elements/store) for the list query language. Handwritten lists
|
|
|
324
400
|
## Result envelopes
|
|
325
401
|
|
|
326
402
|
Success may include optional top-level `meta` (for example pagination). Declared flow errors and
|
|
327
|
-
transport failures share the failure arm
|
|
403
|
+
transport failures share the failure arm. Handle them with [`match`](#match--explain).
|
|
328
404
|
|
|
329
405
|
```typescript
|
|
330
|
-
import { isOk, isFail,
|
|
331
|
-
|
|
332
|
-
const result = await api.bookings.create({ flightId: "SK481", seats: 2 });
|
|
333
|
-
|
|
334
|
-
if (isOk(result)) {
|
|
335
|
-
result.data.confirmationCode;
|
|
336
|
-
} else if (result.error.code === "FlightFull") {
|
|
337
|
-
result.error.data.seatsLeft;
|
|
338
|
-
} else if (isTransportError(result.error)) {
|
|
339
|
-
result.error.data.message;
|
|
340
|
-
}
|
|
406
|
+
import { isOk, isFail, isTransportError } from "okengine/client";
|
|
341
407
|
|
|
342
408
|
isFail(result); // true when error !== null
|
|
409
|
+
isOk(result); // narrows to success `data`
|
|
410
|
+
isTransportError(result.error); // network / non-envelope HTTP
|
|
343
411
|
```
|
|
344
412
|
|
|
413
|
+
`error.code === "FlightFull"` still narrows `error.data`. HTTP status is on the response, not on
|
|
414
|
+
`error`.
|
|
415
|
+
|
|
345
416
|
## Remote types
|
|
346
417
|
|
|
347
418
|
`oke dev` regenerates `oke-client.d.ts` from `GET /_oke/client.json`. A separate storefront repo —
|
|
@@ -362,8 +433,8 @@ const api = createClient(import.meta.env.PUBLIC_API_URL ?? "", { $routes: routes
|
|
|
362
433
|
|
|
363
434
|
Ambient `.d.ts` types `in` / `out` / live / stream stamps. The routes module supplies wire REST.
|
|
364
435
|
|
|
365
|
-
|
|
366
|
-
|
|
436
|
+
Point `createClient` at your app origin (`PUBLIC_API_URL`), or leave it empty when the
|
|
437
|
+
browser page is same-origin with the API.
|
|
367
438
|
|
|
368
439
|
CLI details: [CLI Reference](/docs/reference/cli).
|
|
369
440
|
|
|
@@ -376,6 +447,7 @@ CLI details: [CLI Reference](/docs/reference/cli).
|
|
|
376
447
|
| `createTransport` | function | Low-level HTTP transport (timeout / retry / auth) |
|
|
377
448
|
| `isOk` / `isFail` | function | Envelope predicates |
|
|
378
449
|
| `isErrorCode` / `isTransportError` | function | Error narrowing |
|
|
450
|
+
| `match` / `matchError` / `explain` | function | `okengine/client/explain` — `ok`, codes, kinds, `_` |
|
|
379
451
|
| `Client`, `ClientCall`, `ClientResult`, … | types | Contracts, `page.next()` / `for await` of `list()` |
|
|
380
452
|
| `Register` | interface | Module-augmentation slot for ambient App types |
|
|
381
453
|
| `AppOf` | type | Brand a bare route map as an App |
|
|
@@ -387,7 +459,7 @@ Budget: the `./client` export stays under the measured client-runtime cap (hard
|
|
|
387
459
|
<Accordions>
|
|
388
460
|
|
|
389
461
|
<Accordion title="api.bookings.get is not a function / type error">
|
|
390
|
-
Confirm the Flow is `export`ed from a generated unit (`import "@/flows
|
|
462
|
+
Confirm the Flow is `export`ed from a generated unit (`import "@/flows"` then
|
|
391
463
|
`oke({ name })`), or from a module you still `.adopt({ bookings })`.
|
|
392
464
|
|
|
393
465
|
Type `createClient` with that `App` (or ambient `Register` after `oke-client.d.ts` regenerates).
|
|
@@ -402,8 +474,24 @@ Restart `oke dev` after renaming exports.
|
|
|
402
474
|
|
|
403
475
|
<Accordion title='error.code is "TransportError"'>
|
|
404
476
|
Network failure, abort (`timeout`), non-JSON body, empty error response, or HTTP status without a
|
|
405
|
-
`{ data, error }` envelope.
|
|
406
|
-
|
|
477
|
+
`{ data, error }` envelope. Built-in and domain flow codes never use this code.
|
|
478
|
+
Prefer `error.message` — it always equals `error.data.message`. `explain` maps HTTP 404 transport
|
|
479
|
+
to kind `missing`.
|
|
480
|
+
</Accordion>
|
|
481
|
+
|
|
482
|
+
<Accordion title="Why import from okengine/client/explain?">
|
|
483
|
+
`match` / `explain` stay off the measured `okengine/client` gzip cap. Predicates (`isOk`,
|
|
484
|
+
`isTransportError`) stay on `okengine/client`.
|
|
485
|
+
</Accordion>
|
|
486
|
+
|
|
487
|
+
<Accordion title="Toast showed OutOfStock / FlightFull">
|
|
488
|
+
Domain codes with no `{message}` use the code name as `explain.message`. Handle the named arm, or
|
|
489
|
+
pass `fx.fail("OutOfStock", data, {message})` on the Flow.
|
|
490
|
+
</Accordion>
|
|
491
|
+
|
|
492
|
+
<Accordion title="404 — route miss or NotFound?">
|
|
493
|
+
Bare body `Not Found` means no HTTP route — `TransportError` (kind `missing`). JSON
|
|
494
|
+
`{ error: { code: "NotFound" } }` is `fx.fail.notFound`. Distinguish by envelope, not status.
|
|
407
495
|
</Accordion>
|
|
408
496
|
|
|
409
497
|
<Accordion title="Failed to fetch …/_oke/client.json">
|
|
@@ -427,7 +515,7 @@ Restart `oke dev` after renaming exports.
|
|
|
427
515
|
- [Vault · Config](/docs/elements/vault/config) — `PUBLIC_API_URL`, `vault.env`
|
|
428
516
|
- [Routing](/docs/elements/flow/routing) — `$routes` stamps
|
|
429
517
|
- [HTTP](/docs/elements/flow/http) — verbs and envelopes on the server
|
|
430
|
-
- [Errors](/docs/reference/errors) —
|
|
518
|
+
- [Errors](/docs/reference/errors) — `fx.fail` helpers, store auto-map, `match` on the client
|
|
431
519
|
- [CLI](/docs/reference/cli) — `oke client add`, `oke dev`
|
|
432
520
|
|
|
433
521
|
## Next
|
|
@@ -27,7 +27,7 @@ the envelope.
|
|
|
27
27
|
|
|
28
28
|
```typescript title="src/app.ts"
|
|
29
29
|
import "@/core";
|
|
30
|
-
import "@/flows
|
|
30
|
+
import "@/flows";
|
|
31
31
|
import { oke } from "okengine/http";
|
|
32
32
|
|
|
33
33
|
export const app = oke({ name: "commerce" });
|
|
@@ -46,12 +46,12 @@ import { createClient } from "okengine/client";
|
|
|
46
46
|
import { vault } from "okengine/vault";
|
|
47
47
|
import { app } from "../../src/app";
|
|
48
48
|
|
|
49
|
-
// Base URL via vault.env (empty = same
|
|
49
|
+
// Base URL via vault.env (empty = same origin as the page)
|
|
50
50
|
const api = createClient(app, vault.env("PUBLIC_API_URL") ?? "");
|
|
51
51
|
const { data, error } = await api.bookings.get({ id: "bkg_7f3a" });
|
|
52
52
|
|
|
53
53
|
if (error) {
|
|
54
|
-
// TransportError or a
|
|
54
|
+
// TransportError or a flow code (NotFound, …)
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -128,8 +128,8 @@ const api = createClient(vault.env("PUBLIC_API_URL") ?? "");
|
|
|
128
128
|
```
|
|
129
129
|
|
|
130
130
|
Declare the origin on the server with [Vault · Config](/docs/elements/vault/config)
|
|
131
|
-
(`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local
|
|
132
|
-
dev so `createClient("")` stays same-origin behind
|
|
131
|
+
(`PUBLIC_API_URL`), then set it via env or `oke vault set`. Leave it unset in local frontend
|
|
132
|
+
dev so `createClient("")` stays same-origin behind your proxy.
|
|
133
133
|
|
|
134
134
|
</Tab>
|
|
135
135
|
|
|
@@ -160,7 +160,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
|
|
|
160
160
|
<Cards>
|
|
161
161
|
<Card
|
|
162
162
|
title="Calling"
|
|
163
|
-
description="createClient forms, REST vs RPC,
|
|
163
|
+
description="createClient forms, REST vs RPC, match/explain, envelopes, remote types."
|
|
164
164
|
href="/docs/client/calling"
|
|
165
165
|
/>
|
|
166
166
|
<Card
|
|
@@ -192,7 +192,7 @@ dev so `createClient("")` stays same-origin behind the Vite proxy.
|
|
|
192
192
|
<Cards>
|
|
193
193
|
<Card
|
|
194
194
|
title="Calling"
|
|
195
|
-
description="REST vs RPC,
|
|
195
|
+
description="REST vs RPC, match/explain, options, and remote types."
|
|
196
196
|
href="/docs/client/calling"
|
|
197
197
|
/>
|
|
198
198
|
<Card
|
|
@@ -221,6 +221,11 @@ Prefer [`createAuthClient`](/docs/client/auth) for cookie/Bearer sessions and me
|
|
|
221
221
|
Without a token, status becomes `"unauthenticated"`.
|
|
222
222
|
</Accordion>
|
|
223
223
|
|
|
224
|
+
<Accordion title="Vite SPA is a blank page">
|
|
225
|
+
The console shows `node:async_hooks` / `AsyncLocalStorage` from `okengine/client-react`. Upgrade
|
|
226
|
+
okengine — `Can` / `useLiveQuery` must not import the server realtime binder.
|
|
227
|
+
</Accordion>
|
|
228
|
+
|
|
224
229
|
<Accordion title="useLiveQuery never connects">
|
|
225
230
|
Pass `live: { method, path }` from `app.$routes` for the resource’s `/live` route. Set
|
|
226
231
|
`enabled: true` (or omit). See [Live](/docs/client/live) for exposure and resume errors.
|
|
@@ -6,14 +6,14 @@ source: "docs/spec/unified-theory.md"
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Email (`channel.email`) is the default Channel medium. Declare a binder with a From address,
|
|
9
|
-
|
|
9
|
+
put `{{field}}` bodies on `.template({ catalog })`, and send with `fx.send`.
|
|
10
10
|
|
|
11
11
|
For developers who need local catchers and production HTTP MTAs — same `fx.send` path, swap the
|
|
12
12
|
driver.
|
|
13
13
|
|
|
14
14
|
<Callout title="The one rule">
|
|
15
|
-
Use `channel.email({ from }).template(name, { schema, locales })
|
|
16
|
-
|
|
15
|
+
Use `channel.email({ from }).template(name, { schema, locales, catalog })`. Subject and body
|
|
16
|
+
live on the template — not on `oke()`.
|
|
17
17
|
</Callout>
|
|
18
18
|
|
|
19
19
|
## Smallest Example
|
|
@@ -33,32 +33,20 @@ export const passwordReset = mail.template("auth.resetPassword", {
|
|
|
33
33
|
description: "Password reset link",
|
|
34
34
|
locales: ["en"],
|
|
35
35
|
schema: z.object({ resetLink: z.string().url() }),
|
|
36
|
+
catalog: {
|
|
37
|
+
en: {
|
|
38
|
+
subject: "Reset your password",
|
|
39
|
+
text: "Open {{resetLink}} to choose a new password.",
|
|
40
|
+
html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
36
43
|
});
|
|
37
44
|
```
|
|
38
45
|
|
|
39
46
|
</Step>
|
|
40
47
|
|
|
41
48
|
<Step>
|
|
42
|
-
###
|
|
43
|
-
|
|
44
|
-
```typescript title="src/app.ts"
|
|
45
|
-
import { oke } from "okengine";
|
|
46
|
-
|
|
47
|
-
oke({
|
|
48
|
-
name: "app",
|
|
49
|
-
channel: {
|
|
50
|
-
catalog: {
|
|
51
|
-
"auth.resetPassword": {
|
|
52
|
-
en: {
|
|
53
|
-
subject: "Reset your password",
|
|
54
|
-
text: "Open {{resetLink}} to choose a new password.",
|
|
55
|
-
html: '<p><a href="{{resetLink}}">Reset your password</a></p>',
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
```
|
|
49
|
+
### Send from a Flow
|
|
62
50
|
|
|
63
51
|
```typescript title="src/flows/auth/reset.ts"
|
|
64
52
|
import { on, flow, http } from "okengine";
|
|
@@ -118,13 +106,13 @@ Per-locale `subject` / `text` / `html` with `{{field}}` interpolation from `data
|
|
|
118
106
|
|
|
119
107
|
```typescript
|
|
120
108
|
catalog: {
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
|
|
124
|
-
},
|
|
109
|
+
en: { subject: "Reset", text: "{{resetLink}}", html: "<a href=\"{{resetLink}}\">Reset</a>" },
|
|
110
|
+
ar: { subject: "إعادة التعيين", text: "{{resetLink}}" },
|
|
125
111
|
}
|
|
126
112
|
```
|
|
127
113
|
|
|
114
|
+
Put that object on `.template({ catalog })`. Do not nest the template name again.
|
|
115
|
+
|
|
128
116
|
</Tab>
|
|
129
117
|
|
|
130
118
|
<Tab value="Failover">
|
|
@@ -146,8 +134,8 @@ Receipt status `fallback` means an earlier attempt failed and a later one succee
|
|
|
146
134
|
|
|
147
135
|
<Tab value="Plugin catalog">
|
|
148
136
|
|
|
149
|
-
Official plugins
|
|
150
|
-
`channel.catalog`
|
|
137
|
+
Official plugins put copy on `.template({ catalog })` (OTP, magic link, two-factor). App
|
|
138
|
+
`oke({ channel.catalog })` and plugin `.channelCatalog(…)` overlay those bodies at boot.
|
|
151
139
|
|
|
152
140
|
</Tab>
|
|
153
141
|
|
|
@@ -164,11 +152,12 @@ Official plugins contribute catalogs with `.channelCatalog(…)` (e.g. OTP, magi
|
|
|
164
152
|
|
|
165
153
|
### `.template(name, options?)`
|
|
166
154
|
|
|
167
|
-
| Option | Type | Meaning
|
|
168
|
-
| ------------- | ---------- |
|
|
169
|
-
| `description` | `string` | Console label
|
|
170
|
-
| `locales` | `string[]` | Declared locales
|
|
171
|
-
| `schema` | Schema | Payload contract for `data`
|
|
155
|
+
| Option | Type | Meaning |
|
|
156
|
+
| ------------- | ---------- | -------------------------------------- |
|
|
157
|
+
| `description` | `string` | Console label |
|
|
158
|
+
| `locales` | `string[]` | Declared locales |
|
|
159
|
+
| `schema` | Schema | Payload contract for `data` |
|
|
160
|
+
| `catalog` | object | Per-locale `subject` / `text` / `html` |
|
|
172
161
|
|
|
173
162
|
Default From when neither binder nor template sets one: `"oke@localhost.test"`.
|
|
174
163
|
|
|
@@ -229,8 +218,8 @@ Channel catalogs are **not** ICU — do not use `fx.t` for email bodies ([i18n](
|
|
|
229
218
|
</Accordion>
|
|
230
219
|
|
|
231
220
|
<Accordion title="Message in Mailpit has JSON body / template name as subject">
|
|
232
|
-
No catalog entry for that template + locale. Add `
|
|
233
|
-
`subject` / `text` / `html`.
|
|
221
|
+
No catalog entry for that template + locale. Add `catalog` on `.template(…)` (or overlay with
|
|
222
|
+
`oke({channel.catalog})` / plugin `.channelCatalog`) with `subject` / `text` / `html`.
|
|
234
223
|
</Accordion>
|
|
235
224
|
|
|
236
225
|
<Accordion title="suppressed/opted-out or prior-bounce — no provider call">
|