@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.
Files changed (112) hide show
  1. package/.codekin/reports/code-review/2026-04-11_code-review-daily.md +146 -0
  2. package/.codekin/reports/code-review/2026-04-13_code-review-daily.md +74 -0
  3. package/.codekin/reports/comments/2026-04-04_comment-assessment.md +179 -0
  4. package/.codekin/reports/comments/2026-04-11_comment-assessment.md +161 -0
  5. package/.codekin/reports/comments/2026-04-14_comment-assessment.md +34 -0
  6. package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
  7. package/.codekin/reports/complexity/2026-04-11_complexity-report.md +152 -0
  8. package/.codekin/reports/complexity/2026-04-14_complexity-report.md +34 -0
  9. package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
  10. package/.codekin/reports/dependencies/2026-04-11_dependency-health.md +93 -0
  11. package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
  12. package/.codekin/reports/docs-audit/2026-04-11_docs-audit.md +138 -0
  13. package/.codekin/reports/docs-audit/2026-04-13_docs-audit.md +121 -0
  14. package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
  15. package/.codekin/reports/security/2026-04-11_security-audit.md +297 -0
  16. package/.github/CODEOWNERS +15 -0
  17. package/.github/dependabot.yml +23 -0
  18. package/.github/workflows/ci.yml +38 -0
  19. package/.github/workflows/publish.yml +17 -5
  20. package/.github/workflows/security.yml +29 -0
  21. package/CHANGELOG.md +34 -0
  22. package/README.md +24 -0
  23. package/SECURITY.md +40 -0
  24. package/dist/{chunk-FQCT3I2A.js → chunk-5XJBAPXF.js} +527 -551
  25. package/dist/chunk-5XJBAPXF.js.map +1 -0
  26. package/dist/{chunk-U5QGN3RT.js → chunk-FMPJBK2N.js} +67 -55
  27. package/dist/chunk-FMPJBK2N.js.map +1 -0
  28. package/dist/{chunk-GXY66V7Q.js → chunk-JPXPB7U5.js} +52 -2
  29. package/dist/chunk-JPXPB7U5.js.map +1 -0
  30. package/dist/{chunk-XCF4PLN7.js → chunk-U3PCVK6O.js} +276 -61
  31. package/dist/chunk-U3PCVK6O.js.map +1 -0
  32. package/dist/events/index.d.ts +60 -7
  33. package/dist/events/index.js +1 -1
  34. package/dist/{index-CPfGsmrp.d.ts → index-Cd0IZ40L.d.ts} +56 -17
  35. package/dist/index.d.ts +15 -23
  36. package/dist/index.js +784 -547
  37. package/dist/index.js.map +1 -1
  38. package/dist/scheduler/index.d.ts +4 -4
  39. package/dist/scheduler/index.js +2 -2
  40. package/dist/storage/index.d.ts +322 -56
  41. package/dist/storage/index.js +4 -2
  42. package/dist/{types-BlwFEGzj.d.ts → types-DSEjiLMj.d.ts} +16 -15
  43. package/dist/{types-DzkEqnQJ.d.ts → types-DZUdWmCc.d.ts} +2 -2
  44. package/dist/{types-BVShq7zT.d.ts → types-K5Gjk3H_.d.ts} +4 -4
  45. package/docs/stepflow-api-reference.md +5 -4
  46. package/package.json +12 -8
  47. package/src/core/engine/concurrency.ts +229 -0
  48. package/src/core/engine/lifecycle.ts +176 -0
  49. package/src/core/engine.test.ts +536 -522
  50. package/src/core/engine.ts +116 -235
  51. package/src/core/index.ts +3 -3
  52. package/src/core/orchestrator/persistence.ts +165 -0
  53. package/src/core/orchestrator/plan-resolution.ts +140 -0
  54. package/src/core/orchestrator/state-transitions.ts +109 -0
  55. package/src/core/orchestrator/step-runner.ts +427 -0
  56. package/src/core/orchestrator.ts +57 -395
  57. package/src/core/types.ts +7 -29
  58. package/src/events/index.ts +4 -4
  59. package/src/events/memory.test.ts +39 -41
  60. package/src/events/memory.ts +7 -10
  61. package/src/events/socketio.test.ts +112 -162
  62. package/src/events/socketio.ts +20 -27
  63. package/src/events/types.ts +16 -19
  64. package/src/events/webhook.test.ts +489 -235
  65. package/src/events/webhook.ts +355 -139
  66. package/src/index.ts +19 -19
  67. package/src/planning/evaluate-condition.test.ts +153 -0
  68. package/src/planning/evaluate-condition.ts +105 -0
  69. package/src/planning/index.ts +6 -3
  70. package/src/planning/planner.ts +45 -152
  71. package/src/planning/planning.test.ts +303 -366
  72. package/src/planning/registry.test.ts +204 -251
  73. package/src/planning/registry.ts +20 -106
  74. package/src/planning/types.ts +23 -27
  75. package/src/scheduler/cron.test.ts +149 -150
  76. package/src/scheduler/cron.ts +58 -69
  77. package/src/scheduler/index.ts +4 -14
  78. package/src/scheduler/postgres-persistence.ts +89 -105
  79. package/src/scheduler/sqlite-persistence.test.ts +96 -96
  80. package/src/scheduler/sqlite-persistence.ts +40 -22
  81. package/src/scheduler/types.ts +4 -9
  82. package/src/storage/index.ts +5 -5
  83. package/src/storage/memory.test.ts +133 -183
  84. package/src/storage/memory.ts +21 -42
  85. package/src/storage/postgres-core.ts +550 -0
  86. package/src/storage/postgres-migration.test.ts +69 -0
  87. package/src/storage/postgres-transaction-pagination.test.ts +107 -0
  88. package/src/storage/postgres.ts +222 -937
  89. package/src/storage/sqlite-core.ts +151 -0
  90. package/src/storage/sqlite.test.ts +330 -281
  91. package/src/storage/sqlite.ts +225 -170
  92. package/src/storage/types.ts +33 -58
  93. package/src/utils/errors.test.ts +78 -80
  94. package/src/utils/errors.ts +27 -57
  95. package/src/utils/id.ts +3 -3
  96. package/src/utils/index.ts +6 -4
  97. package/src/utils/logger.test.ts +52 -58
  98. package/src/utils/logger.ts +4 -4
  99. package/src/utils/pinned-fetch.test.ts +84 -0
  100. package/src/utils/pinned-fetch.ts +118 -0
  101. package/src/utils/postgres-deps.ts +20 -4
  102. package/src/utils/retry.test.ts +47 -67
  103. package/src/utils/retry.ts +9 -9
  104. package/src/utils/safe-json.test.ts +166 -0
  105. package/src/utils/safe-json.ts +107 -0
  106. package/src/utils/webhook-signature.test.ts +111 -0
  107. package/src/utils/webhook-signature.ts +74 -0
  108. package/dist/chunk-FQCT3I2A.js.map +0 -1
  109. package/dist/chunk-GXY66V7Q.js.map +0 -1
  110. package/dist/chunk-U5QGN3RT.js.map +0 -1
  111. package/dist/chunk-XCF4PLN7.js.map +0 -1
  112. package/src/scheduler/postgres-persistence.test.ts +0 -906
