@shipfox/api-triggers 13.0.0 → 14.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.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +14 -10
  4. package/dist/core/entities/decision.d.ts +3 -2
  5. package/dist/core/entities/decision.d.ts.map +1 -1
  6. package/dist/core/entities/decision.js.map +1 -1
  7. package/dist/core/entities/job-listener-subscription.d.ts +2 -1
  8. package/dist/core/entities/job-listener-subscription.d.ts.map +1 -1
  9. package/dist/core/entities/job-listener-subscription.js.map +1 -1
  10. package/dist/core/entities/received-event.d.ts +14 -1
  11. package/dist/core/entities/received-event.d.ts.map +1 -1
  12. package/dist/core/entities/received-event.js +2 -1
  13. package/dist/core/entities/received-event.js.map +1 -1
  14. package/dist/core/entities/subscription.d.ts +2 -1
  15. package/dist/core/entities/subscription.d.ts.map +1 -1
  16. package/dist/core/entities/subscription.js.map +1 -1
  17. package/dist/core/fire-cron.d.ts.map +1 -1
  18. package/dist/core/fire-cron.js +3 -1
  19. package/dist/core/fire-cron.js.map +1 -1
  20. package/dist/core/fire-manual.d.ts.map +1 -1
  21. package/dist/core/fire-manual.js +3 -1
  22. package/dist/core/fire-manual.js.map +1 -1
  23. package/dist/core/record-trigger-history.d.ts +2 -0
  24. package/dist/core/record-trigger-history.d.ts.map +1 -1
  25. package/dist/core/record-trigger-history.js +13 -1
  26. package/dist/core/record-trigger-history.js.map +1 -1
  27. package/dist/core/route-event-to-job-listeners.js +2 -0
  28. package/dist/core/route-event-to-job-listeners.js.map +1 -1
  29. package/dist/db/db.d.ts +48 -14
  30. package/dist/db/db.d.ts.map +1 -1
  31. package/dist/db/event-history.d.ts +17 -0
  32. package/dist/db/event-history.d.ts.map +1 -1
  33. package/dist/db/event-history.js +45 -1
  34. package/dist/db/event-history.js.map +1 -1
  35. package/dist/db/event-queries.d.ts +5 -1
  36. package/dist/db/event-queries.d.ts.map +1 -1
  37. package/dist/db/event-queries.js +30 -4
  38. package/dist/db/event-queries.js.map +1 -1
  39. package/dist/db/index.d.ts +1 -1
  40. package/dist/db/index.d.ts.map +1 -1
  41. package/dist/db/index.js +1 -1
  42. package/dist/db/index.js.map +1 -1
  43. package/dist/db/job-listener-subscriptions.d.ts +1 -1
  44. package/dist/db/job-listener-subscriptions.d.ts.map +1 -1
  45. package/dist/db/job-listener-subscriptions.js +10 -3
  46. package/dist/db/job-listener-subscriptions.js.map +1 -1
  47. package/dist/db/schema/decisions.d.ts +3 -3
  48. package/dist/db/schema/decisions.d.ts.map +1 -1
  49. package/dist/db/schema/decisions.js +8 -3
  50. package/dist/db/schema/decisions.js.map +1 -1
  51. package/dist/db/schema/job-listener-subscriptions.d.ts +1 -1
  52. package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -1
  53. package/dist/db/schema/job-listener-subscriptions.js +2 -1
  54. package/dist/db/schema/job-listener-subscriptions.js.map +1 -1
  55. package/dist/db/schema/received-events.d.ts +38 -4
  56. package/dist/db/schema/received-events.d.ts.map +1 -1
  57. package/dist/db/schema/received-events.js +11 -2
  58. package/dist/db/schema/received-events.js.map +1 -1
  59. package/dist/db/schema/subscriptions.d.ts +1 -1
  60. package/dist/db/schema/subscriptions.d.ts.map +1 -1
  61. package/dist/db/schema/subscriptions.js +2 -1
  62. package/dist/db/schema/subscriptions.js.map +1 -1
  63. package/dist/db/subscription-event.d.ts +7 -0
  64. package/dist/db/subscription-event.d.ts.map +1 -0
  65. package/dist/db/subscription-event.js +19 -0
  66. package/dist/db/subscription-event.js.map +1 -0
  67. package/dist/db/subscriptions.d.ts +5 -1
  68. package/dist/db/subscriptions.d.ts.map +1 -1
  69. package/dist/db/subscriptions.js +10 -3
  70. package/dist/db/subscriptions.js.map +1 -1
  71. package/dist/presentation/dto/trigger-events.d.ts +3 -2
  72. package/dist/presentation/dto/trigger-events.d.ts.map +1 -1
  73. package/dist/presentation/dto/trigger-events.js +9 -0
  74. package/dist/presentation/dto/trigger-events.js.map +1 -1
  75. package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -1
  76. package/dist/presentation/routes/get-trigger-event.js +8 -4
  77. package/dist/presentation/routes/get-trigger-event.js.map +1 -1
  78. package/dist/presentation/routes/list-trigger-event-facets.js +1 -1
  79. package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -1
  80. package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -1
  81. package/dist/presentation/routes/list-trigger-events.js +3 -1
  82. package/dist/presentation/routes/list-trigger-events.js.map +1 -1
  83. package/dist/tsconfig.test.tsbuildinfo +1 -1
  84. package/drizzle/0001_nullable_subscription_events.sql +2 -0
  85. package/drizzle/0002_dev_trigger_journal.sql +7 -0
  86. package/drizzle/0003_add_origin_facet_index.sql +1 -0
  87. package/drizzle/meta/0001_snapshot.json +905 -0
  88. package/drizzle/meta/0002_snapshot.json +947 -0
  89. package/drizzle/meta/0003_snapshot.json +968 -0
  90. package/drizzle/meta/_journal.json +21 -0
  91. package/package.json +7 -7
  92. package/src/core/dispatch-integration-event.test.ts +88 -0
  93. package/src/core/entities/decision.ts +3 -2
  94. package/src/core/entities/job-listener-subscription.ts +2 -1
  95. package/src/core/entities/received-event.ts +15 -1
  96. package/src/core/entities/subscription.ts +2 -1
  97. package/src/core/fire-cron.test.ts +20 -0
  98. package/src/core/fire-cron.ts +3 -1
  99. package/src/core/fire-manual.test.ts +25 -0
  100. package/src/core/fire-manual.ts +3 -1
  101. package/src/core/record-trigger-history.test.ts +78 -0
  102. package/src/core/record-trigger-history.ts +19 -0
  103. package/src/core/route-event-to-job-listeners.test.ts +47 -0
  104. package/src/core/route-event-to-job-listeners.ts +2 -0
  105. package/src/db/event-history.test.ts +123 -0
  106. package/src/db/event-history.ts +84 -1
  107. package/src/db/event-queries.ts +56 -3
  108. package/src/db/index.ts +1 -0
  109. package/src/db/job-listener-subscriptions.test.ts +133 -1
  110. package/src/db/job-listener-subscriptions.ts +5 -4
  111. package/src/db/schema/decisions.test.ts +93 -4
  112. package/src/db/schema/decisions.ts +12 -3
  113. package/src/db/schema/job-listener-subscriptions.ts +2 -1
  114. package/src/db/schema/received-events.test.ts +45 -0
  115. package/src/db/schema/received-events.ts +12 -1
  116. package/src/db/schema/subscriptions.ts +2 -1
  117. package/src/db/subscription-event.ts +24 -0
  118. package/src/db/subscriptions.test.ts +137 -0
  119. package/src/db/subscriptions.ts +9 -4
  120. package/src/presentation/dto/trigger-events.test.ts +193 -0
  121. package/src/presentation/dto/trigger-events.ts +12 -0
  122. package/src/presentation/routes/get-trigger-event.test.ts +179 -0
  123. package/src/presentation/routes/get-trigger-event.ts +15 -3
  124. package/src/presentation/routes/list-trigger-event-facets.test.ts +39 -2
  125. package/src/presentation/routes/list-trigger-event-facets.ts +1 -1
  126. package/src/presentation/routes/list-trigger-events.test.ts +99 -0
  127. package/src/presentation/routes/list-trigger-events.ts +4 -0
  128. package/test/factories/received-event.ts +2 -0
  129. package/tsconfig.build.tsbuildinfo +1 -1
