@multiplier-labs/stepflow 0.3.1 → 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-04_comment-assessment.md +179 -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-03_complexity-report.md +152 -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-04_dependency-health.md +94 -0
- package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -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-03_security-audit.md +208 -0
- package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +17 -5
- package/.github/workflows/security.yml +29 -0
- package/CHANGELOG.md +34 -0
- package/README.md +24 -0
- package/SECURITY.md +40 -0
- package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
- package/dist/chunk-5XJBAPXF.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
- package/dist/chunk-FMPJBK2N.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
- package/dist/chunk-JPXPB7U5.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
- package/dist/chunk-U3PCVK6O.js.map +1 -0
- package/dist/events/index.d.ts +60 -7
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
- package/dist/index.d.ts +15 -23
- package/dist/index.js +784 -547
- package/dist/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +4 -4
- package/dist/scheduler/index.js +2 -2
- package/dist/storage/index.d.ts +322 -56
- package/dist/storage/index.js +4 -2
- package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
- package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
- package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
- package/docs/stepflow-api-reference.md +5 -4
- package/package.json +12 -8
- package/src/core/engine/concurrency.ts +229 -0
- package/src/core/engine/lifecycle.ts +176 -0
- package/src/core/engine.test.ts +536 -522
- package/src/core/engine.ts +116 -235
- package/src/core/index.ts +3 -3
- 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 +57 -395
- package/src/core/types.ts +7 -29
- package/src/events/index.ts +4 -4
- package/src/events/memory.test.ts +39 -41
- package/src/events/memory.ts +7 -10
- package/src/events/socketio.test.ts +112 -162
- package/src/events/socketio.ts +20 -27
- package/src/events/types.ts +16 -19
- package/src/events/webhook.test.ts +489 -235
- package/src/events/webhook.ts +355 -139
- package/src/index.ts +19 -19
- package/src/planning/evaluate-condition.test.ts +153 -0
- package/src/planning/evaluate-condition.ts +105 -0
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +45 -152
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +20 -106
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +149 -150
- package/src/scheduler/cron.ts +58 -69
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +89 -105
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +40 -22
- package/src/scheduler/types.ts +4 -9
- package/src/storage/index.ts +5 -5
- package/src/storage/memory.test.ts +133 -183
- package/src/storage/memory.ts +21 -42
- 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 +222 -937
- package/src/storage/sqlite-core.ts +151 -0
- package/src/storage/sqlite.test.ts +330 -281
- package/src/storage/sqlite.ts +225 -170
- package/src/storage/types.ts +33 -58
- package/src/utils/errors.test.ts +78 -80
- package/src/utils/errors.ts +27 -57
- package/src/utils/id.ts +3 -3
- package/src/utils/index.ts +6 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/pinned-fetch.test.ts +84 -0
- package/src/utils/pinned-fetch.ts +118 -0
- package/src/utils/postgres-deps.ts +20 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- 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-FQCT3I2A.js.map +0 -1
- package/dist/chunk-GXY66V7Q.js.map +0 -1
- package/dist/chunk-U5QGN3RT.js.map +0 -1
- package/dist/chunk-XCF4PLN7.js.map +0 -1
- package/src/scheduler/postgres-persistence.test.ts +0 -906
package/src/storage/postgres.ts
CHANGED
|
@@ -1,98 +1,75 @@
|
|
|
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
|
-
import type { Kysely as KyselyType } from
|
|
9
|
-
import type { Pool, PoolConfig } from
|
|
10
|
-
import {
|
|
29
|
+
import type { Kysely as KyselyType } from 'kysely';
|
|
30
|
+
import type { Pool, PoolConfig } from 'pg';
|
|
31
|
+
import type { Logger } from '../core/types.js';
|
|
32
|
+
import { loadPostgresDeps } from '../utils/postgres-deps.js';
|
|
11
33
|
|
|
12
34
|
// Lazy-loaded dependencies - populated by loadPostgresDeps() during initialize()
|
|
13
35
|
let Kysely: any;
|
|
14
36
|
let PostgresDialect: any;
|
|
15
37
|
let sql: any;
|
|
16
38
|
let pgModule: any;
|
|
17
|
-
import { generateId } from
|
|
39
|
+
import { generateId } from '../utils/id.js';
|
|
18
40
|
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
|
-
} from
|
|
33
|
-
import
|
|
34
|
-
|
|
46
|
+
} from './types.js';
|
|
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
|
-
function stripStack<T extends Record<string, unknown>>(
|
|
38
|
-
error: T,
|
|
39
|
-
): Omit<T, "stack"> {
|
|
60
|
+
function stripStack<T extends Record<string, unknown>>(error: T): Omit<T, 'stack'> {
|
|
40
61
|
const { stack: _stack, ...rest } = error;
|
|
41
|
-
return rest as Omit<T,
|
|
62
|
+
return rest as Omit<T, 'stack'>;
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
// ============================================================================
|
|
45
66
|
// Database Types (Kysely schema)
|
|
46
67
|
// ============================================================================
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
kind: string;
|
|
53
|
-
/** Current run status (maps to RunStatus). */
|
|
54
|
-
status: string;
|
|
55
|
-
/** Parent run ID for child workflows, null for top-level runs. */
|
|
56
|
-
parent_run_id: string | null;
|
|
57
|
-
/** JSON-serialized input payload. */
|
|
58
|
-
input_json: string;
|
|
59
|
-
/** JSON-serialized arbitrary metadata. */
|
|
60
|
-
metadata_json: string;
|
|
61
|
-
/** JSON-serialized workflow context (accumulated step results). */
|
|
62
|
-
context_json: string;
|
|
63
|
-
/** JSON-serialized output payload, null until run completes. */
|
|
64
|
-
output_json: string | null;
|
|
65
|
-
/** JSON-serialized error object, null unless run failed. */
|
|
66
|
-
error_json: string | null;
|
|
67
|
-
/** Execution priority (lower number = higher precedence). */
|
|
68
|
-
priority: number;
|
|
69
|
-
/** Workflow-level timeout in milliseconds, null for no timeout. */
|
|
70
|
-
timeout_ms: number | null;
|
|
71
|
-
created_at: Date;
|
|
72
|
-
started_at: Date | null;
|
|
73
|
-
finished_at: Date | null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Kysely row type for the workflow_run_steps table. */
|
|
77
|
-
interface WorkflowRunStepsTable {
|
|
78
|
-
id: string;
|
|
79
|
-
/** Foreign key to workflow_runs.id. */
|
|
80
|
-
run_id: string;
|
|
81
|
-
/** Unique step identifier within the workflow definition. */
|
|
82
|
-
step_key: string;
|
|
83
|
-
/** Human-readable step name. */
|
|
84
|
-
step_name: string;
|
|
85
|
-
/** Current step status (maps to StepStatus). */
|
|
86
|
-
status: string;
|
|
87
|
-
/** Current retry attempt number (1-based). */
|
|
88
|
-
attempt: number;
|
|
89
|
-
/** JSON-serialized step result, null until step completes. */
|
|
90
|
-
result_json: string | null;
|
|
91
|
-
/** JSON-serialized error object, null unless step failed. */
|
|
92
|
-
error_json: string | null;
|
|
93
|
-
started_at: Date | null;
|
|
94
|
-
finished_at: Date | null;
|
|
95
|
-
}
|
|
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).
|
|
96
73
|
|
|
97
74
|
/** Kysely row type for the stepflow_step_results table (extended storage). */
|
|
98
75
|
interface StepflowStepResultsTable {
|
|
@@ -113,26 +90,7 @@ interface StepflowStepResultsTable {
|
|
|
113
90
|
completed_at: Date | null;
|
|
114
91
|
}
|
|
115
92
|
|
|
116
|
-
|
|
117
|
-
interface WorkflowEventsTable {
|
|
118
|
-
id: string;
|
|
119
|
-
/** Foreign key to workflow_runs.id. */
|
|
120
|
-
run_id: string;
|
|
121
|
-
/** Step key that emitted this event, null for run-level events. */
|
|
122
|
-
step_key: string | null;
|
|
123
|
-
/** Event type identifier (e.g. 'run.started', 'step.completed'). */
|
|
124
|
-
event_type: string;
|
|
125
|
-
/** Severity level: 'info', 'warn', or 'error'. */
|
|
126
|
-
level: string;
|
|
127
|
-
/** JSON-serialized event payload, null if no extra data. */
|
|
128
|
-
payload_json: string | null;
|
|
129
|
-
timestamp: Date;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface StepflowDatabase {
|
|
133
|
-
runs: WorkflowRunsTable;
|
|
134
|
-
workflow_run_steps: WorkflowRunStepsTable;
|
|
135
|
-
workflow_events: WorkflowEventsTable;
|
|
93
|
+
interface StepflowDatabase extends PostgresCoreDatabase {
|
|
136
94
|
stepflow_step_results: StepflowStepResultsTable;
|
|
137
95
|
}
|
|
138
96
|
|
|
@@ -172,6 +130,13 @@ export interface PostgresStorageConfig {
|
|
|
172
130
|
* @default true
|
|
173
131
|
*/
|
|
174
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;
|
|
175
140
|
}
|
|
176
141
|
|
|
177
142
|
// ============================================================================
|
|
@@ -216,39 +181,59 @@ export interface PostgresStorageConfig {
|
|
|
216
181
|
* const storage = new PostgresStorageAdapter({ pool });
|
|
217
182
|
* ```
|
|
218
183
|
*/
|
|
219
|
-
export class PostgresStorageAdapter
|
|
184
|
+
export class PostgresStorageAdapter extends PostgresStorageCore {
|
|
220
185
|
private db!: KyselyType<StepflowDatabase>;
|
|
221
186
|
private pool!: Pool;
|
|
222
187
|
private ownsPool = false;
|
|
223
|
-
private schema: string;
|
|
224
188
|
private autoMigrate: boolean;
|
|
225
189
|
private initialized = false;
|
|
226
190
|
private config: PostgresStorageConfig;
|
|
227
191
|
|
|
228
192
|
constructor(config: PostgresStorageConfig) {
|
|
229
|
-
|
|
230
|
-
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)) {
|
|
231
195
|
throw new Error(
|
|
232
|
-
`Invalid schema name "${
|
|
233
|
-
|
|
196
|
+
`Invalid schema name "${schema}". Schema must start with a letter or underscore, ` +
|
|
197
|
+
`contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
234
198
|
);
|
|
235
199
|
}
|
|
200
|
+
super(schema, config.logger);
|
|
236
201
|
this.autoMigrate = config.autoMigrate !== false;
|
|
237
202
|
this.config = config;
|
|
238
203
|
}
|
|
239
204
|
|
|
240
205
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
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.
|
|
243
209
|
*/
|
|
244
|
-
|
|
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> {
|
|
245
220
|
return this.db.withSchema(this.schema);
|
|
246
221
|
}
|
|
247
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
|
+
|
|
248
233
|
private ensureInitialized(): void {
|
|
249
234
|
if (!this.initialized) {
|
|
250
235
|
throw new Error(
|
|
251
|
-
|
|
236
|
+
'PostgresStorageAdapter is not initialized. Call initialize() before using the adapter.'
|
|
252
237
|
);
|
|
253
238
|
}
|
|
254
239
|
}
|
|
@@ -272,16 +257,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
272
257
|
this.pool = this.config.pool;
|
|
273
258
|
this.ownsPool = false;
|
|
274
259
|
} else if (this.config.connectionString) {
|
|
275
|
-
this.pool = new pgModule.Pool({
|
|
276
|
-
connectionString: this.config.connectionString,
|
|
277
|
-
});
|
|
260
|
+
this.pool = new pgModule.Pool({ connectionString: this.config.connectionString });
|
|
278
261
|
this.ownsPool = true;
|
|
279
262
|
} else if (this.config.poolConfig) {
|
|
280
263
|
this.pool = new pgModule.Pool(this.config.poolConfig);
|
|
281
264
|
this.ownsPool = true;
|
|
282
265
|
} else {
|
|
283
266
|
throw new Error(
|
|
284
|
-
|
|
267
|
+
'PostgresStorageConfig must include either pool, connectionString, or poolConfig'
|
|
285
268
|
);
|
|
286
269
|
}
|
|
287
270
|
|
|
@@ -312,15 +295,54 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
312
295
|
// Schema Creation
|
|
313
296
|
// ============================================================================
|
|
314
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
|
+
|
|
315
339
|
/**
|
|
316
340
|
* Create the workflow tables if they don't exist.
|
|
317
341
|
*/
|
|
318
342
|
private async createTables(): Promise<void> {
|
|
319
343
|
// Create schema if not public
|
|
320
|
-
if (this.schema !==
|
|
321
|
-
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(
|
|
322
|
-
this.db,
|
|
323
|
-
);
|
|
344
|
+
if (this.schema !== 'public') {
|
|
345
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(this.db);
|
|
324
346
|
}
|
|
325
347
|
|
|
326
348
|
// Create runs table with new columns
|
|
@@ -347,16 +369,15 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
347
369
|
`.execute(this.db);
|
|
348
370
|
|
|
349
371
|
// Add new columns if they don't exist (for migrations)
|
|
350
|
-
await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.
|
|
358
|
-
|
|
359
|
-
});
|
|
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
|
+
);
|
|
360
381
|
|
|
361
382
|
// Create indexes for runs
|
|
362
383
|
await sql`
|
|
@@ -473,321 +494,14 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
473
494
|
}
|
|
474
495
|
|
|
475
496
|
// ============================================================================
|
|
476
|
-
// Run
|
|
477
|
-
// ============================================================================
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Create a new workflow run.
|
|
481
|
-
* Supports both legacy and new CreateRunInput interfaces.
|
|
482
|
-
*/
|
|
483
|
-
async createRun(
|
|
484
|
-
run: CreateRunInput | Omit<WorkflowRunRecord, "id" | "createdAt">,
|
|
485
|
-
): Promise<WorkflowRunRecord> {
|
|
486
|
-
this.ensureInitialized();
|
|
487
|
-
const id = "id" in run && run.id ? run.id : generateId();
|
|
488
|
-
const createdAt = new Date();
|
|
489
|
-
|
|
490
|
-
await this.qb
|
|
491
|
-
.insertInto("runs")
|
|
492
|
-
.values({
|
|
493
|
-
id,
|
|
494
|
-
kind: run.kind,
|
|
495
|
-
status: run.status,
|
|
496
|
-
parent_run_id: "parentRunId" in run ? (run.parentRunId ?? null) : null,
|
|
497
|
-
input_json: JSON.stringify(run.input),
|
|
498
|
-
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
499
|
-
context_json: JSON.stringify(run.context ?? {}), // Default to empty object
|
|
500
|
-
output_json: null,
|
|
501
|
-
error_json:
|
|
502
|
-
"error" in run && run.error
|
|
503
|
-
? JSON.stringify(sanitizeErrorForStorage(run.error))
|
|
504
|
-
: null,
|
|
505
|
-
priority: "priority" in run ? (run.priority ?? 0) : 0,
|
|
506
|
-
timeout_ms: "timeoutMs" in run ? (run.timeoutMs ?? null) : null,
|
|
507
|
-
created_at: createdAt,
|
|
508
|
-
started_at: null,
|
|
509
|
-
finished_at: null,
|
|
510
|
-
})
|
|
511
|
-
.execute();
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
id,
|
|
515
|
-
kind: run.kind,
|
|
516
|
-
status: run.status as RunStatus,
|
|
517
|
-
parentRunId: "parentRunId" in run ? run.parentRunId : undefined,
|
|
518
|
-
input: run.input,
|
|
519
|
-
context: run.context ?? {},
|
|
520
|
-
output: undefined,
|
|
521
|
-
error: undefined,
|
|
522
|
-
metadata: run.metadata ?? {},
|
|
523
|
-
priority: "priority" in run ? (run.priority ?? 0) : 0,
|
|
524
|
-
timeoutMs: "timeoutMs" in run ? run.timeoutMs : undefined,
|
|
525
|
-
createdAt,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
async getRun(runId: string): Promise<WorkflowRunRecord | null> {
|
|
530
|
-
this.ensureInitialized();
|
|
531
|
-
const row = await this.qb
|
|
532
|
-
.selectFrom("runs")
|
|
533
|
-
.selectAll()
|
|
534
|
-
.where("id", "=", runId)
|
|
535
|
-
.executeTakeFirst();
|
|
536
|
-
|
|
537
|
-
return row ? this.mapRunRow(row) : null;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Update a workflow run.
|
|
542
|
-
* Supports both legacy Partial<WorkflowRunRecord> and new UpdateRunInput interfaces.
|
|
543
|
-
*/
|
|
544
|
-
async updateRun(
|
|
545
|
-
runId: string,
|
|
546
|
-
updates: UpdateRunInput | Partial<WorkflowRunRecord>,
|
|
547
|
-
): Promise<void> {
|
|
548
|
-
this.ensureInitialized();
|
|
549
|
-
const updateData: Partial<WorkflowRunsTable> = {};
|
|
550
|
-
|
|
551
|
-
if (updates.status !== undefined) {
|
|
552
|
-
updateData.status = updates.status;
|
|
553
|
-
}
|
|
554
|
-
if (updates.context !== undefined) {
|
|
555
|
-
updateData.context_json = JSON.stringify(updates.context);
|
|
556
|
-
}
|
|
557
|
-
if ("output" in updates && updates.output !== undefined) {
|
|
558
|
-
updateData.output_json = JSON.stringify(updates.output);
|
|
559
|
-
}
|
|
560
|
-
if (updates.error !== undefined) {
|
|
561
|
-
updateData.error_json = JSON.stringify(
|
|
562
|
-
sanitizeErrorForStorage(updates.error),
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
if (updates.startedAt !== undefined) {
|
|
566
|
-
updateData.started_at = updates.startedAt;
|
|
567
|
-
}
|
|
568
|
-
if (updates.finishedAt !== undefined) {
|
|
569
|
-
updateData.finished_at = updates.finishedAt;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (Object.keys(updateData).length > 0) {
|
|
573
|
-
await this.qb
|
|
574
|
-
.updateTable("runs")
|
|
575
|
-
.set(updateData)
|
|
576
|
-
.where("id", "=", runId)
|
|
577
|
-
.execute();
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* List workflow runs with filtering and pagination.
|
|
583
|
-
*/
|
|
584
|
-
/**
|
|
585
|
-
* Apply common run filters to a Kysely query builder.
|
|
586
|
-
* Used by both the data query and count query in listRuns to avoid duplication.
|
|
587
|
-
*/
|
|
588
|
-
private applyRunsFilters<T extends { where(col: any, op: any, val: any): T }>(
|
|
589
|
-
query: T,
|
|
590
|
-
options: ListRunsOptions,
|
|
591
|
-
): T {
|
|
592
|
-
if (options.kind) {
|
|
593
|
-
query = query.where("kind", "=", options.kind);
|
|
594
|
-
}
|
|
595
|
-
if (options.status) {
|
|
596
|
-
const statuses = Array.isArray(options.status)
|
|
597
|
-
? options.status
|
|
598
|
-
: [options.status];
|
|
599
|
-
query = query.where("status", "in", statuses);
|
|
600
|
-
}
|
|
601
|
-
if (options.parentRunId !== undefined) {
|
|
602
|
-
query = query.where("parent_run_id", "=", options.parentRunId);
|
|
603
|
-
}
|
|
604
|
-
return query;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async listRuns(
|
|
608
|
-
options: ListRunsOptions = {},
|
|
609
|
-
): Promise<PaginatedResult<WorkflowRunRecord>> {
|
|
610
|
-
this.ensureInitialized();
|
|
611
|
-
const limit = options.limit ?? 50;
|
|
612
|
-
const offset = options.offset ?? 0;
|
|
613
|
-
|
|
614
|
-
let query = this.applyRunsFilters(
|
|
615
|
-
this.qb.selectFrom("runs").selectAll(),
|
|
616
|
-
options,
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
const countQuery = this.applyRunsFilters(
|
|
620
|
-
this.qb.selectFrom("runs").select(sql<number>`count(*)`.as("count")),
|
|
621
|
-
options,
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
const countResult = (await countQuery.executeTakeFirst()) as
|
|
625
|
-
| { count?: string | number }
|
|
626
|
-
| undefined;
|
|
627
|
-
const total = Number(countResult?.count ?? 0);
|
|
628
|
-
|
|
629
|
-
// Order
|
|
630
|
-
const orderBy = options.orderBy ?? "createdAt";
|
|
631
|
-
const orderDirection = options.orderDirection ?? "desc";
|
|
632
|
-
const orderColumn =
|
|
633
|
-
orderBy === "createdAt"
|
|
634
|
-
? "created_at"
|
|
635
|
-
: orderBy === "startedAt"
|
|
636
|
-
? "started_at"
|
|
637
|
-
: "finished_at";
|
|
638
|
-
|
|
639
|
-
query = query.orderBy(orderColumn, orderDirection);
|
|
640
|
-
|
|
641
|
-
// Apply pagination
|
|
642
|
-
query = query.limit(limit).offset(offset);
|
|
643
|
-
|
|
644
|
-
const rows = await query.execute();
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
items: rows.map((row) => this.mapRunRow(row)),
|
|
648
|
-
total,
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// ============================================================================
|
|
653
|
-
// Step Operations
|
|
654
|
-
// ============================================================================
|
|
655
|
-
|
|
656
|
-
async createStep(
|
|
657
|
-
step: Omit<WorkflowRunStepRecord, "id">,
|
|
658
|
-
): Promise<WorkflowRunStepRecord> {
|
|
659
|
-
this.ensureInitialized();
|
|
660
|
-
const id = generateId();
|
|
661
|
-
|
|
662
|
-
await this.qb
|
|
663
|
-
.insertInto("workflow_run_steps")
|
|
664
|
-
.values({
|
|
665
|
-
id,
|
|
666
|
-
run_id: step.runId,
|
|
667
|
-
step_key: step.stepKey,
|
|
668
|
-
step_name: step.stepName,
|
|
669
|
-
status: step.status,
|
|
670
|
-
attempt: step.attempt,
|
|
671
|
-
result_json:
|
|
672
|
-
step.result !== undefined ? JSON.stringify(step.result) : null,
|
|
673
|
-
error_json: step.error
|
|
674
|
-
? JSON.stringify(sanitizeErrorForStorage(step.error))
|
|
675
|
-
: null,
|
|
676
|
-
started_at: step.startedAt ?? null,
|
|
677
|
-
finished_at: step.finishedAt ?? null,
|
|
678
|
-
})
|
|
679
|
-
.execute();
|
|
680
|
-
|
|
681
|
-
return { ...step, id };
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
async getStep(stepId: string): Promise<WorkflowRunStepRecord | null> {
|
|
685
|
-
this.ensureInitialized();
|
|
686
|
-
const row = await this.qb
|
|
687
|
-
.selectFrom("workflow_run_steps")
|
|
688
|
-
.selectAll()
|
|
689
|
-
.where("id", "=", stepId)
|
|
690
|
-
.executeTakeFirst();
|
|
691
|
-
|
|
692
|
-
return row ? this.mapStepRow(row) : null;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
async updateStep(
|
|
696
|
-
stepId: string,
|
|
697
|
-
updates: Partial<WorkflowRunStepRecord>,
|
|
698
|
-
): Promise<void> {
|
|
699
|
-
this.ensureInitialized();
|
|
700
|
-
const updateData: Partial<WorkflowRunStepsTable> = {};
|
|
701
|
-
|
|
702
|
-
if (updates.status !== undefined) {
|
|
703
|
-
updateData.status = updates.status;
|
|
704
|
-
}
|
|
705
|
-
if (updates.attempt !== undefined) {
|
|
706
|
-
updateData.attempt = updates.attempt;
|
|
707
|
-
}
|
|
708
|
-
if (updates.result !== undefined) {
|
|
709
|
-
updateData.result_json = JSON.stringify(updates.result);
|
|
710
|
-
}
|
|
711
|
-
if (updates.error !== undefined) {
|
|
712
|
-
updateData.error_json = JSON.stringify(
|
|
713
|
-
sanitizeErrorForStorage(updates.error),
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
if (updates.finishedAt !== undefined) {
|
|
717
|
-
updateData.finished_at = updates.finishedAt;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (Object.keys(updateData).length > 0) {
|
|
721
|
-
await this.qb
|
|
722
|
-
.updateTable("workflow_run_steps")
|
|
723
|
-
.set(updateData)
|
|
724
|
-
.where("id", "=", stepId)
|
|
725
|
-
.execute();
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
|
|
730
|
-
this.ensureInitialized();
|
|
731
|
-
const rows = await this.qb
|
|
732
|
-
.selectFrom("workflow_run_steps")
|
|
733
|
-
.selectAll()
|
|
734
|
-
.where("run_id", "=", runId)
|
|
735
|
-
.orderBy("started_at", "asc")
|
|
736
|
-
.execute();
|
|
737
|
-
|
|
738
|
-
return rows.map((row) => this.mapStepRow(row));
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
// ============================================================================
|
|
742
|
-
// Event Operations
|
|
497
|
+
// Run / Step / Event CRUD
|
|
743
498
|
// ============================================================================
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
.insertInto("workflow_events")
|
|
751
|
-
.values({
|
|
752
|
-
id,
|
|
753
|
-
run_id: event.runId,
|
|
754
|
-
step_key: event.stepKey ?? null,
|
|
755
|
-
event_type: event.eventType,
|
|
756
|
-
level: event.level,
|
|
757
|
-
payload_json:
|
|
758
|
-
event.payload !== undefined ? JSON.stringify(event.payload) : null,
|
|
759
|
-
timestamp: event.timestamp,
|
|
760
|
-
})
|
|
761
|
-
.execute();
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
async getEventsForRun(
|
|
765
|
-
runId: string,
|
|
766
|
-
options: ListEventsOptions = {},
|
|
767
|
-
): Promise<WorkflowEventRecord[]> {
|
|
768
|
-
this.ensureInitialized();
|
|
769
|
-
const limit = options.limit ?? 1000;
|
|
770
|
-
const offset = options.offset ?? 0;
|
|
771
|
-
|
|
772
|
-
let query = this.qb
|
|
773
|
-
.selectFrom("workflow_events")
|
|
774
|
-
.selectAll()
|
|
775
|
-
.where("run_id", "=", runId);
|
|
776
|
-
|
|
777
|
-
if (options.stepKey) {
|
|
778
|
-
query = query.where("step_key", "=", options.stepKey);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (options.level) {
|
|
782
|
-
query = query.where("level", "=", options.level);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
query = query.orderBy("timestamp", "asc").limit(limit).offset(offset);
|
|
786
|
-
|
|
787
|
-
const rows = await query.execute();
|
|
788
|
-
|
|
789
|
-
return rows.map((row) => this.mapEventRow(row));
|
|
790
|
-
}
|
|
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()`.
|
|
791
505
|
|
|
792
506
|
// ============================================================================
|
|
793
507
|
// Transaction Support
|
|
@@ -800,7 +514,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
800
514
|
this.ensureInitialized();
|
|
801
515
|
return await this.db.transaction().execute(async (trx) => {
|
|
802
516
|
// Create a transactional adapter wrapper
|
|
803
|
-
const txAdapter = new PostgresTransactionAdapter(trx, this.schema);
|
|
517
|
+
const txAdapter = new PostgresTransactionAdapter(trx, this.schema, this.logger);
|
|
804
518
|
return await fn(txAdapter);
|
|
805
519
|
});
|
|
806
520
|
}
|
|
@@ -816,8 +530,8 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
816
530
|
async deleteOldRuns(olderThan: Date): Promise<number> {
|
|
817
531
|
this.ensureInitialized();
|
|
818
532
|
const result = await this.qb
|
|
819
|
-
.deleteFrom(
|
|
820
|
-
.where(
|
|
533
|
+
.deleteFrom('runs')
|
|
534
|
+
.where('created_at', '<', olderThan)
|
|
821
535
|
.executeTakeFirst();
|
|
822
536
|
|
|
823
537
|
return Number(result.numDeletedRows);
|
|
@@ -834,33 +548,31 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
834
548
|
async getInterruptedRuns(): Promise<WorkflowRunRecord[]> {
|
|
835
549
|
this.ensureInitialized();
|
|
836
550
|
const rows = await this.qb
|
|
837
|
-
.selectFrom(
|
|
551
|
+
.selectFrom('runs')
|
|
838
552
|
.selectAll()
|
|
839
|
-
.where(
|
|
840
|
-
.orderBy(
|
|
553
|
+
.where('status', 'in', ['queued', 'running'])
|
|
554
|
+
.orderBy('created_at', 'asc')
|
|
841
555
|
.execute();
|
|
842
556
|
|
|
843
|
-
return rows.map(
|
|
557
|
+
return rows.map(row => mapRunRow(row as WorkflowRunsTable, this.mapperCtx));
|
|
844
558
|
}
|
|
845
559
|
|
|
846
560
|
/**
|
|
847
561
|
* Get the last completed step for a run.
|
|
848
562
|
* Useful for resuming from a checkpoint.
|
|
849
563
|
*/
|
|
850
|
-
async getLastCompletedStep(
|
|
851
|
-
runId: string,
|
|
852
|
-
): Promise<WorkflowRunStepRecord | null> {
|
|
564
|
+
async getLastCompletedStep(runId: string): Promise<WorkflowRunStepRecord | null> {
|
|
853
565
|
this.ensureInitialized();
|
|
854
566
|
const row = await this.qb
|
|
855
|
-
.selectFrom(
|
|
567
|
+
.selectFrom('workflow_run_steps')
|
|
856
568
|
.selectAll()
|
|
857
|
-
.where(
|
|
858
|
-
.where(
|
|
859
|
-
.orderBy(
|
|
569
|
+
.where('run_id', '=', runId)
|
|
570
|
+
.where('status', '=', 'succeeded')
|
|
571
|
+
.orderBy('finished_at', 'desc')
|
|
860
572
|
.limit(1)
|
|
861
573
|
.executeTakeFirst();
|
|
862
574
|
|
|
863
|
-
return row ?
|
|
575
|
+
return row ? mapStepRow(row as WorkflowRunStepsTable, this.mapperCtx) : null;
|
|
864
576
|
}
|
|
865
577
|
|
|
866
578
|
// ============================================================================
|
|
@@ -874,15 +586,12 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
874
586
|
* @param workflowKinds - Optional list of workflow kinds to filter by
|
|
875
587
|
* @returns The dequeued run, or null if no runs are available
|
|
876
588
|
*/
|
|
877
|
-
async dequeueRun(
|
|
878
|
-
workflowKinds?: string[],
|
|
879
|
-
): Promise<WorkflowRunRecord | null> {
|
|
589
|
+
async dequeueRun(workflowKinds?: string[]): Promise<WorkflowRunRecord | null> {
|
|
880
590
|
this.ensureInitialized();
|
|
881
591
|
// Use raw SQL for FOR UPDATE SKIP LOCKED
|
|
882
|
-
const kindFilter =
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
: sql``;
|
|
592
|
+
const kindFilter = workflowKinds && workflowKinds.length > 0
|
|
593
|
+
? sql`AND kind = ANY(${workflowKinds}::text[])`
|
|
594
|
+
: sql``;
|
|
886
595
|
|
|
887
596
|
const result = await sql<WorkflowRunsTable>`
|
|
888
597
|
UPDATE ${sql.table(`${this.schema}.runs`)}
|
|
@@ -898,148 +607,40 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
898
607
|
`.execute(this.db);
|
|
899
608
|
|
|
900
609
|
const row = result.rows[0];
|
|
901
|
-
return row ?
|
|
610
|
+
return row ? mapRunRow(row, this.mapperCtx) : null;
|
|
902
611
|
}
|
|
903
612
|
|
|
904
613
|
// ============================================================================
|
|
905
|
-
// Row Mapping
|
|
614
|
+
// Row Mapping (adapter-only tables)
|
|
906
615
|
// ============================================================================
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
kind: row.kind,
|
|
912
|
-
status: row.status as RunStatus,
|
|
913
|
-
parentRunId: row.parent_run_id ?? undefined,
|
|
914
|
-
input:
|
|
915
|
-
typeof row.input_json === "string"
|
|
916
|
-
? JSON.parse(row.input_json)
|
|
917
|
-
: row.input_json,
|
|
918
|
-
context:
|
|
919
|
-
typeof row.context_json === "string"
|
|
920
|
-
? JSON.parse(row.context_json)
|
|
921
|
-
: row.context_json,
|
|
922
|
-
output: row.output_json
|
|
923
|
-
? typeof row.output_json === "string"
|
|
924
|
-
? JSON.parse(row.output_json)
|
|
925
|
-
: row.output_json
|
|
926
|
-
: undefined,
|
|
927
|
-
error: row.error_json
|
|
928
|
-
? typeof row.error_json === "string"
|
|
929
|
-
? JSON.parse(row.error_json)
|
|
930
|
-
: row.error_json
|
|
931
|
-
: undefined,
|
|
932
|
-
metadata:
|
|
933
|
-
typeof row.metadata_json === "string"
|
|
934
|
-
? JSON.parse(row.metadata_json)
|
|
935
|
-
: row.metadata_json,
|
|
936
|
-
priority: row.priority ?? 0,
|
|
937
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
938
|
-
createdAt: new Date(row.created_at),
|
|
939
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
940
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
/**
|
|
945
|
-
* Map a database row to an extended workflow run record.
|
|
946
|
-
*/
|
|
947
|
-
private mapExtendedRunRow(row: WorkflowRunsTable): ExtendedWorkflowRunRecord {
|
|
948
|
-
return {
|
|
949
|
-
id: row.id,
|
|
950
|
-
kind: row.kind,
|
|
951
|
-
status: row.status as ExtendedRunStatus,
|
|
952
|
-
input:
|
|
953
|
-
typeof row.input_json === "string"
|
|
954
|
-
? JSON.parse(row.input_json)
|
|
955
|
-
: row.input_json,
|
|
956
|
-
metadata:
|
|
957
|
-
typeof row.metadata_json === "string"
|
|
958
|
-
? JSON.parse(row.metadata_json)
|
|
959
|
-
: row.metadata_json,
|
|
960
|
-
context:
|
|
961
|
-
typeof row.context_json === "string"
|
|
962
|
-
? JSON.parse(row.context_json)
|
|
963
|
-
: row.context_json,
|
|
964
|
-
output: row.output_json
|
|
965
|
-
? typeof row.output_json === "string"
|
|
966
|
-
? JSON.parse(row.output_json)
|
|
967
|
-
: row.output_json
|
|
968
|
-
: undefined,
|
|
969
|
-
error: row.error_json
|
|
970
|
-
? typeof row.error_json === "string"
|
|
971
|
-
? JSON.parse(row.error_json)
|
|
972
|
-
: row.error_json
|
|
973
|
-
: undefined,
|
|
974
|
-
priority: row.priority ?? 0,
|
|
975
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
976
|
-
createdAt: new Date(row.created_at),
|
|
977
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
978
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
979
|
-
};
|
|
980
|
-
}
|
|
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.
|
|
981
620
|
|
|
982
621
|
private mapStepResultRow(row: StepflowStepResultsTable): StepResult {
|
|
622
|
+
const ctx = this.mapperCtx;
|
|
983
623
|
return {
|
|
984
624
|
id: row.id,
|
|
985
625
|
runId: row.run_id,
|
|
986
626
|
stepName: row.step_name,
|
|
987
627
|
status: row.status as ExtendedStepStatus,
|
|
988
|
-
output: row.output_json
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
error: row.error_json
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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,
|
|
998
638
|
attempt: row.attempt,
|
|
999
639
|
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1000
640
|
completedAt: row.completed_at ? new Date(row.completed_at) : undefined,
|
|
1001
641
|
};
|
|
1002
642
|
}
|
|
1003
643
|
|
|
1004
|
-
private mapStepRow(row: WorkflowRunStepsTable): WorkflowRunStepRecord {
|
|
1005
|
-
return {
|
|
1006
|
-
id: row.id,
|
|
1007
|
-
runId: row.run_id,
|
|
1008
|
-
stepKey: row.step_key,
|
|
1009
|
-
stepName: row.step_name,
|
|
1010
|
-
status: row.status as StepStatus,
|
|
1011
|
-
attempt: row.attempt,
|
|
1012
|
-
result: row.result_json
|
|
1013
|
-
? typeof row.result_json === "string"
|
|
1014
|
-
? JSON.parse(row.result_json)
|
|
1015
|
-
: row.result_json
|
|
1016
|
-
: undefined,
|
|
1017
|
-
error: row.error_json
|
|
1018
|
-
? typeof row.error_json === "string"
|
|
1019
|
-
? JSON.parse(row.error_json)
|
|
1020
|
-
: row.error_json
|
|
1021
|
-
: undefined,
|
|
1022
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1023
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
private mapEventRow(row: WorkflowEventsTable): WorkflowEventRecord {
|
|
1028
|
-
return {
|
|
1029
|
-
id: row.id,
|
|
1030
|
-
runId: row.run_id,
|
|
1031
|
-
stepKey: row.step_key ?? undefined,
|
|
1032
|
-
eventType: row.event_type,
|
|
1033
|
-
level: row.level as "info" | "warn" | "error",
|
|
1034
|
-
payload: row.payload_json
|
|
1035
|
-
? typeof row.payload_json === "string"
|
|
1036
|
-
? JSON.parse(row.payload_json)
|
|
1037
|
-
: row.payload_json
|
|
1038
|
-
: undefined,
|
|
1039
|
-
timestamp: new Date(row.timestamp),
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
644
|
// ============================================================================
|
|
1044
645
|
// New WorkflowStorage Methods
|
|
1045
646
|
// ============================================================================
|
|
@@ -1050,7 +651,10 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1050
651
|
*/
|
|
1051
652
|
async deleteRun(id: string): Promise<void> {
|
|
1052
653
|
this.ensureInitialized();
|
|
1053
|
-
await this.qb
|
|
654
|
+
await this.qb
|
|
655
|
+
.deleteFrom('runs')
|
|
656
|
+
.where('id', '=', id)
|
|
657
|
+
.execute();
|
|
1054
658
|
}
|
|
1055
659
|
|
|
1056
660
|
/**
|
|
@@ -1112,16 +716,13 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1112
716
|
/**
|
|
1113
717
|
* Get a specific step result by run ID and step name.
|
|
1114
718
|
*/
|
|
1115
|
-
async getStepResult(
|
|
1116
|
-
runId: string,
|
|
1117
|
-
stepName: string,
|
|
1118
|
-
): Promise<StepResult | undefined> {
|
|
719
|
+
async getStepResult(runId: string, stepName: string): Promise<StepResult | undefined> {
|
|
1119
720
|
this.ensureInitialized();
|
|
1120
|
-
const row = await this.
|
|
1121
|
-
.selectFrom(
|
|
721
|
+
const row = await this.extQb
|
|
722
|
+
.selectFrom('stepflow_step_results')
|
|
1122
723
|
.selectAll()
|
|
1123
|
-
.where(
|
|
1124
|
-
.where(
|
|
724
|
+
.where('run_id', '=', runId)
|
|
725
|
+
.where('step_name', '=', stepName)
|
|
1125
726
|
.executeTakeFirst();
|
|
1126
727
|
|
|
1127
728
|
return row ? this.mapStepResultRow(row) : undefined;
|
|
@@ -1132,52 +733,46 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1132
733
|
*/
|
|
1133
734
|
async getStepResults(runId: string): Promise<StepResult[]> {
|
|
1134
735
|
this.ensureInitialized();
|
|
1135
|
-
const rows = await this.
|
|
1136
|
-
.selectFrom(
|
|
736
|
+
const rows = await this.extQb
|
|
737
|
+
.selectFrom('stepflow_step_results')
|
|
1137
738
|
.selectAll()
|
|
1138
|
-
.where(
|
|
1139
|
-
.orderBy(
|
|
739
|
+
.where('run_id', '=', runId)
|
|
740
|
+
.orderBy('started_at', 'asc')
|
|
1140
741
|
.execute();
|
|
1141
742
|
|
|
1142
|
-
return rows.map(
|
|
743
|
+
return rows.map(row => this.mapStepResultRow(row));
|
|
1143
744
|
}
|
|
1144
745
|
|
|
1145
746
|
/**
|
|
1146
747
|
* Save or update a step result.
|
|
1147
748
|
* Uses upsert to handle both new and existing results.
|
|
1148
749
|
*/
|
|
1149
|
-
async saveStepResult(
|
|
1150
|
-
result: Omit<StepResult, "id"> & { id?: string },
|
|
1151
|
-
): Promise<void> {
|
|
750
|
+
async saveStepResult(result: Omit<StepResult, 'id'> & { id?: string }): Promise<void> {
|
|
1152
751
|
this.ensureInitialized();
|
|
1153
752
|
const id = result.id ?? generateId();
|
|
1154
753
|
|
|
1155
|
-
await this.
|
|
1156
|
-
.insertInto(
|
|
754
|
+
await this.extQb
|
|
755
|
+
.insertInto('stepflow_step_results')
|
|
1157
756
|
.values({
|
|
1158
757
|
id,
|
|
1159
758
|
run_id: result.runId,
|
|
1160
759
|
step_name: result.stepName,
|
|
1161
760
|
status: result.status,
|
|
1162
761
|
output_json: result.output ? JSON.stringify(result.output) : null,
|
|
1163
|
-
error_json: result.error
|
|
1164
|
-
? JSON.stringify(stripStack(result.error))
|
|
1165
|
-
: null,
|
|
762
|
+
error_json: result.error ? JSON.stringify(stripStack(result.error)) : null,
|
|
1166
763
|
attempt: result.attempt,
|
|
1167
764
|
started_at: result.startedAt ?? null,
|
|
1168
765
|
completed_at: result.completedAt ?? null,
|
|
1169
766
|
})
|
|
1170
|
-
.onConflict(
|
|
1171
|
-
oc.columns([
|
|
767
|
+
.onConflict(oc =>
|
|
768
|
+
oc.columns(['run_id', 'step_name']).doUpdateSet({
|
|
1172
769
|
status: result.status,
|
|
1173
770
|
output_json: result.output ? JSON.stringify(result.output) : null,
|
|
1174
|
-
error_json: result.error
|
|
1175
|
-
? JSON.stringify(stripStack(result.error))
|
|
1176
|
-
: null,
|
|
771
|
+
error_json: result.error ? JSON.stringify(stripStack(result.error)) : null,
|
|
1177
772
|
attempt: result.attempt,
|
|
1178
773
|
started_at: result.startedAt ?? null,
|
|
1179
774
|
completed_at: result.completedAt ?? null,
|
|
1180
|
-
})
|
|
775
|
+
})
|
|
1181
776
|
)
|
|
1182
777
|
.execute();
|
|
1183
778
|
}
|
|
@@ -1191,20 +786,11 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1191
786
|
*/
|
|
1192
787
|
async getStats(): Promise<{ runs: number; steps: number; events: number }> {
|
|
1193
788
|
this.ensureInitialized();
|
|
1194
|
-
const [runsCount, stepsCount, eventsCount] =
|
|
1195
|
-
this.qb
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
this.qb
|
|
1200
|
-
.selectFrom("workflow_run_steps")
|
|
1201
|
-
.select(sql`count(*)`.as("count"))
|
|
1202
|
-
.executeTakeFirst(),
|
|
1203
|
-
this.qb
|
|
1204
|
-
.selectFrom("workflow_events")
|
|
1205
|
-
.select(sql`count(*)`.as("count"))
|
|
1206
|
-
.executeTakeFirst(),
|
|
1207
|
-
])) as { count?: string | number }[];
|
|
789
|
+
const [runsCount, stepsCount, eventsCount] = await Promise.all([
|
|
790
|
+
this.qb.selectFrom('runs').select(sql`count(*)`.as('count')).executeTakeFirst(),
|
|
791
|
+
this.qb.selectFrom('workflow_run_steps').select(sql`count(*)`.as('count')).executeTakeFirst(),
|
|
792
|
+
this.qb.selectFrom('workflow_events').select(sql`count(*)`.as('count')).executeTakeFirst(),
|
|
793
|
+
]) as { count?: string | number }[];
|
|
1208
794
|
|
|
1209
795
|
return {
|
|
1210
796
|
runs: Number(runsCount?.count ?? 0),
|
|
@@ -1220,328 +806,27 @@ export class PostgresStorageAdapter implements StorageAdapter {
|
|
|
1220
806
|
|
|
1221
807
|
/**
|
|
1222
808
|
* A StorageAdapter wrapper for use within transactions.
|
|
1223
|
-
*
|
|
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.
|
|
1224
815
|
*/
|
|
1225
|
-
class PostgresTransactionAdapter
|
|
1226
|
-
private
|
|
1227
|
-
|
|
1228
|
-
constructor(
|
|
1229
|
-
private trx: KyselyType<StepflowDatabase>,
|
|
1230
|
-
private schema: string,
|
|
1231
|
-
) {
|
|
1232
|
-
this.qb = trx.withSchema(schema);
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
async createRun(
|
|
1236
|
-
run: CreateRunInput | Omit<WorkflowRunRecord, "id" | "createdAt">,
|
|
1237
|
-
): Promise<WorkflowRunRecord> {
|
|
1238
|
-
const id = "id" in run && run.id ? run.id : generateId();
|
|
1239
|
-
const createdAt = new Date();
|
|
1240
|
-
|
|
1241
|
-
await this.qb
|
|
1242
|
-
.insertInto("runs")
|
|
1243
|
-
.values({
|
|
1244
|
-
id,
|
|
1245
|
-
kind: run.kind,
|
|
1246
|
-
status: run.status,
|
|
1247
|
-
parent_run_id: "parentRunId" in run ? (run.parentRunId ?? null) : null,
|
|
1248
|
-
input_json: JSON.stringify(run.input),
|
|
1249
|
-
metadata_json: JSON.stringify(run.metadata ?? {}),
|
|
1250
|
-
context_json: JSON.stringify(run.context ?? {}),
|
|
1251
|
-
output_json: null,
|
|
1252
|
-
error_json:
|
|
1253
|
-
"error" in run && run.error
|
|
1254
|
-
? JSON.stringify(sanitizeErrorForStorage(run.error))
|
|
1255
|
-
: null,
|
|
1256
|
-
priority: "priority" in run ? (run.priority ?? 0) : 0,
|
|
1257
|
-
timeout_ms: "timeoutMs" in run ? (run.timeoutMs ?? null) : null,
|
|
1258
|
-
created_at: createdAt,
|
|
1259
|
-
started_at: null,
|
|
1260
|
-
finished_at: null,
|
|
1261
|
-
})
|
|
1262
|
-
.execute();
|
|
1263
|
-
|
|
1264
|
-
return {
|
|
1265
|
-
id,
|
|
1266
|
-
kind: run.kind,
|
|
1267
|
-
status: run.status as RunStatus,
|
|
1268
|
-
parentRunId: "parentRunId" in run ? run.parentRunId : undefined,
|
|
1269
|
-
input: run.input,
|
|
1270
|
-
context: run.context ?? {},
|
|
1271
|
-
output: undefined,
|
|
1272
|
-
error: undefined,
|
|
1273
|
-
metadata: run.metadata ?? {},
|
|
1274
|
-
priority: "priority" in run ? (run.priority ?? 0) : 0,
|
|
1275
|
-
timeoutMs: "timeoutMs" in run ? run.timeoutMs : undefined,
|
|
1276
|
-
createdAt,
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
async getRun(runId: string): Promise<WorkflowRunRecord | null> {
|
|
1281
|
-
const row = await this.qb
|
|
1282
|
-
.selectFrom("runs")
|
|
1283
|
-
.selectAll()
|
|
1284
|
-
.where("id", "=", runId)
|
|
1285
|
-
.executeTakeFirst();
|
|
1286
|
-
|
|
1287
|
-
return row ? this.mapRunRow(row) : null;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
async updateRun(
|
|
1291
|
-
runId: string,
|
|
1292
|
-
updates: UpdateRunInput | Partial<WorkflowRunRecord>,
|
|
1293
|
-
): Promise<void> {
|
|
1294
|
-
const updateData: Partial<WorkflowRunsTable> = {};
|
|
1295
|
-
|
|
1296
|
-
if (updates.status !== undefined) updateData.status = updates.status;
|
|
1297
|
-
if (updates.context !== undefined)
|
|
1298
|
-
updateData.context_json = JSON.stringify(updates.context);
|
|
1299
|
-
if ("output" in updates && updates.output !== undefined)
|
|
1300
|
-
updateData.output_json = JSON.stringify(updates.output);
|
|
1301
|
-
if (updates.error !== undefined)
|
|
1302
|
-
updateData.error_json = JSON.stringify(
|
|
1303
|
-
sanitizeErrorForStorage(updates.error as WorkflowError),
|
|
1304
|
-
);
|
|
1305
|
-
if (updates.startedAt !== undefined)
|
|
1306
|
-
updateData.started_at = updates.startedAt;
|
|
1307
|
-
if (updates.finishedAt !== undefined)
|
|
1308
|
-
updateData.finished_at = updates.finishedAt;
|
|
1309
|
-
|
|
1310
|
-
if (Object.keys(updateData).length > 0) {
|
|
1311
|
-
await this.qb
|
|
1312
|
-
.updateTable("runs")
|
|
1313
|
-
.set(updateData)
|
|
1314
|
-
.where("id", "=", runId)
|
|
1315
|
-
.execute();
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
async listRuns(
|
|
1320
|
-
options: ListRunsOptions = {},
|
|
1321
|
-
): Promise<PaginatedResult<WorkflowRunRecord>> {
|
|
1322
|
-
const limit = options.limit ?? 50;
|
|
1323
|
-
const offset = options.offset ?? 0;
|
|
1324
|
-
|
|
1325
|
-
let query = this.qb.selectFrom("runs").selectAll();
|
|
1326
|
-
|
|
1327
|
-
if (options.kind) query = query.where("kind", "=", options.kind);
|
|
1328
|
-
if (options.status) {
|
|
1329
|
-
const statuses = Array.isArray(options.status)
|
|
1330
|
-
? options.status
|
|
1331
|
-
: [options.status];
|
|
1332
|
-
query = query.where("status", "in", statuses);
|
|
1333
|
-
}
|
|
1334
|
-
if (options.parentRunId !== undefined) {
|
|
1335
|
-
query = query.where("parent_run_id", "=", options.parentRunId);
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
const orderColumn =
|
|
1339
|
-
(options.orderBy ?? "createdAt") === "createdAt"
|
|
1340
|
-
? "created_at"
|
|
1341
|
-
: options.orderBy === "startedAt"
|
|
1342
|
-
? "started_at"
|
|
1343
|
-
: "finished_at";
|
|
1344
|
-
const orderDirection = options.orderDirection ?? "desc";
|
|
1345
|
-
|
|
1346
|
-
query = query
|
|
1347
|
-
.orderBy(orderColumn, orderDirection)
|
|
1348
|
-
.limit(limit)
|
|
1349
|
-
.offset(offset);
|
|
1350
|
-
|
|
1351
|
-
const rows = await query.execute();
|
|
1352
|
-
|
|
1353
|
-
return {
|
|
1354
|
-
items: rows.map((row) => this.mapRunRow(row)),
|
|
1355
|
-
total: rows.length, // Simplified for transaction context
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
async createStep(
|
|
1360
|
-
step: Omit<WorkflowRunStepRecord, "id">,
|
|
1361
|
-
): Promise<WorkflowRunStepRecord> {
|
|
1362
|
-
const id = generateId();
|
|
1363
|
-
|
|
1364
|
-
await this.qb
|
|
1365
|
-
.insertInto("workflow_run_steps")
|
|
1366
|
-
.values({
|
|
1367
|
-
id,
|
|
1368
|
-
run_id: step.runId,
|
|
1369
|
-
step_key: step.stepKey,
|
|
1370
|
-
step_name: step.stepName,
|
|
1371
|
-
status: step.status,
|
|
1372
|
-
attempt: step.attempt,
|
|
1373
|
-
result_json:
|
|
1374
|
-
step.result !== undefined ? JSON.stringify(step.result) : null,
|
|
1375
|
-
error_json: step.error
|
|
1376
|
-
? JSON.stringify(sanitizeErrorForStorage(step.error))
|
|
1377
|
-
: null,
|
|
1378
|
-
started_at: step.startedAt ?? null,
|
|
1379
|
-
finished_at: step.finishedAt ?? null,
|
|
1380
|
-
})
|
|
1381
|
-
.execute();
|
|
816
|
+
export class PostgresTransactionAdapter extends PostgresStorageCore {
|
|
817
|
+
private readonly _qb: CoreSchemaQueryBuilder;
|
|
1382
818
|
|
|
1383
|
-
|
|
819
|
+
constructor(trx: KyselyType<StepflowDatabase>, schema: string, logger?: Logger) {
|
|
820
|
+
super(schema, logger);
|
|
821
|
+
this._qb = trx.withSchema(schema) as unknown as CoreSchemaQueryBuilder;
|
|
1384
822
|
}
|
|
1385
823
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
.selectFrom("workflow_run_steps")
|
|
1389
|
-
.selectAll()
|
|
1390
|
-
.where("id", "=", stepId)
|
|
1391
|
-
.executeTakeFirst();
|
|
1392
|
-
|
|
1393
|
-
return row ? this.mapStepRow(row) : null;
|
|
824
|
+
protected override get qb(): CoreSchemaQueryBuilder {
|
|
825
|
+
return this._qb;
|
|
1394
826
|
}
|
|
1395
827
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
updates: Partial<WorkflowRunStepRecord>,
|
|
1399
|
-
): Promise<void> {
|
|
1400
|
-
const updateData: Partial<WorkflowRunStepsTable> = {};
|
|
1401
|
-
|
|
1402
|
-
if (updates.status !== undefined) updateData.status = updates.status;
|
|
1403
|
-
if (updates.attempt !== undefined) updateData.attempt = updates.attempt;
|
|
1404
|
-
if (updates.result !== undefined)
|
|
1405
|
-
updateData.result_json = JSON.stringify(updates.result);
|
|
1406
|
-
if (updates.error !== undefined)
|
|
1407
|
-
updateData.error_json = JSON.stringify(
|
|
1408
|
-
sanitizeErrorForStorage(updates.error as WorkflowError),
|
|
1409
|
-
);
|
|
1410
|
-
if (updates.finishedAt !== undefined)
|
|
1411
|
-
updateData.finished_at = updates.finishedAt;
|
|
1412
|
-
|
|
1413
|
-
if (Object.keys(updateData).length > 0) {
|
|
1414
|
-
await this.qb
|
|
1415
|
-
.updateTable("workflow_run_steps")
|
|
1416
|
-
.set(updateData)
|
|
1417
|
-
.where("id", "=", stepId)
|
|
1418
|
-
.execute();
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
async getStepsForRun(runId: string): Promise<WorkflowRunStepRecord[]> {
|
|
1423
|
-
const rows = await this.qb
|
|
1424
|
-
.selectFrom("workflow_run_steps")
|
|
1425
|
-
.selectAll()
|
|
1426
|
-
.where("run_id", "=", runId)
|
|
1427
|
-
.orderBy("started_at", "asc")
|
|
1428
|
-
.execute();
|
|
1429
|
-
|
|
1430
|
-
return rows.map((row) => this.mapStepRow(row));
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
async saveEvent(event: Omit<WorkflowEventRecord, "id">): Promise<void> {
|
|
1434
|
-
const id = generateId();
|
|
1435
|
-
|
|
1436
|
-
await this.qb
|
|
1437
|
-
.insertInto("workflow_events")
|
|
1438
|
-
.values({
|
|
1439
|
-
id,
|
|
1440
|
-
run_id: event.runId,
|
|
1441
|
-
step_key: event.stepKey ?? null,
|
|
1442
|
-
event_type: event.eventType,
|
|
1443
|
-
level: event.level,
|
|
1444
|
-
payload_json:
|
|
1445
|
-
event.payload !== undefined ? JSON.stringify(event.payload) : null,
|
|
1446
|
-
timestamp: event.timestamp,
|
|
1447
|
-
})
|
|
1448
|
-
.execute();
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
async getEventsForRun(
|
|
1452
|
-
runId: string,
|
|
1453
|
-
options: ListEventsOptions = {},
|
|
1454
|
-
): Promise<WorkflowEventRecord[]> {
|
|
1455
|
-
let query = this.qb
|
|
1456
|
-
.selectFrom("workflow_events")
|
|
1457
|
-
.selectAll()
|
|
1458
|
-
.where("run_id", "=", runId);
|
|
1459
|
-
|
|
1460
|
-
if (options.stepKey) query = query.where("step_key", "=", options.stepKey);
|
|
1461
|
-
if (options.level) query = query.where("level", "=", options.level);
|
|
1462
|
-
|
|
1463
|
-
const rows = await query
|
|
1464
|
-
.orderBy("timestamp", "asc")
|
|
1465
|
-
.limit(options.limit ?? 1000)
|
|
1466
|
-
.offset(options.offset ?? 0)
|
|
1467
|
-
.execute();
|
|
1468
|
-
|
|
1469
|
-
return rows.map((row) => this.mapEventRow(row));
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
private mapRunRow(row: WorkflowRunsTable): WorkflowRunRecord {
|
|
1473
|
-
return {
|
|
1474
|
-
id: row.id,
|
|
1475
|
-
kind: row.kind,
|
|
1476
|
-
status: row.status as RunStatus,
|
|
1477
|
-
parentRunId: row.parent_run_id ?? undefined,
|
|
1478
|
-
input:
|
|
1479
|
-
typeof row.input_json === "string"
|
|
1480
|
-
? JSON.parse(row.input_json)
|
|
1481
|
-
: row.input_json,
|
|
1482
|
-
context:
|
|
1483
|
-
typeof row.context_json === "string"
|
|
1484
|
-
? JSON.parse(row.context_json)
|
|
1485
|
-
: row.context_json,
|
|
1486
|
-
output: row.output_json
|
|
1487
|
-
? typeof row.output_json === "string"
|
|
1488
|
-
? JSON.parse(row.output_json)
|
|
1489
|
-
: row.output_json
|
|
1490
|
-
: undefined,
|
|
1491
|
-
error: row.error_json
|
|
1492
|
-
? typeof row.error_json === "string"
|
|
1493
|
-
? JSON.parse(row.error_json)
|
|
1494
|
-
: row.error_json
|
|
1495
|
-
: undefined,
|
|
1496
|
-
metadata:
|
|
1497
|
-
typeof row.metadata_json === "string"
|
|
1498
|
-
? JSON.parse(row.metadata_json)
|
|
1499
|
-
: row.metadata_json,
|
|
1500
|
-
priority: row.priority ?? 0,
|
|
1501
|
-
timeoutMs: row.timeout_ms ?? undefined,
|
|
1502
|
-
createdAt: new Date(row.created_at),
|
|
1503
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1504
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
private mapStepRow(row: WorkflowRunStepsTable): WorkflowRunStepRecord {
|
|
1509
|
-
return {
|
|
1510
|
-
id: row.id,
|
|
1511
|
-
runId: row.run_id,
|
|
1512
|
-
stepKey: row.step_key,
|
|
1513
|
-
stepName: row.step_name,
|
|
1514
|
-
status: row.status as StepStatus,
|
|
1515
|
-
attempt: row.attempt,
|
|
1516
|
-
result: row.result_json
|
|
1517
|
-
? typeof row.result_json === "string"
|
|
1518
|
-
? JSON.parse(row.result_json)
|
|
1519
|
-
: row.result_json
|
|
1520
|
-
: undefined,
|
|
1521
|
-
error: row.error_json
|
|
1522
|
-
? typeof row.error_json === "string"
|
|
1523
|
-
? JSON.parse(row.error_json)
|
|
1524
|
-
: row.error_json
|
|
1525
|
-
: undefined,
|
|
1526
|
-
startedAt: row.started_at ? new Date(row.started_at) : undefined,
|
|
1527
|
-
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
private mapEventRow(row: WorkflowEventsTable): WorkflowEventRecord {
|
|
1532
|
-
return {
|
|
1533
|
-
id: row.id,
|
|
1534
|
-
runId: row.run_id,
|
|
1535
|
-
stepKey: row.step_key ?? undefined,
|
|
1536
|
-
eventType: row.event_type,
|
|
1537
|
-
level: row.level as "info" | "warn" | "error",
|
|
1538
|
-
payload: row.payload_json
|
|
1539
|
-
? typeof row.payload_json === "string"
|
|
1540
|
-
? JSON.parse(row.payload_json)
|
|
1541
|
-
: row.payload_json
|
|
1542
|
-
: undefined,
|
|
1543
|
-
timestamp: new Date(row.timestamp),
|
|
1544
|
-
};
|
|
828
|
+
protected override get warnLabel(): string {
|
|
829
|
+
return 'PostgresTransactionAdapter';
|
|
1545
830
|
}
|
|
1546
831
|
}
|
|
1547
832
|
|