@@ -1,15 +1,15 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import Database from "better-sqlite3";
3
- import { SQLiteSchedulePersistence } from "./sqlite-persistence";
4
- import type { WorkflowSchedule } from "./types";
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import Database from 'better-sqlite3';
3
+ import { SQLiteSchedulePersistence } from './sqlite-persistence';
4
+ import type { WorkflowSchedule } from './types';
5
5
 
6
- describe("SQLiteSchedulePersistence", () => {
6
+ describe('SQLiteSchedulePersistence', () => {
7
7
  let db: Database.Database;
8
8
  let persistence: SQLiteSchedulePersistence;
9
9
 
10
10
  beforeEach(() => {
11
11
  // Use in-memory database for testing
12
- db = new Database(":memory:");
12
+ db = new Database(':memory:');
13
13
  persistence = new SQLiteSchedulePersistence({ db });
14
14
  });
15
15
 
@@ -17,35 +17,35 @@ describe("SQLiteSchedulePersistence", () => {
17
17
  db.close();
18
18
  });
19
19
 
20
- describe("saveSchedule", () => {
21
- it("should save a cron schedule", async () => {
20
+ describe('saveSchedule', () => {
21
+ it('should save a cron schedule', async () => {
22
22
  const schedule: WorkflowSchedule = {
23
- id: "test-1",
24
- workflowKind: "test.workflow",
25
- triggerType: "cron",
26
- cronExpression: "0 * * * *",
27
- timezone: "UTC",
23
+ id: 'test-1',
24
+ workflowKind: 'test.workflow',
25
+ triggerType: 'cron',
26
+ cronExpression: '0 * * * *',
27
+ timezone: 'UTC',
28
28
  enabled: true,
29
- input: { foo: "bar" },
30
- metadata: { user: "test" },
29
+ input: { foo: 'bar' },
30
+ metadata: { user: 'test' },
31
31
  };
32
32
 
33
33
  await persistence.saveSchedule(schedule);
34
34
 
35
35
  const loaded = await persistence.loadSchedules();
36
36
  expect(loaded).toHaveLength(1);
37
- expect(loaded[0].id).toBe("test-1");
38
- expect(loaded[0].cronExpression).toBe("0 * * * *");
39
- expect(loaded[0].input).toEqual({ foo: "bar" });
37
+ expect(loaded[0].id).toBe('test-1');
38
+ expect(loaded[0].cronExpression).toBe('0 * * * *');
39
+ expect(loaded[0].input).toEqual({ foo: 'bar' });
40
40
  });
41
41
 
42
- it("should save a workflow completion trigger", async () => {
42
+ it('should save a workflow completion trigger', async () => {
43
43
  const schedule: WorkflowSchedule = {
44
- id: "test-2",
45
- workflowKind: "notification.send",
46
- triggerType: "workflow_completed",
47
- triggerOnWorkflowKind: "order.process",
48
- triggerOnStatus: ["succeeded", "failed"],
44
+ id: 'test-2',
45
+ workflowKind: 'notification.send',
46
+ triggerType: 'workflow_completed',
47
+ triggerOnWorkflowKind: 'order.process',
48
+ triggerOnStatus: ['succeeded', 'failed'],
49
49
  enabled: true,
50
50
  };
51
51
 
@@ -53,23 +53,23 @@ describe("SQLiteSchedulePersistence", () => {
53
53
 
54
54
  const loaded = await persistence.loadSchedules();
55
55
  expect(loaded).toHaveLength(1);
56
- expect(loaded[0].triggerType).toBe("workflow_completed");
57
- expect(loaded[0].triggerOnWorkflowKind).toBe("order.process");
58
- expect(loaded[0].triggerOnStatus).toEqual(["succeeded", "failed"]);
56
+ expect(loaded[0].triggerType).toBe('workflow_completed');
57
+ expect(loaded[0].triggerOnWorkflowKind).toBe('order.process');
58
+ expect(loaded[0].triggerOnStatus).toEqual(['succeeded', 'failed']);
59
59
  });
60
60
 
61
- it("should save schedule with dates", async () => {
62
- const lastRunAt = new Date("2024-01-01T12:00:00Z");
63
- const nextRunAt = new Date("2024-01-01T13:00:00Z");
61
+ it('should save schedule with dates', async () => {
62
+ const lastRunAt = new Date('2024-01-01T12:00:00Z');
63
+ const nextRunAt = new Date('2024-01-01T13:00:00Z');
64
64
 
65
65
  const schedule: WorkflowSchedule = {
66
- id: "test-3",
67
- workflowKind: "test.workflow",
68
- triggerType: "cron",
69
- cronExpression: "0 * * * *",
66
+ id: 'test-3',
67
+ workflowKind: 'test.workflow',
68
+ triggerType: 'cron',
69
+ cronExpression: '0 * * * *',
70
70
  enabled: true,
71
71
  lastRunAt,
72
- lastRunId: "run-123",
72
+ lastRunId: 'run-123',
73
73
  nextRunAt,
74
74
  };
75
75
 
@@ -77,30 +77,30 @@ describe("SQLiteSchedulePersistence", () => {
77
77
 
78
78
  const loaded = await persistence.loadSchedules();
79
79
  expect(loaded[0].lastRunAt).toEqual(lastRunAt);
80
- expect(loaded[0].lastRunId).toBe("run-123");
80
+ expect(loaded[0].lastRunId).toBe('run-123');
81
81
  expect(loaded[0].nextRunAt).toEqual(nextRunAt);
82
82
  });
83
83
  });
84
84
 
85
- describe("loadSchedules", () => {
86
- it("should return empty array when no schedules", async () => {
85
+ describe('loadSchedules', () => {
86
+ it('should return empty array when no schedules', async () => {
87
87
  const schedules = await persistence.loadSchedules();
88
88
  expect(schedules).toHaveLength(0);
89
89
  });
90
90
 
91
- it("should load multiple schedules", async () => {
91
+ it('should load multiple schedules', async () => {
92
92
  await persistence.saveSchedule({
93
- id: "schedule-1",
94
- workflowKind: "workflow.a",
95
- triggerType: "cron",
96
- cronExpression: "0 * * * *",
93
+ id: 'schedule-1',
94
+ workflowKind: 'workflow.a',
95
+ triggerType: 'cron',
96
+ cronExpression: '0 * * * *',
97
97
  enabled: true,
98
98
  });
99
99
 
100
100
  await persistence.saveSchedule({
101
- id: "schedule-2",
102
- workflowKind: "workflow.b",
103
- triggerType: "manual",
101
+ id: 'schedule-2',
102
+ workflowKind: 'workflow.b',
103
+ triggerType: 'manual',
104
104
  enabled: false,
105
105
  });
106
106
 
@@ -109,102 +109,102 @@ describe("SQLiteSchedulePersistence", () => {
109
109
  });
110
110
  });
111
111
 
112
- describe("updateSchedule", () => {
113
- it("should update enabled status", async () => {
112
+ describe('updateSchedule', () => {
113
+ it('should update enabled status', async () => {
114
114
  await persistence.saveSchedule({
115
- id: "update-1",
116
- workflowKind: "test.workflow",
117
- triggerType: "cron",
118
- cronExpression: "0 * * * *",
115
+ id: 'update-1',
116
+ workflowKind: 'test.workflow',
117
+ triggerType: 'cron',
118
+ cronExpression: '0 * * * *',
119
119
  enabled: true,
120
120
  });
121
121
 
122
- await persistence.updateSchedule("update-1", { enabled: false });
122
+ await persistence.updateSchedule('update-1', { enabled: false });
123
123
 
124
124
  const loaded = await persistence.loadSchedules();
125
125
  expect(loaded[0].enabled).toBe(false);
126
126
  });
127
127
 
128
- it("should update cron expression", async () => {
128
+ it('should update cron expression', async () => {
129
129
  await persistence.saveSchedule({
130
- id: "update-2",
131
- workflowKind: "test.workflow",
132
- triggerType: "cron",
133
- cronExpression: "0 * * * *",
130
+ id: 'update-2',
131
+ workflowKind: 'test.workflow',
132
+ triggerType: 'cron',
133
+ cronExpression: '0 * * * *',
134
134
  enabled: true,
135
135
  });
136
136
 
137
- await persistence.updateSchedule("update-2", {
138
- cronExpression: "30 * * * *",
137
+ await persistence.updateSchedule('update-2', {
138
+ cronExpression: '30 * * * *',
139
139
  });
140
140
 
141
141
  const loaded = await persistence.loadSchedules();
142
- expect(loaded[0].cronExpression).toBe("30 * * * *");
142
+ expect(loaded[0].cronExpression).toBe('30 * * * *');
143
143
  });
144
144
 
145
- it("should update lastRunAt and lastRunId", async () => {
145
+ it('should update lastRunAt and lastRunId', async () => {
146
146
  await persistence.saveSchedule({
147
- id: "update-3",
148
- workflowKind: "test.workflow",
149
- triggerType: "cron",
150
- cronExpression: "0 * * * *",
147
+ id: 'update-3',
148
+ workflowKind: 'test.workflow',
149
+ triggerType: 'cron',
150
+ cronExpression: '0 * * * *',
151
151
  enabled: true,
152
152
  });
153
153
 
154
154
  const now = new Date();
155
- await persistence.updateSchedule("update-3", {
155
+ await persistence.updateSchedule('update-3', {
156
156
  lastRunAt: now,
157
- lastRunId: "run-456",
157
+ lastRunId: 'run-456',
158
158
  });
159
159
 
160
160
  const loaded = await persistence.loadSchedules();
161
161
  expect(loaded[0].lastRunAt).toEqual(now);
162
- expect(loaded[0].lastRunId).toBe("run-456");
162
+ expect(loaded[0].lastRunId).toBe('run-456');
163
163
  });
164
164
 
165
- it("should throw for non-existent schedule", async () => {
165
+ it('should throw for non-existent schedule', async () => {
166
166
  await expect(
167
- persistence.updateSchedule("nonexistent", { enabled: false }),
168
- ).rejects.toThrow("Schedule not found");
167
+ persistence.updateSchedule('nonexistent', { enabled: false })
168
+ ).rejects.toThrow('Schedule not found');
169
169
  });
170
170
  });
171
171
 
172
- describe("deleteSchedule", () => {
173
- it("should delete a schedule", async () => {
172
+ describe('deleteSchedule', () => {
173
+ it('should delete a schedule', async () => {
174
174
  await persistence.saveSchedule({
175
- id: "delete-1",
176
- workflowKind: "test.workflow",
177
- triggerType: "cron",
178
- cronExpression: "0 * * * *",
175
+ id: 'delete-1',
176
+ workflowKind: 'test.workflow',
177
+ triggerType: 'cron',
178
+ cronExpression: '0 * * * *',
179
179
  enabled: true,
180
180
  });
181
181
 
182
- await persistence.deleteSchedule("delete-1");
182
+ await persistence.deleteSchedule('delete-1');
183
183
 
184
184
  const loaded = await persistence.loadSchedules();
185
185
  expect(loaded).toHaveLength(0);
186
186
  });
187
187
 
188
- it("should handle deleting non-existent schedule gracefully", async () => {
188
+ it('should handle deleting non-existent schedule gracefully', async () => {
189
189
  // This should not throw
190
- await persistence.deleteSchedule("nonexistent");
190
+ await persistence.deleteSchedule('nonexistent');
191
191
  });
192
192
  });
193
193
 
194
- describe("data integrity", () => {
195
- it("should preserve all fields through save/load cycle", async () => {
194
+ describe('data integrity', () => {
195
+ it('should preserve all fields through save/load cycle', async () => {
196
196
  const schedule: WorkflowSchedule = {
197
- id: "integrity-1",
198
- workflowKind: "complex.workflow",
199
- triggerType: "workflow_completed",
200
- triggerOnWorkflowKind: "parent.workflow",
201
- triggerOnStatus: ["succeeded"],
197
+ id: 'integrity-1',
198
+ workflowKind: 'complex.workflow',
199
+ triggerType: 'workflow_completed',
200
+ triggerOnWorkflowKind: 'parent.workflow',
201
+ triggerOnStatus: ['succeeded'],
202
202
  input: { nested: { value: 123 } },
203
- metadata: { tags: ["a", "b"] },
203
+ metadata: { tags: ['a', 'b'] },
204
204
  enabled: true,
205
- lastRunAt: new Date("2024-06-15T10:30:00Z"),
206
- lastRunId: "run-789",
207
- nextRunAt: new Date("2024-06-15T11:00:00Z"),
205
+ lastRunAt: new Date('2024-06-15T10:30:00Z'),
206
+ lastRunId: 'run-789',
207
+ nextRunAt: new Date('2024-06-15T11:00:00Z'),
208
208
  };
209
209
 
210
210
  await persistence.saveSchedule(schedule);
@@ -213,11 +213,11 @@ describe("SQLiteSchedulePersistence", () => {
213
213
  expect(loaded[0]).toEqual(schedule);
214
214
  });
215
215
 
216
- it("should handle null/undefined values correctly", async () => {
216
+ it('should handle null/undefined values correctly', async () => {
217
217
  const schedule: WorkflowSchedule = {
218
- id: "nullable-1",
219
- workflowKind: "test.workflow",
220
- triggerType: "manual",
218
+ id: 'nullable-1',
219
+ workflowKind: 'test.workflow',
220
+ triggerType: 'manual',
221
221
  enabled: false,
222
222
  // All other optional fields are undefined
223
223
  };
@@ -4,10 +4,12 @@
4
4
  * Stores workflow schedules in a SQLite database table.
5
5
  */
6
6
 
7
- import type { Database, Statement } from "better-sqlite3";
8
- import type { WorkflowSchedule } from "./types";
9
- import type { SchedulePersistence } from "./cron";
10
- import { generateId } from "../utils/id";
7
+ import type { Database, Statement } from 'better-sqlite3';
8
+ import type { WorkflowSchedule } from './types';
9
+ import type { SchedulePersistence } from './cron';
10
+ import type { Logger, RunStatus } from '../core/types';
11
+ import { generateId } from '../utils/id';
12
+ import { safeJsonParse as sharedSafeJsonParse } from '../utils/safe-json';
11
13
 
12
14
  // ============================================================================
13
15
  // Configuration Types
@@ -22,6 +24,13 @@ export interface SQLiteSchedulePersistenceConfig {
22
24
 
23
25
  /** Table name for schedules (default: workflow_schedules) */
24
26
  tableName?: string;
27
+
28
+ /**
29
+ * Optional structured logger used to surface JSON corruption events.
30
+ * Without one, parse failures still increment the corruption counter
31
+ * exposed via `getJsonParseCorruptionCount()` but produce no log output.
32
+ */
33
+ logger?: Logger;
25
34
  }
26
35
 
27
36
  // ============================================================================
@@ -47,6 +56,7 @@ export interface SQLiteSchedulePersistenceConfig {
47
56
  export class SQLiteSchedulePersistence implements SchedulePersistence {
48
57
  private db: Database;
49
58
  private tableName: string;
59
+ private logger: Logger | undefined;
50
60
  private stmts: {
51
61
  insert: Statement;
52
62
  update: Statement;
@@ -57,10 +67,11 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
57
67
 
58
68
  constructor(config: SQLiteSchedulePersistenceConfig) {
59
69
  this.db = config.db;
60
- this.tableName = config.tableName ?? "workflow_schedules";
70
+ this.tableName = config.tableName ?? 'workflow_schedules';
61
71
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.tableName)) {
62
72
  throw new Error(`Invalid table name: ${this.tableName}`);
63
73
  }
74
+ this.logger = config.logger;
64
75
  this.initializeDatabase();
65
76
  }
66
77
 
@@ -159,7 +170,7 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
159
170
 
160
171
  async loadSchedules(): Promise<WorkflowSchedule[]> {
161
172
  const rows = this.stmts!.getAll.all() as ScheduleRow[];
162
- return rows.map((row) => this.rowToSchedule(row));
173
+ return rows.map(row => this.rowToSchedule(row));
163
174
  }
164
175
 
165
176
  async saveSchedule(schedule: WorkflowSchedule): Promise<void> {
@@ -170,26 +181,19 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
170
181
  schedule.cronExpression ?? null,
171
182
  schedule.timezone ?? null,
172
183
  schedule.triggerOnWorkflowKind ?? null,
173
- schedule.triggerOnStatus
174
- ? JSON.stringify(schedule.triggerOnStatus)
175
- : null,
184
+ schedule.triggerOnStatus ? JSON.stringify(schedule.triggerOnStatus) : null,
176
185
  schedule.input ? JSON.stringify(schedule.input) : null,
177
186
  schedule.metadata ? JSON.stringify(schedule.metadata) : null,
178
187
  schedule.enabled ? 1 : 0,
179
188
  schedule.lastRunAt?.toISOString() ?? null,
180
189
  schedule.lastRunId ?? null,
181
- schedule.nextRunAt?.toISOString() ?? null,
190
+ schedule.nextRunAt?.toISOString() ?? null
182
191
  );
183
192
  }
184
193
 
185
- async updateSchedule(
186
- scheduleId: string,
187
- updates: Partial<WorkflowSchedule>,
188
- ): Promise<void> {
194
+ async updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void> {
189
195
  // Get existing schedule to merge with updates
190
- const existing = this.stmts!.getById.get(scheduleId) as
191
- | ScheduleRow
192
- | undefined;
196
+ const existing = this.stmts!.getById.get(scheduleId) as ScheduleRow | undefined;
193
197
  if (!existing) {
194
198
  throw new Error(`Schedule not found: ${scheduleId}`);
195
199
  }
@@ -212,7 +216,7 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
212
216
  merged.lastRunAt?.toISOString() ?? null,
213
217
  merged.lastRunId ?? null,
214
218
  merged.nextRunAt?.toISOString() ?? null,
215
- scheduleId,
219
+ scheduleId
216
220
  );
217
221
  }
218
222
 
@@ -224,19 +228,33 @@ export class SQLiteSchedulePersistence implements SchedulePersistence {
224
228
  // Helper Methods
225
229
  // ============================================================================
226
230
 
231
+ private safeJsonParse(
232
+ json: string,
233
+ fallback: unknown,
234
+ rowId: string,
235
+ column: string,
236
+ ): unknown {
237
+ return sharedSafeJsonParse(json, fallback, {
238
+ component: 'SQLiteSchedulePersistence',
239
+ rowId,
240
+ column,
241
+ logger: this.logger,
242
+ });
243
+ }
244
+
227
245
  private rowToSchedule(row: ScheduleRow): WorkflowSchedule {
228
246
  return {
229
247
  id: row.id,
230
248
  workflowKind: row.workflow_kind,
231
- triggerType: row.trigger_type as WorkflowSchedule["triggerType"],
249
+ triggerType: row.trigger_type as WorkflowSchedule['triggerType'],
232
250
  cronExpression: row.cron_expression ?? undefined,
233
251
  timezone: row.timezone ?? undefined,
234
252
  triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? undefined,
235
253
  triggerOnStatus: row.trigger_on_status
236
- ? JSON.parse(row.trigger_on_status)
254
+ ? this.safeJsonParse(row.trigger_on_status, undefined, row.id, 'trigger_on_status') as RunStatus[] | undefined
237
255
  : undefined,
238
- input: row.input ? JSON.parse(row.input) : undefined,
239
- metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
256
+ input: row.input ? this.safeJsonParse(row.input, undefined, row.id, 'input') as Record<string, unknown> | undefined : undefined,
257
+ metadata: row.metadata ? this.safeJsonParse(row.metadata, undefined, row.id, 'metadata') as Record<string, unknown> | undefined : undefined,
240
258
  enabled: row.enabled === 1,
241
259
  lastRunAt: row.last_run_at ? new Date(row.last_run_at) : undefined,
242
260
  lastRunId: row.last_run_id ?? undefined,
@@ -5,12 +5,12 @@
5
5
  * and {@link PostgresSchedulePersistence}.
6
6
  */
7
7
 
8
- import type { WorkflowKind, RunStatus } from "../core/types";
8
+ import type { WorkflowKind, RunStatus } from '../core/types';
9
9
 
10
10
  /**
11
11
  * Schedule trigger types.
12
12
  */
13
- export type TriggerType = "cron" | "workflow_completed" | "manual";
13
+ export type TriggerType = 'cron' | 'workflow_completed' | 'manual';
14
14
 
15
15
  /**
16
16
  * Schedule definition.
@@ -57,18 +57,13 @@ export interface Scheduler {
57
57
  stop(): Promise<void>;
58
58
 
59
59
  /** Add a schedule */
60
- addSchedule(
61
- schedule: Omit<WorkflowSchedule, "id">,
62
- ): Promise<WorkflowSchedule>;
60
+ addSchedule(schedule: Omit<WorkflowSchedule, 'id'>): Promise<WorkflowSchedule>;
63
61
 
64
62
  /** Remove a schedule */
65
63
  removeSchedule(scheduleId: string): Promise<void>;
66
64
 
67
65
  /** Update a schedule */
68
- updateSchedule(
69
- scheduleId: string,
70
- updates: Partial<WorkflowSchedule>,
71
- ): Promise<void>;
66
+ updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void>;
72
67
 
73
68
  /** Get all schedules */
74
69
  getSchedules(): Promise<WorkflowSchedule[]>;
@@ -2,10 +2,10 @@
2
2
  * Storage module exports.
3
3
  */
4
4
 
5
- export * from "./types";
6
- export * from "./memory";
7
- export * from "./sqlite";
8
- export * from "./postgres";
5
+ export * from './types';
6
+ export * from './memory';
7
+ export * from './sqlite';
8
+ export * from './postgres';
9
9
 
10
10
  // Re-export specific types for convenience
11
11
  export type {
@@ -24,4 +24,4 @@ export type {
24
24
  StepflowRunsTable,
25
25
  StepflowStepResultsTable,
26
26
  StepflowDatabase,
27
- } from "./types";
27
+ } from './types';