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/src/kernel/call.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test, beforeEach } from "bun:test";
|
|
2
2
|
import { oke } from "./app.ts";
|
|
3
|
+
import { fail } from "./fail-helpers.ts";
|
|
3
4
|
import { flow, resetFlowSeq } from "./flow.ts";
|
|
5
|
+
import { isFlowFailure } from "./hooks.ts";
|
|
4
6
|
import { on, resetBindings } from "./on.ts";
|
|
5
7
|
import { http } from "./triggers.ts";
|
|
6
8
|
|
|
@@ -120,3 +122,183 @@ describe("fx.call — untriggered flows", () => {
|
|
|
120
122
|
expect(runs).toBe(2);
|
|
121
123
|
});
|
|
122
124
|
});
|
|
125
|
+
|
|
126
|
+
describe("error pipeline — RPC / call propagation", () => {
|
|
127
|
+
test("regression: unhandled RPC exception must never become 204 No Content", async () => {
|
|
128
|
+
const boom = flow("math.boom", {
|
|
129
|
+
do: () => {
|
|
130
|
+
throw new Error("sensitive leak");
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const app = oke({ autoBoot: false, name: "rpc-boom" }).adopt(boom);
|
|
134
|
+
|
|
135
|
+
const res = await app.fetch(
|
|
136
|
+
new Request("http://localhost/_oke/math/boom", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "content-type": "application/json" },
|
|
139
|
+
body: "{}",
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(res.status).toBe(500);
|
|
144
|
+
expect(res.status).not.toBe(204);
|
|
145
|
+
const body = (await res.json()) as {
|
|
146
|
+
data: unknown;
|
|
147
|
+
error: { code: string; data: unknown; message?: string };
|
|
148
|
+
};
|
|
149
|
+
expect(body.data).toBeNull();
|
|
150
|
+
expect(body.error.code).toBe("InternalError");
|
|
151
|
+
expect(body.error.data).toEqual({});
|
|
152
|
+
expect(body.error.message).toBe("Something went wrong. Try again.");
|
|
153
|
+
expect(JSON.stringify(body)).not.toContain("sensitive leak");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("RPC store auto-map unique violation is Conflict 409", async () => {
|
|
157
|
+
const clash = flow("math.clash", {
|
|
158
|
+
do: () => {
|
|
159
|
+
throw Object.assign(new Error("duplicate key"), {
|
|
160
|
+
code: "23505",
|
|
161
|
+
constraint: "users_email_key",
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
const app = oke({ autoBoot: false, name: "rpc-clash" }).adopt(clash);
|
|
166
|
+
|
|
167
|
+
const res = await app.fetch(
|
|
168
|
+
new Request("http://localhost/_oke/math/clash", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "content-type": "application/json" },
|
|
171
|
+
body: "{}",
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
expect(res.status).toBe(409);
|
|
176
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
177
|
+
expect(body.error.code).toBe("Conflict");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("ExecuteResult.error is a derived projection of ctx.error", async () => {
|
|
181
|
+
const boom = flow("math.boom", {
|
|
182
|
+
do: () => {
|
|
183
|
+
throw new Error("disk failed");
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const app = oke({ autoBoot: false, name: "exec-err" }).adopt(boom);
|
|
187
|
+
const result = await app.execute(boom, {}, { kind: "internal" });
|
|
188
|
+
|
|
189
|
+
expect(result.failure).toBeUndefined();
|
|
190
|
+
expect(result.ctx.error).toBeDefined();
|
|
191
|
+
expect(result.error).toBe(result.ctx.error);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("app.call returns FlowFailure value for controlled fail", async () => {
|
|
195
|
+
const missing = flow("notes.missing", {
|
|
196
|
+
do: () => fail.notFound({ id: "1" }),
|
|
197
|
+
});
|
|
198
|
+
const app = oke({ autoBoot: false, name: "call-fail" }).adopt(missing);
|
|
199
|
+
const out = await app.call(missing, {});
|
|
200
|
+
expect(isFlowFailure(out)).toBe(true);
|
|
201
|
+
if (isFlowFailure(out)) {
|
|
202
|
+
expect(out.error.code).toBe("NotFound");
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("app.call rethrows unhandled exception (never undefined)", async () => {
|
|
207
|
+
const boom = flow("notes.boom", {
|
|
208
|
+
do: () => {
|
|
209
|
+
throw new Error("disk failed");
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const app = oke({ autoBoot: false, name: "call-throw" }).adopt(boom);
|
|
213
|
+
await expect(app.call(boom, {})).rejects.toThrow("disk failed");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("fx.call returns FlowFailure value for controlled fail", async () => {
|
|
217
|
+
const child = flow("child.fail", {
|
|
218
|
+
do: () => fail.notFound({ id: "x" }),
|
|
219
|
+
});
|
|
220
|
+
on(
|
|
221
|
+
http.post("/run"),
|
|
222
|
+
flow("parent.run", {
|
|
223
|
+
effects: { calls: ["child.fail"] },
|
|
224
|
+
do: async (_input: Record<string, never>, fx) => {
|
|
225
|
+
const out = await fx.call("child.fail", {});
|
|
226
|
+
expect(isFlowFailure(out)).toBe(true);
|
|
227
|
+
return isFlowFailure(out) ? out : { unexpected: true };
|
|
228
|
+
},
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
const app = oke({ autoBoot: false, name: "fx-fail" }).adopt(child);
|
|
232
|
+
const res = await app.fetch(
|
|
233
|
+
new Request("http://localhost/run", {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: { "content-type": "application/json" },
|
|
236
|
+
body: "{}",
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
expect(res.status).toBe(404);
|
|
240
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
241
|
+
expect(body.error.code).toBe("NotFound");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("fx.call rethrows when child throws", async () => {
|
|
245
|
+
const child = flow("child.boom", {
|
|
246
|
+
do: () => {
|
|
247
|
+
throw new Error("worker crash");
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
on(
|
|
251
|
+
http.post("/run"),
|
|
252
|
+
flow("parent.run", {
|
|
253
|
+
effects: { calls: ["child.boom"] },
|
|
254
|
+
do: async (_input: Record<string, never>, fx) => {
|
|
255
|
+
return fx.call("child.boom", {});
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
const app = oke({ autoBoot: false, name: "fx-throw" }).adopt(child);
|
|
260
|
+
const res = await app.fetch(
|
|
261
|
+
new Request("http://localhost/run", {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "content-type": "application/json" },
|
|
264
|
+
body: "{}",
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
expect(res.status).toBe(500);
|
|
268
|
+
const body = (await res.json()) as { data: unknown; error: { code: string; message?: string } };
|
|
269
|
+
expect(body.data).toBeNull();
|
|
270
|
+
expect(body.error.code).toBe("InternalError");
|
|
271
|
+
expect(JSON.stringify(body)).not.toContain("worker crash");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("nested fx.call chain propagates throw to outer boundary", async () => {
|
|
275
|
+
const deep = flow("deep.boom", {
|
|
276
|
+
do: () => {
|
|
277
|
+
throw new Error("deep crash");
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
const mid = flow("mid.call", {
|
|
281
|
+
effects: { calls: ["deep.boom"] },
|
|
282
|
+
do: async (_input: Record<string, never>, fx) => fx.call("deep.boom", {}),
|
|
283
|
+
});
|
|
284
|
+
on(
|
|
285
|
+
http.post("/run"),
|
|
286
|
+
flow("outer.run", {
|
|
287
|
+
effects: { calls: ["mid.call"] },
|
|
288
|
+
do: async (_input: Record<string, never>, fx) => fx.call("mid.call", {}),
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
const app = oke({ autoBoot: false, name: "nested-throw" }).adopt(deep).adopt(mid);
|
|
292
|
+
const res = await app.fetch(
|
|
293
|
+
new Request("http://localhost/run", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "content-type": "application/json" },
|
|
296
|
+
body: "{}",
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
expect(res.status).toBe(500);
|
|
300
|
+
const body = (await res.json()) as { error: { code: string } };
|
|
301
|
+
expect(body.error.code).toBe("InternalError");
|
|
302
|
+
expect(JSON.stringify(body)).not.toContain("deep crash");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OKE1510 — kept off the kernel edge graph.
|
|
3
|
+
*
|
|
4
|
+
* Vault boot gaps. Loaded via computed `import.meta.require` from
|
|
5
|
+
* {@link lookupOkeError}.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OkeErrorDefinition } from "./errors.ts";
|
|
9
|
+
|
|
10
|
+
/** Vault contract has no value in any resolution layer (boot). */
|
|
11
|
+
export const VAULT_SECRET_MISSING: OkeErrorDefinition = {
|
|
12
|
+
code: 1510,
|
|
13
|
+
domain: "vault",
|
|
14
|
+
cause: "{count} secrets have no value in any resolution layer.",
|
|
15
|
+
fix: "Set each name (`oke vault set <name>`, or `.env.local`).",
|
|
16
|
+
};
|
|
@@ -20,6 +20,7 @@ import { CHANNEL_SCHEMA } from "./errors-channel.ts";
|
|
|
20
20
|
import { FLOW_NAME_DUPLICATE, FLOW_UNNAMED } from "./errors-flow-name.ts";
|
|
21
21
|
import { ONCE_SIGNAL_MULTI_FLOW } from "./errors-once-signal.ts";
|
|
22
22
|
import { TENANT_NOT_MEMBER, TENANT_REQUIRED, TENANT_UNKNOWN_SCOPE } from "./errors-tenant.ts";
|
|
23
|
+
import { VAULT_SECRET_MISSING } from "./errors-vault.ts";
|
|
23
24
|
import {
|
|
24
25
|
assertCodesInDomainRanges,
|
|
25
26
|
assertUniqueCodes,
|
|
@@ -65,8 +66,10 @@ describe("OKE error-code registry", () => {
|
|
|
65
66
|
expect(files).toContain("errors-live-resume.ts");
|
|
66
67
|
expect(files).toContain("errors-channel.ts");
|
|
67
68
|
expect(files).toContain("errors-tenant.ts");
|
|
69
|
+
expect(files).toContain("errors-vault.ts");
|
|
68
70
|
const codes = defs.map((d) => d.code);
|
|
69
71
|
expect(codes).toContain(1210);
|
|
72
|
+
expect(codes).toContain(1510);
|
|
70
73
|
expect(codes).toContain(1605);
|
|
71
74
|
expect(codes).toContain(1810);
|
|
72
75
|
expect(codes).toContain(1820);
|
|
@@ -160,6 +163,10 @@ describe("OKE error-code registry", () => {
|
|
|
160
163
|
expect(lookupOkeError(1605)).toEqual(CHANNEL_SCHEMA);
|
|
161
164
|
});
|
|
162
165
|
|
|
166
|
+
test("lookupOkeError finds the lazy VAULT_SECRET_MISSING entry", () => {
|
|
167
|
+
expect(lookupOkeError(1510)).toEqual(VAULT_SECRET_MISSING);
|
|
168
|
+
});
|
|
169
|
+
|
|
163
170
|
test("lookupOkeError finds lazy tenant entries", () => {
|
|
164
171
|
expect(lookupOkeError(1810)).toEqual(TENANT_REQUIRED);
|
|
165
172
|
expect(lookupOkeError(1820)).toEqual(TENANT_NOT_MEMBER);
|
package/src/kernel/errors.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { docsUrl as absoluteDocsUrl } from "../docs-origin.ts";
|
|
10
10
|
import { getActiveDefaultLocale, getActiveLocale } from "../i18n/locale-context.ts";
|
|
11
11
|
import { lazyRequire } from "./lazy-require.ts";
|
|
12
|
+
import type { BuiltinErrorMap } from "./builtin-errors.ts";
|
|
12
13
|
|
|
13
14
|
/** Catalogs — kept off the edge `fail` / `OKE_ERRORS` static graph. */
|
|
14
15
|
function loadMessages(): typeof import("../i18n/messages.ts") {
|
|
@@ -144,6 +145,93 @@ export interface FailOptions {
|
|
|
144
145
|
readonly message?: string;
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Callable `fail` plus built-in helpers (`fail.notFound`, `fail.forbidden`, …).
|
|
150
|
+
*/
|
|
151
|
+
export interface FailFn {
|
|
152
|
+
/**
|
|
153
|
+
* Flow-boundary failure value (does not throw).
|
|
154
|
+
*
|
|
155
|
+
* @param code - Declared or built-in error code
|
|
156
|
+
* @param data - Error payload
|
|
157
|
+
* @param opts - Optional message override
|
|
158
|
+
*/
|
|
159
|
+
<E>(code: string, data: E, opts?: FailOptions): FlowFailure<E>;
|
|
160
|
+
/** Missing row / object — HTTP 404. */
|
|
161
|
+
notFound(
|
|
162
|
+
data?: BuiltinErrorMap["NotFound"],
|
|
163
|
+
opts?: FailOptions,
|
|
164
|
+
): FlowFailure<BuiltinErrorMap["NotFound"]>;
|
|
165
|
+
/** Not authenticated — HTTP 401. */
|
|
166
|
+
unauthorized(
|
|
167
|
+
data?: BuiltinErrorMap["Unauthorized"],
|
|
168
|
+
opts?: FailOptions,
|
|
169
|
+
): FlowFailure<BuiltinErrorMap["Unauthorized"]>;
|
|
170
|
+
/** Authenticated but denied — HTTP 403. */
|
|
171
|
+
forbidden(
|
|
172
|
+
data?: BuiltinErrorMap["Forbidden"],
|
|
173
|
+
opts?: FailOptions,
|
|
174
|
+
): FlowFailure<BuiltinErrorMap["Forbidden"]>;
|
|
175
|
+
/** Unique / exclusion conflict — HTTP 409. */
|
|
176
|
+
conflict(
|
|
177
|
+
data?: BuiltinErrorMap["Conflict"],
|
|
178
|
+
opts?: FailOptions,
|
|
179
|
+
): FlowFailure<BuiltinErrorMap["Conflict"]>;
|
|
180
|
+
/** Foreign-key violation — HTTP 409. */
|
|
181
|
+
foreignKey(
|
|
182
|
+
data?: BuiltinErrorMap["ForeignKey"],
|
|
183
|
+
opts?: FailOptions,
|
|
184
|
+
): FlowFailure<BuiltinErrorMap["ForeignKey"]>;
|
|
185
|
+
/** Rate budget exhausted — HTTP 429. */
|
|
186
|
+
rateLimited(
|
|
187
|
+
data?: BuiltinErrorMap["RateLimited"],
|
|
188
|
+
opts?: FailOptions,
|
|
189
|
+
): FlowFailure<BuiltinErrorMap["RateLimited"]>;
|
|
190
|
+
/** Maintenance / connection — HTTP 503. */
|
|
191
|
+
serviceUnavailable(
|
|
192
|
+
data?: BuiltinErrorMap["ServiceUnavailable"],
|
|
193
|
+
opts?: FailOptions,
|
|
194
|
+
): FlowFailure<BuiltinErrorMap["ServiceUnavailable"]>;
|
|
195
|
+
/** SQL / store constraint that is not unique or FK. */
|
|
196
|
+
database(
|
|
197
|
+
data: BuiltinErrorMap["DatabaseError"],
|
|
198
|
+
opts?: FailOptions,
|
|
199
|
+
): FlowFailure<BuiltinErrorMap["DatabaseError"]>;
|
|
200
|
+
/** Unhandled throw — HTTP 500. Catalog message only. */
|
|
201
|
+
internal(
|
|
202
|
+
data?: BuiltinErrorMap["InternalError"],
|
|
203
|
+
opts?: FailOptions,
|
|
204
|
+
): FlowFailure<BuiltinErrorMap["InternalError"]>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Create a flow-boundary failure value (does not throw).
|
|
209
|
+
*
|
|
210
|
+
* When `opts.message` is omitted, attaches a localized message from the
|
|
211
|
+
* built-in / app catalogs (`errors.{code}.{reason}` → `errors.{code}`) using
|
|
212
|
+
* the active request locale. Custom codes with no catalog entry stay
|
|
213
|
+
* message-less.
|
|
214
|
+
*
|
|
215
|
+
* @param code - Declared error code from the flow's `errors` map
|
|
216
|
+
* @param data - Error payload
|
|
217
|
+
* @param opts - Optional message override
|
|
218
|
+
*/
|
|
219
|
+
function failImpl<E>(code: string, data: E, opts?: FailOptions): FlowFailure<E> {
|
|
220
|
+
const message =
|
|
221
|
+
opts?.message !== undefined
|
|
222
|
+
? opts.message
|
|
223
|
+
: loadFailureMessage().resolveFailureMessage(code, data);
|
|
224
|
+
const error: FlowErrorValue<E> = message !== undefined ? { code, data, message } : { code, data };
|
|
225
|
+
return { data: null, error };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Flow-boundary failure (callable). Built-in helpers live on the lazy
|
|
230
|
+
* `fail-helpers` chunk so they stay off the kernel edge graph — {@link createFx}
|
|
231
|
+
* and the public `okengine` export load that chunk.
|
|
232
|
+
*/
|
|
233
|
+
export const fail: FailFn = failImpl as FailFn;
|
|
234
|
+
|
|
147
235
|
/**
|
|
148
236
|
* Permanent error registry. Codes are stable within their domain range after
|
|
149
237
|
* the domain-range renumber; each entry declares `domain` for range guards.
|
|
@@ -239,7 +327,7 @@ export const OKE_ERRORS = {
|
|
|
239
327
|
},
|
|
240
328
|
/**
|
|
241
329
|
* A `src/flows/<unit>` folder exists on disk but no adopted flow carries
|
|
242
|
-
* that unit — the generated `.adopt()` barrel (`src/flows/
|
|
330
|
+
* that unit — the generated `.adopt()` barrel (`src/flows/index.ts`)
|
|
243
331
|
* is stale or was hand-edited. dev+compose / prod — never a silently-incomplete
|
|
244
332
|
* route table in a deploy-shaped environment.
|
|
245
333
|
*/
|
|
@@ -247,7 +335,7 @@ export const OKE_ERRORS = {
|
|
|
247
335
|
code: 1030,
|
|
248
336
|
domain: "kernel",
|
|
249
337
|
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
250
|
-
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/
|
|
338
|
+
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/index.ts`.",
|
|
251
339
|
},
|
|
252
340
|
/**
|
|
253
341
|
* `http.get()` was never stamped from the file tree — refuse a silent `/`.
|
|
@@ -256,7 +344,7 @@ export const OKE_ERRORS = {
|
|
|
256
344
|
code: 1040,
|
|
257
345
|
domain: "kernel",
|
|
258
346
|
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
259
|
-
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows
|
|
347
|
+
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows`, or pass an explicit path to `http.{method}("/…")`.',
|
|
260
348
|
},
|
|
261
349
|
/**
|
|
262
350
|
* Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
|
|
@@ -368,6 +456,12 @@ export function lookupOkeError(code: OkeErrorCode): OkeErrorDefinition | undefin
|
|
|
368
456
|
["errors", "live", "resume"].join("-"),
|
|
369
457
|
).LIVE_RESUME_GAP;
|
|
370
458
|
}
|
|
459
|
+
if (code === 1510) {
|
|
460
|
+
return lazyRequire<typeof import("./errors-vault.ts")>(
|
|
461
|
+
import.meta.dir,
|
|
462
|
+
["errors", "vault"].join("-"),
|
|
463
|
+
).VAULT_SECRET_MISSING;
|
|
464
|
+
}
|
|
371
465
|
if (code === 1605) {
|
|
372
466
|
return lazyRequire<typeof import("./errors-channel.ts")>(
|
|
373
467
|
import.meta.dir,
|
|
@@ -383,27 +477,6 @@ export function lookupOkeError(code: OkeErrorCode): OkeErrorDefinition | undefin
|
|
|
383
477
|
return undefined;
|
|
384
478
|
}
|
|
385
479
|
|
|
386
|
-
/**
|
|
387
|
-
* Create a flow-boundary failure value (does not throw).
|
|
388
|
-
*
|
|
389
|
-
* When `opts.message` is omitted, attaches a localized message from the
|
|
390
|
-
* built-in / app catalogs (`errors.{code}.{reason}` → `errors.{code}`) using
|
|
391
|
-
* the active request locale. Custom codes with no catalog entry stay
|
|
392
|
-
* message-less.
|
|
393
|
-
*
|
|
394
|
-
* @param code - Declared error code from the flow's `errors` map
|
|
395
|
-
* @param data - Error payload
|
|
396
|
-
* @param opts - Optional message override
|
|
397
|
-
*/
|
|
398
|
-
export function fail<E>(code: string, data: E, opts?: FailOptions): FlowFailure<E> {
|
|
399
|
-
const message =
|
|
400
|
-
opts?.message !== undefined
|
|
401
|
-
? opts.message
|
|
402
|
-
: loadFailureMessage().resolveFailureMessage(code, data);
|
|
403
|
-
const error: FlowErrorValue<E> = message !== undefined ? { code, data, message } : { code, data };
|
|
404
|
-
return { data: null, error };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
480
|
/**
|
|
408
481
|
* Format the canonical multi-line OKE error message (§21).
|
|
409
482
|
*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in `fail.notFound` / `fail.forbidden` / … helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept off the kernel edge graph — loaded via computed `import.meta.require`.
|
|
5
|
+
* Mutates the shared {@link fail} function so `fx.fail` and `import { fail }`
|
|
6
|
+
* share one identity.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fail as failImpl, type FailFn, type FailOptions } from "./errors.ts";
|
|
10
|
+
import type { BuiltinErrorMap } from "./builtin-errors.ts";
|
|
11
|
+
|
|
12
|
+
const empty = {};
|
|
13
|
+
const helper =
|
|
14
|
+
(code: string) =>
|
|
15
|
+
(data = empty, opts?: FailOptions) =>
|
|
16
|
+
failImpl(code, data, opts);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Callable `fail` plus helpers. Same function object as kernel `fail`.
|
|
20
|
+
*/
|
|
21
|
+
export const fail: FailFn = Object.assign(failImpl, {
|
|
22
|
+
notFound: helper("NotFound"),
|
|
23
|
+
unauthorized: helper("Unauthorized"),
|
|
24
|
+
forbidden: helper("Forbidden"),
|
|
25
|
+
conflict: helper("Conflict"),
|
|
26
|
+
foreignKey: helper("ForeignKey"),
|
|
27
|
+
rateLimited: helper("RateLimited"),
|
|
28
|
+
serviceUnavailable: helper("ServiceUnavailable"),
|
|
29
|
+
database: (data: BuiltinErrorMap["DatabaseError"], opts?: FailOptions) =>
|
|
30
|
+
failImpl("DatabaseError", data, opts),
|
|
31
|
+
internal: helper("InternalError"),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type { FailFn, FailOptions } from "./errors.ts";
|
package/src/kernel/flow-units.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-evaluation registry for `src/flows/
|
|
2
|
+
* Module-evaluation registry for `src/flows/index.ts`.
|
|
3
3
|
*
|
|
4
|
-
* Same consume/ignore posture as `on()` / `listBindings()`:
|
|
4
|
+
* Same consume/ignore posture as `on()` / `listBindings()`: the adopt barrel
|
|
5
5
|
* calls {@link registerFlowUnits}; {@link oke} drains the bag into `$routes`
|
|
6
6
|
* unless `registry: "ignore"`.
|
|
7
7
|
*/
|
|
@@ -19,7 +19,7 @@ const pending: Record<string, FlowUnitBag> = {};
|
|
|
19
19
|
/**
|
|
20
20
|
* Record generated flow units for the next {@link oke} construction.
|
|
21
21
|
*
|
|
22
|
-
* @param units - `{ notes, main, … }` from `
|
|
22
|
+
* @param units - `{ notes, main, … }` from `src/flows/index.ts`
|
|
23
23
|
*/
|
|
24
24
|
export function registerFlowUnits(units: Record<string, FlowUnitBag>): void {
|
|
25
25
|
Object.assign(pending, units);
|
package/src/kernel/fx.test.ts
CHANGED
|
@@ -728,6 +728,14 @@ describe("errors — registry", () => {
|
|
|
728
728
|
});
|
|
729
729
|
});
|
|
730
730
|
|
|
731
|
+
test("fx.fail.notFound is callable without errors: declaration", () => {
|
|
732
|
+
const fx = createFx({ flow: "x", effects: {} });
|
|
733
|
+
const result = fx.fail.notFound({ id: "n1" });
|
|
734
|
+
expect(result.error.code).toBe("NotFound");
|
|
735
|
+
expect(result.error.data).toEqual({ id: "n1" });
|
|
736
|
+
expect(result.data).toBeNull();
|
|
737
|
+
});
|
|
738
|
+
|
|
731
739
|
test("capability token allows only declared resources", () => {
|
|
732
740
|
const token = createCapabilityToken("f", {
|
|
733
741
|
reads: ["sql:a"],
|
package/src/kernel/fx.ts
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
recordWouldHaveFired,
|
|
59
59
|
touchDryRunStore,
|
|
60
60
|
} from "./dry-run.ts";
|
|
61
|
-
import
|
|
61
|
+
import type { FailFn } from "./errors.ts";
|
|
62
62
|
import { currentAbortSignal, linkAbort } from "./abort-scope.ts";
|
|
63
63
|
import {
|
|
64
64
|
fxAll,
|
|
@@ -82,6 +82,13 @@ import { okid } from "../okid.ts";
|
|
|
82
82
|
import { lazyRequire } from "./lazy-require.ts";
|
|
83
83
|
import type { AppMessageKey, MessageValues } from "../i18n/types.ts";
|
|
84
84
|
|
|
85
|
+
function loadFail(): FailFn {
|
|
86
|
+
return lazyRequire<typeof import("./fail-helpers.ts")>(
|
|
87
|
+
import.meta.dir,
|
|
88
|
+
["fail", "helpers"].join("-"),
|
|
89
|
+
).fail;
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
/** Lazy runs/window helpers — kept off the cold `oke` static graph. */
|
|
86
93
|
async function loadRunsWindow(): Promise<typeof import("../runs/window.ts")> {
|
|
87
94
|
return import("../runs/window.ts");
|
|
@@ -182,7 +189,11 @@ export interface FxTenant {
|
|
|
182
189
|
|
|
183
190
|
/** Clock surface on `fx`. */
|
|
184
191
|
export interface FxClock {
|
|
185
|
-
/**
|
|
192
|
+
/**
|
|
193
|
+
* Current epoch-ms (injectable via {@link CreateFxOptions.now}).
|
|
194
|
+
* Pass into SQL `timestamp` / `date` columns as-is — store insert/update
|
|
195
|
+
* and WHERE binds coerce finite numbers and parseable ISO strings to `Date`.
|
|
196
|
+
*/
|
|
186
197
|
now(): number;
|
|
187
198
|
/**
|
|
188
199
|
* Instant `duration` before {@link FxClock.now} (`"30d"` → now − 30 days).
|
|
@@ -465,11 +476,17 @@ export type JsonPage<T> = {
|
|
|
465
476
|
* JSON response helpers. `fx.json.create` answers 201; `fx.json.ok` can carry
|
|
466
477
|
* a top-level `meta` (Stripe-style `{ data, meta?, error }`);
|
|
467
478
|
* `fx.json.empty` answers 204.
|
|
479
|
+
* When the exposure declares `out`, the kernel projects the success value onto
|
|
480
|
+
* that schema — `fx.json.create(row)` is enough; no Date→ISO mapper.
|
|
468
481
|
*/
|
|
469
482
|
export interface FxJson {
|
|
470
483
|
/** 200 — body `{ data: value, meta?, error: null }`. */
|
|
471
484
|
ok<T>(value: T, opts?: { readonly meta?: Record<string, unknown> }): JsonResult<T>;
|
|
472
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* 201 — body `{ data: value, error: null }`.
|
|
487
|
+
* When the exposure declares `out`, the kernel projects `value` onto that
|
|
488
|
+
* schema (Date → ISO-8601, extra keys stripped) so a store `row` is enough.
|
|
489
|
+
*/
|
|
473
490
|
create<T>(value: T): JsonResult<T>;
|
|
474
491
|
/** 204 — no body. */
|
|
475
492
|
empty(): JsonResult<never>;
|
|
@@ -874,11 +891,10 @@ export interface Fx {
|
|
|
874
891
|
/**
|
|
875
892
|
* Flow-boundary failure value (does not throw).
|
|
876
893
|
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* @param opts - Optional message
|
|
894
|
+
* Built-in helpers (`fx.fail.notFound`, `fx.fail.forbidden`, …) need no
|
|
895
|
+
* `errors:` declaration. Domain codes still use `fx.fail("OutOfStock", data)`.
|
|
880
896
|
*/
|
|
881
|
-
fail
|
|
897
|
+
readonly fail: FailFn;
|
|
882
898
|
/** JSON response helpers (status + Stripe-style envelope). */
|
|
883
899
|
readonly json: FxJson;
|
|
884
900
|
/**
|
|
@@ -2236,7 +2252,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
2236
2252
|
operator,
|
|
2237
2253
|
principal,
|
|
2238
2254
|
tenant,
|
|
2239
|
-
fail,
|
|
2255
|
+
fail: loadFail(),
|
|
2240
2256
|
json: {
|
|
2241
2257
|
ok<T>(value: T, opts?: { readonly meta?: Record<string, unknown> }): JsonResult<T> {
|
|
2242
2258
|
return {
|
package/src/kernel/index.ts
CHANGED
|
@@ -106,14 +106,15 @@ export {
|
|
|
106
106
|
type RunTelemetry,
|
|
107
107
|
} from "./run-telemetry.ts";
|
|
108
108
|
|
|
109
|
+
export { fail } from "./fail-helpers.ts";
|
|
109
110
|
export {
|
|
110
|
-
fail,
|
|
111
111
|
formatOkeMessage,
|
|
112
112
|
lookupOkeError,
|
|
113
113
|
OKE_ERROR_RANGES,
|
|
114
114
|
OKE_ERRORS,
|
|
115
115
|
OkeError,
|
|
116
116
|
throwOke,
|
|
117
|
+
type FailFn,
|
|
117
118
|
type FailOptions,
|
|
118
119
|
type FlowErrorValue,
|
|
119
120
|
type FlowFailure,
|
|
@@ -122,6 +123,16 @@ export {
|
|
|
122
123
|
type OkeErrorDomain,
|
|
123
124
|
type OkeErrorParams,
|
|
124
125
|
} from "./errors.ts";
|
|
126
|
+
export {
|
|
127
|
+
BUILTIN_ERROR_STATUS,
|
|
128
|
+
httpStatusForFailure,
|
|
129
|
+
statusForBuiltinError,
|
|
130
|
+
type BuiltinErrorBag,
|
|
131
|
+
type BuiltinErrorCode,
|
|
132
|
+
type BuiltinErrorMap,
|
|
133
|
+
type BuiltinValidationIssue,
|
|
134
|
+
type DatabaseErrorReason,
|
|
135
|
+
} from "./builtin-errors.ts";
|
|
125
136
|
|
|
126
137
|
export {
|
|
127
138
|
flow,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-path mutation id header — shared by the kernel HTTP glue and
|
|
3
|
+
* `okengine/client-react` optimistic mutate. Keep this file free of Node
|
|
4
|
+
* APIs so Vite SPAs can import `Can` / `useLiveQuery`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Header echoed into `mutationId` on write-path live-query events. */
|
|
8
|
+
export const MUTATION_ID_HEADER = "x-oke-mutation-id";
|
package/src/kernel/plugin.ts
CHANGED
|
@@ -290,9 +290,10 @@ export interface PluginApi {
|
|
|
290
290
|
*/
|
|
291
291
|
channelTemplate(decl: ChannelTemplateDecl): PluginApi;
|
|
292
292
|
/**
|
|
293
|
-
* Contribute template body catalog entries
|
|
293
|
+
* Contribute template body catalog entries — overlay merged into boot
|
|
294
|
+
* channel catalog after `.template({ catalog })` bodies (`{{field}}`).
|
|
294
295
|
*
|
|
295
|
-
* @param catalog - Locale bodies keyed by template name
|
|
296
|
+
* @param catalog - Locale bodies keyed by template name
|
|
296
297
|
*/
|
|
297
298
|
channelCatalog(catalog: TemplateCatalog): PluginApi;
|
|
298
299
|
}
|
|
@@ -475,7 +476,7 @@ export interface PluginDef<D extends Record<string, unknown> = {}> {
|
|
|
475
476
|
gate(decl: GateDecl | GateAllDecl): PluginDef<D>;
|
|
476
477
|
/** Queue a channel template. */
|
|
477
478
|
channelTemplate(decl: ChannelTemplateDecl): PluginDef<D>;
|
|
478
|
-
/** Queue channel template body catalog entries. */
|
|
479
|
+
/** Queue channel template body catalog entries (overlay after `.template({ catalog })`). */
|
|
479
480
|
channelCatalog(catalog: TemplateCatalog): PluginDef<D>;
|
|
480
481
|
}
|
|
481
482
|
|