@@ -2,8 +2,16 @@ import {requireWorkspaceResourceAccess} from '@shipfox/api-auth-context';
2
2
  import {triggerEventDetailResponseSchema} from '@shipfox/api-triggers-dto';
3
3
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
4
  import {z} from 'zod';
5
- import {getTriggerEventById, listDecisionsByReceivedEventId} from '#db/index.js';
6
- import {toTriggerDecisionDto, toTriggerEventDto} from '#presentation/dto/trigger-events.js';
5
+ import {
6
+ getTriggerEventById,
7
+ listDecisionsByReceivedEventId,
8
+ listReplaysOfTriggerEvent,
9
+ } from '#db/index.js';
10
+ import {
11
+ toTriggerDecisionDto,
12
+ toTriggerEventDto,
13
+ toTriggerEventReplayDto,
14
+ } from '#presentation/dto/trigger-events.js';
7
15
 
8
16
  export const getTriggerEventRoute = defineRoute({
9
17
  method: 'GET',
@@ -32,11 +40,15 @@ export const getTriggerEventRoute = defineRoute({
32
40
  notFoundError: new ClientError('Trigger event not found', 'not-found', {status: 404}),
33
41
  });
34
42
 
35
- const decisions = await listDecisionsByReceivedEventId(event.id);
43
+ const [decisions, replays] = await Promise.all([
44
+ listDecisionsByReceivedEventId(event.id),
45
+ listReplaysOfTriggerEvent(event.id, event.workspaceId),
46
+ ]);
36
47
 
37
48
  return {
38
49
  ...toTriggerEventDto(event),
39
50
  decisions: decisions.map(toTriggerDecisionDto),
51
+ replays: replays.map(toTriggerEventReplayDto),
40
52
  };
41
53
  },
42
54
  });
