@shipfox/api-triggers 5.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +51 -0
  3. package/README.md +23 -3
  4. package/dist/core/dispatch-integration-event.d.ts +2 -0
  5. package/dist/core/dispatch-integration-event.d.ts.map +1 -1
  6. package/dist/core/dispatch-integration-event.js +9 -5
  7. package/dist/core/dispatch-integration-event.js.map +1 -1
  8. package/dist/core/drain-cron-schedules.d.ts +2 -0
  9. package/dist/core/drain-cron-schedules.d.ts.map +1 -1
  10. package/dist/core/drain-cron-schedules.js +1 -0
  11. package/dist/core/drain-cron-schedules.js.map +1 -1
  12. package/dist/core/fire-cron.d.ts +6 -2
  13. package/dist/core/fire-cron.d.ts.map +1 -1
  14. package/dist/core/fire-cron.js +8 -5
  15. package/dist/core/fire-cron.js.map +1 -1
  16. package/dist/core/fire-manual.d.ts +6 -2
  17. package/dist/core/fire-manual.d.ts.map +1 -1
  18. package/dist/core/fire-manual.js +8 -4
  19. package/dist/core/fire-manual.js.map +1 -1
  20. package/dist/core/route-event-to-job-listeners.d.ts +2 -0
  21. package/dist/core/route-event-to-job-listeners.d.ts.map +1 -1
  22. package/dist/core/route-event-to-job-listeners.js +2 -3
  23. package/dist/core/route-event-to-job-listeners.js.map +1 -1
  24. package/dist/core/workflows-client.d.ts +69 -0
  25. package/dist/core/workflows-client.d.ts.map +1 -0
  26. package/dist/core/workflows-client.js +20 -0
  27. package/dist/core/workflows-client.js.map +1 -0
  28. package/dist/db/job-listener-subscriptions.d.ts +1 -0
  29. package/dist/db/job-listener-subscriptions.d.ts.map +1 -1
  30. package/dist/db/job-listener-subscriptions.js +6 -0
  31. package/dist/db/job-listener-subscriptions.js.map +1 -1
  32. package/dist/index.d.ts +5 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +56 -50
  35. package/dist/index.js.map +1 -1
  36. package/dist/presentation/e2e-routes.d.ts +3 -0
  37. package/dist/presentation/e2e-routes.d.ts.map +1 -0
  38. package/dist/presentation/e2e-routes.js +31 -0
  39. package/dist/presentation/e2e-routes.js.map +1 -0
  40. package/dist/presentation/index.d.ts +2 -2
  41. package/dist/presentation/index.d.ts.map +1 -1
  42. package/dist/presentation/index.js +2 -2
  43. package/dist/presentation/index.js.map +1 -1
  44. package/dist/presentation/routes/fire-manual.d.ts +2 -1
  45. package/dist/presentation/routes/fire-manual.d.ts.map +1 -1
  46. package/dist/presentation/routes/fire-manual.js +61 -58
  47. package/dist/presentation/routes/fire-manual.js.map +1 -1
  48. package/dist/presentation/routes/index.d.ts +2 -1
  49. package/dist/presentation/routes/index.d.ts.map +1 -1
  50. package/dist/presentation/routes/index.js +23 -21
  51. package/dist/presentation/routes/index.js.map +1 -1
  52. package/dist/presentation/subscribers/index.d.ts +1 -1
  53. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  54. package/dist/presentation/subscribers/index.js +1 -1
  55. package/dist/presentation/subscribers/index.js.map +1 -1
  56. package/dist/presentation/subscribers/on-integration-event-received.d.ts +2 -1
  57. package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -1
  58. package/dist/presentation/subscribers/on-integration-event-received.js +16 -13
  59. package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -1
  60. package/dist/temporal/activities/drain-cron-batch.d.ts +2 -1
  61. package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -1
  62. package/dist/temporal/activities/drain-cron-batch.js +2 -1
  63. package/dist/temporal/activities/drain-cron-batch.js.map +1 -1
  64. package/dist/temporal/activities/index.d.ts +4 -3
  65. package/dist/temporal/activities/index.d.ts.map +1 -1
  66. package/dist/temporal/activities/index.js +2 -2
  67. package/dist/temporal/activities/index.js.map +1 -1
  68. package/dist/tsconfig.test.tsbuildinfo +1 -1
  69. package/package.json +14 -12
  70. package/src/core/dispatch-integration-event.history-failure.test.ts +11 -6
  71. package/src/core/dispatch-integration-event.test.ts +50 -36
  72. package/src/core/dispatch-integration-event.ts +8 -5
  73. package/src/core/drain-cron-schedules.test.ts +30 -29
  74. package/src/core/drain-cron-schedules.ts +3 -0
  75. package/src/core/fire-cron.test.ts +35 -24
  76. package/src/core/fire-cron.ts +9 -7
  77. package/src/core/fire-manual.test.ts +19 -22
  78. package/src/core/fire-manual.ts +9 -6
  79. package/src/core/record-trigger-history.test.ts +3 -5
  80. package/src/core/route-event-to-job-listeners.test.ts +5 -4
  81. package/src/core/route-event-to-job-listeners.ts +5 -4
  82. package/src/core/workflows-client.test.ts +73 -0
  83. package/src/core/workflows-client.ts +32 -0
  84. package/src/db/job-listener-subscriptions.ts +9 -0
  85. package/src/index.ts +51 -42
  86. package/src/presentation/e2e-routes.test.ts +44 -0
  87. package/src/presentation/e2e-routes.ts +24 -0
  88. package/src/presentation/index.ts +2 -2
  89. package/src/presentation/routes/fire-manual.test.ts +14 -30
  90. package/src/presentation/routes/fire-manual.ts +62 -54
  91. package/src/presentation/routes/index.ts +18 -15
  92. package/src/presentation/subscribers/index.ts +1 -1
  93. package/src/presentation/subscribers/on-integration-event-received.test.ts +6 -3
  94. package/src/presentation/subscribers/on-integration-event-received.ts +20 -16
  95. package/src/temporal/activities/drain-cron-batch.ts +5 -1
  96. package/src/temporal/activities/index.ts +3 -2
  97. package/tsconfig.build.tsbuildinfo +1 -1
