pg-workflows 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -6
- package/dist/index.cjs +111 -54
- package/dist/index.d.cts +15 -16
- package/dist/index.d.ts +15 -16
- package/dist/index.js +111 -54
- package/dist/index.js.map +8 -8
- package/package.json +8 -12
package/dist/index.js
CHANGED
|
@@ -23,11 +23,13 @@ class WorkflowEngineError extends Error {
|
|
|
23
23
|
workflowId;
|
|
24
24
|
runId;
|
|
25
25
|
cause;
|
|
26
|
-
|
|
26
|
+
issues;
|
|
27
|
+
constructor(message, workflowId, runId, cause = undefined, issues) {
|
|
27
28
|
super(message);
|
|
28
29
|
this.workflowId = workflowId;
|
|
29
30
|
this.runId = runId;
|
|
30
31
|
this.cause = cause;
|
|
32
|
+
this.issues = issues;
|
|
31
33
|
this.name = "WorkflowEngineError";
|
|
32
34
|
if (Error.captureStackTrace) {
|
|
33
35
|
Error.captureStackTrace(this, WorkflowEngineError);
|
|
@@ -164,17 +166,17 @@ function parseWorkflowHandler(handler) {
|
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
// src/db/migration.ts
|
|
169
|
+
var MIGRATION_LOCK_ID = 738291645;
|
|
170
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
167
171
|
async function runMigrations(db) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await db.executeSql(`
|
|
177
|
-
CREATE TABLE workflow_runs (
|
|
172
|
+
if (await isSchemaUpToDate(db)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const currentVersion = await getCurrentVersion(db);
|
|
176
|
+
const commands = [];
|
|
177
|
+
if (currentVersion < 1) {
|
|
178
|
+
commands.push(`
|
|
179
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
178
180
|
id varchar(32) PRIMARY KEY NOT NULL,
|
|
179
181
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
180
182
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
@@ -193,25 +195,66 @@ async function runMigrations(db) {
|
|
|
193
195
|
retry_count integer DEFAULT 0 NOT NULL,
|
|
194
196
|
max_retries integer DEFAULT 0 NOT NULL,
|
|
195
197
|
job_id varchar(256)
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
CREATE INDEX
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
)
|
|
199
|
+
`);
|
|
200
|
+
commands.push(`
|
|
201
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at)
|
|
202
|
+
`);
|
|
203
|
+
commands.push(`
|
|
204
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC)
|
|
205
|
+
`);
|
|
206
|
+
commands.push(`
|
|
207
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC)
|
|
208
|
+
`);
|
|
209
|
+
commands.push(`
|
|
210
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC)
|
|
211
|
+
`);
|
|
212
|
+
commands.push(`
|
|
213
|
+
CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC)
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
if (currentVersion < 2) {
|
|
217
|
+
commands.push("DROP INDEX IF EXISTS workflow_runs_workflow_id_idx");
|
|
218
|
+
commands.push("DROP INDEX IF EXISTS workflow_runs_resource_id_idx");
|
|
219
|
+
commands.push("ALTER TABLE workflow_runs ADD COLUMN IF NOT EXISTS idempotency_key varchar(256)");
|
|
220
|
+
commands.push(`
|
|
221
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_runs_idempotency_key_idx ON workflow_runs (idempotency_key) WHERE idempotency_key IS NOT NULL
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
if (currentVersion === 0) {
|
|
225
|
+
commands.push(`INSERT INTO workflow_schema_version (version) VALUES (${CURRENT_SCHEMA_VERSION})`);
|
|
205
226
|
} else {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
commands.push(`UPDATE workflow_schema_version SET version = ${CURRENT_SCHEMA_VERSION}`);
|
|
228
|
+
}
|
|
229
|
+
if (commands.length === 0) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const sql = [
|
|
233
|
+
"BEGIN",
|
|
234
|
+
"SET LOCAL lock_timeout = '30s'",
|
|
235
|
+
"SET LOCAL idle_in_transaction_session_timeout = '30s'",
|
|
236
|
+
`SELECT pg_advisory_xact_lock(${MIGRATION_LOCK_ID})`,
|
|
237
|
+
"CREATE TABLE IF NOT EXISTS workflow_schema_version (version integer NOT NULL)",
|
|
238
|
+
...commands,
|
|
239
|
+
"COMMIT"
|
|
240
|
+
].join(`;
|
|
241
|
+
`);
|
|
242
|
+
await db.executeSql(sql, []);
|
|
243
|
+
}
|
|
244
|
+
async function isSchemaUpToDate(db) {
|
|
245
|
+
try {
|
|
246
|
+
const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
|
|
247
|
+
return (result.rows[0]?.version ?? 0) >= CURRENT_SCHEMA_VERSION;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function getCurrentVersion(db) {
|
|
253
|
+
try {
|
|
254
|
+
const result = await db.executeSql("SELECT version FROM workflow_schema_version LIMIT 1", []);
|
|
255
|
+
return result.rows[0]?.version ?? 0;
|
|
256
|
+
} catch {
|
|
257
|
+
return 0;
|
|
215
258
|
}
|
|
216
259
|
}
|
|
217
260
|
|
|
@@ -239,7 +282,8 @@ function mapRowToWorkflowRun(row) {
|
|
|
239
282
|
timeoutAt: row.timeout_at ? new Date(row.timeout_at) : null,
|
|
240
283
|
retryCount: row.retry_count,
|
|
241
284
|
maxRetries: row.max_retries,
|
|
242
|
-
jobId: row.job_id
|
|
285
|
+
jobId: row.job_id,
|
|
286
|
+
idempotencyKey: row.idempotency_key
|
|
243
287
|
};
|
|
244
288
|
}
|
|
245
289
|
async function insertWorkflowRun({
|
|
@@ -249,7 +293,8 @@ async function insertWorkflowRun({
|
|
|
249
293
|
status,
|
|
250
294
|
input,
|
|
251
295
|
maxRetries,
|
|
252
|
-
timeoutAt
|
|
296
|
+
timeoutAt,
|
|
297
|
+
idempotencyKey
|
|
253
298
|
}, db) {
|
|
254
299
|
const runId = generateKSUID("run");
|
|
255
300
|
const now = new Date;
|
|
@@ -265,9 +310,11 @@ async function insertWorkflowRun({
|
|
|
265
310
|
created_at,
|
|
266
311
|
updated_at,
|
|
267
312
|
timeline,
|
|
268
|
-
retry_count
|
|
313
|
+
retry_count,
|
|
314
|
+
idempotency_key
|
|
269
315
|
)
|
|
270
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
316
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
317
|
+
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
|
|
271
318
|
RETURNING *`, [
|
|
272
319
|
runId,
|
|
273
320
|
resourceId ?? null,
|
|
@@ -280,13 +327,19 @@ async function insertWorkflowRun({
|
|
|
280
327
|
now,
|
|
281
328
|
now,
|
|
282
329
|
"{}",
|
|
283
|
-
0
|
|
330
|
+
0,
|
|
331
|
+
idempotencyKey ?? null
|
|
332
|
+
]);
|
|
333
|
+
if (result.rows[0]) {
|
|
334
|
+
return { run: mapRowToWorkflowRun(result.rows[0]), created: true };
|
|
335
|
+
}
|
|
336
|
+
const existing = await db.executeSql("SELECT * FROM workflow_runs WHERE idempotency_key = $1", [
|
|
337
|
+
idempotencyKey
|
|
284
338
|
]);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw new Error("Failed to insert workflow run");
|
|
339
|
+
if (!existing.rows[0]) {
|
|
340
|
+
throw new Error(`Idempotency conflict: existing run not found for key "${idempotencyKey}"`);
|
|
288
341
|
}
|
|
289
|
-
return mapRowToWorkflowRun(
|
|
342
|
+
return { run: mapRowToWorkflowRun(existing.rows[0]), created: false };
|
|
290
343
|
}
|
|
291
344
|
async function getWorkflowRun({
|
|
292
345
|
runId,
|
|
@@ -606,6 +659,7 @@ class WorkflowEngine {
|
|
|
606
659
|
resourceId,
|
|
607
660
|
workflowId,
|
|
608
661
|
input,
|
|
662
|
+
idempotencyKey,
|
|
609
663
|
options
|
|
610
664
|
}) {
|
|
611
665
|
if (!this._started) {
|
|
@@ -621,33 +675,36 @@ class WorkflowEngine {
|
|
|
621
675
|
throw new WorkflowEngineError(`Workflow ${workflowId} has no steps`, workflowId);
|
|
622
676
|
}
|
|
623
677
|
if (workflow2.inputSchema) {
|
|
624
|
-
const result = workflow2.inputSchema.
|
|
625
|
-
if (
|
|
626
|
-
throw new WorkflowEngineError(result.
|
|
678
|
+
const result = await workflow2.inputSchema["~standard"].validate(input);
|
|
679
|
+
if (result.issues) {
|
|
680
|
+
throw new WorkflowEngineError(JSON.stringify(result.issues), workflowId, undefined, undefined, result.issues);
|
|
627
681
|
}
|
|
628
682
|
}
|
|
629
683
|
const initialStepId = workflow2.steps[0]?.id ?? "__start__";
|
|
630
684
|
const run = await withPostgresTransaction(this.boss.getDb(), async (_db) => {
|
|
631
685
|
const timeoutAt = options?.timeout ? new Date(Date.now() + options.timeout) : workflow2.timeout ? new Date(Date.now() + workflow2.timeout) : null;
|
|
632
|
-
const insertedRun = await insertWorkflowRun({
|
|
686
|
+
const { run: insertedRun, created } = await insertWorkflowRun({
|
|
633
687
|
resourceId,
|
|
634
688
|
workflowId,
|
|
635
689
|
currentStepId: initialStepId,
|
|
636
690
|
status: "running" /* RUNNING */,
|
|
637
691
|
input,
|
|
638
692
|
maxRetries: options?.retries ?? workflow2.retries ?? 0,
|
|
639
|
-
timeoutAt
|
|
693
|
+
timeoutAt,
|
|
694
|
+
idempotencyKey
|
|
640
695
|
}, _db);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
696
|
+
if (created) {
|
|
697
|
+
const job = {
|
|
698
|
+
runId: insertedRun.id,
|
|
699
|
+
resourceId,
|
|
700
|
+
workflowId,
|
|
701
|
+
input
|
|
702
|
+
};
|
|
703
|
+
await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
|
|
704
|
+
startAfter: new Date,
|
|
705
|
+
expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
|
|
706
|
+
});
|
|
707
|
+
}
|
|
651
708
|
return insertedRun;
|
|
652
709
|
}, this.pool);
|
|
653
710
|
this.logger.log("Started workflow run", {
|
|
@@ -1356,5 +1413,5 @@ export {
|
|
|
1356
1413
|
StepType
|
|
1357
1414
|
};
|
|
1358
1415
|
|
|
1359
|
-
//# debugId=
|
|
1416
|
+
//# debugId=28A68A20D54F4D2C64756E2164756E21
|
|
1360
1417
|
//# sourceMappingURL=index.js.map
|