@shipfox/api-triggers 4.0.0 → 6.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 +4 -4
- package/CHANGELOG.md +65 -0
- package/README.md +23 -3
- package/dist/core/dispatch-integration-event.d.ts +2 -0
- package/dist/core/dispatch-integration-event.d.ts.map +1 -1
- package/dist/core/dispatch-integration-event.js +9 -5
- package/dist/core/dispatch-integration-event.js.map +1 -1
- package/dist/core/drain-cron-schedules.d.ts +2 -0
- package/dist/core/drain-cron-schedules.d.ts.map +1 -1
- package/dist/core/drain-cron-schedules.js +1 -0
- package/dist/core/drain-cron-schedules.js.map +1 -1
- package/dist/core/fire-cron.d.ts +6 -2
- package/dist/core/fire-cron.d.ts.map +1 -1
- package/dist/core/fire-cron.js +8 -5
- package/dist/core/fire-cron.js.map +1 -1
- package/dist/core/fire-manual.d.ts +6 -2
- package/dist/core/fire-manual.d.ts.map +1 -1
- package/dist/core/fire-manual.js +8 -4
- package/dist/core/fire-manual.js.map +1 -1
- package/dist/core/route-event-to-job-listeners.d.ts +2 -0
- package/dist/core/route-event-to-job-listeners.d.ts.map +1 -1
- package/dist/core/route-event-to-job-listeners.js +2 -3
- package/dist/core/route-event-to-job-listeners.js.map +1 -1
- package/dist/core/workflows-client.d.ts +69 -0
- package/dist/core/workflows-client.d.ts.map +1 -0
- package/dist/core/workflows-client.js +20 -0
- package/dist/core/workflows-client.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +52 -50
- package/dist/index.js.map +1 -1
- package/dist/presentation/index.d.ts +2 -2
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +2 -2
- package/dist/presentation/index.js.map +1 -1
- package/dist/presentation/routes/fire-manual.d.ts +2 -1
- package/dist/presentation/routes/fire-manual.d.ts.map +1 -1
- package/dist/presentation/routes/fire-manual.js +61 -58
- package/dist/presentation/routes/fire-manual.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +23 -21
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/subscribers/index.d.ts +1 -1
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +1 -1
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/presentation/subscribers/on-integration-event-received.d.ts +2 -1
- package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -1
- package/dist/presentation/subscribers/on-integration-event-received.js +16 -13
- package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -1
- package/dist/temporal/activities/drain-cron-batch.d.ts +2 -1
- package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -1
- package/dist/temporal/activities/drain-cron-batch.js +2 -1
- package/dist/temporal/activities/drain-cron-batch.js.map +1 -1
- package/dist/temporal/activities/index.d.ts +4 -3
- package/dist/temporal/activities/index.d.ts.map +1 -1
- package/dist/temporal/activities/index.js +2 -2
- package/dist/temporal/activities/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +27 -35
- package/src/core/dispatch-integration-event.history-failure.test.ts +11 -6
- package/src/core/dispatch-integration-event.test.ts +50 -36
- package/src/core/dispatch-integration-event.ts +8 -5
- package/src/core/drain-cron-schedules.test.ts +30 -29
- package/src/core/drain-cron-schedules.ts +3 -0
- package/src/core/fire-cron.test.ts +35 -24
- package/src/core/fire-cron.ts +9 -7
- package/src/core/fire-manual.test.ts +19 -22
- package/src/core/fire-manual.ts +9 -6
- package/src/core/record-trigger-history.test.ts +3 -5
- package/src/core/route-event-to-job-listeners.test.ts +5 -4
- package/src/core/route-event-to-job-listeners.ts +5 -4
- package/src/core/workflows-client.test.ts +73 -0
- package/src/core/workflows-client.ts +32 -0
- package/src/index.ts +49 -42
- package/src/presentation/index.ts +2 -2
- package/src/presentation/routes/fire-manual.test.ts +14 -30
- package/src/presentation/routes/fire-manual.ts +62 -54
- package/src/presentation/routes/index.ts +18 -15
- package/src/presentation/subscribers/index.ts +1 -1
- package/src/presentation/subscribers/on-integration-event-received.test.ts +6 -3
- package/src/presentation/subscribers/on-integration-event-received.ts +20 -16
- package/src/temporal/activities/drain-cron-batch.ts +5 -1
- package/src/temporal/activities/index.ts +3 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/core/fire-cron.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {isPermanentRunWorkflowError, runWorkflow, type WorkflowRun} from '@shipfox/api-workflows';
|
|
2
1
|
import {getTriggerSubscriptionById} from '#db/subscriptions.js';
|
|
3
2
|
import {cronFiredCount, cronFireLag} from '#metrics/instance.js';
|
|
4
3
|
import {readConfigInputs} from './config.js';
|
|
5
4
|
import {TriggerSubscriptionNotCronError, TriggerSubscriptionNotFoundError} from './errors.js';
|
|
6
5
|
import {beginTriggerHistory, toReason} from './record-trigger-history.js';
|
|
6
|
+
import {isPermanentStartRunError, type WorkflowsModuleClient} from './workflows-client.js';
|
|
7
7
|
|
|
8
8
|
export interface FireCronSubscriptionParams {
|
|
9
|
+
workflows: WorkflowsModuleClient;
|
|
9
10
|
subscriptionId: string;
|
|
10
11
|
/**
|
|
11
12
|
* The `next_fire_at` value the schedule held when it was claimed. Anchoring the
|
|
@@ -16,7 +17,7 @@ export interface FireCronSubscriptionParams {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export type FireCronSubscriptionResult =
|
|
19
|
-
| {outcome: 'fired'; run:
|
|
20
|
+
| {outcome: 'fired'; run: {id: string; name: string}}
|
|
20
21
|
| {outcome: 'errored'};
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -56,9 +57,10 @@ export async function fireCronSubscription(
|
|
|
56
57
|
eventRef,
|
|
57
58
|
};
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
const inputs = readConfigInputs(subscription);
|
|
61
|
+
let run: {id: string; name: string};
|
|
60
62
|
try {
|
|
61
|
-
run = await
|
|
63
|
+
run = await params.workflows.startRunFromTrigger({
|
|
62
64
|
workspaceId: subscription.workspaceId,
|
|
63
65
|
projectId: subscription.projectId,
|
|
64
66
|
definitionId: subscription.workflowDefinitionId,
|
|
@@ -68,13 +70,13 @@ export async function fireCronSubscription(
|
|
|
68
70
|
event: 'tick',
|
|
69
71
|
scheduleId: subscription.id,
|
|
70
72
|
},
|
|
71
|
-
inputs:
|
|
72
|
-
|
|
73
|
+
...(inputs === undefined ? {} : {inputs}),
|
|
74
|
+
idempotencyKey: eventRef,
|
|
73
75
|
});
|
|
74
76
|
} catch (error) {
|
|
75
77
|
const failure = await beginTriggerHistory(historyBase);
|
|
76
78
|
await failure.dispatchErrored(subscription, toReason(error));
|
|
77
|
-
if (
|
|
79
|
+
if (isPermanentStartRunError(error)) {
|
|
78
80
|
recordFire('errored', params.scheduledSlot);
|
|
79
81
|
await failure.allErrored(1);
|
|
80
82
|
return {outcome: 'errored'};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
|
|
2
|
+
import {createInterModuleKnownError} from '@shipfox/inter-module';
|
|
1
3
|
import {eq} from 'drizzle-orm';
|
|
2
4
|
import {db} from '#db/db.js';
|
|
3
5
|
import {triggersDecisions} from '#db/schema/decisions.js';
|
|
@@ -7,27 +9,10 @@ import {TriggerSubscriptionNotManualError} from './errors.js';
|
|
|
7
9
|
|
|
8
10
|
const runWorkflow = vi.fn();
|
|
9
11
|
|
|
10
|
-
// Keep real-enough permanent-error classes + the classifier so the manual path's
|
|
11
|
-
// permanent/transient branching is exercised without loading the workflows module graph.
|
|
12
|
-
vi.mock('@shipfox/api-workflows', () => {
|
|
13
|
-
class DefinitionNotFoundError extends Error {
|
|
14
|
-
constructor(definitionId: string) {
|
|
15
|
-
super(`Definition not found: ${definitionId}`);
|
|
16
|
-
this.name = 'DefinitionNotFoundError';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
runWorkflow: (...args: unknown[]) => runWorkflow(...args),
|
|
21
|
-
DefinitionNotFoundError,
|
|
22
|
-
isPermanentRunWorkflowError: (error: unknown) => error instanceof DefinitionNotFoundError,
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
import {DefinitionNotFoundError} from '@shipfox/api-workflows';
|
|
27
|
-
|
|
28
|
-
// Import after mocks so the function under test sees the spy.
|
|
29
12
|
const {fireManualSubscription} = await import('./fire-manual.js');
|
|
30
13
|
|
|
14
|
+
const workflows = {startRunFromTrigger: (...args: unknown[]) => runWorkflow(...args)} as never;
|
|
15
|
+
|
|
31
16
|
function eventsForWorkspace(workspaceId: string) {
|
|
32
17
|
return db()
|
|
33
18
|
.select()
|
|
@@ -57,12 +42,15 @@ describe('fireManualSubscription (trigger history)', () => {
|
|
|
57
42
|
runWorkflow.mockResolvedValue(run);
|
|
58
43
|
|
|
59
44
|
const result = await fireManualSubscription({
|
|
45
|
+
workflows,
|
|
60
46
|
subscriptionId: subscription.id,
|
|
61
47
|
callerWorkspaceId: subscription.workspaceId,
|
|
62
48
|
userId: crypto.randomUUID(),
|
|
63
49
|
});
|
|
64
50
|
|
|
65
51
|
expect(result).toEqual(run);
|
|
52
|
+
const [payload] = runWorkflow.mock.calls[0] as [Record<string, unknown>];
|
|
53
|
+
expect(payload).not.toHaveProperty('inputs');
|
|
66
54
|
const [event] = await db()
|
|
67
55
|
.select()
|
|
68
56
|
.from(triggersReceivedEvents)
|
|
@@ -88,6 +76,7 @@ describe('fireManualSubscription (trigger history)', () => {
|
|
|
88
76
|
|
|
89
77
|
await expect(
|
|
90
78
|
fireManualSubscription({
|
|
79
|
+
workflows,
|
|
91
80
|
subscriptionId: subscription.id,
|
|
92
81
|
callerWorkspaceId: subscription.workspaceId,
|
|
93
82
|
userId: crypto.randomUUID(),
|
|
@@ -114,15 +103,22 @@ describe('fireManualSubscription (trigger history)', () => {
|
|
|
114
103
|
event: 'fire',
|
|
115
104
|
config: {},
|
|
116
105
|
});
|
|
117
|
-
runWorkflow.mockRejectedValue(
|
|
106
|
+
runWorkflow.mockRejectedValue(
|
|
107
|
+
createInterModuleKnownError(
|
|
108
|
+
workflowsInterModuleContract.methods.startRunFromTrigger,
|
|
109
|
+
'definition-not-found',
|
|
110
|
+
{definitionId: crypto.randomUUID()},
|
|
111
|
+
),
|
|
112
|
+
);
|
|
118
113
|
|
|
119
114
|
await expect(
|
|
120
115
|
fireManualSubscription({
|
|
116
|
+
workflows,
|
|
121
117
|
subscriptionId: subscription.id,
|
|
122
118
|
callerWorkspaceId: subscription.workspaceId,
|
|
123
119
|
userId: crypto.randomUUID(),
|
|
124
120
|
}),
|
|
125
|
-
).rejects.toThrow('
|
|
121
|
+
).rejects.toThrow('definition-not-found');
|
|
126
122
|
|
|
127
123
|
const events = await eventsForWorkspace(subscription.workspaceId);
|
|
128
124
|
expect(events).toHaveLength(1);
|
|
@@ -133,7 +129,7 @@ describe('fireManualSubscription (trigger history)', () => {
|
|
|
133
129
|
expect(event.processedAt).toBeInstanceOf(Date);
|
|
134
130
|
const decisions = await decisionsForEvent(event.id);
|
|
135
131
|
expect(decisions[0]?.decision).toBe('dispatch-error');
|
|
136
|
-
expect(decisions[0]?.reason).toContain('
|
|
132
|
+
expect(decisions[0]?.reason).toContain('definition-not-found');
|
|
137
133
|
});
|
|
138
134
|
|
|
139
135
|
test('does not record a received event when the subscription is not a manual trigger', async () => {
|
|
@@ -145,6 +141,7 @@ describe('fireManualSubscription (trigger history)', () => {
|
|
|
145
141
|
|
|
146
142
|
await expect(
|
|
147
143
|
fireManualSubscription({
|
|
144
|
+
workflows,
|
|
148
145
|
subscriptionId: subscription.id,
|
|
149
146
|
callerWorkspaceId: subscription.workspaceId,
|
|
150
147
|
userId: crypto.randomUUID(),
|
package/src/core/fire-manual.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
|
-
import {isPermanentRunWorkflowError, runWorkflow, type WorkflowRun} from '@shipfox/api-workflows';
|
|
3
2
|
import {getTriggerSubscriptionById} from '#db/subscriptions.js';
|
|
4
3
|
import {
|
|
5
4
|
eventOutcomeCount,
|
|
@@ -13,8 +12,10 @@ import {
|
|
|
13
12
|
TriggerWorkspaceMismatchError,
|
|
14
13
|
} from './errors.js';
|
|
15
14
|
import {beginTriggerHistory, toReason} from './record-trigger-history.js';
|
|
15
|
+
import {isPermanentStartRunError, type WorkflowsModuleClient} from './workflows-client.js';
|
|
16
16
|
|
|
17
17
|
export interface FireManualSubscriptionParams {
|
|
18
|
+
workflows: WorkflowsModuleClient;
|
|
18
19
|
subscriptionId: string;
|
|
19
20
|
callerWorkspaceId: string;
|
|
20
21
|
userId: string;
|
|
@@ -23,7 +24,7 @@ export interface FireManualSubscriptionParams {
|
|
|
23
24
|
|
|
24
25
|
export async function fireManualSubscription(
|
|
25
26
|
params: FireManualSubscriptionParams,
|
|
26
|
-
): Promise<
|
|
27
|
+
): Promise<{id: string; name: string}> {
|
|
27
28
|
const subscription = await getTriggerSubscriptionById(params.subscriptionId);
|
|
28
29
|
if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);
|
|
29
30
|
if (subscription.source !== 'manual') {
|
|
@@ -55,9 +56,10 @@ export async function fireManualSubscription(
|
|
|
55
56
|
|
|
56
57
|
eventReceivedCount.add(1, {provider: 'manual'});
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
const inputs = params.inputs ?? readConfigInputs(subscription);
|
|
60
|
+
let run: {id: string; name: string};
|
|
59
61
|
try {
|
|
60
|
-
run = await
|
|
62
|
+
run = await params.workflows.startRunFromTrigger({
|
|
61
63
|
workspaceId: subscription.workspaceId,
|
|
62
64
|
projectId: subscription.projectId,
|
|
63
65
|
definitionId: subscription.workflowDefinitionId,
|
|
@@ -68,12 +70,13 @@ export async function fireManualSubscription(
|
|
|
68
70
|
subscriptionId: subscription.id,
|
|
69
71
|
userId: params.userId,
|
|
70
72
|
},
|
|
71
|
-
inputs:
|
|
73
|
+
...(inputs === undefined ? {} : {inputs}),
|
|
74
|
+
idempotencyKey: randomUUID(),
|
|
72
75
|
});
|
|
73
76
|
} catch (error) {
|
|
74
77
|
const failure = await beginTriggerHistory({...historyBase, eventRef: randomUUID()});
|
|
75
78
|
await failure.dispatchErrored(subscription, toReason(error));
|
|
76
|
-
if (
|
|
79
|
+
if (isPermanentStartRunError(error)) {
|
|
77
80
|
eventOutcomeCount.add(1, {provider: 'manual', outcome: 'errored'});
|
|
78
81
|
await failure.allErrored(1);
|
|
79
82
|
} else {
|
|
@@ -5,11 +5,6 @@ const insertReceivedEvent = vi.fn();
|
|
|
5
5
|
const markReceivedEventRouted = vi.fn();
|
|
6
6
|
const upsertTriggeredDecision = vi.fn();
|
|
7
7
|
|
|
8
|
-
vi.mock('@shipfox/api-workflows', () => ({
|
|
9
|
-
runWorkflow: (...args: unknown[]) => runWorkflow(...args),
|
|
10
|
-
isPermanentRunWorkflowError: () => false,
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
8
|
vi.mock('#db/event-history.js', () => ({
|
|
14
9
|
insertReceivedEvent: (...args: unknown[]) => insertReceivedEvent(...args),
|
|
15
10
|
markReceivedEventDiscarded: vi.fn(),
|
|
@@ -28,6 +23,8 @@ vi.mock('#db/event-history.js', () => ({
|
|
|
28
23
|
const {beginTriggerHistory, toReason} = await import('./record-trigger-history.js');
|
|
29
24
|
const {fireManualSubscription} = await import('./fire-manual.js');
|
|
30
25
|
|
|
26
|
+
const workflows = {startRunFromTrigger: (...args: unknown[]) => runWorkflow(...args)} as never;
|
|
27
|
+
|
|
31
28
|
describe('trigger history is best-effort and never blocks triggering', () => {
|
|
32
29
|
beforeEach(() => {
|
|
33
30
|
runWorkflow.mockReset();
|
|
@@ -80,6 +77,7 @@ describe('trigger history is best-effort and never blocks triggering', () => {
|
|
|
80
77
|
});
|
|
81
78
|
|
|
82
79
|
const result = await fireManualSubscription({
|
|
80
|
+
workflows,
|
|
83
81
|
subscriptionId: subscription.id,
|
|
84
82
|
callerWorkspaceId: subscription.workspaceId,
|
|
85
83
|
userId: crypto.randomUUID(),
|
|
@@ -8,10 +8,6 @@ const listenerFilterErrored = vi.fn();
|
|
|
8
8
|
const listenerDispatchErrored = vi.fn();
|
|
9
9
|
const loggerWarn = vi.fn();
|
|
10
10
|
|
|
11
|
-
vi.mock('@shipfox/api-workflows', () => ({
|
|
12
|
-
deliverEventToListener: (...args: unknown[]) => deliverEventToListener(...args),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
11
|
vi.mock('@shipfox/node-opentelemetry', () => ({
|
|
16
12
|
logger: () => ({warn: loggerWarn}),
|
|
17
13
|
}));
|
|
@@ -33,6 +29,10 @@ vi.mock('#db/job-listener-subscriptions.js', async (importOriginal) => {
|
|
|
33
29
|
|
|
34
30
|
const {routeEventToJobListeners} = await import('./route-event-to-job-listeners.js');
|
|
35
31
|
|
|
32
|
+
const workflows = {
|
|
33
|
+
deliverEventToJobListener: (...args: unknown[]) => deliverEventToListener(...args),
|
|
34
|
+
} as never;
|
|
35
|
+
|
|
36
36
|
interface RouteOverrides {
|
|
37
37
|
eventRef?: string;
|
|
38
38
|
workspaceId?: string;
|
|
@@ -43,6 +43,7 @@ interface RouteOverrides {
|
|
|
43
43
|
|
|
44
44
|
function route(overrides: RouteOverrides = {}) {
|
|
45
45
|
return routeEventToJobListeners({
|
|
46
|
+
workflows,
|
|
46
47
|
history: buildHistory(),
|
|
47
48
|
eventRef: overrides.eventRef ?? crypto.randomUUID(),
|
|
48
49
|
workspaceId: overrides.workspaceId ?? crypto.randomUUID(),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {deliverEventToListener, type JobListenerEventDisposition} from '@shipfox/api-workflows';
|
|
2
1
|
import {logger} from '@shipfox/node-opentelemetry';
|
|
3
2
|
import {findMatchingJobListenerSubscriptions} from '#db/job-listener-subscriptions.js';
|
|
4
3
|
import {evaluateStoredFilter, type StoredFilterEvaluation} from './config.js';
|
|
5
4
|
import type {JobListenerSubscription} from './entities/job-listener-subscription.js';
|
|
6
5
|
import {type TriggerHistoryRecorder, toReason} from './record-trigger-history.js';
|
|
6
|
+
import type {WorkflowsModuleClient} from './workflows-client.js';
|
|
7
7
|
|
|
8
8
|
export interface RouteEventToJobListenersParams {
|
|
9
|
+
workflows: WorkflowsModuleClient;
|
|
9
10
|
history: TriggerHistoryRecorder;
|
|
10
11
|
eventRef: string;
|
|
11
12
|
workspaceId: string;
|
|
@@ -70,7 +71,7 @@ export async function routeEventToJobListeners(
|
|
|
70
71
|
for (const subscription of effectiveMatchByJobId.values()) {
|
|
71
72
|
const disposition = listenerDisposition(subscription);
|
|
72
73
|
try {
|
|
73
|
-
const result = await
|
|
74
|
+
const result = await params.workflows.deliverEventToJobListener({
|
|
74
75
|
jobId: subscription.jobId,
|
|
75
76
|
disposition,
|
|
76
77
|
eventRef: params.eventRef,
|
|
@@ -79,7 +80,7 @@ export async function routeEventToJobListeners(
|
|
|
79
80
|
event: params.event,
|
|
80
81
|
provider: params.provider,
|
|
81
82
|
payload: params.payload,
|
|
82
|
-
receivedAt: params.receivedAt,
|
|
83
|
+
receivedAt: params.receivedAt.toISOString(),
|
|
83
84
|
});
|
|
84
85
|
if (!result.skipped) {
|
|
85
86
|
acceptedJobCount += 1;
|
|
@@ -153,7 +154,7 @@ function readFilterSnapshot(
|
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
function listenerDisposition(subscription: JobListenerSubscription):
|
|
157
|
+
function listenerDisposition(subscription: JobListenerSubscription): 'fire' | 'resolve' {
|
|
157
158
|
return subscription.kind === 'until' ? 'resolve' : 'fire';
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type WorkflowsModuleClient,
|
|
3
|
+
workflowsInterModuleContract,
|
|
4
|
+
} from '@shipfox/api-workflows-dto/inter-module';
|
|
5
|
+
import {
|
|
6
|
+
createInterModuleClient,
|
|
7
|
+
createInterModuleKnownError,
|
|
8
|
+
defineInterModulePresentation,
|
|
9
|
+
} from '@shipfox/inter-module';
|
|
10
|
+
import {createFakeInterModuleClients} from '@shipfox/node-module/inter-module/testing';
|
|
11
|
+
import {isPermanentStartRunError} from './workflows-client.js';
|
|
12
|
+
|
|
13
|
+
const input = {
|
|
14
|
+
workspaceId: '00000000-0000-4000-8000-000000000001',
|
|
15
|
+
projectId: '00000000-0000-4000-8000-000000000002',
|
|
16
|
+
definitionId: '00000000-0000-4000-8000-000000000003',
|
|
17
|
+
triggerPayload: {
|
|
18
|
+
provider: 'github',
|
|
19
|
+
source: 'github',
|
|
20
|
+
event: 'push',
|
|
21
|
+
deliveryId: 'delivery-1',
|
|
22
|
+
data: {ref: 'refs/heads/main'},
|
|
23
|
+
},
|
|
24
|
+
idempotencyKey: 'subscription-1:event-1',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function localWorkflowsClient(): WorkflowsModuleClient {
|
|
28
|
+
return createFakeInterModuleClients({
|
|
29
|
+
workflows: defineInterModulePresentation(workflowsInterModuleContract, {
|
|
30
|
+
startRunFromTrigger: ({definitionId}) => {
|
|
31
|
+
if (definitionId.endsWith('0003')) return {id: definitionId, name: 'Build'};
|
|
32
|
+
throw createInterModuleKnownError(
|
|
33
|
+
workflowsInterModuleContract.methods.startRunFromTrigger,
|
|
34
|
+
'definition-not-found',
|
|
35
|
+
{definitionId},
|
|
36
|
+
);
|
|
37
|
+
},
|
|
38
|
+
deliverEventToJobListener: () => ({buffered: true, skipped: false}),
|
|
39
|
+
getStepLogContext: () => ({harness: 'pi' as const}),
|
|
40
|
+
getLeasedAgentToolContext: () => ({
|
|
41
|
+
workspaceId: '00000000-0000-4000-8000-000000000006',
|
|
42
|
+
integrations: [],
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
}).workflows;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function serializedWorkflowsClient(local: WorkflowsModuleClient): WorkflowsModuleClient {
|
|
49
|
+
return createInterModuleClient(workflowsInterModuleContract, async (call) => {
|
|
50
|
+
const copiedInput = JSON.parse(JSON.stringify(call.input)) as never;
|
|
51
|
+
const client = local as unknown as Record<string, (input: never) => Promise<unknown>>;
|
|
52
|
+
return await client[call.method]?.(copiedInput);
|
|
53
|
+
}) as WorkflowsModuleClient;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runConsumerSuite(client: WorkflowsModuleClient): Promise<void> {
|
|
57
|
+
await expect(client.startRunFromTrigger(input)).resolves.toEqual({
|
|
58
|
+
id: input.definitionId,
|
|
59
|
+
name: 'Build',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result = client.startRunFromTrigger({...input, definitionId: crypto.randomUUID()});
|
|
63
|
+
await expect(result).rejects.toSatisfy(isPermanentStartRunError);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('WorkflowsModuleClient consumer parity', () => {
|
|
67
|
+
test('keeps local and serialized clients equivalent for trigger consumers', async () => {
|
|
68
|
+
const local = localWorkflowsClient();
|
|
69
|
+
|
|
70
|
+
await runConsumerSuite(local);
|
|
71
|
+
await runConsumerSuite(serializedWorkflowsClient(local));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type WorkflowsModuleClient,
|
|
3
|
+
workflowsInterModuleContract,
|
|
4
|
+
} from '@shipfox/api-workflows-dto/inter-module';
|
|
5
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
6
|
+
|
|
7
|
+
export type {WorkflowsModuleClient};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Workflows declares only failures that can never succeed on retry for trigger
|
|
11
|
+
* run creation. Every other outcome is opaque and must remain retryable because
|
|
12
|
+
* it may have committed before the caller stopped waiting.
|
|
13
|
+
*/
|
|
14
|
+
export function isPermanentStartRunError(error: unknown): boolean {
|
|
15
|
+
return isInterModuleKnownError(workflowsInterModuleContract.methods.startRunFromTrigger, error);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isInterpolationUnresolvableError(
|
|
19
|
+
error: unknown,
|
|
20
|
+
): error is Extract<ReturnType<typeof startRunKnownError>, {code: 'interpolation-unresolvable'}> {
|
|
21
|
+
return (
|
|
22
|
+
isInterModuleKnownError(workflowsInterModuleContract.methods.startRunFromTrigger, error) &&
|
|
23
|
+
error.code === 'interpolation-unresolvable'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function startRunKnownError(error: unknown) {
|
|
28
|
+
if (!isInterModuleKnownError(workflowsInterModuleContract.methods.startRunFromTrigger, error)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return error;
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,14 +14,15 @@ import {
|
|
|
14
14
|
WORKFLOWS_JOB_TERMINATED,
|
|
15
15
|
type WorkflowsEventMapDto,
|
|
16
16
|
} from '@shipfox/api-workflows-dto';
|
|
17
|
+
import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
|
|
17
18
|
import {type ShipfoxModule, subscriberFactory} from '@shipfox/node-module';
|
|
18
19
|
import {db, migrationsPath, triggersOutbox} from '#db/index.js';
|
|
19
20
|
import {registerTriggersServiceMetrics} from '#metrics/index.js';
|
|
20
|
-
import {
|
|
21
|
+
import {createTriggerRoutes} from '#presentation/index.js';
|
|
21
22
|
import {
|
|
23
|
+
createOnIntegrationEventReceived,
|
|
22
24
|
onDefinitionDeleted,
|
|
23
25
|
onDefinitionResolved,
|
|
24
|
-
onIntegrationEventReceived,
|
|
25
26
|
onJobActivated,
|
|
26
27
|
onJobTerminated,
|
|
27
28
|
} from '#presentation/subscribers/index.js';
|
|
@@ -66,43 +67,49 @@ const subscriber = subscriberFactory<
|
|
|
66
67
|
DefinitionsEventMap & IntegrationsEventMap & WorkflowsEventMapDto
|
|
67
68
|
>();
|
|
68
69
|
|
|
69
|
-
export
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
70
|
+
export interface CreateTriggersModuleOptions {
|
|
71
|
+
workflows: WorkflowsModuleClient;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createTriggersModule({workflows}: CreateTriggersModuleOptions): ShipfoxModule {
|
|
75
|
+
return {
|
|
76
|
+
name: 'triggers',
|
|
77
|
+
database: {db, migrationsPath},
|
|
78
|
+
routes: createTriggerRoutes(workflows),
|
|
79
|
+
metrics: registerTriggersServiceMetrics,
|
|
80
|
+
publishers: [{name: 'triggers', table: triggersOutbox, db}],
|
|
81
|
+
subscribers: [
|
|
82
|
+
subscriber(DEFINITION_RESOLVED, onDefinitionResolved),
|
|
83
|
+
subscriber(DEFINITION_DELETED, onDefinitionDeleted),
|
|
84
|
+
subscriber(INTEGRATION_EVENT_RECEIVED, createOnIntegrationEventReceived(workflows)),
|
|
85
|
+
subscriber(WORKFLOWS_JOB_ACTIVATED, onJobActivated),
|
|
86
|
+
subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
|
|
87
|
+
],
|
|
88
|
+
workers: [
|
|
89
|
+
{
|
|
90
|
+
taskQueue: TRIGGERS_MAINTENANCE_TASK_QUEUE,
|
|
91
|
+
workflowsPath: temporalWorkflowsPath,
|
|
92
|
+
activities: createTriggersMaintenanceActivities,
|
|
93
|
+
workflows: [
|
|
94
|
+
{
|
|
95
|
+
name: 'pruneTriggerEventsCron',
|
|
96
|
+
id: 'triggers-prune-trigger-events',
|
|
97
|
+
cronSchedule: '0 * * * *',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
taskQueue: TRIGGERS_CRON_TASK_QUEUE,
|
|
103
|
+
workflowsPath: temporalWorkflowsPath,
|
|
104
|
+
activities: () => createTriggersCronActivities(workflows),
|
|
105
|
+
workflows: [
|
|
106
|
+
{
|
|
107
|
+
name: 'cronTickCron',
|
|
108
|
+
id: 'triggers-cron-tick',
|
|
109
|
+
cronSchedule: '* * * * *',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {createTriggerRoutes} from './routes/index.js';
|
|
2
2
|
export {
|
|
3
|
+
createOnIntegrationEventReceived,
|
|
3
4
|
onDefinitionDeleted,
|
|
4
5
|
onDefinitionResolved,
|
|
5
|
-
onIntegrationEventReceived,
|
|
6
6
|
} from './subscribers/index.js';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
type WorkflowsModuleClient,
|
|
4
|
+
workflowsInterModuleContract,
|
|
5
|
+
} from '@shipfox/api-workflows-dto/inter-module';
|
|
6
|
+
import {createInterModuleKnownError} from '@shipfox/inter-module';
|
|
3
7
|
import type {FastifyInstance} from 'fastify';
|
|
4
8
|
import Fastify from 'fastify';
|
|
5
9
|
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
|
|
@@ -7,33 +11,13 @@ import {triggerSubscriptionFactory} from '#test/index.js';
|
|
|
7
11
|
|
|
8
12
|
const fireManualSubscriptionMock = vi.hoisted(() => vi.fn());
|
|
9
13
|
|
|
10
|
-
vi.mock('@shipfox/api-workflows', () => {
|
|
11
|
-
class InterpolationUnresolvableError extends Error {
|
|
12
|
-
readonly field: string;
|
|
13
|
-
readonly source: string;
|
|
14
|
-
readonly envKey?: string;
|
|
15
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
definitionId: string,
|
|
18
|
-
params: {readonly field: string; readonly source: string; readonly envKey?: string},
|
|
19
|
-
) {
|
|
20
|
-
super(`Workflow interpolation cannot be resolved for definition ${definitionId}`);
|
|
21
|
-
this.name = 'InterpolationUnresolvableError';
|
|
22
|
-
this.field = params.field;
|
|
23
|
-
this.source = params.source;
|
|
24
|
-
if (params.envKey !== undefined) this.envKey = params.envKey;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return {InterpolationUnresolvableError};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
14
|
vi.mock('#core/fire-manual.js', () => ({
|
|
31
15
|
fireManualSubscription: fireManualSubscriptionMock,
|
|
32
16
|
}));
|
|
33
17
|
|
|
34
|
-
|
|
18
|
+
const {createFireManualTriggerRoute} = await import('./fire-manual.js');
|
|
35
19
|
|
|
36
|
-
const {
|
|
20
|
+
const workflows = {} as WorkflowsModuleClient;
|
|
37
21
|
|
|
38
22
|
describe('POST /:definitionId/fire-manual', () => {
|
|
39
23
|
let app: FastifyInstance;
|
|
@@ -51,7 +35,7 @@ describe('POST /:definitionId/fire-manual', () => {
|
|
|
51
35
|
);
|
|
52
36
|
done();
|
|
53
37
|
});
|
|
54
|
-
app.post('/:definitionId/fire-manual',
|
|
38
|
+
app.post('/:definitionId/fire-manual', createFireManualTriggerRoute(workflows));
|
|
55
39
|
await app.ready();
|
|
56
40
|
});
|
|
57
41
|
|
|
@@ -65,7 +49,7 @@ describe('POST /:definitionId/fire-manual', () => {
|
|
|
65
49
|
const definitionId = crypto.randomUUID();
|
|
66
50
|
const runId = crypto.randomUUID();
|
|
67
51
|
await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
|
|
68
|
-
fireManualSubscriptionMock.mockResolvedValue({id: runId
|
|
52
|
+
fireManualSubscriptionMock.mockResolvedValue({id: runId, name: 'Manual run'});
|
|
69
53
|
|
|
70
54
|
const res = await app.inject({
|
|
71
55
|
method: 'POST',
|
|
@@ -81,11 +65,11 @@ describe('POST /:definitionId/fire-manual', () => {
|
|
|
81
65
|
const definitionId = crypto.randomUUID();
|
|
82
66
|
await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
|
|
83
67
|
fireManualSubscriptionMock.mockRejectedValue(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
envKey: 'REF',
|
|
88
|
-
|
|
68
|
+
createInterModuleKnownError(
|
|
69
|
+
workflowsInterModuleContract.methods.startRunFromTrigger,
|
|
70
|
+
'interpolation-unresolvable',
|
|
71
|
+
{definitionId, field: 'env', source: 'event.ref', envKey: 'REF'},
|
|
72
|
+
),
|
|
89
73
|
);
|
|
90
74
|
|
|
91
75
|
const res = await app.inject({
|