@shipfox/api-triggers 2.0.0
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/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +224 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/core/compute-next-fire-at.d.ts +9 -0
- package/dist/core/compute-next-fire-at.d.ts.map +1 -0
- package/dist/core/compute-next-fire-at.js +28 -0
- package/dist/core/compute-next-fire-at.js.map +1 -0
- package/dist/core/config.d.ts +26 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +68 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dispatch-integration-event.d.ts +14 -0
- package/dist/core/dispatch-integration-event.d.ts.map +1 -0
- package/dist/core/dispatch-integration-event.js +136 -0
- package/dist/core/dispatch-integration-event.js.map +1 -0
- package/dist/core/drain-cron-schedules.d.ts +30 -0
- package/dist/core/drain-cron-schedules.d.ts.map +1 -0
- package/dist/core/drain-cron-schedules.js +76 -0
- package/dist/core/drain-cron-schedules.js.map +1 -0
- package/dist/core/entities/cron-schedule.d.ts +11 -0
- package/dist/core/entities/cron-schedule.d.ts.map +1 -0
- package/dist/core/entities/cron-schedule.js +3 -0
- package/dist/core/entities/cron-schedule.js.map +1 -0
- package/dist/core/entities/decision.d.ts +23 -0
- package/dist/core/entities/decision.d.ts.map +1 -0
- package/dist/core/entities/decision.js +7 -0
- package/dist/core/entities/decision.js.map +1 -0
- package/dist/core/entities/index.d.ts +3 -0
- package/dist/core/entities/index.d.ts.map +1 -0
- package/dist/core/entities/index.js +3 -0
- package/dist/core/entities/index.js.map +1 -0
- package/dist/core/entities/job-listener-subscription.d.ts +14 -0
- package/dist/core/entities/job-listener-subscription.d.ts.map +1 -0
- package/dist/core/entities/job-listener-subscription.js +3 -0
- package/dist/core/entities/job-listener-subscription.js.map +1 -0
- package/dist/core/entities/received-event.d.ts +28 -0
- package/dist/core/entities/received-event.d.ts.map +1 -0
- package/dist/core/entities/received-event.js +14 -0
- package/dist/core/entities/received-event.js.map +1 -0
- package/dist/core/entities/subscription.d.ts +13 -0
- package/dist/core/entities/subscription.d.ts.map +1 -0
- package/dist/core/entities/subscription.js +3 -0
- package/dist/core/entities/subscription.js.map +1 -0
- package/dist/core/errors.d.ts +25 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +41 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fire-cron.d.ts +28 -0
- package/dist/core/fire-cron.d.ts.map +1 -0
- package/dist/core/fire-cron.js +85 -0
- package/dist/core/fire-cron.js.map +1 -0
- package/dist/core/fire-manual.d.ts +9 -0
- package/dist/core/fire-manual.d.ts.map +1 -0
- package/dist/core/fire-manual.js +87 -0
- package/dist/core/fire-manual.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/record-trigger-history.d.ts +35 -0
- package/dist/core/record-trigger-history.d.ts.map +1 -0
- package/dist/core/record-trigger-history.js +75 -0
- package/dist/core/record-trigger-history.js.map +1 -0
- package/dist/core/route-event-to-job-listeners.d.ts +31 -0
- package/dist/core/route-event-to-job-listeners.d.ts.map +1 -0
- package/dist/core/route-event-to-job-listeners.js +140 -0
- package/dist/core/route-event-to-job-listeners.js.map +1 -0
- package/dist/db/cron-schedules.d.ts +36 -0
- package/dist/db/cron-schedules.d.ts.map +1 -0
- package/dist/db/cron-schedules.js +104 -0
- package/dist/db/cron-schedules.js.map +1 -0
- package/dist/db/db.d.ts +2515 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +28 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/event-history.d.ts +50 -0
- package/dist/db/event-history.d.ts.map +1 -0
- package/dist/db/event-history.js +207 -0
- package/dist/db/event-history.js.map +1 -0
- package/dist/db/event-queries.d.ts +38 -0
- package/dist/db/event-queries.d.ts.map +1 -0
- package/dist/db/event-queries.js +78 -0
- package/dist/db/event-queries.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +14 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/job-listener-subscriptions.d.ts +24 -0
- package/dist/db/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/job-listener-subscriptions.js +69 -0
- package/dist/db/job-listener-subscriptions.js.map +1 -0
- package/dist/db/received-events.d.ts +7 -0
- package/dist/db/received-events.d.ts.map +1 -0
- package/dist/db/received-events.js +11 -0
- package/dist/db/received-events.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/cron-schedules.d.ts +148 -0
- package/dist/db/schema/cron-schedules.d.ts.map +1 -0
- package/dist/db/schema/cron-schedules.js +39 -0
- package/dist/db/schema/cron-schedules.js.map +1 -0
- package/dist/db/schema/decisions.d.ts +284 -0
- package/dist/db/schema/decisions.d.ts.map +1 -0
- package/dist/db/schema/decisions.js +70 -0
- package/dist/db/schema/decisions.js.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts +185 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.js +41 -0
- package/dist/db/schema/job-listener-subscriptions.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/received-events.d.ts +544 -0
- package/dist/db/schema/received-events.d.ts.map +1 -0
- package/dist/db/schema/received-events.js +86 -0
- package/dist/db/schema/received-events.js.map +1 -0
- package/dist/db/schema/subscriptions.d.ts +182 -0
- package/dist/db/schema/subscriptions.d.ts.map +1 -0
- package/dist/db/schema/subscriptions.js +39 -0
- package/dist/db/schema/subscriptions.js.map +1 -0
- package/dist/db/subscriptions.d.ts +26 -0
- package/dist/db/subscriptions.d.ts.map +1 -0
- package/dist/db/subscriptions.js +103 -0
- package/dist/db/subscriptions.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +15 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +32 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +15 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/trigger-events.d.ts +7 -0
- package/dist/presentation/dto/trigger-events.d.ts.map +1 -0
- package/dist/presentation/dto/trigger-events.js +47 -0
- package/dist/presentation/dto/trigger-events.js.map +1 -0
- package/dist/presentation/index.d.ts +3 -0
- package/dist/presentation/index.d.ts.map +1 -0
- package/dist/presentation/index.js +4 -0
- package/dist/presentation/index.js.map +1 -0
- package/dist/presentation/routes/fire-manual.d.ts +2 -0
- package/dist/presentation/routes/fire-manual.d.ts.map +1 -0
- package/dist/presentation/routes/fire-manual.js +71 -0
- package/dist/presentation/routes/fire-manual.js.map +1 -0
- package/dist/presentation/routes/get-trigger-event.d.ts +2 -0
- package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -0
- package/dist/presentation/routes/get-trigger-event.js +37 -0
- package/dist/presentation/routes/get-trigger-event.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +27 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.js +29 -0
- package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -0
- package/dist/presentation/routes/list-trigger-events.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-events.js +56 -0
- package/dist/presentation/routes/list-trigger-events.js.map +1 -0
- package/dist/presentation/subscribers/index.d.ts +6 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -0
- package/dist/presentation/subscribers/index.js +7 -0
- package/dist/presentation/subscribers/index.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.js +8 -0
- package/dist/presentation/subscribers/on-definition-deleted.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.js +11 -0
- package/dist/presentation/subscribers/on-definition-resolved.js.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts +4 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.js +17 -0
- package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.js +13 -0
- package/dist/presentation/subscribers/on-job-activated.js.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.js +6 -0
- package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts +4 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.js +20 -0
- package/dist/temporal/activities/drain-cron-batch.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +10 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +15 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts +4 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.js +11 -0
- package/dist/temporal/activities/prune-trigger-events.js.map +1 -0
- package/dist/temporal/constants.d.ts +7 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +7 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts +9 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.js +54 -0
- package/dist/temporal/workflows/cron-tick-cron.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +3 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +4 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js +18 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +109 -0
- package/drizzle/meta/0000_snapshot.json +905 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +14 -0
- package/package.json +83 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +53 -0
- package/src/core/compute-next-fire-at.test.ts +69 -0
- package/src/core/compute-next-fire-at.ts +37 -0
- package/src/core/config.test.ts +213 -0
- package/src/core/config.ts +86 -0
- package/src/core/dispatch-integration-event.history-failure.test.ts +66 -0
- package/src/core/dispatch-integration-event.test.ts +1006 -0
- package/src/core/dispatch-integration-event.ts +150 -0
- package/src/core/drain-cron-schedules.test.ts +265 -0
- package/src/core/drain-cron-schedules.ts +90 -0
- package/src/core/entities/cron-schedule.ts +10 -0
- package/src/core/entities/decision.ts +24 -0
- package/src/core/entities/index.ts +5 -0
- package/src/core/entities/job-listener-subscription.ts +14 -0
- package/src/core/entities/received-event.ts +36 -0
- package/src/core/entities/subscription.ts +12 -0
- package/src/core/errors.ts +63 -0
- package/src/core/fire-cron.test.ts +146 -0
- package/src/core/fire-cron.ts +98 -0
- package/src/core/fire-manual.test.ts +157 -0
- package/src/core/fire-manual.ts +92 -0
- package/src/core/index.ts +34 -0
- package/src/core/record-trigger-history.test.ts +159 -0
- package/src/core/record-trigger-history.ts +145 -0
- package/src/core/route-event-to-job-listeners.test.ts +369 -0
- package/src/core/route-event-to-job-listeners.ts +184 -0
- package/src/db/cron-schedules.test.ts +358 -0
- package/src/db/cron-schedules.ts +163 -0
- package/src/db/db.ts +31 -0
- package/src/db/event-history.test.ts +421 -0
- package/src/db/event-history.ts +279 -0
- package/src/db/event-queries.ts +140 -0
- package/src/db/index.ts +51 -0
- package/src/db/job-listener-subscriptions.test.ts +270 -0
- package/src/db/job-listener-subscriptions.ts +133 -0
- package/src/db/received-events.ts +16 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/cron-schedules.ts +37 -0
- package/src/db/schema/decisions.test.ts +279 -0
- package/src/db/schema/decisions.ts +77 -0
- package/src/db/schema/job-listener-subscriptions.ts +65 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/received-events.test.ts +135 -0
- package/src/db/schema/received-events.ts +93 -0
- package/src/db/schema/subscriptions.ts +46 -0
- package/src/db/subscriptions.test.ts +340 -0
- package/src/db/subscriptions.ts +179 -0
- package/src/index.ts +108 -0
- package/src/metrics/index.ts +8 -0
- package/src/metrics/instance.ts +33 -0
- package/src/metrics/service.ts +18 -0
- package/src/presentation/dto/trigger-events.test.ts +144 -0
- package/src/presentation/dto/trigger-events.ts +60 -0
- package/src/presentation/index.ts +6 -0
- package/src/presentation/routes/fire-manual.test.ts +118 -0
- package/src/presentation/routes/fire-manual.ts +70 -0
- package/src/presentation/routes/get-trigger-event.test.ts +184 -0
- package/src/presentation/routes/get-trigger-event.ts +37 -0
- package/src/presentation/routes/index.ts +21 -0
- package/src/presentation/routes/list-trigger-event-facets.test.ts +116 -0
- package/src/presentation/routes/list-trigger-event-facets.ts +29 -0
- package/src/presentation/routes/list-trigger-events.test.ts +379 -0
- package/src/presentation/routes/list-trigger-events.ts +68 -0
- package/src/presentation/subscribers/index.ts +5 -0
- package/src/presentation/subscribers/on-definition-deleted.test.ts +42 -0
- package/src/presentation/subscribers/on-definition-deleted.ts +6 -0
- package/src/presentation/subscribers/on-definition-resolved.test.ts +67 -0
- package/src/presentation/subscribers/on-definition-resolved.ts +11 -0
- package/src/presentation/subscribers/on-integration-event-received.test.ts +53 -0
- package/src/presentation/subscribers/on-integration-event-received.ts +21 -0
- package/src/presentation/subscribers/on-job-activated.ts +14 -0
- package/src/presentation/subscribers/on-job-terminated.ts +6 -0
- package/src/temporal/activities/drain-cron-batch.ts +20 -0
- package/src/temporal/activities/index.ts +15 -0
- package/src/temporal/activities/prune-trigger-events.test.ts +78 -0
- package/src/temporal/activities/prune-trigger-events.ts +9 -0
- package/src/temporal/constants.ts +7 -0
- package/src/temporal/workflows/cron-tick-cron.ts +56 -0
- package/src/temporal/workflows/index.ts +2 -0
- package/src/temporal/workflows/prune-trigger-events-cron.ts +17 -0
- package/test/env.ts +8 -0
- package/test/factories/cron-schedule.ts +34 -0
- package/test/factories/decision.ts +49 -0
- package/test/factories/job-listener-subscription.ts +42 -0
- package/test/factories/received-event.ts +49 -0
- package/test/factories/subscription.ts +38 -0
- package/test/globalSetup.ts +20 -0
- package/test/index.ts +5 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {and, eq} from 'drizzle-orm';
|
|
2
|
+
import {getCronScheduleBySubscriptionId} from './cron-schedules.js';
|
|
3
|
+
import {db} from './db.js';
|
|
4
|
+
import {triggersCronSchedules} from './schema/cron-schedules.js';
|
|
5
|
+
import {triggerSubscriptions} from './schema/subscriptions.js';
|
|
6
|
+
import {deleteSubscriptionsForDefinition, projectDefinitionTriggers} from './subscriptions.js';
|
|
7
|
+
|
|
8
|
+
describe('cron schedule projection', () => {
|
|
9
|
+
let workspaceId: string;
|
|
10
|
+
let projectId: string;
|
|
11
|
+
let workflowDefinitionId: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.useFakeTimers();
|
|
15
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
16
|
+
workspaceId = crypto.randomUUID();
|
|
17
|
+
projectId = crypto.randomUUID();
|
|
18
|
+
workflowDefinitionId = crypto.randomUUID();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('creates a cron schedule row with a future next fire time', async () => {
|
|
26
|
+
await projectDefinitionTriggers({
|
|
27
|
+
workspaceId,
|
|
28
|
+
projectId,
|
|
29
|
+
workflowDefinitionId,
|
|
30
|
+
triggers: {
|
|
31
|
+
nightly: {
|
|
32
|
+
source: 'cron',
|
|
33
|
+
event: 'tick',
|
|
34
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
39
|
+
|
|
40
|
+
const schedule = await getCronScheduleBySubscriptionId(subscription.id);
|
|
41
|
+
|
|
42
|
+
expect(schedule).toMatchObject({
|
|
43
|
+
subscriptionId: subscription.id,
|
|
44
|
+
workspaceId,
|
|
45
|
+
cronExpression: '0 2 * * *',
|
|
46
|
+
timezone: 'UTC',
|
|
47
|
+
lastFiredAt: null,
|
|
48
|
+
});
|
|
49
|
+
expect(schedule?.nextFireAt.getTime()).toBeGreaterThan(Date.now());
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('preserves next fire and last fired when the expression and timezone are unchanged', async () => {
|
|
53
|
+
await projectDefinitionTriggers({
|
|
54
|
+
workspaceId,
|
|
55
|
+
projectId,
|
|
56
|
+
workflowDefinitionId,
|
|
57
|
+
triggers: {
|
|
58
|
+
nightly: {
|
|
59
|
+
source: 'cron',
|
|
60
|
+
event: 'tick',
|
|
61
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
66
|
+
const before = await getRequiredCronSchedule(subscription.id);
|
|
67
|
+
const lastFiredAt = new Date('2026-01-01T02:00:00.000Z');
|
|
68
|
+
await db()
|
|
69
|
+
.update(triggersCronSchedules)
|
|
70
|
+
.set({lastFiredAt})
|
|
71
|
+
.where(eq(triggersCronSchedules.subscriptionId, subscription.id));
|
|
72
|
+
vi.setSystemTime(new Date('2026-01-01T01:00:00.000Z'));
|
|
73
|
+
|
|
74
|
+
await projectDefinitionTriggers({
|
|
75
|
+
workspaceId,
|
|
76
|
+
projectId,
|
|
77
|
+
workflowDefinitionId,
|
|
78
|
+
triggers: {
|
|
79
|
+
nightly: {
|
|
80
|
+
source: 'cron',
|
|
81
|
+
event: 'tick',
|
|
82
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const after = await getRequiredCronSchedule(subscription.id);
|
|
87
|
+
|
|
88
|
+
expect(after.nextFireAt).toEqual(before.nextFireAt);
|
|
89
|
+
expect(after.lastFiredAt).toEqual(lastFiredAt);
|
|
90
|
+
expect(after.updatedAt).toEqual(before.updatedAt);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('recomputes next fire when the expression changes', async () => {
|
|
94
|
+
await projectDefinitionTriggers({
|
|
95
|
+
workspaceId,
|
|
96
|
+
projectId,
|
|
97
|
+
workflowDefinitionId,
|
|
98
|
+
triggers: {
|
|
99
|
+
nightly: {
|
|
100
|
+
source: 'cron',
|
|
101
|
+
event: 'tick',
|
|
102
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
107
|
+
const before = await getRequiredCronSchedule(subscription.id);
|
|
108
|
+
|
|
109
|
+
await projectDefinitionTriggers({
|
|
110
|
+
workspaceId,
|
|
111
|
+
projectId,
|
|
112
|
+
workflowDefinitionId,
|
|
113
|
+
triggers: {
|
|
114
|
+
nightly: {
|
|
115
|
+
source: 'cron',
|
|
116
|
+
event: 'tick',
|
|
117
|
+
config: {schedule: '0 3 * * *', timezone: 'UTC'},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const after = await getRequiredCronSchedule(subscription.id);
|
|
122
|
+
|
|
123
|
+
expect(after.cronExpression).toBe('0 3 * * *');
|
|
124
|
+
expect(after.nextFireAt).not.toEqual(before.nextFireAt);
|
|
125
|
+
expect(after.nextFireAt).toEqual(new Date('2026-01-01T03:00:00.000Z'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('recomputes next fire when the timezone changes', async () => {
|
|
129
|
+
await projectDefinitionTriggers({
|
|
130
|
+
workspaceId,
|
|
131
|
+
projectId,
|
|
132
|
+
workflowDefinitionId,
|
|
133
|
+
triggers: {
|
|
134
|
+
nightly: {
|
|
135
|
+
source: 'cron',
|
|
136
|
+
event: 'tick',
|
|
137
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
142
|
+
const before = await getRequiredCronSchedule(subscription.id);
|
|
143
|
+
|
|
144
|
+
await projectDefinitionTriggers({
|
|
145
|
+
workspaceId,
|
|
146
|
+
projectId,
|
|
147
|
+
workflowDefinitionId,
|
|
148
|
+
triggers: {
|
|
149
|
+
nightly: {
|
|
150
|
+
source: 'cron',
|
|
151
|
+
event: 'tick',
|
|
152
|
+
config: {schedule: '0 2 * * *', timezone: 'America/New_York'},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const after = await getRequiredCronSchedule(subscription.id);
|
|
157
|
+
|
|
158
|
+
expect(after.timezone).toBe('America/New_York');
|
|
159
|
+
expect(after.nextFireAt).not.toEqual(before.nextFireAt);
|
|
160
|
+
expect(after.nextFireAt).toEqual(new Date('2026-01-01T07:00:00.000Z'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('drops a cron schedule when the trigger source changes in place', async () => {
|
|
164
|
+
await projectDefinitionTriggers({
|
|
165
|
+
workspaceId,
|
|
166
|
+
projectId,
|
|
167
|
+
workflowDefinitionId,
|
|
168
|
+
triggers: {
|
|
169
|
+
nightly: {
|
|
170
|
+
source: 'cron',
|
|
171
|
+
event: 'tick',
|
|
172
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
177
|
+
|
|
178
|
+
await projectDefinitionTriggers({
|
|
179
|
+
workspaceId,
|
|
180
|
+
projectId,
|
|
181
|
+
workflowDefinitionId,
|
|
182
|
+
triggers: {
|
|
183
|
+
nightly: {source: 'github', event: 'push'},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
const schedule = await getCronScheduleBySubscriptionId(subscription.id);
|
|
187
|
+
|
|
188
|
+
expect(schedule).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('drops a cron schedule when the trigger is removed', async () => {
|
|
192
|
+
await projectDefinitionTriggers({
|
|
193
|
+
workspaceId,
|
|
194
|
+
projectId,
|
|
195
|
+
workflowDefinitionId,
|
|
196
|
+
triggers: {
|
|
197
|
+
nightly: {
|
|
198
|
+
source: 'cron',
|
|
199
|
+
event: 'tick',
|
|
200
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
205
|
+
|
|
206
|
+
await projectDefinitionTriggers({
|
|
207
|
+
workspaceId,
|
|
208
|
+
projectId,
|
|
209
|
+
workflowDefinitionId,
|
|
210
|
+
triggers: {},
|
|
211
|
+
});
|
|
212
|
+
const schedule = await getCronScheduleBySubscriptionId(subscription.id);
|
|
213
|
+
|
|
214
|
+
expect(schedule).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('drops a cron schedule when definition subscriptions are deleted', async () => {
|
|
218
|
+
await projectDefinitionTriggers({
|
|
219
|
+
workspaceId,
|
|
220
|
+
projectId,
|
|
221
|
+
workflowDefinitionId,
|
|
222
|
+
triggers: {
|
|
223
|
+
nightly: {
|
|
224
|
+
source: 'cron',
|
|
225
|
+
event: 'tick',
|
|
226
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
231
|
+
|
|
232
|
+
const deletedCount = await deleteSubscriptionsForDefinition({workflowDefinitionId});
|
|
233
|
+
const schedule = await getCronScheduleBySubscriptionId(subscription.id);
|
|
234
|
+
|
|
235
|
+
expect(deletedCount).toBe(1);
|
|
236
|
+
expect(schedule).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test.each([
|
|
240
|
+
{name: 'missing schedule', config: {}},
|
|
241
|
+
{name: 'invalid expression', config: {schedule: 'not a cron', timezone: 'UTC'}},
|
|
242
|
+
])('drops a stale cron schedule when resynced with $name', async ({config}) => {
|
|
243
|
+
await projectDefinitionTriggers({
|
|
244
|
+
workspaceId,
|
|
245
|
+
projectId,
|
|
246
|
+
workflowDefinitionId,
|
|
247
|
+
triggers: {
|
|
248
|
+
nightly: {
|
|
249
|
+
source: 'cron',
|
|
250
|
+
event: 'tick',
|
|
251
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
256
|
+
|
|
257
|
+
await projectDefinitionTriggers({
|
|
258
|
+
workspaceId,
|
|
259
|
+
projectId,
|
|
260
|
+
workflowDefinitionId,
|
|
261
|
+
triggers: {
|
|
262
|
+
nightly: {source: 'cron', event: 'tick', config},
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const schedule = await getCronScheduleBySubscriptionId(subscription.id);
|
|
266
|
+
|
|
267
|
+
expect(schedule).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('skips invalid cron config without blocking sibling trigger sync', async () => {
|
|
271
|
+
await projectDefinitionTriggers({
|
|
272
|
+
workspaceId,
|
|
273
|
+
projectId,
|
|
274
|
+
workflowDefinitionId,
|
|
275
|
+
triggers: {
|
|
276
|
+
malformed: {source: 'cron', event: 'tick', config: {}},
|
|
277
|
+
bad_expression: {
|
|
278
|
+
source: 'cron',
|
|
279
|
+
event: 'tick',
|
|
280
|
+
config: {schedule: 'not a cron', timezone: 'UTC'},
|
|
281
|
+
},
|
|
282
|
+
on_push: {source: 'github', event: 'push'},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const malformed = await getSubscriptionByName(workflowDefinitionId, 'malformed');
|
|
286
|
+
const badExpression = await getSubscriptionByName(workflowDefinitionId, 'bad_expression');
|
|
287
|
+
const onPush = await getSubscriptionByName(workflowDefinitionId, 'on_push');
|
|
288
|
+
|
|
289
|
+
const malformedSchedule = await getCronScheduleBySubscriptionId(malformed.id);
|
|
290
|
+
const badExpressionSchedule = await getCronScheduleBySubscriptionId(badExpression.id);
|
|
291
|
+
|
|
292
|
+
expect(malformedSchedule).toBeUndefined();
|
|
293
|
+
expect(badExpressionSchedule).toBeUndefined();
|
|
294
|
+
expect(onPush.source).toBe('github');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('defaults a missing timezone to UTC defensively', async () => {
|
|
298
|
+
await projectDefinitionTriggers({
|
|
299
|
+
workspaceId,
|
|
300
|
+
projectId,
|
|
301
|
+
workflowDefinitionId,
|
|
302
|
+
triggers: {
|
|
303
|
+
hourly: {
|
|
304
|
+
source: 'cron',
|
|
305
|
+
event: 'tick',
|
|
306
|
+
config: {schedule: '0 * * * *'},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'hourly');
|
|
311
|
+
|
|
312
|
+
const schedule = await getRequiredCronSchedule(subscription.id);
|
|
313
|
+
|
|
314
|
+
expect(schedule.timezone).toBe('UTC');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('stores only with and filter in the subscription config', async () => {
|
|
318
|
+
await projectDefinitionTriggers({
|
|
319
|
+
workspaceId,
|
|
320
|
+
projectId,
|
|
321
|
+
workflowDefinitionId,
|
|
322
|
+
triggers: {
|
|
323
|
+
nightly: {
|
|
324
|
+
source: 'cron',
|
|
325
|
+
event: 'tick',
|
|
326
|
+
with: {branch: 'main'},
|
|
327
|
+
filter: 'true',
|
|
328
|
+
config: {schedule: '0 2 * * *', timezone: 'UTC'},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const subscription = await getSubscriptionByName(workflowDefinitionId, 'nightly');
|
|
334
|
+
|
|
335
|
+
expect(subscription.config).toEqual({with: {branch: 'main'}, filter: 'true'});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
async function getSubscriptionByName(workflowDefinitionId: string, name: string) {
|
|
340
|
+
const [row] = await db()
|
|
341
|
+
.select()
|
|
342
|
+
.from(triggerSubscriptions)
|
|
343
|
+
.where(
|
|
344
|
+
and(
|
|
345
|
+
eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId),
|
|
346
|
+
eq(triggerSubscriptions.name, name),
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
.limit(1);
|
|
350
|
+
if (!row) throw new Error(`Expected subscription ${name} to exist`);
|
|
351
|
+
return row;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function getRequiredCronSchedule(subscriptionId: string) {
|
|
355
|
+
const schedule = await getCronScheduleBySubscriptionId(subscriptionId);
|
|
356
|
+
if (!schedule) throw new Error(`Expected cron schedule for subscription ${subscriptionId}`);
|
|
357
|
+
return schedule;
|
|
358
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
2
|
+
import {triggerSourceConfigSchemas} from '@shipfox/workflow-document';
|
|
3
|
+
import {eq, lte, sql} from 'drizzle-orm';
|
|
4
|
+
import {config} from '#config.js';
|
|
5
|
+
import {computeNextFireAt} from '#core/compute-next-fire-at.js';
|
|
6
|
+
import type {CronSchedule} from '#core/entities/cron-schedule.js';
|
|
7
|
+
import {db, type Executor, type Tx} from './db.js';
|
|
8
|
+
import {toCronSchedule, triggersCronSchedules} from './schema/cron-schedules.js';
|
|
9
|
+
|
|
10
|
+
const defaultCronTimezone = 'UTC';
|
|
11
|
+
|
|
12
|
+
export interface SyncCronScheduleParams {
|
|
13
|
+
readonly tx: Tx;
|
|
14
|
+
readonly subscriptionId: string;
|
|
15
|
+
readonly workspaceId: string;
|
|
16
|
+
readonly triggerConfig: Record<string, unknown> | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function syncCronSchedule(params: SyncCronScheduleParams): Promise<void> {
|
|
20
|
+
const parsed = triggerSourceConfigSchemas.cron.safeParse(params.triggerConfig ?? {});
|
|
21
|
+
if (!parsed.success || parsed.data.schedule === undefined) {
|
|
22
|
+
logger().warn(
|
|
23
|
+
{subscriptionId: params.subscriptionId},
|
|
24
|
+
'cron trigger missing/invalid schedule config; skipping cron row',
|
|
25
|
+
);
|
|
26
|
+
await deleteCronScheduleForSubscription(params);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const schedule = parsed.data.schedule;
|
|
31
|
+
const timezone = parsed.data.timezone ?? defaultCronTimezone;
|
|
32
|
+
let nextFireAt: Date;
|
|
33
|
+
try {
|
|
34
|
+
nextFireAt = computeNextFireAt({
|
|
35
|
+
cronExpression: schedule,
|
|
36
|
+
timezone,
|
|
37
|
+
from: new Date(),
|
|
38
|
+
subscriptionId: params.subscriptionId,
|
|
39
|
+
jitterWindowSeconds: config.TRIGGER_CRON_JITTER_WINDOW_SECONDS,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
logger().warn(
|
|
43
|
+
{err: error, reason: errorMessage(error), subscriptionId: params.subscriptionId},
|
|
44
|
+
'cron trigger schedule could not be parsed; skipping cron row',
|
|
45
|
+
);
|
|
46
|
+
await deleteCronScheduleForSubscription(params);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await params.tx
|
|
51
|
+
.insert(triggersCronSchedules)
|
|
52
|
+
.values({
|
|
53
|
+
subscriptionId: params.subscriptionId,
|
|
54
|
+
workspaceId: params.workspaceId,
|
|
55
|
+
cronExpression: schedule,
|
|
56
|
+
timezone,
|
|
57
|
+
nextFireAt,
|
|
58
|
+
})
|
|
59
|
+
.onConflictDoUpdate({
|
|
60
|
+
target: triggersCronSchedules.subscriptionId,
|
|
61
|
+
set: {
|
|
62
|
+
workspaceId: sql`excluded.workspace_id`,
|
|
63
|
+
cronExpression: sql`excluded.cron_expression`,
|
|
64
|
+
timezone: sql`excluded.timezone`,
|
|
65
|
+
updatedAt: sql`case when ${triggersCronSchedules.workspaceId} <> excluded.workspace_id or ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then ${new Date()} else ${triggersCronSchedules.updatedAt} end`,
|
|
66
|
+
nextFireAt: sql`case when ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then excluded.next_fire_at else ${triggersCronSchedules.nextFireAt} end`,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DeleteCronScheduleForSubscriptionParams {
|
|
72
|
+
readonly tx: Tx;
|
|
73
|
+
readonly subscriptionId: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function deleteCronScheduleForSubscription(
|
|
77
|
+
params: DeleteCronScheduleForSubscriptionParams,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
await params.tx
|
|
80
|
+
.delete(triggersCronSchedules)
|
|
81
|
+
.where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Reads the database clock so a tick's due check and its next-fire computation share
|
|
85
|
+
// one time reference, immune to application/database clock skew.
|
|
86
|
+
export async function selectDbNow(executor: Executor): Promise<Date> {
|
|
87
|
+
const result = await executor.execute<{now: Date | string}>(sql`select now() as now`);
|
|
88
|
+
const raw = result.rows[0]?.now;
|
|
89
|
+
if (raw === undefined) throw new Error('Failed to read the database clock');
|
|
90
|
+
return raw instanceof Date ? raw : new Date(raw);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ClaimDueCronSchedulesParams {
|
|
94
|
+
readonly tx: Tx;
|
|
95
|
+
readonly limit: number;
|
|
96
|
+
readonly now: Date;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Claims schedules due as of `now` (the shared database clock captured for this tick)
|
|
101
|
+
* and holds `FOR UPDATE SKIP LOCKED` locks until the caller's transaction ends, so
|
|
102
|
+
* concurrent drains partition due rows instead of double-claiming them.
|
|
103
|
+
*/
|
|
104
|
+
export async function claimDueCronSchedules(
|
|
105
|
+
params: ClaimDueCronSchedulesParams,
|
|
106
|
+
): Promise<CronSchedule[]> {
|
|
107
|
+
const rows = await params.tx
|
|
108
|
+
.select()
|
|
109
|
+
.from(triggersCronSchedules)
|
|
110
|
+
.where(lte(triggersCronSchedules.nextFireAt, params.now))
|
|
111
|
+
.orderBy(triggersCronSchedules.nextFireAt)
|
|
112
|
+
.limit(params.limit)
|
|
113
|
+
.for('update', {skipLocked: true});
|
|
114
|
+
return rows.map(toCronSchedule);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AdvanceCronScheduleParams {
|
|
118
|
+
readonly tx: Tx;
|
|
119
|
+
readonly subscriptionId: string;
|
|
120
|
+
readonly nextFireAt: Date;
|
|
121
|
+
readonly lastFiredAt: Date;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function advanceCronSchedule(params: AdvanceCronScheduleParams): Promise<void> {
|
|
125
|
+
await params.tx
|
|
126
|
+
.update(triggersCronSchedules)
|
|
127
|
+
.set({
|
|
128
|
+
nextFireAt: params.nextFireAt,
|
|
129
|
+
lastFiredAt: params.lastFiredAt,
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
})
|
|
132
|
+
.where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function countDueCronSchedules(): Promise<number> {
|
|
136
|
+
const [row] = await db()
|
|
137
|
+
.select({count: sql<number>`count(*)::int`})
|
|
138
|
+
.from(triggersCronSchedules)
|
|
139
|
+
.where(lte(triggersCronSchedules.nextFireAt, sql`now()`));
|
|
140
|
+
return row?.count ?? 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getCronScheduleBySubscriptionId(
|
|
144
|
+
subscriptionId: string,
|
|
145
|
+
): Promise<CronSchedule | undefined> {
|
|
146
|
+
const rows = await db()
|
|
147
|
+
.select()
|
|
148
|
+
.from(triggersCronSchedules)
|
|
149
|
+
.where(eq(triggersCronSchedules.subscriptionId, subscriptionId))
|
|
150
|
+
.limit(1);
|
|
151
|
+
const row = rows[0];
|
|
152
|
+
if (!row) return undefined;
|
|
153
|
+
return toCronSchedule(row);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function errorMessage(error: unknown): string {
|
|
157
|
+
if (error instanceof Error) return error.message;
|
|
158
|
+
try {
|
|
159
|
+
return String(error);
|
|
160
|
+
} catch {
|
|
161
|
+
return '[unprintable thrown value]';
|
|
162
|
+
}
|
|
163
|
+
}
|
package/src/db/db.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
|
|
2
|
+
import {pgClient} from '@shipfox/node-postgres';
|
|
3
|
+
import {triggersCronSchedules} from './schema/cron-schedules.js';
|
|
4
|
+
import {triggersDecisions} from './schema/decisions.js';
|
|
5
|
+
import {jobListenerSubscriptions} from './schema/job-listener-subscriptions.js';
|
|
6
|
+
import {triggersOutbox} from './schema/outbox.js';
|
|
7
|
+
import {triggersReceivedEvents} from './schema/received-events.js';
|
|
8
|
+
import {triggerSubscriptions} from './schema/subscriptions.js';
|
|
9
|
+
|
|
10
|
+
export const schema = {
|
|
11
|
+
triggersCronSchedules,
|
|
12
|
+
jobListenerSubscriptions,
|
|
13
|
+
triggerSubscriptions,
|
|
14
|
+
triggersOutbox,
|
|
15
|
+
triggersReceivedEvents,
|
|
16
|
+
triggersDecisions,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let _db: NodePgDatabase<typeof schema> | undefined;
|
|
20
|
+
|
|
21
|
+
export function db() {
|
|
22
|
+
if (!_db) _db = drizzle(pgClient(), {schema});
|
|
23
|
+
return _db;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
27
|
+
export type Executor = ReturnType<typeof db> | Tx;
|
|
28
|
+
|
|
29
|
+
export function closeDb(): void {
|
|
30
|
+
_db = undefined;
|
|
31
|
+
}
|