@multiplier-labs/stepflow 0.3.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/.github/workflows/publish.yml +30 -0
- package/README.md +408 -0
- package/dist/chunk-FQCT3I2A.js +1512 -0
- package/dist/chunk-FQCT3I2A.js.map +1 -0
- package/dist/chunk-GXY66V7Q.js +111 -0
- package/dist/chunk-GXY66V7Q.js.map +1 -0
- package/dist/chunk-U5QGN3RT.js +715 -0
- package/dist/chunk-U5QGN3RT.js.map +1 -0
- package/dist/chunk-XCF4PLN7.js +575 -0
- package/dist/chunk-XCF4PLN7.js.map +1 -0
- package/dist/events/index.d.ts +346 -0
- package/dist/events/index.js +11 -0
- package/dist/events/index.js.map +1 -0
- package/dist/index-CPfGsmrp.d.ts +567 -0
- package/dist/index.d.ts +748 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/scheduler/index.d.ts +6 -0
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/storage/index.d.ts +402 -0
- package/dist/storage/index.js +13 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/types-BVShq7zT.d.ts +150 -0
- package/dist/types-BlwFEGzj.d.ts +276 -0
- package/dist/types-DzkEqnQJ.d.ts +75 -0
- package/docs/github-packages-setup.md +97 -0
- package/docs/stepflow-api-reference.md +1662 -0
- package/package.json +85 -0
- package/src/core/engine.test.ts +1421 -0
- package/src/core/engine.ts +717 -0
- package/src/core/index.ts +7 -0
- package/src/core/orchestrator.ts +590 -0
- package/src/core/types.ts +242 -0
- package/src/events/index.ts +8 -0
- package/src/events/memory.test.ts +109 -0
- package/src/events/memory.ts +95 -0
- package/src/events/socketio.test.ts +428 -0
- package/src/events/socketio.ts +269 -0
- package/src/events/types.ts +121 -0
- package/src/events/webhook.test.ts +695 -0
- package/src/events/webhook.ts +591 -0
- package/src/index.ts +270 -0
- package/src/planning/index.ts +52 -0
- package/src/planning/planner.ts +601 -0
- package/src/planning/planning.test.ts +790 -0
- package/src/planning/registry.test.ts +445 -0
- package/src/planning/registry.ts +369 -0
- package/src/planning/types.ts +570 -0
- package/src/scheduler/cron.test.ts +480 -0
- package/src/scheduler/cron.ts +449 -0
- package/src/scheduler/index.ts +18 -0
- package/src/scheduler/postgres-persistence.test.ts +906 -0
- package/src/scheduler/postgres-persistence.ts +546 -0
- package/src/scheduler/sqlite-persistence.test.ts +235 -0
- package/src/scheduler/sqlite-persistence.ts +268 -0
- package/src/scheduler/types.ts +78 -0
- package/src/storage/index.ts +27 -0
- package/src/storage/memory.test.ts +399 -0
- package/src/storage/memory.ts +248 -0
- package/src/storage/postgres.ts +1550 -0
- package/src/storage/sqlite.test.ts +593 -0
- package/src/storage/sqlite.ts +662 -0
- package/src/storage/types.ts +368 -0
- package/src/utils/errors.test.ts +152 -0
- package/src/utils/errors.ts +191 -0
- package/src/utils/id.ts +21 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.test.ts +127 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/postgres-deps.ts +63 -0
- package/src/utils/retry.test.ts +197 -0
- package/src/utils/retry.ts +121 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ConsoleLogger,
|
|
3
|
+
generateId,
|
|
4
|
+
loadPostgresDeps
|
|
5
|
+
} from "./chunk-GXY66V7Q.js";
|
|
6
|
+
|
|
7
|
+
// src/scheduler/cron.ts
|
|
8
|
+
import { CronExpressionParser } from "cron-parser";
|
|
9
|
+
var CronScheduler = class {
|
|
10
|
+
engine;
|
|
11
|
+
logger;
|
|
12
|
+
pollInterval;
|
|
13
|
+
persistence;
|
|
14
|
+
schedules = /* @__PURE__ */ new Map();
|
|
15
|
+
running = false;
|
|
16
|
+
pollTimer = null;
|
|
17
|
+
eventUnsubscribe = null;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.engine = config.engine;
|
|
20
|
+
this.logger = config.logger ?? new ConsoleLogger();
|
|
21
|
+
this.pollInterval = config.pollInterval ?? 1e3;
|
|
22
|
+
this.persistence = config.persistence;
|
|
23
|
+
}
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Scheduler Interface Implementation
|
|
26
|
+
// ============================================================================
|
|
27
|
+
/**
|
|
28
|
+
* Start the scheduler.
|
|
29
|
+
* Begins polling for cron schedules and subscribes to workflow completion events.
|
|
30
|
+
*/
|
|
31
|
+
async start() {
|
|
32
|
+
if (this.running) {
|
|
33
|
+
this.logger.warn("Scheduler is already running");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.logger.info("Starting scheduler...");
|
|
37
|
+
if (this.persistence) {
|
|
38
|
+
const loaded = await this.persistence.loadSchedules();
|
|
39
|
+
for (const schedule of loaded) {
|
|
40
|
+
this.schedules.set(schedule.id, schedule);
|
|
41
|
+
}
|
|
42
|
+
this.logger.info(`Loaded ${loaded.length} schedules from persistence`);
|
|
43
|
+
}
|
|
44
|
+
for (const schedule of this.schedules.values()) {
|
|
45
|
+
if (schedule.triggerType === "cron" && schedule.cronExpression) {
|
|
46
|
+
this.updateNextRunTime(schedule);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.pollTimer = setInterval(
|
|
50
|
+
() => this.checkSchedules(),
|
|
51
|
+
this.pollInterval
|
|
52
|
+
);
|
|
53
|
+
this.eventUnsubscribe = this.engine.subscribeToAll((event) => {
|
|
54
|
+
this.handleWorkflowEvent(event);
|
|
55
|
+
});
|
|
56
|
+
this.running = true;
|
|
57
|
+
this.logger.info("Scheduler started");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Stop the scheduler.
|
|
61
|
+
* Stops polling and unsubscribes from events.
|
|
62
|
+
*/
|
|
63
|
+
async stop() {
|
|
64
|
+
if (!this.running) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.logger.info("Stopping scheduler...");
|
|
68
|
+
if (this.pollTimer) {
|
|
69
|
+
clearInterval(this.pollTimer);
|
|
70
|
+
this.pollTimer = null;
|
|
71
|
+
}
|
|
72
|
+
if (this.eventUnsubscribe) {
|
|
73
|
+
this.eventUnsubscribe();
|
|
74
|
+
this.eventUnsubscribe = null;
|
|
75
|
+
}
|
|
76
|
+
this.running = false;
|
|
77
|
+
this.logger.info("Scheduler stopped");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Add a new schedule.
|
|
81
|
+
*/
|
|
82
|
+
async addSchedule(scheduleData) {
|
|
83
|
+
const schedule = {
|
|
84
|
+
...scheduleData,
|
|
85
|
+
id: generateId()
|
|
86
|
+
};
|
|
87
|
+
if (schedule.triggerType === "cron" && schedule.cronExpression) {
|
|
88
|
+
try {
|
|
89
|
+
CronExpressionParser.parse(schedule.cronExpression, {
|
|
90
|
+
tz: schedule.timezone
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`Invalid cron expression: ${schedule.cronExpression}`);
|
|
94
|
+
}
|
|
95
|
+
this.updateNextRunTime(schedule);
|
|
96
|
+
}
|
|
97
|
+
if (schedule.triggerType === "workflow_completed") {
|
|
98
|
+
if (!schedule.triggerOnWorkflowKind) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"triggerOnWorkflowKind is required for workflow_completed triggers"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.schedules.set(schedule.id, schedule);
|
|
105
|
+
if (this.persistence) {
|
|
106
|
+
await this.persistence.saveSchedule(schedule);
|
|
107
|
+
}
|
|
108
|
+
this.logger.info(
|
|
109
|
+
`Added schedule: ${schedule.id} (${schedule.workflowKind})`
|
|
110
|
+
);
|
|
111
|
+
return schedule;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Remove a schedule.
|
|
115
|
+
*/
|
|
116
|
+
async removeSchedule(scheduleId) {
|
|
117
|
+
if (!this.schedules.has(scheduleId)) {
|
|
118
|
+
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
119
|
+
}
|
|
120
|
+
this.schedules.delete(scheduleId);
|
|
121
|
+
if (this.persistence) {
|
|
122
|
+
await this.persistence.deleteSchedule(scheduleId);
|
|
123
|
+
}
|
|
124
|
+
this.logger.info(`Removed schedule: ${scheduleId}`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Update a schedule.
|
|
128
|
+
*/
|
|
129
|
+
async updateSchedule(scheduleId, updates) {
|
|
130
|
+
const schedule = this.schedules.get(scheduleId);
|
|
131
|
+
if (!schedule) {
|
|
132
|
+
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
133
|
+
}
|
|
134
|
+
delete updates.id;
|
|
135
|
+
Object.assign(schedule, updates);
|
|
136
|
+
if (updates.cronExpression !== void 0 || updates.timezone !== void 0) {
|
|
137
|
+
if (schedule.cronExpression) {
|
|
138
|
+
try {
|
|
139
|
+
CronExpressionParser.parse(schedule.cronExpression, {
|
|
140
|
+
tz: schedule.timezone
|
|
141
|
+
});
|
|
142
|
+
this.updateNextRunTime(schedule);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Invalid cron expression: ${schedule.cronExpression}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (this.persistence) {
|
|
151
|
+
await this.persistence.updateSchedule(scheduleId, updates);
|
|
152
|
+
}
|
|
153
|
+
this.logger.debug(`Updated schedule: ${scheduleId}`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get all schedules.
|
|
157
|
+
*/
|
|
158
|
+
async getSchedules() {
|
|
159
|
+
return Array.from(this.schedules.values());
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get a schedule by ID.
|
|
163
|
+
*/
|
|
164
|
+
getSchedule(scheduleId) {
|
|
165
|
+
return this.schedules.get(scheduleId);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Manually trigger a scheduled workflow.
|
|
169
|
+
*/
|
|
170
|
+
async triggerNow(scheduleId) {
|
|
171
|
+
const schedule = this.schedules.get(scheduleId);
|
|
172
|
+
if (!schedule) {
|
|
173
|
+
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
174
|
+
}
|
|
175
|
+
return this.executeSchedule(schedule);
|
|
176
|
+
}
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Internal Methods
|
|
179
|
+
// ============================================================================
|
|
180
|
+
/**
|
|
181
|
+
* Check all cron schedules and execute those that are due.
|
|
182
|
+
*/
|
|
183
|
+
checkSchedules() {
|
|
184
|
+
const now = /* @__PURE__ */ new Date();
|
|
185
|
+
for (const schedule of this.schedules.values()) {
|
|
186
|
+
if (!schedule.enabled) continue;
|
|
187
|
+
if (schedule.triggerType !== "cron") continue;
|
|
188
|
+
if (!schedule.nextRunAt) continue;
|
|
189
|
+
if (schedule.nextRunAt <= now) {
|
|
190
|
+
this.executeSchedule(schedule).catch((error) => {
|
|
191
|
+
this.logger.error(
|
|
192
|
+
`Failed to execute schedule ${schedule.id}:`,
|
|
193
|
+
error
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
this.updateNextRunTime(schedule);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Handle workflow events for completion triggers.
|
|
202
|
+
*/
|
|
203
|
+
handleWorkflowEvent(event) {
|
|
204
|
+
if (event.eventType !== "run.completed" && event.eventType !== "run.failed") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const completedStatus = event.eventType === "run.completed" ? "succeeded" : "failed";
|
|
208
|
+
const completedKind = event.kind;
|
|
209
|
+
for (const schedule of this.schedules.values()) {
|
|
210
|
+
if (!schedule.enabled) continue;
|
|
211
|
+
if (schedule.triggerType !== "workflow_completed") continue;
|
|
212
|
+
if (schedule.triggerOnWorkflowKind !== completedKind) continue;
|
|
213
|
+
if (schedule.triggerOnStatus && !schedule.triggerOnStatus.includes(completedStatus)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
this.executeSchedule(schedule, {
|
|
217
|
+
triggerRunId: event.runId,
|
|
218
|
+
triggerStatus: completedStatus
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
this.logger.error(`Failed to execute schedule ${schedule.id}:`, error);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Execute a schedule by starting the workflow.
|
|
226
|
+
*/
|
|
227
|
+
async executeSchedule(schedule, triggerContext) {
|
|
228
|
+
this.logger.info(
|
|
229
|
+
`Executing schedule: ${schedule.id} (${schedule.workflowKind})`
|
|
230
|
+
);
|
|
231
|
+
const metadata = {
|
|
232
|
+
...schedule.metadata,
|
|
233
|
+
scheduleId: schedule.id,
|
|
234
|
+
triggerType: schedule.triggerType,
|
|
235
|
+
...triggerContext ?? {}
|
|
236
|
+
};
|
|
237
|
+
const runId = await this.engine.startRun({
|
|
238
|
+
kind: schedule.workflowKind,
|
|
239
|
+
input: schedule.input,
|
|
240
|
+
metadata
|
|
241
|
+
});
|
|
242
|
+
schedule.lastRunAt = /* @__PURE__ */ new Date();
|
|
243
|
+
schedule.lastRunId = runId;
|
|
244
|
+
if (this.persistence) {
|
|
245
|
+
await this.persistence.updateSchedule(schedule.id, {
|
|
246
|
+
lastRunAt: schedule.lastRunAt,
|
|
247
|
+
lastRunId: schedule.lastRunId,
|
|
248
|
+
nextRunAt: schedule.nextRunAt
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return runId;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Update the next run time for a cron schedule.
|
|
255
|
+
*/
|
|
256
|
+
updateNextRunTime(schedule) {
|
|
257
|
+
if (!schedule.cronExpression) return;
|
|
258
|
+
try {
|
|
259
|
+
const interval = CronExpressionParser.parse(schedule.cronExpression, {
|
|
260
|
+
currentDate: /* @__PURE__ */ new Date(),
|
|
261
|
+
tz: schedule.timezone
|
|
262
|
+
});
|
|
263
|
+
schedule.nextRunAt = interval.next().toDate();
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.logger.error(
|
|
266
|
+
`Failed to parse cron expression for schedule ${schedule.id}:`,
|
|
267
|
+
error
|
|
268
|
+
);
|
|
269
|
+
schedule.nextRunAt = void 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/scheduler/sqlite-persistence.ts
|
|
275
|
+
var SQLiteSchedulePersistence = class {
|
|
276
|
+
db;
|
|
277
|
+
tableName;
|
|
278
|
+
stmts = null;
|
|
279
|
+
constructor(config) {
|
|
280
|
+
this.db = config.db;
|
|
281
|
+
this.tableName = config.tableName ?? "workflow_schedules";
|
|
282
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(this.tableName)) {
|
|
283
|
+
throw new Error(`Invalid table name: ${this.tableName}`);
|
|
284
|
+
}
|
|
285
|
+
this.initializeDatabase();
|
|
286
|
+
}
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// Database Initialization
|
|
289
|
+
// ============================================================================
|
|
290
|
+
initializeDatabase() {
|
|
291
|
+
this.db.exec(`
|
|
292
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
293
|
+
id TEXT PRIMARY KEY,
|
|
294
|
+
workflow_kind TEXT NOT NULL,
|
|
295
|
+
trigger_type TEXT NOT NULL,
|
|
296
|
+
cron_expression TEXT,
|
|
297
|
+
timezone TEXT,
|
|
298
|
+
trigger_on_workflow_kind TEXT,
|
|
299
|
+
trigger_on_status TEXT,
|
|
300
|
+
input TEXT,
|
|
301
|
+
metadata TEXT,
|
|
302
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
303
|
+
last_run_at TEXT,
|
|
304
|
+
last_run_id TEXT,
|
|
305
|
+
next_run_at TEXT,
|
|
306
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
307
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
308
|
+
)
|
|
309
|
+
`);
|
|
310
|
+
this.db.exec(`
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_workflow_kind
|
|
312
|
+
ON ${this.tableName}(workflow_kind);
|
|
313
|
+
|
|
314
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_enabled
|
|
315
|
+
ON ${this.tableName}(enabled);
|
|
316
|
+
|
|
317
|
+
CREATE INDEX IF NOT EXISTS idx_${this.tableName}_trigger_type
|
|
318
|
+
ON ${this.tableName}(trigger_type);
|
|
319
|
+
`);
|
|
320
|
+
this.stmts = {
|
|
321
|
+
insert: this.db.prepare(`
|
|
322
|
+
INSERT INTO ${this.tableName} (
|
|
323
|
+
id, workflow_kind, trigger_type, cron_expression, timezone,
|
|
324
|
+
trigger_on_workflow_kind, trigger_on_status, input, metadata,
|
|
325
|
+
enabled, last_run_at, last_run_id, next_run_at
|
|
326
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
+
`),
|
|
328
|
+
update: this.db.prepare(`
|
|
329
|
+
UPDATE ${this.tableName}
|
|
330
|
+
SET workflow_kind = COALESCE(?, workflow_kind),
|
|
331
|
+
trigger_type = COALESCE(?, trigger_type),
|
|
332
|
+
cron_expression = ?,
|
|
333
|
+
timezone = ?,
|
|
334
|
+
trigger_on_workflow_kind = ?,
|
|
335
|
+
trigger_on_status = ?,
|
|
336
|
+
input = ?,
|
|
337
|
+
metadata = ?,
|
|
338
|
+
enabled = COALESCE(?, enabled),
|
|
339
|
+
last_run_at = ?,
|
|
340
|
+
last_run_id = ?,
|
|
341
|
+
next_run_at = ?,
|
|
342
|
+
updated_at = datetime('now')
|
|
343
|
+
WHERE id = ?
|
|
344
|
+
`),
|
|
345
|
+
delete: this.db.prepare(`
|
|
346
|
+
DELETE FROM ${this.tableName} WHERE id = ?
|
|
347
|
+
`),
|
|
348
|
+
getAll: this.db.prepare(`
|
|
349
|
+
SELECT * FROM ${this.tableName}
|
|
350
|
+
`),
|
|
351
|
+
getById: this.db.prepare(`
|
|
352
|
+
SELECT * FROM ${this.tableName} WHERE id = ?
|
|
353
|
+
`)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// SchedulePersistence Interface Implementation
|
|
358
|
+
// ============================================================================
|
|
359
|
+
async loadSchedules() {
|
|
360
|
+
const rows = this.stmts.getAll.all();
|
|
361
|
+
return rows.map((row) => this.rowToSchedule(row));
|
|
362
|
+
}
|
|
363
|
+
async saveSchedule(schedule) {
|
|
364
|
+
this.stmts.insert.run(
|
|
365
|
+
schedule.id,
|
|
366
|
+
schedule.workflowKind,
|
|
367
|
+
schedule.triggerType,
|
|
368
|
+
schedule.cronExpression ?? null,
|
|
369
|
+
schedule.timezone ?? null,
|
|
370
|
+
schedule.triggerOnWorkflowKind ?? null,
|
|
371
|
+
schedule.triggerOnStatus ? JSON.stringify(schedule.triggerOnStatus) : null,
|
|
372
|
+
schedule.input ? JSON.stringify(schedule.input) : null,
|
|
373
|
+
schedule.metadata ? JSON.stringify(schedule.metadata) : null,
|
|
374
|
+
schedule.enabled ? 1 : 0,
|
|
375
|
+
schedule.lastRunAt?.toISOString() ?? null,
|
|
376
|
+
schedule.lastRunId ?? null,
|
|
377
|
+
schedule.nextRunAt?.toISOString() ?? null
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
async updateSchedule(scheduleId, updates) {
|
|
381
|
+
const existing = this.stmts.getById.get(scheduleId);
|
|
382
|
+
if (!existing) {
|
|
383
|
+
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
384
|
+
}
|
|
385
|
+
const merged = {
|
|
386
|
+
...this.rowToSchedule(existing),
|
|
387
|
+
...updates
|
|
388
|
+
};
|
|
389
|
+
this.stmts.update.run(
|
|
390
|
+
updates.workflowKind ?? null,
|
|
391
|
+
updates.triggerType ?? null,
|
|
392
|
+
merged.cronExpression ?? null,
|
|
393
|
+
merged.timezone ?? null,
|
|
394
|
+
merged.triggerOnWorkflowKind ?? null,
|
|
395
|
+
merged.triggerOnStatus ? JSON.stringify(merged.triggerOnStatus) : null,
|
|
396
|
+
merged.input ? JSON.stringify(merged.input) : null,
|
|
397
|
+
merged.metadata ? JSON.stringify(merged.metadata) : null,
|
|
398
|
+
updates.enabled !== void 0 ? updates.enabled ? 1 : 0 : null,
|
|
399
|
+
merged.lastRunAt?.toISOString() ?? null,
|
|
400
|
+
merged.lastRunId ?? null,
|
|
401
|
+
merged.nextRunAt?.toISOString() ?? null,
|
|
402
|
+
scheduleId
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
async deleteSchedule(scheduleId) {
|
|
406
|
+
this.stmts.delete.run(scheduleId);
|
|
407
|
+
}
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// Helper Methods
|
|
410
|
+
// ============================================================================
|
|
411
|
+
rowToSchedule(row) {
|
|
412
|
+
return {
|
|
413
|
+
id: row.id,
|
|
414
|
+
workflowKind: row.workflow_kind,
|
|
415
|
+
triggerType: row.trigger_type,
|
|
416
|
+
cronExpression: row.cron_expression ?? void 0,
|
|
417
|
+
timezone: row.timezone ?? void 0,
|
|
418
|
+
triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? void 0,
|
|
419
|
+
triggerOnStatus: row.trigger_on_status ? JSON.parse(row.trigger_on_status) : void 0,
|
|
420
|
+
input: row.input ? JSON.parse(row.input) : void 0,
|
|
421
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
422
|
+
enabled: row.enabled === 1,
|
|
423
|
+
lastRunAt: row.last_run_at ? new Date(row.last_run_at) : void 0,
|
|
424
|
+
lastRunId: row.last_run_id ?? void 0,
|
|
425
|
+
nextRunAt: row.next_run_at ? new Date(row.next_run_at) : void 0
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/scheduler/postgres-persistence.ts
|
|
431
|
+
var Kysely;
|
|
432
|
+
var PostgresDialect;
|
|
433
|
+
var sql;
|
|
434
|
+
var pgModule;
|
|
435
|
+
var PostgresSchedulePersistence = class {
|
|
436
|
+
db;
|
|
437
|
+
pool;
|
|
438
|
+
ownsPool = false;
|
|
439
|
+
schema;
|
|
440
|
+
tableName;
|
|
441
|
+
autoMigrate;
|
|
442
|
+
initialized = false;
|
|
443
|
+
config;
|
|
444
|
+
constructor(config) {
|
|
445
|
+
this.schema = config.schema ?? "public";
|
|
446
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.schema)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Invalid schema name "${this.schema}". Schema must start with a letter or underscore, contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
this.tableName = config.tableName ?? "workflow_schedules";
|
|
452
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.tableName)) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Invalid table name "${this.tableName}". Table name must start with a letter or underscore, contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
this.autoMigrate = config.autoMigrate !== false;
|
|
458
|
+
this.config = config;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get a schema-scoped query builder.
|
|
462
|
+
* All queries MUST use this instead of this.db directly to respect config.schema.
|
|
463
|
+
*/
|
|
464
|
+
get qb() {
|
|
465
|
+
return this.db.withSchema(this.schema);
|
|
466
|
+
}
|
|
467
|
+
ensureInitialized() {
|
|
468
|
+
if (!this.initialized) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
"PostgresSchedulePersistence is not initialized. Call initialize() before using the adapter."
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Initialize the persistence layer.
|
|
476
|
+
* Creates the schedules table if autoMigrate is enabled.
|
|
477
|
+
*/
|
|
478
|
+
async initialize() {
|
|
479
|
+
if (this.initialized) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const deps = await loadPostgresDeps();
|
|
483
|
+
Kysely = deps.Kysely;
|
|
484
|
+
PostgresDialect = deps.PostgresDialect;
|
|
485
|
+
sql = deps.sql;
|
|
486
|
+
pgModule = deps.pgModule;
|
|
487
|
+
if (this.config.pool) {
|
|
488
|
+
this.pool = this.config.pool;
|
|
489
|
+
this.ownsPool = false;
|
|
490
|
+
} else if (this.config.connectionString) {
|
|
491
|
+
this.pool = new pgModule.Pool({
|
|
492
|
+
connectionString: this.config.connectionString
|
|
493
|
+
});
|
|
494
|
+
this.ownsPool = true;
|
|
495
|
+
} else if (this.config.poolConfig) {
|
|
496
|
+
this.pool = new pgModule.Pool(this.config.poolConfig);
|
|
497
|
+
this.ownsPool = true;
|
|
498
|
+
} else {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"PostgresSchedulePersistenceConfig must include either pool, connectionString, or poolConfig"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
this.db = new Kysely({
|
|
504
|
+
dialect: new PostgresDialect({
|
|
505
|
+
pool: this.pool
|
|
506
|
+
})
|
|
507
|
+
});
|
|
508
|
+
if (this.autoMigrate) {
|
|
509
|
+
await this.createTables();
|
|
510
|
+
}
|
|
511
|
+
this.initialized = true;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Close the database connection.
|
|
515
|
+
* Only closes the pool if it was created by this adapter.
|
|
516
|
+
*/
|
|
517
|
+
async close() {
|
|
518
|
+
await this.db.destroy();
|
|
519
|
+
if (this.ownsPool) {
|
|
520
|
+
await this.pool.end();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// ============================================================================
|
|
524
|
+
// Database Initialization
|
|
525
|
+
// ============================================================================
|
|
526
|
+
async createTables() {
|
|
527
|
+
const fullTableName = this.schema === "public" ? this.tableName : `${this.schema}.${this.tableName}`;
|
|
528
|
+
if (this.schema !== "public") {
|
|
529
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(
|
|
530
|
+
this.db
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
await sql`
|
|
534
|
+
CREATE TABLE IF NOT EXISTS ${sql.table(fullTableName)} (
|
|
535
|
+
id TEXT PRIMARY KEY,
|
|
536
|
+
workflow_kind TEXT NOT NULL,
|
|
537
|
+
trigger_type TEXT NOT NULL,
|
|
538
|
+
cron_expression TEXT,
|
|
539
|
+
timezone TEXT DEFAULT 'UTC',
|
|
540
|
+
trigger_on_workflow_kind TEXT,
|
|
541
|
+
trigger_on_status JSONB,
|
|
542
|
+
input_json JSONB NOT NULL DEFAULT '{}',
|
|
543
|
+
metadata_json JSONB NOT NULL DEFAULT '{}',
|
|
544
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
545
|
+
last_run_at TIMESTAMPTZ,
|
|
546
|
+
last_run_id TEXT,
|
|
547
|
+
next_run_at TIMESTAMPTZ,
|
|
548
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
549
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
550
|
+
CONSTRAINT ${sql.ref(`${this.tableName}_trigger_type_check`)} CHECK (
|
|
551
|
+
trigger_type IN ('cron', 'workflow_completed', 'manual')
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
`.execute(this.db);
|
|
555
|
+
await sql`
|
|
556
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${this.tableName}_workflow_kind`)}
|
|
557
|
+
ON ${sql.table(fullTableName)} (workflow_kind)
|
|
558
|
+
`.execute(this.db);
|
|
559
|
+
await sql`
|
|
560
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${this.tableName}_enabled`)}
|
|
561
|
+
ON ${sql.table(fullTableName)} (enabled)
|
|
562
|
+
`.execute(this.db);
|
|
563
|
+
await sql`
|
|
564
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${this.tableName}_trigger_type`)}
|
|
565
|
+
ON ${sql.table(fullTableName)} (trigger_type)
|
|
566
|
+
`.execute(this.db);
|
|
567
|
+
await sql`
|
|
568
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${this.tableName}_next_run`)}
|
|
569
|
+
ON ${sql.table(fullTableName)} (next_run_at)
|
|
570
|
+
WHERE enabled = true
|
|
571
|
+
`.execute(this.db);
|
|
572
|
+
}
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// SchedulePersistence Interface Implementation
|
|
575
|
+
// ============================================================================
|
|
576
|
+
async loadSchedules() {
|
|
577
|
+
this.ensureInitialized();
|
|
578
|
+
const rows = await this.qb.selectFrom(this.tableName).selectAll().execute();
|
|
579
|
+
return rows.map((row) => this.rowToSchedule(row));
|
|
580
|
+
}
|
|
581
|
+
async saveSchedule(schedule) {
|
|
582
|
+
this.ensureInitialized();
|
|
583
|
+
await this.qb.insertInto(this.tableName).values({
|
|
584
|
+
id: schedule.id,
|
|
585
|
+
workflow_kind: schedule.workflowKind,
|
|
586
|
+
trigger_type: schedule.triggerType,
|
|
587
|
+
cron_expression: schedule.cronExpression ?? null,
|
|
588
|
+
timezone: schedule.timezone ?? null,
|
|
589
|
+
trigger_on_workflow_kind: schedule.triggerOnWorkflowKind ?? null,
|
|
590
|
+
trigger_on_status: schedule.triggerOnStatus ? JSON.stringify(schedule.triggerOnStatus) : null,
|
|
591
|
+
input_json: schedule.input ? JSON.stringify(schedule.input) : null,
|
|
592
|
+
metadata_json: schedule.metadata ? JSON.stringify(schedule.metadata) : null,
|
|
593
|
+
enabled: schedule.enabled,
|
|
594
|
+
last_run_at: schedule.lastRunAt ?? null,
|
|
595
|
+
last_run_id: schedule.lastRunId ?? null,
|
|
596
|
+
next_run_at: schedule.nextRunAt ?? null,
|
|
597
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
598
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
599
|
+
}).execute();
|
|
600
|
+
}
|
|
601
|
+
async updateSchedule(scheduleId, updates) {
|
|
602
|
+
this.ensureInitialized();
|
|
603
|
+
const existing = await this.qb.selectFrom(this.tableName).selectAll().where("id", "=", scheduleId).executeTakeFirst();
|
|
604
|
+
if (!existing) {
|
|
605
|
+
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
606
|
+
}
|
|
607
|
+
const merged = {
|
|
608
|
+
...this.rowToSchedule(existing),
|
|
609
|
+
...updates
|
|
610
|
+
};
|
|
611
|
+
const fieldMappings = [
|
|
612
|
+
{ domainKey: "workflowKind", dbKey: "workflow_kind" },
|
|
613
|
+
{ domainKey: "triggerType", dbKey: "trigger_type" },
|
|
614
|
+
{ domainKey: "cronExpression", dbKey: "cron_expression" },
|
|
615
|
+
{ domainKey: "timezone", dbKey: "timezone" },
|
|
616
|
+
{ domainKey: "triggerOnWorkflowKind", dbKey: "trigger_on_workflow_kind" },
|
|
617
|
+
{
|
|
618
|
+
domainKey: "triggerOnStatus",
|
|
619
|
+
dbKey: "trigger_on_status",
|
|
620
|
+
serialize: (v) => v ? JSON.stringify(v) : null
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
domainKey: "input",
|
|
624
|
+
dbKey: "input_json",
|
|
625
|
+
serialize: (v) => v ? JSON.stringify(v) : null
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
domainKey: "metadata",
|
|
629
|
+
dbKey: "metadata_json",
|
|
630
|
+
serialize: (v) => v ? JSON.stringify(v) : null
|
|
631
|
+
},
|
|
632
|
+
{ domainKey: "enabled", dbKey: "enabled" },
|
|
633
|
+
{ domainKey: "lastRunAt", dbKey: "last_run_at" },
|
|
634
|
+
{ domainKey: "lastRunId", dbKey: "last_run_id" },
|
|
635
|
+
{ domainKey: "nextRunAt", dbKey: "next_run_at" }
|
|
636
|
+
];
|
|
637
|
+
const updateData = {
|
|
638
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
639
|
+
};
|
|
640
|
+
for (const { domainKey, dbKey, serialize } of fieldMappings) {
|
|
641
|
+
if (updates[domainKey] !== void 0 || merged[domainKey] !== void 0) {
|
|
642
|
+
const value = serialize ? serialize(merged[domainKey]) : merged[domainKey] ?? null;
|
|
643
|
+
updateData[dbKey] = value;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
await this.qb.updateTable(this.tableName).set(updateData).where("id", "=", scheduleId).execute();
|
|
647
|
+
}
|
|
648
|
+
async deleteSchedule(scheduleId) {
|
|
649
|
+
this.ensureInitialized();
|
|
650
|
+
await this.qb.deleteFrom(this.tableName).where("id", "=", scheduleId).execute();
|
|
651
|
+
}
|
|
652
|
+
// ============================================================================
|
|
653
|
+
// Additional Methods
|
|
654
|
+
// ============================================================================
|
|
655
|
+
/**
|
|
656
|
+
* Get a schedule by ID.
|
|
657
|
+
*/
|
|
658
|
+
async getSchedule(scheduleId) {
|
|
659
|
+
this.ensureInitialized();
|
|
660
|
+
const row = await this.qb.selectFrom(this.tableName).selectAll().where("id", "=", scheduleId).executeTakeFirst();
|
|
661
|
+
return row ? this.rowToSchedule(row) : null;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get all enabled schedules that are due to run.
|
|
665
|
+
*/
|
|
666
|
+
async getDueSchedules() {
|
|
667
|
+
this.ensureInitialized();
|
|
668
|
+
const now = /* @__PURE__ */ new Date();
|
|
669
|
+
const rows = await this.qb.selectFrom(this.tableName).selectAll().where("enabled", "=", true).where("trigger_type", "=", "cron").where("next_run_at", "<=", now).execute();
|
|
670
|
+
return rows.map((row) => this.rowToSchedule(row));
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Get schedules by workflow kind.
|
|
674
|
+
*/
|
|
675
|
+
async getSchedulesByWorkflowKind(workflowKind) {
|
|
676
|
+
this.ensureInitialized();
|
|
677
|
+
const rows = await this.qb.selectFrom(this.tableName).selectAll().where("workflow_kind", "=", workflowKind).execute();
|
|
678
|
+
return rows.map((row) => this.rowToSchedule(row));
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Get workflow completion triggers for a specific workflow kind.
|
|
682
|
+
*/
|
|
683
|
+
async getCompletionTriggers(triggerOnWorkflowKind) {
|
|
684
|
+
this.ensureInitialized();
|
|
685
|
+
const rows = await this.qb.selectFrom(this.tableName).selectAll().where("enabled", "=", true).where("trigger_type", "=", "workflow_completed").where("trigger_on_workflow_kind", "=", triggerOnWorkflowKind).execute();
|
|
686
|
+
return rows.map((row) => this.rowToSchedule(row));
|
|
687
|
+
}
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// Helper Methods
|
|
690
|
+
// ============================================================================
|
|
691
|
+
rowToSchedule(row) {
|
|
692
|
+
return {
|
|
693
|
+
id: row.id,
|
|
694
|
+
workflowKind: row.workflow_kind,
|
|
695
|
+
triggerType: row.trigger_type,
|
|
696
|
+
cronExpression: row.cron_expression ?? void 0,
|
|
697
|
+
timezone: row.timezone ?? void 0,
|
|
698
|
+
triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? void 0,
|
|
699
|
+
triggerOnStatus: row.trigger_on_status ? typeof row.trigger_on_status === "string" ? JSON.parse(row.trigger_on_status) : row.trigger_on_status : void 0,
|
|
700
|
+
input: row.input_json ? typeof row.input_json === "string" ? JSON.parse(row.input_json) : row.input_json : void 0,
|
|
701
|
+
metadata: row.metadata_json ? typeof row.metadata_json === "string" ? JSON.parse(row.metadata_json) : row.metadata_json : void 0,
|
|
702
|
+
enabled: row.enabled,
|
|
703
|
+
lastRunAt: row.last_run_at ? new Date(row.last_run_at) : void 0,
|
|
704
|
+
lastRunId: row.last_run_id ?? void 0,
|
|
705
|
+
nextRunAt: row.next_run_at ? new Date(row.next_run_at) : void 0
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
export {
|
|
711
|
+
CronScheduler,
|
|
712
|
+
SQLiteSchedulePersistence,
|
|
713
|
+
PostgresSchedulePersistence
|
|
714
|
+
};
|
|
715
|
+
//# sourceMappingURL=chunk-U5QGN3RT.js.map
|