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
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map driver SQL errors to typed {@link FlowFailure} values.
|
|
3
|
+
*
|
|
4
|
+
* Lives on the store path — not the kernel edge graph. Public payloads are
|
|
5
|
+
* names only (constraint / table / column / sqlstate). Never copy
|
|
6
|
+
* `message` / `detail` / `hint`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fail } from "../../kernel/fail-helpers.ts";
|
|
10
|
+
import type { FlowFailure } from "../../kernel/errors.ts";
|
|
11
|
+
import type { BuiltinErrorMap } from "../../kernel/builtin-errors.ts";
|
|
12
|
+
|
|
13
|
+
/** How to treat serialization / deadlock SQLSTATE (`40001` / `40P01`). */
|
|
14
|
+
export type SqlRetryableMode = "leave" | "unavailable";
|
|
15
|
+
|
|
16
|
+
/** Options for {@link sqlErrorToFailure}. */
|
|
17
|
+
export interface SqlErrorToFailureOptions {
|
|
18
|
+
/**
|
|
19
|
+
* `leave` — return `undefined` so {@link flow.retry} can rethrow.
|
|
20
|
+
* `unavailable` — map to `ServiceUnavailable` after retries exhaust.
|
|
21
|
+
*/
|
|
22
|
+
readonly retryable?: SqlRetryableMode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SqlFailure = FlowFailure<
|
|
26
|
+
| BuiltinErrorMap["Conflict"]
|
|
27
|
+
| BuiltinErrorMap["DatabaseError"]
|
|
28
|
+
| BuiltinErrorMap["ServiceUnavailable"]
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
type SqlErrShape = {
|
|
32
|
+
readonly name?: unknown;
|
|
33
|
+
readonly code?: unknown;
|
|
34
|
+
readonly errno?: unknown;
|
|
35
|
+
readonly message?: unknown;
|
|
36
|
+
readonly cause?: unknown;
|
|
37
|
+
readonly constraint?: unknown;
|
|
38
|
+
readonly table?: unknown;
|
|
39
|
+
readonly column?: unknown;
|
|
40
|
+
readonly table_name?: unknown;
|
|
41
|
+
readonly column_name?: unknown;
|
|
42
|
+
readonly constraint_name?: unknown;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const CONFLICT_STATE = new Set(["23505", "23P01"]);
|
|
46
|
+
const FOREIGN_KEY_STATE = new Set(["23503", "23001"]);
|
|
47
|
+
const NOT_NULL_STATE = new Set(["23502"]);
|
|
48
|
+
const CHECK_STATE = new Set(["23514"]);
|
|
49
|
+
const RETRY_STATE = new Set(["40001", "40P01", "55P03"]);
|
|
50
|
+
const TOO_LONG_STATE = new Set(["22001"]);
|
|
51
|
+
const OUT_OF_RANGE_STATE = new Set(["22003", "22008"]);
|
|
52
|
+
const INVALID_STATE = new Set(["22P02", "22007", "22023", "42804"]);
|
|
53
|
+
const UNAVAILABLE_STATE = new Set([
|
|
54
|
+
"53300",
|
|
55
|
+
"53100",
|
|
56
|
+
"53200",
|
|
57
|
+
"53400",
|
|
58
|
+
"57P01",
|
|
59
|
+
"57P02",
|
|
60
|
+
"57P03",
|
|
61
|
+
"57P04",
|
|
62
|
+
"57014",
|
|
63
|
+
"25P03",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/** bun:sqlite / libsql busy / locked (leave thrown for retry). */
|
|
67
|
+
const SQLITE_RETRY_ERRNO = new Set([5, 6, 261, 517]);
|
|
68
|
+
/** bun:sqlite PRIMARYKEY / ROWID unique. */
|
|
69
|
+
const SQLITE_CONFLICT_ERRNO = new Set([1555, 2067, 2579]);
|
|
70
|
+
const SQLITE_UNAVAILABLE_ERRNO = new Set([8, 10, 13, 14, 15]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* True when `err` is a serialization / deadlock / lock-busy failure.
|
|
74
|
+
*
|
|
75
|
+
* Left thrown during retries; mapped to `ServiceUnavailable` after exhaust.
|
|
76
|
+
*
|
|
77
|
+
* @param err - Caught driver error
|
|
78
|
+
*/
|
|
79
|
+
export function isRetryableSqlError(err: unknown): boolean {
|
|
80
|
+
return walk(err, (node) => retrySignal(node));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Map a driver SQL error to a typed failure, or `undefined` when it is not a
|
|
85
|
+
* SQL constraint / connection failure (or is retryable under `leave`).
|
|
86
|
+
*
|
|
87
|
+
* @param err - Caught value from `query` / `exec`
|
|
88
|
+
* @param options - Retryable handling
|
|
89
|
+
*/
|
|
90
|
+
export function sqlErrorToFailure(
|
|
91
|
+
err: unknown,
|
|
92
|
+
options: SqlErrorToFailureOptions = {},
|
|
93
|
+
): SqlFailure | undefined {
|
|
94
|
+
const retryable = options.retryable ?? "leave";
|
|
95
|
+
let unknownNode: unknown;
|
|
96
|
+
let current: unknown = err;
|
|
97
|
+
for (let i = 0; i < 4; i++) {
|
|
98
|
+
if (!current || typeof current !== "object") break;
|
|
99
|
+
if (retrySignal(current)) {
|
|
100
|
+
if (retryable === "leave") return undefined;
|
|
101
|
+
return fail.serviceUnavailable();
|
|
102
|
+
}
|
|
103
|
+
if (isConnectionUnavailable(current)) return fail.serviceUnavailable();
|
|
104
|
+
const mapped = mapConstraint(current);
|
|
105
|
+
if (mapped) return mapped;
|
|
106
|
+
if (unknownNode === undefined && looksLikeSqlError(current)) unknownNode = current;
|
|
107
|
+
current = causeOf(current);
|
|
108
|
+
}
|
|
109
|
+
if (unknownNode !== undefined) {
|
|
110
|
+
return fail.database({ reason: "unknown", ...publicNames(unknownNode) });
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function mapConstraint(err: unknown): SqlFailure | undefined {
|
|
116
|
+
const code = sqlCode(err);
|
|
117
|
+
const names = publicNames(err);
|
|
118
|
+
|
|
119
|
+
if (CONFLICT_STATE.has(code)) return fail.conflict(names);
|
|
120
|
+
if (FOREIGN_KEY_STATE.has(code)) return fail.foreignKey(names);
|
|
121
|
+
if (NOT_NULL_STATE.has(code)) return fail.database({ reason: "not_null", ...names });
|
|
122
|
+
if (CHECK_STATE.has(code)) return fail.database({ reason: "check", ...names });
|
|
123
|
+
if (TOO_LONG_STATE.has(code)) return fail.database({ reason: "too_long", ...names });
|
|
124
|
+
if (OUT_OF_RANGE_STATE.has(code)) return fail.database({ reason: "out_of_range", ...names });
|
|
125
|
+
if (INVALID_STATE.has(code)) return fail.database({ reason: "invalid", ...names });
|
|
126
|
+
|
|
127
|
+
const sqlite = sqliteConstraint(err);
|
|
128
|
+
if (sqlite === "unique") return fail.conflict(names);
|
|
129
|
+
if (sqlite === "fk") return fail.foreignKey(names);
|
|
130
|
+
if (sqlite === "not_null") return fail.database({ reason: "not_null", ...names });
|
|
131
|
+
if (sqlite === "check") return fail.database({ reason: "check", ...names });
|
|
132
|
+
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function retrySignal(err: unknown): boolean {
|
|
137
|
+
const code = sqlCode(err);
|
|
138
|
+
if (RETRY_STATE.has(code)) return true;
|
|
139
|
+
const errno = sqlErrno(err);
|
|
140
|
+
if (errno !== undefined && SQLITE_RETRY_ERRNO.has(errno)) return true;
|
|
141
|
+
const named = sqlCode(err);
|
|
142
|
+
if (named === "SQLITE_BUSY" || named === "SQLITE_LOCKED" || named === "SQLITE_BUSY_SNAPSHOT") {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function isConnectionUnavailable(err: unknown): boolean {
|
|
149
|
+
const e = err as SqlErrShape;
|
|
150
|
+
const code = sqlCode(err);
|
|
151
|
+
if (code.startsWith("08")) return true;
|
|
152
|
+
if (code.startsWith("53")) return true;
|
|
153
|
+
if (UNAVAILABLE_STATE.has(code)) return true;
|
|
154
|
+
if (code.startsWith("ERR_POSTGRES_")) return true;
|
|
155
|
+
if (code === "ERR_OKE_POSTGRES_PAUSED") return true;
|
|
156
|
+
if (typeof e.name === "string" && e.name === "SharedPostgresPausedError") return true;
|
|
157
|
+
const errno = sqlErrno(err);
|
|
158
|
+
if (errno !== undefined && SQLITE_UNAVAILABLE_ERRNO.has(errno)) return true;
|
|
159
|
+
if (code === "SQLITE_READONLY" || code === "SQLITE_FULL" || code === "SQLITE_CANTOPEN") {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
const message = typeof e.message === "string" ? e.message : "";
|
|
163
|
+
if (/postgres pools are paused/i.test(message)) return true;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function looksLikeSqlError(err: unknown): boolean {
|
|
168
|
+
const e = err as SqlErrShape;
|
|
169
|
+
const code = sqlCode(err);
|
|
170
|
+
if (/^[0-9A-Z]{5}$/.test(code)) return true;
|
|
171
|
+
if (code.startsWith("SQLITE_")) return true;
|
|
172
|
+
if (typeof e.name === "string" && /postgres|sqlite|pglite|libsql/i.test(e.name)) return true;
|
|
173
|
+
const message = typeof e.message === "string" ? e.message : "";
|
|
174
|
+
return /sqlstate|constraint|syntax error/i.test(message);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sqlCode(err: unknown): string {
|
|
178
|
+
if (!err || typeof err !== "object") return "";
|
|
179
|
+
const code = (err as SqlErrShape).code;
|
|
180
|
+
return typeof code === "string" ? code : "";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sqlErrno(err: unknown): number | undefined {
|
|
184
|
+
if (!err || typeof err !== "object") return undefined;
|
|
185
|
+
const errno = (err as SqlErrShape).errno;
|
|
186
|
+
return typeof errno === "number" ? errno : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function publicNames(err: unknown): {
|
|
190
|
+
readonly sqlstate?: string;
|
|
191
|
+
readonly constraint?: string;
|
|
192
|
+
readonly table?: string;
|
|
193
|
+
readonly column?: string;
|
|
194
|
+
} {
|
|
195
|
+
const e = err as SqlErrShape;
|
|
196
|
+
const sqlstate = sqlCode(err);
|
|
197
|
+
const constraint = firstString(e.constraint, e.constraint_name);
|
|
198
|
+
const table = firstString(e.table, e.table_name);
|
|
199
|
+
const column = firstString(e.column, e.column_name);
|
|
200
|
+
const fromMessage = parseSqliteNames(typeof e.message === "string" ? e.message : "");
|
|
201
|
+
const out: {
|
|
202
|
+
sqlstate?: string;
|
|
203
|
+
constraint?: string;
|
|
204
|
+
table?: string;
|
|
205
|
+
column?: string;
|
|
206
|
+
} = {};
|
|
207
|
+
if (/^[0-9A-Z]{5}$/.test(sqlstate)) out.sqlstate = sqlstate;
|
|
208
|
+
const constraintName = constraint ?? fromMessage.constraint;
|
|
209
|
+
if (constraintName) out.constraint = constraintName;
|
|
210
|
+
const tableName = table ?? fromMessage.table;
|
|
211
|
+
const columnName = column ?? fromMessage.column;
|
|
212
|
+
if (tableName) out.table = tableName;
|
|
213
|
+
if (columnName) out.column = columnName;
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function firstString(...values: readonly unknown[]): string | undefined {
|
|
218
|
+
for (const v of values) {
|
|
219
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseSqliteNames(message: string): {
|
|
225
|
+
table?: string;
|
|
226
|
+
column?: string;
|
|
227
|
+
constraint?: string;
|
|
228
|
+
} {
|
|
229
|
+
const dotted =
|
|
230
|
+
/(?:UNIQUE|NOT NULL|PRIMARY KEY) constraint failed:\s*([A-Za-z_][\w]*)\.([A-Za-z_][\w]*)/i.exec(
|
|
231
|
+
message,
|
|
232
|
+
);
|
|
233
|
+
if (dotted?.[1] && dotted[2]) {
|
|
234
|
+
return { table: dotted[1], column: dotted[2] };
|
|
235
|
+
}
|
|
236
|
+
const check = /CHECK constraint failed:\s*([A-Za-z_][\w]*)/i.exec(message);
|
|
237
|
+
if (check?.[1]) return { constraint: check[1] };
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
type SqliteKind = "unique" | "fk" | "not_null" | "check";
|
|
242
|
+
|
|
243
|
+
function sqliteConstraint(err: unknown): SqliteKind | undefined {
|
|
244
|
+
const e = err as SqlErrShape;
|
|
245
|
+
const message = typeof e.message === "string" ? e.message : "";
|
|
246
|
+
const errno = sqlErrno(err);
|
|
247
|
+
const code = sqlCode(err);
|
|
248
|
+
if (
|
|
249
|
+
(errno !== undefined && SQLITE_CONFLICT_ERRNO.has(errno)) ||
|
|
250
|
+
code === "SQLITE_CONSTRAINT_UNIQUE" ||
|
|
251
|
+
code === "SQLITE_CONSTRAINT_PRIMARYKEY" ||
|
|
252
|
+
/UNIQUE constraint failed/i.test(message) ||
|
|
253
|
+
/PRIMARY KEY constraint failed/i.test(message)
|
|
254
|
+
) {
|
|
255
|
+
return "unique";
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
errno === 787 ||
|
|
259
|
+
code === "SQLITE_CONSTRAINT_FOREIGNKEY" ||
|
|
260
|
+
/FOREIGN KEY constraint failed/i.test(message)
|
|
261
|
+
) {
|
|
262
|
+
return "fk";
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
errno === 1299 ||
|
|
266
|
+
code === "SQLITE_CONSTRAINT_NOTNULL" ||
|
|
267
|
+
/NOT NULL constraint failed/i.test(message)
|
|
268
|
+
) {
|
|
269
|
+
return "not_null";
|
|
270
|
+
}
|
|
271
|
+
if (
|
|
272
|
+
errno === 275 ||
|
|
273
|
+
code === "SQLITE_CONSTRAINT_CHECK" ||
|
|
274
|
+
/CHECK constraint failed/i.test(message)
|
|
275
|
+
) {
|
|
276
|
+
return "check";
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function causeOf(err: unknown): unknown {
|
|
282
|
+
if (!err || typeof err !== "object") return undefined;
|
|
283
|
+
return (err as SqlErrShape).cause;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function walk(err: unknown, pred: (node: unknown) => boolean): boolean {
|
|
287
|
+
let current: unknown = err;
|
|
288
|
+
for (let i = 0; i < 4; i++) {
|
|
289
|
+
if (pred(current)) return true;
|
|
290
|
+
current = causeOf(current);
|
|
291
|
+
if (current === undefined) break;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
@@ -15,6 +15,7 @@ import { and, desc, eq, like, lt, or } from "drizzle-orm";
|
|
|
15
15
|
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
16
16
|
import { pgliteDriver } from "../../drivers/pglite.ts";
|
|
17
17
|
import type { SqlConnection } from "../../drivers/types.ts";
|
|
18
|
+
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
18
19
|
import { createSqlStoreHandle, type SqlStoreHandle } from "./sql-session.ts";
|
|
19
20
|
|
|
20
21
|
const posts = pgTable("posts", {
|
|
@@ -168,3 +169,54 @@ describe("SqlStoreHandle — upsert", () => {
|
|
|
168
169
|
expect(row).toEqual({ id: "n1", title: "two", createdAt: 20 });
|
|
169
170
|
});
|
|
170
171
|
});
|
|
172
|
+
|
|
173
|
+
describe("SqlStoreHandle — SQL constraints become typed failures", () => {
|
|
174
|
+
test("unique insert is Conflict; missing FK is ForeignKey", async () => {
|
|
175
|
+
await sharedConn.exec(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS "acct" (
|
|
177
|
+
"id" TEXT PRIMARY KEY,
|
|
178
|
+
"email" TEXT NOT NULL UNIQUE
|
|
179
|
+
)
|
|
180
|
+
`);
|
|
181
|
+
await sharedConn.exec(`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS "note" (
|
|
183
|
+
"id" TEXT PRIMARY KEY,
|
|
184
|
+
"acct_id" TEXT NOT NULL REFERENCES "acct"("id")
|
|
185
|
+
)
|
|
186
|
+
`);
|
|
187
|
+
await sharedConn.exec(`TRUNCATE "note", "acct" RESTART IDENTITY CASCADE`);
|
|
188
|
+
|
|
189
|
+
const acct = pgTable("acct", {
|
|
190
|
+
id: text("id").primaryKey(),
|
|
191
|
+
email: text("email").notNull(),
|
|
192
|
+
});
|
|
193
|
+
const note = pgTable("note", {
|
|
194
|
+
id: text("id").primaryKey(),
|
|
195
|
+
acctId: text("acct_id").notNull(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await sharedHandle.insert(acct).values({ id: "a1", email: "a@b.c" });
|
|
199
|
+
let unique: unknown;
|
|
200
|
+
try {
|
|
201
|
+
await sharedHandle.insert(acct).values({ id: "a2", email: "a@b.c" });
|
|
202
|
+
} catch (err) {
|
|
203
|
+
unique = err;
|
|
204
|
+
}
|
|
205
|
+
expect(isFlowFailure(unique)).toBe(true);
|
|
206
|
+
if (isFlowFailure(unique)) {
|
|
207
|
+
expect(unique.error.code).toBe("Conflict");
|
|
208
|
+
expect(JSON.stringify(unique)).not.toContain("a@b.c");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let fk: unknown;
|
|
212
|
+
try {
|
|
213
|
+
await sharedHandle.insert(note).values({ id: "n1", acctId: "missing" });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
fk = err;
|
|
216
|
+
}
|
|
217
|
+
expect(isFlowFailure(fk)).toBe(true);
|
|
218
|
+
if (isFlowFailure(fk)) {
|
|
219
|
+
expect(fk.error.code).toBe("ForeignKey");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
type RlsIdentity,
|
|
15
15
|
} from "../../drivers/pg-rls.ts";
|
|
16
16
|
import { throwOke } from "../../kernel/errors.ts";
|
|
17
|
+
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
18
|
+
import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
|
|
17
19
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
20
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
19
21
|
import { isMissingDomainRelationError } from "./missing-relation.ts";
|
|
@@ -22,9 +24,11 @@ import {
|
|
|
22
24
|
compileWhere,
|
|
23
25
|
compileOrderBy,
|
|
24
26
|
resolveSelectColumns,
|
|
27
|
+
type CompiledWhere,
|
|
25
28
|
type WhereMap,
|
|
26
29
|
} from "./sql-condition.ts";
|
|
27
30
|
import {
|
|
31
|
+
coerceTemporalBindValue,
|
|
28
32
|
mapRowToJs,
|
|
29
33
|
prepareInsertRow,
|
|
30
34
|
prepareUpdateRow,
|
|
@@ -547,9 +551,13 @@ export function createSqlStoreHandle(
|
|
|
547
551
|
try {
|
|
548
552
|
return await fn();
|
|
549
553
|
} catch (err) {
|
|
554
|
+
if (isFlowFailure(err)) throw err;
|
|
550
555
|
if (domainDdl === "off" && isMissingDomainRelationError(err)) {
|
|
551
556
|
throwOke("DOMAIN_SCHEMA_MISSING");
|
|
552
557
|
}
|
|
558
|
+
if (isRetryableSqlError(err)) throw err;
|
|
559
|
+
const failure = sqlErrorToFailure(err, { retryable: "leave" });
|
|
560
|
+
if (failure) throw failure;
|
|
553
561
|
throw err;
|
|
554
562
|
}
|
|
555
563
|
}
|
|
@@ -622,6 +630,46 @@ export function createSqlStoreHandle(
|
|
|
622
630
|
return out;
|
|
623
631
|
}
|
|
624
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Coerce epoch-ms / ISO strings on temporal WHERE binds the same way
|
|
635
|
+
* insert/update SET does, so `lte(col, fx.clock.now())` is a timestamp.
|
|
636
|
+
*
|
|
637
|
+
* @param table - Table whose columns supply SQL types
|
|
638
|
+
* @param compiled - Predicate list from {@link compileWhere}
|
|
639
|
+
*/
|
|
640
|
+
function coerceCompiledWhere(
|
|
641
|
+
table: TableHandle | unknown,
|
|
642
|
+
compiled: CompiledWhere,
|
|
643
|
+
): CompiledWhere {
|
|
644
|
+
const cols = resolveColumns(table);
|
|
645
|
+
if (cols.length === 0 || compiled.predicates.length === 0) return compiled;
|
|
646
|
+
const bySql = new Map(cols.map((c) => [c.sqlName, c]));
|
|
647
|
+
|
|
648
|
+
let changed = false;
|
|
649
|
+
const predicates = compiled.predicates.map((p) => {
|
|
650
|
+
const col = bySql.get(p.column);
|
|
651
|
+
if (!col || p.op === "is null" || p.op === "is not null") return p;
|
|
652
|
+
if (p.op === "in" && Array.isArray(p.value)) {
|
|
653
|
+
const incoming = p.value;
|
|
654
|
+
const value = incoming.map((v) => coerceTemporalBindValue(col.sqlType, v));
|
|
655
|
+
if (value.some((v, i) => v !== incoming[i])) changed = true;
|
|
656
|
+
return { ...p, value };
|
|
657
|
+
}
|
|
658
|
+
const value = coerceTemporalBindValue(col.sqlType, p.value);
|
|
659
|
+
if (value !== p.value) changed = true;
|
|
660
|
+
return { ...p, value };
|
|
661
|
+
});
|
|
662
|
+
if (!changed) return compiled;
|
|
663
|
+
|
|
664
|
+
const params: unknown[] = [];
|
|
665
|
+
for (const p of predicates) {
|
|
666
|
+
if (p.op === "is null" || p.op === "is not null") continue;
|
|
667
|
+
if (p.op === "in" && Array.isArray(p.value)) params.push(...p.value);
|
|
668
|
+
else params.push(p.value);
|
|
669
|
+
}
|
|
670
|
+
return { clause: compiled.clause, params, predicates };
|
|
671
|
+
}
|
|
672
|
+
|
|
625
673
|
function compileTableWhere(
|
|
626
674
|
table: TableHandle | unknown,
|
|
627
675
|
where: unknown,
|
|
@@ -632,9 +680,9 @@ export function createSqlStoreHandle(
|
|
|
632
680
|
!Array.isArray(where) &&
|
|
633
681
|
!("queryChunks" in (where as object))
|
|
634
682
|
) {
|
|
635
|
-
return compileWhere(normalizeWhereMap(table, where as WhereMap));
|
|
683
|
+
return coerceCompiledWhere(table, compileWhere(normalizeWhereMap(table, where as WhereMap)));
|
|
636
684
|
}
|
|
637
|
-
return compileWhere(where);
|
|
685
|
+
return coerceCompiledWhere(table, compileWhere(where));
|
|
638
686
|
}
|
|
639
687
|
|
|
640
688
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store-path error map — SQL, Redis KV, then files. Off the kernel edge graph.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
6
|
+
import type { FlowFailure } from "../../kernel/errors.ts";
|
|
7
|
+
import { filesErrorToFailure, type FilesErrorToFailureOptions } from "./files-errors.ts";
|
|
8
|
+
import { kvErrorToFailure, type KvErrorToFailureOptions } from "./kv-errors.ts";
|
|
9
|
+
import { sqlErrorToFailure, type SqlErrorToFailureOptions } from "./sql-errors.ts";
|
|
10
|
+
|
|
11
|
+
/** Shared retryable handling for SQL, Redis KV, and files mappers. */
|
|
12
|
+
export type StoreErrorToFailureOptions = SqlErrorToFailureOptions &
|
|
13
|
+
KvErrorToFailureOptions &
|
|
14
|
+
FilesErrorToFailureOptions;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Map a store driver throw to a typed failure (SQL, then Redis KV, then files).
|
|
18
|
+
*
|
|
19
|
+
* @param err - Caught value from `fx.store`
|
|
20
|
+
* @param options - Retryable handling
|
|
21
|
+
*/
|
|
22
|
+
export function storeErrorToFailure(
|
|
23
|
+
err: unknown,
|
|
24
|
+
options: StoreErrorToFailureOptions = {},
|
|
25
|
+
): FlowFailure | undefined {
|
|
26
|
+
return (
|
|
27
|
+
sqlErrorToFailure(err, options) ??
|
|
28
|
+
kvErrorToFailure(err, options) ??
|
|
29
|
+
filesErrorToFailure(err, options)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run a store op; remap known SQL / Redis / files failures, leave retryable throws.
|
|
35
|
+
*
|
|
36
|
+
* @param fn - Store operation
|
|
37
|
+
*/
|
|
38
|
+
export async function withStoreErrorMap<T>(fn: () => Promise<T>): Promise<T> {
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (isFlowFailure(err)) throw err;
|
|
43
|
+
const mapped = storeErrorToFailure(err, { retryable: "leave" });
|
|
44
|
+
if (mapped) throw mapped;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -295,7 +295,8 @@ function drizzleSqlType(
|
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
297
|
* Bind value for a temporal SQL column. Epoch-ms numbers (e.g. `fx.clock.now()`)
|
|
298
|
-
*
|
|
298
|
+
* and parseable ISO datetime strings (HTTP `z.iso.datetime()`) become `Date`
|
|
299
|
+
* so Postgres `timestamp` / `date` accept them; other values pass through.
|
|
299
300
|
*
|
|
300
301
|
* @param sqlType - Resolved DDL type
|
|
301
302
|
* @param value - Incoming JS value
|
|
@@ -304,13 +305,14 @@ export function coerceTemporalBindValue(
|
|
|
304
305
|
sqlType: ResolvedColumn["sqlType"],
|
|
305
306
|
value: unknown,
|
|
306
307
|
): unknown {
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
typeof value === "number" &&
|
|
310
|
-
Number.isFinite(value)
|
|
311
|
-
) {
|
|
308
|
+
if (sqlType !== "TIMESTAMP" && sqlType !== "DATE") return value;
|
|
309
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
312
310
|
return new Date(value);
|
|
313
311
|
}
|
|
312
|
+
if (typeof value === "string") {
|
|
313
|
+
const parsed = Date.parse(value);
|
|
314
|
+
if (Number.isFinite(parsed)) return new Date(parsed);
|
|
315
|
+
}
|
|
314
316
|
return value;
|
|
315
317
|
}
|
|
316
318
|
|
package/src/http.ts
CHANGED
|
@@ -23,7 +23,15 @@ export { http, internal } from "./kernel/triggers.ts";
|
|
|
23
23
|
export type { BoundaryContract } from "./kernel/boundary-contract.ts";
|
|
24
24
|
export { clock } from "./elements/clock/declare.ts";
|
|
25
25
|
export { gate, GATE_PUBLIC_NAME } from "./elements/gate/declare.ts";
|
|
26
|
-
export { fail, type
|
|
26
|
+
export { fail, type FailFn } from "./kernel/fail-helpers.ts";
|
|
27
|
+
export { type FlowFailure, type FlowErrorValue } from "./kernel/errors.ts";
|
|
28
|
+
export type {
|
|
29
|
+
BuiltinErrorBag,
|
|
30
|
+
BuiltinErrorCode,
|
|
31
|
+
BuiltinErrorMap,
|
|
32
|
+
BuiltinValidationIssue,
|
|
33
|
+
DatabaseErrorReason,
|
|
34
|
+
} from "./kernel/builtin-errors.ts";
|
|
27
35
|
export { isFlowFailure } from "./kernel/hooks.ts";
|
|
28
36
|
export { plugin, isPlugin, type PluginDef } from "./kernel/plugin.ts";
|
|
29
37
|
export { createBunRuntime, APP_PORT, type Runtime, type ServeOptions } from "./runtime/index.ts";
|
package/src/i18n/catalogs/ar.ts
CHANGED
|
@@ -64,6 +64,10 @@ export const builtinAr = {
|
|
|
64
64
|
cause: "جدول النطاق غير موجود — لم تُطبَّق الترحيلات.",
|
|
65
65
|
fix: "شغّل `oke db migrate` على هذه البيئة.",
|
|
66
66
|
},
|
|
67
|
+
"1510": {
|
|
68
|
+
cause: "{count} أسرار بلا قيمة في أي طبقة حل.",
|
|
69
|
+
fix: "عيّن كل اسم (`oke vault set <name>` أو `.env.local`).",
|
|
70
|
+
},
|
|
67
71
|
},
|
|
68
72
|
errors: {
|
|
69
73
|
Unauthorized: "المصادقة مطلوبة.",
|
|
@@ -71,6 +75,19 @@ export const builtinAr = {
|
|
|
71
75
|
RateLimited: "طلبات كثيرة جداً. حاول لاحقاً.",
|
|
72
76
|
ValidationError: "فشل التحقق من الطلب.",
|
|
73
77
|
NotFound: "المورد المطلوب غير موجود.",
|
|
78
|
+
Conflict: "هذه القيمة مستخدمة بالفعل.",
|
|
79
|
+
ForeignKey: "السجل المرتبط غير موجود.",
|
|
80
|
+
DatabaseError: "رفضت قاعدة البيانات هذا التغيير.",
|
|
81
|
+
"DatabaseError.not_null": "حقل مطلوب مفقود.",
|
|
82
|
+
"DatabaseError.check": "قيمة لم تستوفِ قيداً في قاعدة البيانات.",
|
|
83
|
+
"DatabaseError.invalid": "قيمة غير صالحة لهذا العمود.",
|
|
84
|
+
"DatabaseError.too_long": "القيمة أطول من المسموح لهذا العمود.",
|
|
85
|
+
"DatabaseError.out_of_range": "القيمة خارج النطاق المسموح.",
|
|
86
|
+
"DatabaseError.retryable": "قاعدة البيانات مشغولة. حاول بعد قليل.",
|
|
87
|
+
ServiceUnavailable: "الخدمة غير متاحة مؤقتاً.",
|
|
88
|
+
InternalError: "حدث خطأ. حاول مرة أخرى.",
|
|
89
|
+
InvalidQuery: "الاستعلام غير صالح.",
|
|
90
|
+
UnsupportedMediaType: "نوع المحتوى هذا غير مدعوم.",
|
|
74
91
|
AuthFailed: "فشلت المصادقة.",
|
|
75
92
|
AuthRateLimited: "محاولات مصادقة كثيرة جداً. حاول لاحقاً.",
|
|
76
93
|
"AuthFailed.invalid_credentials": "بيانات الاعتماد غير صحيحة.",
|
|
@@ -83,6 +100,7 @@ export const builtinAr = {
|
|
|
83
100
|
"AuthFailed.password_policy": "كلمة المرور لا تستوفي متطلبات السياسة.",
|
|
84
101
|
"AuthFailed.invalid_origin": "تم رفض أصل مفتاح المرور.",
|
|
85
102
|
"Forbidden.csrf": "تم حظر الطلب عبر المواقع.",
|
|
103
|
+
"Forbidden.invalid_key": "مفتاح الكائن هذا غير مسموح.",
|
|
86
104
|
"Forbidden.ip_denied": "عنوان IP الخاص بك محظور.",
|
|
87
105
|
"Forbidden.ip_not_allowed": "عنوان IP الخاص بك غير مسموح.",
|
|
88
106
|
"Forbidden.policy_denied": "رفضت السياسة هذا الطلب.",
|
package/src/i18n/catalogs/en.ts
CHANGED
|
@@ -64,6 +64,10 @@ export const builtinEn = {
|
|
|
64
64
|
cause: "domain table not found — migrations have not been applied.",
|
|
65
65
|
fix: "run `oke db migrate` against this environment.",
|
|
66
66
|
},
|
|
67
|
+
"1510": {
|
|
68
|
+
cause: "{count} secrets have no value in any resolution layer.",
|
|
69
|
+
fix: "Set each name (`oke vault set <name>`, or `.env.local`).",
|
|
70
|
+
},
|
|
67
71
|
},
|
|
68
72
|
errors: {
|
|
69
73
|
Unauthorized: "Authentication required.",
|
|
@@ -71,6 +75,19 @@ export const builtinEn = {
|
|
|
71
75
|
RateLimited: "Too many requests. Try again later.",
|
|
72
76
|
ValidationError: "The request failed validation.",
|
|
73
77
|
NotFound: "The requested resource was not found.",
|
|
78
|
+
Conflict: "That value is already in use.",
|
|
79
|
+
ForeignKey: "That related record does not exist.",
|
|
80
|
+
DatabaseError: "The database rejected this change.",
|
|
81
|
+
"DatabaseError.not_null": "A required field is missing.",
|
|
82
|
+
"DatabaseError.check": "A value did not meet a database check.",
|
|
83
|
+
"DatabaseError.invalid": "A value is not valid for that column.",
|
|
84
|
+
"DatabaseError.too_long": "A value is too long for that column.",
|
|
85
|
+
"DatabaseError.out_of_range": "A value is outside the allowed range.",
|
|
86
|
+
"DatabaseError.retryable": "The database is busy. Try again shortly.",
|
|
87
|
+
ServiceUnavailable: "The service is temporarily unavailable.",
|
|
88
|
+
InternalError: "Something went wrong. Try again.",
|
|
89
|
+
InvalidQuery: "The query is not valid.",
|
|
90
|
+
UnsupportedMediaType: "That content type is not supported.",
|
|
74
91
|
AuthFailed: "Authentication failed.",
|
|
75
92
|
AuthRateLimited: "Too many authentication attempts. Try again later.",
|
|
76
93
|
"AuthFailed.invalid_credentials": "Invalid credentials.",
|
|
@@ -84,6 +101,7 @@ export const builtinEn = {
|
|
|
84
101
|
"AuthFailed.password_policy": "That password does not meet the policy requirements.",
|
|
85
102
|
"AuthFailed.invalid_origin": "Passkey origin was rejected.",
|
|
86
103
|
"Forbidden.csrf": "Cross-site request blocked.",
|
|
104
|
+
"Forbidden.invalid_key": "That object key is not allowed.",
|
|
87
105
|
"Forbidden.ip_denied": "Your IP address is blocked.",
|
|
88
106
|
"Forbidden.ip_not_allowed": "Your IP address is not allowed.",
|
|
89
107
|
"Forbidden.policy_denied": "Policy denied this request.",
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,15 @@ export { http, internal, mcp } from "./kernel/triggers.ts";
|
|
|
32
32
|
export type { BoundaryContract } from "./kernel/boundary-contract.ts";
|
|
33
33
|
export { registerFlowUnits } from "./kernel/flow-units.ts";
|
|
34
34
|
export { stampFlowName, stampHttpPath } from "./kernel/stamp-http.ts";
|
|
35
|
-
export { fail, type
|
|
35
|
+
export { fail, type FailFn } from "./kernel/fail-helpers.ts";
|
|
36
|
+
export { type FlowFailure, type FlowErrorValue } from "./kernel/errors.ts";
|
|
37
|
+
export type {
|
|
38
|
+
BuiltinErrorBag,
|
|
39
|
+
BuiltinErrorCode,
|
|
40
|
+
BuiltinErrorMap,
|
|
41
|
+
BuiltinValidationIssue,
|
|
42
|
+
DatabaseErrorReason,
|
|
43
|
+
} from "./kernel/builtin-errors.ts";
|
|
36
44
|
export { isFlowFailure } from "./kernel/hooks.ts";
|
|
37
45
|
export { plugin, isPlugin, type PluginDef, type PluginCapabilities } from "./kernel/plugin.ts";
|
|
38
46
|
export type { Fx, FxPrincipal, StepOptions } from "./kernel/fx.ts";
|
|
@@ -82,7 +82,7 @@ describe("boot-level: stale barrel through a real oke() boot", () => {
|
|
|
82
82
|
const root = await mkdtemp(join(tmpdir(), "oke-adopt-fresh-"));
|
|
83
83
|
try {
|
|
84
84
|
// "notes" exists on disk but this app only ever adopted "main" —
|
|
85
|
-
// exactly what a stale `src/flows/
|
|
85
|
+
// exactly what a stale `src/flows/index.ts` after adding a unit
|
|
86
86
|
// (and forgetting to rerun `oke dev` / `oke build`) looks like.
|
|
87
87
|
await makeUnitDir(root, "notes");
|
|
88
88
|
const mainFlow = flow("main.health", {
|