@multiplier-labs/stepflow 0.3.1 → 0.3.3
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/comments/2026-04-04_comment-assessment.md +179 -0
- package/.codekin/reports/complexity/2026-04-03_complexity-report.md +152 -0
- package/.codekin/reports/dependencies/2026-04-04_dependency-health.md +94 -0
- package/.codekin/reports/docs-audit/2026-04-04_docs-audit.md +10 -0
- package/.codekin/reports/security/2026-04-03_security-audit.md +208 -0
- package/.github/CODEOWNERS +15 -0
- package/.github/dependabot.yml +23 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +11 -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-4VOKMGP6.js} +23 -42
- package/dist/chunk-4VOKMGP6.js.map +1 -0
- package/dist/{chunk-XCF4PLN7.js → chunk-GYPNJBDP.js} +12 -16
- package/dist/chunk-GYPNJBDP.js.map +1 -0
- package/dist/{chunk-U5QGN3RT.js → chunk-KHTUWLVZ.js} +14 -47
- package/dist/chunk-KHTUWLVZ.js.map +1 -0
- package/dist/{chunk-GXY66V7Q.js → chunk-ML35PIHX.js} +1 -1
- package/dist/chunk-ML35PIHX.js.map +1 -0
- package/dist/events/index.d.ts +4 -4
- package/dist/events/index.js +1 -1
- package/dist/{index-CPfGsmrp.d.ts → index-Dzr437iW.d.ts} +6 -6
- package/dist/index.d.ts +14 -12
- package/dist/index.js +40 -82
- 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 +13 -13
- package/dist/storage/index.js +2 -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 +2 -3
- package/package.json +7 -6
- package/src/core/engine.test.ts +466 -522
- package/src/core/engine.ts +42 -67
- package/src/core/index.ts +3 -3
- package/src/core/orchestrator.ts +64 -83
- 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 +197 -235
- package/src/events/webhook.ts +44 -71
- package/src/index.ts +19 -19
- package/src/planning/index.ts +6 -3
- package/src/planning/planner.ts +66 -91
- package/src/planning/planning.test.ts +303 -366
- package/src/planning/registry.test.ts +204 -251
- package/src/planning/registry.ts +39 -43
- package/src/planning/types.ts +23 -27
- package/src/scheduler/cron.test.ts +141 -150
- package/src/scheduler/cron.ts +32 -67
- package/src/scheduler/index.ts +4 -14
- package/src/scheduler/postgres-persistence.ts +71 -99
- package/src/scheduler/sqlite-persistence.test.ts +96 -96
- package/src/scheduler/sqlite-persistence.ts +12 -19
- 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.ts +197 -393
- package/src/storage/sqlite.test.ts +154 -281
- package/src/storage/sqlite.ts +50 -85
- 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 +4 -4
- package/src/utils/logger.test.ts +52 -58
- package/src/utils/logger.ts +4 -4
- package/src/utils/postgres-deps.ts +4 -4
- package/src/utils/retry.test.ts +47 -67
- package/src/utils/retry.ts +9 -9
- 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
package/src/scheduler/cron.ts
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* Provides time-based and workflow-completion-based triggers for workflows.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { CronExpressionParser } from
|
|
8
|
-
import type { WorkflowKind, RunStatus, Logger } from
|
|
9
|
-
import type { WorkflowEngine } from
|
|
10
|
-
import type { Scheduler, WorkflowSchedule, TriggerType } from
|
|
11
|
-
import type { WorkflowEvent } from
|
|
12
|
-
import { generateId } from
|
|
13
|
-
import { ConsoleLogger } from
|
|
7
|
+
import { CronExpressionParser } from 'cron-parser';
|
|
8
|
+
import type { WorkflowKind, RunStatus, Logger } from '../core/types';
|
|
9
|
+
import type { WorkflowEngine } from '../core/engine';
|
|
10
|
+
import type { Scheduler, WorkflowSchedule, TriggerType } from './types';
|
|
11
|
+
import type { WorkflowEvent } from '../events/types';
|
|
12
|
+
import { generateId } from '../utils/id';
|
|
13
|
+
import { ConsoleLogger } from '../utils/logger';
|
|
14
14
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// Configuration Types
|
|
@@ -45,10 +45,7 @@ export interface SchedulePersistence {
|
|
|
45
45
|
saveSchedule(schedule: WorkflowSchedule): Promise<void>;
|
|
46
46
|
|
|
47
47
|
/** Update a schedule */
|
|
48
|
-
updateSchedule(
|
|
49
|
-
scheduleId: string,
|
|
50
|
-
updates: Partial<WorkflowSchedule>,
|
|
51
|
-
): Promise<void>;
|
|
48
|
+
updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void>;
|
|
52
49
|
|
|
53
50
|
/** Delete a schedule */
|
|
54
51
|
deleteSchedule(scheduleId: string): Promise<void>;
|
|
@@ -116,11 +113,11 @@ export class CronScheduler implements Scheduler {
|
|
|
116
113
|
*/
|
|
117
114
|
async start(): Promise<void> {
|
|
118
115
|
if (this.running) {
|
|
119
|
-
this.logger.warn(
|
|
116
|
+
this.logger.warn('Scheduler is already running');
|
|
120
117
|
return;
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
this.logger.info(
|
|
120
|
+
this.logger.info('Starting scheduler...');
|
|
124
121
|
|
|
125
122
|
// Load schedules from persistence if available
|
|
126
123
|
if (this.persistence) {
|
|
@@ -133,16 +130,13 @@ export class CronScheduler implements Scheduler {
|
|
|
133
130
|
|
|
134
131
|
// Calculate next run times for cron schedules
|
|
135
132
|
for (const schedule of this.schedules.values()) {
|
|
136
|
-
if (schedule.triggerType ===
|
|
133
|
+
if (schedule.triggerType === 'cron' && schedule.cronExpression) {
|
|
137
134
|
this.updateNextRunTime(schedule);
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
// Start polling for cron schedules
|
|
142
|
-
this.pollTimer = setInterval(
|
|
143
|
-
() => this.checkSchedules(),
|
|
144
|
-
this.pollInterval,
|
|
145
|
-
);
|
|
139
|
+
this.pollTimer = setInterval(() => this.checkSchedules(), this.pollInterval);
|
|
146
140
|
|
|
147
141
|
// Subscribe to workflow completion events
|
|
148
142
|
this.eventUnsubscribe = this.engine.subscribeToAll((event) => {
|
|
@@ -150,7 +144,7 @@ export class CronScheduler implements Scheduler {
|
|
|
150
144
|
});
|
|
151
145
|
|
|
152
146
|
this.running = true;
|
|
153
|
-
this.logger.info(
|
|
147
|
+
this.logger.info('Scheduler started');
|
|
154
148
|
}
|
|
155
149
|
|
|
156
150
|
/**
|
|
@@ -162,7 +156,7 @@ export class CronScheduler implements Scheduler {
|
|
|
162
156
|
return;
|
|
163
157
|
}
|
|
164
158
|
|
|
165
|
-
this.logger.info(
|
|
159
|
+
this.logger.info('Stopping scheduler...');
|
|
166
160
|
|
|
167
161
|
if (this.pollTimer) {
|
|
168
162
|
clearInterval(this.pollTimer);
|
|
@@ -175,22 +169,20 @@ export class CronScheduler implements Scheduler {
|
|
|
175
169
|
}
|
|
176
170
|
|
|
177
171
|
this.running = false;
|
|
178
|
-
this.logger.info(
|
|
172
|
+
this.logger.info('Scheduler stopped');
|
|
179
173
|
}
|
|
180
174
|
|
|
181
175
|
/**
|
|
182
176
|
* Add a new schedule.
|
|
183
177
|
*/
|
|
184
|
-
async addSchedule(
|
|
185
|
-
scheduleData: Omit<WorkflowSchedule, "id">,
|
|
186
|
-
): Promise<WorkflowSchedule> {
|
|
178
|
+
async addSchedule(scheduleData: Omit<WorkflowSchedule, 'id'>): Promise<WorkflowSchedule> {
|
|
187
179
|
const schedule: WorkflowSchedule = {
|
|
188
180
|
...scheduleData,
|
|
189
181
|
id: generateId(),
|
|
190
182
|
};
|
|
191
183
|
|
|
192
184
|
// Validate cron expression if provided
|
|
193
|
-
if (schedule.triggerType ===
|
|
185
|
+
if (schedule.triggerType === 'cron' && schedule.cronExpression) {
|
|
194
186
|
try {
|
|
195
187
|
CronExpressionParser.parse(schedule.cronExpression, {
|
|
196
188
|
tz: schedule.timezone,
|
|
@@ -204,11 +196,9 @@ export class CronScheduler implements Scheduler {
|
|
|
204
196
|
}
|
|
205
197
|
|
|
206
198
|
// Validate workflow completion trigger
|
|
207
|
-
if (schedule.triggerType ===
|
|
199
|
+
if (schedule.triggerType === 'workflow_completed') {
|
|
208
200
|
if (!schedule.triggerOnWorkflowKind) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
"triggerOnWorkflowKind is required for workflow_completed triggers",
|
|
211
|
-
);
|
|
201
|
+
throw new Error('triggerOnWorkflowKind is required for workflow_completed triggers');
|
|
212
202
|
}
|
|
213
203
|
}
|
|
214
204
|
|
|
@@ -219,9 +209,7 @@ export class CronScheduler implements Scheduler {
|
|
|
219
209
|
await this.persistence.saveSchedule(schedule);
|
|
220
210
|
}
|
|
221
211
|
|
|
222
|
-
this.logger.info(
|
|
223
|
-
`Added schedule: ${schedule.id} (${schedule.workflowKind})`,
|
|
224
|
-
);
|
|
212
|
+
this.logger.info(`Added schedule: ${schedule.id} (${schedule.workflowKind})`);
|
|
225
213
|
|
|
226
214
|
return schedule;
|
|
227
215
|
}
|
|
@@ -246,10 +234,7 @@ export class CronScheduler implements Scheduler {
|
|
|
246
234
|
/**
|
|
247
235
|
* Update a schedule.
|
|
248
236
|
*/
|
|
249
|
-
async updateSchedule(
|
|
250
|
-
scheduleId: string,
|
|
251
|
-
updates: Partial<WorkflowSchedule>,
|
|
252
|
-
): Promise<void> {
|
|
237
|
+
async updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void> {
|
|
253
238
|
const schedule = this.schedules.get(scheduleId);
|
|
254
239
|
if (!schedule) {
|
|
255
240
|
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
@@ -262,10 +247,7 @@ export class CronScheduler implements Scheduler {
|
|
|
262
247
|
Object.assign(schedule, updates);
|
|
263
248
|
|
|
264
249
|
// Revalidate and update next run time if cron expression changed
|
|
265
|
-
if (
|
|
266
|
-
updates.cronExpression !== undefined ||
|
|
267
|
-
updates.timezone !== undefined
|
|
268
|
-
) {
|
|
250
|
+
if (updates.cronExpression !== undefined || updates.timezone !== undefined) {
|
|
269
251
|
if (schedule.cronExpression) {
|
|
270
252
|
try {
|
|
271
253
|
CronExpressionParser.parse(schedule.cronExpression, {
|
|
@@ -273,9 +255,7 @@ export class CronScheduler implements Scheduler {
|
|
|
273
255
|
});
|
|
274
256
|
this.updateNextRunTime(schedule);
|
|
275
257
|
} catch (error) {
|
|
276
|
-
throw new Error(
|
|
277
|
-
`Invalid cron expression: ${schedule.cronExpression}`,
|
|
278
|
-
);
|
|
258
|
+
throw new Error(`Invalid cron expression: ${schedule.cronExpression}`);
|
|
279
259
|
}
|
|
280
260
|
}
|
|
281
261
|
}
|
|
@@ -325,16 +305,13 @@ export class CronScheduler implements Scheduler {
|
|
|
325
305
|
|
|
326
306
|
for (const schedule of this.schedules.values()) {
|
|
327
307
|
if (!schedule.enabled) continue;
|
|
328
|
-
if (schedule.triggerType !==
|
|
308
|
+
if (schedule.triggerType !== 'cron') continue;
|
|
329
309
|
if (!schedule.nextRunAt) continue;
|
|
330
310
|
|
|
331
311
|
if (schedule.nextRunAt <= now) {
|
|
332
312
|
// Execute the schedule
|
|
333
313
|
this.executeSchedule(schedule).catch((error) => {
|
|
334
|
-
this.logger.error(
|
|
335
|
-
`Failed to execute schedule ${schedule.id}:`,
|
|
336
|
-
error,
|
|
337
|
-
);
|
|
314
|
+
this.logger.error(`Failed to execute schedule ${schedule.id}:`, error);
|
|
338
315
|
});
|
|
339
316
|
|
|
340
317
|
// Update next run time
|
|
@@ -349,28 +326,21 @@ export class CronScheduler implements Scheduler {
|
|
|
349
326
|
private handleWorkflowEvent(event: WorkflowEvent): void {
|
|
350
327
|
// Only handle run completion events
|
|
351
328
|
// Note: orchestrator emits 'run.completed' for success, 'run.failed' for failure
|
|
352
|
-
if (
|
|
353
|
-
event.eventType !== "run.completed" &&
|
|
354
|
-
event.eventType !== "run.failed"
|
|
355
|
-
) {
|
|
329
|
+
if (event.eventType !== 'run.completed' && event.eventType !== 'run.failed') {
|
|
356
330
|
return;
|
|
357
331
|
}
|
|
358
332
|
|
|
359
|
-
const completedStatus: RunStatus =
|
|
360
|
-
event.eventType === "run.completed" ? "succeeded" : "failed";
|
|
333
|
+
const completedStatus: RunStatus = event.eventType === 'run.completed' ? 'succeeded' : 'failed';
|
|
361
334
|
const completedKind = event.kind;
|
|
362
335
|
|
|
363
336
|
// Find schedules triggered by this workflow completion
|
|
364
337
|
for (const schedule of this.schedules.values()) {
|
|
365
338
|
if (!schedule.enabled) continue;
|
|
366
|
-
if (schedule.triggerType !==
|
|
339
|
+
if (schedule.triggerType !== 'workflow_completed') continue;
|
|
367
340
|
if (schedule.triggerOnWorkflowKind !== completedKind) continue;
|
|
368
341
|
|
|
369
342
|
// Check if the status matches
|
|
370
|
-
if (
|
|
371
|
-
schedule.triggerOnStatus &&
|
|
372
|
-
!schedule.triggerOnStatus.includes(completedStatus)
|
|
373
|
-
) {
|
|
343
|
+
if (schedule.triggerOnStatus && !schedule.triggerOnStatus.includes(completedStatus)) {
|
|
374
344
|
continue;
|
|
375
345
|
}
|
|
376
346
|
|
|
@@ -389,11 +359,9 @@ export class CronScheduler implements Scheduler {
|
|
|
389
359
|
*/
|
|
390
360
|
private async executeSchedule(
|
|
391
361
|
schedule: WorkflowSchedule,
|
|
392
|
-
triggerContext?: { triggerRunId?: string; triggerStatus?: RunStatus }
|
|
362
|
+
triggerContext?: { triggerRunId?: string; triggerStatus?: RunStatus }
|
|
393
363
|
): Promise<string> {
|
|
394
|
-
this.logger.info(
|
|
395
|
-
`Executing schedule: ${schedule.id} (${schedule.workflowKind})`,
|
|
396
|
-
);
|
|
364
|
+
this.logger.info(`Executing schedule: ${schedule.id} (${schedule.workflowKind})`);
|
|
397
365
|
|
|
398
366
|
// Build metadata with trigger info
|
|
399
367
|
const metadata = {
|
|
@@ -439,10 +407,7 @@ export class CronScheduler implements Scheduler {
|
|
|
439
407
|
|
|
440
408
|
schedule.nextRunAt = interval.next().toDate();
|
|
441
409
|
} catch (error) {
|
|
442
|
-
this.logger.error(
|
|
443
|
-
`Failed to parse cron expression for schedule ${schedule.id}:`,
|
|
444
|
-
error,
|
|
445
|
-
);
|
|
410
|
+
this.logger.error(`Failed to parse cron expression for schedule ${schedule.id}:`, error);
|
|
446
411
|
schedule.nextRunAt = undefined;
|
|
447
412
|
}
|
|
448
413
|
}
|
package/src/scheduler/index.ts
CHANGED
|
@@ -2,17 +2,7 @@
|
|
|
2
2
|
* Scheduler module exports.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export * from
|
|
6
|
-
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type SchedulePersistence,
|
|
10
|
-
} from "./cron";
|
|
11
|
-
export {
|
|
12
|
-
SQLiteSchedulePersistence,
|
|
13
|
-
type SQLiteSchedulePersistenceConfig,
|
|
14
|
-
} from "./sqlite-persistence";
|
|
15
|
-
export {
|
|
16
|
-
PostgresSchedulePersistence,
|
|
17
|
-
type PostgresSchedulePersistenceConfig,
|
|
18
|
-
} from "./postgres-persistence";
|
|
5
|
+
export * from './types';
|
|
6
|
+
export { CronScheduler, type CronSchedulerConfig, type SchedulePersistence } from './cron';
|
|
7
|
+
export { SQLiteSchedulePersistence, type SQLiteSchedulePersistenceConfig } from './sqlite-persistence';
|
|
8
|
+
export { PostgresSchedulePersistence, type PostgresSchedulePersistenceConfig } from './postgres-persistence';
|
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
* Stores workflow schedules in a PostgreSQL database table using Kysely.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Kysely as KyselyType } from
|
|
8
|
-
import type { Pool, PoolConfig } from
|
|
9
|
-
import { loadPostgresDeps } from
|
|
7
|
+
import type { Kysely as KyselyType } from 'kysely';
|
|
8
|
+
import type { Pool, PoolConfig } from 'pg';
|
|
9
|
+
import { loadPostgresDeps } from '../utils/postgres-deps.js';
|
|
10
10
|
|
|
11
11
|
// Lazy-loaded dependencies - populated by loadPostgresDeps() during initialize()
|
|
12
12
|
let Kysely: any;
|
|
13
13
|
let PostgresDialect: any;
|
|
14
14
|
let sql: any;
|
|
15
15
|
let pgModule: any;
|
|
16
|
-
import type { WorkflowSchedule } from
|
|
17
|
-
import type { SchedulePersistence } from
|
|
16
|
+
import type { WorkflowSchedule } from './types.js';
|
|
17
|
+
import type { SchedulePersistence } from './cron.js';
|
|
18
18
|
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// Database Types (Kysely schema)
|
|
@@ -143,18 +143,18 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
143
143
|
private config: PostgresSchedulePersistenceConfig;
|
|
144
144
|
|
|
145
145
|
constructor(config: PostgresSchedulePersistenceConfig) {
|
|
146
|
-
this.schema = config.schema ??
|
|
146
|
+
this.schema = config.schema ?? 'public';
|
|
147
147
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.schema)) {
|
|
148
148
|
throw new Error(
|
|
149
149
|
`Invalid schema name "${this.schema}". Schema must start with a letter or underscore, ` +
|
|
150
|
-
|
|
150
|
+
`contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
|
-
this.tableName = config.tableName ??
|
|
153
|
+
this.tableName = config.tableName ?? 'workflow_schedules';
|
|
154
154
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(this.tableName)) {
|
|
155
155
|
throw new Error(
|
|
156
156
|
`Invalid table name "${this.tableName}". Table name must start with a letter or underscore, ` +
|
|
157
|
-
|
|
157
|
+
`contain only alphanumeric characters and underscores, and be at most 63 characters.`
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
this.autoMigrate = config.autoMigrate !== false;
|
|
@@ -172,7 +172,7 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
172
172
|
private ensureInitialized(): void {
|
|
173
173
|
if (!this.initialized) {
|
|
174
174
|
throw new Error(
|
|
175
|
-
|
|
175
|
+
'PostgresSchedulePersistence is not initialized. Call initialize() before using the adapter.'
|
|
176
176
|
);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -196,16 +196,14 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
196
196
|
this.pool = this.config.pool;
|
|
197
197
|
this.ownsPool = false;
|
|
198
198
|
} else if (this.config.connectionString) {
|
|
199
|
-
this.pool = new pgModule.Pool({
|
|
200
|
-
connectionString: this.config.connectionString,
|
|
201
|
-
});
|
|
199
|
+
this.pool = new pgModule.Pool({ connectionString: this.config.connectionString });
|
|
202
200
|
this.ownsPool = true;
|
|
203
201
|
} else if (this.config.poolConfig) {
|
|
204
202
|
this.pool = new pgModule.Pool(this.config.poolConfig);
|
|
205
203
|
this.ownsPool = true;
|
|
206
204
|
} else {
|
|
207
205
|
throw new Error(
|
|
208
|
-
|
|
206
|
+
'PostgresSchedulePersistenceConfig must include either pool, connectionString, or poolConfig'
|
|
209
207
|
);
|
|
210
208
|
}
|
|
211
209
|
|
|
@@ -238,16 +236,13 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
238
236
|
// ============================================================================
|
|
239
237
|
|
|
240
238
|
private async createTables(): Promise<void> {
|
|
241
|
-
const fullTableName =
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
: `${this.schema}.${this.tableName}`;
|
|
239
|
+
const fullTableName = this.schema === 'public'
|
|
240
|
+
? this.tableName
|
|
241
|
+
: `${this.schema}.${this.tableName}`;
|
|
245
242
|
|
|
246
243
|
// Create schema if not public
|
|
247
|
-
if (this.schema !==
|
|
248
|
-
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(
|
|
249
|
-
this.db,
|
|
250
|
-
);
|
|
244
|
+
if (this.schema !== 'public') {
|
|
245
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.ref(this.schema)}`.execute(this.db);
|
|
251
246
|
}
|
|
252
247
|
|
|
253
248
|
// Create schedules table.
|
|
@@ -316,12 +311,12 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
316
311
|
|
|
317
312
|
async loadSchedules(): Promise<WorkflowSchedule[]> {
|
|
318
313
|
this.ensureInitialized();
|
|
319
|
-
const rows =
|
|
314
|
+
const rows = await this.qb
|
|
320
315
|
.selectFrom(this.tableName as any)
|
|
321
316
|
.selectAll()
|
|
322
|
-
.execute()
|
|
317
|
+
.execute() as WorkflowSchedulesTable[];
|
|
323
318
|
|
|
324
|
-
return rows.map(
|
|
319
|
+
return rows.map(row => this.rowToSchedule(row));
|
|
325
320
|
}
|
|
326
321
|
|
|
327
322
|
async saveSchedule(schedule: WorkflowSchedule): Promise<void> {
|
|
@@ -335,13 +330,9 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
335
330
|
cron_expression: schedule.cronExpression ?? null,
|
|
336
331
|
timezone: schedule.timezone ?? null,
|
|
337
332
|
trigger_on_workflow_kind: schedule.triggerOnWorkflowKind ?? null,
|
|
338
|
-
trigger_on_status: schedule.triggerOnStatus
|
|
339
|
-
? JSON.stringify(schedule.triggerOnStatus)
|
|
340
|
-
: null,
|
|
333
|
+
trigger_on_status: schedule.triggerOnStatus ? JSON.stringify(schedule.triggerOnStatus) : null,
|
|
341
334
|
input_json: schedule.input ? JSON.stringify(schedule.input) : null,
|
|
342
|
-
metadata_json: schedule.metadata
|
|
343
|
-
? JSON.stringify(schedule.metadata)
|
|
344
|
-
: null,
|
|
335
|
+
metadata_json: schedule.metadata ? JSON.stringify(schedule.metadata) : null,
|
|
345
336
|
enabled: schedule.enabled,
|
|
346
337
|
last_run_at: schedule.lastRunAt ?? null,
|
|
347
338
|
last_run_id: schedule.lastRunId ?? null,
|
|
@@ -352,20 +343,17 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
352
343
|
.execute();
|
|
353
344
|
}
|
|
354
345
|
|
|
355
|
-
async updateSchedule(
|
|
356
|
-
scheduleId: string,
|
|
357
|
-
updates: Partial<WorkflowSchedule>,
|
|
358
|
-
): Promise<void> {
|
|
346
|
+
async updateSchedule(scheduleId: string, updates: Partial<WorkflowSchedule>): Promise<void> {
|
|
359
347
|
this.ensureInitialized();
|
|
360
348
|
// Partial update pattern: fetch the existing row, merge with incoming updates,
|
|
361
349
|
// then build a column-level update object. Each field is only included if it was
|
|
362
350
|
// explicitly provided in `updates` (or affected by the merge), so unchanged
|
|
363
351
|
// columns are left untouched in the database.
|
|
364
|
-
const existing =
|
|
352
|
+
const existing = await this.qb
|
|
365
353
|
.selectFrom(this.tableName as any)
|
|
366
354
|
.selectAll()
|
|
367
|
-
.where(
|
|
368
|
-
.executeTakeFirst()
|
|
355
|
+
.where('id', '=', scheduleId)
|
|
356
|
+
.executeTakeFirst() as WorkflowSchedulesTable | undefined;
|
|
369
357
|
|
|
370
358
|
if (!existing) {
|
|
371
359
|
throw new Error(`Schedule not found: ${scheduleId}`);
|
|
@@ -383,30 +371,18 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
383
371
|
dbKey: keyof WorkflowSchedulesTable;
|
|
384
372
|
serialize?: (value: any) => any;
|
|
385
373
|
}> = [
|
|
386
|
-
{ domainKey:
|
|
387
|
-
{ domainKey:
|
|
388
|
-
{ domainKey:
|
|
389
|
-
{ domainKey:
|
|
390
|
-
{ domainKey:
|
|
391
|
-
{
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
dbKey: "input_json",
|
|
399
|
-
serialize: (v) => (v ? JSON.stringify(v) : null),
|
|
400
|
-
},
|
|
401
|
-
{
|
|
402
|
-
domainKey: "metadata",
|
|
403
|
-
dbKey: "metadata_json",
|
|
404
|
-
serialize: (v) => (v ? JSON.stringify(v) : null),
|
|
405
|
-
},
|
|
406
|
-
{ domainKey: "enabled", dbKey: "enabled" },
|
|
407
|
-
{ domainKey: "lastRunAt", dbKey: "last_run_at" },
|
|
408
|
-
{ domainKey: "lastRunId", dbKey: "last_run_id" },
|
|
409
|
-
{ domainKey: "nextRunAt", dbKey: "next_run_at" },
|
|
374
|
+
{ domainKey: 'workflowKind', dbKey: 'workflow_kind' },
|
|
375
|
+
{ domainKey: 'triggerType', dbKey: 'trigger_type' },
|
|
376
|
+
{ domainKey: 'cronExpression', dbKey: 'cron_expression' },
|
|
377
|
+
{ domainKey: 'timezone', dbKey: 'timezone' },
|
|
378
|
+
{ domainKey: 'triggerOnWorkflowKind', dbKey: 'trigger_on_workflow_kind' },
|
|
379
|
+
{ domainKey: 'triggerOnStatus', dbKey: 'trigger_on_status', serialize: v => v ? JSON.stringify(v) : null },
|
|
380
|
+
{ domainKey: 'input', dbKey: 'input_json', serialize: v => v ? JSON.stringify(v) : null },
|
|
381
|
+
{ domainKey: 'metadata', dbKey: 'metadata_json', serialize: v => v ? JSON.stringify(v) : null },
|
|
382
|
+
{ domainKey: 'enabled', dbKey: 'enabled' },
|
|
383
|
+
{ domainKey: 'lastRunAt', dbKey: 'last_run_at' },
|
|
384
|
+
{ domainKey: 'lastRunId', dbKey: 'last_run_id' },
|
|
385
|
+
{ domainKey: 'nextRunAt', dbKey: 'next_run_at' },
|
|
410
386
|
];
|
|
411
387
|
|
|
412
388
|
const updateData: Partial<WorkflowSchedulesTable> = {
|
|
@@ -425,7 +401,7 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
425
401
|
await this.qb
|
|
426
402
|
.updateTable(this.tableName as any)
|
|
427
403
|
.set(updateData)
|
|
428
|
-
.where(
|
|
404
|
+
.where('id', '=', scheduleId)
|
|
429
405
|
.execute();
|
|
430
406
|
}
|
|
431
407
|
|
|
@@ -433,7 +409,7 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
433
409
|
this.ensureInitialized();
|
|
434
410
|
await this.qb
|
|
435
411
|
.deleteFrom(this.tableName as any)
|
|
436
|
-
.where(
|
|
412
|
+
.where('id', '=', scheduleId)
|
|
437
413
|
.execute();
|
|
438
414
|
}
|
|
439
415
|
|
|
@@ -446,11 +422,11 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
446
422
|
*/
|
|
447
423
|
async getSchedule(scheduleId: string): Promise<WorkflowSchedule | null> {
|
|
448
424
|
this.ensureInitialized();
|
|
449
|
-
const row =
|
|
425
|
+
const row = await this.qb
|
|
450
426
|
.selectFrom(this.tableName as any)
|
|
451
427
|
.selectAll()
|
|
452
|
-
.where(
|
|
453
|
-
.executeTakeFirst()
|
|
428
|
+
.where('id', '=', scheduleId)
|
|
429
|
+
.executeTakeFirst() as WorkflowSchedulesTable | undefined;
|
|
454
430
|
|
|
455
431
|
return row ? this.rowToSchedule(row) : null;
|
|
456
432
|
}
|
|
@@ -462,49 +438,45 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
462
438
|
this.ensureInitialized();
|
|
463
439
|
const now = new Date();
|
|
464
440
|
|
|
465
|
-
const rows =
|
|
441
|
+
const rows = await this.qb
|
|
466
442
|
.selectFrom(this.tableName as any)
|
|
467
443
|
.selectAll()
|
|
468
|
-
.where(
|
|
469
|
-
.where(
|
|
470
|
-
.where(
|
|
471
|
-
.execute()
|
|
444
|
+
.where('enabled', '=', true)
|
|
445
|
+
.where('trigger_type', '=', 'cron')
|
|
446
|
+
.where('next_run_at', '<=', now)
|
|
447
|
+
.execute() as WorkflowSchedulesTable[];
|
|
472
448
|
|
|
473
|
-
return rows.map(
|
|
449
|
+
return rows.map(row => this.rowToSchedule(row));
|
|
474
450
|
}
|
|
475
451
|
|
|
476
452
|
/**
|
|
477
453
|
* Get schedules by workflow kind.
|
|
478
454
|
*/
|
|
479
|
-
async getSchedulesByWorkflowKind(
|
|
480
|
-
workflowKind: string,
|
|
481
|
-
): Promise<WorkflowSchedule[]> {
|
|
455
|
+
async getSchedulesByWorkflowKind(workflowKind: string): Promise<WorkflowSchedule[]> {
|
|
482
456
|
this.ensureInitialized();
|
|
483
|
-
const rows =
|
|
457
|
+
const rows = await this.qb
|
|
484
458
|
.selectFrom(this.tableName as any)
|
|
485
459
|
.selectAll()
|
|
486
|
-
.where(
|
|
487
|
-
.execute()
|
|
460
|
+
.where('workflow_kind', '=', workflowKind)
|
|
461
|
+
.execute() as WorkflowSchedulesTable[];
|
|
488
462
|
|
|
489
|
-
return rows.map(
|
|
463
|
+
return rows.map(row => this.rowToSchedule(row));
|
|
490
464
|
}
|
|
491
465
|
|
|
492
466
|
/**
|
|
493
467
|
* Get workflow completion triggers for a specific workflow kind.
|
|
494
468
|
*/
|
|
495
|
-
async getCompletionTriggers(
|
|
496
|
-
triggerOnWorkflowKind: string,
|
|
497
|
-
): Promise<WorkflowSchedule[]> {
|
|
469
|
+
async getCompletionTriggers(triggerOnWorkflowKind: string): Promise<WorkflowSchedule[]> {
|
|
498
470
|
this.ensureInitialized();
|
|
499
|
-
const rows =
|
|
471
|
+
const rows = await this.qb
|
|
500
472
|
.selectFrom(this.tableName as any)
|
|
501
473
|
.selectAll()
|
|
502
|
-
.where(
|
|
503
|
-
.where(
|
|
504
|
-
.where(
|
|
505
|
-
.execute()
|
|
474
|
+
.where('enabled', '=', true)
|
|
475
|
+
.where('trigger_type', '=', 'workflow_completed')
|
|
476
|
+
.where('trigger_on_workflow_kind', '=', triggerOnWorkflowKind)
|
|
477
|
+
.execute() as WorkflowSchedulesTable[];
|
|
506
478
|
|
|
507
|
-
return rows.map(
|
|
479
|
+
return rows.map(row => this.rowToSchedule(row));
|
|
508
480
|
}
|
|
509
481
|
|
|
510
482
|
// ============================================================================
|
|
@@ -515,24 +487,24 @@ export class PostgresSchedulePersistence implements SchedulePersistence {
|
|
|
515
487
|
return {
|
|
516
488
|
id: row.id,
|
|
517
489
|
workflowKind: row.workflow_kind,
|
|
518
|
-
triggerType: row.trigger_type as WorkflowSchedule[
|
|
490
|
+
triggerType: row.trigger_type as WorkflowSchedule['triggerType'],
|
|
519
491
|
cronExpression: row.cron_expression ?? undefined,
|
|
520
492
|
timezone: row.timezone ?? undefined,
|
|
521
493
|
triggerOnWorkflowKind: row.trigger_on_workflow_kind ?? undefined,
|
|
522
494
|
triggerOnStatus: row.trigger_on_status
|
|
523
|
-
? typeof row.trigger_on_status ===
|
|
524
|
-
|
|
525
|
-
|
|
495
|
+
? (typeof row.trigger_on_status === 'string'
|
|
496
|
+
? JSON.parse(row.trigger_on_status)
|
|
497
|
+
: row.trigger_on_status)
|
|
526
498
|
: undefined,
|
|
527
499
|
input: row.input_json
|
|
528
|
-
? typeof row.input_json ===
|
|
529
|
-
|
|
530
|
-
|
|
500
|
+
? (typeof row.input_json === 'string'
|
|
501
|
+
? JSON.parse(row.input_json)
|
|
502
|
+
: row.input_json)
|
|
531
503
|
: undefined,
|
|
532
504
|
metadata: row.metadata_json
|
|
533
|
-
? typeof row.metadata_json ===
|
|
534
|
-
|
|
535
|
-
|
|
505
|
+
? (typeof row.metadata_json === 'string'
|
|
506
|
+
? JSON.parse(row.metadata_json)
|
|
507
|
+
: row.metadata_json)
|
|
536
508
|
: undefined,
|
|
537
509
|
enabled: row.enabled,
|
|
538
510
|
lastRunAt: row.last_run_at ? new Date(row.last_run_at) : undefined,
|