gencow 0.1.250 → 0.1.251
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/core/index.js +721 -260
- package/core/package.json +1 -1
- package/lib/deploy-auditor.mjs +1 -0
- package/lib/jobs-command.mjs +15 -5
- package/package.json +4 -4
- package/runtime/server.mjs +7441 -7304
- package/runtime/tooling.mjs +333 -134
- package/templateFeature/agent/agent.ts +39 -13
package/core/index.js
CHANGED
|
@@ -2085,6 +2085,7 @@ function getJobAdmissionStatus(code) {
|
|
|
2085
2085
|
return 503;
|
|
2086
2086
|
case "job_rate_limited":
|
|
2087
2087
|
case "job_backlog_limit_exceeded":
|
|
2088
|
+
case "workflow_parked_capacity_exceeded":
|
|
2088
2089
|
case "job_queue_depth_limit_exceeded":
|
|
2089
2090
|
return 429;
|
|
2090
2091
|
}
|
|
@@ -2146,11 +2147,69 @@ function isJobAdmissionKillSwitchActive(env, scope) {
|
|
|
2146
2147
|
}
|
|
2147
2148
|
|
|
2148
2149
|
// ../core/src/workflow.ts
|
|
2149
|
-
import { sql as
|
|
2150
|
+
import { sql as sql5 } from "drizzle-orm";
|
|
2150
2151
|
|
|
2151
|
-
// ../core/src/workflow-
|
|
2152
|
+
// ../core/src/workflow-v2-start-schema.ts
|
|
2152
2153
|
import { sql } from "drizzle-orm";
|
|
2154
|
+
function rows(result) {
|
|
2155
|
+
if (Array.isArray(result)) return result;
|
|
2156
|
+
const nested = result?.rows;
|
|
2157
|
+
return Array.isArray(nested) ? nested : [];
|
|
2158
|
+
}
|
|
2159
|
+
function numberFrom(value) {
|
|
2160
|
+
const parsed = Number(value ?? 0);
|
|
2161
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
2162
|
+
}
|
|
2163
|
+
async function hasWorkflowV2StartSchema(db) {
|
|
2164
|
+
const runColumns = [
|
|
2165
|
+
"id",
|
|
2166
|
+
"workflow_name",
|
|
2167
|
+
"workflow_version",
|
|
2168
|
+
"args_json",
|
|
2169
|
+
"user_id",
|
|
2170
|
+
"max_active_duration_ms",
|
|
2171
|
+
"lifecycle_deadline_at",
|
|
2172
|
+
"retry_count",
|
|
2173
|
+
"max_retries",
|
|
2174
|
+
"max_attempts",
|
|
2175
|
+
"command_protocol",
|
|
2176
|
+
// tryInsertWorkflowV2Run writes it on every start, so the capability must prove it exists.
|
|
2177
|
+
"idempotency_key"
|
|
2178
|
+
];
|
|
2179
|
+
const outboxColumns = ["id", "run_id", "kind", "available_at", "status"];
|
|
2180
|
+
const runColumnList = sql.join(
|
|
2181
|
+
runColumns.map((column) => sql`${column}`),
|
|
2182
|
+
sql`, `
|
|
2183
|
+
);
|
|
2184
|
+
const outboxColumnList = sql.join(
|
|
2185
|
+
outboxColumns.map((column) => sql`${column}`),
|
|
2186
|
+
sql`, `
|
|
2187
|
+
);
|
|
2188
|
+
const result = await db.execute(sql`
|
|
2189
|
+
SELECT
|
|
2190
|
+
(
|
|
2191
|
+
SELECT COUNT(*)::int
|
|
2192
|
+
FROM information_schema.columns
|
|
2193
|
+
WHERE table_schema = 'public'
|
|
2194
|
+
AND table_name = '_gencow_workflow_runs_v2'
|
|
2195
|
+
AND column_name IN (${runColumnList})
|
|
2196
|
+
) AS run_column_count,
|
|
2197
|
+
(
|
|
2198
|
+
SELECT COUNT(*)::int
|
|
2199
|
+
FROM information_schema.columns
|
|
2200
|
+
WHERE table_schema = 'public'
|
|
2201
|
+
AND table_name = '_gencow_workflow_outbox_v2'
|
|
2202
|
+
AND column_name IN (${outboxColumnList})
|
|
2203
|
+
) AS outbox_column_count
|
|
2204
|
+
`);
|
|
2205
|
+
const row = rows(result)[0];
|
|
2206
|
+
return numberFrom(row?.run_column_count) === runColumns.length && numberFrom(row?.outbox_column_count) === outboxColumns.length;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// ../core/src/workflow-admission.ts
|
|
2210
|
+
import { sql as sql2 } from "drizzle-orm";
|
|
2153
2211
|
var DEFAULT_WORKFLOW_PENDING_PER_APP = 1e3;
|
|
2212
|
+
var DEFAULT_WORKFLOW_PARKED_PER_APP = 1e4;
|
|
2154
2213
|
var DEFAULT_WORKFLOW_RUNNING_PER_APP = 50;
|
|
2155
2214
|
var DEFAULT_WORKFLOW_PENDING_PER_NAME = 300;
|
|
2156
2215
|
var DEFAULT_WORKFLOW_STARTS_PER_MINUTE_PER_APP = 120;
|
|
@@ -2161,6 +2220,7 @@ var WORKFLOW_ADMISSION_ENV_KEYS = [
|
|
|
2161
2220
|
"GENCOW_JOB_KILL_SWITCH",
|
|
2162
2221
|
"GENCOW_PLATFORM_JOB_KILL_SWITCH",
|
|
2163
2222
|
"GENCOW_WORKFLOW_PENDING_PER_APP",
|
|
2223
|
+
"GENCOW_WORKFLOW_PARKED_PER_APP",
|
|
2164
2224
|
"GENCOW_WORKFLOW_RUNNING_PER_APP",
|
|
2165
2225
|
"GENCOW_WORKFLOW_PENDING_PER_NAME",
|
|
2166
2226
|
"GENCOW_WORKFLOW_STARTS_PER_MINUTE_PER_APP",
|
|
@@ -2170,14 +2230,14 @@ var WORKFLOW_ADMISSION_ENV_KEYS = [
|
|
|
2170
2230
|
"GENCOW_JOB_CREATES_PER_MINUTE_PER_APP",
|
|
2171
2231
|
"GENCOW_JOB_PAYLOAD_MAX_BYTES"
|
|
2172
2232
|
];
|
|
2173
|
-
function
|
|
2233
|
+
function rows2(result) {
|
|
2174
2234
|
if (Array.isArray(result)) return result;
|
|
2175
2235
|
if (result && typeof result === "object" && Array.isArray(result.rows)) {
|
|
2176
2236
|
return result.rows;
|
|
2177
2237
|
}
|
|
2178
2238
|
return [];
|
|
2179
2239
|
}
|
|
2180
|
-
function
|
|
2240
|
+
function numberFrom2(value) {
|
|
2181
2241
|
const parsed = Number(value ?? 0);
|
|
2182
2242
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
2183
2243
|
}
|
|
@@ -2203,6 +2263,7 @@ function isWorkflowAdmissionEnabled(env) {
|
|
|
2203
2263
|
function resolveWorkflowAdmissionPolicy(env) {
|
|
2204
2264
|
return {
|
|
2205
2265
|
pendingPerApp: readPositiveIntegerEnv(env, "GENCOW_WORKFLOW_PENDING_PER_APP") ?? readPositiveIntegerEnv(env, "GENCOW_JOB_PENDING_PER_APP") ?? DEFAULT_WORKFLOW_PENDING_PER_APP,
|
|
2266
|
+
parkedPerApp: readPositiveIntegerEnv(env, "GENCOW_WORKFLOW_PARKED_PER_APP") ?? DEFAULT_WORKFLOW_PARKED_PER_APP,
|
|
2206
2267
|
runningPerApp: readPositiveIntegerEnv(env, "GENCOW_WORKFLOW_RUNNING_PER_APP") ?? DEFAULT_WORKFLOW_RUNNING_PER_APP,
|
|
2207
2268
|
pendingPerName: readPositiveIntegerEnv(env, "GENCOW_WORKFLOW_PENDING_PER_NAME") ?? DEFAULT_WORKFLOW_PENDING_PER_NAME,
|
|
2208
2269
|
startsPerMinutePerApp: readPositiveIntegerEnv(env, "GENCOW_WORKFLOW_STARTS_PER_MINUTE_PER_APP") ?? readPositiveIntegerEnv(env, "GENCOW_JOB_CREATES_PER_MINUTE_PER_APP") ?? DEFAULT_WORKFLOW_STARTS_PER_MINUTE_PER_APP,
|
|
@@ -2211,33 +2272,37 @@ function resolveWorkflowAdmissionPolicy(env) {
|
|
|
2211
2272
|
};
|
|
2212
2273
|
}
|
|
2213
2274
|
async function countWorkflowV2Backlog(db, workflowName) {
|
|
2214
|
-
const result = await db.execute(
|
|
2275
|
+
const result = await db.execute(sql2`
|
|
2215
2276
|
SELECT
|
|
2216
|
-
COALESCE(SUM(CASE WHEN
|
|
2217
|
-
COALESCE(SUM(CASE WHEN
|
|
2218
|
-
COALESCE(SUM(CASE WHEN
|
|
2277
|
+
COALESCE(SUM(CASE WHEN status IN ('queued', 'running') THEN 1 ELSE 0 END), 0)::int AS pending,
|
|
2278
|
+
COALESCE(SUM(CASE WHEN status IN ('waiting', 'sleeping') THEN 1 ELSE 0 END), 0)::int AS parked,
|
|
2279
|
+
COALESCE(SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END), 0)::int AS running,
|
|
2280
|
+
COALESCE(SUM(CASE WHEN status IN ('queued', 'running') AND workflow_name = ${workflowName} THEN 1 ELSE 0 END), 0)::int AS pending_by_name
|
|
2219
2281
|
FROM _gencow_workflow_runs_v2
|
|
2282
|
+
WHERE completed_at IS NULL
|
|
2220
2283
|
`);
|
|
2221
|
-
const row =
|
|
2284
|
+
const row = rows2(result)[0];
|
|
2222
2285
|
return {
|
|
2223
|
-
pending:
|
|
2224
|
-
|
|
2225
|
-
|
|
2286
|
+
pending: numberFrom2(row?.pending),
|
|
2287
|
+
parked: numberFrom2(row?.parked),
|
|
2288
|
+
running: numberFrom2(row?.running),
|
|
2289
|
+
pendingByName: numberFrom2(row?.pending_by_name)
|
|
2226
2290
|
};
|
|
2227
2291
|
}
|
|
2228
2292
|
async function countWorkflowV1Backlog(db, workflowName) {
|
|
2229
|
-
const result = await db.execute(
|
|
2293
|
+
const result = await db.execute(sql2`
|
|
2230
2294
|
SELECT
|
|
2231
2295
|
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0)::int AS pending,
|
|
2232
2296
|
COALESCE(SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END), 0)::int AS running,
|
|
2233
2297
|
COALESCE(SUM(CASE WHEN status = 'pending' AND name = ${workflowName} THEN 1 ELSE 0 END), 0)::int AS pending_by_name
|
|
2234
2298
|
FROM _gencow_workflows
|
|
2235
2299
|
`);
|
|
2236
|
-
const row =
|
|
2300
|
+
const row = rows2(result)[0];
|
|
2237
2301
|
return {
|
|
2238
|
-
pending:
|
|
2239
|
-
|
|
2240
|
-
|
|
2302
|
+
pending: numberFrom2(row?.pending),
|
|
2303
|
+
parked: 0,
|
|
2304
|
+
running: numberFrom2(row?.running),
|
|
2305
|
+
pendingByName: numberFrom2(row?.pending_by_name)
|
|
2241
2306
|
};
|
|
2242
2307
|
}
|
|
2243
2308
|
async function countWorkflowBacklog(db, workflowName, requireV2 = false) {
|
|
@@ -2250,7 +2315,7 @@ async function countWorkflowBacklog(db, workflowName, requireV2 = false) {
|
|
|
2250
2315
|
}
|
|
2251
2316
|
async function consumeWorkflowRateBucket(db, scope, limit, now) {
|
|
2252
2317
|
try {
|
|
2253
|
-
const result = await db.execute(
|
|
2318
|
+
const result = await db.execute(sql2`
|
|
2254
2319
|
INSERT INTO _gencow_job_admission_buckets (scope, bucket_start, count, updated_at)
|
|
2255
2320
|
VALUES (${scope}, date_trunc('minute', ${now.toISOString()}::timestamptz), 1, NOW())
|
|
2256
2321
|
ON CONFLICT (scope, bucket_start) DO UPDATE
|
|
@@ -2259,7 +2324,7 @@ async function consumeWorkflowRateBucket(db, scope, limit, now) {
|
|
|
2259
2324
|
WHERE _gencow_job_admission_buckets.count < ${limit}
|
|
2260
2325
|
RETURNING count
|
|
2261
2326
|
`);
|
|
2262
|
-
return
|
|
2327
|
+
return rows2(result).length > 0;
|
|
2263
2328
|
} catch (error) {
|
|
2264
2329
|
if (!isMissingAdmissionSchemaError(error)) throw error;
|
|
2265
2330
|
throw new JobAdmissionError(
|
|
@@ -2269,7 +2334,7 @@ async function consumeWorkflowRateBucket(db, scope, limit, now) {
|
|
|
2269
2334
|
}
|
|
2270
2335
|
}
|
|
2271
2336
|
async function acquireWorkflowStartAdmissionLock(db) {
|
|
2272
|
-
await db.execute(
|
|
2337
|
+
await db.execute(sql2`SELECT pg_advisory_xact_lock(hashtextextended('workflow:create', 0))`);
|
|
2273
2338
|
}
|
|
2274
2339
|
async function applyWorkflowStartAdmission(options) {
|
|
2275
2340
|
const env = options.env ?? (typeof process === "undefined" ? {} : process.env);
|
|
@@ -2291,6 +2356,23 @@ async function applyWorkflowStartAdmission(options) {
|
|
|
2291
2356
|
details: { limit: policy.pendingPerApp, pending: counts.pending }
|
|
2292
2357
|
});
|
|
2293
2358
|
}
|
|
2359
|
+
if (counts.parked >= policy.parkedPerApp) {
|
|
2360
|
+
throw new JobAdmissionError(
|
|
2361
|
+
"workflow_parked_capacity_exceeded",
|
|
2362
|
+
"workflow sleeping/waiting limit exceeded",
|
|
2363
|
+
{
|
|
2364
|
+
details: {
|
|
2365
|
+
limit: policy.parkedPerApp,
|
|
2366
|
+
parked: counts.parked,
|
|
2367
|
+
stage: "admission",
|
|
2368
|
+
stateChanged: false,
|
|
2369
|
+
retryable: true,
|
|
2370
|
+
retryCondition: "Retry after sleeping or waiting runs complete, are signalled, or are cancelled.",
|
|
2371
|
+
nextAction: "Cancel or signal parked runs, or raise GENCOW_WORKFLOW_PARKED_PER_APP; no workflow was started."
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2294
2376
|
if (counts.running >= policy.runningPerApp) {
|
|
2295
2377
|
throw new JobAdmissionError("job_backlog_limit_exceeded", "workflow running backlog limit exceeded", {
|
|
2296
2378
|
details: { limit: policy.runningPerApp, running: counts.running }
|
|
@@ -2343,10 +2425,83 @@ async function runWorkflowAdmissionCriticalSection(db, callback) {
|
|
|
2343
2425
|
}
|
|
2344
2426
|
|
|
2345
2427
|
// ../core/src/workflow-json.ts
|
|
2346
|
-
import { sql as
|
|
2428
|
+
import { sql as sql3 } from "drizzle-orm";
|
|
2347
2429
|
function workflowJsonb(value) {
|
|
2348
2430
|
const json = JSON.stringify(value ?? {});
|
|
2349
|
-
return
|
|
2431
|
+
return sql3`${json}::text::jsonb`;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
// ../core/src/workflow-idempotent-start.ts
|
|
2435
|
+
import { sql as sql4 } from "drizzle-orm";
|
|
2436
|
+
var MAX_IDEMPOTENCY_KEY_LENGTH = 200;
|
|
2437
|
+
function normalizeWorkflowIdempotencyKey(value) {
|
|
2438
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
2439
|
+
if (typeof value !== "string") {
|
|
2440
|
+
throw new GencowValidationError(`Workflow idempotencyKey: expected a string or null`);
|
|
2441
|
+
}
|
|
2442
|
+
if (value.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
2443
|
+
throw new GencowValidationError(
|
|
2444
|
+
`Workflow idempotencyKey: at most ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
return value;
|
|
2448
|
+
}
|
|
2449
|
+
function rows3(result) {
|
|
2450
|
+
if (Array.isArray(result)) return result;
|
|
2451
|
+
const nested = result?.rows;
|
|
2452
|
+
return Array.isArray(nested) ? nested : [];
|
|
2453
|
+
}
|
|
2454
|
+
async function hashWorkflowIdempotencyKey(db, workflowName, userId, key) {
|
|
2455
|
+
const hashed = rows3(
|
|
2456
|
+
await db.execute(sql4`
|
|
2457
|
+
SELECT encode(sha256(convert_to(
|
|
2458
|
+
${workflowName} || chr(31) || ${userId ?? "anon"} || chr(31) || ${key},
|
|
2459
|
+
'UTF8')), 'hex') AS key_hash
|
|
2460
|
+
`)
|
|
2461
|
+
);
|
|
2462
|
+
return String(hashed[0]?.key_hash ?? "");
|
|
2463
|
+
}
|
|
2464
|
+
function deduplicatedWorkflowStart(workflowName, existing) {
|
|
2465
|
+
return {
|
|
2466
|
+
id: existing.id,
|
|
2467
|
+
name: workflowName,
|
|
2468
|
+
status: existing.status === "running" ? "running" : "pending",
|
|
2469
|
+
scheduledJobId: `start:${existing.id}`,
|
|
2470
|
+
deduplicated: true
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
function isWorkflowIdempotencyConflict(error) {
|
|
2474
|
+
const source = error?.cause ?? error;
|
|
2475
|
+
const { code, constraint, message } = source ?? {};
|
|
2476
|
+
return code === "23505" && `${constraint ?? ""} ${message ?? ""}`.includes("idempotency");
|
|
2477
|
+
}
|
|
2478
|
+
async function findLiveWorkflowIdempotentRun(options) {
|
|
2479
|
+
const keyHash = await hashWorkflowIdempotencyKey(options.db, options.workflowName, options.userId, options.key);
|
|
2480
|
+
const found = rows3(
|
|
2481
|
+
await options.db.execute(sql4`
|
|
2482
|
+
SELECT id, status FROM _gencow_workflow_runs_v2
|
|
2483
|
+
WHERE idempotency_key = ${keyHash} AND completed_at IS NULL AND cancel_requested_at IS NULL LIMIT 1
|
|
2484
|
+
`)
|
|
2485
|
+
)[0];
|
|
2486
|
+
return found ? { id: String(found.id), status: String(found.status) } : null;
|
|
2487
|
+
}
|
|
2488
|
+
async function lookupWorkflowIdempotentStart(options) {
|
|
2489
|
+
const keyHash = await hashWorkflowIdempotencyKey(options.db, options.workflowName, options.userId, options.key);
|
|
2490
|
+
await options.db.execute(sql4`SELECT pg_advisory_xact_lock(hashtextextended(${keyHash}, 0))`);
|
|
2491
|
+
await options.db.execute(sql4`
|
|
2492
|
+
UPDATE _gencow_workflow_runs_v2
|
|
2493
|
+
SET idempotency_key = NULL
|
|
2494
|
+
WHERE idempotency_key = ${keyHash} AND (completed_at IS NOT NULL OR cancel_requested_at IS NOT NULL)
|
|
2495
|
+
`);
|
|
2496
|
+
const found = rows3(
|
|
2497
|
+
await options.db.execute(sql4`
|
|
2498
|
+
SELECT id, status FROM _gencow_workflow_runs_v2 WHERE idempotency_key = ${keyHash} LIMIT 1
|
|
2499
|
+
`)
|
|
2500
|
+
)[0];
|
|
2501
|
+
return {
|
|
2502
|
+
keyHash,
|
|
2503
|
+
existing: found ? { id: String(found.id), status: String(found.status) } : null
|
|
2504
|
+
};
|
|
2350
2505
|
}
|
|
2351
2506
|
|
|
2352
2507
|
// ../core/src/workflow-wake-signal.ts
|
|
@@ -2505,63 +2660,15 @@ function isMissingWorkflowV2SchemaError2(error) {
|
|
|
2505
2660
|
const message = error instanceof Error ? error.message : String(error);
|
|
2506
2661
|
return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('column "max_active_duration_ms"') || message.includes('column "retry_count"') || message.includes('column "user_id"');
|
|
2507
2662
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
if (
|
|
2511
|
-
|
|
2512
|
-
}
|
|
2513
|
-
return [];
|
|
2514
|
-
}
|
|
2515
|
-
function numberFrom2(value) {
|
|
2516
|
-
const parsed = Number(value ?? 0);
|
|
2517
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
2518
|
-
}
|
|
2519
|
-
async function hasWorkflowV2StartSchema(db) {
|
|
2520
|
-
const runColumns = [
|
|
2521
|
-
"id",
|
|
2522
|
-
"workflow_name",
|
|
2523
|
-
"workflow_version",
|
|
2524
|
-
"args_json",
|
|
2525
|
-
"user_id",
|
|
2526
|
-
"max_active_duration_ms",
|
|
2527
|
-
"lifecycle_deadline_at",
|
|
2528
|
-
"retry_count",
|
|
2529
|
-
"max_retries",
|
|
2530
|
-
"max_attempts"
|
|
2531
|
-
];
|
|
2532
|
-
const outboxColumns = ["id", "run_id", "kind", "available_at", "status"];
|
|
2533
|
-
const runColumnList = sql3.join(
|
|
2534
|
-
runColumns.map((column) => sql3`${column}`),
|
|
2535
|
-
sql3`, `
|
|
2536
|
-
);
|
|
2537
|
-
const outboxColumnList = sql3.join(
|
|
2538
|
-
outboxColumns.map((column) => sql3`${column}`),
|
|
2539
|
-
sql3`, `
|
|
2540
|
-
);
|
|
2541
|
-
const result = await db.execute(sql3`
|
|
2542
|
-
SELECT
|
|
2543
|
-
(
|
|
2544
|
-
SELECT COUNT(*)::int
|
|
2545
|
-
FROM information_schema.columns
|
|
2546
|
-
WHERE table_schema = 'public'
|
|
2547
|
-
AND table_name = '_gencow_workflow_runs_v2'
|
|
2548
|
-
AND column_name IN (${runColumnList})
|
|
2549
|
-
) AS run_column_count,
|
|
2550
|
-
(
|
|
2551
|
-
SELECT COUNT(*)::int
|
|
2552
|
-
FROM information_schema.columns
|
|
2553
|
-
WHERE table_schema = 'public'
|
|
2554
|
-
AND table_name = '_gencow_workflow_outbox_v2'
|
|
2555
|
-
AND column_name IN (${outboxColumnList})
|
|
2556
|
-
) AS outbox_column_count
|
|
2557
|
-
`);
|
|
2558
|
-
const row = rows2(result)[0];
|
|
2559
|
-
return numberFrom2(row?.run_column_count) === runColumns.length && numberFrom2(row?.outbox_column_count) === outboxColumns.length;
|
|
2663
|
+
var COMMAND_PROTOCOL_1_WORKFLOW_NAMES = /* @__PURE__ */ new Set(["__gencow.documents.ingest.run"]);
|
|
2664
|
+
function defaultWorkflowCommandProtocol(workflowName, env = typeof process !== "undefined" ? process.env : {}) {
|
|
2665
|
+
if (COMMAND_PROTOCOL_1_WORKFLOW_NAMES.has(workflowName)) return 1;
|
|
2666
|
+
return env.GENCOW_WORKFLOW_COMMAND_PROTOCOL_DEFAULT === "1" ? 1 : 2;
|
|
2560
2667
|
}
|
|
2561
2668
|
async function tryInsertWorkflowV2Run(options) {
|
|
2562
2669
|
try {
|
|
2563
2670
|
if (!await hasWorkflowV2StartSchema(options.db)) return false;
|
|
2564
|
-
await options.db.execute(
|
|
2671
|
+
await options.db.execute(sql5`
|
|
2565
2672
|
WITH inserted_run AS (
|
|
2566
2673
|
INSERT INTO _gencow_workflow_runs_v2 (
|
|
2567
2674
|
id,
|
|
@@ -2573,7 +2680,9 @@ async function tryInsertWorkflowV2Run(options) {
|
|
|
2573
2680
|
lifecycle_deadline_at,
|
|
2574
2681
|
retry_count,
|
|
2575
2682
|
max_retries,
|
|
2576
|
-
max_attempts
|
|
2683
|
+
max_attempts,
|
|
2684
|
+
command_protocol,
|
|
2685
|
+
idempotency_key
|
|
2577
2686
|
)
|
|
2578
2687
|
VALUES (
|
|
2579
2688
|
${options.workflowId},
|
|
@@ -2588,7 +2697,9 @@ async function tryInsertWorkflowV2Run(options) {
|
|
|
2588
2697
|
END,
|
|
2589
2698
|
0,
|
|
2590
2699
|
${options.maxRetries},
|
|
2591
|
-
${options.maxRetries + 1}
|
|
2700
|
+
${options.maxRetries + 1},
|
|
2701
|
+
${options.commandProtocol ?? defaultWorkflowCommandProtocol(options.workflowName)},
|
|
2702
|
+
${options.idempotencyKeyHash ?? null}
|
|
2592
2703
|
)
|
|
2593
2704
|
RETURNING id
|
|
2594
2705
|
),
|
|
@@ -2626,7 +2737,7 @@ async function tryInsertWorkflowV2Run(options) {
|
|
|
2626
2737
|
}
|
|
2627
2738
|
async function tryDeleteWorkflowV2Run(db, workflowId) {
|
|
2628
2739
|
try {
|
|
2629
|
-
await db.execute(
|
|
2740
|
+
await db.execute(sql5`
|
|
2630
2741
|
DELETE FROM _gencow_workflow_runs_v2
|
|
2631
2742
|
WHERE id = ${workflowId}
|
|
2632
2743
|
`);
|
|
@@ -2683,16 +2794,37 @@ function composeWorkflowStartMiddleware(options) {
|
|
|
2683
2794
|
return await run(0, initialContext, initialInput);
|
|
2684
2795
|
};
|
|
2685
2796
|
}
|
|
2686
|
-
async function startWorkflowRun(ctx, args, def) {
|
|
2797
|
+
async function startWorkflowRun(ctx, args, def, conflictRetries = 2) {
|
|
2687
2798
|
const workflowId = crypto.randomUUID();
|
|
2688
2799
|
const resumeAction = getWorkflowResumeActionName(def.name);
|
|
2689
2800
|
const ownerId = ctx.auth.getUserIdentity()?.id ?? null;
|
|
2690
2801
|
const persistedArgs = serializeWorkflowValue(args ?? {});
|
|
2691
2802
|
assertWorkflowPayloadWithinLimit({ workflowName: def.name, kind: "args", serializedValue: persistedArgs });
|
|
2692
2803
|
const realtimeToken = createWorkflowRealtimeToken();
|
|
2804
|
+
const idempotencyKey = ownerId ? normalizeWorkflowIdempotencyKey(def.idempotencyKey?.(args, ctx)) : null;
|
|
2693
2805
|
let insertedWorkflowV2 = false;
|
|
2806
|
+
let deduplicated = null;
|
|
2694
2807
|
try {
|
|
2695
2808
|
await runWorkflowAdmissionCriticalSection(ctx.unsafeDb, async (db) => {
|
|
2809
|
+
let idempotencyKeyHash = null;
|
|
2810
|
+
if (idempotencyKey !== null) {
|
|
2811
|
+
if (!await hasWorkflowV2StartSchema(db)) {
|
|
2812
|
+
throw new GencowValidationError(
|
|
2813
|
+
`Workflow ${def.name}: idempotencyKey needs the Workflow V2 tables. Redeploy so the app schema is upgraded, then start again.`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
const lookup = await lookupWorkflowIdempotentStart({
|
|
2817
|
+
db,
|
|
2818
|
+
workflowName: def.name,
|
|
2819
|
+
userId: ownerId,
|
|
2820
|
+
key: idempotencyKey
|
|
2821
|
+
});
|
|
2822
|
+
if (lookup.existing) {
|
|
2823
|
+
deduplicated = deduplicatedWorkflowStart(def.name, lookup.existing);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
idempotencyKeyHash = lookup.keyHash;
|
|
2827
|
+
}
|
|
2696
2828
|
await applyWorkflowStartAdmission({ db, workflowName: def.name, args: persistedArgs });
|
|
2697
2829
|
insertedWorkflowV2 = await tryInsertWorkflowV2Run({
|
|
2698
2830
|
db,
|
|
@@ -2703,10 +2835,11 @@ async function startWorkflowRun(ctx, args, def) {
|
|
|
2703
2835
|
userId: ownerId,
|
|
2704
2836
|
maxActiveDurationMs: def.maxActiveDurationMs,
|
|
2705
2837
|
lifecycleTimeoutMs: def.lifecycleTimeoutMs,
|
|
2706
|
-
maxRetries: def.maxRetries
|
|
2838
|
+
maxRetries: def.maxRetries,
|
|
2839
|
+
idempotencyKeyHash
|
|
2707
2840
|
});
|
|
2708
2841
|
const legacyArgs = insertedWorkflowV2 ? serializeWorkflowValue(void 0) : persistedArgs;
|
|
2709
|
-
await db.execute(
|
|
2842
|
+
await db.execute(sql5`
|
|
2710
2843
|
INSERT INTO _gencow_workflows (
|
|
2711
2844
|
id, name, args, realtime_token, status, retry_count, max_retries, max_duration_ms, user_id
|
|
2712
2845
|
) VALUES (
|
|
@@ -2715,6 +2848,7 @@ async function startWorkflowRun(ctx, args, def) {
|
|
|
2715
2848
|
)
|
|
2716
2849
|
`);
|
|
2717
2850
|
});
|
|
2851
|
+
if (deduplicated) return deduplicated;
|
|
2718
2852
|
return {
|
|
2719
2853
|
id: workflowId,
|
|
2720
2854
|
name: def.name,
|
|
@@ -2722,8 +2856,18 @@ async function startWorkflowRun(ctx, args, def) {
|
|
|
2722
2856
|
scheduledJobId: insertedWorkflowV2 ? `start:${workflowId}` : ctx.scheduler.runAfter(0, resumeAction, { workflowId })
|
|
2723
2857
|
};
|
|
2724
2858
|
} catch (error) {
|
|
2725
|
-
await ctx.unsafeDb.execute(
|
|
2859
|
+
await ctx.unsafeDb.execute(sql5`DELETE FROM _gencow_workflows WHERE id = ${workflowId}`);
|
|
2726
2860
|
if (insertedWorkflowV2) await tryDeleteWorkflowV2Run(ctx.unsafeDb, workflowId);
|
|
2861
|
+
if (idempotencyKey !== null && isWorkflowIdempotencyConflict(error)) {
|
|
2862
|
+
const winner = await findLiveWorkflowIdempotentRun({
|
|
2863
|
+
db: ctx.unsafeDb,
|
|
2864
|
+
workflowName: def.name,
|
|
2865
|
+
userId: ownerId,
|
|
2866
|
+
key: idempotencyKey
|
|
2867
|
+
});
|
|
2868
|
+
if (winner) return deduplicatedWorkflowStart(def.name, winner);
|
|
2869
|
+
if (conflictRetries > 0) return startWorkflowRun(ctx, args, def, conflictRetries - 1);
|
|
2870
|
+
}
|
|
2727
2871
|
throw error;
|
|
2728
2872
|
}
|
|
2729
2873
|
}
|
|
@@ -2810,7 +2954,7 @@ function workflow(name, options) {
|
|
|
2810
2954
|
return metadata.legacyStart;
|
|
2811
2955
|
}
|
|
2812
2956
|
var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
2813
|
-
constructor(workflowName, middlewares = [], inputSchema, inputValidationIndex = -1, anonymousAllowed = false, workflowVersion, maxDurationValue, lifecycleTimeoutValue, concurrencyValue, retriesValue) {
|
|
2957
|
+
constructor(workflowName, middlewares = [], inputSchema, inputValidationIndex = -1, anonymousAllowed = false, workflowVersion, maxDurationValue, lifecycleTimeoutValue, concurrencyValue, retriesValue, idempotencyKeyValue) {
|
|
2814
2958
|
this.workflowName = workflowName;
|
|
2815
2959
|
this.middlewares = middlewares;
|
|
2816
2960
|
this.inputSchema = inputSchema;
|
|
@@ -2821,6 +2965,7 @@ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
|
2821
2965
|
this.lifecycleTimeoutValue = lifecycleTimeoutValue;
|
|
2822
2966
|
this.concurrencyValue = concurrencyValue;
|
|
2823
2967
|
this.retriesValue = retriesValue;
|
|
2968
|
+
this.idempotencyKeyValue = idempotencyKeyValue;
|
|
2824
2969
|
}
|
|
2825
2970
|
workflowName;
|
|
2826
2971
|
middlewares;
|
|
@@ -2832,6 +2977,7 @@ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
|
2832
2977
|
lifecycleTimeoutValue;
|
|
2833
2978
|
concurrencyValue;
|
|
2834
2979
|
retriesValue;
|
|
2980
|
+
idempotencyKeyValue;
|
|
2835
2981
|
copy(changes = {}) {
|
|
2836
2982
|
return new _GencowWorkflowBuilderImpl(
|
|
2837
2983
|
changes.workflowName ?? this.workflowName,
|
|
@@ -2843,7 +2989,8 @@ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
|
2843
2989
|
changes.maxDurationValue ?? this.maxDurationValue,
|
|
2844
2990
|
changes.lifecycleTimeoutValue ?? this.lifecycleTimeoutValue,
|
|
2845
2991
|
changes.concurrencyValue ?? this.concurrencyValue,
|
|
2846
|
-
changes.retriesValue ?? this.retriesValue
|
|
2992
|
+
changes.retriesValue ?? this.retriesValue,
|
|
2993
|
+
changes.idempotencyKeyValue ?? this.idempotencyKeyValue
|
|
2847
2994
|
);
|
|
2848
2995
|
}
|
|
2849
2996
|
name(name) {
|
|
@@ -2876,6 +3023,9 @@ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
|
2876
3023
|
retries(count) {
|
|
2877
3024
|
return this.copy({ retriesValue: count });
|
|
2878
3025
|
}
|
|
3026
|
+
idempotencyKey(derive) {
|
|
3027
|
+
return this.copy({ idempotencyKeyValue: derive });
|
|
3028
|
+
}
|
|
2879
3029
|
handler(handler) {
|
|
2880
3030
|
if (!this.workflowName)
|
|
2881
3031
|
throw new Error("Workflow name is required. Call .name(...) before .handler(...)");
|
|
@@ -2893,6 +3043,7 @@ var GencowWorkflowBuilderImpl = class _GencowWorkflowBuilderImpl {
|
|
|
2893
3043
|
),
|
|
2894
3044
|
concurrency: normalizeConcurrency(this.concurrencyValue),
|
|
2895
3045
|
maxRetries: clampRetries(this.retriesValue),
|
|
3046
|
+
idempotencyKey: this.idempotencyKeyValue,
|
|
2896
3047
|
handler
|
|
2897
3048
|
};
|
|
2898
3049
|
return createWorkflowMetadata({
|
|
@@ -2920,8 +3071,254 @@ function deriveWorkflowStatus(status, currentStep) {
|
|
|
2920
3071
|
return "queued";
|
|
2921
3072
|
}
|
|
2922
3073
|
|
|
3074
|
+
// ../core/src/workflow-errors.ts
|
|
3075
|
+
var WORKFLOW_STEP_FAILED_CODE = "workflow_step_failed";
|
|
3076
|
+
var WORKFLOW_WAIT_TIMEOUT_CODE = "workflow_wait_timeout";
|
|
3077
|
+
var WorkflowStepFailedError = class extends Error {
|
|
3078
|
+
constructor(stepName, attempt, message, branchIndex) {
|
|
3079
|
+
super(message);
|
|
3080
|
+
this.stepName = stepName;
|
|
3081
|
+
this.attempt = attempt;
|
|
3082
|
+
this.branchIndex = branchIndex;
|
|
3083
|
+
this.name = "WorkflowStepFailedError";
|
|
3084
|
+
}
|
|
3085
|
+
stepName;
|
|
3086
|
+
attempt;
|
|
3087
|
+
branchIndex;
|
|
3088
|
+
code = WORKFLOW_STEP_FAILED_CODE;
|
|
3089
|
+
};
|
|
3090
|
+
var WorkflowWaitTimeoutError = class extends Error {
|
|
3091
|
+
constructor(event) {
|
|
3092
|
+
super(`waitForEvent("${event}") timed out`);
|
|
3093
|
+
this.event = event;
|
|
3094
|
+
this.name = "WorkflowWaitTimeoutError";
|
|
3095
|
+
}
|
|
3096
|
+
event;
|
|
3097
|
+
code = WORKFLOW_WAIT_TIMEOUT_CODE;
|
|
3098
|
+
};
|
|
3099
|
+
var WorkflowNonRetryableError = class extends Error {
|
|
3100
|
+
constructor(message, options) {
|
|
3101
|
+
super(message, options);
|
|
3102
|
+
this.name = "WorkflowNonRetryableError";
|
|
3103
|
+
}
|
|
3104
|
+
};
|
|
3105
|
+
var WORKFLOW_STEP_TIMEOUT_CODE = "workflow_step_timeout";
|
|
3106
|
+
var WORKFLOW_STEP_OPTIONS_INVALID_CODE = "workflow_step_options_invalid";
|
|
3107
|
+
var WorkflowStepTimeoutError = class extends Error {
|
|
3108
|
+
constructor(stepName, timeoutMs) {
|
|
3109
|
+
super(`workflow step "${stepName}" timed out after ${timeoutMs}ms`);
|
|
3110
|
+
this.stepName = stepName;
|
|
3111
|
+
this.timeoutMs = timeoutMs;
|
|
3112
|
+
this.name = "WorkflowStepTimeoutError";
|
|
3113
|
+
}
|
|
3114
|
+
stepName;
|
|
3115
|
+
timeoutMs;
|
|
3116
|
+
code = WORKFLOW_STEP_TIMEOUT_CODE;
|
|
3117
|
+
};
|
|
3118
|
+
var WorkflowStepOptionsError = class extends Error {
|
|
3119
|
+
code = WORKFLOW_STEP_OPTIONS_INVALID_CODE;
|
|
3120
|
+
constructor(stepName, reason) {
|
|
3121
|
+
super(`wf.step(${JSON.stringify(stepName)}) options: ${reason}`);
|
|
3122
|
+
this.name = "WorkflowStepOptionsError";
|
|
3123
|
+
}
|
|
3124
|
+
};
|
|
3125
|
+
var WorkflowNestedCommandError = class extends Error {
|
|
3126
|
+
constructor(command) {
|
|
3127
|
+
super(
|
|
3128
|
+
`wf.${command}() cannot be called inside a step or parallel branch. Call it from the workflow handler itself; a step body is plain code.`
|
|
3129
|
+
);
|
|
3130
|
+
this.name = "WorkflowNestedCommandError";
|
|
3131
|
+
}
|
|
3132
|
+
};
|
|
3133
|
+
var WorkflowCommandNameError = class extends Error {
|
|
3134
|
+
constructor(command, name, reason) {
|
|
3135
|
+
super(`wf.${command}(${JSON.stringify(name)}): ${reason}`);
|
|
3136
|
+
this.name = "WorkflowCommandNameError";
|
|
3137
|
+
}
|
|
3138
|
+
};
|
|
3139
|
+
var MAX_COMMAND_NAME_LENGTH = 200;
|
|
3140
|
+
function normalizeWorkflowCommandName(command, name, options) {
|
|
3141
|
+
if (typeof name !== "string") {
|
|
3142
|
+
throw new WorkflowCommandNameError(command, String(name), "name must be a string");
|
|
3143
|
+
}
|
|
3144
|
+
const normalized = name.normalize("NFC");
|
|
3145
|
+
if (normalized.length === 0) throw new WorkflowCommandNameError(command, name, "name must not be empty");
|
|
3146
|
+
if (normalized.length > MAX_COMMAND_NAME_LENGTH) {
|
|
3147
|
+
throw new WorkflowCommandNameError(command, name, `name must be at most ${MAX_COMMAND_NAME_LENGTH} characters`);
|
|
3148
|
+
}
|
|
3149
|
+
if (/[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
3150
|
+
throw new WorkflowCommandNameError(command, name, "name must not contain control characters");
|
|
3151
|
+
}
|
|
3152
|
+
if (normalized.includes("/")) throw new WorkflowCommandNameError(command, name, 'name must not contain "/"');
|
|
3153
|
+
if (!options?.allowHash && normalized.includes("#")) {
|
|
3154
|
+
throw new WorkflowCommandNameError(command, name, 'name must not contain "#"');
|
|
3155
|
+
}
|
|
3156
|
+
return normalized;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// ../core/src/workflow-step-options.ts
|
|
3160
|
+
var MIN_BACKOFF_INITIAL_MS = 100;
|
|
3161
|
+
var MAX_BACKOFF_MS = 24 * 60 * 60 * 1e3;
|
|
3162
|
+
function validateWorkflowStepOptions(stepName, options) {
|
|
3163
|
+
if (!options) return { timeoutMs: null };
|
|
3164
|
+
const fail = (reason) => {
|
|
3165
|
+
throw new WorkflowStepOptionsError(stepName, reason);
|
|
3166
|
+
};
|
|
3167
|
+
const duration = (value, label) => {
|
|
3168
|
+
try {
|
|
3169
|
+
return parseWorkflowDurationMs(value, label);
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
return fail(error instanceof Error ? error.message : `${label} is invalid`);
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
if (options.retries != null && (!Number.isInteger(options.retries) || options.retries < 0)) {
|
|
3175
|
+
fail("retries must be a non-negative integer");
|
|
3176
|
+
}
|
|
3177
|
+
let timeoutMs = null;
|
|
3178
|
+
if (options.timeout != null) {
|
|
3179
|
+
timeoutMs = duration(options.timeout, "timeout");
|
|
3180
|
+
if (timeoutMs <= 0) fail("timeout must be greater than 0");
|
|
3181
|
+
}
|
|
3182
|
+
const backoff = options.backoff;
|
|
3183
|
+
if (backoff) {
|
|
3184
|
+
if (backoff.initial != null && duration(backoff.initial, "backoff.initial") < MIN_BACKOFF_INITIAL_MS) {
|
|
3185
|
+
fail(`backoff.initial must be at least ${MIN_BACKOFF_INITIAL_MS}ms`);
|
|
3186
|
+
}
|
|
3187
|
+
if (backoff.factor != null && !(backoff.factor >= 1 && backoff.factor <= 10)) {
|
|
3188
|
+
fail("backoff.factor must be between 1 and 10");
|
|
3189
|
+
}
|
|
3190
|
+
if (backoff.max != null && duration(backoff.max, "backoff.max") > MAX_BACKOFF_MS) {
|
|
3191
|
+
fail("backoff.max must be at most 24h");
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
return { timeoutMs };
|
|
3195
|
+
}
|
|
3196
|
+
|
|
2923
3197
|
// ../core/src/workflows-api.ts
|
|
2924
|
-
import { sql as
|
|
3198
|
+
import { sql as sql8 } from "drizzle-orm";
|
|
3199
|
+
|
|
3200
|
+
// ../core/src/workflows-signal-record.ts
|
|
3201
|
+
import { sql as sql6 } from "drizzle-orm";
|
|
3202
|
+
function rowsFromResult(result) {
|
|
3203
|
+
if (Array.isArray(result)) return result;
|
|
3204
|
+
if (result && typeof result === "object" && Array.isArray(result.rows)) {
|
|
3205
|
+
return result.rows;
|
|
3206
|
+
}
|
|
3207
|
+
return [];
|
|
3208
|
+
}
|
|
3209
|
+
function isMissingWorkflowV2SignalSchemaError(error) {
|
|
3210
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
3211
|
+
const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
|
|
3212
|
+
const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
|
|
3213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3214
|
+
return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || (code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2") || message.includes('relation "_gencow_workflow_signals_v2" does not exist') || message.includes("relation _gencow_workflow_signals_v2 does not exist") || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
|
|
3215
|
+
}
|
|
3216
|
+
var WAKES_PARKED_WAIT = sql6.raw(`(run.status = 'waiting'
|
|
3217
|
+
AND (run.current_step_key IS NULL
|
|
3218
|
+
OR starts_with(run.current_step_key, 'wait:' || inserted.event_name || '#'))
|
|
3219
|
+
AND NOT EXISTS (
|
|
3220
|
+
SELECT 1 FROM _gencow_workflow_timers_v2 expired
|
|
3221
|
+
WHERE expired.run_id = run.id AND expired.kind = 'wait_timeout'
|
|
3222
|
+
AND expired.status = 'pending' AND expired.fire_at <= NOW()
|
|
3223
|
+
AND (
|
|
3224
|
+
EXISTS (SELECT 1 FROM _gencow_workflow_steps_v2 parked
|
|
3225
|
+
WHERE parked.run_id = expired.run_id AND parked.step_key = expired.command_key
|
|
3226
|
+
AND parked.status = 'pending')
|
|
3227
|
+
OR (run.command_protocol = 1
|
|
3228
|
+
AND expired.dedupe_key = run.current_step_key || ':timeout')
|
|
3229
|
+
)))`);
|
|
3230
|
+
async function tryRecordWorkflowV2Signal(options) {
|
|
3231
|
+
const eventName = options.event.normalize("NFC");
|
|
3232
|
+
try {
|
|
3233
|
+
const result = await options.db.execute(sql6`
|
|
3234
|
+
WITH eligible_run AS (
|
|
3235
|
+
SELECT id
|
|
3236
|
+
FROM _gencow_workflow_runs_v2
|
|
3237
|
+
WHERE id = ${options.workflowId}
|
|
3238
|
+
AND status IN ('queued', 'running', 'sleeping', 'waiting')
|
|
3239
|
+
AND completed_at IS NULL
|
|
3240
|
+
AND cancel_requested_at IS NULL
|
|
3241
|
+
FOR UPDATE
|
|
3242
|
+
), inserted AS (
|
|
3243
|
+
INSERT INTO _gencow_workflow_signals_v2 (
|
|
3244
|
+
id,
|
|
3245
|
+
run_id,
|
|
3246
|
+
event_name,
|
|
3247
|
+
payload_json,
|
|
3248
|
+
idempotency_key
|
|
3249
|
+
)
|
|
3250
|
+
SELECT
|
|
3251
|
+
${options.signalId},
|
|
3252
|
+
eligible_run.id,
|
|
3253
|
+
${eventName},
|
|
3254
|
+
${workflowJsonb(options.payload)},
|
|
3255
|
+
${options.idempotencyKey ?? options.signalId}
|
|
3256
|
+
FROM eligible_run
|
|
3257
|
+
ON CONFLICT (run_id, event_name, idempotency_key) DO NOTHING
|
|
3258
|
+
RETURNING id, run_id, event_name
|
|
3259
|
+
),
|
|
3260
|
+
queued_run AS (
|
|
3261
|
+
-- SET expressions run on the newest row version after any concurrent commit. A wait that parks
|
|
3262
|
+
-- while this statement is blocked on the run lock is therefore still woken; a WHERE on status
|
|
3263
|
+
-- would be judged against the statement snapshot and miss it.
|
|
3264
|
+
UPDATE _gencow_workflow_runs_v2 run
|
|
3265
|
+
SET
|
|
3266
|
+
status = CASE WHEN ${WAKES_PARKED_WAIT} THEN 'queued' ELSE run.status END,
|
|
3267
|
+
runnable_at = CASE WHEN ${WAKES_PARKED_WAIT} THEN NOW() ELSE run.runnable_at END,
|
|
3268
|
+
lease_owner = CASE WHEN ${WAKES_PARKED_WAIT} THEN NULL ELSE run.lease_owner END,
|
|
3269
|
+
lease_expires_at = CASE WHEN ${WAKES_PARKED_WAIT} THEN NULL ELSE run.lease_expires_at END,
|
|
3270
|
+
heartbeat_at = CASE WHEN ${WAKES_PARKED_WAIT} THEN NULL ELSE run.heartbeat_at END,
|
|
3271
|
+
updated_at = CASE WHEN ${WAKES_PARKED_WAIT} THEN NOW() ELSE run.updated_at END
|
|
3272
|
+
FROM inserted
|
|
3273
|
+
WHERE run.id = inserted.run_id
|
|
3274
|
+
AND run.completed_at IS NULL
|
|
3275
|
+
AND run.cancel_requested_at IS NULL
|
|
3276
|
+
RETURNING run.id, (run.status = 'queued') AS woken
|
|
3277
|
+
),
|
|
3278
|
+
wake_outbox AS (
|
|
3279
|
+
INSERT INTO _gencow_workflow_outbox_v2 (
|
|
3280
|
+
id,
|
|
3281
|
+
run_id,
|
|
3282
|
+
kind,
|
|
3283
|
+
available_at,
|
|
3284
|
+
status
|
|
3285
|
+
)
|
|
3286
|
+
SELECT
|
|
3287
|
+
'signal:' || inserted.id,
|
|
3288
|
+
inserted.run_id,
|
|
3289
|
+
'wake_signal',
|
|
3290
|
+
NOW(),
|
|
3291
|
+
'pending'
|
|
3292
|
+
FROM inserted
|
|
3293
|
+
JOIN queued_run ON queued_run.id = inserted.run_id AND queued_run.woken
|
|
3294
|
+
ON CONFLICT (id) DO NOTHING
|
|
3295
|
+
RETURNING id
|
|
3296
|
+
),
|
|
3297
|
+
wake_notified AS (
|
|
3298
|
+
SELECT pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD})
|
|
3299
|
+
FROM wake_outbox
|
|
3300
|
+
)
|
|
3301
|
+
SELECT 1
|
|
3302
|
+
FROM inserted
|
|
3303
|
+
LEFT JOIN wake_notified ON TRUE
|
|
3304
|
+
`);
|
|
3305
|
+
if (rowsFromResult(result).length > 0) return "accepted";
|
|
3306
|
+
if (options.idempotencyKey) {
|
|
3307
|
+
const prior = await options.db.execute(sql6`
|
|
3308
|
+
SELECT 1 FROM _gencow_workflow_signals_v2
|
|
3309
|
+
WHERE run_id = ${options.workflowId}
|
|
3310
|
+
AND event_name = ${eventName}
|
|
3311
|
+
AND idempotency_key = ${options.idempotencyKey}
|
|
3312
|
+
LIMIT 1
|
|
3313
|
+
`);
|
|
3314
|
+
if (rowsFromResult(prior).length > 0) return "duplicate";
|
|
3315
|
+
}
|
|
3316
|
+
return "rejected";
|
|
3317
|
+
} catch (error) {
|
|
3318
|
+
if (isMissingWorkflowV2SignalSchemaError(error)) return "absent";
|
|
3319
|
+
throw error;
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
2925
3322
|
|
|
2926
3323
|
// ../core/src/workflow-public-read-model.ts
|
|
2927
3324
|
function parseJsonField(value) {
|
|
@@ -2942,9 +3339,55 @@ function toOptionalIsoString(value) {
|
|
|
2942
3339
|
if (typeof value === "number" && !Number.isFinite(value)) return toIsoString(value);
|
|
2943
3340
|
return value ? toIsoString(value) : null;
|
|
2944
3341
|
}
|
|
3342
|
+
function publicWorkflowErrorCode(code) {
|
|
3343
|
+
if (!code) return null;
|
|
3344
|
+
return code === "active_timeout" ? "workflow_active_timeout" : code;
|
|
3345
|
+
}
|
|
3346
|
+
function publicV1WorkflowStatus(status, errorCode) {
|
|
3347
|
+
return status === "failed" && errorCode === "cancelled" ? "cancelled" : status;
|
|
3348
|
+
}
|
|
3349
|
+
var PUBLIC_ERROR_SUMMARIES = {
|
|
3350
|
+
workflow_error: "A workflow step failed.",
|
|
3351
|
+
workflow_step_timeout: "A workflow step exceeded its timeout.",
|
|
3352
|
+
workflow_step_options_invalid: "A workflow step has invalid options.",
|
|
3353
|
+
workflow_active_timeout: "The workflow exceeded its maximum active duration.",
|
|
3354
|
+
workflow_nondeterministic: "The workflow code changed in a way that no longer matches this run's history.",
|
|
3355
|
+
definition_missing: "The workflow definition is not deployed.",
|
|
3356
|
+
cancelled: "The workflow was cancelled."
|
|
3357
|
+
};
|
|
3358
|
+
var ERROR_DETAIL_MAX_CHARS = 300;
|
|
3359
|
+
var SQLSTATE = /^[0-9A-Z]{5}$/;
|
|
3360
|
+
function looksLikeDriverFailure(body, message) {
|
|
3361
|
+
return body?.name === "DrizzleQueryError" || (message?.startsWith("Failed query:") ?? false);
|
|
3362
|
+
}
|
|
3363
|
+
function publicStepError(errorJson) {
|
|
3364
|
+
if (errorJson === null || errorJson === void 0) return null;
|
|
3365
|
+
const body = errorJson && typeof errorJson === "object" ? errorJson : null;
|
|
3366
|
+
const message = typeof errorJson === "string" ? errorJson : body && typeof body.message === "string" ? body.message : null;
|
|
3367
|
+
const driverCode = body && typeof body.code === "string" ? body.code : null;
|
|
3368
|
+
if (driverCode && SQLSTATE.test(driverCode) || looksLikeDriverFailure(body, message)) {
|
|
3369
|
+
return "The step failed.";
|
|
3370
|
+
}
|
|
3371
|
+
return message ? message.slice(0, ERROR_DETAIL_MAX_CHARS) : null;
|
|
3372
|
+
}
|
|
3373
|
+
function summarizeWorkflowError(rawCode, errorJson) {
|
|
3374
|
+
const errorCode = publicWorkflowErrorCode(rawCode);
|
|
3375
|
+
if (!errorCode && (errorJson === null || errorJson === void 0)) {
|
|
3376
|
+
return { error: null, errorCode: null, errorDetail: null };
|
|
3377
|
+
}
|
|
3378
|
+
const error = errorCode && PUBLIC_ERROR_SUMMARIES[errorCode] || "The workflow failed.";
|
|
3379
|
+
const body = errorJson && typeof errorJson === "object" ? errorJson : null;
|
|
3380
|
+
const message = typeof errorJson === "string" ? errorJson : body && typeof body.message === "string" ? body.message : null;
|
|
3381
|
+
const driverCode = body && typeof body.code === "string" ? body.code : null;
|
|
3382
|
+
const userThrown = errorCode === "workflow_error" && !(driverCode && SQLSTATE.test(driverCode)) && !looksLikeDriverFailure(body, message);
|
|
3383
|
+
return {
|
|
3384
|
+
error,
|
|
3385
|
+
errorCode,
|
|
3386
|
+
errorDetail: userThrown && message ? message.slice(0, ERROR_DETAIL_MAX_CHARS) : null
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
2945
3389
|
function publicV2Status(status) {
|
|
2946
|
-
if (status === "completed" || status === "failed") return status;
|
|
2947
|
-
if (status === "cancelled") return "failed";
|
|
3390
|
+
if (status === "completed" || status === "failed" || status === "cancelled") return status;
|
|
2948
3391
|
if (status === "running") return "running";
|
|
2949
3392
|
return "pending";
|
|
2950
3393
|
}
|
|
@@ -2953,15 +3396,19 @@ function publicV2DerivedStatus(status) {
|
|
|
2953
3396
|
return publicV2Status(status);
|
|
2954
3397
|
}
|
|
2955
3398
|
function mapV2WorkflowSummary(row) {
|
|
2956
|
-
const
|
|
3399
|
+
const summary = summarizeWorkflowError(
|
|
3400
|
+
row.status === "cancelled" ? "cancelled" : row.error_code,
|
|
3401
|
+
parseJsonField(row.error_json)
|
|
3402
|
+
);
|
|
2957
3403
|
return {
|
|
2958
3404
|
id: row.id,
|
|
2959
3405
|
name: row.workflow_name,
|
|
2960
3406
|
status: publicV2Status(row.status),
|
|
2961
3407
|
derivedStatus: publicV2DerivedStatus(row.status),
|
|
2962
3408
|
currentStep: row.current_step_key,
|
|
2963
|
-
error:
|
|
2964
|
-
errorCode:
|
|
3409
|
+
error: summary.error,
|
|
3410
|
+
errorCode: summary.errorCode,
|
|
3411
|
+
errorDetail: summary.errorDetail,
|
|
2965
3412
|
retryCount: row.retry_count,
|
|
2966
3413
|
maxRetries: row.max_retries,
|
|
2967
3414
|
maxDurationMs: Number(row.max_active_duration_ms),
|
|
@@ -2970,23 +3417,84 @@ function mapV2WorkflowSummary(row) {
|
|
|
2970
3417
|
completedAt: toOptionalIsoString(row.completed_at)
|
|
2971
3418
|
};
|
|
2972
3419
|
}
|
|
3420
|
+
var BRANCH_STEP_KEY = /^parallel#\d+\/b\d+$/;
|
|
3421
|
+
function publicV2StepKind(row) {
|
|
3422
|
+
if (row.command_kind === "sleep" || row.command_kind === "wait") return row.command_kind;
|
|
3423
|
+
if (row.command_kind !== "step") return void 0;
|
|
3424
|
+
return BRANCH_STEP_KEY.test(row.step_key) ? "branch" : "step";
|
|
3425
|
+
}
|
|
2973
3426
|
function mapV2WorkflowStep(row) {
|
|
2974
3427
|
const error = parseJsonField(row.error_json);
|
|
2975
3428
|
return {
|
|
2976
3429
|
name: row.step_key,
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3430
|
+
kind: publicV2StepKind(row),
|
|
3431
|
+
status: row.status === "running" || row.status === "failed" || row.status === "completed" || row.status === "cancelled" ? row.status : row.status === "skipped" ? "completed" : "pending",
|
|
3432
|
+
output: row.output_codec === "envelope" ? deserializeWorkflowValue(parseJsonField(row.output_json)) : parseJsonField(row.output_json),
|
|
3433
|
+
error: publicStepError(error),
|
|
2980
3434
|
startedAt: toOptionalIsoString(row.started_at),
|
|
2981
3435
|
updatedAt: toIsoString(row.updated_at),
|
|
2982
3436
|
completedAt: toOptionalIsoString(row.completed_at)
|
|
2983
3437
|
};
|
|
2984
3438
|
}
|
|
2985
3439
|
|
|
3440
|
+
// ../core/src/workflows-cancel-api.ts
|
|
3441
|
+
import { sql as sql7 } from "drizzle-orm";
|
|
3442
|
+
function rowsOf(result) {
|
|
3443
|
+
if (Array.isArray(result)) return result;
|
|
3444
|
+
const rows4 = result?.rows;
|
|
3445
|
+
return Array.isArray(rows4) ? rows4 : [];
|
|
3446
|
+
}
|
|
3447
|
+
function buildWorkflowCancelDefinition() {
|
|
3448
|
+
return {
|
|
3449
|
+
name: "workflows.cancel",
|
|
3450
|
+
argsSchema: { id: v.string() },
|
|
3451
|
+
isPublic: true,
|
|
3452
|
+
handler: async (ctx, args) => {
|
|
3453
|
+
const viewerId = ctx.auth.requireAuth().id;
|
|
3454
|
+
const notFound = { ok: false, workflowId: args.id, status: "not_found" };
|
|
3455
|
+
let run;
|
|
3456
|
+
try {
|
|
3457
|
+
run = rowsOf(
|
|
3458
|
+
await ctx.unsafeDb.execute(
|
|
3459
|
+
sql7`SELECT id, user_id, status, completed_at FROM _gencow_workflow_runs_v2 WHERE id = ${args.id} LIMIT 1`
|
|
3460
|
+
)
|
|
3461
|
+
)[0];
|
|
3462
|
+
} catch (error) {
|
|
3463
|
+
const wrapped = error;
|
|
3464
|
+
if (wrapped?.code !== "42P01" && wrapped?.cause?.code !== "42P01") throw error;
|
|
3465
|
+
}
|
|
3466
|
+
if (!run) {
|
|
3467
|
+
const legacy = await ctx.unsafeDb.execute(sql7`SELECT id, status FROM _gencow_workflows WHERE id = ${args.id} AND user_id = ${viewerId} LIMIT 1`).then((result) => rowsOf(result)[0] ?? null).catch(() => null);
|
|
3468
|
+
if (!legacy) return notFound;
|
|
3469
|
+
const finished = legacy.status === "completed" || legacy.status === "failed";
|
|
3470
|
+
return finished ? { ok: true, workflowId: args.id, status: "already_terminal" } : { ok: false, workflowId: args.id, status: "not_cancellable" };
|
|
3471
|
+
}
|
|
3472
|
+
if (!run.user_id || run.user_id !== viewerId) return notFound;
|
|
3473
|
+
if (run.completed_at !== null || ["completed", "failed", "cancelled"].includes(run.status)) {
|
|
3474
|
+
return { ok: true, workflowId: run.id, status: "already_terminal" };
|
|
3475
|
+
}
|
|
3476
|
+
const updated = rowsOf(
|
|
3477
|
+
await ctx.unsafeDb.execute(sql7`
|
|
3478
|
+
WITH marked AS (
|
|
3479
|
+
UPDATE _gencow_workflow_runs_v2
|
|
3480
|
+
SET cancel_requested_at = COALESCE(cancel_requested_at, NOW()), updated_at = NOW(),
|
|
3481
|
+
idempotency_key = NULL
|
|
3482
|
+
WHERE id = ${run.id} AND user_id = ${viewerId} AND completed_at IS NULL
|
|
3483
|
+
RETURNING id
|
|
3484
|
+
)
|
|
3485
|
+
SELECT id, pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD}) FROM marked
|
|
3486
|
+
`)
|
|
3487
|
+
);
|
|
3488
|
+
if (updated.length === 0) return { ok: true, workflowId: run.id, status: "already_terminal" };
|
|
3489
|
+
return { ok: true, workflowId: run.id, status: "cancel_requested" };
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
3493
|
+
|
|
2986
3494
|
// ../core/src/workflows-api.ts
|
|
2987
|
-
var WORKFLOW_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "failed"]);
|
|
3495
|
+
var WORKFLOW_STATUSES = /* @__PURE__ */ new Set(["pending", "running", "completed", "failed", "cancelled"]);
|
|
2988
3496
|
var WORKFLOW_DERIVED_PENDING_STATUSES = /* @__PURE__ */ new Set(["queued", "waiting", "sleeping"]);
|
|
2989
|
-
function
|
|
3497
|
+
function rowsFromResult2(result) {
|
|
2990
3498
|
if (Array.isArray(result)) return result;
|
|
2991
3499
|
if (result && typeof result === "object" && Array.isArray(result.rows)) {
|
|
2992
3500
|
return result.rows;
|
|
@@ -2997,11 +3505,11 @@ function mapWorkflowSummary(row) {
|
|
|
2997
3505
|
return {
|
|
2998
3506
|
id: row.id,
|
|
2999
3507
|
name: row.name,
|
|
3000
|
-
status: row.status,
|
|
3001
|
-
derivedStatus: deriveWorkflowStatus(row.status, row.current_step),
|
|
3508
|
+
status: publicV1WorkflowStatus(row.status, row.error_code),
|
|
3509
|
+
derivedStatus: deriveWorkflowStatus(publicV1WorkflowStatus(row.status, row.error_code), row.current_step),
|
|
3002
3510
|
currentStep: row.current_step,
|
|
3003
3511
|
error: row.error,
|
|
3004
|
-
errorCode: row.error_code,
|
|
3512
|
+
errorCode: publicWorkflowErrorCode(row.error_code),
|
|
3005
3513
|
retryCount: row.retry_count,
|
|
3006
3514
|
maxRetries: row.max_retries,
|
|
3007
3515
|
maxDurationMs: Number(row.max_duration_ms),
|
|
@@ -3065,27 +3573,27 @@ function normalizeDerivedStatus(status) {
|
|
|
3065
3573
|
async function ensureWorkflowRealtimeToken(db, workflowId, currentToken) {
|
|
3066
3574
|
if (currentToken && currentToken.trim() !== "") return currentToken;
|
|
3067
3575
|
const nextToken = createWorkflowRealtimeToken();
|
|
3068
|
-
const updateResult = await db.execute(
|
|
3576
|
+
const updateResult = await db.execute(sql8`
|
|
3069
3577
|
UPDATE _gencow_workflows
|
|
3070
3578
|
SET realtime_token = ${nextToken}
|
|
3071
3579
|
WHERE id = ${workflowId}
|
|
3072
3580
|
AND (realtime_token IS NULL OR realtime_token = '')
|
|
3073
3581
|
RETURNING realtime_token
|
|
3074
3582
|
`);
|
|
3075
|
-
const updatedToken =
|
|
3583
|
+
const updatedToken = rowsFromResult2(updateResult)[0]?.realtime_token ?? null;
|
|
3076
3584
|
if (updatedToken && updatedToken.trim() !== "") return updatedToken;
|
|
3077
|
-
const rereadResult = await db.execute(
|
|
3585
|
+
const rereadResult = await db.execute(sql8`
|
|
3078
3586
|
SELECT realtime_token
|
|
3079
3587
|
FROM _gencow_workflows
|
|
3080
3588
|
WHERE id = ${workflowId}
|
|
3081
3589
|
LIMIT 1
|
|
3082
3590
|
`);
|
|
3083
|
-
const rereadToken =
|
|
3591
|
+
const rereadToken = rowsFromResult2(rereadResult)[0]?.realtime_token ?? null;
|
|
3084
3592
|
return rereadToken && rereadToken.trim() !== "" ? rereadToken : null;
|
|
3085
3593
|
}
|
|
3086
3594
|
async function loadWorkflowSignalTarget(db, workflowId) {
|
|
3087
3595
|
try {
|
|
3088
|
-
const result = await db.execute(
|
|
3596
|
+
const result = await db.execute(sql8`
|
|
3089
3597
|
SELECT
|
|
3090
3598
|
id,
|
|
3091
3599
|
name,
|
|
@@ -3096,19 +3604,12 @@ async function loadWorkflowSignalTarget(db, workflowId) {
|
|
|
3096
3604
|
WHERE id = ${workflowId}
|
|
3097
3605
|
LIMIT 1
|
|
3098
3606
|
`);
|
|
3099
|
-
return
|
|
3607
|
+
return rowsFromResult2(result)[0] ?? null;
|
|
3100
3608
|
} catch (error) {
|
|
3101
3609
|
if (isMissingWorkflowV1Table(error)) return null;
|
|
3102
3610
|
throw error;
|
|
3103
3611
|
}
|
|
3104
3612
|
}
|
|
3105
|
-
function isMissingWorkflowV2SignalSchemaError(error) {
|
|
3106
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
3107
|
-
const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
|
|
3108
|
-
const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
|
|
3109
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3110
|
-
return code === "42P01" || code === "42703" || causeCode === "42P01" || causeCode === "42703" || (code === "23503" || causeCode === "23503") && message.includes("_gencow_workflow_signals_v2") || message.includes('relation "_gencow_workflow_signals_v2" does not exist') || message.includes("relation _gencow_workflow_signals_v2 does not exist") || message.includes('relation "_gencow_workflow_runs_v2" does not exist') || message.includes("relation _gencow_workflow_runs_v2 does not exist") || message.includes('relation "_gencow_workflow_outbox_v2" does not exist') || message.includes("relation _gencow_workflow_outbox_v2 does not exist") || message.includes('relation "_gencow_workflow_steps_v2" does not exist') || message.includes("relation _gencow_workflow_steps_v2 does not exist");
|
|
3111
|
-
}
|
|
3112
3613
|
function isMissingWorkflowV2PayloadSchemaError(error) {
|
|
3113
3614
|
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
3114
3615
|
const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
|
|
@@ -3125,7 +3626,7 @@ function isMissingWorkflowV1Table(error) {
|
|
|
3125
3626
|
}
|
|
3126
3627
|
async function loadWorkflowV2Run(db, workflowId) {
|
|
3127
3628
|
try {
|
|
3128
|
-
const result = await db.execute(
|
|
3629
|
+
const result = await db.execute(sql8`
|
|
3129
3630
|
SELECT id, workflow_name, status, args_json, result_json, error_json, error_code,
|
|
3130
3631
|
current_step_key, retry_count, max_retries, max_active_duration_ms,
|
|
3131
3632
|
created_at, updated_at, completed_at, user_id
|
|
@@ -3133,106 +3634,47 @@ async function loadWorkflowV2Run(db, workflowId) {
|
|
|
3133
3634
|
WHERE id = ${workflowId}
|
|
3134
3635
|
LIMIT 1
|
|
3135
3636
|
`);
|
|
3136
|
-
return
|
|
3637
|
+
return rowsFromResult2(result)[0] ?? null;
|
|
3137
3638
|
} catch (error) {
|
|
3138
3639
|
if (isMissingWorkflowV2PayloadSchemaError(error)) return null;
|
|
3139
3640
|
throw error;
|
|
3140
3641
|
}
|
|
3141
3642
|
}
|
|
3643
|
+
function isMissingWorkflowV2JournalColumnError(error) {
|
|
3644
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
3645
|
+
const cause = error && typeof error === "object" && "cause" in error ? error.cause : null;
|
|
3646
|
+
const causeCode = cause && typeof cause === "object" && "code" in cause ? String(cause.code) : "";
|
|
3647
|
+
return code === "42703" || causeCode === "42703";
|
|
3648
|
+
}
|
|
3142
3649
|
async function loadWorkflowV2Steps(db, workflowId) {
|
|
3143
3650
|
try {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3651
|
+
try {
|
|
3652
|
+
const stepResult = await db.execute(sql8`
|
|
3653
|
+
SELECT step_key, status, command_kind, output_json, output_codec, error_json, started_at, updated_at, completed_at
|
|
3654
|
+
FROM _gencow_workflow_steps_v2
|
|
3655
|
+
WHERE run_id = ${workflowId}
|
|
3656
|
+
AND (command_kind IS NULL OR command_kind = 'step')
|
|
3657
|
+
ORDER BY step_index ASC, COALESCE(started_at, updated_at) ASC, step_key ASC
|
|
3658
|
+
`);
|
|
3659
|
+
return rowsFromResult2(stepResult);
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
if (!isMissingWorkflowV2JournalColumnError(error)) throw error;
|
|
3662
|
+
const stepResult = await db.execute(sql8`
|
|
3663
|
+
SELECT step_key, status, output_json, error_json, started_at, updated_at, completed_at
|
|
3664
|
+
FROM _gencow_workflow_steps_v2
|
|
3665
|
+
WHERE run_id = ${workflowId}
|
|
3666
|
+
ORDER BY step_index ASC, COALESCE(started_at, updated_at) ASC, step_key ASC
|
|
3667
|
+
`);
|
|
3668
|
+
return rowsFromResult2(stepResult);
|
|
3669
|
+
}
|
|
3151
3670
|
} catch (error) {
|
|
3152
3671
|
if (isMissingWorkflowV2PayloadSchemaError(error)) return [];
|
|
3153
3672
|
throw error;
|
|
3154
3673
|
}
|
|
3155
3674
|
}
|
|
3156
|
-
async function tryRecordWorkflowV2Signal(options) {
|
|
3157
|
-
try {
|
|
3158
|
-
const result = await options.db.execute(sql4`
|
|
3159
|
-
WITH eligible_run AS (
|
|
3160
|
-
SELECT id
|
|
3161
|
-
FROM _gencow_workflow_runs_v2
|
|
3162
|
-
WHERE id = ${options.workflowId}
|
|
3163
|
-
AND status IN ('queued', 'running', 'sleeping', 'waiting')
|
|
3164
|
-
AND completed_at IS NULL
|
|
3165
|
-
AND cancel_requested_at IS NULL
|
|
3166
|
-
FOR UPDATE
|
|
3167
|
-
), inserted AS (
|
|
3168
|
-
INSERT INTO _gencow_workflow_signals_v2 (
|
|
3169
|
-
id,
|
|
3170
|
-
run_id,
|
|
3171
|
-
event_name,
|
|
3172
|
-
payload_json,
|
|
3173
|
-
idempotency_key
|
|
3174
|
-
)
|
|
3175
|
-
SELECT
|
|
3176
|
-
${options.signalId},
|
|
3177
|
-
eligible_run.id,
|
|
3178
|
-
${options.event},
|
|
3179
|
-
${workflowJsonb(options.payload)},
|
|
3180
|
-
${options.signalId}
|
|
3181
|
-
FROM eligible_run
|
|
3182
|
-
RETURNING id, run_id
|
|
3183
|
-
),
|
|
3184
|
-
queued_run AS (
|
|
3185
|
-
UPDATE _gencow_workflow_runs_v2 run
|
|
3186
|
-
SET
|
|
3187
|
-
status = 'queued',
|
|
3188
|
-
runnable_at = NOW(),
|
|
3189
|
-
lease_owner = NULL,
|
|
3190
|
-
lease_expires_at = NULL,
|
|
3191
|
-
heartbeat_at = NULL,
|
|
3192
|
-
updated_at = NOW()
|
|
3193
|
-
FROM inserted
|
|
3194
|
-
WHERE run.id = inserted.run_id
|
|
3195
|
-
AND run.status = 'waiting'
|
|
3196
|
-
AND run.completed_at IS NULL
|
|
3197
|
-
AND run.cancel_requested_at IS NULL
|
|
3198
|
-
RETURNING run.id
|
|
3199
|
-
),
|
|
3200
|
-
wake_outbox AS (
|
|
3201
|
-
INSERT INTO _gencow_workflow_outbox_v2 (
|
|
3202
|
-
id,
|
|
3203
|
-
run_id,
|
|
3204
|
-
kind,
|
|
3205
|
-
available_at,
|
|
3206
|
-
status
|
|
3207
|
-
)
|
|
3208
|
-
SELECT
|
|
3209
|
-
'signal:' || inserted.id,
|
|
3210
|
-
inserted.run_id,
|
|
3211
|
-
'wake_signal',
|
|
3212
|
-
NOW(),
|
|
3213
|
-
'pending'
|
|
3214
|
-
FROM inserted
|
|
3215
|
-
JOIN queued_run ON queued_run.id = inserted.run_id
|
|
3216
|
-
ON CONFLICT (id) DO NOTHING
|
|
3217
|
-
RETURNING id
|
|
3218
|
-
),
|
|
3219
|
-
wake_notified AS (
|
|
3220
|
-
SELECT pg_notify(${WORKFLOW_WAKE_SIGNAL_CHANNEL}, ${WORKFLOW_WAKE_SIGNAL_PAYLOAD})
|
|
3221
|
-
FROM wake_outbox
|
|
3222
|
-
)
|
|
3223
|
-
SELECT 1
|
|
3224
|
-
FROM inserted
|
|
3225
|
-
LEFT JOIN wake_notified ON TRUE
|
|
3226
|
-
`);
|
|
3227
|
-
return rowsFromResult(result).length > 0 ? "accepted" : "rejected";
|
|
3228
|
-
} catch (error) {
|
|
3229
|
-
if (isMissingWorkflowV2SignalSchemaError(error)) return "absent";
|
|
3230
|
-
throw error;
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
3675
|
async function mirrorWorkflowV2SignalToV1(options) {
|
|
3234
3676
|
try {
|
|
3235
|
-
await options.db.execute(
|
|
3677
|
+
await options.db.execute(sql8`
|
|
3236
3678
|
INSERT INTO _gencow_workflow_events (id, workflow_id, event_name, payload)
|
|
3237
3679
|
SELECT ${options.signalId}, workflow.id, ${options.event}, NULL
|
|
3238
3680
|
FROM _gencow_workflows workflow
|
|
@@ -3249,7 +3691,7 @@ async function loadWorkflowSnapshot(db, workflowId, options) {
|
|
|
3249
3691
|
const v2Run = await loadWorkflowV2Run(db, workflowId);
|
|
3250
3692
|
let workflowResult;
|
|
3251
3693
|
try {
|
|
3252
|
-
workflowResult = await db.execute(
|
|
3694
|
+
workflowResult = await db.execute(sql8`
|
|
3253
3695
|
SELECT
|
|
3254
3696
|
id,
|
|
3255
3697
|
name,
|
|
@@ -3275,7 +3717,7 @@ async function loadWorkflowSnapshot(db, workflowId, options) {
|
|
|
3275
3717
|
if (!v2Run || !isMissingWorkflowV1Table(error)) throw error;
|
|
3276
3718
|
workflowResult = [];
|
|
3277
3719
|
}
|
|
3278
|
-
const row =
|
|
3720
|
+
const row = rowsFromResult2(workflowResult)[0] ?? null;
|
|
3279
3721
|
if (!v2Run && !row) return null;
|
|
3280
3722
|
const viewerUserId = options?.viewerUserId ?? null;
|
|
3281
3723
|
const ownerId = v2Run ? v2Run.user_id : row?.user_id;
|
|
@@ -3289,7 +3731,7 @@ async function loadWorkflowSnapshot(db, workflowId, options) {
|
|
|
3289
3731
|
if (steps.length === 0) {
|
|
3290
3732
|
let stepsResult;
|
|
3291
3733
|
try {
|
|
3292
|
-
stepsResult = await db.execute(
|
|
3734
|
+
stepsResult = await db.execute(sql8`
|
|
3293
3735
|
SELECT
|
|
3294
3736
|
step_name,
|
|
3295
3737
|
status,
|
|
@@ -3306,7 +3748,7 @@ async function loadWorkflowSnapshot(db, workflowId, options) {
|
|
|
3306
3748
|
if (!v2Run || !isMissingWorkflowV1Table(error)) throw error;
|
|
3307
3749
|
stepsResult = [];
|
|
3308
3750
|
}
|
|
3309
|
-
steps =
|
|
3751
|
+
steps = rowsFromResult2(stepsResult).map(mapWorkflowStep);
|
|
3310
3752
|
}
|
|
3311
3753
|
return {
|
|
3312
3754
|
...v2Run ? mapV2WorkflowSummary(v2Run) : mapWorkflowSummary(row),
|
|
@@ -3333,7 +3775,8 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3333
3775
|
argsSchema: {
|
|
3334
3776
|
id: v.string(),
|
|
3335
3777
|
event: v.string(),
|
|
3336
|
-
payload: v.optional(v.any())
|
|
3778
|
+
payload: v.optional(v.any()),
|
|
3779
|
+
idempotencyKey: v.optional(v.string())
|
|
3337
3780
|
},
|
|
3338
3781
|
isPublic: true,
|
|
3339
3782
|
handler: async (ctx, args) => {
|
|
@@ -3370,14 +3813,19 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3370
3813
|
serializedValue: persistedPayload
|
|
3371
3814
|
});
|
|
3372
3815
|
const signalId = crypto.randomUUID();
|
|
3816
|
+
const idempotencyKey = normalizeWorkflowIdempotencyKey(args.idempotencyKey);
|
|
3373
3817
|
if (v2Run) {
|
|
3374
3818
|
const recordedV2 = await tryRecordWorkflowV2Signal({
|
|
3375
3819
|
db: ctx.unsafeDb,
|
|
3376
3820
|
workflowId,
|
|
3377
3821
|
event: normalizedEvent,
|
|
3378
3822
|
signalId,
|
|
3379
|
-
payload: persistedPayload
|
|
3823
|
+
payload: persistedPayload,
|
|
3824
|
+
idempotencyKey
|
|
3380
3825
|
});
|
|
3826
|
+
if (recordedV2 === "duplicate") {
|
|
3827
|
+
return { ok: true, workflowId, event: normalizedEvent, scheduledJobId: null, deduplicated: true };
|
|
3828
|
+
}
|
|
3381
3829
|
if (recordedV2 !== "accepted") {
|
|
3382
3830
|
return { ok: false, workflowId, event: normalizedEvent, scheduledJobId: null };
|
|
3383
3831
|
}
|
|
@@ -3390,7 +3838,7 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3390
3838
|
});
|
|
3391
3839
|
}
|
|
3392
3840
|
} else {
|
|
3393
|
-
await ctx.unsafeDb.execute(
|
|
3841
|
+
await ctx.unsafeDb.execute(sql8`
|
|
3394
3842
|
INSERT INTO _gencow_workflow_events (
|
|
3395
3843
|
id,
|
|
3396
3844
|
workflow_id,
|
|
@@ -3434,23 +3882,21 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3434
3882
|
const userId = ctx.auth.requireAuth().id;
|
|
3435
3883
|
const limit = normalizeListLimit(args.limit);
|
|
3436
3884
|
const requestedStatus = normalizeDerivedStatus(args.status);
|
|
3437
|
-
const v1Derived =
|
|
3885
|
+
const v1Derived = sql8`CASE WHEN w.status = 'pending' AND w.current_step LIKE 'sleep#%' THEN 'sleeping'
|
|
3438
3886
|
WHEN w.status = 'pending' AND w.current_step LIKE 'wait:%' THEN 'waiting'
|
|
3439
|
-
WHEN w.status = 'pending' THEN 'queued'
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
ELSE v.status END
|
|
3444
|
-
ELSE w.status END`;
|
|
3445
|
-
const effectiveDerived = sql4`CASE WHEN v.id IS NOT NULL THEN
|
|
3446
|
-
CASE WHEN v.status = 'cancelled' THEN 'failed'
|
|
3447
|
-
WHEN v.status IN ('queued', 'waiting', 'sleeping') THEN v.status
|
|
3887
|
+
WHEN w.status = 'pending' THEN 'queued'
|
|
3888
|
+
WHEN w.status = 'failed' AND w.error_code = 'cancelled' THEN 'cancelled' ELSE w.status END`;
|
|
3889
|
+
const effectiveStatus = sql8`CASE WHEN v.id IS NOT NULL THEN
|
|
3890
|
+
CASE WHEN v.status IN ('queued', 'waiting', 'sleeping') THEN 'pending'
|
|
3448
3891
|
ELSE v.status END
|
|
3892
|
+
ELSE CASE WHEN w.status = 'failed' AND w.error_code = 'cancelled' THEN 'cancelled' ELSE w.status END END`;
|
|
3893
|
+
const effectiveDerived = sql8`CASE WHEN v.id IS NOT NULL THEN
|
|
3894
|
+
v.status
|
|
3449
3895
|
ELSE ${v1Derived} END`;
|
|
3450
3896
|
const statusForFilter = requestedStatus === "pending" ? effectiveStatus : effectiveDerived;
|
|
3451
|
-
const v1StatusForFilter = requestedStatus === "pending" ?
|
|
3897
|
+
const v1StatusForFilter = requestedStatus === "pending" ? sql8`w.status` : v1Derived;
|
|
3452
3898
|
try {
|
|
3453
|
-
const result = await ctx.unsafeDb.execute(
|
|
3899
|
+
const result = await ctx.unsafeDb.execute(sql8`
|
|
3454
3900
|
SELECT w.*, v.id AS v2_id, v.workflow_name AS v2_name,
|
|
3455
3901
|
v.status AS v2_status, v.error_json AS v2_error_json,
|
|
3456
3902
|
v.error_code AS v2_error_code, v.current_step_key AS v2_current_step,
|
|
@@ -3465,12 +3911,11 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3465
3911
|
ORDER BY COALESCE(v.created_at, w.started_at) DESC, COALESCE(v.id, w.id) DESC
|
|
3466
3912
|
LIMIT ${limit}
|
|
3467
3913
|
`);
|
|
3468
|
-
return
|
|
3914
|
+
return rowsFromResult2(result).map(mapJoinedWorkflowSummary);
|
|
3469
3915
|
} catch (error) {
|
|
3470
3916
|
if (isMissingWorkflowV1Table(error)) {
|
|
3471
|
-
const v2StatusForFilter = requestedStatus === "pending" ?
|
|
3472
|
-
|
|
3473
|
-
const result2 = await ctx.unsafeDb.execute(sql4`
|
|
3917
|
+
const v2StatusForFilter = requestedStatus === "pending" ? sql8`CASE WHEN status IN ('queued', 'waiting', 'sleeping') THEN 'pending' ELSE status END` : sql8`status`;
|
|
3918
|
+
const result2 = await ctx.unsafeDb.execute(sql8`
|
|
3474
3919
|
SELECT id, workflow_name, status, args_json, result_json, error_json, error_code,
|
|
3475
3920
|
current_step_key, retry_count, max_retries, max_active_duration_ms,
|
|
3476
3921
|
created_at, updated_at, completed_at, user_id
|
|
@@ -3480,24 +3925,25 @@ function buildWorkflowBuiltinDefinitions() {
|
|
|
3480
3925
|
ORDER BY created_at DESC, id DESC
|
|
3481
3926
|
LIMIT ${limit}
|
|
3482
3927
|
`);
|
|
3483
|
-
return
|
|
3928
|
+
return rowsFromResult2(result2).map(mapV2WorkflowSummary);
|
|
3484
3929
|
}
|
|
3485
3930
|
if (!isMissingWorkflowV2PayloadSchemaError(error)) throw error;
|
|
3486
|
-
const result = await ctx.unsafeDb.execute(
|
|
3931
|
+
const result = await ctx.unsafeDb.execute(sql8`
|
|
3487
3932
|
SELECT w.* FROM _gencow_workflows w
|
|
3488
3933
|
WHERE w.user_id = ${userId}
|
|
3489
3934
|
AND (${requestedStatus ?? null}::text IS NULL OR ${v1StatusForFilter} = ${requestedStatus ?? null})
|
|
3490
3935
|
ORDER BY w.started_at DESC, w.id DESC
|
|
3491
3936
|
LIMIT ${limit}
|
|
3492
3937
|
`);
|
|
3493
|
-
return
|
|
3938
|
+
return rowsFromResult2(result).map(mapWorkflowSummary);
|
|
3494
3939
|
}
|
|
3495
3940
|
}
|
|
3496
3941
|
};
|
|
3497
3942
|
const definitions = Object.freeze({
|
|
3498
3943
|
get: Object.freeze(get),
|
|
3499
3944
|
list: Object.freeze(list),
|
|
3500
|
-
signal: Object.freeze(signal)
|
|
3945
|
+
signal: Object.freeze(signal),
|
|
3946
|
+
cancel: Object.freeze(buildWorkflowCancelDefinition())
|
|
3501
3947
|
});
|
|
3502
3948
|
return definitions;
|
|
3503
3949
|
}
|
|
@@ -3511,6 +3957,7 @@ function registerWorkflowsApi() {
|
|
|
3511
3957
|
queryRegistry.set(definitions.get.key, definitions.get);
|
|
3512
3958
|
queryRegistry.set(definitions.list.key, definitions.list);
|
|
3513
3959
|
mutationRegistry.push(definitions.signal);
|
|
3960
|
+
mutationRegistry.push(definitions.cancel);
|
|
3514
3961
|
globalThis.__gencow_workflowsApiRegistered = definitions;
|
|
3515
3962
|
return definitions;
|
|
3516
3963
|
}
|
|
@@ -3839,11 +4286,11 @@ function defineConfig(config) {
|
|
|
3839
4286
|
}
|
|
3840
4287
|
|
|
3841
4288
|
// ../core/src/rls-context-key.ts
|
|
3842
|
-
import { sql as
|
|
4289
|
+
import { sql as sql9 } from "drizzle-orm";
|
|
3843
4290
|
function sessionSettingSqlForKey(sessionKey) {
|
|
3844
4291
|
const guc = sessionKeyToGucName(sessionKey);
|
|
3845
4292
|
const escaped = guc.replace(/'/g, "''");
|
|
3846
|
-
return
|
|
4293
|
+
return sql9.raw(`current_setting('${escaped}', true)`);
|
|
3847
4294
|
}
|
|
3848
4295
|
function rlsContextKey(key) {
|
|
3849
4296
|
return sessionSettingSqlForKey(key);
|
|
@@ -3852,11 +4299,11 @@ function rlsContextKeys() {
|
|
|
3852
4299
|
return (key) => rlsContextKey(key);
|
|
3853
4300
|
}
|
|
3854
4301
|
function eqRlsContextColumn(column, key) {
|
|
3855
|
-
return
|
|
4302
|
+
return sql9`${column} = ${rlsContextKey(key)}`;
|
|
3856
4303
|
}
|
|
3857
4304
|
|
|
3858
4305
|
// ../core/src/rls-define-table.ts
|
|
3859
|
-
import { sql as
|
|
4306
|
+
import { sql as sql10 } from "drizzle-orm";
|
|
3860
4307
|
import { getTableName } from "drizzle-orm";
|
|
3861
4308
|
import { pgPolicy } from "drizzle-orm/pg-core";
|
|
3862
4309
|
var ALL_COMMANDS = ["select", "insert", "update", "delete"];
|
|
@@ -3868,22 +4315,22 @@ function escapeSqlStringLiteral(value) {
|
|
|
3868
4315
|
return value.replace(/'/g, "''");
|
|
3869
4316
|
}
|
|
3870
4317
|
function sqlStringLiteral(value) {
|
|
3871
|
-
return
|
|
4318
|
+
return sql10.raw(`'${escapeSqlStringLiteral(value)}'`);
|
|
3872
4319
|
}
|
|
3873
4320
|
function andExprs(exprs) {
|
|
3874
|
-
if (exprs.length === 0) return
|
|
4321
|
+
if (exprs.length === 0) return sql10`true`;
|
|
3875
4322
|
if (exprs.length === 1) return exprs[0];
|
|
3876
|
-
return
|
|
3877
|
-
exprs.map((e) =>
|
|
3878
|
-
|
|
4323
|
+
return sql10.join(
|
|
4324
|
+
exprs.map((e) => sql10`(${e})`),
|
|
4325
|
+
sql10` AND `
|
|
3879
4326
|
);
|
|
3880
4327
|
}
|
|
3881
4328
|
function orExprs(exprs) {
|
|
3882
|
-
if (exprs.length === 0) return
|
|
4329
|
+
if (exprs.length === 0) return sql10`false`;
|
|
3883
4330
|
if (exprs.length === 1) return exprs[0];
|
|
3884
|
-
return
|
|
3885
|
-
exprs.map((e) =>
|
|
3886
|
-
|
|
4331
|
+
return sql10.join(
|
|
4332
|
+
exprs.map((e) => sql10`(${e})`),
|
|
4333
|
+
sql10` OR `
|
|
3887
4334
|
);
|
|
3888
4335
|
}
|
|
3889
4336
|
function combinedExprForCommand(constraints, command) {
|
|
@@ -3919,20 +4366,20 @@ var TableRlsBuilder = class _TableRlsBuilder {
|
|
|
3919
4366
|
constraints = [];
|
|
3920
4367
|
readPublicSelect = false;
|
|
3921
4368
|
matchColumn(column, keySql, options) {
|
|
3922
|
-
const expr =
|
|
4369
|
+
const expr = sql10`${column} = ${keySql}`;
|
|
3923
4370
|
this.constraints.push({ commands: normalizeCommands(options?.commands), expr });
|
|
3924
4371
|
return this;
|
|
3925
4372
|
}
|
|
3926
4373
|
sessionEq(keySql, value, options) {
|
|
3927
|
-
const expr =
|
|
4374
|
+
const expr = sql10`${keySql} = ${sqlStringLiteral(stringifySessionValue(value))}`;
|
|
3928
4375
|
this.constraints.push({ commands: normalizeCommands(options?.commands), expr });
|
|
3929
4376
|
return this;
|
|
3930
4377
|
}
|
|
3931
4378
|
sessionIn(keySql, values, options) {
|
|
3932
4379
|
const literals = values.map((v2) => stringifySessionValue(v2));
|
|
3933
|
-
const expr = literals.length === 0 ?
|
|
4380
|
+
const expr = literals.length === 0 ? sql10`false` : sql10`${keySql} IN (${sql10.join(
|
|
3934
4381
|
literals.map((v2) => sqlStringLiteral(v2)),
|
|
3935
|
-
|
|
4382
|
+
sql10`, `
|
|
3936
4383
|
)})`;
|
|
3937
4384
|
this.constraints.push({ commands: normalizeCommands(options?.commands), expr });
|
|
3938
4385
|
return this;
|
|
@@ -3971,17 +4418,17 @@ var TableRlsBuilder = class _TableRlsBuilder {
|
|
|
3971
4418
|
return this.sessionCompare(keySql, "<", n, options);
|
|
3972
4419
|
}
|
|
3973
4420
|
sessionCompare(keySql, op, n, options) {
|
|
3974
|
-
const bound =
|
|
4421
|
+
const bound = sql10.raw(String(n));
|
|
3975
4422
|
const expr = (() => {
|
|
3976
4423
|
switch (op) {
|
|
3977
4424
|
case ">=":
|
|
3978
|
-
return
|
|
4425
|
+
return sql10`(${keySql})::numeric >= ${bound}`;
|
|
3979
4426
|
case ">":
|
|
3980
|
-
return
|
|
4427
|
+
return sql10`(${keySql})::numeric > ${bound}`;
|
|
3981
4428
|
case "<=":
|
|
3982
|
-
return
|
|
4429
|
+
return sql10`(${keySql})::numeric <= ${bound}`;
|
|
3983
4430
|
case "<":
|
|
3984
|
-
return
|
|
4431
|
+
return sql10`(${keySql})::numeric < ${bound}`;
|
|
3985
4432
|
}
|
|
3986
4433
|
})();
|
|
3987
4434
|
this.constraints.push({ commands: normalizeCommands(options?.commands), expr });
|
|
@@ -4020,7 +4467,7 @@ var TableRlsBuilder = class _TableRlsBuilder {
|
|
|
4020
4467
|
policies.push(
|
|
4021
4468
|
pgPolicy(this.policyName(command, counters[command]++), {
|
|
4022
4469
|
for: "select",
|
|
4023
|
-
using:
|
|
4470
|
+
using: sql10`true`
|
|
4024
4471
|
})
|
|
4025
4472
|
);
|
|
4026
4473
|
continue;
|
|
@@ -4084,7 +4531,7 @@ import { pgTable } from "drizzle-orm/pg-core";
|
|
|
4084
4531
|
var pgBaseTable = pgTable;
|
|
4085
4532
|
|
|
4086
4533
|
// ../core/src/rls-db.ts
|
|
4087
|
-
import { sql as
|
|
4534
|
+
import { sql as sql11 } from "drizzle-orm";
|
|
4088
4535
|
|
|
4089
4536
|
// ../core/src/rls-db-scoped-query.ts
|
|
4090
4537
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -4258,7 +4705,7 @@ function transactionConfigSql(config) {
|
|
|
4258
4705
|
if (typeof value.deferrable !== "boolean") return null;
|
|
4259
4706
|
chunks.push(value.deferrable ? "deferrable" : "not deferrable");
|
|
4260
4707
|
}
|
|
4261
|
-
return chunks.length > 0 ?
|
|
4708
|
+
return chunks.length > 0 ? sql11.raw(`SET TRANSACTION ${chunks.join(", ")}`) : null;
|
|
4262
4709
|
}
|
|
4263
4710
|
async function applyTransactionConfig(tx, config) {
|
|
4264
4711
|
const statement = transactionConfigSql(config);
|
|
@@ -4300,7 +4747,7 @@ async function applyRlsSessionVars(client, rls) {
|
|
|
4300
4747
|
}
|
|
4301
4748
|
async function injectRlsVarsOnTx(tx, rls, onTransactionContextFailure) {
|
|
4302
4749
|
try {
|
|
4303
|
-
await forEachSetConfig(rls, (name, value) => tx.execute(
|
|
4750
|
+
await forEachSetConfig(rls, (name, value) => tx.execute(sql11`SELECT set_config(${name}, ${value}, true)`));
|
|
4304
4751
|
} catch (error) {
|
|
4305
4752
|
try {
|
|
4306
4753
|
onTransactionContextFailure?.(error);
|
|
@@ -5042,7 +5489,7 @@ function buildRowOutputValidator(columns) {
|
|
|
5042
5489
|
}
|
|
5043
5490
|
|
|
5044
5491
|
// ../core/src/createCrud.cursor.ts
|
|
5045
|
-
import { sql as
|
|
5492
|
+
import { sql as sql12 } from "drizzle-orm";
|
|
5046
5493
|
function normalizeOrderDir(raw) {
|
|
5047
5494
|
const d = typeof raw === "string" ? raw.toLowerCase() : "";
|
|
5048
5495
|
return d === "asc" ? "asc" : "desc";
|
|
@@ -5075,16 +5522,16 @@ function buildTupleComparePredicate(columns, values, direction) {
|
|
|
5075
5522
|
if (columns.length !== values.length || columns.length === 0) {
|
|
5076
5523
|
throw new Error("cursor column count mismatch");
|
|
5077
5524
|
}
|
|
5078
|
-
const opToken = direction === "asc" ?
|
|
5079
|
-
const colTuple =
|
|
5080
|
-
columns.map((c) =>
|
|
5081
|
-
|
|
5525
|
+
const opToken = direction === "asc" ? sql12`>` : sql12`<`;
|
|
5526
|
+
const colTuple = sql12.join(
|
|
5527
|
+
columns.map((c) => sql12`${c}`),
|
|
5528
|
+
sql12`, `
|
|
5082
5529
|
);
|
|
5083
|
-
const valTuple =
|
|
5084
|
-
values.map((val) =>
|
|
5085
|
-
|
|
5530
|
+
const valTuple = sql12.join(
|
|
5531
|
+
values.map((val) => sql12`${val}`),
|
|
5532
|
+
sql12`, `
|
|
5086
5533
|
);
|
|
5087
|
-
return
|
|
5534
|
+
return sql12`(${colTuple}) ${opToken} (${valTuple})`;
|
|
5088
5535
|
}
|
|
5089
5536
|
|
|
5090
5537
|
// ../core/src/createCrud.ts
|
|
@@ -5646,7 +6093,18 @@ export {
|
|
|
5646
6093
|
TableRlsBuilder,
|
|
5647
6094
|
WORKFLOW_REALTIME_KEY_PREFIX,
|
|
5648
6095
|
WORKFLOW_RESUME_ACTION_PREFIX,
|
|
6096
|
+
WORKFLOW_STEP_FAILED_CODE,
|
|
6097
|
+
WORKFLOW_STEP_OPTIONS_INVALID_CODE,
|
|
6098
|
+
WORKFLOW_STEP_TIMEOUT_CODE,
|
|
6099
|
+
WORKFLOW_WAIT_TIMEOUT_CODE,
|
|
6100
|
+
WorkflowCommandNameError,
|
|
6101
|
+
WorkflowNestedCommandError,
|
|
6102
|
+
WorkflowNonRetryableError,
|
|
5649
6103
|
WorkflowPayloadPolicyError,
|
|
6104
|
+
WorkflowStepFailedError,
|
|
6105
|
+
WorkflowStepOptionsError,
|
|
6106
|
+
WorkflowStepTimeoutError,
|
|
6107
|
+
WorkflowWaitTimeoutError,
|
|
5650
6108
|
advanceUnreadCursor,
|
|
5651
6109
|
applyFilterOp,
|
|
5652
6110
|
assertWorkflowPayloadWithinLimit,
|
|
@@ -5678,6 +6136,7 @@ export {
|
|
|
5678
6136
|
cron,
|
|
5679
6137
|
cronJobs,
|
|
5680
6138
|
defaultRlsSessionContext,
|
|
6139
|
+
defaultWorkflowCommandProtocol,
|
|
5681
6140
|
defineApi,
|
|
5682
6141
|
defineAuth,
|
|
5683
6142
|
defineConfig,
|
|
@@ -5719,6 +6178,7 @@ export {
|
|
|
5719
6178
|
normalizeRealtimeChannelEpoch,
|
|
5720
6179
|
normalizeRealtimeQuerySubscriptionEpoch,
|
|
5721
6180
|
normalizeUnreadCursor,
|
|
6181
|
+
normalizeWorkflowCommandName,
|
|
5722
6182
|
ownerRls,
|
|
5723
6183
|
parseArgs,
|
|
5724
6184
|
parseFilterNode,
|
|
@@ -5750,6 +6210,7 @@ export {
|
|
|
5750
6210
|
tableRls,
|
|
5751
6211
|
v,
|
|
5752
6212
|
validateRlsSessionContext,
|
|
6213
|
+
validateWorkflowStepOptions,
|
|
5753
6214
|
verifyRealtimeChannelToken,
|
|
5754
6215
|
verifyRealtimeQuerySubscriptionToken,
|
|
5755
6216
|
verifyRealtimeTicket,
|