@multiplier-labs/stepflow 0.3.3 → 0.3.4
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/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
- package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
- package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
- package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
- package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
- package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
- package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/workflows/publish.yml +6 -0
- package/dist/{chunk-4VOKMGP6.js → chunk-5XJBAPXF.js} +517 -522
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-KHTUWLVZ.js → chunk-FMPJBK2N.js} +55 -10
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-ML35PIHX.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-GYPNJBDP.js → chunk-U3PCVK6O.js} +266 -47
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +56 -3
- package/dist/events/index.js +1 -1
- package/dist/{index-Dzr437iW.d.ts → index-Cd0IZ40L.d.ts} +50 -11
- package/dist/index.d.ts +2 -12
- package/dist/index.js +764 -485
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +313 -47
- package/dist/storage/index.js +4 -2
- package/docs/stepflow-api-reference.md +3 -1
- package/package.json +7 -4
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +70 -0
- package/src/core/engine.ts +91 -185
- package/src/core/orchestrator/persistence.ts +165 -0
- package/src/core/orchestrator/plan-resolution.ts +140 -0
- package/src/core/orchestrator/state-transitions.ts +109 -0
- package/src/core/orchestrator/step-runner.ts +427 -0
- package/src/core/orchestrator.ts +40 -359
- package/src/events/webhook.test.ts +292 -0
- package/src/events/webhook.ts +320 -77
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/planner.ts +1 -83
- package/src/planning/registry.ts +3 -85
- package/src/scheduler/cron.test.ts +9 -1
- package/src/scheduler/cron.ts +26 -2
- package/src/scheduler/postgres-persistence.ts +27 -15
- package/src/scheduler/sqlite-persistence.ts +28 -3
- package/src/storage/postgres-core.ts +550 -0
- package/src/storage/postgres-migration.test.ts +69 -0
- package/src/storage/postgres-transaction-pagination.test.ts +107 -0
- package/src/storage/postgres.ts +171 -690
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +177 -1
- package/src/storage/sqlite.ts +183 -93
- package/src/utils/index.ts +2 -0
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +16 -0
- package/src/utils/safe-json.test.ts +166 -0
- package/src/utils/safe-json.ts +107 -0
- package/src/utils/webhook-signature.test.ts +111 -0
- package/src/utils/webhook-signature.ts +74 -0
- package/dist/chunk-4VOKMGP6.js.map +0 -1
- package/dist/chunk-GYPNJBDP.js.map +0 -1
- package/dist/chunk-KHTUWLVZ.js.map +0 -1
- package/dist/chunk-ML35PIHX.js.map +0 -1
package/src/storage/postgres.ts
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostgreSQL storage adapter using Kysely.
|
|
3
3
|
*
|
|
4
|
-
* Provides durable persistence for workflow runs, steps, and events
|
|
5
|
-
*
|
|
4
|
+
* Provides durable persistence for workflow runs, steps, and events with
|
|
5
|
+
* support for distributed deployments and connection pooling.
|
|
6
|
+
*
|
|
7
|
+
* ## Module layout
|
|
8
|
+
*
|
|
9
|
+
* - `PostgresStorageAdapter` (this file) owns the `pg.Pool` / Kysely
|
|
10
|
+
* instance, runs schema migrations on `initialize()`, and implements all
|
|
11
|
+
* non-CRUD methods: connection lifecycle (`close`, `transaction`),
|
|
12
|
+
* workflow-level operations (`dequeueRun`, `cleanupStaleRuns`,
|
|
13
|
+
* `markRunsAsFailed`, `getStats`, `deleteRun`, `deleteOldRuns`,
|
|
14
|
+
* `getInterruptedRuns`, `getLastCompletedStep`), and the
|
|
15
|
+
* `stepflow_step_results` accessors.
|
|
16
|
+
*
|
|
17
|
+
* - `PostgresTransactionAdapter` (this file) is a thin shell used by
|
|
18
|
+
* `PostgresStorageAdapter.transaction()`. It binds a transaction-scoped
|
|
19
|
+
* Kysely query builder; everything else comes from the shared core.
|
|
20
|
+
* Exported only for regression tests.
|
|
21
|
+
*
|
|
22
|
+
* - `PostgresStorageCore` (./postgres-core.ts) holds the shared CRUD
|
|
23
|
+
* methods (`createRun`, `getRun`, `updateRun`, `listRuns`, `createStep`,
|
|
24
|
+
* `getStep`, `updateStep`, `getStepsForRun`, `saveEvent`,
|
|
25
|
+
* `getEventsForRun`), the row mappers, and the JSON-parse helpers. Both
|
|
26
|
+
* classes here extend it.
|
|
6
27
|
*/
|
|
7
28
|
|
|
8
29
|
import type { Kysely as KyselyType } from 'kysely';
|
|
9
30
|
import type { Pool, PoolConfig } from 'pg';
|
|
31
|
+
import type { Logger } from '../core/types.js';
|
|
10
32
|
import { loadPostgresDeps } from '../utils/postgres-deps.js';
|
|
11
33
|
|
|
12
34
|
// Lazy-loaded dependencies - populated by loadPostgresDeps() during initialize()
|
|
@@ -19,19 +41,20 @@ import type {
|
|
|
19
41
|
StorageAdapter,
|
|
20
42
|
WorkflowRunRecord,
|
|
21
43
|
WorkflowRunStepRecord,
|
|
22
|
-
WorkflowEventRecord,
|
|
23
|
-
ListRunsOptions,
|
|
24
|
-
ListEventsOptions,
|
|
25
|
-
PaginatedResult,
|
|
26
|
-
CreateRunInput,
|
|
27
|
-
UpdateRunInput,
|
|
28
44
|
StepResult,
|
|
29
|
-
ExtendedWorkflowRunRecord,
|
|
30
|
-
ExtendedRunStatus,
|
|
31
45
|
ExtendedStepStatus,
|
|
32
46
|
} from './types.js';
|
|
33
|
-
import
|
|
34
|
-
|
|
47
|
+
import {
|
|
48
|
+
PostgresStorageCore,
|
|
49
|
+
mapRunRow,
|
|
50
|
+
mapStepRow,
|
|
51
|
+
safeParseOptionalField,
|
|
52
|
+
type CoreSchemaQueryBuilder,
|
|
53
|
+
type PostgresCoreDatabase,
|
|
54
|
+
type SchemaQueryBuilder,
|
|
55
|
+
type WorkflowRunsTable,
|
|
56
|
+
type WorkflowRunStepsTable,
|
|
57
|
+
} from './postgres-core.js';
|
|
35
58
|
|
|
36
59
|
/** Strip `stack` from a generic error record before persistence. */
|
|
37
60
|
function stripStack<T extends Record<string, unknown>>(error: T): Omit<T, 'stack'> {
|
|
@@ -42,55 +65,11 @@ function stripStack<T extends Record<string, unknown>>(error: T): Omit<T, 'stack
|
|
|
42
65
|
// ============================================================================
|
|
43
66
|
// Database Types (Kysely schema)
|
|
44
67
|
// ============================================================================
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
kind: string;
|
|
51
|
-
/** Current run status (maps to RunStatus). */
|
|
52
|
-
status: string;
|
|
53
|
-
/** Parent run ID for child workflows, null for top-level runs. */
|
|
54
|
-
parent_run_id: string | null;
|
|
55
|
-
/** JSON-serialized input payload. */
|
|
56
|
-
input_json: string;
|
|
57
|
-
/** JSON-serialized arbitrary metadata. */
|
|
58
|
-
metadata_json: string;
|
|
59
|
-
/** JSON-serialized workflow context (accumulated step results). */
|
|
60
|
-
context_json: string;
|
|
61
|
-
/** JSON-serialized output payload, null until run completes. */
|
|
62
|
-
output_json: string | null;
|
|
63
|
-
/** JSON-serialized error object, null unless run failed. */
|
|
64
|
-
error_json: string | null;
|
|
65
|
-
/** Execution priority (lower number = higher precedence). */
|
|
66
|
-
priority: number;
|
|
67
|
-
/** Workflow-level timeout in milliseconds, null for no timeout. */
|
|
68
|
-
timeout_ms: number | null;
|
|
69
|
-
created_at: Date;
|
|
70
|
-
started_at: Date | null;
|
|
71
|
-
finished_at: Date | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Kysely row type for the workflow_run_steps table. */
|
|
75
|
-
interface WorkflowRunStepsTable {
|
|
76
|
-
id: string;
|
|
77
|
-
/** Foreign key to workflow_runs.id. */
|
|
78
|
-
run_id: string;
|
|
79
|
-
/** Unique step identifier within the workflow definition. */
|
|
80
|
-
step_key: string;
|
|
81
|
-
/** Human-readable step name. */
|
|
82
|
-
step_name: string;
|
|
83
|
-
/** Current step status (maps to StepStatus). */
|
|
84
|
-
status: string;
|
|
85
|
-
/** Current retry attempt number (1-based). */
|
|
86
|
-
attempt: number;
|
|
87
|
-
/** JSON-serialized step result, null until step completes. */
|
|
88
|
-
result_json: string | null;
|
|
89
|
-
/** JSON-serialized error object, null unless step failed. */
|
|
90
|
-
error_json: string | null;
|
|
91
|
-
started_at: Date | null;
|
|
92
|
-
finished_at: Date | null;
|
|
93
|
-
}
|
|
68
|
+
//
|
|
69
|
+
// The `runs`, `workflow_run_steps`, and `workflow_events` table shapes live
|
|
70
|
+
// in `./postgres-core.ts` so the shared CRUD core can target them without
|
|
71
|
+
// re-declaration. The full `StepflowDatabase` schema below adds the
|
|
72
|
+
// adapter-only `stepflow_step_results` table (extended storage).
|
|
94
73
|
|
|
95
74
|
/** Kysely row type for the stepflow_step_results table (extended storage). */
|
|
96
75
|
interface StepflowStepResultsTable {
|
|
@@ -111,26 +90,7 @@ interface StepflowStepResultsTable {
|
|
|
111
90
|
completed_at: Date | null;
|
|
112
91
|
}
|
|
113
92
|
|
|
114
|
-
|
|
115
|
-
interface WorkflowEventsTable {
|
|
116
|
-
id: string;
|
|
117
|
-
/** Foreign key to workflow_runs.id. */
|
|
118
|
-
run_id: string;
|
|
119
|
-
/** Step key that emitted this event, null for run-level events. */
|
|
120
|
-
step_key: string | null;
|
|
121
|
-
/** Event type identifier (e.g. 'run.started', 'step.completed'). */
|
|
122
|
-
event_type: string;
|
|
123
|
-
/** Severity level: 'info', 'warn', or 'error'. */
|
|
124
|
-
level: string;
|
|
125
|
-
/** JSON-serialized event payload, null if no extra data. */
|
|
126
|
-
payload_json: string | null;
|
|
127
|
-
timestamp: Date;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
interface StepflowDatabase {
|
|
131
|
-
runs: WorkflowRunsTable;
|
|
132
|
-
workflow_run_steps: WorkflowRunStepsTable;
|
|
133
|
-
workflow_events: WorkflowEventsTable;
|
|
93
|
+
interface StepflowDatabase extends PostgresCoreDatabase {
|
|
134
94
|
stepflow_step_results: StepflowStepResultsTable;
|
|
135
95
|
}
|
|
136
96
|
|
|
@@ -170,6 +130,13 @@ export interface PostgresStorageConfig {
|
|
|
170
130
|
* @default true
|
|
171
131
|
*/
|
|
172
132
|
autoMigrate?: boolean;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optional structured logger used to surface JSON corruption events.
|
|
136
|
+
* Without one, parse failures still increment the corruption counter
|
|
137
|
+
* exposed via `getJsonParseCorruptionCount()` but produce no log output.
|
|
138
|
+
*/
|
|
139
|
+
logger?: Logger;
|
|
173
140
|
}
|
|
174
141
|
|
|
175
142
|
// ============================================================================
|
|
@@ -214,35 +181,55 @@ export interface PostgresStorageConfig {
|
|
|
214
181
|
* const storage = new PostgresStorageAdapter({ pool });
|
|
215
182
|
* ```
|
|
216
183
|
*/
|
|
217
|
-
export class PostgresStorageAdapter
|
|
184
|
+
export class PostgresStorageAdapter extends PostgresStorageCore {
|
|
218
185
|
private db!: KyselyType<StepflowDatabase>;
|
|
219
186
|
private pool!: Pool;
|
|
220
187
|
private ownsPool = false;
|
|
221
|
-
private schema: string;
|
|
222
188
|
private autoMigrate: boolean;
|
|
223
189
|
private initialized = false;
|
|
224
190
|
private config: PostgresStorageConfig;
|
|
225
191
|
|
|
226
192
|
constructor(config: PostgresStorageConfig) {
|
|
227
|
-
|
|
228
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(
|
|
193
|
+
const schema = config.schema ?? 'public';
|
|
194
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(schema)) {
|
|
229
195
|
throw new Error(
|
|
230
|
-
`Invalid schema name "${
|
|
196
|
+
`Invalid schema name "${schema}". Schema must start with a letter or underscore, ` +
|
|
231
197
|
`contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
232
198
|
);
|
|
233
199
|
}
|
|
200
|
+
super(schema, config.logger);
|
|
234
201
|
this.autoMigrate = config.autoMigrate !== false;
|
|
235
202
|
this.config = config;
|
|
236
203
|
}
|
|
237
204
|
|
|
238
205
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
206
|
+
* Core-tables query builder consumed by `PostgresStorageCore`. Returns the
|
|
207
|
+
* schema-scoped Kysely view typed against the three core tables, which is
|
|
208
|
+
* what the shared CRUD methods need.
|
|
241
209
|
*/
|
|
242
|
-
|
|
210
|
+
protected override get qb(): CoreSchemaQueryBuilder {
|
|
211
|
+
return this.db.withSchema(this.schema) as unknown as CoreSchemaQueryBuilder;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extended query builder for adapter-only tables (currently
|
|
216
|
+
* `stepflow_step_results`). Kept separate from `qb` so the base class stays
|
|
217
|
+
* typed against the core schema.
|
|
218
|
+
*/
|
|
219
|
+
private get extQb(): SchemaQueryBuilder<StepflowDatabase> {
|
|
243
220
|
return this.db.withSchema(this.schema);
|
|
244
221
|
}
|
|
245
222
|
|
|
223
|
+
/** Tag used by shared row mappers in `console.warn` messages. */
|
|
224
|
+
protected override get warnLabel(): string {
|
|
225
|
+
return 'PostgresStorageAdapter';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Override of `PostgresStorageCore.ensureReady()` — enforces `initialize()`. */
|
|
229
|
+
protected override ensureReady(): void {
|
|
230
|
+
this.ensureInitialized();
|
|
231
|
+
}
|
|
232
|
+
|
|
246
233
|
private ensureInitialized(): void {
|
|
247
234
|
if (!this.initialized) {
|
|
248
235
|
throw new Error(
|
|
@@ -308,6 +295,47 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
308
295
|
// Schema Creation
|
|
309
296
|
// ============================================================================
|
|
310
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Run a single migration step. Tolerates only known-benign Postgres errors
|
|
300
|
+
* (duplicate object codes that map to "already applied"), and rethrows
|
|
301
|
+
* everything else with a clear message so partial-state upgrades are not
|
|
302
|
+
* silently swallowed.
|
|
303
|
+
*/
|
|
304
|
+
protected async runMigration(label: string, fn: () => Promise<unknown>): Promise<void> {
|
|
305
|
+
try {
|
|
306
|
+
await fn();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (PostgresStorageAdapter.isBenignMigrationError(err)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
312
|
+
throw new Error(`Schema migration failed (${label}): ${message}`, { cause: err });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Postgres SQLSTATE codes that indicate a migration step has already been
|
|
318
|
+
* applied (object already exists). These are safe to ignore on idempotent
|
|
319
|
+
* re-runs; everything else must propagate.
|
|
320
|
+
*
|
|
321
|
+
* - 42701: duplicate_column
|
|
322
|
+
* - 42P07: duplicate_table
|
|
323
|
+
* - 42P06: duplicate_schema
|
|
324
|
+
* - 42710: duplicate_object (e.g., index, constraint)
|
|
325
|
+
*/
|
|
326
|
+
private static readonly BENIGN_MIGRATION_CODES = new Set([
|
|
327
|
+
'42701',
|
|
328
|
+
'42P07',
|
|
329
|
+
'42P06',
|
|
330
|
+
'42710',
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
private static isBenignMigrationError(err: unknown): boolean {
|
|
334
|
+
if (!err || typeof err !== 'object') return false;
|
|
335
|
+
const code = (err as { code?: unknown }).code;
|
|
336
|
+
return typeof code === 'string' && PostgresStorageAdapter.BENIGN_MIGRATION_CODES.has(code);
|
|
337
|
+
}
|
|
338
|
+
|
|
311
339
|
/**
|
|
312
340
|
* Create the workflow tables if they don't exist.
|
|
313
341
|
*/
|
|
@@ -341,14 +369,15 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
341
369
|
`.execute(this.db);
|
|
342
370
|
|
|
343
371
|
// Add new columns if they don't exist (for migrations)
|
|
344
|
-
await
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
372
|
+
await this.runMigration(
|
|
373
|
+
`add columns to ${this.schema}.runs`,
|
|
374
|
+
() => sql`
|
|
375
|
+
ALTER TABLE ${sql.table(`${this.schema}.runs`)}
|
|
376
|
+
ADD COLUMN IF NOT EXISTS output_json JSONB,
|
|
377
|
+
ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0,
|
|
378
|
+
ADD COLUMN IF NOT EXISTS timeout_ms INTEGER
|
|
379
|
+
`.execute(this.db)
|
|
380
|
+
);
|
|
352
381
|
|
|
353
382
|
// Create indexes for runs
|
|
354
383
|
await sql`
|
|
@@ -465,287 +494,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
465
494
|
}
|
|
466
495
|
|
|
467
496
|
// ============================================================================
|
|
468
|
-
// Run
|
|
469
|
-
// ============================================================================
|
|
470
|
-
|
|
471
|
-
/**
|
|
472
|
-
* Create a new workflow run.
|
|
473
|
-
* Supports both legacy and new CreateRunInput interfaces.
|
|
474
|
-
*/
|
|
475
|
-
async createRun(run: CreateRunInput | Omit<WorkflowRunRecord, 'id' | 'createdAt'>): Promise<WorkflowRunRecord> {
|
|
476
|
-
this.ensureInitialized();
|
|
477
|
-
const id = 'id' in run && run.id ? run.id : generateId();
|
|
478
|
-
const createdAt = new Date();
|
|
479
|
-
|
|
480
|
-
await this.qb
|
|
481
|
-
.insertInto('runs')
|
|
482
|
-
.values({
|
|
483
|
-
id,
|
|
484
|
-
kind: run.kind,
|
|
485
|
-
status: run.status,
|
|
486
|
-
parent_run_id: 'parentRunId' in run ? (run.parentRunId ?? null) : null,
|
|
487
|
-
input_json: JSON.stringify(run.input),
|
|
488
|
-
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
489
|
-
context_json: JSON.stringify(run.context ?? {}), // Default to empty object
|
|
490
|
-
output_json: null,
|
|
491
|
-
error_json: 'error' in run && run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
492
|
-
priority: 'priority' in run ? (run.priority ?? 0) : 0,
|
|
493
|
-
timeout_ms: 'timeoutMs' in run ? (run.timeoutMs ?? null) : null,
|
|
494
|
-
created_at: createdAt,
|
|
495
|
-
started_at: null,
|
|
496
|
-
finished_at: null,
|
|
497
|
-
})
|
|
498
|
-
.execute();
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
id,
|
|
502
|
-
kind: run.kind,
|
|
503
|
-
status: run.status as RunStatus,
|
|
504
|
-
parentRunId: 'parentRunId' in run ? run.parentRunId : undefined,
|
|
505
|
-
input: run.input,
|
|
506
|
-
context: run.context ?? {},
|
|
507
|
-
output: undefined,
|
|
508
|
-
error: undefined,
|
|
509
|
-
metadata: run.metadata ?? {},
|
|
510
|
-
priority: 'priority' in run ? (run.priority ?? 0) : 0,
|
|
511
|
-
timeoutMs: 'timeoutMs' in run ? run.timeoutMs : undefined,
|
|
512
|
-
createdAt,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async getRun(runId: string): Promise<WorkflowRunRecord | null> {
|
|
517
|
-
this.ensureInitialized();
|
|
518
|
-
const row = await this.qb
|
|
519
|
-
.selectFrom('runs')
|
|
520
|
-
.selectAll()
|
|
521
|
-
.where('id', '=', runId)
|
|
522
|
-
.executeTakeFirst();
|
|
523
|
-
|
|
524
|
-
return row ? this.mapRunRow(row) : null;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Update a workflow run.
|
|
529
|
-
* Supports both legacy Partial<WorkflowRunRecord> and new UpdateRunInput interfaces.
|
|
530
|
-
*/
|
|
531
|
-
async updateRun(runId: string, updates: UpdateRunInput | Partial<WorkflowRunRecord>): Promise<void> {
|
|
532
|
-
this.ensureInitialized();
|
|
533
|
-
const updateData: Partial<WorkflowRunsTable> = {};
|
|
534
|
-
|
|
535
|
-
if (updates.status !== undefined) {
|
|
536
|
-
updateData.status = updates.status;
|
|
537
|
-
}
|
|
538
|
-
if (updates.context !== undefined) {
|
|
539
|
-
updateData.context_json = JSON.stringify(updates.context);
|
|
540
|
-
}
|
|
541
|
-
if ('output' in updates && updates.output !== undefined) {
|
|
542
|
-
updateData.output_json = JSON.stringify(updates.output);
|
|
543
|
-
}
|
|
544
|
-
if (updates.error !== undefined) {
|
|
545
|
-
updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
546
|
-
}
|
|
547
|
-
if (updates.startedAt !== undefined) {
|
|
548
|
-
updateData.started_at = updates.startedAt;
|
|
549
|
-
}
|
|
550
|
-
if (updates.finishedAt !== undefined) {
|
|
551
|
-
updateData.finished_at = updates.finishedAt;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (Object.keys(updateData).length > 0) {
|
|
555
|
-
await this.qb
|
|
556
|
-
.updateTable('runs')
|
|
557
|
-
.set(updateData)
|
|
558
|
-
.where('id', '=', runId)
|
|
559
|
-
.execute();
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* List workflow runs with filtering and pagination.
|
|
565
|
-
*/
|
|
566
|
-
/**
|
|
567
|
-
* Apply common run filters to a Kysely query builder.
|
|
568
|
-
* Used by both the data query and count query in listRuns to avoid duplication.
|
|
569
|
-
*/
|
|
570
|
-
private applyRunsFilters<T extends { where(col: any, op: any, val: any): T }>(
|
|
571
|
-
query: T,
|
|
572
|
-
options: ListRunsOptions
|
|
573
|
-
): T {
|
|
574
|
-
if (options.kind) {
|
|
575
|
-
query = query.where('kind', '=', options.kind);
|
|
576
|
-
}
|
|
577
|
-
if (options.status) {
|
|
578
|
-
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
579
|
-
query = query.where('status', 'in', statuses);
|
|
580
|
-
}
|
|
581
|
-
if (options.parentRunId !== undefined) {
|
|
582
|
-
query = query.where('parent_run_id', '=', options.parentRunId);
|
|
583
|
-
}
|
|
584
|
-
return query;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async listRuns(options: ListRunsOptions = {}): Promise<PaginatedResult<WorkflowRunRecord>> {
|
|
588
|
-
this.ensureInitialized();
|
|
589
|
-
const limit = options.limit ?? 50;
|
|
590
|
-
const offset = options.offset ?? 0;
|
|
591
|
-
|
|
592
|
-
let query = this.applyRunsFilters(
|
|
593
|
-
this.qb.selectFrom('runs').selectAll(),
|
|
594
|
-
options
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
const countQuery = this.applyRunsFilters(
|
|
598
|
-
this.qb.selectFrom('runs').select(sql<number>`count(*)`.as('count')),
|
|
599
|
-
options
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
const countResult = await countQuery.executeTakeFirst() as { count?: string | number } | undefined;
|
|
603
|
-
const total = Number(countResult?.count ?? 0);
|
|
604
|
-
|
|
605
|
-
// Order
|
|
606
|
-
const orderBy = options.orderBy ?? 'createdAt';
|
|
607
|
-
const orderDirection = options.orderDirection ?? 'desc';
|
|
608
|
-
const orderColumn = orderBy === 'createdAt' ? 'created_at' :
|
|
609
|
-
orderBy === 'startedAt' ? 'started_at' : 'finished_at';
|
|
610
|
-
|
|
611
|
-
query = query.orderBy(orderColumn, orderDirection);
|
|
612
|
-
|
|
613
|
-
// Apply pagination
|
|
614
|
-
query = query.limit(limit).offset(offset);
|
|
615
|
-
|
|
616
|
-
const rows = await query.execute();
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
items: rows.map(row => this.mapRunRow(row)),
|
|
620
|
-
total,
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// ============================================================================
|
|
625
|
-
// Step Operations
|
|
626
|
-
// ============================================================================
|
|
627
|
-
|
|
628
|
-
async createStep(step: Omit<WorkflowRunStepRecord, 'id'>): Promise<WorkflowRunStepRecord> {
|
|
629
|
-
this.ensureInitialized();
|
|
630
|
-
const id = generateId();
|
|
631
|
-
|
|
632
|
-
await this.qb
|
|
633
|
-
.insertInto('workflow_run_steps')
|
|
634
|
-
.values({
|
|
635
|
-
id,
|
|
636
|
-
run_id: step.runId,
|
|
637
|
-
step_key: step.stepKey,
|
|
638
|
-
step_name: step.stepName,
|
|
639
|
-
status: step.status,
|
|
640
|
-
attempt: step.attempt,
|
|
641
|
-
result_json: step.result !== undefined ? JSON.stringify(step.result) : null,
|
|
642
|
-
error_json: step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
|
|
643
|
-
started_at: step.startedAt ?? null,
|
|
644
|
-
finished_at: step.finishedAt ?? null,
|
|
645
|
-
})
|
|
646
|
-
.execute();
|
|
647
|
-
|
|
648
|
-
return { ...step, id };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
|
|
652
|
-
this.ensureInitialized();
|
|
653
|
-
const row = await this.qb
|
|
654
|
-
.selectFrom('workflow_run_steps')
|
|
655
|
-
.selectAll()
|
|
656
|
-
.where('id', '=', stepId)
|
|
657
|
-
.executeTakeFirst();
|
|
658
|
-
|
|
659
|
-
return row ? this.mapStepRow(row) : null;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async updateStep(stepId: string, updates: Partial<WorkflowRunStepRecord>): Promise<void> {
|
|
663
|
-
this.ensureInitialized();
|
|
664
|
-
const updateData: Partial<WorkflowRunStepsTable> = {};
|
|
665
|
-
|
|
666
|
-
if (updates.status !== undefined) {
|
|
667
|
-
updateData.status = updates.status;
|
|
668
|
-
}
|
|
669
|
-
if (updates.attempt !== undefined) {
|
|
670
|
-
updateData.attempt = updates.attempt;
|
|
671
|
-
}
|
|
672
|
-
if (updates.result !== undefined) {
|
|
673
|
-
updateData.result_json = JSON.stringify(updates.result);
|
|
674
|
-
}
|
|
675
|
-
if (updates.error !== undefined) {
|
|
676
|
-
updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error));
|
|
677
|
-
}
|
|
678
|
-
if (updates.finishedAt !== undefined) {
|
|
679
|
-
updateData.finished_at = updates.finishedAt;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (Object.keys(updateData).length > 0) {
|
|
683
|
-
await this.qb
|
|
684
|
-
.updateTable('workflow_run_steps')
|
|
685
|
-
.set(updateData)
|
|
686
|
-
.where('id', '=', stepId)
|
|
687
|
-
.execute();
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
|
|
692
|
-
this.ensureInitialized();
|
|
693
|
-
const rows = await this.qb
|
|
694
|
-
.selectFrom('workflow_run_steps')
|
|
695
|
-
.selectAll()
|
|
696
|
-
.where('run_id', '=', runId)
|
|
697
|
-
.orderBy('started_at', 'asc')
|
|
698
|
-
.execute();
|
|
699
|
-
|
|
700
|
-
return rows.map(row => this.mapStepRow(row));
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// ============================================================================
|
|
704
|
-
// Event Operations
|
|
497
|
+
// Run / Step / Event CRUD
|
|
705
498
|
// ============================================================================
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
.insertInto('workflow_events')
|
|
713
|
-
.values({
|
|
714
|
-
id,
|
|
715
|
-
run_id: event.runId,
|
|
716
|
-
step_key: event.stepKey ?? null,
|
|
717
|
-
event_type: event.eventType,
|
|
718
|
-
level: event.level,
|
|
719
|
-
payload_json: event.payload !== undefined ? JSON.stringify(event.payload) : null,
|
|
720
|
-
timestamp: event.timestamp,
|
|
721
|
-
})
|
|
722
|
-
.execute();
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
async getEventsForRun(runId: string, options: ListEventsOptions = {}): Promise<WorkflowEventRecord[]> {
|
|
726
|
-
this.ensureInitialized();
|
|
727
|
-
const limit = options.limit ?? 1000;
|
|
728
|
-
const offset = options.offset ?? 0;
|
|
729
|
-
|
|
730
|
-
let query = this.qb
|
|
731
|
-
.selectFrom('workflow_events')
|
|
732
|
-
.selectAll()
|
|
733
|
-
.where('run_id', '=', runId);
|
|
734
|
-
|
|
735
|
-
if (options.stepKey) {
|
|
736
|
-
query = query.where('step_key', '=', options.stepKey);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (options.level) {
|
|
740
|
-
query = query.where('level', '=', options.level);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
query = query.orderBy('timestamp', 'asc').limit(limit).offset(offset);
|
|
744
|
-
|
|
745
|
-
const rows = await query.execute();
|
|
746
|
-
|
|
747
|
-
return rows.map(row => this.mapEventRow(row));
|
|
748
|
-
}
|
|
499
|
+
//
|
|
500
|
+
// The CRUD methods (`createRun`, `getRun`, `updateRun`, `listRuns`,
|
|
501
|
+
// `createStep`, `getStep`, `updateStep`, `getStepsForRun`, `saveEvent`,
|
|
502
|
+
// `getEventsForRun`) are inherited from `PostgresStorageCore`. Each call
|
|
503
|
+
// routes through `this.qb` (overridden above) and `this.ensureReady()`
|
|
504
|
+
// which calls `this.ensureInitialized()`.
|
|
749
505
|
|
|
750
506
|
// ============================================================================
|
|
751
507
|
// Transaction Support
|
|
@@ -758,7 +514,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
758
514
|
this.ensureInitialized();
|
|
759
515
|
return await this.db.transaction().execute(async (trx) => {
|
|
760
516
|
// Create a transactional adapter wrapper
|
|
761
|
-
const txAdapter = new PostgresTransactionAdapter(trx, this.schema);
|
|
517
|
+
const txAdapter = new PostgresTransactionAdapter(trx, this.schema, this.logger);
|
|
762
518
|
return await fn(txAdapter);
|
|
763
519
|
});
|
|
764
520
|
}
|
|
@@ -798,7 +554,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
798
554
|
.orderBy('created_at', 'asc')
|
|
799
555
|
.execute();
|
|
800
556
|
|
|
801
|
-
return rows.map(row =>
|
|
557
|
+
return rows.map(row => mapRunRow(row as WorkflowRunsTable, this.mapperCtx));
|
|
802
558
|
}
|
|
803
559
|
|
|
804
560
|
/**
|
|
@@ -816,7 +572,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
816
572
|
.limit(1)
|
|
817
573
|
.executeTakeFirst();
|
|
818
574
|
|
|
819
|
-
return row ?
|
|
575
|
+
return row ? mapStepRow(row as WorkflowRunStepsTable, this.mapperCtx) : null;
|
|
820
576
|
}
|
|
821
577
|
|
|
822
578
|
// ============================================================================
|
|
@@ -851,94 +607,40 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
851
607
|
`.execute(this.db);
|
|
852
608
|
|
|
853
609
|
const row = result.rows[0];
|
|
854
|
-
return row ?
|
|
610
|
+
return row ? mapRunRow(row, this.mapperCtx) : null;
|
|
855
611
|
}
|
|
856
612
|
|
|
857
613
|
// ============================================================================
|
|
858
|
-
// Row Mapping
|
|
614
|
+
// Row Mapping (adapter-only tables)
|
|
859
615
|
// ============================================================================
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
kind: row.kind,
|
|
865
|
-
status: row.status as RunStatus,
|
|
866
|
-
parentRunId: row.parent_run_id ?? undefined,
|
|
867
|
-
input: typeof row.input_json === 'string' ? JSON.parse(row.input_json) : row.input_json,
|
|
868
|
-
context: typeof row.context_json === 'string' ? JSON.parse(row.context_json) : row.context_json,
|
|
869
|
-
output: row.output_json ? (typeof row.output_json === 'string' ? JSON.parse(row.output_json) : row.output_json) : undefined,
|
|
870
|
-
error: row.error_json ? (typeof row.error_json === 'string' ? JSON.parse(row.error_json) : row.error_json) : undefined,
|
|
871
|
-
metadata: typeof row.metadata_json === 'string' ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
872
|
-
priority: row.priority ?? 0,
|
|
873
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
874
|
-
createdAt: new Date(row.created_at),
|
|
875
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
876
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Map a database row to an extended workflow run record.
|
|
882
|
-
*/
|
|
883
|
-
private mapExtendedRunRow(row: WorkflowRunsTable): ExtendedWorkflowRunRecord {
|
|
884
|
-
return {
|
|
885
|
-
id: row.id,
|
|
886
|
-
kind: row.kind,
|
|
887
|
-
status: row.status as ExtendedRunStatus,
|
|
888
|
-
input: typeof row.input_json === 'string' ? JSON.parse(row.input_json) : row.input_json,
|
|
889
|
-
metadata: typeof row.metadata_json === 'string' ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
890
|
-
context: typeof row.context_json === 'string' ? JSON.parse(row.context_json) : row.context_json,
|
|
891
|
-
output: row.output_json ? (typeof row.output_json === 'string' ? JSON.parse(row.output_json) : row.output_json) : undefined,
|
|
892
|
-
error: row.error_json ? (typeof row.error_json === 'string' ? JSON.parse(row.error_json) : row.error_json) : undefined,
|
|
893
|
-
priority: row.priority ?? 0,
|
|
894
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
895
|
-
createdAt: new Date(row.created_at),
|
|
896
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
897
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
898
|
-
};
|
|
899
|
-
}
|
|
616
|
+
//
|
|
617
|
+
// Mappers and JSON-parse helpers for the three core tables live in
|
|
618
|
+
// `./postgres-core.ts`. The single mapper kept here covers
|
|
619
|
+
// `stepflow_step_results`, the only table outside the core schema.
|
|
900
620
|
|
|
901
621
|
private mapStepResultRow(row: StepflowStepResultsTable): StepResult {
|
|
622
|
+
const ctx = this.mapperCtx;
|
|
902
623
|
return {
|
|
903
624
|
id: row.id,
|
|
904
625
|
runId: row.run_id,
|
|
905
626
|
stepName: row.step_name,
|
|
906
627
|
status: row.status as ExtendedStepStatus,
|
|
907
|
-
output:
|
|
908
|
-
|
|
628
|
+
output: safeParseOptionalField(row.output_json, {
|
|
629
|
+
...ctx,
|
|
630
|
+
rowId: row.id,
|
|
631
|
+
column: 'output_json',
|
|
632
|
+
}) as Record<string, unknown> | undefined,
|
|
633
|
+
error: safeParseOptionalField(row.error_json, {
|
|
634
|
+
...ctx,
|
|
635
|
+
rowId: row.id,
|
|
636
|
+
column: 'error_json',
|
|
637
|
+
}) as Record<string, unknown> | undefined,
|
|
909
638
|
attempt: row.attempt,
|
|
910
639
|
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
911
640
|
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
912
641
|
};
|
|
913
642
|
}
|
|
914
643
|
|
|
915
|
-
private mapStepRow(row: WorkflowRunStepsTable): WorkflowRunStepRecord {
|
|
916
|
-
return {
|
|
917
|
-
id: row.id,
|
|
918
|
-
runId: row.run_id,
|
|
919
|
-
stepKey: row.step_key,
|
|
920
|
-
stepName: row.step_name,
|
|
921
|
-
status: row.status as StepStatus,
|
|
922
|
-
attempt: row.attempt,
|
|
923
|
-
result: row.result_json ? (typeof row.result_json === 'string' ? JSON.parse(row.result_json) : row.result_json) : undefined,
|
|
924
|
-
error: row.error_json ? (typeof row.error_json === 'string' ? JSON.parse(row.error_json) : row.error_json) : undefined,
|
|
925
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
926
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
private mapEventRow(row: WorkflowEventsTable): WorkflowEventRecord {
|
|
931
|
-
return {
|
|
932
|
-
id: row.id,
|
|
933
|
-
runId: row.run_id,
|
|
934
|
-
stepKey: row.step_key ?? undefined,
|
|
935
|
-
eventType: row.event_type,
|
|
936
|
-
level: row.level as 'info' | 'warn' | 'error',
|
|
937
|
-
payload: row.payload_json ? (typeof row.payload_json === 'string' ? JSON.parse(row.payload_json) : row.payload_json) : undefined,
|
|
938
|
-
timestamp: new Date(row.timestamp),
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
|
|
942
644
|
// ============================================================================
|
|
943
645
|
// New WorkflowStorage Methods
|
|
944
646
|
// ============================================================================
|
|
@@ -1016,7 +718,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1016
718
|
*/
|
|
1017
719
|
async getStepResult(runId: string, stepName: string): Promise<StepResult | undefined> {
|
|
1018
720
|
this.ensureInitialized();
|
|
1019
|
-
const row = await this.
|
|
721
|
+
const row = await this.extQb
|
|
1020
722
|
.selectFrom('stepflow_step_results')
|
|
1021
723
|
.selectAll()
|
|
1022
724
|
.where('run_id', '=', runId)
|
|
@@ -1031,7 +733,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1031
733
|
*/
|
|
1032
734
|
async getStepResults(runId: string): Promise<StepResult[]> {
|
|
1033
735
|
this.ensureInitialized();
|
|
1034
|
-
const rows = await this.
|
|
736
|
+
const rows = await this.extQb
|
|
1035
737
|
.selectFrom('stepflow_step_results')
|
|
1036
738
|
.selectAll()
|
|
1037
739
|
.where('run_id', '=', runId)
|
|
@@ -1049,7 +751,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1049
751
|
this.ensureInitialized();
|
|
1050
752
|
const id = result.id ?? generateId();
|
|
1051
753
|
|
|
1052
|
-
await this.
|
|
754
|
+
await this.extQb
|
|
1053
755
|
.insertInto('stepflow_step_results')
|
|
1054
756
|
.values({
|
|
1055
757
|
id,
|
|
@@ -1104,248 +806,27 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1104
806
|
|
|
1105
807
|
/**
|
|
1106
808
|
* A StorageAdapter wrapper for use within transactions.
|
|
1107
|
-
*
|
|
809
|
+
*
|
|
810
|
+
* Used internally by `PostgresStorageAdapter.transaction()`. All CRUD methods
|
|
811
|
+
* are inherited from `PostgresStorageCore`; this class only binds the
|
|
812
|
+
* transaction-scoped query builder.
|
|
813
|
+
*
|
|
814
|
+
* Exported for regression testing; not part of the public API.
|
|
1108
815
|
*/
|
|
1109
|
-
class PostgresTransactionAdapter
|
|
1110
|
-
private
|
|
1111
|
-
|
|
1112
|
-
constructor(
|
|
1113
|
-
private trx: KyselyType<StepflowDatabase>,
|
|
1114
|
-
private schema: string
|
|
1115
|
-
) {
|
|
1116
|
-
this.qb = trx.withSchema(schema);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
async createRun(run: CreateRunInput | Omit<WorkflowRunRecord, 'id' | 'createdAt'>): Promise<WorkflowRunRecord> {
|
|
1120
|
-
const id = 'id' in run && run.id ? run.id : generateId();
|
|
1121
|
-
const createdAt = new Date();
|
|
1122
|
-
|
|
1123
|
-
await this.qb
|
|
1124
|
-
.insertInto('runs')
|
|
1125
|
-
.values({
|
|
1126
|
-
id,
|
|
1127
|
-
kind: run.kind,
|
|
1128
|
-
status: run.status,
|
|
1129
|
-
parent_run_id: 'parentRunId' in run ? (run.parentRunId ?? null) : null,
|
|
1130
|
-
input_json: JSON.stringify(run.input),
|
|
1131
|
-
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
1132
|
-
context_json: JSON.stringify(run.context ?? {}),
|
|
1133
|
-
output_json: null,
|
|
1134
|
-
error_json: 'error' in run && run.error ? JSON.stringify(sanitizeErrorForStorage(run.error)) : null,
|
|
1135
|
-
priority: 'priority' in run ? (run.priority ?? 0) : 0,
|
|
1136
|
-
timeout_ms: 'timeoutMs' in run ? (run.timeoutMs ?? null) : null,
|
|
1137
|
-
created_at: createdAt,
|
|
1138
|
-
started_at: null,
|
|
1139
|
-
finished_at: null,
|
|
1140
|
-
})
|
|
1141
|
-
.execute();
|
|
1142
|
-
|
|
1143
|
-
return {
|
|
1144
|
-
id,
|
|
1145
|
-
kind: run.kind,
|
|
1146
|
-
status: run.status as RunStatus,
|
|
1147
|
-
parentRunId: 'parentRunId' in run ? run.parentRunId : undefined,
|
|
1148
|
-
input: run.input,
|
|
1149
|
-
context: run.context ?? {},
|
|
1150
|
-
output: undefined,
|
|
1151
|
-
error: undefined,
|
|
1152
|
-
metadata: run.metadata ?? {},
|
|
1153
|
-
priority: 'priority' in run ? (run.priority ?? 0) : 0,
|
|
1154
|
-
timeoutMs: 'timeoutMs' in run ? run.timeoutMs : undefined,
|
|
1155
|
-
createdAt,
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
async getRun(runId: string): Promise<WorkflowRunRecord | null> {
|
|
1160
|
-
const row = await this.qb
|
|
1161
|
-
.selectFrom('runs')
|
|
1162
|
-
.selectAll()
|
|
1163
|
-
.where('id', '=', runId)
|
|
1164
|
-
.executeTakeFirst();
|
|
1165
|
-
|
|
1166
|
-
return row ? this.mapRunRow(row) : null;
|
|
1167
|
-
}
|
|
816
|
+
export class PostgresTransactionAdapter extends PostgresStorageCore {
|
|
817
|
+
private readonly _qb: CoreSchemaQueryBuilder;
|
|
1168
818
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (updates.status !== undefined) updateData.status = updates.status;
|
|
1173
|
-
if (updates.context !== undefined) updateData.context_json = JSON.stringify(updates.context);
|
|
1174
|
-
if ('output' in updates && updates.output !== undefined) updateData.output_json = JSON.stringify(updates.output);
|
|
1175
|
-
if (updates.error !== undefined) updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error as WorkflowError));
|
|
1176
|
-
if (updates.startedAt !== undefined) updateData.started_at = updates.startedAt;
|
|
1177
|
-
if (updates.finishedAt !== undefined) updateData.finished_at = updates.finishedAt;
|
|
1178
|
-
|
|
1179
|
-
if (Object.keys(updateData).length > 0) {
|
|
1180
|
-
await this.qb.updateTable('runs').set(updateData).where('id', '=', runId).execute();
|
|
1181
|
-
}
|
|
819
|
+
constructor(trx: KyselyType<StepflowDatabase>, schema: string, logger?: Logger) {
|
|
820
|
+
super(schema, logger);
|
|
821
|
+
this._qb = trx.withSchema(schema) as unknown as CoreSchemaQueryBuilder;
|
|
1182
822
|
}
|
|
1183
823
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
const offset = options.offset ?? 0;
|
|
1187
|
-
|
|
1188
|
-
let query = this.qb.selectFrom('runs').selectAll();
|
|
1189
|
-
|
|
1190
|
-
if (options.kind) query = query.where('kind', '=', options.kind);
|
|
1191
|
-
if (options.status) {
|
|
1192
|
-
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
1193
|
-
query = query.where('status', 'in', statuses);
|
|
1194
|
-
}
|
|
1195
|
-
if (options.parentRunId !== undefined) {
|
|
1196
|
-
query = query.where('parent_run_id', '=', options.parentRunId);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
const orderColumn = (options.orderBy ?? 'createdAt') === 'createdAt' ? 'created_at' :
|
|
1200
|
-
options.orderBy === 'startedAt' ? 'started_at' : 'finished_at';
|
|
1201
|
-
const orderDirection = options.orderDirection ?? 'desc';
|
|
1202
|
-
|
|
1203
|
-
query = query.orderBy(orderColumn, orderDirection).limit(limit).offset(offset);
|
|
1204
|
-
|
|
1205
|
-
const rows = await query.execute();
|
|
1206
|
-
|
|
1207
|
-
return {
|
|
1208
|
-
items: rows.map(row => this.mapRunRow(row)),
|
|
1209
|
-
total: rows.length, // Simplified for transaction context
|
|
1210
|
-
};
|
|
824
|
+
protected override get qb(): CoreSchemaQueryBuilder {
|
|
825
|
+
return this._qb;
|
|
1211
826
|
}
|
|
1212
827
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
await this.qb
|
|
1217
|
-
.insertInto('workflow_run_steps')
|
|
1218
|
-
.values({
|
|
1219
|
-
id,
|
|
1220
|
-
run_id: step.runId,
|
|
1221
|
-
step_key: step.stepKey,
|
|
1222
|
-
step_name: step.stepName,
|
|
1223
|
-
status: step.status,
|
|
1224
|
-
attempt: step.attempt,
|
|
1225
|
-
result_json: step.result !== undefined ? JSON.stringify(step.result) : null,
|
|
1226
|
-
error_json: step.error ? JSON.stringify(sanitizeErrorForStorage(step.error)) : null,
|
|
1227
|
-
started_at: step.startedAt ?? null,
|
|
1228
|
-
finished_at: step.finishedAt ?? null,
|
|
1229
|
-
})
|
|
1230
|
-
.execute();
|
|
1231
|
-
|
|
1232
|
-
return { ...step, id };
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
|
|
1236
|
-
const row = await this.qb
|
|
1237
|
-
.selectFrom('workflow_run_steps')
|
|
1238
|
-
.selectAll()
|
|
1239
|
-
.where('id', '=', stepId)
|
|
1240
|
-
.executeTakeFirst();
|
|
1241
|
-
|
|
1242
|
-
return row ? this.mapStepRow(row) : null;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
async updateStep(stepId: string, updates: Partial<WorkflowRunStepRecord>): Promise<void> {
|
|
1246
|
-
const updateData: Partial<WorkflowRunStepsTable> = {};
|
|
1247
|
-
|
|
1248
|
-
if (updates.status !== undefined) updateData.status = updates.status;
|
|
1249
|
-
if (updates.attempt !== undefined) updateData.attempt = updates.attempt;
|
|
1250
|
-
if (updates.result !== undefined) updateData.result_json = JSON.stringify(updates.result);
|
|
1251
|
-
if (updates.error !== undefined) updateData.error_json = JSON.stringify(sanitizeErrorForStorage(updates.error as WorkflowError));
|
|
1252
|
-
if (updates.finishedAt !== undefined) updateData.finished_at = updates.finishedAt;
|
|
1253
|
-
|
|
1254
|
-
if (Object.keys(updateData).length > 0) {
|
|
1255
|
-
await this.qb.updateTable('workflow_run_steps').set(updateData).where('id', '=', stepId).execute();
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
|
|
1260
|
-
const rows = await this.qb
|
|
1261
|
-
.selectFrom('workflow_run_steps')
|
|
1262
|
-
.selectAll()
|
|
1263
|
-
.where('run_id', '=', runId)
|
|
1264
|
-
.orderBy('started_at', 'asc')
|
|
1265
|
-
.execute();
|
|
1266
|
-
|
|
1267
|
-
return rows.map(row => this.mapStepRow(row));
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
async saveEvent(event: Omit<WorkflowEventRecord, 'id'>): Promise<void> {
|
|
1271
|
-
const id = generateId();
|
|
1272
|
-
|
|
1273
|
-
await this.qb
|
|
1274
|
-
.insertInto('workflow_events')
|
|
1275
|
-
.values({
|
|
1276
|
-
id,
|
|
1277
|
-
run_id: event.runId,
|
|
1278
|
-
step_key: event.stepKey ?? null,
|
|
1279
|
-
event_type: event.eventType,
|
|
1280
|
-
level: event.level,
|
|
1281
|
-
payload_json: event.payload !== undefined ? JSON.stringify(event.payload) : null,
|
|
1282
|
-
timestamp: event.timestamp,
|
|
1283
|
-
})
|
|
1284
|
-
.execute();
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
async getEventsForRun(runId: string, options: ListEventsOptions = {}): Promise<WorkflowEventRecord[]> {
|
|
1288
|
-
let query = this.qb
|
|
1289
|
-
.selectFrom('workflow_events')
|
|
1290
|
-
.selectAll()
|
|
1291
|
-
.where('run_id', '=', runId);
|
|
1292
|
-
|
|
1293
|
-
if (options.stepKey) query = query.where('step_key', '=', options.stepKey);
|
|
1294
|
-
if (options.level) query = query.where('level', '=', options.level);
|
|
1295
|
-
|
|
1296
|
-
const rows = await query
|
|
1297
|
-
.orderBy('timestamp', 'asc')
|
|
1298
|
-
.limit(options.limit ?? 1000)
|
|
1299
|
-
.offset(options.offset ?? 0)
|
|
1300
|
-
.execute();
|
|
1301
|
-
|
|
1302
|
-
return rows.map(row => this.mapEventRow(row));
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
private mapRunRow(row: WorkflowRunsTable): WorkflowRunRecord {
|
|
1306
|
-
return {
|
|
1307
|
-
id: row.id,
|
|
1308
|
-
kind: row.kind,
|
|
1309
|
-
status: row.status as RunStatus,
|
|
1310
|
-
parentRunId: row.parent_run_id ?? undefined,
|
|
1311
|
-
input: typeof row.input_json === 'string' ? JSON.parse(row.input_json) : row.input_json,
|
|
1312
|
-
context: typeof row.context_json === 'string' ? JSON.parse(row.context_json) : row.context_json,
|
|
1313
|
-
output: row.output_json ? (typeof row.output_json === 'string' ? JSON.parse(row.output_json) : row.output_json) : undefined,
|
|
1314
|
-
error: row.error_json ? (typeof row.error_json === 'string' ? JSON.parse(row.error_json) : row.error_json) : undefined,
|
|
1315
|
-
metadata: typeof row.metadata_json === 'string' ? JSON.parse(row.metadata_json) : row.metadata_json,
|
|
1316
|
-
priority: row.priority ?? 0,
|
|
1317
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
1318
|
-
createdAt: new Date(row.created_at),
|
|
1319
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1320
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
private mapStepRow(row: WorkflowRunStepsTable): WorkflowRunStepRecord {
|
|
1325
|
-
return {
|
|
1326
|
-
id: row.id,
|
|
1327
|
-
runId: row.run_id,
|
|
1328
|
-
stepKey: row.step_key,
|
|
1329
|
-
stepName: row.step_name,
|
|
1330
|
-
status: row.status as StepStatus,
|
|
1331
|
-
attempt: row.attempt,
|
|
1332
|
-
result: row.result_json ? (typeof row.result_json === 'string' ? JSON.parse(row.result_json) : row.result_json) : undefined,
|
|
1333
|
-
error: row.error_json ? (typeof row.error_json === 'string' ? JSON.parse(row.error_json) : row.error_json) : undefined,
|
|
1334
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1335
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
1336
|
-
};
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
private mapEventRow(row: WorkflowEventsTable): WorkflowEventRecord {
|
|
1340
|
-
return {
|
|
1341
|
-
id: row.id,
|
|
1342
|
-
runId: row.run_id,
|
|
1343
|
-
stepKey: row.step_key ?? undefined,
|
|
1344
|
-
eventType: row.event_type,
|
|
1345
|
-
level: row.level as 'info' | 'warn' | 'error',
|
|
1346
|
-
payload: row.payload_json ? (typeof row.payload_json === 'string' ? JSON.parse(row.payload_json) : row.payload_json) : undefined,
|
|
1347
|
-
timestamp: new Date(row.timestamp),
|
|
1348
|
-
};
|
|
828
|
+
protected override get warnLabel(): string {
|
|
829
|
+
return 'PostgresTransactionAdapter';
|
|
1349
830
|
}
|
|
1350
831
|
}
|
|
1351
832
|
|