okengine 0.20.0 → 0.21.1
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/package.json +3 -7
- package/site/content/docs/client/calling.mdx +117 -29
- package/site/content/docs/client/index.mdx +3 -3
- package/site/content/docs/elements/flow/http.mdx +18 -15
- package/site/content/docs/elements/flow/index.mdx +26 -18
- 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 +2 -3
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/sql.mdx +9 -6
- package/site/content/docs/elements/vault/index.mdx +11 -8
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/configuration.mdx +1 -1
- package/site/content/docs/reference/errors.mdx +199 -25
- package/site/content/docs/reference/fx.mdx +6 -1
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +758 -25
- package/src/cli/dev-app-runner.ts +2 -1
- package/src/cli/dev.test.ts +105 -2
- package/src/cli/dev.ts +37 -2
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +14 -27
- 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/live.ts +44 -101
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +26 -68
- package/src/client/stream.ts +25 -67
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +51 -112
- package/src/client/types.ts +17 -6
- package/src/client/wire.ts +119 -0
- package/src/client-react/live-resource.ts +6 -2
- package/src/compiler/aot.ts +3 -32
- package/src/compiler/dynamic.ts +13 -11
- package/src/compiler/interpret.ts +45 -0
- package/src/compiler/response.ts +17 -27
- 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-DFLu0wTA.js → access-page-Bgt9bq2r.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-DGscxaF5.js → agent-disclosure-B43CXZZR.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-BGmRZk7d.js → cache-glyph-92uM5MO7.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button--feUYxvG.js → call-pii-button-DtGVPtcs.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-JWvpaiGY.js → collapsible-DN7l6zmC.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-D9yCJG4n.js → duration-tone-BmIR9FV8.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-Bs6MD9GB.js → flows-page-BMHs-IzK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-xH8MrEnv.js → highlighted-json-BlAEVgNW.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-C4vB6ZIw.js → http-method-BDf7OAHv.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-yTCY4AcS.js → index-BKpaes3n.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BxJ3R6dU.js → observability-page-WnVLI-0j.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-QRKB_IE8.js → replica-lag-B3GLNVfF.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DqZ-fMu5.js → request-meta-DMbnAe3f.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-Dixb6L7a.js → store-page-KvFDingJ.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-CazhjtiU.js → trace-detail-sheet-Htk8Cm9t.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-DlnqYKfr.js → tree-expand-toggle-CFMPWX4f.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-BXTLjU2-.js → units-page-C4NdNuxP.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-39KR__bc.js → vault-page-CBYl0LW_.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/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/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- 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 +26 -4
- package/src/elements/store/store-errors.ts +47 -0
- 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/app.ts +23 -4
- 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/client-descriptor.test.ts +78 -0
- package/src/kernel/client-descriptor.ts +23 -0
- package/src/kernel/errors-text.ts +99 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +184 -159
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/fx-sql-handle.ts +305 -0
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +49 -335
- package/src/kernel/index.ts +12 -1
- package/src/kernel/json-result.ts +59 -0
- package/src/kernel/project-out.ts +6 -1
- package/src/okid-extended.ts +175 -0
- package/src/okid-shared.ts +103 -0
- package/src/okid.ts +30 -213
- package/src/release/build-lib.ts +9 -0
- 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,9 @@ 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 { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
19
|
+
import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
|
|
17
20
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
21
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
19
22
|
import { isMissingDomainRelationError } from "./missing-relation.ts";
|
|
@@ -34,9 +37,8 @@ import {
|
|
|
34
37
|
resolveTableName,
|
|
35
38
|
type TableHandle,
|
|
36
39
|
} from "./table.ts";
|
|
37
|
-
import { isSchemaTableDecl } from "./schema-decl.ts";
|
|
38
40
|
import { SearchConfigError } from "./search-errors.ts";
|
|
39
|
-
import {
|
|
41
|
+
import type { SearchColumnMeta, SqlSearchOptions } from "./search-runtime.ts";
|
|
40
42
|
|
|
41
43
|
export type { WhereMap } from "./sql-condition.ts";
|
|
42
44
|
|
|
@@ -549,9 +551,13 @@ export function createSqlStoreHandle(
|
|
|
549
551
|
try {
|
|
550
552
|
return await fn();
|
|
551
553
|
} catch (err) {
|
|
554
|
+
if (isFlowFailure(err)) throw err;
|
|
552
555
|
if (domainDdl === "off" && isMissingDomainRelationError(err)) {
|
|
553
556
|
throwOke("DOMAIN_SCHEMA_MISSING");
|
|
554
557
|
}
|
|
558
|
+
if (isRetryableSqlError(err)) throw err;
|
|
559
|
+
const failure = sqlErrorToFailure(err, { retryable: "leave" });
|
|
560
|
+
if (failure) throw failure;
|
|
555
561
|
throw err;
|
|
556
562
|
}
|
|
557
563
|
}
|
|
@@ -1206,7 +1212,7 @@ export function createSqlStoreHandle(
|
|
|
1206
1212
|
await ensureFromMeta(table);
|
|
1207
1213
|
const columns = searchColumnsFromTable(table);
|
|
1208
1214
|
const pk = resolvePkColumn(table);
|
|
1209
|
-
return runSqlSearch({
|
|
1215
|
+
return loadSearchRuntime().runSqlSearch({
|
|
1210
1216
|
conn: connection,
|
|
1211
1217
|
table,
|
|
1212
1218
|
columns,
|
|
@@ -1251,13 +1257,29 @@ export function resolvePkColumn(table: unknown): string {
|
|
|
1251
1257
|
return "id";
|
|
1252
1258
|
}
|
|
1253
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Hybrid search (BM25 / LSH / fusion). First `search()` loads it.
|
|
1262
|
+
* A static import pins that graph on every HTTP cold start.
|
|
1263
|
+
*/
|
|
1264
|
+
function loadSearchRuntime(): typeof import("./search-runtime.ts") {
|
|
1265
|
+
return lazyRequire(import.meta.dir, ["search", "runtime"].join("-"));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Schema-table predicate. Only `search()` needs it.
|
|
1270
|
+
* `schema-decl` is the declare-site module and stays off the HTTP import graph.
|
|
1271
|
+
*/
|
|
1272
|
+
function loadSchemaDecl(): typeof import("./schema-decl.ts") {
|
|
1273
|
+
return lazyRequire(import.meta.dir, ["schema", "decl"].join("-"));
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1254
1276
|
/**
|
|
1255
1277
|
* Collect `.searchable()` / `.embed()` column meta from a schema table.
|
|
1256
1278
|
*
|
|
1257
1279
|
* @param table - Table handle
|
|
1258
1280
|
*/
|
|
1259
1281
|
function searchColumnsFromTable(table: unknown): SearchColumnMeta[] {
|
|
1260
|
-
if (!isSchemaTableDecl(table)) {
|
|
1282
|
+
if (!loadSchemaDecl().isSchemaTableDecl(table)) {
|
|
1261
1283
|
throw new Error(
|
|
1262
1284
|
"search(): table must be a store.schema.table() declaration with .searchable() columns",
|
|
1263
1285
|
);
|
|
@@ -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
|
+
}
|
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";
|