@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
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from
|
|
2
|
-
import Database from
|
|
3
|
-
import { SQLiteSchedulePersistence } from
|
|
4
|
-
import type { WorkflowSchedule } from
|
|
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(
|
|
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(
|
|
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(
|
|
21
|
-
it(
|
|
20
|
+
describe('saveSchedule', () => {
|
|
21
|
+
it('should save a cron schedule', async () => {
|
|
22
22
|
const schedule: WorkflowSchedule = {
|
|
23
|
-
id:
|
|
24
|
-
workflowKind:
|
|
25
|
-
triggerType:
|
|
26
|
-
cronExpression:
|
|
27
|
-
timezone:
|
|
23
|
+
id: 'test-1',
|
|
24
|
+
workflowKind: 'test.workflow',
|
|
25
|
+
triggerType: 'cron',
|
|
26
|
+
cronExpression: '0 * * * *',
|
|
27
|
+
timezone: 'UTC',
|
|
28
28
|
enabled: true,
|
|
29
|
-
input: { foo:
|
|
30
|
-
metadata: { user:
|
|
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(
|
|
38
|
-
expect(loaded[0].cronExpression).toBe(
|
|
39
|
-
expect(loaded[0].input).toEqual({ foo:
|
|
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(
|
|
42
|
+
it('should save a workflow completion trigger', async () => {
|
|
43
43
|
const schedule: WorkflowSchedule = {
|
|
44
|
-
id:
|
|
45
|
-
workflowKind:
|
|
46
|
-
triggerType:
|
|
47
|
-
triggerOnWorkflowKind:
|
|
48
|
-
triggerOnStatus: [
|
|
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(
|
|
57
|
-
expect(loaded[0].triggerOnWorkflowKind).toBe(
|
|
58
|
-
expect(loaded[0].triggerOnStatus).toEqual([
|
|
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(
|
|
62
|
-
const lastRunAt = new Date(
|
|
63
|
-
const nextRunAt = new Date(
|
|
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:
|
|
67
|
-
workflowKind:
|
|
68
|
-
triggerType:
|
|
69
|
-
cronExpression:
|
|
66
|
+
id: 'test-3',
|
|
67
|
+
workflowKind: 'test.workflow',
|
|
68
|
+
triggerType: 'cron',
|
|
69
|
+
cronExpression: '0 * * * *',
|
|
70
70
|
enabled: true,
|
|
71
71
|
lastRunAt,
|
|
72
|
-
lastRunId:
|
|
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(
|
|
80
|
+
expect(loaded[0].lastRunId).toBe('run-123');
|
|
81
81
|
expect(loaded[0].nextRunAt).toEqual(nextRunAt);
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
describe(
|
|
86
|
-
it(
|
|
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(
|
|
91
|
+
it('should load multiple schedules', async () => {
|
|
92
92
|
await persistence.saveSchedule({
|
|
93
|
-
id:
|
|
94
|
-
workflowKind:
|
|
95
|
-
triggerType:
|
|
96
|
-
cronExpression:
|
|
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:
|
|
102
|
-
workflowKind:
|
|
103
|
-
triggerType:
|
|
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(
|
|
113
|
-
it(
|
|
112
|
+
describe('updateSchedule', () => {
|
|
113
|
+
it('should update enabled status', async () => {
|
|
114
114
|
await persistence.saveSchedule({
|
|
115
|
-
id:
|
|
116
|
-
workflowKind:
|
|
117
|
-
triggerType:
|
|
118
|
-
cronExpression:
|
|
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(
|
|
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(
|
|
128
|
+
it('should update cron expression', async () => {
|
|
129
129
|
await persistence.saveSchedule({
|
|
130
|
-
id:
|
|
131
|
-
workflowKind:
|
|
132
|
-
triggerType:
|
|
133
|
-
cronExpression:
|
|
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(
|
|
138
|
-
cronExpression:
|
|
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(
|
|
142
|
+
expect(loaded[0].cronExpression).toBe('30 * * * *');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
it(
|
|
145
|
+
it('should update lastRunAt and lastRunId', async () => {
|
|
146
146
|
await persistence.saveSchedule({
|
|
147
|
-
id:
|
|
148
|
-
workflowKind:
|
|
149
|
-
triggerType:
|
|
150
|
-
cronExpression:
|
|
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(
|
|
155
|
+
await persistence.updateSchedule('update-3', {
|
|
156
156
|
lastRunAt: now,
|
|
157
|
-
lastRunId:
|
|
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(
|
|
162
|
+
expect(loaded[0].lastRunId).toBe('run-456');
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it(
|
|
165
|
+
it('should throw for non-existent schedule', async () => {
|
|
166
166
|
await expect(
|
|
167
|
-
persistence.updateSchedule(
|
|
168
|
-
).rejects.toThrow(
|
|
167
|
+
persistence.updateSchedule('nonexistent', { enabled: false })
|
|
168
|
+
).rejects.toThrow('Schedule not found');
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
describe(
|
|
173
|
-
it(
|
|
172
|
+
describe('deleteSchedule', () => {
|
|
173
|
+
it('should delete a schedule', async () => {
|
|
174
174
|
await persistence.saveSchedule({
|
|
175
|
-
id:
|
|
176
|
-
workflowKind:
|
|
177
|
-
triggerType:
|
|
178
|
-
cronExpression:
|
|
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(
|
|
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(
|
|
188
|
+
it('should handle deleting non-existent schedule gracefully', async () => {
|
|
189
189
|
// This should not throw
|
|
190
|
-
await persistence.deleteSchedule(
|
|
190
|
+
await persistence.deleteSchedule('nonexistent');
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
-
describe(
|
|
195
|
-
it(
|
|
194
|
+
describe('data integrity', () => {
|
|
195
|
+
it('should preserve all fields through save/load cycle', async () => {
|
|
196
196
|
const schedule: WorkflowSchedule = {
|
|
197
|
-
id:
|
|
198
|
-
workflowKind:
|
|
199
|
-
triggerType:
|
|
200
|
-
triggerOnWorkflowKind:
|
|
201
|
-
triggerOnStatus: [
|
|
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: [
|
|
203
|
+
metadata: { tags: ['a', 'b'] },
|
|
204
204
|
enabled: true,
|
|
205
|
-
lastRunAt: new Date(
|
|
206
|
-
lastRunId:
|
|
207
|
-
nextRunAt: new Date(
|
|
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(
|
|
216
|
+
it('should handle null/undefined values correctly', async () => {
|
|
217
217
|
const schedule: WorkflowSchedule = {
|
|
218
|
-
id:
|
|
219
|
-
workflowKind:
|
|
220
|
-
triggerType:
|
|
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
|
|
8
|
-
import type { WorkflowSchedule } from
|
|
9
|
-
import type { SchedulePersistence } from
|
|
10
|
-
import {
|
|
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 ??
|
|
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(
|
|
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[
|
|
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
|
-
?
|
|
254
|
+
? this.safeJsonParse(row.trigger_on_status, undefined, row.id, 'trigger_on_status') as RunStatus[] | undefined
|
|
237
255
|
: undefined,
|
|
238
|
-
input: row.input ?
|
|
239
|
-
metadata: row.metadata ?
|
|
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,
|
package/src/scheduler/types.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* and {@link PostgresSchedulePersistence}.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { WorkflowKind, RunStatus } from
|
|
8
|
+
import type { WorkflowKind, RunStatus } from '../core/types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Schedule trigger types.
|
|
12
12
|
*/
|
|
13
|
-
export type TriggerType =
|
|
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[]>;
|
package/src/storage/index.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Storage module exports.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
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
|
|
27
|
+
} from './types';
|