@@ -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(new DefinitionNotFoundError('def-gone'));
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('Definition not found');
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('Definition not found');
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(),
@@ -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<WorkflowRun> {
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
- let run: WorkflowRun;
59
+ const inputs = params.inputs ?? readConfigInputs(subscription);
60
+ let run: {id: string; name: string};
59
61
  try {
60
- run = await runWorkflow({
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: params.inputs ?? readConfigInputs(subscription),
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 (isPermanentRunWorkflowError(error)) {
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 deliverEventToListener({
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): JobListenerEventDisposition {
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
+ }
@@ -131,3 +131,12 @@ export async function findMatchingJobListenerSubscriptions(
131
131
  );
132
132
  return rows.map(toJobListenerSubscription);
133
133
  }
134
+
135
+ export async function hasJobListenerSubscriptions(jobId: string): Promise<boolean> {
136
+ const [subscription] = await db()
137
+ .select({id: jobListenerSubscriptions.id})
138
+ .from(jobListenerSubscriptions)
139
+ .where(eq(jobListenerSubscriptions.jobId, jobId))
140
+ .limit(1);
141
+ return subscription !== undefined;
142
+ }
package/src/index.ts CHANGED
@@ -14,14 +14,16 @@ 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 {routes} from '#presentation/index.js';
21
+ import {triggersE2eRoutes} from '#presentation/e2e-routes.js';
22
+ import {createTriggerRoutes} from '#presentation/index.js';
21
23
  import {
24
+ createOnIntegrationEventReceived,
22
25
  onDefinitionDeleted,
23
26
  onDefinitionResolved,
24
- onIntegrationEventReceived,
25
27
  onJobActivated,
26
28
  onJobTerminated,
27
29
  } from '#presentation/subscribers/index.js';
@@ -66,43 +68,50 @@ const subscriber = subscriberFactory<
66
68
  DefinitionsEventMap & IntegrationsEventMap & WorkflowsEventMapDto
67
69
  >();
68
70
 
69
- export const triggersModule: ShipfoxModule = {
70
- name: 'triggers',
71
- database: {db, migrationsPath},
72
- routes,
73
- metrics: registerTriggersServiceMetrics,
74
- publishers: [{name: 'triggers', table: triggersOutbox, db}],
75
- subscribers: [
76
- subscriber(DEFINITION_RESOLVED, onDefinitionResolved),
77
- subscriber(DEFINITION_DELETED, onDefinitionDeleted),
78
- subscriber(INTEGRATION_EVENT_RECEIVED, onIntegrationEventReceived),
79
- subscriber(WORKFLOWS_JOB_ACTIVATED, onJobActivated),
80
- subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
81
- ],
82
- workers: [
83
- {
84
- taskQueue: TRIGGERS_MAINTENANCE_TASK_QUEUE,
85
- workflowsPath: temporalWorkflowsPath,
86
- activities: createTriggersMaintenanceActivities,
87
- workflows: [
88
- {
89
- name: 'pruneTriggerEventsCron',
90
- id: 'triggers-prune-trigger-events',
91
- cronSchedule: '0 * * * *',
92
- },
93
- ],
94
- },
95
- {
96
- taskQueue: TRIGGERS_CRON_TASK_QUEUE,
97
- workflowsPath: temporalWorkflowsPath,
98
- activities: createTriggersCronActivities,
99
- workflows: [
100
- {
101
- name: 'cronTickCron',
102
- id: 'triggers-cron-tick',
103
- cronSchedule: '* * * * *',
104
- },
105
- ],
106
- },
107
- ],
108
- };
71
+ export interface CreateTriggersModuleOptions {
72
+ workflows: WorkflowsModuleClient;
73
+ }
74
+
75
+ export function createTriggersModule({workflows}: CreateTriggersModuleOptions): ShipfoxModule {
76
+ return {
77
+ name: 'triggers',
78
+ database: {db, migrationsPath},
79
+ routes: createTriggerRoutes(workflows),
80
+ e2eRoutes: [triggersE2eRoutes],
81
+ metrics: registerTriggersServiceMetrics,
82
+ publishers: [{name: 'triggers', table: triggersOutbox, db}],
83
+ subscribers: [
84
+ subscriber(DEFINITION_RESOLVED, onDefinitionResolved),
85
+ subscriber(DEFINITION_DELETED, onDefinitionDeleted),
86
+ subscriber(INTEGRATION_EVENT_RECEIVED, createOnIntegrationEventReceived(workflows)),
87
+ subscriber(WORKFLOWS_JOB_ACTIVATED, onJobActivated),
88
+ subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
89
+ ],
90
+ workers: [
91
+ {
92
+ taskQueue: TRIGGERS_MAINTENANCE_TASK_QUEUE,
93
+ workflowsPath: temporalWorkflowsPath,
94
+ activities: createTriggersMaintenanceActivities,
95
+ workflows: [
96
+ {
97
+ name: 'pruneTriggerEventsCron',
98
+ id: 'triggers-prune-trigger-events',
99
+ cronSchedule: '0 * * * *',
100
+ },
101
+ ],
102
+ },
103
+ {
104
+ taskQueue: TRIGGERS_CRON_TASK_QUEUE,
105
+ workflowsPath: temporalWorkflowsPath,
106
+ activities: () => createTriggersCronActivities(workflows),
107
+ workflows: [
108
+ {
109
+ name: 'cronTickCron',
110
+ id: 'triggers-cron-tick',
111
+ cronSchedule: '* * * * *',
112
+ },
113
+ ],
114
+ },
115
+ ],
116
+ };
117
+ }
@@ -0,0 +1,44 @@
1
+ import {closeApp, createApp} from '@shipfox/node-fastify';
2
+ import {projectJobListenerSubscriptions} from '#db/job-listener-subscriptions.js';
3
+ import {triggersE2eRoutes} from './e2e-routes.js';
4
+
5
+ describe('triggers E2E routes', () => {
6
+ afterEach(async () => {
7
+ await closeApp();
8
+ });
9
+
10
+ test('reports a listener without projected subscriptions as not ready', async () => {
11
+ const jobId = crypto.randomUUID();
12
+ const app = await createApp({routes: [triggersE2eRoutes], swagger: false});
13
+
14
+ const response = await app.inject({
15
+ method: 'GET',
16
+ url: `/triggers/listeners/${jobId}/readiness`,
17
+ });
18
+
19
+ expect(response.statusCode).toBe(200);
20
+ expect(response.json()).toEqual({ready: false});
21
+ });
22
+
23
+ test('reports a listener with projected subscriptions as ready', async () => {
24
+ const workspaceId = crypto.randomUUID();
25
+ const workflowRunId = crypto.randomUUID();
26
+ const jobId = crypto.randomUUID();
27
+ await projectJobListenerSubscriptions({
28
+ workspaceId,
29
+ workflowRunId,
30
+ jobId,
31
+ on: [{source: 'listener-source', event: 'received'}],
32
+ until: null,
33
+ });
34
+ const app = await createApp({routes: [triggersE2eRoutes], swagger: false});
35
+
36
+ const response = await app.inject({
37
+ method: 'GET',
38
+ url: `/triggers/listeners/${jobId}/readiness`,
39
+ });
40
+
41
+ expect(response.statusCode).toBe(200);
42
+ expect(response.json()).toEqual({ready: true});
43
+ });
44
+ });
@@ -0,0 +1,24 @@
1
+ import {defineRoute, type RouteGroup} from '@shipfox/node-fastify';
2
+ import {z} from 'zod';
3
+ import {hasJobListenerSubscriptions} from '#db/job-listener-subscriptions.js';
4
+
5
+ const listenerReadinessParamsSchema = z.object({jobId: z.string().uuid()});
6
+ const listenerReadinessResponseSchema = z.object({ready: z.boolean()});
7
+
8
+ const listenerReadinessRoute = defineRoute({
9
+ method: 'GET',
10
+ path: '/listeners/:jobId/readiness',
11
+ description: 'Report whether trigger subscriptions for a listener job are ready in E2E tests.',
12
+ schema: {
13
+ params: listenerReadinessParamsSchema,
14
+ response: {200: listenerReadinessResponseSchema},
15
+ },
16
+ handler: async (request) => ({
17
+ ready: await hasJobListenerSubscriptions(request.params.jobId),
18
+ }),
19
+ });
20
+
21
+ export const triggersE2eRoutes: RouteGroup = {
22
+ prefix: '/triggers',
23
+ routes: [listenerReadinessRoute],
24
+ };
@@ -1,6 +1,6 @@
1
- export {triggerRoutes as routes} from './routes/index.js';
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 type {WorkflowRun} from '@shipfox/api-workflows';
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
- import {InterpolationUnresolvableError} from '@shipfox/api-workflows';
18
+ const {createFireManualTriggerRoute} = await import('./fire-manual.js');
35
19
 
36
- const {fireManualTriggerRoute} = await import('./fire-manual.js');
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', fireManualTriggerRoute);
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} satisfies Pick<WorkflowRun, 'id'>);
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
- new InterpolationUnresolvableError(definitionId, {
85
- field: 'env',
86
- source: 'event.ref',
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({