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/dist/index.js CHANGED
@@ -23,11 +23,13 @@ class WorkflowEngineError extends Error {
23
23
  workflowId;
24
24
  runId;
25
25
  cause;
26
- constructor(message, workflowId, runId, cause = undefined) {
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
- const tableExistsResult = await db.executeSql(`
169
- SELECT EXISTS (
170
- SELECT FROM information_schema.tables
171
- WHERE table_schema = 'public'
172
- AND table_name = 'workflow_runs'
173
- );
174
- `, []);
175
- if (!tableExistsResult.rows[0]?.exists) {
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
- await db.executeSql(`
199
- CREATE INDEX workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
200
- CREATE INDEX workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
201
- CREATE INDEX workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
202
- CREATE INDEX workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
203
- CREATE INDEX workflow_runs_resource_id_workflow_id_created_at_idx ON workflow_runs USING btree (resource_id, workflow_id, created_at DESC);
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
- await db.executeSql(`
207
- DROP INDEX IF EXISTS workflow_runs_workflow_id_idx;
208
- DROP INDEX IF EXISTS workflow_runs_resource_id_idx;
209
- CREATE INDEX IF NOT EXISTS workflow_runs_created_at_idx ON workflow_runs USING btree (created_at);
210
- CREATE INDEX IF NOT EXISTS workflow_runs_resource_id_created_at_idx ON workflow_runs USING btree (resource_id, created_at DESC);
211
- CREATE INDEX IF NOT EXISTS workflow_runs_status_created_at_idx ON workflow_runs USING btree (status, created_at DESC);
212
- CREATE INDEX IF NOT EXISTS workflow_runs_workflow_id_created_at_idx ON workflow_runs USING btree (workflow_id, created_at DESC);
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
- `, []);
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
- const insertedRun = result.rows[0];
286
- if (!insertedRun) {
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(insertedRun);
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.safeParse(input);
625
- if (!result.success) {
626
- throw new WorkflowEngineError(result.error.message, workflowId);
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
- const job = {
642
- runId: insertedRun.id,
643
- resourceId,
644
- workflowId,
645
- input
646
- };
647
- await this.boss.send(WORKFLOW_RUN_QUEUE_NAME, job, {
648
- startAfter: new Date,
649
- expireInSeconds: options?.expireInSeconds ?? defaultExpireInSeconds
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=0EF998205E7ED0B464756E2164756E21
1416
+ //# debugId=28A68A20D54F4D2C64756E2164756E21
1360
1417
  //# sourceMappingURL=index.js.map