@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,906 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
-
import { PostgresSchedulePersistence } from "./postgres-persistence";
|
|
3
|
-
import type { WorkflowSchedule } from "./types";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Mock helpers – We mock `loadPostgresDeps` so we never need a real PG server.
|
|
7
|
-
// Instead, a fake Kysely instance backed by an in-memory row store is provided.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
/** Rows stored in our fake database keyed by schedule id. */
|
|
11
|
-
let fakeRows: Map<string, Record<string, any>>;
|
|
12
|
-
|
|
13
|
-
/** Tracks SQL raw calls made via the `sql` template tag (for createTables). */
|
|
14
|
-
let sqlExecuteCalls: number;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Chainable query-builder mock that operates on `fakeRows`.
|
|
18
|
-
* Each method returns `this` to allow chaining, except terminal methods
|
|
19
|
-
* (execute / executeTakeFirst) which resolve with data from the store.
|
|
20
|
-
*/
|
|
21
|
-
function createQueryBuilder() {
|
|
22
|
-
let _table: string;
|
|
23
|
-
let _wheres: Array<{ col: string; op: string; val: any }> = [];
|
|
24
|
-
let _insertValues: Record<string, any> | null = null;
|
|
25
|
-
let _updateValues: Record<string, any> | null = null;
|
|
26
|
-
|
|
27
|
-
const matchesWhere = (row: Record<string, any>) =>
|
|
28
|
-
_wheres.every(({ col, op, val }) => {
|
|
29
|
-
switch (op) {
|
|
30
|
-
case "=":
|
|
31
|
-
return row[col] === val;
|
|
32
|
-
case "<=":
|
|
33
|
-
return row[col] <= val;
|
|
34
|
-
default:
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const qb: any = {
|
|
40
|
-
withSchema() {
|
|
41
|
-
return qb;
|
|
42
|
-
},
|
|
43
|
-
selectFrom(table: string) {
|
|
44
|
-
_table = table;
|
|
45
|
-
_wheres = [];
|
|
46
|
-
return qb;
|
|
47
|
-
},
|
|
48
|
-
selectAll() {
|
|
49
|
-
return qb;
|
|
50
|
-
},
|
|
51
|
-
where(col: string, op: string, val: any) {
|
|
52
|
-
_wheres.push({ col, op, val });
|
|
53
|
-
return qb;
|
|
54
|
-
},
|
|
55
|
-
insertInto(table: string) {
|
|
56
|
-
_table = table;
|
|
57
|
-
return qb;
|
|
58
|
-
},
|
|
59
|
-
values(vals: Record<string, any>) {
|
|
60
|
-
_insertValues = vals;
|
|
61
|
-
return qb;
|
|
62
|
-
},
|
|
63
|
-
updateTable(table: string) {
|
|
64
|
-
_table = table;
|
|
65
|
-
_wheres = [];
|
|
66
|
-
return qb;
|
|
67
|
-
},
|
|
68
|
-
set(vals: Record<string, any>) {
|
|
69
|
-
_updateValues = vals;
|
|
70
|
-
return qb;
|
|
71
|
-
},
|
|
72
|
-
deleteFrom(table: string) {
|
|
73
|
-
_table = table;
|
|
74
|
-
_wheres = [];
|
|
75
|
-
return qb;
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
async execute() {
|
|
79
|
-
// INSERT
|
|
80
|
-
if (_insertValues) {
|
|
81
|
-
fakeRows.set(_insertValues.id, { ..._insertValues });
|
|
82
|
-
_insertValues = null;
|
|
83
|
-
return [];
|
|
84
|
-
}
|
|
85
|
-
// UPDATE
|
|
86
|
-
if (_updateValues) {
|
|
87
|
-
for (const [, row] of fakeRows) {
|
|
88
|
-
if (matchesWhere(row)) {
|
|
89
|
-
Object.assign(row, _updateValues);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
_updateValues = null;
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
// DELETE
|
|
96
|
-
if (
|
|
97
|
-
_wheres.length >= 0 &&
|
|
98
|
-
_insertValues === null &&
|
|
99
|
-
_updateValues === null
|
|
100
|
-
) {
|
|
101
|
-
// If it's a deleteFrom call
|
|
102
|
-
const toDelete: string[] = [];
|
|
103
|
-
for (const [id, row] of fakeRows) {
|
|
104
|
-
if (matchesWhere(row)) toDelete.push(id);
|
|
105
|
-
}
|
|
106
|
-
for (const id of toDelete) fakeRows.delete(id);
|
|
107
|
-
// For selectFrom case, return matching rows
|
|
108
|
-
const results: Record<string, any>[] = [];
|
|
109
|
-
for (const [, row] of fakeRows) {
|
|
110
|
-
if (matchesWhere(row)) results.push({ ...row });
|
|
111
|
-
}
|
|
112
|
-
return results;
|
|
113
|
-
}
|
|
114
|
-
return [];
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
async executeTakeFirst() {
|
|
118
|
-
for (const [, row] of fakeRows) {
|
|
119
|
-
if (matchesWhere(row)) return { ...row };
|
|
120
|
-
}
|
|
121
|
-
return undefined;
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return qb;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// We need to distinguish between select/delete/update calls properly.
|
|
129
|
-
// Let's refactor the mock with an operation tracker.
|
|
130
|
-
function createTrackedQueryBuilder() {
|
|
131
|
-
let _op: "select" | "insert" | "update" | "delete" = "select";
|
|
132
|
-
let _wheres: Array<{ col: string; op: string; val: any }> = [];
|
|
133
|
-
let _insertValues: Record<string, any> | null = null;
|
|
134
|
-
let _updateValues: Record<string, any> | null = null;
|
|
135
|
-
|
|
136
|
-
const matchesWhere = (row: Record<string, any>) =>
|
|
137
|
-
_wheres.every(({ col, op, val }) => {
|
|
138
|
-
switch (op) {
|
|
139
|
-
case "=":
|
|
140
|
-
return row[col] === val;
|
|
141
|
-
case "<=":
|
|
142
|
-
return row[col] <= val;
|
|
143
|
-
default:
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const qb: any = {
|
|
149
|
-
withSchema() {
|
|
150
|
-
return qb;
|
|
151
|
-
},
|
|
152
|
-
selectFrom() {
|
|
153
|
-
_op = "select";
|
|
154
|
-
_wheres = [];
|
|
155
|
-
return qb;
|
|
156
|
-
},
|
|
157
|
-
selectAll() {
|
|
158
|
-
return qb;
|
|
159
|
-
},
|
|
160
|
-
where(col: string, op: string, val: any) {
|
|
161
|
-
_wheres.push({ col, op, val });
|
|
162
|
-
return qb;
|
|
163
|
-
},
|
|
164
|
-
insertInto() {
|
|
165
|
-
_op = "insert";
|
|
166
|
-
return qb;
|
|
167
|
-
},
|
|
168
|
-
values(vals: Record<string, any>) {
|
|
169
|
-
_insertValues = vals;
|
|
170
|
-
return qb;
|
|
171
|
-
},
|
|
172
|
-
updateTable() {
|
|
173
|
-
_op = "update";
|
|
174
|
-
_wheres = [];
|
|
175
|
-
return qb;
|
|
176
|
-
},
|
|
177
|
-
set(vals: Record<string, any>) {
|
|
178
|
-
_updateValues = vals;
|
|
179
|
-
return qb;
|
|
180
|
-
},
|
|
181
|
-
deleteFrom() {
|
|
182
|
-
_op = "delete";
|
|
183
|
-
_wheres = [];
|
|
184
|
-
return qb;
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
async execute() {
|
|
188
|
-
if (_op === "insert" && _insertValues) {
|
|
189
|
-
fakeRows.set(_insertValues.id, { ..._insertValues });
|
|
190
|
-
_insertValues = null;
|
|
191
|
-
return [];
|
|
192
|
-
}
|
|
193
|
-
if (_op === "update" && _updateValues) {
|
|
194
|
-
for (const [, row] of fakeRows) {
|
|
195
|
-
if (matchesWhere(row)) Object.assign(row, _updateValues);
|
|
196
|
-
}
|
|
197
|
-
_updateValues = null;
|
|
198
|
-
return [];
|
|
199
|
-
}
|
|
200
|
-
if (_op === "delete") {
|
|
201
|
-
const toDelete: string[] = [];
|
|
202
|
-
for (const [id, row] of fakeRows) {
|
|
203
|
-
if (matchesWhere(row)) toDelete.push(id);
|
|
204
|
-
}
|
|
205
|
-
for (const id of toDelete) fakeRows.delete(id);
|
|
206
|
-
return [];
|
|
207
|
-
}
|
|
208
|
-
// select
|
|
209
|
-
const results: Record<string, any>[] = [];
|
|
210
|
-
for (const [, row] of fakeRows) {
|
|
211
|
-
if (matchesWhere(row)) results.push({ ...row });
|
|
212
|
-
}
|
|
213
|
-
return results;
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async executeTakeFirst() {
|
|
217
|
-
for (const [, row] of fakeRows) {
|
|
218
|
-
if (matchesWhere(row)) return { ...row };
|
|
219
|
-
}
|
|
220
|
-
return undefined;
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
return qb;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Mock loadPostgresDeps so the class never touches real pg/kysely
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
let mockDb: ReturnType<typeof createTrackedQueryBuilder>;
|
|
232
|
-
|
|
233
|
-
vi.mock("../utils/postgres-deps.js", () => ({
|
|
234
|
-
loadPostgresDeps: async () => {
|
|
235
|
-
sqlExecuteCalls = 0;
|
|
236
|
-
return {
|
|
237
|
-
Kysely: class {
|
|
238
|
-
withSchema() {
|
|
239
|
-
return mockDb;
|
|
240
|
-
}
|
|
241
|
-
destroy() {
|
|
242
|
-
return Promise.resolve();
|
|
243
|
-
}
|
|
244
|
-
},
|
|
245
|
-
PostgresDialect: class {
|
|
246
|
-
constructor() {}
|
|
247
|
-
},
|
|
248
|
-
sql: new Proxy(() => {}, {
|
|
249
|
-
apply() {
|
|
250
|
-
return {
|
|
251
|
-
execute: async () => {
|
|
252
|
-
sqlExecuteCalls++;
|
|
253
|
-
},
|
|
254
|
-
};
|
|
255
|
-
},
|
|
256
|
-
get(_target: any, prop: string) {
|
|
257
|
-
if (prop === "ref" || prop === "table") return (v: any) => v;
|
|
258
|
-
return undefined;
|
|
259
|
-
},
|
|
260
|
-
}),
|
|
261
|
-
pgModule: {
|
|
262
|
-
Pool: class {
|
|
263
|
-
end() {
|
|
264
|
-
return Promise.resolve();
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
},
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
// Tests
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
|
|
276
|
-
describe("PostgresSchedulePersistence", () => {
|
|
277
|
-
let persistence: PostgresSchedulePersistence;
|
|
278
|
-
|
|
279
|
-
beforeEach(async () => {
|
|
280
|
-
fakeRows = new Map();
|
|
281
|
-
sqlExecuteCalls = 0;
|
|
282
|
-
mockDb = createTrackedQueryBuilder();
|
|
283
|
-
persistence = new PostgresSchedulePersistence({
|
|
284
|
-
connectionString: "postgresql://test:test@localhost:5432/test",
|
|
285
|
-
});
|
|
286
|
-
await persistence.initialize();
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
// ========================================================================
|
|
290
|
-
// Constructor / Schema Validation
|
|
291
|
-
// ========================================================================
|
|
292
|
-
|
|
293
|
-
describe("constructor validation", () => {
|
|
294
|
-
it("should accept valid schema names", () => {
|
|
295
|
-
expect(
|
|
296
|
-
() =>
|
|
297
|
-
new PostgresSchedulePersistence({
|
|
298
|
-
connectionString: "pg://x",
|
|
299
|
-
schema: "my_schema",
|
|
300
|
-
}),
|
|
301
|
-
).not.toThrow();
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("should reject invalid schema names", () => {
|
|
305
|
-
expect(
|
|
306
|
-
() =>
|
|
307
|
-
new PostgresSchedulePersistence({
|
|
308
|
-
connectionString: "pg://x",
|
|
309
|
-
schema: "0bad",
|
|
310
|
-
}),
|
|
311
|
-
).toThrow("Invalid schema name");
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("should reject schema names starting with a digit", () => {
|
|
315
|
-
expect(
|
|
316
|
-
() =>
|
|
317
|
-
new PostgresSchedulePersistence({
|
|
318
|
-
connectionString: "pg://x",
|
|
319
|
-
schema: "1abc",
|
|
320
|
-
}),
|
|
321
|
-
).toThrow("Invalid schema name");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("should reject schema names with special characters", () => {
|
|
325
|
-
expect(
|
|
326
|
-
() =>
|
|
327
|
-
new PostgresSchedulePersistence({
|
|
328
|
-
connectionString: "pg://x",
|
|
329
|
-
schema: "my-schema",
|
|
330
|
-
}),
|
|
331
|
-
).toThrow("Invalid schema name");
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("should reject schema names longer than 63 characters", () => {
|
|
335
|
-
const longName = "a".repeat(64);
|
|
336
|
-
expect(
|
|
337
|
-
() =>
|
|
338
|
-
new PostgresSchedulePersistence({
|
|
339
|
-
connectionString: "pg://x",
|
|
340
|
-
schema: longName,
|
|
341
|
-
}),
|
|
342
|
-
).toThrow("Invalid schema name");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("should accept schema name at max length (63 chars)", () => {
|
|
346
|
-
const maxName = "a".repeat(63);
|
|
347
|
-
expect(
|
|
348
|
-
() =>
|
|
349
|
-
new PostgresSchedulePersistence({
|
|
350
|
-
connectionString: "pg://x",
|
|
351
|
-
schema: maxName,
|
|
352
|
-
}),
|
|
353
|
-
).not.toThrow();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("should accept schema starting with underscore", () => {
|
|
357
|
-
expect(
|
|
358
|
-
() =>
|
|
359
|
-
new PostgresSchedulePersistence({
|
|
360
|
-
connectionString: "pg://x",
|
|
361
|
-
schema: "_private",
|
|
362
|
-
}),
|
|
363
|
-
).not.toThrow();
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("should default schema to public", async () => {
|
|
367
|
-
// No error thrown with default schema
|
|
368
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
369
|
-
expect(p).toBeDefined();
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// ========================================================================
|
|
374
|
-
// initialize()
|
|
375
|
-
// ========================================================================
|
|
376
|
-
|
|
377
|
-
describe("initialize", () => {
|
|
378
|
-
it("should be idempotent (multiple calls are safe)", async () => {
|
|
379
|
-
// Already initialized in beforeEach, calling again should be a no-op
|
|
380
|
-
await persistence.initialize();
|
|
381
|
-
expect(true).toBe(true); // no error
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("should throw when no connection config is provided", async () => {
|
|
385
|
-
const p = new PostgresSchedulePersistence({});
|
|
386
|
-
await expect(p.initialize()).rejects.toThrow(
|
|
387
|
-
"must include either pool, connectionString, or poolConfig",
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("should accept an existing pool", async () => {
|
|
392
|
-
const fakePool = { end: vi.fn() } as any;
|
|
393
|
-
const p = new PostgresSchedulePersistence({ pool: fakePool });
|
|
394
|
-
await p.initialize();
|
|
395
|
-
// Should work without errors
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("should accept poolConfig", async () => {
|
|
399
|
-
const p = new PostgresSchedulePersistence({
|
|
400
|
-
poolConfig: { host: "localhost", port: 5432, database: "test" },
|
|
401
|
-
});
|
|
402
|
-
await p.initialize();
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("should create tables when autoMigrate is true (default)", async () => {
|
|
406
|
-
// sqlExecuteCalls is incremented by our mock sql template tag
|
|
407
|
-
// createTables runs multiple SQL statements
|
|
408
|
-
expect(sqlExecuteCalls).toBeGreaterThan(0);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("should skip table creation when autoMigrate is false", async () => {
|
|
412
|
-
sqlExecuteCalls = 0;
|
|
413
|
-
const p = new PostgresSchedulePersistence({
|
|
414
|
-
connectionString: "pg://x",
|
|
415
|
-
autoMigrate: false,
|
|
416
|
-
});
|
|
417
|
-
await p.initialize();
|
|
418
|
-
expect(sqlExecuteCalls).toBe(0);
|
|
419
|
-
});
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
// ========================================================================
|
|
423
|
-
// ensureInitialized guard
|
|
424
|
-
// ========================================================================
|
|
425
|
-
|
|
426
|
-
describe("ensureInitialized guard", () => {
|
|
427
|
-
it("should throw on loadSchedules before initialize", async () => {
|
|
428
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
429
|
-
await expect(p.loadSchedules()).rejects.toThrow("not initialized");
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it("should throw on saveSchedule before initialize", async () => {
|
|
433
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
434
|
-
await expect(
|
|
435
|
-
p.saveSchedule({
|
|
436
|
-
id: "x",
|
|
437
|
-
workflowKind: "w",
|
|
438
|
-
triggerType: "cron",
|
|
439
|
-
enabled: true,
|
|
440
|
-
}),
|
|
441
|
-
).rejects.toThrow("not initialized");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
it("should throw on updateSchedule before initialize", async () => {
|
|
445
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
446
|
-
await expect(p.updateSchedule("x", { enabled: false })).rejects.toThrow(
|
|
447
|
-
"not initialized",
|
|
448
|
-
);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("should throw on deleteSchedule before initialize", async () => {
|
|
452
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
453
|
-
await expect(p.deleteSchedule("x")).rejects.toThrow("not initialized");
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it("should throw on getSchedule before initialize", async () => {
|
|
457
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
458
|
-
await expect(p.getSchedule("x")).rejects.toThrow("not initialized");
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("should throw on getDueSchedules before initialize", async () => {
|
|
462
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
463
|
-
await expect(p.getDueSchedules()).rejects.toThrow("not initialized");
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it("should throw on getSchedulesByWorkflowKind before initialize", async () => {
|
|
467
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
468
|
-
await expect(p.getSchedulesByWorkflowKind("w")).rejects.toThrow(
|
|
469
|
-
"not initialized",
|
|
470
|
-
);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("should throw on getCompletionTriggers before initialize", async () => {
|
|
474
|
-
const p = new PostgresSchedulePersistence({ connectionString: "pg://x" });
|
|
475
|
-
await expect(p.getCompletionTriggers("w")).rejects.toThrow(
|
|
476
|
-
"not initialized",
|
|
477
|
-
);
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// ========================================================================
|
|
482
|
-
// saveSchedule
|
|
483
|
-
// ========================================================================
|
|
484
|
-
|
|
485
|
-
describe("saveSchedule", () => {
|
|
486
|
-
it("should save a cron schedule", async () => {
|
|
487
|
-
const schedule: WorkflowSchedule = {
|
|
488
|
-
id: "test-1",
|
|
489
|
-
workflowKind: "test.workflow",
|
|
490
|
-
triggerType: "cron",
|
|
491
|
-
cronExpression: "0 * * * *",
|
|
492
|
-
timezone: "UTC",
|
|
493
|
-
enabled: true,
|
|
494
|
-
input: { foo: "bar" },
|
|
495
|
-
metadata: { user: "test" },
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
await persistence.saveSchedule(schedule);
|
|
499
|
-
|
|
500
|
-
const loaded = await persistence.loadSchedules();
|
|
501
|
-
expect(loaded).toHaveLength(1);
|
|
502
|
-
expect(loaded[0].id).toBe("test-1");
|
|
503
|
-
expect(loaded[0].cronExpression).toBe("0 * * * *");
|
|
504
|
-
expect(loaded[0].input).toEqual({ foo: "bar" });
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it("should save a workflow completion trigger", async () => {
|
|
508
|
-
const schedule: WorkflowSchedule = {
|
|
509
|
-
id: "test-2",
|
|
510
|
-
workflowKind: "notification.send",
|
|
511
|
-
triggerType: "workflow_completed",
|
|
512
|
-
triggerOnWorkflowKind: "order.process",
|
|
513
|
-
triggerOnStatus: ["succeeded", "failed"],
|
|
514
|
-
enabled: true,
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
await persistence.saveSchedule(schedule);
|
|
518
|
-
|
|
519
|
-
const loaded = await persistence.loadSchedules();
|
|
520
|
-
expect(loaded).toHaveLength(1);
|
|
521
|
-
expect(loaded[0].triggerType).toBe("workflow_completed");
|
|
522
|
-
expect(loaded[0].triggerOnWorkflowKind).toBe("order.process");
|
|
523
|
-
expect(loaded[0].triggerOnStatus).toEqual(["succeeded", "failed"]);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it("should save schedule with dates", async () => {
|
|
527
|
-
const lastRunAt = new Date("2024-01-01T12:00:00Z");
|
|
528
|
-
const nextRunAt = new Date("2024-01-01T13:00:00Z");
|
|
529
|
-
|
|
530
|
-
const schedule: WorkflowSchedule = {
|
|
531
|
-
id: "test-3",
|
|
532
|
-
workflowKind: "test.workflow",
|
|
533
|
-
triggerType: "cron",
|
|
534
|
-
cronExpression: "0 * * * *",
|
|
535
|
-
enabled: true,
|
|
536
|
-
lastRunAt,
|
|
537
|
-
lastRunId: "run-123",
|
|
538
|
-
nextRunAt,
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
await persistence.saveSchedule(schedule);
|
|
542
|
-
|
|
543
|
-
const loaded = await persistence.loadSchedules();
|
|
544
|
-
expect(loaded[0].lastRunAt).toEqual(lastRunAt);
|
|
545
|
-
expect(loaded[0].lastRunId).toBe("run-123");
|
|
546
|
-
expect(loaded[0].nextRunAt).toEqual(nextRunAt);
|
|
547
|
-
});
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// ========================================================================
|
|
551
|
-
// loadSchedules
|
|
552
|
-
// ========================================================================
|
|
553
|
-
|
|
554
|
-
describe("loadSchedules", () => {
|
|
555
|
-
it("should return empty array when no schedules", async () => {
|
|
556
|
-
const schedules = await persistence.loadSchedules();
|
|
557
|
-
expect(schedules).toHaveLength(0);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
it("should load multiple schedules", async () => {
|
|
561
|
-
await persistence.saveSchedule({
|
|
562
|
-
id: "schedule-1",
|
|
563
|
-
workflowKind: "workflow.a",
|
|
564
|
-
triggerType: "cron",
|
|
565
|
-
cronExpression: "0 * * * *",
|
|
566
|
-
enabled: true,
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
await persistence.saveSchedule({
|
|
570
|
-
id: "schedule-2",
|
|
571
|
-
workflowKind: "workflow.b",
|
|
572
|
-
triggerType: "manual",
|
|
573
|
-
enabled: false,
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
const schedules = await persistence.loadSchedules();
|
|
577
|
-
expect(schedules).toHaveLength(2);
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// ========================================================================
|
|
582
|
-
// getSchedule
|
|
583
|
-
// ========================================================================
|
|
584
|
-
|
|
585
|
-
describe("getSchedule", () => {
|
|
586
|
-
it("should return schedule by id", async () => {
|
|
587
|
-
await persistence.saveSchedule({
|
|
588
|
-
id: "get-1",
|
|
589
|
-
workflowKind: "test.workflow",
|
|
590
|
-
triggerType: "cron",
|
|
591
|
-
cronExpression: "0 * * * *",
|
|
592
|
-
enabled: true,
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
const schedule = await persistence.getSchedule("get-1");
|
|
596
|
-
expect(schedule).not.toBeNull();
|
|
597
|
-
expect(schedule!.id).toBe("get-1");
|
|
598
|
-
expect(schedule!.workflowKind).toBe("test.workflow");
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it("should return null for non-existent schedule", async () => {
|
|
602
|
-
const schedule = await persistence.getSchedule("nonexistent");
|
|
603
|
-
expect(schedule).toBeNull();
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
// ========================================================================
|
|
608
|
-
// updateSchedule
|
|
609
|
-
// ========================================================================
|
|
610
|
-
|
|
611
|
-
describe("updateSchedule", () => {
|
|
612
|
-
it("should update enabled status", async () => {
|
|
613
|
-
await persistence.saveSchedule({
|
|
614
|
-
id: "update-1",
|
|
615
|
-
workflowKind: "test.workflow",
|
|
616
|
-
triggerType: "cron",
|
|
617
|
-
cronExpression: "0 * * * *",
|
|
618
|
-
enabled: true,
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
await persistence.updateSchedule("update-1", { enabled: false });
|
|
622
|
-
|
|
623
|
-
const loaded = await persistence.loadSchedules();
|
|
624
|
-
expect(loaded[0].enabled).toBe(false);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it("should update cron expression", async () => {
|
|
628
|
-
await persistence.saveSchedule({
|
|
629
|
-
id: "update-2",
|
|
630
|
-
workflowKind: "test.workflow",
|
|
631
|
-
triggerType: "cron",
|
|
632
|
-
cronExpression: "0 * * * *",
|
|
633
|
-
enabled: true,
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
await persistence.updateSchedule("update-2", {
|
|
637
|
-
cronExpression: "30 * * * *",
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
const loaded = await persistence.loadSchedules();
|
|
641
|
-
expect(loaded[0].cronExpression).toBe("30 * * * *");
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it("should update lastRunAt and lastRunId", async () => {
|
|
645
|
-
await persistence.saveSchedule({
|
|
646
|
-
id: "update-3",
|
|
647
|
-
workflowKind: "test.workflow",
|
|
648
|
-
triggerType: "cron",
|
|
649
|
-
cronExpression: "0 * * * *",
|
|
650
|
-
enabled: true,
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
const now = new Date();
|
|
654
|
-
await persistence.updateSchedule("update-3", {
|
|
655
|
-
lastRunAt: now,
|
|
656
|
-
lastRunId: "run-456",
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
const loaded = await persistence.loadSchedules();
|
|
660
|
-
expect(loaded[0].lastRunAt).toEqual(now);
|
|
661
|
-
expect(loaded[0].lastRunId).toBe("run-456");
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it("should throw for non-existent schedule", async () => {
|
|
665
|
-
await expect(
|
|
666
|
-
persistence.updateSchedule("nonexistent", { enabled: false }),
|
|
667
|
-
).rejects.toThrow("Schedule not found");
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// ========================================================================
|
|
672
|
-
// deleteSchedule
|
|
673
|
-
// ========================================================================
|
|
674
|
-
|
|
675
|
-
describe("deleteSchedule", () => {
|
|
676
|
-
it("should delete a schedule", async () => {
|
|
677
|
-
await persistence.saveSchedule({
|
|
678
|
-
id: "delete-1",
|
|
679
|
-
workflowKind: "test.workflow",
|
|
680
|
-
triggerType: "cron",
|
|
681
|
-
cronExpression: "0 * * * *",
|
|
682
|
-
enabled: true,
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
await persistence.deleteSchedule("delete-1");
|
|
686
|
-
|
|
687
|
-
const loaded = await persistence.loadSchedules();
|
|
688
|
-
expect(loaded).toHaveLength(0);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
it("should handle deleting non-existent schedule gracefully", async () => {
|
|
692
|
-
await persistence.deleteSchedule("nonexistent");
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// ========================================================================
|
|
697
|
-
// getDueSchedules
|
|
698
|
-
// ========================================================================
|
|
699
|
-
|
|
700
|
-
describe("getDueSchedules", () => {
|
|
701
|
-
it("should return enabled cron schedules with past next_run_at", async () => {
|
|
702
|
-
const pastDate = new Date("2020-01-01T00:00:00Z");
|
|
703
|
-
await persistence.saveSchedule({
|
|
704
|
-
id: "due-1",
|
|
705
|
-
workflowKind: "test.workflow",
|
|
706
|
-
triggerType: "cron",
|
|
707
|
-
cronExpression: "0 * * * *",
|
|
708
|
-
enabled: true,
|
|
709
|
-
nextRunAt: pastDate,
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// disabled schedule should not appear
|
|
713
|
-
await persistence.saveSchedule({
|
|
714
|
-
id: "due-2",
|
|
715
|
-
workflowKind: "test.workflow",
|
|
716
|
-
triggerType: "cron",
|
|
717
|
-
cronExpression: "0 * * * *",
|
|
718
|
-
enabled: false,
|
|
719
|
-
nextRunAt: pastDate,
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
// non-cron should not appear
|
|
723
|
-
await persistence.saveSchedule({
|
|
724
|
-
id: "due-3",
|
|
725
|
-
workflowKind: "test.workflow",
|
|
726
|
-
triggerType: "manual",
|
|
727
|
-
enabled: true,
|
|
728
|
-
nextRunAt: pastDate,
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
const due = await persistence.getDueSchedules();
|
|
732
|
-
expect(due).toHaveLength(1);
|
|
733
|
-
expect(due[0].id).toBe("due-1");
|
|
734
|
-
});
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// ========================================================================
|
|
738
|
-
// getSchedulesByWorkflowKind
|
|
739
|
-
// ========================================================================
|
|
740
|
-
|
|
741
|
-
describe("getSchedulesByWorkflowKind", () => {
|
|
742
|
-
it("should filter schedules by workflow kind", async () => {
|
|
743
|
-
await persistence.saveSchedule({
|
|
744
|
-
id: "kind-1",
|
|
745
|
-
workflowKind: "target.workflow",
|
|
746
|
-
triggerType: "cron",
|
|
747
|
-
cronExpression: "0 * * * *",
|
|
748
|
-
enabled: true,
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
await persistence.saveSchedule({
|
|
752
|
-
id: "kind-2",
|
|
753
|
-
workflowKind: "other.workflow",
|
|
754
|
-
triggerType: "cron",
|
|
755
|
-
cronExpression: "0 * * * *",
|
|
756
|
-
enabled: true,
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
const schedules =
|
|
760
|
-
await persistence.getSchedulesByWorkflowKind("target.workflow");
|
|
761
|
-
expect(schedules).toHaveLength(1);
|
|
762
|
-
expect(schedules[0].id).toBe("kind-1");
|
|
763
|
-
});
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
// ========================================================================
|
|
767
|
-
// getCompletionTriggers
|
|
768
|
-
// ========================================================================
|
|
769
|
-
|
|
770
|
-
describe("getCompletionTriggers", () => {
|
|
771
|
-
it("should return enabled completion triggers for a workflow kind", async () => {
|
|
772
|
-
await persistence.saveSchedule({
|
|
773
|
-
id: "ct-1",
|
|
774
|
-
workflowKind: "notification.send",
|
|
775
|
-
triggerType: "workflow_completed",
|
|
776
|
-
triggerOnWorkflowKind: "order.process",
|
|
777
|
-
triggerOnStatus: ["succeeded"],
|
|
778
|
-
enabled: true,
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
// disabled trigger should not appear
|
|
782
|
-
await persistence.saveSchedule({
|
|
783
|
-
id: "ct-2",
|
|
784
|
-
workflowKind: "notification.send",
|
|
785
|
-
triggerType: "workflow_completed",
|
|
786
|
-
triggerOnWorkflowKind: "order.process",
|
|
787
|
-
enabled: false,
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
// different trigger kind should not appear
|
|
791
|
-
await persistence.saveSchedule({
|
|
792
|
-
id: "ct-3",
|
|
793
|
-
workflowKind: "notification.send",
|
|
794
|
-
triggerType: "workflow_completed",
|
|
795
|
-
triggerOnWorkflowKind: "different.workflow",
|
|
796
|
-
enabled: true,
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
const triggers = await persistence.getCompletionTriggers("order.process");
|
|
800
|
-
expect(triggers).toHaveLength(1);
|
|
801
|
-
expect(triggers[0].id).toBe("ct-1");
|
|
802
|
-
});
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
// ========================================================================
|
|
806
|
-
// close()
|
|
807
|
-
// ========================================================================
|
|
808
|
-
|
|
809
|
-
describe("close", () => {
|
|
810
|
-
it("should close without error", async () => {
|
|
811
|
-
await persistence.close();
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
it("should end owned pool on close", async () => {
|
|
815
|
-
// connectionString path creates an owned pool
|
|
816
|
-
await persistence.close();
|
|
817
|
-
// No error means pool.end() was called successfully
|
|
818
|
-
});
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
// ========================================================================
|
|
822
|
-
// Data integrity
|
|
823
|
-
// ========================================================================
|
|
824
|
-
|
|
825
|
-
describe("data integrity", () => {
|
|
826
|
-
it("should preserve all fields through save/load cycle", async () => {
|
|
827
|
-
const schedule: WorkflowSchedule = {
|
|
828
|
-
id: "integrity-1",
|
|
829
|
-
workflowKind: "complex.workflow",
|
|
830
|
-
triggerType: "workflow_completed",
|
|
831
|
-
triggerOnWorkflowKind: "parent.workflow",
|
|
832
|
-
triggerOnStatus: ["succeeded"],
|
|
833
|
-
input: { nested: { value: 123 } },
|
|
834
|
-
metadata: { tags: ["a", "b"] },
|
|
835
|
-
enabled: true,
|
|
836
|
-
lastRunAt: new Date("2024-06-15T10:30:00Z"),
|
|
837
|
-
lastRunId: "run-789",
|
|
838
|
-
nextRunAt: new Date("2024-06-15T11:00:00Z"),
|
|
839
|
-
};
|
|
840
|
-
|
|
841
|
-
await persistence.saveSchedule(schedule);
|
|
842
|
-
|
|
843
|
-
const loaded = await persistence.loadSchedules();
|
|
844
|
-
expect(loaded[0].id).toBe(schedule.id);
|
|
845
|
-
expect(loaded[0].workflowKind).toBe(schedule.workflowKind);
|
|
846
|
-
expect(loaded[0].triggerType).toBe(schedule.triggerType);
|
|
847
|
-
expect(loaded[0].triggerOnWorkflowKind).toBe(
|
|
848
|
-
schedule.triggerOnWorkflowKind,
|
|
849
|
-
);
|
|
850
|
-
expect(loaded[0].triggerOnStatus).toEqual(schedule.triggerOnStatus);
|
|
851
|
-
expect(loaded[0].input).toEqual(schedule.input);
|
|
852
|
-
expect(loaded[0].metadata).toEqual(schedule.metadata);
|
|
853
|
-
expect(loaded[0].enabled).toBe(schedule.enabled);
|
|
854
|
-
expect(loaded[0].lastRunAt).toEqual(schedule.lastRunAt);
|
|
855
|
-
expect(loaded[0].lastRunId).toBe(schedule.lastRunId);
|
|
856
|
-
expect(loaded[0].nextRunAt).toEqual(schedule.nextRunAt);
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
it("should handle null/undefined values correctly", async () => {
|
|
860
|
-
const schedule: WorkflowSchedule = {
|
|
861
|
-
id: "nullable-1",
|
|
862
|
-
workflowKind: "test.workflow",
|
|
863
|
-
triggerType: "manual",
|
|
864
|
-
enabled: false,
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
await persistence.saveSchedule(schedule);
|
|
868
|
-
|
|
869
|
-
const loaded = await persistence.loadSchedules();
|
|
870
|
-
expect(loaded[0].cronExpression).toBeUndefined();
|
|
871
|
-
expect(loaded[0].timezone).toBeUndefined();
|
|
872
|
-
expect(loaded[0].input).toBeUndefined();
|
|
873
|
-
expect(loaded[0].metadata).toBeUndefined();
|
|
874
|
-
expect(loaded[0].lastRunAt).toBeUndefined();
|
|
875
|
-
});
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
// ========================================================================
|
|
879
|
-
// Custom tableName / schema
|
|
880
|
-
// ========================================================================
|
|
881
|
-
|
|
882
|
-
describe("custom tableName", () => {
|
|
883
|
-
it("should use custom tableName when provided", async () => {
|
|
884
|
-
const p = new PostgresSchedulePersistence({
|
|
885
|
-
connectionString: "pg://x",
|
|
886
|
-
tableName: "my_schedules",
|
|
887
|
-
});
|
|
888
|
-
await p.initialize();
|
|
889
|
-
// Should initialize without error using custom table name
|
|
890
|
-
});
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
describe("custom schema", () => {
|
|
894
|
-
it("should create schema if not public", async () => {
|
|
895
|
-
sqlExecuteCalls = 0;
|
|
896
|
-
const p = new PostgresSchedulePersistence({
|
|
897
|
-
connectionString: "pg://x",
|
|
898
|
-
schema: "custom_schema",
|
|
899
|
-
});
|
|
900
|
-
await p.initialize();
|
|
901
|
-
// createTables runs CREATE SCHEMA + CREATE TABLE + indexes
|
|
902
|
-
// More SQL calls than with public schema
|
|
903
|
-
expect(sqlExecuteCalls).toBeGreaterThan(0);
|
|
904
|
-
});
|
|
905
|
-
});
|
|
906
|
-
});
|