@@ -7,7 +7,11 @@ import {receivedEventFactory} from '#test/index.js';
7
7
  import {listTriggerEventFacetsRoute} from './list-trigger-event-facets.js';
8
8
 
9
9
  const facets = (res: {
10
- json: () => {sources: TriggerEventFacetItemDto[]; events: TriggerEventFacetItemDto[]};
10
+ json: () => {
11
+ sources: TriggerEventFacetItemDto[];
12
+ events: TriggerEventFacetItemDto[];
13
+ origins: TriggerEventFacetItemDto[];
14
+ };
11
15
  }) => res.json();
12
16
 
13
17
  describe('GET /trigger-events/facets', () => {
@@ -60,6 +64,39 @@ describe('GET /trigger-events/facets', () => {
60
64
  ]);
61
65
  });
62
66
 
67
+ test('returns distinct origins with counts, ordered by count desc', async () => {
68
+ await receivedEventFactory.create({workspaceId, source: 'github', event: 'push'});
69
+ await receivedEventFactory.create({
70
+ workspaceId,
71
+ origin: 'dev',
72
+ source: 'github',
73
+ event: 'push',
74
+ });
75
+ await receivedEventFactory.create({
76
+ workspaceId,
77
+ origin: 'dev',
78
+ source: 'github',
79
+ event: 'push',
80
+ });
81
+ await receivedEventFactory.create({
82
+ workspaceId,
83
+ origin: 'manual',
84
+ source: 'manual',
85
+ event: 'fire',
86
+ });
87
+
88
+ const res = await app.inject({
89
+ method: 'GET',
90
+ url: `/trigger-events/facets?workspace_id=${workspaceId}`,
91
+ });
92
+
93
+ expect(facets(res).origins).toEqual([
94
+ {value: 'dev', count: 2},
95
+ {value: 'integration', count: 1},
96
+ {value: 'manual', count: 1},
97
+ ]);
98
+ });
99
+
63
100
  test('caps each facet at the top 50 values by count', async () => {
64
101
  await Promise.all(
65
102
  Array.from({length: 51}, (_, index) =>
@@ -94,7 +131,7 @@ describe('GET /trigger-events/facets', () => {
94
131
  });
95
132
 
96
133
  expect(res.statusCode).toBe(200);
97
- expect(res.json()).toEqual({sources: [], events: []});
134
+ expect(res.json()).toEqual({sources: [], events: [], origins: []});
98
135
  });
99
136
 
100
137
  test('returns workspace-suspended for a suspended membership claim', async () => {
@@ -9,7 +9,7 @@ import {listTriggerEventFacets} from '#db/index.js';
9
9
  export const listTriggerEventFacetsRoute = defineRoute({
10
10
  method: 'GET',
11
11
  path: '/facets',
12
- description: 'Distinct source and event filter values (with counts) for a workspace.',
12
+ description: 'Distinct source, event, and origin filter values (with counts) for a workspace.',
13
13
  schema: {
14
14
  querystring: triggerEventFacetsQuerySchema,
15
15
  response: {
@@ -171,6 +171,105 @@ describe('GET /trigger-events', () => {
171
171
  expect(eventIds(res)).toEqual([discarded.id]);
172
172
  });
173
173
 
174
+ test('filters by origin', async () => {
175
+ const integration = await receivedEventFactory.create({workspaceId, origin: 'integration'});
176
+ await receivedEventFactory.create({workspaceId, origin: 'manual'});
177
+
178
+ const res = await app.inject({
179
+ method: 'GET',
180
+ url: `/trigger-events?workspace_id=${workspaceId}&origin=integration`,
181
+ });
182
+
183
+ expect(eventIds(res)).toEqual([integration.id]);
184
+ });
185
+
186
+ test('filters by repeated origin keys (IN-list)', async () => {
187
+ const dev = await receivedEventFactory.create({workspaceId, origin: 'dev'});
188
+ const manual = await receivedEventFactory.create({workspaceId, origin: 'manual'});
189
+ await receivedEventFactory.create({workspaceId, origin: 'integration'});
190
+
191
+ const res = await app.inject({
192
+ method: 'GET',
193
+ url: `/trigger-events?workspace_id=${workspaceId}&origin=dev&origin=manual`,
194
+ });
195
+
196
+ expect(eventIds(res).sort()).toEqual([dev.id, manual.id].sort());
197
+ });
198
+
199
+ test('filters by comma-separated origins', async () => {
200
+ const dev = await receivedEventFactory.create({workspaceId, origin: 'dev'});
201
+ const manual = await receivedEventFactory.create({workspaceId, origin: 'manual'});
202
+ await receivedEventFactory.create({workspaceId, origin: 'integration'});
203
+
204
+ const res = await app.inject({
205
+ method: 'GET',
206
+ url: `/trigger-events?workspace_id=${workspaceId}&origin=dev,manual`,
207
+ });
208
+
209
+ expect(eventIds(res).sort()).toEqual([dev.id, manual.id].sort());
210
+ });
211
+
212
+ test('rejects an unknown origin value', async () => {
213
+ const res = await app.inject({
214
+ method: 'GET',
215
+ url: `/trigger-events?workspace_id=${workspaceId}&origin=bogus`,
216
+ });
217
+
218
+ expect(res.statusCode).toBe(400);
219
+ });
220
+
221
+ test('filters replayable events to integration events with a stored payload', async () => {
222
+ const replayable = await receivedEventFactory.create({
223
+ workspaceId,
224
+ origin: 'integration',
225
+ payload: {ref: 'refs/heads/main'},
226
+ });
227
+ // A pruned integration event keeps its origin but lost its payload.
228
+ await receivedEventFactory.create({workspaceId, origin: 'integration', payload: null});
229
+ // A dev replay row carries a payload but is not itself replayable.
230
+ await receivedEventFactory.create({workspaceId, origin: 'dev', payload: {ref: 'main'}});
231
+ await receivedEventFactory.create({workspaceId, origin: 'manual', payload: {}});
232
+
233
+ const res = await app.inject({
234
+ method: 'GET',
235
+ url: `/trigger-events?workspace_id=${workspaceId}&replayable=true`,
236
+ });
237
+
238
+ expect(res.statusCode).toBe(200);
239
+ expect(eventIds(res)).toEqual([replayable.id]);
240
+ });
241
+
242
+ test('rejects a non-true replayable value', async () => {
243
+ const res = await app.inject({
244
+ method: 'GET',
245
+ url: `/trigger-events?workspace_id=${workspaceId}&replayable=false`,
246
+ });
247
+
248
+ expect(res.statusCode).toBe(400);
249
+ });
250
+
251
+ test('exposes replay_of_event_id on list items', async () => {
252
+ const source = await receivedEventFactory.create({workspaceId});
253
+ const replay = await receivedEventFactory.create({
254
+ workspaceId,
255
+ origin: 'dev',
256
+ source: 'github',
257
+ event: 'push',
258
+ replayOfEventId: source.id,
259
+ });
260
+
261
+ const res = await app.inject({
262
+ method: 'GET',
263
+ url: `/trigger-events?workspace_id=${workspaceId}`,
264
+ });
265
+
266
+ const items = res.json().trigger_events;
267
+ expect(items.find((item: {id: string}) => item.id === replay.id).replay_of_event_id).toBe(
268
+ source.id,
269
+ );
270
+ expect(items.find((item: {id: string}) => item.id === source.id).replay_of_event_id).toBeNull();
271
+ });
272
+
174
273
  test('filters and serializes the errored outcome', async () => {
175
274
  const errored = await receivedEventFactory.create({workspaceId, outcome: 'errored'});
176
275
 
@@ -23,7 +23,9 @@ export const listTriggerEventsRoute = defineRoute({
23
23
  workspace_id: workspaceId,
24
24
  source,
25
25
  event,
26
+ origin: origins,
26
27
  outcome: outcomes,
28
+ replayable,
27
29
  from,
28
30
  to,
29
31
  limit,
@@ -46,7 +48,9 @@ export const listTriggerEventsRoute = defineRoute({
46
48
  filters: {
47
49
  source,
48
50
  event,
51
+ origins,
49
52
  outcomes,
53
+ replayable,
50
54
  from: from ? new Date(from) : undefined,
51
55
  to: to ? new Date(to) : undefined,
52
56
  },
@@ -14,6 +14,7 @@ export const receivedEventFactory = Factory.define<TriggerReceivedEvent>(({seque
14
14
  provider: event.provider,
15
15
  source: event.source,
16
16
  event: event.event,
17
+ replayOfEventId: event.replayOfEventId,
17
18
  deliveryId: event.deliveryId,
18
19
  connectionId: event.connectionId,
19
20
  connectionName: event.connectionName,
@@ -36,6 +37,7 @@ export const receivedEventFactory = Factory.define<TriggerReceivedEvent>(({seque
36
37
  provider: 'github',
37
38
  source: 'github',
38
39
  event: 'push',
40
+ replayOfEventId: null,
39
41
  deliveryId: crypto.randomUUID(),
40
42
  connectionId: crypto.randomUUID(),
41
43
  connectionName: 'Acme Production',