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
|
@@ -6,16 +6,16 @@ source: "docs/spec/unified-theory.md"
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Channel is how your backend **reaches a person** — the order-confirmation email, the SMS
|
|
9
|
-
sign-in code, a WhatsApp notice, or a device push. You declare a template on a medium
|
|
10
|
-
`{{field}}`
|
|
9
|
+
sign-in code, a WhatsApp notice, or a device push. You declare a template on a medium with
|
|
10
|
+
per-locale `{{field}}` bodies, then send from a Flow with `fx.send`.
|
|
11
11
|
|
|
12
12
|
For developers wiring Mailpit locally and Resend / Taqnyat / FCM in production — templates and
|
|
13
13
|
drivers first, never vendor SDKs inside `do`.
|
|
14
14
|
|
|
15
15
|
<Callout title="The one rule">
|
|
16
|
-
Declare a template (`channel.email(…).template(
|
|
17
|
-
Bodies live
|
|
18
|
-
|
|
16
|
+
Declare a template (`channel.email(…).template({ catalog })`), then `fx.send(template, { to, data })`.
|
|
17
|
+
Bodies live on the declare (`subject` / `text` / `html` per locale). Consent and prior bounces
|
|
18
|
+
suppress before any driver runs.
|
|
19
19
|
</Callout>
|
|
20
20
|
|
|
21
21
|
<ChannelPhysics />
|
|
@@ -27,18 +27,28 @@ drivers first, never vendor SDKs inside `do`.
|
|
|
27
27
|
<Step>
|
|
28
28
|
### Declare an email template
|
|
29
29
|
|
|
30
|
-
```typescript title="src/core/
|
|
30
|
+
```typescript title="src/core/email.ts"
|
|
31
31
|
import { channel } from "okengine";
|
|
32
32
|
import { z } from "zod";
|
|
33
33
|
|
|
34
|
-
const mail = channel.email({ from: "
|
|
34
|
+
const mail = channel.email({ from: "Shorter <shorter@localhost>" });
|
|
35
35
|
|
|
36
|
-
export const
|
|
36
|
+
export const linkCreatedMail = mail.template("link-created", {
|
|
37
|
+
description: "Short URL delivered to the demo inbox",
|
|
37
38
|
locales: ["en"],
|
|
38
39
|
schema: z.object({
|
|
39
40
|
id: z.string(),
|
|
40
|
-
|
|
41
|
+
code: z.string(),
|
|
42
|
+
url: z.string(),
|
|
43
|
+
shortUrl: z.string(),
|
|
41
44
|
}),
|
|
45
|
+
catalog: {
|
|
46
|
+
en: {
|
|
47
|
+
subject: "Short link {{code}}",
|
|
48
|
+
text: "{{shortUrl}} → {{url}}",
|
|
49
|
+
html: '<p><a href="{{shortUrl}}">{{code}}</a> → {{url}}</p>',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
42
52
|
});
|
|
43
53
|
```
|
|
44
54
|
|
|
@@ -50,26 +60,42 @@ Import this module before `oke()` so auto-registry adopts the template (or pass
|
|
|
50
60
|
<Step>
|
|
51
61
|
### Send from a Flow
|
|
52
62
|
|
|
53
|
-
```typescript title="src/flows/
|
|
54
|
-
import { on, flow } from "okengine";
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
63
|
+
```typescript title="src/flows/links/signals.ts"
|
|
64
|
+
import { on, flow, signal } from "okengine";
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
import { linkCreatedMail } from "@/core";
|
|
67
|
+
|
|
68
|
+
export const linkCreated = signal.once("link-created", {
|
|
69
|
+
retries: 3,
|
|
70
|
+
deadLetter: true,
|
|
71
|
+
schema: z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
code: z.string(),
|
|
74
|
+
url: z.string(),
|
|
75
|
+
shortUrl: z.string(),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
57
78
|
|
|
58
79
|
export const onCreated = on(
|
|
59
|
-
|
|
60
|
-
flow("
|
|
80
|
+
linkCreated,
|
|
81
|
+
flow("links.onCreated", {
|
|
61
82
|
do: async (payload, fx) => {
|
|
62
|
-
await fx.send(
|
|
83
|
+
await fx.send(linkCreatedMail, {
|
|
63
84
|
to: "you@localhost",
|
|
64
|
-
data: {
|
|
85
|
+
data: {
|
|
86
|
+
id: payload.id,
|
|
87
|
+
code: payload.code,
|
|
88
|
+
url: payload.url,
|
|
89
|
+
shortUrl: payload.shortUrl,
|
|
90
|
+
},
|
|
65
91
|
});
|
|
66
92
|
},
|
|
67
93
|
}),
|
|
68
94
|
);
|
|
69
95
|
```
|
|
70
96
|
|
|
71
|
-
The compiler stamps `sends: ["
|
|
72
|
-
`email:
|
|
97
|
+
The compiler stamps `sends: ["link-created"]` on the Flow’s effects (template name — not
|
|
98
|
+
`email:link-created`). Declaration and subscriber share `signals.ts`.
|
|
73
99
|
|
|
74
100
|
</Step>
|
|
75
101
|
|
|
@@ -77,8 +103,8 @@ The compiler stamps `sends: ["note-created"]` on the Flow’s effects (template
|
|
|
77
103
|
### See it locally
|
|
78
104
|
|
|
79
105
|
With `drivers.channel.email.dev: "smtp"` and Mailpit pinned, open the Mailpit UI
|
|
80
|
-
(`MAILPIT_UI_URL`).
|
|
81
|
-
`text: JSON.stringify(data)`.
|
|
106
|
+
(`MAILPIT_UI_URL`). Subject and HTML come from the template `catalog`. Omit `catalog` and the
|
|
107
|
+
runtime falls back to `subject: link-created` and `text: JSON.stringify(data)`.
|
|
82
108
|
|
|
83
109
|
</Step>
|
|
84
110
|
|
|
@@ -95,9 +121,14 @@ From a bare send to catalog bodies, locale, and same-medium failover:
|
|
|
95
121
|
Template handle + recipient — catalog optional for local smoke tests:
|
|
96
122
|
|
|
97
123
|
```typescript
|
|
98
|
-
await fx.send(
|
|
124
|
+
await fx.send(linkCreatedMail, {
|
|
99
125
|
to: "alice@example.com",
|
|
100
|
-
data: {
|
|
126
|
+
data: {
|
|
127
|
+
id: "lnk_1",
|
|
128
|
+
code: "ok",
|
|
129
|
+
url: "https://example.com",
|
|
130
|
+
shortUrl: "http://localhost:6530/ok",
|
|
131
|
+
},
|
|
101
132
|
});
|
|
102
133
|
```
|
|
103
134
|
|
|
@@ -105,23 +136,22 @@ await fx.send(noteCreatedMail, {
|
|
|
105
136
|
|
|
106
137
|
<Tab value="Catalog">
|
|
107
138
|
|
|
108
|
-
Bodies are `{{field}}` strings per locale — not ICU, not React.
|
|
109
|
-
plugin `.channelCatalog(…)
|
|
110
|
-
|
|
111
|
-
```typescript title="src/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
},
|
|
139
|
+
Bodies are `{{field}}` strings per locale — not ICU, not React. Put them on the template.
|
|
140
|
+
`oke({ channel.catalog })` and plugin `.channelCatalog(…)` are overlays (later locale wins):
|
|
141
|
+
|
|
142
|
+
```typescript title="src/core/email.ts"
|
|
143
|
+
export const linkCreatedMail = mail.template("link-created", {
|
|
144
|
+
locales: ["en"],
|
|
145
|
+
schema: z.object({
|
|
146
|
+
code: z.string(),
|
|
147
|
+
url: z.string(),
|
|
148
|
+
shortUrl: z.string(),
|
|
149
|
+
}),
|
|
150
|
+
catalog: {
|
|
151
|
+
en: {
|
|
152
|
+
subject: "Short link {{code}}",
|
|
153
|
+
text: "{{shortUrl}} → {{url}}",
|
|
154
|
+
html: '<p><a href="{{shortUrl}}">{{code}}</a> → {{url}}</p>',
|
|
125
155
|
},
|
|
126
156
|
},
|
|
127
157
|
});
|
|
@@ -136,9 +166,14 @@ Precedence: explicit `locale` → `profileLocale` → `Accept-Language` →
|
|
|
136
166
|
`fx.locale`.
|
|
137
167
|
|
|
138
168
|
```typescript
|
|
139
|
-
await fx.send(
|
|
169
|
+
await fx.send(linkCreatedMail, {
|
|
140
170
|
to: "alice@example.com",
|
|
141
|
-
data: {
|
|
171
|
+
data: {
|
|
172
|
+
id: "lnk_1",
|
|
173
|
+
code: "ok",
|
|
174
|
+
url: "https://example.com",
|
|
175
|
+
shortUrl: "http://localhost:6530/ok",
|
|
176
|
+
},
|
|
142
177
|
locale: "ar",
|
|
143
178
|
});
|
|
144
179
|
```
|
|
@@ -154,9 +189,14 @@ Order same-medium drivers for failover. Provider / 5xx errors advance; permanent
|
|
|
154
189
|
(400, invalid address) do **not**:
|
|
155
190
|
|
|
156
191
|
```typescript
|
|
157
|
-
await fx.send(
|
|
192
|
+
await fx.send(linkCreatedMail, {
|
|
158
193
|
to: "alice@example.com",
|
|
159
|
-
data: {
|
|
194
|
+
data: {
|
|
195
|
+
id: "lnk_1",
|
|
196
|
+
code: "ok",
|
|
197
|
+
url: "https://example.com",
|
|
198
|
+
shortUrl: "http://localhost:6530/ok",
|
|
199
|
+
},
|
|
160
200
|
via: ["smtp", "resend"],
|
|
161
201
|
});
|
|
162
202
|
```
|
|
@@ -193,12 +233,14 @@ succeeded — every attempt is kept on the receipt.
|
|
|
193
233
|
| `description` | `string` | name | Console / docs label |
|
|
194
234
|
| `locales` | `string[]` | — | Declared locale tags for the template |
|
|
195
235
|
| `schema` | Schema | — | Payload shape (Zod / Standard Schema) |
|
|
236
|
+
| `catalog` | object | — | Per-locale `subject` / `text` / `html` |
|
|
196
237
|
| `from` | `string` | binder’s | Override sender on agnostic `channel.template` only |
|
|
197
238
|
| `medium` | medium | `"email"` | Only on `channel.template()` |
|
|
198
239
|
|
|
199
240
|
Empty name throws `TypeError: channel.template: name is required`.
|
|
200
241
|
|
|
201
|
-
|
|
242
|
+
`oke({ channel.catalog })` overlays bodies after template catalogs (tests / plugin copy
|
|
243
|
+
overrides). Manifest stays free of copy.
|
|
202
244
|
|
|
203
245
|
## `fx` surface
|
|
204
246
|
|
|
@@ -295,7 +337,7 @@ Env knobs: [Environment variables](/docs/reference/environment-variables). Local
|
|
|
295
337
|
|
|
296
338
|
<Accordion title="OKE1004 — UNDECLARED_SEND">
|
|
297
339
|
Cause: `Flow "{flow}" sends "{resource}" without declaring it.` Touch the template handle
|
|
298
|
-
inside `do` (inference) or list `effects: { sends: ["
|
|
340
|
+
inside `do` (inference) or list `effects: { sends: ["link-created"] }`.
|
|
299
341
|
</Accordion>
|
|
300
342
|
|
|
301
343
|
<Accordion title="channel: no email transport in driver chain">
|
|
@@ -37,6 +37,9 @@ export const orderPush = push.template("order.status", {
|
|
|
37
37
|
title: z.string(),
|
|
38
38
|
body: z.string(),
|
|
39
39
|
}),
|
|
40
|
+
catalog: {
|
|
41
|
+
en: { subject: "{{title}}", text: "{{body}}" },
|
|
42
|
+
},
|
|
40
43
|
});
|
|
41
44
|
```
|
|
42
45
|
|
|
@@ -59,11 +62,6 @@ export const app = oke({
|
|
|
59
62
|
privateKey: process.env.FCM_PRIVATE_KEY,
|
|
60
63
|
}),
|
|
61
64
|
],
|
|
62
|
-
catalog: {
|
|
63
|
-
"order.status": {
|
|
64
|
-
en: { subject: "{{title}}", text: "{{body}}" },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
65
|
},
|
|
68
66
|
});
|
|
69
67
|
```
|
|
@@ -128,13 +126,13 @@ Web Push where the subscription is supplied on the runtime path.
|
|
|
128
126
|
<Tab value="Catalog">
|
|
129
127
|
|
|
130
128
|
```typescript
|
|
131
|
-
|
|
129
|
+
catalog: {
|
|
132
130
|
en: { subject: "{{title}}", text: "{{body}}" },
|
|
133
131
|
}
|
|
134
132
|
```
|
|
135
133
|
|
|
136
|
-
`subject` becomes the notification title on FCM; `text`
|
|
137
|
-
through as FCM data payload when present.
|
|
134
|
+
Put that on `push.template({ catalog })`. `subject` becomes the notification title on FCM; `text`
|
|
135
|
+
is the body. Extra `data` fields pass through as FCM data payload when present.
|
|
138
136
|
|
|
139
137
|
</Tab>
|
|
140
138
|
|
|
@@ -163,7 +161,7 @@ Config key `drivers.channel.push` exists for Manifest / tooling ids (`console`
|
|
|
163
161
|
|
|
164
162
|
### `.template(name, options?)`
|
|
165
163
|
|
|
166
|
-
`description` · `locales` · `schema` — same as other mediums.
|
|
164
|
+
`description` · `locales` · `schema` · `catalog` — same as other mediums.
|
|
167
165
|
|
|
168
166
|
## Troubleshooting
|
|
169
167
|
|
|
@@ -52,6 +52,7 @@ const sms = channel.sms({ sender: "ACME" });
|
|
|
52
52
|
export const orderShippedSms = sms.template("order.shipped", {
|
|
53
53
|
locales: ["en"],
|
|
54
54
|
schema: z.object({ tracking: z.string() }),
|
|
55
|
+
catalog: { en: { text: "Shipped — track {{tracking}}" } },
|
|
55
56
|
});
|
|
56
57
|
```
|
|
57
58
|
|
|
@@ -62,7 +63,7 @@ await fx.send(orderShippedSms, {
|
|
|
62
63
|
});
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Missing `catalog` falls back to `subject: templateName` and `text: JSON.stringify(data)`.
|
|
66
67
|
|
|
67
68
|
</Step>
|
|
68
69
|
|
|
@@ -102,6 +103,7 @@ Transactional SMS through the same `fx.send` path as email:
|
|
|
102
103
|
const sms = channel.sms({ from: "ACME" });
|
|
103
104
|
export const alertSms = sms.template("ops.alert", {
|
|
104
105
|
schema: z.object({ message: z.string() }),
|
|
106
|
+
catalog: { en: { text: "{{message}}" } },
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
await fx.send(alertSms, { to: phone, data: { message: "Disk 90%" } });
|
|
@@ -202,7 +204,7 @@ Same medium options as email: `from` / `sender`.
|
|
|
202
204
|
|
|
203
205
|
### `.template(name, options?)`
|
|
204
206
|
|
|
205
|
-
`description` · `locales` · `schema`
|
|
207
|
+
`description` · `locales` · `schema` · `catalog` — `text` is typical for SMS.
|
|
206
208
|
|
|
207
209
|
## Troubleshooting
|
|
208
210
|
|
|
@@ -229,8 +231,8 @@ Same medium options as email: `from` / `sender`.
|
|
|
229
231
|
</Accordion>
|
|
230
232
|
|
|
231
233
|
<Accordion title="channel: otp delivery failed on all channels">
|
|
232
|
-
Every medium in the failover list failed. Check driver credentials, suppression, and
|
|
233
|
-
|
|
234
|
+
Every medium in the failover list failed. Check driver credentials, suppression, and `catalog` on
|
|
235
|
+
each `.template()`.
|
|
234
236
|
</Accordion>
|
|
235
237
|
|
|
236
238
|
</Accordions>
|
|
@@ -53,6 +53,10 @@ const wa = channel.whatsapp();
|
|
|
53
53
|
export const bookingReminder = wa.template("booking.reminder", {
|
|
54
54
|
locales: ["en", "ar"],
|
|
55
55
|
schema: z.object({ when: z.string(), place: z.string() }),
|
|
56
|
+
catalog: {
|
|
57
|
+
en: { text: "Reminder: {{when}} at {{place}}" },
|
|
58
|
+
ar: { text: "تذكير: {{when}} — {{place}}" },
|
|
59
|
+
},
|
|
56
60
|
});
|
|
57
61
|
```
|
|
58
62
|
|
|
@@ -85,14 +89,12 @@ and SMS.
|
|
|
85
89
|
Catalog typically supplies `text` (and optional `subject` where the transport uses it):
|
|
86
90
|
|
|
87
91
|
```typescript
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
},
|
|
95
|
-
},
|
|
92
|
+
export const bookingReminder = wa.template("booking.reminder", {
|
|
93
|
+
locales: ["en", "ar"],
|
|
94
|
+
schema: z.object({ when: z.string(), place: z.string() }),
|
|
95
|
+
catalog: {
|
|
96
|
+
en: { text: "Reminder: {{when}} at {{place}}" },
|
|
97
|
+
ar: { text: "تذكير: {{when}} — {{place}}" },
|
|
96
98
|
},
|
|
97
99
|
});
|
|
98
100
|
```
|
|
@@ -165,7 +167,7 @@ oke boot: unknown whatsapp channel driver "…"
|
|
|
165
167
|
|
|
166
168
|
### `.template(name, options?)`
|
|
167
169
|
|
|
168
|
-
`description` · `locales` · `schema` — same as other mediums.
|
|
170
|
+
`description` · `locales` · `schema` · `catalog` — same as other mediums.
|
|
169
171
|
|
|
170
172
|
## Troubleshooting
|
|
171
173
|
|
|
@@ -21,32 +21,19 @@ For developers scheduling work on okengine — one handle shape; drivers swap by
|
|
|
21
21
|
<Steps>
|
|
22
22
|
|
|
23
23
|
<Step>
|
|
24
|
-
### Declare
|
|
24
|
+
### Declare and bind in the unit
|
|
25
25
|
|
|
26
|
-
```typescript title="src/
|
|
27
|
-
import { clock } from "okengine";
|
|
28
|
-
|
|
29
|
-
export const digestClock = clock.every("notes.digest", "1d");
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
</Step>
|
|
33
|
-
|
|
34
|
-
<Step>
|
|
35
|
-
### Bind a Flow
|
|
26
|
+
```typescript title="src/flows/links/expire.ts"
|
|
27
|
+
import { on, flow, clock } from "okengine/http";
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
import { on, flow } from "okengine";
|
|
39
|
-
import { digestClock } from "@/clocks/digest";
|
|
40
|
-
import { db, notes } from "@/schema";
|
|
41
|
-
import { isNull } from "drizzle-orm";
|
|
29
|
+
const expireClock = clock.every("links.expire", "1h");
|
|
42
30
|
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
flow(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return { active: rows.length, at: new Date(fx.clock.now()).toISOString() };
|
|
31
|
+
export const expire = on(
|
|
32
|
+
expireClock,
|
|
33
|
+
flow({
|
|
34
|
+
do: async (_input, fx) => {
|
|
35
|
+
const now = fx.clock.now();
|
|
36
|
+
return { archived: 0, at: new Date(now).toISOString() };
|
|
50
37
|
},
|
|
51
38
|
}),
|
|
52
39
|
);
|
|
@@ -57,9 +44,11 @@ export const digest = on(
|
|
|
57
44
|
<Step>
|
|
58
45
|
### See it tick
|
|
59
46
|
|
|
60
|
-
With `oke dev`, the scheduler reconciles `
|
|
47
|
+
With `oke dev`, the scheduler reconciles `links.expire` into the Store and leader-elects
|
|
61
48
|
before each fire. `do` receives no payload — read time with `fx.clock.now()` (epoch-ms).
|
|
62
|
-
|
|
49
|
+
|
|
50
|
+
Pass that number into SQL `timestamp` columns as-is — insert, update, and WHERE binds
|
|
51
|
+
coerce to `Date`. For a hand-built ISO reply, use `new Date(fx.clock.now()).toISOString()`.
|
|
63
52
|
|
|
64
53
|
Under `drivers.clock.test = "frozen"`, advance time in tests instead of waiting a day.
|
|
65
54
|
|
|
@@ -110,7 +110,7 @@ export const create = on(
|
|
|
110
110
|
|
|
111
111
|
<Tab value="Failures">
|
|
112
112
|
|
|
113
|
-
Declare typed domain errors
|
|
113
|
+
Declare typed domain errors on `errors:`. Built-in codes use helpers (`fx.fail.notFound`) with no bag:
|
|
114
114
|
|
|
115
115
|
```typescript title="src/flows/orders/[id]/get.ts"
|
|
116
116
|
import { on, flow, http } from "okengine";
|
|
@@ -120,12 +120,11 @@ export const get = on(
|
|
|
120
120
|
http.get({
|
|
121
121
|
in: z.object({ id: z.string() }),
|
|
122
122
|
out: z.object({ id: z.string(), sku: z.string(), qty: z.number() }),
|
|
123
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
124
123
|
}),
|
|
125
124
|
flow({
|
|
126
125
|
do: async ({ id }, fx) => {
|
|
127
126
|
const [order] = await fx.store(db).select().from(orders).where(eq(orders.id, id));
|
|
128
|
-
if (!order) return fx.fail(
|
|
127
|
+
if (!order) return fx.fail.notFound({ id });
|
|
129
128
|
return order;
|
|
130
129
|
},
|
|
131
130
|
}),
|
|
@@ -287,24 +286,26 @@ export const get = on(
|
|
|
287
286
|
http.get({
|
|
288
287
|
in: z.object({ id: z.string() }),
|
|
289
288
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
290
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
291
289
|
}),
|
|
292
290
|
flow({
|
|
293
291
|
do: async ({ id }, fx) => {
|
|
294
292
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
295
|
-
if (!note) return fx.fail(
|
|
293
|
+
if (!note) return fx.fail.notFound({ id });
|
|
296
294
|
return note;
|
|
297
295
|
},
|
|
298
296
|
}),
|
|
299
297
|
);
|
|
300
298
|
```
|
|
301
299
|
|
|
300
|
+
Declared `out` projects the row — return it as-is (Date or `fx.clock.now()` epoch-ms → ISO-8601; extra columns strip).
|
|
301
|
+
|
|
302
302
|
</Tab>
|
|
303
303
|
|
|
304
304
|
<Tab value="POST">
|
|
305
305
|
|
|
306
|
-
Create a resource or run a command. `fx.json.create(
|
|
307
|
-
`{ data: value, error: null }
|
|
306
|
+
Create a resource or run a command. `fx.json.create(row)` returns `201 Created` with
|
|
307
|
+
`{ data: value, error: null }`. Declared `out` projects the store row (Date / epoch-ms → ISO).
|
|
308
|
+
HTTP `in` timestamps are ISO (`z.iso.datetime()`); store writes coerce them to `Date`.
|
|
308
309
|
|
|
309
310
|
```typescript title="src/flows/notes/create.ts"
|
|
310
311
|
import { on, flow, http } from "okengine";
|
|
@@ -319,8 +320,8 @@ export const create = on(
|
|
|
319
320
|
flow({
|
|
320
321
|
do: async ({ title }, fx) => {
|
|
321
322
|
const id = fx.id();
|
|
322
|
-
await fx.store(db).insert(notes).values({ id, title });
|
|
323
|
-
return fx.json.create(
|
|
323
|
+
const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
|
|
324
|
+
return fx.json.create(row);
|
|
324
325
|
},
|
|
325
326
|
}),
|
|
326
327
|
);
|
|
@@ -375,7 +376,6 @@ export const update = on(
|
|
|
375
376
|
title: z.string().min(1).optional(),
|
|
376
377
|
}),
|
|
377
378
|
out: z.object({ id: z.string(), title: z.string() }),
|
|
378
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
379
379
|
}),
|
|
380
380
|
flow({
|
|
381
381
|
do: async ({ id, title }, fx) => {
|
|
@@ -383,7 +383,7 @@ export const update = on(
|
|
|
383
383
|
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
384
384
|
}
|
|
385
385
|
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
386
|
-
if (!note) return fx.fail(
|
|
386
|
+
if (!note) return fx.fail.notFound({ id });
|
|
387
387
|
return note;
|
|
388
388
|
},
|
|
389
389
|
}),
|
|
@@ -462,7 +462,6 @@ import { db, notes } from "@/schema";
|
|
|
462
462
|
export const head = on(
|
|
463
463
|
http.head("/notes/:id", {
|
|
464
464
|
in: z.object({ id: z.string() }),
|
|
465
|
-
errors: { NotFound: z.object({ id: z.string() }) },
|
|
466
465
|
}),
|
|
467
466
|
flow({
|
|
468
467
|
do: async ({ id }, fx) => {
|
|
@@ -471,7 +470,7 @@ export const head = on(
|
|
|
471
470
|
.select({ id: notes.id })
|
|
472
471
|
.from(notes)
|
|
473
472
|
.where(eq(notes.id, id));
|
|
474
|
-
if (!note) return fx.fail(
|
|
473
|
+
if (!note) return fx.fail.notFound({ id });
|
|
475
474
|
return;
|
|
476
475
|
},
|
|
477
476
|
}),
|
|
@@ -574,7 +573,7 @@ pathless `http.resource()` — pass an explicit base path.
|
|
|
574
573
|
| `out` | Schema | _(required)_ | Item shape (get / list / update return) |
|
|
575
574
|
| `update` | Schema | `in` | Patch fields. Wire body is `{ id, ...patch }` |
|
|
576
575
|
| `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
|
|
577
|
-
| `errors` | error map |
|
|
576
|
+
| `errors` | error map | — | Extra domain codes. Built-in `NotFound` is always on |
|
|
578
577
|
| `id` | column | table PK | Column bound to `:id` |
|
|
579
578
|
| `list` | object | see List Options | List query grammar (`GET /notes`) |
|
|
580
579
|
| `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
|
|
@@ -934,8 +933,9 @@ Every HTTP flow returns the same envelope shape. You choose status and optional
|
|
|
934
933
|
custom wrapper.
|
|
935
934
|
|
|
936
935
|
<Callout title="Envelope is fixed">
|
|
937
|
-
Success and failure always use `{ data, error }` (optional top-level `meta`).
|
|
938
|
-
|
|
936
|
+
Success and failure always use `{ data, error }` (optional top-level `meta`). Use `fx.json.*` for
|
|
937
|
+
status and `meta`; use `fx.fail` for typed errors. A raw `Response` from `do` is the exception —
|
|
938
|
+
`302` + `Location` for redirects.
|
|
939
939
|
</Callout>
|
|
940
940
|
|
|
941
941
|
**Success** — returning a value from `do` produces `200 OK`:
|
|
@@ -946,18 +946,26 @@ custom wrapper.
|
|
|
946
946
|
|
|
947
947
|
Returning `undefined` produces a `204 No Content` response with an empty body.
|
|
948
948
|
|
|
949
|
-
**Custom status** — `fx.json.create` for `201 Created`, or `fx.json.ok` with optional `meta
|
|
949
|
+
**Custom status** — `fx.json.create` for `201 Created`, or `fx.json.ok` with optional `meta`.
|
|
950
|
+
When `out` is set, pass the store row — the kernel projects it (Date → ISO-8601; extra keys strip):
|
|
950
951
|
|
|
951
952
|
```typescript
|
|
952
|
-
return fx.json.create(
|
|
953
|
+
return fx.json.create(row);
|
|
953
954
|
// or
|
|
954
955
|
return fx.json.ok({ id: "ord_1" }, { meta: { traceId: fx.runId } });
|
|
955
956
|
```
|
|
956
957
|
|
|
957
|
-
**
|
|
958
|
+
**Redirects** — return a raw `Response` so the kernel does not wrap `{ data, error }`:
|
|
958
959
|
|
|
959
960
|
```typescript
|
|
960
|
-
return
|
|
961
|
+
return new Response(null, { status: 302, headers: { Location: url } });
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
**Typed failures** — built-in helpers need no `errors:` bag. Domain codes still go on `errors:`:
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
return fx.fail.notFound({ id: "123" });
|
|
968
|
+
return fx.fail("OutOfStock", { available: 0 });
|
|
961
969
|
```
|
|
962
970
|
|
|
963
971
|
```json
|
|
@@ -965,7 +973,7 @@ return fx.fail("NotFound", { id: "123" });
|
|
|
965
973
|
"data": null,
|
|
966
974
|
"error": {
|
|
967
975
|
"code": "NotFound",
|
|
968
|
-
"message": "
|
|
976
|
+
"message": "The requested resource was not found.",
|
|
969
977
|
"data": { "id": "123" }
|
|
970
978
|
}
|
|
971
979
|
}
|
|
@@ -976,8 +984,14 @@ Standard status code mappings:
|
|
|
976
984
|
- `ValidationError` → `422 Unprocessable Entity`
|
|
977
985
|
- `Unauthorized` → `401 Unauthorized`
|
|
978
986
|
- `Forbidden` → `403 Forbidden`
|
|
979
|
-
- `
|
|
980
|
-
-
|
|
987
|
+
- `NotFound` → `404 Not Found` (JSON envelope — router miss is plain-text `404`)
|
|
988
|
+
- `Conflict` / `ForeignKey` → `409 Conflict`
|
|
989
|
+
- `UnsupportedMediaType` → `415 Unsupported Media Type`
|
|
990
|
+
- `RateLimited` / `AuthRateLimited` → `429 Too Many Requests`
|
|
991
|
+
- `DatabaseError` → `422` (`not_null` / `check` / `invalid` / `too_long` / `out_of_range`), `503` (`retryable`), else `500`
|
|
992
|
+
- `ServiceUnavailable` → `503 Service Unavailable`
|
|
993
|
+
- `InternalError` → `500 Internal Server Error` (catalog message only)
|
|
994
|
+
- Domain codes (`OutOfStock`, `FlightFull`, `Duplicate`) → `400 Bad Request`
|
|
981
995
|
|
|
982
996
|
## Troubleshooting
|
|
983
997
|
|
|
@@ -1004,6 +1018,11 @@ Standard status code mappings:
|
|
|
1004
1018
|
`error.data.issues` array for the specific field validation failure.
|
|
1005
1019
|
</Accordion>
|
|
1006
1020
|
|
|
1021
|
+
<Accordion title="I mapped Date columns to ISO by hand">
|
|
1022
|
+
Declared `out` already projects store rows. `fx.json.create(row)`, `return row`, and
|
|
1023
|
+
`fx.json.withQuery(rows, input)` are enough — extra columns strip. A miss is not a client error.
|
|
1024
|
+
</Accordion>
|
|
1025
|
+
|
|
1007
1026
|
<Accordion title="Browser blocked by CORS / missing Access-Control-*">
|
|
1008
1027
|
Cross-origin access is closed until you plug the [`cors`](/docs/plugins/cors) plugin with an
|
|
1009
1028
|
explicit `origin`. Same-origin calls need no CORS headers. Preflight `OPTIONS` is answered by the
|