@multiplier-labs/stepflow 0.3.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/.github/workflows/publish.yml +30 -0
- package/README.md +408 -0
- package/dist/chunk-FQCT3I2A.js +1512 -0
- package/dist/chunk-FQCT3I2A.js.map +1 -0
- package/dist/chunk-GXY66V7Q.js +111 -0
- package/dist/chunk-GXY66V7Q.js.map +1 -0
- package/dist/chunk-U5QGN3RT.js +715 -0
- package/dist/chunk-U5QGN3RT.js.map +1 -0
- package/dist/chunk-XCF4PLN7.js +575 -0
- package/dist/chunk-XCF4PLN7.js.map +1 -0
- package/dist/events/index.d.ts +346 -0
- package/dist/events/index.js +11 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index-CPfGsmrp.d.ts +567 -0
- package/dist/index.d.ts +748 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/storage/index.d.ts +402 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-BVShq7zT.d.ts +150 -0
- package/dist/types-BlwFEGzj.d.ts +276 -0
- package/dist/types-DzkEqnQJ.d.ts +75 -0
- package/docs/github-packages-setup.md +97 -0
- package/docs/stepflow-api-reference.md +1662 -0
- package/package.json +85 -0
- package/src/core/engine.test.ts +1421 -0
- package/src/core/engine.ts +717 -0
- package/src/core/index.ts +7 -0
- package/src/core/orchestrator.ts +590 -0
- package/src/core/types.ts +242 -0
- package/src/events/index.ts +8 -0
- package/src/events/memory.test.ts +109 -0
- package/src/events/memory.ts +95 -0
- package/src/events/socketio.test.ts +428 -0
- package/src/events/socketio.ts +269 -0
- package/src/events/types.ts +121 -0
- package/src/events/webhook.test.ts +695 -0
- package/src/events/webhook.ts +591 -0
- package/src/index.ts +270 -0
- package/src/planning/index.ts +52 -0
- package/src/planning/planner.ts +601 -0
- package/src/planning/planning.test.ts +790 -0
- package/src/planning/registry.test.ts +445 -0
- package/src/planning/registry.ts +369 -0
- package/src/planning/types.ts +570 -0
- package/src/scheduler/cron.test.ts +480 -0
- package/src/scheduler/cron.ts +449 -0
- package/src/scheduler/index.ts +18 -0
- package/src/scheduler/postgres-persistence.test.ts +906 -0
- package/src/scheduler/postgres-persistence.ts +546 -0
- package/src/scheduler/sqlite-persistence.test.ts +235 -0
- package/src/scheduler/sqlite-persistence.ts +268 -0
- package/src/scheduler/types.ts +78 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/memory.test.ts +399 -0
- package/src/storage/memory.ts +248 -0
- package/src/storage/postgres.ts +1550 -0
- package/src/storage/sqlite.test.ts +593 -0
- package/src/storage/sqlite.ts +662 -0
- package/src/storage/types.ts +368 -0
- package/src/utils/errors.test.ts +152 -0
- package/src/utils/errors.ts +191 -0
- package/src/utils/id.ts +21 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.test.ts +127 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/postgres-deps.ts +63 -0
- package/src/utils/retry.test.ts +197 -0
- package/src/utils/retry.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,1512 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generateId,
|
|
3
|
+
loadPostgresDeps,
|
|
4
|
+
sanitizeErrorForStorage
|
|
5
|
+
} from "./chunk-GXY66V7Q.js";
|
|
6
|
+
|
|
7
|
+
// src/storage/memory.ts
|
|
8
|
+
var MemoryStorageAdapter = class {
|
|
9
|
+
runs = /* @__PURE__ */ new Map();
|
|
10
|
+
steps = /* @__PURE__ */ new Map();
|
|
11
|
+
events = /* @__PURE__ */ new Map();
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Run Operations
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/** Create and persist a new workflow run record. */
|
|
16
|
+
async createRun(run) {
|
|
17
|
+
const record = {
|
|
18
|
+
...run,
|
|
19
|
+
id: generateId(),
|
|
20
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
21
|
+
};
|
|
22
|
+
this.runs.set(record.id, record);
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
/** Retrieve a workflow run by ID, or null if not found. */
|
|
26
|
+
async getRun(runId) {
|
|
27
|
+
return this.runs.get(runId) ?? null;
|
|
28
|
+
}
|
|
29
|
+
/** Apply partial updates to an existing workflow run. No-op if the run does not exist. */
|
|
30
|
+
async updateRun(runId, updates) {
|
|
31
|
+
const run = this.runs.get(runId);
|
|
32
|
+
if (run) {
|
|
33
|
+
Object.assign(run, updates);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** List workflow runs with optional filtering, sorting, and pagination. */
|
|
37
|
+
async listRuns(options = {}) {
|
|
38
|
+
let items = Array.from(this.runs.values());
|
|
39
|
+
if (options.kind) {
|
|
40
|
+
items = items.filter((r) => r.kind === options.kind);
|
|
41
|
+
}
|
|
42
|
+
if (options.status) {
|
|
43
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
44
|
+
items = items.filter((r) => statuses.includes(r.status));
|
|
45
|
+
}
|
|
46
|
+
if (options.parentRunId !== void 0) {
|
|
47
|
+
items = items.filter((r) => r.parentRunId === options.parentRunId);
|
|
48
|
+
}
|
|
49
|
+
const orderBy = options.orderBy ?? "createdAt";
|
|
50
|
+
const direction = options.orderDirection ?? "desc";
|
|
51
|
+
items.sort((a, b) => {
|
|
52
|
+
const aVal = a[orderBy]?.getTime() ?? 0;
|
|
53
|
+
const bVal = b[orderBy]?.getTime() ?? 0;
|
|
54
|
+
return direction === "asc" ? aVal - bVal : bVal - aVal;
|
|
55
|
+
});
|
|
56
|
+
const total = items.length;
|
|
57
|
+
const offset = options.offset ?? 0;
|
|
58
|
+
const limit = options.limit ?? 50;
|
|
59
|
+
items = items.slice(offset, offset + limit);
|
|
60
|
+
return { items, total, limit, offset };
|
|
61
|
+
}
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Step Operations
|
|
64
|
+
// ============================================================================
|
|
65
|
+
/** Create and persist a new step execution record. */
|
|
66
|
+
async createStep(step) {
|
|
67
|
+
const record = {
|
|
68
|
+
...step,
|
|
69
|
+
id: generateId()
|
|
70
|
+
};
|
|
71
|
+
this.steps.set(record.id, record);
|
|
72
|
+
return record;
|
|
73
|
+
}
|
|
74
|
+
/** Retrieve a step record by ID, or null if not found. */
|
|
75
|
+
async getStep(stepId) {
|
|
76
|
+
return this.steps.get(stepId) ?? null;
|
|
77
|
+
}
|
|
78
|
+
/** Apply partial updates to an existing step record. No-op if the step does not exist. */
|
|
79
|
+
async updateStep(stepId, updates) {
|
|
80
|
+
const step = this.steps.get(stepId);
|
|
81
|
+
if (step) {
|
|
82
|
+
Object.assign(step, updates);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Retrieve all step records for a workflow run, ordered by start time ascending. */
|
|
86
|
+
async getStepsForRun(runId) {
|
|
87
|
+
return Array.from(this.steps.values()).filter((s) => s.runId === runId).sort(
|
|
88
|
+
(a, b) => (a.startedAt?.getTime() ?? 0) - (b.startedAt?.getTime() ?? 0)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Event Operations
|
|
93
|
+
// ============================================================================
|
|
94
|
+
/** Persist a workflow event record. */
|
|
95
|
+
async saveEvent(event) {
|
|
96
|
+
const record = {
|
|
97
|
+
...event,
|
|
98
|
+
id: generateId()
|
|
99
|
+
};
|
|
100
|
+
this.events.set(record.id, record);
|
|
101
|
+
}
|
|
102
|
+
/** Retrieve events for a workflow run with optional filtering and pagination. */
|
|
103
|
+
async getEventsForRun(runId, options = {}) {
|
|
104
|
+
let items = Array.from(this.events.values()).filter(
|
|
105
|
+
(e) => e.runId === runId
|
|
106
|
+
);
|
|
107
|
+
if (options.stepKey) {
|
|
108
|
+
items = items.filter((e) => e.stepKey === options.stepKey);
|
|
109
|
+
}
|
|
110
|
+
if (options.level) {
|
|
111
|
+
items = items.filter((e) => e.level === options.level);
|
|
112
|
+
}
|
|
113
|
+
items.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
114
|
+
const offset = options.offset ?? 0;
|
|
115
|
+
const limit = options.limit ?? 100;
|
|
116
|
+
return items.slice(offset, offset + limit);
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Optional Operations
|
|
120
|
+
// ============================================================================
|
|
121
|
+
/** Delete runs (and their associated steps and events) created before the given date. Returns the number of deleted runs. */
|
|
122
|
+
async deleteOldRuns(olderThan) {
|
|
123
|
+
let deleted = 0;
|
|
124
|
+
for (const [id, run] of this.runs) {
|
|
125
|
+
if (run.createdAt < olderThan) {
|
|
126
|
+
for (const [stepId, step] of this.steps) {
|
|
127
|
+
if (step.runId === id) {
|
|
128
|
+
this.steps.delete(stepId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const [eventId, event] of this.events) {
|
|
132
|
+
if (event.runId === id) {
|
|
133
|
+
this.events.delete(eventId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
this.runs.delete(id);
|
|
137
|
+
deleted++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return deleted;
|
|
141
|
+
}
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Testing Utilities
|
|
144
|
+
// ============================================================================
|
|
145
|
+
/**
|
|
146
|
+
* Clear all stored data. Useful for testing.
|
|
147
|
+
*/
|
|
148
|
+
clear() {
|
|
149
|
+
this.runs.clear();
|
|
150
|
+
this.steps.clear();
|
|
151
|
+
this.events.clear();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get counts of stored records. Useful for testing.
|
|
155
|
+
*/
|
|
156
|
+
getStats() {
|
|
157
|
+
return {
|
|
158
|
+
runs: this.runs.size,
|
|
159
|
+
steps: this.steps.size,
|
|
160
|
+
events: this.events.size
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/storage/sqlite.ts
|
|
166
|
+
var CREATE_TABLES_SQL = `
|
|
167
|
+
-- Workflow runs table
|
|
168
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
169
|
+
id TEXT PRIMARY KEY,
|
|
170
|
+
kind TEXT NOT NULL,
|
|
171
|
+
status TEXT NOT NULL,
|
|
172
|
+
parent_run_id TEXT,
|
|
173
|
+
input_json TEXT NOT NULL,
|
|
174
|
+
metadata_json TEXT NOT NULL,
|
|
175
|
+
context_json TEXT NOT NULL,
|
|
176
|
+
error_json TEXT,
|
|
177
|
+
created_at TEXT NOT NULL,
|
|
178
|
+
started_at TEXT,
|
|
179
|
+
finished_at TEXT
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_kind_status ON workflow_runs(kind, status);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_parent ON workflow_runs(parent_run_id);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_created ON workflow_runs(created_at);
|
|
185
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
|
|
186
|
+
|
|
187
|
+
-- Workflow run steps table
|
|
188
|
+
CREATE TABLE IF NOT EXISTS workflow_run_steps (
|
|
189
|
+
id TEXT PRIMARY KEY,
|
|
190
|
+
run_id TEXT NOT NULL,
|
|
191
|
+
step_key TEXT NOT NULL,
|
|
192
|
+
step_name TEXT,
|
|
193
|
+
status TEXT NOT NULL,
|
|
194
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
195
|
+
result_json TEXT,
|
|
196
|
+
error_json TEXT,
|
|
197
|
+
started_at TEXT,
|
|
198
|
+
finished_at TEXT,
|
|
199
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run ON workflow_run_steps(run_id);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_key ON workflow_run_steps(run_id, step_key);
|
|
204
|
+
|
|
205
|
+
-- Workflow events table
|
|
206
|
+
CREATE TABLE IF NOT EXISTS workflow_events (
|
|
207
|
+
id TEXT PRIMARY KEY,
|
|
208
|
+
run_id TEXT NOT NULL,
|
|
209
|
+
step_key TEXT,
|
|
210
|
+
event_type TEXT NOT NULL,
|
|
211
|
+
level TEXT NOT NULL,
|
|
212
|
+
payload_json TEXT,
|
|
213
|
+
timestamp TEXT NOT NULL,
|
|
214
|
+
FOREIGN KEY (run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run ON workflow_events(run_id);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_ts ON workflow_events(run_id, timestamp);
|
|
219
|
+
`;
|
|
220
|
+
var SQLiteStorageAdapter = class {
|
|
221
|
+
db;
|
|
222
|
+
prefix;
|
|
223
|
+
// Prepared statements (cached for performance)
|
|
224
|
+
stmts = null;
|
|
225
|
+
constructor(config) {
|
|
226
|
+
this.db = config.db;
|
|
227
|
+
this.prefix = config.tablePrefix ?? "workflow";
|
|
228
|
+
this.db.pragma("foreign_keys = ON");
|
|
229
|
+
if (config.autoCreateTables !== false) {
|
|
230
|
+
this.createTables();
|
|
231
|
+
}
|
|
232
|
+
this.prepareStatements();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create the workflow tables if they don't exist.
|
|
236
|
+
*/
|
|
237
|
+
createTables() {
|
|
238
|
+
this.db.exec(CREATE_TABLES_SQL);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Prepare all SQL statements for better performance.
|
|
242
|
+
*/
|
|
243
|
+
prepareStatements() {
|
|
244
|
+
this.stmts = {
|
|
245
|
+
insertRun: this.db.prepare(`
|
|
246
|
+
INSERT INTO workflow_runs (id, kind, status, parent_run_id, input_json, metadata_json, context_json, error_json, created_at, started_at, finished_at)
|
|
247
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
248
|
+
`),
|
|
249
|
+
getRun: this.db.prepare(`
|
|
250
|
+
SELECT * FROM workflow_runs WHERE id = ?
|
|
251
|
+
`),
|
|
252
|
+
updateRun: this.db.prepare(`
|
|
253
|
+
UPDATE workflow_runs
|
|
254
|
+
SET status = COALESCE(?, status),
|
|
255
|
+
context_json = COALESCE(?, context_json),
|
|
256
|
+
error_json = COALESCE(?, error_json),
|
|
257
|
+
started_at = COALESCE(?, started_at),
|
|
258
|
+
finished_at = COALESCE(?, finished_at)
|
|
259
|
+
WHERE id = ?
|
|
260
|
+
`),
|
|
261
|
+
// json_each() is a SQLite table-valued function that expands a JSON array into rows.
|
|
262
|
+
// We use it here to support filtering by multiple statuses in a single prepared statement:
|
|
263
|
+
// the caller passes a JSON array like '["running","failed"]', and json_each unpacks it
|
|
264
|
+
// so the IN clause can match against each value. When status is NULL, the entire
|
|
265
|
+
// condition is bypassed via the (? IS NULL OR ...) guard.
|
|
266
|
+
listRuns: this.db.prepare(`
|
|
267
|
+
SELECT * FROM workflow_runs
|
|
268
|
+
WHERE (? IS NULL OR kind = ?)
|
|
269
|
+
AND (? IS NULL OR status IN (SELECT value FROM json_each(?)))
|
|
270
|
+
AND (? IS NULL OR parent_run_id = ?)
|
|
271
|
+
ORDER BY created_at DESC
|
|
272
|
+
LIMIT ? OFFSET ?
|
|
273
|
+
`),
|
|
274
|
+
countRuns: this.db.prepare(`
|
|
275
|
+
SELECT COUNT(*) as count FROM workflow_runs
|
|
276
|
+
WHERE (? IS NULL OR kind = ?)
|
|
277
|
+
AND (? IS NULL OR status IN (SELECT value FROM json_each(?)))
|
|
278
|
+
AND (? IS NULL OR parent_run_id = ?)
|
|
279
|
+
`),
|
|
280
|
+
insertStep: this.db.prepare(`
|
|
281
|
+
INSERT INTO workflow_run_steps (id, run_id, step_key, step_name, status, attempt, result_json, error_json, started_at, finished_at)
|
|
282
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
283
|
+
`),
|
|
284
|
+
getStep: this.db.prepare(`
|
|
285
|
+
SELECT * FROM workflow_run_steps WHERE id = ?
|
|
286
|
+
`),
|
|
287
|
+
updateStep: this.db.prepare(`
|
|
288
|
+
UPDATE workflow_run_steps
|
|
289
|
+
SET status = COALESCE(?, status),
|
|
290
|
+
attempt = COALESCE(?, attempt),
|
|
291
|
+
result_json = COALESCE(?, result_json),
|
|
292
|
+
error_json = COALESCE(?, error_json),
|
|
293
|
+
finished_at = COALESCE(?, finished_at)
|
|
294
|
+
WHERE id = ?
|
|
295
|
+
`),
|
|
296
|
+
getStepsForRun: this.db.prepare(`
|
|
297
|
+
SELECT * FROM workflow_run_steps WHERE run_id = ? ORDER BY started_at ASC
|
|
298
|
+
`),
|
|
299
|
+
insertEvent: this.db.prepare(`
|
|
300
|
+
INSERT INTO workflow_events (id, run_id, step_key, event_type, level, payload_json, timestamp)
|
|
301
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
302
|
+
`),
|
|
303
|
+
getEventsForRun: this.db.prepare(`
|
|
304
|
+
SELECT * FROM workflow_events
|
|
305
|
+
WHERE run_id = ?
|
|
306
|
+
AND (? IS NULL OR step_key = ?)
|
|
307
|
+
AND (? IS NULL OR level = ?)
|
|
308
|
+
ORDER BY timestamp ASC
|
|
309
|
+
LIMIT ? OFFSET ?
|
|
310
|
+
`),
|
|
311
|
+
deleteOldRuns: this.db.prepare(`
|
|
312
|
+
DELETE FROM workflow_runs WHERE created_at < ?
|
|
313
|
+
`),
|
|
314
|
+
deleteStepsForRuns: this.db.prepare(`
|
|
315
|
+
DELETE FROM workflow_run_steps WHERE run_id IN (SELECT id FROM workflow_runs WHERE created_at < ?)
|
|
316
|
+
`),
|
|
317
|
+
deleteEventsForRuns: this.db.prepare(`
|
|
318
|
+
DELETE FROM workflow_events WHERE run_id IN (SELECT id FROM workflow_runs WHERE created_at < ?)
|
|
319
|
+
`),
|
|
320
|
+
getInterruptedRuns: this.db.prepare(`
|
|
321
|
+
SELECT * FROM workflow_runs
|
|
322
|
+
WHERE status IN ('queued', 'running')
|
|
323
|
+
ORDER BY created_at ASC
|
|
324
|
+
`),
|
|
325
|
+
getLastCompletedStep: this.db.prepare(`
|
|
326
|
+
SELECT * FROM workflow_run_steps
|
|
327
|
+
WHERE run_id = ? AND status = 'succeeded'
|
|
328
|
+
ORDER BY finished_at DESC
|
|
329
|
+
LIMIT 1
|
|
330
|
+
`)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Run Operations
|
|
335
|
+
// ============================================================================
|
|
336
|
+
async createRun(run) {
|
|
337
|
+
const id = generateId();
|
|
338
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
339
|
+
this.stmts.insertRun.run(
|
|
340
|
+
id,
|
|
341
|
+
run.kind,
|
|
342
|
+
run.status,
|
|
343
|
+
run.parentRunId ?? null,
|
|
344
|
+
JSON.stringify(run.input),
|
|
345
|
+
JSON.stringify(run.metadata),
|
|
346
|
+
JSON.stringify(run.context),
|
|
347
|
+
run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
348
|
+
createdAt.toISOString(),
|
|
349
|
+
run.startedAt?.toISOString() ?? null,
|
|
350
|
+
run.finishedAt?.toISOString() ?? null
|
|
351
|
+
);
|
|
352
|
+
return {
|
|
353
|
+
...run,
|
|
354
|
+
id,
|
|
355
|
+
createdAt
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async getRun(runId) {
|
|
359
|
+
const row = this.stmts.getRun.get(runId);
|
|
360
|
+
return row ? this.mapRunRow(row) : null;
|
|
361
|
+
}
|
|
362
|
+
async updateRun(runId, updates) {
|
|
363
|
+
this.stmts.updateRun.run(
|
|
364
|
+
updates.status ?? null,
|
|
365
|
+
updates.context ? JSON.stringify(updates.context) : null,
|
|
366
|
+
updates.error ? JSON.stringify(sanitizeErrorForStorage(updates.error)) : null,
|
|
367
|
+
updates.startedAt?.toISOString() ?? null,
|
|
368
|
+
updates.finishedAt?.toISOString() ?? null,
|
|
369
|
+
runId
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
async listRuns(options = {}) {
|
|
373
|
+
const limit = options.limit ?? 50;
|
|
374
|
+
const offset = options.offset ?? 0;
|
|
375
|
+
const kind = options.kind ?? null;
|
|
376
|
+
const status = options.status ?? null;
|
|
377
|
+
const statusJson = status ? JSON.stringify(Array.isArray(status) ? status : [status]) : null;
|
|
378
|
+
const parentRunId = options.parentRunId ?? null;
|
|
379
|
+
const rows = this.stmts.listRuns.all(
|
|
380
|
+
kind,
|
|
381
|
+
kind,
|
|
382
|
+
statusJson,
|
|
383
|
+
statusJson,
|
|
384
|
+
parentRunId,
|
|
385
|
+
parentRunId,
|
|
386
|
+
limit,
|
|
387
|
+
offset
|
|
388
|
+
);
|
|
389
|
+
const countResult = this.stmts.countRuns.get(
|
|
390
|
+
kind,
|
|
391
|
+
kind,
|
|
392
|
+
statusJson,
|
|
393
|
+
statusJson,
|
|
394
|
+
parentRunId,
|
|
395
|
+
parentRunId
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
items: rows.map((row) => this.mapRunRow(row)),
|
|
399
|
+
total: countResult.count,
|
|
400
|
+
limit,
|
|
401
|
+
offset
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// Step Operations
|
|
406
|
+
// ============================================================================
|
|
407
|
+
async createStep(step) {
|
|
408
|
+
const id = generateId();
|
|
409
|
+
this.stmts.insertStep.run(
|
|
410
|
+
id,
|
|
411
|
+
step.runId,
|
|
412
|
+
step.stepKey,
|
|
413
|
+
step.stepName,
|
|
414
|
+
step.status,
|
|
415
|
+
step.attempt,
|
|
416
|
+
step.result !== void 0 ? JSON.stringify(step.result) : null,
|
|
417
|
+
step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
|
|
418
|
+
step.startedAt?.toISOString() ?? null,
|
|
419
|
+
step.finishedAt?.toISOString() ?? null
|
|
420
|
+
);
|
|
421
|
+
return { ...step, id };
|
|
422
|
+
}
|
|
423
|
+
async getStep(stepId) {
|
|
424
|
+
const row = this.stmts.getStep.get(stepId);
|
|
425
|
+
return row ? this.mapStepRow(row) : null;
|
|
426
|
+
}
|
|
427
|
+
async updateStep(stepId, updates) {
|
|
428
|
+
this.stmts.updateStep.run(
|
|
429
|
+
updates.status ?? null,
|
|
430
|
+
updates.attempt ?? null,
|
|
431
|
+
updates.result !== void 0 ? JSON.stringify(updates.result) : null,
|
|
432
|
+
updates.error ? JSON.stringify(sanitizeErrorForStorage(updates.error)) : null,
|
|
433
|
+
updates.finishedAt?.toISOString() ?? null,
|
|
434
|
+
stepId
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
async getStepsForRun(runId) {
|
|
438
|
+
const rows = this.stmts.getStepsForRun.all(runId);
|
|
439
|
+
return rows.map((row) => this.mapStepRow(row));
|
|
440
|
+
}
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// Event Operations
|
|
443
|
+
// ============================================================================
|
|
444
|
+
async saveEvent(event) {
|
|
445
|
+
const id = generateId();
|
|
446
|
+
this.stmts.insertEvent.run(
|
|
447
|
+
id,
|
|
448
|
+
event.runId,
|
|
449
|
+
event.stepKey ?? null,
|
|
450
|
+
event.eventType,
|
|
451
|
+
event.level,
|
|
452
|
+
event.payload !== void 0 ? JSON.stringify(event.payload) : null,
|
|
453
|
+
event.timestamp.toISOString()
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
async getEventsForRun(runId, options = {}) {
|
|
457
|
+
const limit = options.limit ?? 1e3;
|
|
458
|
+
const offset = options.offset ?? 0;
|
|
459
|
+
const stepKey = options.stepKey ?? null;
|
|
460
|
+
const level = options.level ?? null;
|
|
461
|
+
const rows = this.stmts.getEventsForRun.all(
|
|
462
|
+
runId,
|
|
463
|
+
stepKey,
|
|
464
|
+
stepKey,
|
|
465
|
+
level,
|
|
466
|
+
level,
|
|
467
|
+
limit,
|
|
468
|
+
offset
|
|
469
|
+
);
|
|
470
|
+
return rows.map((row) => this.mapEventRow(row));
|
|
471
|
+
}
|
|
472
|
+
// ============================================================================
|
|
473
|
+
// Transaction Support
|
|
474
|
+
// ============================================================================
|
|
475
|
+
/**
|
|
476
|
+
* Execute a function within a database transaction (async interface).
|
|
477
|
+
*
|
|
478
|
+
* @deprecated Use `transactionSync()` instead. This method only works when
|
|
479
|
+
* the callback performs purely synchronous operations wrapped in async/await.
|
|
480
|
+
* If the callback awaits real async I/O (network, timers, etc.), it will
|
|
481
|
+
* throw an error. `transactionSync()` makes the synchronous requirement explicit.
|
|
482
|
+
*/
|
|
483
|
+
async transaction(fn) {
|
|
484
|
+
let fnPromise;
|
|
485
|
+
this.transactionSync(() => {
|
|
486
|
+
fnPromise = fn(this);
|
|
487
|
+
});
|
|
488
|
+
return fnPromise;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Execute a synchronous transaction (preferred for better-sqlite3).
|
|
492
|
+
* Use this when you need transaction guarantees.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* ```typescript
|
|
496
|
+
* storage.transactionSync(() => {
|
|
497
|
+
* // Multiple operations in a single transaction
|
|
498
|
+
* const run = storage.createRunSync(...);
|
|
499
|
+
* storage.updateRunSync(run.id, ...);
|
|
500
|
+
* });
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
transactionSync(fn) {
|
|
504
|
+
return this.db.transaction(fn)();
|
|
505
|
+
}
|
|
506
|
+
// ============================================================================
|
|
507
|
+
// Cleanup Operations
|
|
508
|
+
// ============================================================================
|
|
509
|
+
/**
|
|
510
|
+
* Delete runs older than the specified date.
|
|
511
|
+
* Also deletes associated steps and events (via CASCADE).
|
|
512
|
+
*/
|
|
513
|
+
async deleteOldRuns(olderThan) {
|
|
514
|
+
const isoDate = olderThan.toISOString();
|
|
515
|
+
return this.db.transaction(() => {
|
|
516
|
+
this.stmts.deleteEventsForRuns.run(isoDate);
|
|
517
|
+
this.stmts.deleteStepsForRuns.run(isoDate);
|
|
518
|
+
const result = this.stmts.deleteOldRuns.run(isoDate);
|
|
519
|
+
return result.changes;
|
|
520
|
+
})();
|
|
521
|
+
}
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// Resume Support
|
|
524
|
+
// ============================================================================
|
|
525
|
+
/**
|
|
526
|
+
* Get all runs that were interrupted (status is 'running' or 'queued').
|
|
527
|
+
* These runs can potentially be resumed.
|
|
528
|
+
*/
|
|
529
|
+
async getInterruptedRuns() {
|
|
530
|
+
const rows = this.stmts.getInterruptedRuns.all();
|
|
531
|
+
return rows.map((row) => this.mapRunRow(row));
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Get the last completed step for a run.
|
|
535
|
+
* Useful for resuming from a checkpoint.
|
|
536
|
+
*/
|
|
537
|
+
async getLastCompletedStep(runId) {
|
|
538
|
+
const row = this.stmts.getLastCompletedStep.get(runId);
|
|
539
|
+
return row ? this.mapStepRow(row) : null;
|
|
540
|
+
}
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// Row Mapping
|
|
543
|
+
// ============================================================================
|
|
544
|
+
mapRunRow(row) {
|
|
545
|
+
return {
|
|
546
|
+
id: row.id,
|
|
547
|
+
kind: row.kind,
|
|
548
|
+
status: row.status,
|
|
549
|
+
parentRunId: row.parent_run_id ?? void 0,
|
|
550
|
+
input: JSON.parse(row.input_json),
|
|
551
|
+
metadata: JSON.parse(row.metadata_json),
|
|
552
|
+
context: JSON.parse(row.context_json),
|
|
553
|
+
error: row.error_json ? JSON.parse(row.error_json) : void 0,
|
|
554
|
+
createdAt: new Date(row.created_at),
|
|
555
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
556
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
mapStepRow(row) {
|
|
560
|
+
return {
|
|
561
|
+
id: row.id,
|
|
562
|
+
runId: row.run_id,
|
|
563
|
+
stepKey: row.step_key,
|
|
564
|
+
stepName: row.step_name,
|
|
565
|
+
status: row.status,
|
|
566
|
+
attempt: row.attempt,
|
|
567
|
+
result: row.result_json ? JSON.parse(row.result_json) : void 0,
|
|
568
|
+
error: row.error_json ? JSON.parse(row.error_json) : void 0,
|
|
569
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
570
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
mapEventRow(row) {
|
|
574
|
+
return {
|
|
575
|
+
id: row.id,
|
|
576
|
+
runId: row.run_id,
|
|
577
|
+
stepKey: row.step_key ?? void 0,
|
|
578
|
+
eventType: row.event_type,
|
|
579
|
+
level: row.level,
|
|
580
|
+
payload: row.payload_json ? JSON.parse(row.payload_json) : void 0,
|
|
581
|
+
timestamp: new Date(row.timestamp)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// ============================================================================
|
|
585
|
+
// Utility Methods
|
|
586
|
+
// ============================================================================
|
|
587
|
+
/**
|
|
588
|
+
* Close the database connection.
|
|
589
|
+
*/
|
|
590
|
+
close() {
|
|
591
|
+
this.db.close();
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Get database statistics.
|
|
595
|
+
*/
|
|
596
|
+
getStats() {
|
|
597
|
+
const runsCount = this.db.prepare("SELECT COUNT(*) as count FROM workflow_runs").get();
|
|
598
|
+
const stepsCount = this.db.prepare("SELECT COUNT(*) as count FROM workflow_run_steps").get();
|
|
599
|
+
const eventsCount = this.db.prepare("SELECT COUNT(*) as count FROM workflow_events").get();
|
|
600
|
+
return {
|
|
601
|
+
runs: runsCount.count,
|
|
602
|
+
steps: stepsCount.count,
|
|
603
|
+
events: eventsCount.count
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// src/storage/postgres.ts
|
|
609
|
+
var Kysely;
|
|
610
|
+
var PostgresDialect;
|
|
611
|
+
var sql;
|
|
612
|
+
var pgModule;
|
|
613
|
+
function stripStack(error) {
|
|
614
|
+
const { stack: _stack, ...rest } = error;
|
|
615
|
+
return rest;
|
|
616
|
+
}
|
|
617
|
+
var PostgresStorageAdapter = class {
|
|
618
|
+
db;
|
|
619
|
+
pool;
|
|
620
|
+
ownsPool = false;
|
|
621
|
+
schema;
|
|
622
|
+
autoMigrate;
|
|
623
|
+
initialized = false;
|
|
624
|
+
config;
|
|
625
|
+
constructor(config) {
|
|
626
|
+
this.schema = config.schema ?? "public";
|
|
627
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.schema)) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Invalid schema name "${this.schema}". Schema must start with a letter or underscore, contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
this.autoMigrate = config.autoMigrate !== false;
|
|
633
|
+
this.config = config;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Get a schema-scoped query builder.
|
|
637
|
+
* All queries MUST use this instead of this.db directly to respect config.schema.
|
|
638
|
+
*/
|
|
639
|
+
get qb() {
|
|
640
|
+
return this.db.withSchema(this.schema);
|
|
641
|
+
}
|
|
642
|
+
ensureInitialized() {
|
|
643
|
+
if (!this.initialized) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
"PostgresStorageAdapter is not initialized. Call initialize() before using the adapter."
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Initialize the storage adapter.
|
|
651
|
+
* Creates tables if autoMigrate is enabled.
|
|
652
|
+
*/
|
|
653
|
+
async initialize() {
|
|
654
|
+
if (this.initialized) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const deps = await loadPostgresDeps();
|
|
658
|
+
Kysely = deps.Kysely;
|
|
659
|
+
PostgresDialect = deps.PostgresDialect;
|
|
660
|
+
sql = deps.sql;
|
|
661
|
+
pgModule = deps.pgModule;
|
|
662
|
+
if (this.config.pool) {
|
|
663
|
+
this.pool = this.config.pool;
|
|
664
|
+
this.ownsPool = false;
|
|
665
|
+
} else if (this.config.connectionString) {
|
|
666
|
+
this.pool = new pgModule.Pool({
|
|
667
|
+
connectionString: this.config.connectionString
|
|
668
|
+
});
|
|
669
|
+
this.ownsPool = true;
|
|
670
|
+
} else if (this.config.poolConfig) {
|
|
671
|
+
this.pool = new pgModule.Pool(this.config.poolConfig);
|
|
672
|
+
this.ownsPool = true;
|
|
673
|
+
} else {
|
|
674
|
+
throw new Error(
|
|
675
|
+
"PostgresStorageConfig must include either pool, connectionString, or poolConfig"
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
this.db = new Kysely({
|
|
679
|
+
dialect: new PostgresDialect({
|
|
680
|
+
pool: this.pool
|
|
681
|
+
})
|
|
682
|
+
});
|
|
683
|
+
if (this.autoMigrate) {
|
|
684
|
+
await this.createTables();
|
|
685
|
+
}
|
|
686
|
+
this.initialized = true;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Close the database connection.
|
|
690
|
+
* Only closes the pool if it was created by this adapter.
|
|
691
|
+
*/
|
|
692
|
+
async close() {
|
|
693
|
+
await this.db.destroy();
|
|
694
|
+
}
|
|
695
|
+
// ============================================================================
|
|
696
|
+
// Schema Creation
|
|
697
|
+
// ============================================================================
|
|
698
|
+
/**
|
|
699
|
+
* Create the workflow tables if they don't exist.
|
|
700
|
+
*/
|
|
701
|
+
async createTables() {
|
|
702
|
+
if (this.schema !== "public") {
|
|
703
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(
|
|
704
|
+
this.db
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
await sql`
|
|
708
|
+
CREATE TABLE IF NOT EXISTS ${sql.table(`${this.schema}.runs`)} (
|
|
709
|
+
id TEXT PRIMARY KEY,
|
|
710
|
+
kind TEXT NOT NULL,
|
|
711
|
+
status TEXT NOT NULL,
|
|
712
|
+
parent_run_id TEXT,
|
|
713
|
+
input_json JSONB NOT NULL DEFAULT '{}',
|
|
714
|
+
metadata_json JSONB NOT NULL DEFAULT '{}',
|
|
715
|
+
context_json JSONB NOT NULL DEFAULT '{}',
|
|
716
|
+
output_json JSONB,
|
|
717
|
+
error_json JSONB,
|
|
718
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
719
|
+
timeout_ms INTEGER,
|
|
720
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
721
|
+
started_at TIMESTAMPTZ,
|
|
722
|
+
finished_at TIMESTAMPTZ,
|
|
723
|
+
CONSTRAINT runs_status_check CHECK (
|
|
724
|
+
status IN ('pending', 'queued', 'running', 'succeeded', 'failed', 'canceled', 'timeout')
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
`.execute(this.db);
|
|
728
|
+
await sql`
|
|
729
|
+
ALTER TABLE ${sql.table(`${this.schema}.runs`)}
|
|
730
|
+
ADD COLUMN IF NOT EXISTS output_json JSONB,
|
|
731
|
+
ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0,
|
|
732
|
+
ADD COLUMN IF NOT EXISTS timeout_ms INTEGER
|
|
733
|
+
`.execute(this.db).catch(() => {
|
|
734
|
+
});
|
|
735
|
+
await sql`
|
|
736
|
+
CREATE INDEX IF NOT EXISTS idx_runs_kind_status
|
|
737
|
+
ON ${sql.table(`${this.schema}.runs`)} (kind, status)
|
|
738
|
+
`.execute(this.db);
|
|
739
|
+
await sql`
|
|
740
|
+
CREATE INDEX IF NOT EXISTS idx_runs_parent
|
|
741
|
+
ON ${sql.table(`${this.schema}.runs`)} (parent_run_id)
|
|
742
|
+
`.execute(this.db);
|
|
743
|
+
await sql`
|
|
744
|
+
CREATE INDEX IF NOT EXISTS idx_runs_created
|
|
745
|
+
ON ${sql.table(`${this.schema}.runs`)} (created_at DESC)
|
|
746
|
+
`.execute(this.db);
|
|
747
|
+
await sql`
|
|
748
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status
|
|
749
|
+
ON ${sql.table(`${this.schema}.runs`)} (status)
|
|
750
|
+
`.execute(this.db);
|
|
751
|
+
await sql`
|
|
752
|
+
CREATE INDEX IF NOT EXISTS idx_runs_priority
|
|
753
|
+
ON ${sql.table(`${this.schema}.runs`)} (priority DESC, created_at ASC)
|
|
754
|
+
`.execute(this.db);
|
|
755
|
+
await sql`
|
|
756
|
+
CREATE TABLE IF NOT EXISTS ${sql.table(`${this.schema}.workflow_run_steps`)} (
|
|
757
|
+
id TEXT PRIMARY KEY,
|
|
758
|
+
run_id TEXT NOT NULL REFERENCES ${sql.table(`${this.schema}.runs`)} (id) ON DELETE CASCADE,
|
|
759
|
+
step_key TEXT NOT NULL,
|
|
760
|
+
step_name TEXT,
|
|
761
|
+
status TEXT NOT NULL,
|
|
762
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
763
|
+
result_json JSONB,
|
|
764
|
+
error_json JSONB,
|
|
765
|
+
started_at TIMESTAMPTZ,
|
|
766
|
+
finished_at TIMESTAMPTZ,
|
|
767
|
+
CONSTRAINT workflow_run_steps_status_check CHECK (
|
|
768
|
+
status IN ('pending', 'running', 'succeeded', 'failed', 'skipped', 'completed')
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
`.execute(this.db);
|
|
772
|
+
await sql`
|
|
773
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run
|
|
774
|
+
ON ${sql.table(`${this.schema}.workflow_run_steps`)} (run_id)
|
|
775
|
+
`.execute(this.db);
|
|
776
|
+
await sql`
|
|
777
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_run_steps_run_key
|
|
778
|
+
ON ${sql.table(`${this.schema}.workflow_run_steps`)} (run_id, step_key)
|
|
779
|
+
`.execute(this.db);
|
|
780
|
+
await sql`
|
|
781
|
+
CREATE TABLE IF NOT EXISTS ${sql.table(`${this.schema}.stepflow_step_results`)} (
|
|
782
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
783
|
+
run_id TEXT NOT NULL REFERENCES ${sql.table(`${this.schema}.runs`)} (id) ON DELETE CASCADE,
|
|
784
|
+
step_name TEXT NOT NULL,
|
|
785
|
+
status TEXT NOT NULL,
|
|
786
|
+
output_json JSONB,
|
|
787
|
+
error_json JSONB,
|
|
788
|
+
attempt INTEGER NOT NULL DEFAULT 1,
|
|
789
|
+
started_at TIMESTAMPTZ,
|
|
790
|
+
completed_at TIMESTAMPTZ,
|
|
791
|
+
CONSTRAINT stepflow_step_results_status_check CHECK (
|
|
792
|
+
status IN ('pending', 'running', 'completed', 'failed', 'skipped')
|
|
793
|
+
),
|
|
794
|
+
UNIQUE (run_id, step_name)
|
|
795
|
+
)
|
|
796
|
+
`.execute(this.db);
|
|
797
|
+
await sql`
|
|
798
|
+
CREATE INDEX IF NOT EXISTS idx_stepflow_step_results_run
|
|
799
|
+
ON ${sql.table(`${this.schema}.stepflow_step_results`)} (run_id)
|
|
800
|
+
`.execute(this.db);
|
|
801
|
+
await sql`
|
|
802
|
+
CREATE INDEX IF NOT EXISTS idx_stepflow_step_results_run_name
|
|
803
|
+
ON ${sql.table(`${this.schema}.stepflow_step_results`)} (run_id, step_name)
|
|
804
|
+
`.execute(this.db);
|
|
805
|
+
await sql`
|
|
806
|
+
CREATE TABLE IF NOT EXISTS ${sql.table(`${this.schema}.workflow_events`)} (
|
|
807
|
+
id TEXT PRIMARY KEY,
|
|
808
|
+
run_id TEXT NOT NULL REFERENCES ${sql.table(`${this.schema}.runs`)} (id) ON DELETE CASCADE,
|
|
809
|
+
step_key TEXT,
|
|
810
|
+
event_type TEXT NOT NULL,
|
|
811
|
+
level TEXT NOT NULL,
|
|
812
|
+
payload_json JSONB,
|
|
813
|
+
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
814
|
+
CONSTRAINT workflow_events_level_check CHECK (
|
|
815
|
+
level IN ('info', 'warn', 'error')
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
`.execute(this.db);
|
|
819
|
+
await sql`
|
|
820
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run
|
|
821
|
+
ON ${sql.table(`${this.schema}.workflow_events`)} (run_id)
|
|
822
|
+
`.execute(this.db);
|
|
823
|
+
await sql`
|
|
824
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_events_run_ts
|
|
825
|
+
ON ${sql.table(`${this.schema}.workflow_events`)} (run_id, timestamp)
|
|
826
|
+
`.execute(this.db);
|
|
827
|
+
}
|
|
828
|
+
// ============================================================================
|
|
829
|
+
// Run Operations
|
|
830
|
+
// ============================================================================
|
|
831
|
+
/**
|
|
832
|
+
* Create a new workflow run.
|
|
833
|
+
* Supports both legacy and new CreateRunInput interfaces.
|
|
834
|
+
*/
|
|
835
|
+
async createRun(run) {
|
|
836
|
+
this.ensureInitialized();
|
|
837
|
+
const id = "id" in run && run.id ? run.id : generateId();
|
|
838
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
839
|
+
await this.qb.insertInto("runs").values({
|
|
840
|
+
id,
|
|
841
|
+
kind: run.kind,
|
|
842
|
+
status: run.status,
|
|
843
|
+
parent_run_id: "parentRunId" in run ? run.parentRunId ?? null : null,
|
|
844
|
+
input_json: JSON.stringify(run.input),
|
|
845
|
+
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
846
|
+
context_json: JSON.stringify(run.context ?? {}),
|
|
847
|
+
// Default to empty object
|
|
848
|
+
output_json: null,
|
|
849
|
+
error_json: "error" in run && run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
850
|
+
priority: "priority" in run ? run.priority ?? 0 : 0,
|
|
851
|
+
timeout_ms: "timeoutMs" in run ? run.timeoutMs ?? null : null,
|
|
852
|
+
created_at: createdAt,
|
|
853
|
+
started_at: null,
|
|
854
|
+
finished_at: null
|
|
855
|
+
}).execute();
|
|
856
|
+
return {
|
|
857
|
+
id,
|
|
858
|
+
kind: run.kind,
|
|
859
|
+
status: run.status,
|
|
860
|
+
parentRunId: "parentRunId" in run ? run.parentRunId : void 0,
|
|
861
|
+
input: run.input,
|
|
862
|
+
context: run.context ?? {},
|
|
863
|
+
output: void 0,
|
|
864
|
+
error: void 0,
|
|
865
|
+
metadata: run.metadata ?? {},
|
|
866
|
+
priority: "priority" in run ? run.priority ?? 0 : 0,
|
|
867
|
+
timeoutMs: "timeoutMs" in run ? run.timeoutMs : void 0,
|
|
868
|
+
createdAt
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
async getRun(runId) {
|
|
872
|
+
this.ensureInitialized();
|
|
873
|
+
const row = await this.qb.selectFrom("runs").selectAll().where("id", "=", runId).executeTakeFirst();
|
|
874
|
+
return row ? this.mapRunRow(row) : null;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Update a workflow run.
|
|
878
|
+
* Supports both legacy Partial<WorkflowRunRecord> and new UpdateRunInput interfaces.
|
|
879
|
+
*/
|
|
880
|
+
async updateRun(runId, updates) {
|
|
881
|
+
this.ensureInitialized();
|
|
882
|
+
const updateData = {};
|
|
883
|
+
if (updates.status !== void 0) {
|
|
884
|
+
updateData.status = updates.status;
|
|
885
|
+
}
|
|
886
|
+
if (updates.context !== void 0) {
|
|
887
|
+
updateData.context_json = JSON.stringify(updates.context);
|
|
888
|
+
}
|
|
889
|
+
if ("output" in updates && updates.output !== void 0) {
|
|
890
|
+
updateData.output_json = JSON.stringify(updates.output);
|
|
891
|
+
}
|
|
892
|
+
if (updates.error !== void 0) {
|
|
893
|
+
updateData.error_json = JSON.stringify(
|
|
894
|
+
sanitizeErrorForStorage(updates.error)
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (updates.startedAt !== void 0) {
|
|
898
|
+
updateData.started_at = updates.startedAt;
|
|
899
|
+
}
|
|
900
|
+
if (updates.finishedAt !== void 0) {
|
|
901
|
+
updateData.finished_at = updates.finishedAt;
|
|
902
|
+
}
|
|
903
|
+
if (Object.keys(updateData).length > 0) {
|
|
904
|
+
await this.qb.updateTable("runs").set(updateData).where("id", "=", runId).execute();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* List workflow runs with filtering and pagination.
|
|
909
|
+
*/
|
|
910
|
+
/**
|
|
911
|
+
* Apply common run filters to a Kysely query builder.
|
|
912
|
+
* Used by both the data query and count query in listRuns to avoid duplication.
|
|
913
|
+
*/
|
|
914
|
+
applyRunsFilters(query, options) {
|
|
915
|
+
if (options.kind) {
|
|
916
|
+
query = query.where("kind", "=", options.kind);
|
|
917
|
+
}
|
|
918
|
+
if (options.status) {
|
|
919
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
920
|
+
query = query.where("status", "in", statuses);
|
|
921
|
+
}
|
|
922
|
+
if (options.parentRunId !== void 0) {
|
|
923
|
+
query = query.where("parent_run_id", "=", options.parentRunId);
|
|
924
|
+
}
|
|
925
|
+
return query;
|
|
926
|
+
}
|
|
927
|
+
async listRuns(options = {}) {
|
|
928
|
+
this.ensureInitialized();
|
|
929
|
+
const limit = options.limit ?? 50;
|
|
930
|
+
const offset = options.offset ?? 0;
|
|
931
|
+
let query = this.applyRunsFilters(
|
|
932
|
+
this.qb.selectFrom("runs").selectAll(),
|
|
933
|
+
options
|
|
934
|
+
);
|
|
935
|
+
const countQuery = this.applyRunsFilters(
|
|
936
|
+
this.qb.selectFrom("runs").select(sql`count(*)`.as("count")),
|
|
937
|
+
options
|
|
938
|
+
);
|
|
939
|
+
const countResult = await countQuery.executeTakeFirst();
|
|
940
|
+
const total = Number(countResult?.count ?? 0);
|
|
941
|
+
const orderBy = options.orderBy ?? "createdAt";
|
|
942
|
+
const orderDirection = options.orderDirection ?? "desc";
|
|
943
|
+
const orderColumn = orderBy === "createdAt" ? "created_at" : orderBy === "startedAt" ? "started_at" : "finished_at";
|
|
944
|
+
query = query.orderBy(orderColumn, orderDirection);
|
|
945
|
+
query = query.limit(limit).offset(offset);
|
|
946
|
+
const rows = await query.execute();
|
|
947
|
+
return {
|
|
948
|
+
items: rows.map((row) => this.mapRunRow(row)),
|
|
949
|
+
total
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
// ============================================================================
|
|
953
|
+
// Step Operations
|
|
954
|
+
// ============================================================================
|
|
955
|
+
async createStep(step) {
|
|
956
|
+
this.ensureInitialized();
|
|
957
|
+
const id = generateId();
|
|
958
|
+
await this.qb.insertInto("workflow_run_steps").values({
|
|
959
|
+
id,
|
|
960
|
+
run_id: step.runId,
|
|
961
|
+
step_key: step.stepKey,
|
|
962
|
+
step_name: step.stepName,
|
|
963
|
+
status: step.status,
|
|
964
|
+
attempt: step.attempt,
|
|
965
|
+
result_json: step.result !== void 0 ? JSON.stringify(step.result) : null,
|
|
966
|
+
error_json: step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
|
|
967
|
+
started_at: step.startedAt ?? null,
|
|
968
|
+
finished_at: step.finishedAt ?? null
|
|
969
|
+
}).execute();
|
|
970
|
+
return { ...step, id };
|
|
971
|
+
}
|
|
972
|
+
async getStep(stepId) {
|
|
973
|
+
this.ensureInitialized();
|
|
974
|
+
const row = await this.qb.selectFrom("workflow_run_steps").selectAll().where("id", "=", stepId).executeTakeFirst();
|
|
975
|
+
return row ? this.mapStepRow(row) : null;
|
|
976
|
+
}
|
|
977
|
+
async updateStep(stepId, updates) {
|
|
978
|
+
this.ensureInitialized();
|
|
979
|
+
const updateData = {};
|
|
980
|
+
if (updates.status !== void 0) {
|
|
981
|
+
updateData.status = updates.status;
|
|
982
|
+
}
|
|
983
|
+
if (updates.attempt !== void 0) {
|
|
984
|
+
updateData.attempt = updates.attempt;
|
|
985
|
+
}
|
|
986
|
+
if (updates.result !== void 0) {
|
|
987
|
+
updateData.result_json = JSON.stringify(updates.result);
|
|
988
|
+
}
|
|
989
|
+
if (updates.error !== void 0) {
|
|
990
|
+
updateData.error_json = JSON.stringify(
|
|
991
|
+
sanitizeErrorForStorage(updates.error)
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (updates.finishedAt !== void 0) {
|
|
995
|
+
updateData.finished_at = updates.finishedAt;
|
|
996
|
+
}
|
|
997
|
+
if (Object.keys(updateData).length > 0) {
|
|
998
|
+
await this.qb.updateTable("workflow_run_steps").set(updateData).where("id", "=", stepId).execute();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async getStepsForRun(runId) {
|
|
1002
|
+
this.ensureInitialized();
|
|
1003
|
+
const rows = await this.qb.selectFrom("workflow_run_steps").selectAll().where("run_id", "=", runId).orderBy("started_at", "asc").execute();
|
|
1004
|
+
return rows.map((row) => this.mapStepRow(row));
|
|
1005
|
+
}
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
// Event Operations
|
|
1008
|
+
// ============================================================================
|
|
1009
|
+
async saveEvent(event) {
|
|
1010
|
+
this.ensureInitialized();
|
|
1011
|
+
const id = generateId();
|
|
1012
|
+
await this.qb.insertInto("workflow_events").values({
|
|
1013
|
+
id,
|
|
1014
|
+
run_id: event.runId,
|
|
1015
|
+
step_key: event.stepKey ?? null,
|
|
1016
|
+
event_type: event.eventType,
|
|
1017
|
+
level: event.level,
|
|
1018
|
+
payload_json: event.payload !== void 0 ? JSON.stringify(event.payload) : null,
|
|
1019
|
+
timestamp: event.timestamp
|
|
1020
|
+
}).execute();
|
|
1021
|
+
}
|
|
1022
|
+
async getEventsForRun(runId, options = {}) {
|
|
1023
|
+
this.ensureInitialized();
|
|
1024
|
+
const limit = options.limit ?? 1e3;
|
|
1025
|
+
const offset = options.offset ?? 0;
|
|
1026
|
+
let query = this.qb.selectFrom("workflow_events").selectAll().where("run_id", "=", runId);
|
|
1027
|
+
if (options.stepKey) {
|
|
1028
|
+
query = query.where("step_key", "=", options.stepKey);
|
|
1029
|
+
}
|
|
1030
|
+
if (options.level) {
|
|
1031
|
+
query = query.where("level", "=", options.level);
|
|
1032
|
+
}
|
|
1033
|
+
query = query.orderBy("timestamp", "asc").limit(limit).offset(offset);
|
|
1034
|
+
const rows = await query.execute();
|
|
1035
|
+
return rows.map((row) => this.mapEventRow(row));
|
|
1036
|
+
}
|
|
1037
|
+
// ============================================================================
|
|
1038
|
+
// Transaction Support
|
|
1039
|
+
// ============================================================================
|
|
1040
|
+
/**
|
|
1041
|
+
* Execute a function within a database transaction.
|
|
1042
|
+
*/
|
|
1043
|
+
async transaction(fn) {
|
|
1044
|
+
this.ensureInitialized();
|
|
1045
|
+
return await this.db.transaction().execute(async (trx) => {
|
|
1046
|
+
const txAdapter = new PostgresTransactionAdapter(trx, this.schema);
|
|
1047
|
+
return await fn(txAdapter);
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
// ============================================================================
|
|
1051
|
+
// Cleanup Operations
|
|
1052
|
+
// ============================================================================
|
|
1053
|
+
/**
|
|
1054
|
+
* Delete runs older than the specified date.
|
|
1055
|
+
* Also deletes associated steps and events (via CASCADE).
|
|
1056
|
+
*/
|
|
1057
|
+
async deleteOldRuns(olderThan) {
|
|
1058
|
+
this.ensureInitialized();
|
|
1059
|
+
const result = await this.qb.deleteFrom("runs").where("created_at", "<", olderThan).executeTakeFirst();
|
|
1060
|
+
return Number(result.numDeletedRows);
|
|
1061
|
+
}
|
|
1062
|
+
// ============================================================================
|
|
1063
|
+
// Resume Support
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
/**
|
|
1066
|
+
* Get all runs that were interrupted (status is 'running' or 'queued').
|
|
1067
|
+
* These runs can potentially be resumed.
|
|
1068
|
+
*/
|
|
1069
|
+
async getInterruptedRuns() {
|
|
1070
|
+
this.ensureInitialized();
|
|
1071
|
+
const rows = await this.qb.selectFrom("runs").selectAll().where("status", "in", ["queued", "running"]).orderBy("created_at", "asc").execute();
|
|
1072
|
+
return rows.map((row) => this.mapRunRow(row));
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Get the last completed step for a run.
|
|
1076
|
+
* Useful for resuming from a checkpoint.
|
|
1077
|
+
*/
|
|
1078
|
+
async getLastCompletedStep(runId) {
|
|
1079
|
+
this.ensureInitialized();
|
|
1080
|
+
const row = await this.qb.selectFrom("workflow_run_steps").selectAll().where("run_id", "=", runId).where("status", "=", "succeeded").orderBy("finished_at", "desc").limit(1).executeTakeFirst();
|
|
1081
|
+
return row ? this.mapStepRow(row) : null;
|
|
1082
|
+
}
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
// Atomic Dequeue (for distributed workers)
|
|
1085
|
+
// ============================================================================
|
|
1086
|
+
/**
|
|
1087
|
+
* Atomically dequeue a run for processing.
|
|
1088
|
+
* Uses FOR UPDATE SKIP LOCKED for safe concurrent access.
|
|
1089
|
+
*
|
|
1090
|
+
* @param workflowKinds - Optional list of workflow kinds to filter by
|
|
1091
|
+
* @returns The dequeued run, or null if no runs are available
|
|
1092
|
+
*/
|
|
1093
|
+
async dequeueRun(workflowKinds) {
|
|
1094
|
+
this.ensureInitialized();
|
|
1095
|
+
const kindFilter = workflowKinds && workflowKinds.length > 0 ? sql`AND kind = ANY(${workflowKinds}::text[])` : sql``;
|
|
1096
|
+
const result = await sql`
|
|
1097
|
+
UPDATE ${sql.table(`${this.schema}.runs`)}
|
|
1098
|
+
SET status = 'running', started_at = NOW()
|
|
1099
|
+
WHERE id = (
|
|
1100
|
+
SELECT id FROM ${sql.table(`${this.schema}.runs`)}
|
|
1101
|
+
WHERE status = 'queued' ${kindFilter}
|
|
1102
|
+
ORDER BY priority DESC, created_at ASC
|
|
1103
|
+
LIMIT 1
|
|
1104
|
+
FOR UPDATE SKIP LOCKED
|
|
1105
|
+
)
|
|
1106
|
+
RETURNING *
|
|
1107
|
+
`.execute(this.db);
|
|
1108
|
+
const row = result.rows[0];
|
|
1109
|
+
return row ? this.mapRunRow(row) : null;
|
|
1110
|
+
}
|
|
1111
|
+
// ============================================================================
|
|
1112
|
+
// Row Mapping
|
|
1113
|
+
// ============================================================================
|
|
1114
|
+
mapRunRow(row) {
|
|
1115
|
+
return {
|
|
1116
|
+
id: row.id,
|
|
1117
|
+
kind: row.kind,
|
|
1118
|
+
status: row.status,
|
|
1119
|
+
parentRunId: row.parent_run_id ?? void 0,
|
|
1120
|
+
input: typeof row.input_json === "string" ? JSON.parse(row.input_json) : row.input_json,
|
|
1121
|
+
context: typeof row.context_json === "string" ? JSON.parse(row.context_json) : row.context_json,
|
|
1122
|
+
output: row.output_json ? typeof row.output_json === "string" ? JSON.parse(row.output_json) : row.output_json : void 0,
|
|
1123
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1124
|
+
metadata: typeof row.metadata_json === "string" ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
1125
|
+
priority: row.priority ?? 0,
|
|
1126
|
+
timeoutMs: row.timeout_ms ?? void 0,
|
|
1127
|
+
createdAt: new Date(row.created_at),
|
|
1128
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1129
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Map a database row to an extended workflow run record.
|
|
1134
|
+
*/
|
|
1135
|
+
mapExtendedRunRow(row) {
|
|
1136
|
+
return {
|
|
1137
|
+
id: row.id,
|
|
1138
|
+
kind: row.kind,
|
|
1139
|
+
status: row.status,
|
|
1140
|
+
input: typeof row.input_json === "string" ? JSON.parse(row.input_json) : row.input_json,
|
|
1141
|
+
metadata: typeof row.metadata_json === "string" ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
1142
|
+
context: typeof row.context_json === "string" ? JSON.parse(row.context_json) : row.context_json,
|
|
1143
|
+
output: row.output_json ? typeof row.output_json === "string" ? JSON.parse(row.output_json) : row.output_json : void 0,
|
|
1144
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1145
|
+
priority: row.priority ?? 0,
|
|
1146
|
+
timeoutMs: row.timeout_ms ?? void 0,
|
|
1147
|
+
createdAt: new Date(row.created_at),
|
|
1148
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1149
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
mapStepResultRow(row) {
|
|
1153
|
+
return {
|
|
1154
|
+
id: row.id,
|
|
1155
|
+
runId: row.run_id,
|
|
1156
|
+
stepName: row.step_name,
|
|
1157
|
+
status: row.status,
|
|
1158
|
+
output: row.output_json ? typeof row.output_json === "string" ? JSON.parse(row.output_json) : row.output_json : void 0,
|
|
1159
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1160
|
+
attempt: row.attempt,
|
|
1161
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1162
|
+
completedAt: row.completed_at ? new Date(row.completed_at) : void 0
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
mapStepRow(row) {
|
|
1166
|
+
return {
|
|
1167
|
+
id: row.id,
|
|
1168
|
+
runId: row.run_id,
|
|
1169
|
+
stepKey: row.step_key,
|
|
1170
|
+
stepName: row.step_name,
|
|
1171
|
+
status: row.status,
|
|
1172
|
+
attempt: row.attempt,
|
|
1173
|
+
result: row.result_json ? typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json : void 0,
|
|
1174
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1175
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1176
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
mapEventRow(row) {
|
|
1180
|
+
return {
|
|
1181
|
+
id: row.id,
|
|
1182
|
+
runId: row.run_id,
|
|
1183
|
+
stepKey: row.step_key ?? void 0,
|
|
1184
|
+
eventType: row.event_type,
|
|
1185
|
+
level: row.level,
|
|
1186
|
+
payload: row.payload_json ? typeof row.payload_json === "string" ? JSON.parse(row.payload_json) : row.payload_json : void 0,
|
|
1187
|
+
timestamp: new Date(row.timestamp)
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
// ============================================================================
|
|
1191
|
+
// New WorkflowStorage Methods
|
|
1192
|
+
// ============================================================================
|
|
1193
|
+
/**
|
|
1194
|
+
* Delete a workflow run by ID.
|
|
1195
|
+
* Also deletes associated steps and events (via CASCADE).
|
|
1196
|
+
*/
|
|
1197
|
+
async deleteRun(id) {
|
|
1198
|
+
this.ensureInitialized();
|
|
1199
|
+
await this.qb.deleteFrom("runs").where("id", "=", id).execute();
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Cleanup stale runs that have exceeded their timeout.
|
|
1203
|
+
* Marks them as 'timeout' status with an appropriate error.
|
|
1204
|
+
*
|
|
1205
|
+
* @param defaultTimeoutMs - Default timeout in ms for runs without explicit timeout (default: 600000 = 10 minutes)
|
|
1206
|
+
* @returns Number of runs marked as timed out
|
|
1207
|
+
*/
|
|
1208
|
+
async cleanupStaleRuns(defaultTimeoutMs = 6e5) {
|
|
1209
|
+
this.ensureInitialized();
|
|
1210
|
+
const result = await sql`
|
|
1211
|
+
UPDATE ${sql.table(`${this.schema}.runs`)}
|
|
1212
|
+
SET
|
|
1213
|
+
status = 'timeout',
|
|
1214
|
+
error_json = jsonb_build_object(
|
|
1215
|
+
'code', 'WORKFLOW_TIMEOUT',
|
|
1216
|
+
'message', 'Workflow exceeded maximum execution time and was marked as timed out'
|
|
1217
|
+
),
|
|
1218
|
+
finished_at = NOW()
|
|
1219
|
+
WHERE status = 'running'
|
|
1220
|
+
AND started_at IS NOT NULL
|
|
1221
|
+
AND (
|
|
1222
|
+
(timeout_ms IS NOT NULL AND started_at < NOW() - (timeout_ms || ' milliseconds')::interval)
|
|
1223
|
+
OR
|
|
1224
|
+
(timeout_ms IS NULL AND started_at < NOW() - (${defaultTimeoutMs} || ' milliseconds')::interval)
|
|
1225
|
+
)
|
|
1226
|
+
RETURNING id
|
|
1227
|
+
`.execute(this.db);
|
|
1228
|
+
return result.rows.length;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Mark multiple runs as failed with a given reason.
|
|
1232
|
+
* Useful for cleanup when a worker shuts down unexpectedly.
|
|
1233
|
+
*
|
|
1234
|
+
* @param runIds - Array of run IDs to mark as failed
|
|
1235
|
+
* @param reason - Reason message for the failure
|
|
1236
|
+
*/
|
|
1237
|
+
async markRunsAsFailed(runIds, reason) {
|
|
1238
|
+
this.ensureInitialized();
|
|
1239
|
+
if (runIds.length === 0) return;
|
|
1240
|
+
await sql`
|
|
1241
|
+
UPDATE ${sql.table(`${this.schema}.runs`)}
|
|
1242
|
+
SET
|
|
1243
|
+
status = 'failed',
|
|
1244
|
+
error_json = jsonb_build_object(
|
|
1245
|
+
'code', 'WORKER_SHUTDOWN',
|
|
1246
|
+
'message', ${reason}
|
|
1247
|
+
),
|
|
1248
|
+
finished_at = NOW()
|
|
1249
|
+
WHERE id = ANY(${runIds})
|
|
1250
|
+
AND status = 'running'
|
|
1251
|
+
`.execute(this.db);
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Get a specific step result by run ID and step name.
|
|
1255
|
+
*/
|
|
1256
|
+
async getStepResult(runId, stepName) {
|
|
1257
|
+
this.ensureInitialized();
|
|
1258
|
+
const row = await this.qb.selectFrom("stepflow_step_results").selectAll().where("run_id", "=", runId).where("step_name", "=", stepName).executeTakeFirst();
|
|
1259
|
+
return row ? this.mapStepResultRow(row) : void 0;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Get all step results for a run.
|
|
1263
|
+
*/
|
|
1264
|
+
async getStepResults(runId) {
|
|
1265
|
+
this.ensureInitialized();
|
|
1266
|
+
const rows = await this.qb.selectFrom("stepflow_step_results").selectAll().where("run_id", "=", runId).orderBy("started_at", "asc").execute();
|
|
1267
|
+
return rows.map((row) => this.mapStepResultRow(row));
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* Save or update a step result.
|
|
1271
|
+
* Uses upsert to handle both new and existing results.
|
|
1272
|
+
*/
|
|
1273
|
+
async saveStepResult(result) {
|
|
1274
|
+
this.ensureInitialized();
|
|
1275
|
+
const id = result.id ?? generateId();
|
|
1276
|
+
await this.qb.insertInto("stepflow_step_results").values({
|
|
1277
|
+
id,
|
|
1278
|
+
run_id: result.runId,
|
|
1279
|
+
step_name: result.stepName,
|
|
1280
|
+
status: result.status,
|
|
1281
|
+
output_json: result.output ? JSON.stringify(result.output) : null,
|
|
1282
|
+
error_json: result.error ? JSON.stringify(stripStack(result.error)) : null,
|
|
1283
|
+
attempt: result.attempt,
|
|
1284
|
+
started_at: result.startedAt ?? null,
|
|
1285
|
+
completed_at: result.completedAt ?? null
|
|
1286
|
+
}).onConflict(
|
|
1287
|
+
(oc) => oc.columns(["run_id", "step_name"]).doUpdateSet({
|
|
1288
|
+
status: result.status,
|
|
1289
|
+
output_json: result.output ? JSON.stringify(result.output) : null,
|
|
1290
|
+
error_json: result.error ? JSON.stringify(stripStack(result.error)) : null,
|
|
1291
|
+
attempt: result.attempt,
|
|
1292
|
+
started_at: result.startedAt ?? null,
|
|
1293
|
+
completed_at: result.completedAt ?? null
|
|
1294
|
+
})
|
|
1295
|
+
).execute();
|
|
1296
|
+
}
|
|
1297
|
+
// ============================================================================
|
|
1298
|
+
// Utility Methods
|
|
1299
|
+
// ============================================================================
|
|
1300
|
+
/**
|
|
1301
|
+
* Get database statistics.
|
|
1302
|
+
*/
|
|
1303
|
+
async getStats() {
|
|
1304
|
+
this.ensureInitialized();
|
|
1305
|
+
const [runsCount, stepsCount, eventsCount] = await Promise.all([
|
|
1306
|
+
this.qb.selectFrom("runs").select(sql`count(*)`.as("count")).executeTakeFirst(),
|
|
1307
|
+
this.qb.selectFrom("workflow_run_steps").select(sql`count(*)`.as("count")).executeTakeFirst(),
|
|
1308
|
+
this.qb.selectFrom("workflow_events").select(sql`count(*)`.as("count")).executeTakeFirst()
|
|
1309
|
+
]);
|
|
1310
|
+
return {
|
|
1311
|
+
runs: Number(runsCount?.count ?? 0),
|
|
1312
|
+
steps: Number(stepsCount?.count ?? 0),
|
|
1313
|
+
events: Number(eventsCount?.count ?? 0)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
var PostgresTransactionAdapter = class {
|
|
1318
|
+
constructor(trx, schema) {
|
|
1319
|
+
this.trx = trx;
|
|
1320
|
+
this.schema = schema;
|
|
1321
|
+
this.qb = trx.withSchema(schema);
|
|
1322
|
+
}
|
|
1323
|
+
qb;
|
|
1324
|
+
async createRun(run) {
|
|
1325
|
+
const id = "id" in run && run.id ? run.id : generateId();
|
|
1326
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
1327
|
+
await this.qb.insertInto("runs").values({
|
|
1328
|
+
id,
|
|
1329
|
+
kind: run.kind,
|
|
1330
|
+
status: run.status,
|
|
1331
|
+
parent_run_id: "parentRunId" in run ? run.parentRunId ?? null : null,
|
|
1332
|
+
input_json: JSON.stringify(run.input),
|
|
1333
|
+
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
1334
|
+
context_json: JSON.stringify(run.context ?? {}),
|
|
1335
|
+
output_json: null,
|
|
1336
|
+
error_json: "error" in run && run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
1337
|
+
priority: "priority" in run ? run.priority ?? 0 : 0,
|
|
1338
|
+
timeout_ms: "timeoutMs" in run ? run.timeoutMs ?? null : null,
|
|
1339
|
+
created_at: createdAt,
|
|
1340
|
+
started_at: null,
|
|
1341
|
+
finished_at: null
|
|
1342
|
+
}).execute();
|
|
1343
|
+
return {
|
|
1344
|
+
id,
|
|
1345
|
+
kind: run.kind,
|
|
1346
|
+
status: run.status,
|
|
1347
|
+
parentRunId: "parentRunId" in run ? run.parentRunId : void 0,
|
|
1348
|
+
input: run.input,
|
|
1349
|
+
context: run.context ?? {},
|
|
1350
|
+
output: void 0,
|
|
1351
|
+
error: void 0,
|
|
1352
|
+
metadata: run.metadata ?? {},
|
|
1353
|
+
priority: "priority" in run ? run.priority ?? 0 : 0,
|
|
1354
|
+
timeoutMs: "timeoutMs" in run ? run.timeoutMs : void 0,
|
|
1355
|
+
createdAt
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
async getRun(runId) {
|
|
1359
|
+
const row = await this.qb.selectFrom("runs").selectAll().where("id", "=", runId).executeTakeFirst();
|
|
1360
|
+
return row ? this.mapRunRow(row) : null;
|
|
1361
|
+
}
|
|
1362
|
+
async updateRun(runId, updates) {
|
|
1363
|
+
const updateData = {};
|
|
1364
|
+
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1365
|
+
if (updates.context !== void 0)
|
|
1366
|
+
updateData.context_json = JSON.stringify(updates.context);
|
|
1367
|
+
if ("output" in updates && updates.output !== void 0)
|
|
1368
|
+
updateData.output_json = JSON.stringify(updates.output);
|
|
1369
|
+
if (updates.error !== void 0)
|
|
1370
|
+
updateData.error_json = JSON.stringify(
|
|
1371
|
+
sanitizeErrorForStorage(updates.error)
|
|
1372
|
+
);
|
|
1373
|
+
if (updates.startedAt !== void 0)
|
|
1374
|
+
updateData.started_at = updates.startedAt;
|
|
1375
|
+
if (updates.finishedAt !== void 0)
|
|
1376
|
+
updateData.finished_at = updates.finishedAt;
|
|
1377
|
+
if (Object.keys(updateData).length > 0) {
|
|
1378
|
+
await this.qb.updateTable("runs").set(updateData).where("id", "=", runId).execute();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
async listRuns(options = {}) {
|
|
1382
|
+
const limit = options.limit ?? 50;
|
|
1383
|
+
const offset = options.offset ?? 0;
|
|
1384
|
+
let query = this.qb.selectFrom("runs").selectAll();
|
|
1385
|
+
if (options.kind) query = query.where("kind", "=", options.kind);
|
|
1386
|
+
if (options.status) {
|
|
1387
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1388
|
+
query = query.where("status", "in", statuses);
|
|
1389
|
+
}
|
|
1390
|
+
if (options.parentRunId !== void 0) {
|
|
1391
|
+
query = query.where("parent_run_id", "=", options.parentRunId);
|
|
1392
|
+
}
|
|
1393
|
+
const orderColumn = (options.orderBy ?? "createdAt") === "createdAt" ? "created_at" : options.orderBy === "startedAt" ? "started_at" : "finished_at";
|
|
1394
|
+
const orderDirection = options.orderDirection ?? "desc";
|
|
1395
|
+
query = query.orderBy(orderColumn, orderDirection).limit(limit).offset(offset);
|
|
1396
|
+
const rows = await query.execute();
|
|
1397
|
+
return {
|
|
1398
|
+
items: rows.map((row) => this.mapRunRow(row)),
|
|
1399
|
+
total: rows.length
|
|
1400
|
+
// Simplified for transaction context
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
async createStep(step) {
|
|
1404
|
+
const id = generateId();
|
|
1405
|
+
await this.qb.insertInto("workflow_run_steps").values({
|
|
1406
|
+
id,
|
|
1407
|
+
run_id: step.runId,
|
|
1408
|
+
step_key: step.stepKey,
|
|
1409
|
+
step_name: step.stepName,
|
|
1410
|
+
status: step.status,
|
|
1411
|
+
attempt: step.attempt,
|
|
1412
|
+
result_json: step.result !== void 0 ? JSON.stringify(step.result) : null,
|
|
1413
|
+
error_json: step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
|
|
1414
|
+
started_at: step.startedAt ?? null,
|
|
1415
|
+
finished_at: step.finishedAt ?? null
|
|
1416
|
+
}).execute();
|
|
1417
|
+
return { ...step, id };
|
|
1418
|
+
}
|
|
1419
|
+
async getStep(stepId) {
|
|
1420
|
+
const row = await this.qb.selectFrom("workflow_run_steps").selectAll().where("id", "=", stepId).executeTakeFirst();
|
|
1421
|
+
return row ? this.mapStepRow(row) : null;
|
|
1422
|
+
}
|
|
1423
|
+
async updateStep(stepId, updates) {
|
|
1424
|
+
const updateData = {};
|
|
1425
|
+
if (updates.status !== void 0) updateData.status = updates.status;
|
|
1426
|
+
if (updates.attempt !== void 0) updateData.attempt = updates.attempt;
|
|
1427
|
+
if (updates.result !== void 0)
|
|
1428
|
+
updateData.result_json = JSON.stringify(updates.result);
|
|
1429
|
+
if (updates.error !== void 0)
|
|
1430
|
+
updateData.error_json = JSON.stringify(
|
|
1431
|
+
sanitizeErrorForStorage(updates.error)
|
|
1432
|
+
);
|
|
1433
|
+
if (updates.finishedAt !== void 0)
|
|
1434
|
+
updateData.finished_at = updates.finishedAt;
|
|
1435
|
+
if (Object.keys(updateData).length > 0) {
|
|
1436
|
+
await this.qb.updateTable("workflow_run_steps").set(updateData).where("id", "=", stepId).execute();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
async getStepsForRun(runId) {
|
|
1440
|
+
const rows = await this.qb.selectFrom("workflow_run_steps").selectAll().where("run_id", "=", runId).orderBy("started_at", "asc").execute();
|
|
1441
|
+
return rows.map((row) => this.mapStepRow(row));
|
|
1442
|
+
}
|
|
1443
|
+
async saveEvent(event) {
|
|
1444
|
+
const id = generateId();
|
|
1445
|
+
await this.qb.insertInto("workflow_events").values({
|
|
1446
|
+
id,
|
|
1447
|
+
run_id: event.runId,
|
|
1448
|
+
step_key: event.stepKey ?? null,
|
|
1449
|
+
event_type: event.eventType,
|
|
1450
|
+
level: event.level,
|
|
1451
|
+
payload_json: event.payload !== void 0 ? JSON.stringify(event.payload) : null,
|
|
1452
|
+
timestamp: event.timestamp
|
|
1453
|
+
}).execute();
|
|
1454
|
+
}
|
|
1455
|
+
async getEventsForRun(runId, options = {}) {
|
|
1456
|
+
let query = this.qb.selectFrom("workflow_events").selectAll().where("run_id", "=", runId);
|
|
1457
|
+
if (options.stepKey) query = query.where("step_key", "=", options.stepKey);
|
|
1458
|
+
if (options.level) query = query.where("level", "=", options.level);
|
|
1459
|
+
const rows = await query.orderBy("timestamp", "asc").limit(options.limit ?? 1e3).offset(options.offset ?? 0).execute();
|
|
1460
|
+
return rows.map((row) => this.mapEventRow(row));
|
|
1461
|
+
}
|
|
1462
|
+
mapRunRow(row) {
|
|
1463
|
+
return {
|
|
1464
|
+
id: row.id,
|
|
1465
|
+
kind: row.kind,
|
|
1466
|
+
status: row.status,
|
|
1467
|
+
parentRunId: row.parent_run_id ?? void 0,
|
|
1468
|
+
input: typeof row.input_json === "string" ? JSON.parse(row.input_json) : row.input_json,
|
|
1469
|
+
context: typeof row.context_json === "string" ? JSON.parse(row.context_json) : row.context_json,
|
|
1470
|
+
output: row.output_json ? typeof row.output_json === "string" ? JSON.parse(row.output_json) : row.output_json : void 0,
|
|
1471
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1472
|
+
metadata: typeof row.metadata_json === "string" ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
1473
|
+
priority: row.priority ?? 0,
|
|
1474
|
+
timeoutMs: row.timeout_ms ?? void 0,
|
|
1475
|
+
createdAt: new Date(row.created_at),
|
|
1476
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1477
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
mapStepRow(row) {
|
|
1481
|
+
return {
|
|
1482
|
+
id: row.id,
|
|
1483
|
+
runId: row.run_id,
|
|
1484
|
+
stepKey: row.step_key,
|
|
1485
|
+
stepName: row.step_name,
|
|
1486
|
+
status: row.status,
|
|
1487
|
+
attempt: row.attempt,
|
|
1488
|
+
result: row.result_json ? typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json : void 0,
|
|
1489
|
+
error: row.error_json ? typeof row.error_json === "string" ? JSON.parse(row.error_json) : row.error_json : void 0,
|
|
1490
|
+
startedAt: row.started_at ? new Date(row.started_at) : void 0,
|
|
1491
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : void 0
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
mapEventRow(row) {
|
|
1495
|
+
return {
|
|
1496
|
+
id: row.id,
|
|
1497
|
+
runId: row.run_id,
|
|
1498
|
+
stepKey: row.step_key ?? void 0,
|
|
1499
|
+
eventType: row.event_type,
|
|
1500
|
+
level: row.level,
|
|
1501
|
+
payload: row.payload_json ? typeof row.payload_json === "string" ? JSON.parse(row.payload_json) : row.payload_json : void 0,
|
|
1502
|
+
timestamp: new Date(row.timestamp)
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
export {
|
|
1508
|
+
MemoryStorageAdapter,
|
|
1509
|
+
SQLiteStorageAdapter,
|
|
1510
|
+
PostgresStorageAdapter
|
|
1511
|
+
};
|
|
1512
|
+
//# sourceMappingURL=chunk-FQCT3I2A.js.map
|