@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.
Files changed (331) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +224 -0
  5. package/LICENSE +21 -0
  6. package/README.md +300 -0
  7. package/dist/config.d.ts +10 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +42 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/core/compute-next-fire-at.d.ts +9 -0
  12. package/dist/core/compute-next-fire-at.d.ts.map +1 -0
  13. package/dist/core/compute-next-fire-at.js +28 -0
  14. package/dist/core/compute-next-fire-at.js.map +1 -0
  15. package/dist/core/config.d.ts +26 -0
  16. package/dist/core/config.d.ts.map +1 -0
  17. package/dist/core/config.js +68 -0
  18. package/dist/core/config.js.map +1 -0
  19. package/dist/core/dispatch-integration-event.d.ts +14 -0
  20. package/dist/core/dispatch-integration-event.d.ts.map +1 -0
  21. package/dist/core/dispatch-integration-event.js +136 -0
  22. package/dist/core/dispatch-integration-event.js.map +1 -0
  23. package/dist/core/drain-cron-schedules.d.ts +30 -0
  24. package/dist/core/drain-cron-schedules.d.ts.map +1 -0
  25. package/dist/core/drain-cron-schedules.js +76 -0
  26. package/dist/core/drain-cron-schedules.js.map +1 -0
  27. package/dist/core/entities/cron-schedule.d.ts +11 -0
  28. package/dist/core/entities/cron-schedule.d.ts.map +1 -0
  29. package/dist/core/entities/cron-schedule.js +3 -0
  30. package/dist/core/entities/cron-schedule.js.map +1 -0
  31. package/dist/core/entities/decision.d.ts +23 -0
  32. package/dist/core/entities/decision.d.ts.map +1 -0
  33. package/dist/core/entities/decision.js +7 -0
  34. package/dist/core/entities/decision.js.map +1 -0
  35. package/dist/core/entities/index.d.ts +3 -0
  36. package/dist/core/entities/index.d.ts.map +1 -0
  37. package/dist/core/entities/index.js +3 -0
  38. package/dist/core/entities/index.js.map +1 -0
  39. package/dist/core/entities/job-listener-subscription.d.ts +14 -0
  40. package/dist/core/entities/job-listener-subscription.d.ts.map +1 -0
  41. package/dist/core/entities/job-listener-subscription.js +3 -0
  42. package/dist/core/entities/job-listener-subscription.js.map +1 -0
  43. package/dist/core/entities/received-event.d.ts +28 -0
  44. package/dist/core/entities/received-event.d.ts.map +1 -0
  45. package/dist/core/entities/received-event.js +14 -0
  46. package/dist/core/entities/received-event.js.map +1 -0
  47. package/dist/core/entities/subscription.d.ts +13 -0
  48. package/dist/core/entities/subscription.d.ts.map +1 -0
  49. package/dist/core/entities/subscription.js +3 -0
  50. package/dist/core/entities/subscription.js.map +1 -0
  51. package/dist/core/errors.d.ts +25 -0
  52. package/dist/core/errors.d.ts.map +1 -0
  53. package/dist/core/errors.js +41 -0
  54. package/dist/core/errors.js.map +1 -0
  55. package/dist/core/fire-cron.d.ts +28 -0
  56. package/dist/core/fire-cron.d.ts.map +1 -0
  57. package/dist/core/fire-cron.js +85 -0
  58. package/dist/core/fire-cron.js.map +1 -0
  59. package/dist/core/fire-manual.d.ts +9 -0
  60. package/dist/core/fire-manual.d.ts.map +1 -0
  61. package/dist/core/fire-manual.js +87 -0
  62. package/dist/core/fire-manual.js.map +1 -0
  63. package/dist/core/index.d.ts +11 -0
  64. package/dist/core/index.d.ts.map +1 -0
  65. package/dist/core/index.js +10 -0
  66. package/dist/core/index.js.map +1 -0
  67. package/dist/core/record-trigger-history.d.ts +35 -0
  68. package/dist/core/record-trigger-history.d.ts.map +1 -0
  69. package/dist/core/record-trigger-history.js +75 -0
  70. package/dist/core/record-trigger-history.js.map +1 -0
  71. package/dist/core/route-event-to-job-listeners.d.ts +31 -0
  72. package/dist/core/route-event-to-job-listeners.d.ts.map +1 -0
  73. package/dist/core/route-event-to-job-listeners.js +140 -0
  74. package/dist/core/route-event-to-job-listeners.js.map +1 -0
  75. package/dist/db/cron-schedules.d.ts +36 -0
  76. package/dist/db/cron-schedules.d.ts.map +1 -0
  77. package/dist/db/cron-schedules.js +104 -0
  78. package/dist/db/cron-schedules.js.map +1 -0
  79. package/dist/db/db.d.ts +2515 -0
  80. package/dist/db/db.d.ts.map +1 -0
  81. package/dist/db/db.js +28 -0
  82. package/dist/db/db.js.map +1 -0
  83. package/dist/db/event-history.d.ts +50 -0
  84. package/dist/db/event-history.d.ts.map +1 -0
  85. package/dist/db/event-history.js +207 -0
  86. package/dist/db/event-history.js.map +1 -0
  87. package/dist/db/event-queries.d.ts +38 -0
  88. package/dist/db/event-queries.d.ts.map +1 -0
  89. package/dist/db/event-queries.js +78 -0
  90. package/dist/db/event-queries.js.map +1 -0
  91. package/dist/db/index.d.ts +11 -0
  92. package/dist/db/index.d.ts.map +1 -0
  93. package/dist/db/index.js +14 -0
  94. package/dist/db/index.js.map +1 -0
  95. package/dist/db/job-listener-subscriptions.d.ts +24 -0
  96. package/dist/db/job-listener-subscriptions.d.ts.map +1 -0
  97. package/dist/db/job-listener-subscriptions.js +69 -0
  98. package/dist/db/job-listener-subscriptions.js.map +1 -0
  99. package/dist/db/received-events.d.ts +7 -0
  100. package/dist/db/received-events.d.ts.map +1 -0
  101. package/dist/db/received-events.js +11 -0
  102. package/dist/db/received-events.js.map +1 -0
  103. package/dist/db/schema/common.d.ts +2 -0
  104. package/dist/db/schema/common.d.ts.map +1 -0
  105. package/dist/db/schema/common.js +4 -0
  106. package/dist/db/schema/common.js.map +1 -0
  107. package/dist/db/schema/cron-schedules.d.ts +148 -0
  108. package/dist/db/schema/cron-schedules.d.ts.map +1 -0
  109. package/dist/db/schema/cron-schedules.js +39 -0
  110. package/dist/db/schema/cron-schedules.js.map +1 -0
  111. package/dist/db/schema/decisions.d.ts +284 -0
  112. package/dist/db/schema/decisions.d.ts.map +1 -0
  113. package/dist/db/schema/decisions.js +70 -0
  114. package/dist/db/schema/decisions.js.map +1 -0
  115. package/dist/db/schema/job-listener-subscriptions.d.ts +185 -0
  116. package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -0
  117. package/dist/db/schema/job-listener-subscriptions.js +41 -0
  118. package/dist/db/schema/job-listener-subscriptions.js.map +1 -0
  119. package/dist/db/schema/outbox.d.ts +195 -0
  120. package/dist/db/schema/outbox.d.ts.map +1 -0
  121. package/dist/db/schema/outbox.js +5 -0
  122. package/dist/db/schema/outbox.js.map +1 -0
  123. package/dist/db/schema/received-events.d.ts +544 -0
  124. package/dist/db/schema/received-events.d.ts.map +1 -0
  125. package/dist/db/schema/received-events.js +86 -0
  126. package/dist/db/schema/received-events.js.map +1 -0
  127. package/dist/db/schema/subscriptions.d.ts +182 -0
  128. package/dist/db/schema/subscriptions.d.ts.map +1 -0
  129. package/dist/db/schema/subscriptions.js +39 -0
  130. package/dist/db/schema/subscriptions.js.map +1 -0
  131. package/dist/db/subscriptions.d.ts +26 -0
  132. package/dist/db/subscriptions.d.ts.map +1 -0
  133. package/dist/db/subscriptions.js +103 -0
  134. package/dist/db/subscriptions.js.map +1 -0
  135. package/dist/index.d.ts +7 -0
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +68 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/metrics/index.d.ts +3 -0
  140. package/dist/metrics/index.d.ts.map +1 -0
  141. package/dist/metrics/index.js +4 -0
  142. package/dist/metrics/index.js.map +1 -0
  143. package/dist/metrics/instance.d.ts +15 -0
  144. package/dist/metrics/instance.d.ts.map +1 -0
  145. package/dist/metrics/instance.js +32 -0
  146. package/dist/metrics/instance.js.map +1 -0
  147. package/dist/metrics/service.d.ts +2 -0
  148. package/dist/metrics/service.d.ts.map +1 -0
  149. package/dist/metrics/service.js +15 -0
  150. package/dist/metrics/service.js.map +1 -0
  151. package/dist/presentation/dto/trigger-events.d.ts +7 -0
  152. package/dist/presentation/dto/trigger-events.d.ts.map +1 -0
  153. package/dist/presentation/dto/trigger-events.js +47 -0
  154. package/dist/presentation/dto/trigger-events.js.map +1 -0
  155. package/dist/presentation/index.d.ts +3 -0
  156. package/dist/presentation/index.d.ts.map +1 -0
  157. package/dist/presentation/index.js +4 -0
  158. package/dist/presentation/index.js.map +1 -0
  159. package/dist/presentation/routes/fire-manual.d.ts +2 -0
  160. package/dist/presentation/routes/fire-manual.d.ts.map +1 -0
  161. package/dist/presentation/routes/fire-manual.js +71 -0
  162. package/dist/presentation/routes/fire-manual.js.map +1 -0
  163. package/dist/presentation/routes/get-trigger-event.d.ts +2 -0
  164. package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -0
  165. package/dist/presentation/routes/get-trigger-event.js +37 -0
  166. package/dist/presentation/routes/get-trigger-event.js.map +1 -0
  167. package/dist/presentation/routes/index.d.ts +3 -0
  168. package/dist/presentation/routes/index.d.ts.map +1 -0
  169. package/dist/presentation/routes/index.js +27 -0
  170. package/dist/presentation/routes/index.js.map +1 -0
  171. package/dist/presentation/routes/list-trigger-event-facets.d.ts +2 -0
  172. package/dist/presentation/routes/list-trigger-event-facets.d.ts.map +1 -0
  173. package/dist/presentation/routes/list-trigger-event-facets.js +29 -0
  174. package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -0
  175. package/dist/presentation/routes/list-trigger-events.d.ts +2 -0
  176. package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -0
  177. package/dist/presentation/routes/list-trigger-events.js +56 -0
  178. package/dist/presentation/routes/list-trigger-events.js.map +1 -0
  179. package/dist/presentation/subscribers/index.d.ts +6 -0
  180. package/dist/presentation/subscribers/index.d.ts.map +1 -0
  181. package/dist/presentation/subscribers/index.js +7 -0
  182. package/dist/presentation/subscribers/index.js.map +1 -0
  183. package/dist/presentation/subscribers/on-definition-deleted.d.ts +3 -0
  184. package/dist/presentation/subscribers/on-definition-deleted.d.ts.map +1 -0
  185. package/dist/presentation/subscribers/on-definition-deleted.js +8 -0
  186. package/dist/presentation/subscribers/on-definition-deleted.js.map +1 -0
  187. package/dist/presentation/subscribers/on-definition-resolved.d.ts +3 -0
  188. package/dist/presentation/subscribers/on-definition-resolved.d.ts.map +1 -0
  189. package/dist/presentation/subscribers/on-definition-resolved.js +11 -0
  190. package/dist/presentation/subscribers/on-definition-resolved.js.map +1 -0
  191. package/dist/presentation/subscribers/on-integration-event-received.d.ts +4 -0
  192. package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -0
  193. package/dist/presentation/subscribers/on-integration-event-received.js +17 -0
  194. package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -0
  195. package/dist/presentation/subscribers/on-job-activated.d.ts +3 -0
  196. package/dist/presentation/subscribers/on-job-activated.d.ts.map +1 -0
  197. package/dist/presentation/subscribers/on-job-activated.js +13 -0
  198. package/dist/presentation/subscribers/on-job-activated.js.map +1 -0
  199. package/dist/presentation/subscribers/on-job-terminated.d.ts +3 -0
  200. package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
  201. package/dist/presentation/subscribers/on-job-terminated.js +6 -0
  202. package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
  203. package/dist/temporal/activities/drain-cron-batch.d.ts +4 -0
  204. package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -0
  205. package/dist/temporal/activities/drain-cron-batch.js +20 -0
  206. package/dist/temporal/activities/drain-cron-batch.js.map +1 -0
  207. package/dist/temporal/activities/index.d.ts +10 -0
  208. package/dist/temporal/activities/index.d.ts.map +1 -0
  209. package/dist/temporal/activities/index.js +15 -0
  210. package/dist/temporal/activities/index.js.map +1 -0
  211. package/dist/temporal/activities/prune-trigger-events.d.ts +4 -0
  212. package/dist/temporal/activities/prune-trigger-events.d.ts.map +1 -0
  213. package/dist/temporal/activities/prune-trigger-events.js +11 -0
  214. package/dist/temporal/activities/prune-trigger-events.js.map +1 -0
  215. package/dist/temporal/constants.d.ts +7 -0
  216. package/dist/temporal/constants.d.ts.map +1 -0
  217. package/dist/temporal/constants.js +7 -0
  218. package/dist/temporal/constants.js.map +1 -0
  219. package/dist/temporal/workflows/cron-tick-cron.d.ts +9 -0
  220. package/dist/temporal/workflows/cron-tick-cron.d.ts.map +1 -0
  221. package/dist/temporal/workflows/cron-tick-cron.js +54 -0
  222. package/dist/temporal/workflows/cron-tick-cron.js.map +1 -0
  223. package/dist/temporal/workflows/index.d.ts +3 -0
  224. package/dist/temporal/workflows/index.d.ts.map +1 -0
  225. package/dist/temporal/workflows/index.js +4 -0
  226. package/dist/temporal/workflows/index.js.map +1 -0
  227. package/dist/temporal/workflows/prune-trigger-events-cron.d.ts +2 -0
  228. package/dist/temporal/workflows/prune-trigger-events-cron.d.ts.map +1 -0
  229. package/dist/temporal/workflows/prune-trigger-events-cron.js +18 -0
  230. package/dist/temporal/workflows/prune-trigger-events-cron.js.map +1 -0
  231. package/dist/tsconfig.test.tsbuildinfo +1 -0
  232. package/drizzle/0000_initial.sql +109 -0
  233. package/drizzle/meta/0000_snapshot.json +905 -0
  234. package/drizzle/meta/_journal.json +13 -0
  235. package/drizzle.config.ts +14 -0
  236. package/package.json +83 -0
  237. package/src/config.test.ts +33 -0
  238. package/src/config.ts +53 -0
  239. package/src/core/compute-next-fire-at.test.ts +69 -0
  240. package/src/core/compute-next-fire-at.ts +37 -0
  241. package/src/core/config.test.ts +213 -0
  242. package/src/core/config.ts +86 -0
  243. package/src/core/dispatch-integration-event.history-failure.test.ts +66 -0
  244. package/src/core/dispatch-integration-event.test.ts +1006 -0
  245. package/src/core/dispatch-integration-event.ts +150 -0
  246. package/src/core/drain-cron-schedules.test.ts +265 -0
  247. package/src/core/drain-cron-schedules.ts +90 -0
  248. package/src/core/entities/cron-schedule.ts +10 -0
  249. package/src/core/entities/decision.ts +24 -0
  250. package/src/core/entities/index.ts +5 -0
  251. package/src/core/entities/job-listener-subscription.ts +14 -0
  252. package/src/core/entities/received-event.ts +36 -0
  253. package/src/core/entities/subscription.ts +12 -0
  254. package/src/core/errors.ts +63 -0
  255. package/src/core/fire-cron.test.ts +146 -0
  256. package/src/core/fire-cron.ts +98 -0
  257. package/src/core/fire-manual.test.ts +157 -0
  258. package/src/core/fire-manual.ts +92 -0
  259. package/src/core/index.ts +34 -0
  260. package/src/core/record-trigger-history.test.ts +159 -0
  261. package/src/core/record-trigger-history.ts +145 -0
  262. package/src/core/route-event-to-job-listeners.test.ts +369 -0
  263. package/src/core/route-event-to-job-listeners.ts +184 -0
  264. package/src/db/cron-schedules.test.ts +358 -0
  265. package/src/db/cron-schedules.ts +163 -0
  266. package/src/db/db.ts +31 -0
  267. package/src/db/event-history.test.ts +421 -0
  268. package/src/db/event-history.ts +279 -0
  269. package/src/db/event-queries.ts +140 -0
  270. package/src/db/index.ts +51 -0
  271. package/src/db/job-listener-subscriptions.test.ts +270 -0
  272. package/src/db/job-listener-subscriptions.ts +133 -0
  273. package/src/db/received-events.ts +16 -0
  274. package/src/db/schema/common.ts +3 -0
  275. package/src/db/schema/cron-schedules.ts +37 -0
  276. package/src/db/schema/decisions.test.ts +279 -0
  277. package/src/db/schema/decisions.ts +77 -0
  278. package/src/db/schema/job-listener-subscriptions.ts +65 -0
  279. package/src/db/schema/outbox.ts +4 -0
  280. package/src/db/schema/received-events.test.ts +135 -0
  281. package/src/db/schema/received-events.ts +93 -0
  282. package/src/db/schema/subscriptions.ts +46 -0
  283. package/src/db/subscriptions.test.ts +340 -0
  284. package/src/db/subscriptions.ts +179 -0
  285. package/src/index.ts +108 -0
  286. package/src/metrics/index.ts +8 -0
  287. package/src/metrics/instance.ts +33 -0
  288. package/src/metrics/service.ts +18 -0
  289. package/src/presentation/dto/trigger-events.test.ts +144 -0
  290. package/src/presentation/dto/trigger-events.ts +60 -0
  291. package/src/presentation/index.ts +6 -0
  292. package/src/presentation/routes/fire-manual.test.ts +118 -0
  293. package/src/presentation/routes/fire-manual.ts +70 -0
  294. package/src/presentation/routes/get-trigger-event.test.ts +184 -0
  295. package/src/presentation/routes/get-trigger-event.ts +37 -0
  296. package/src/presentation/routes/index.ts +21 -0
  297. package/src/presentation/routes/list-trigger-event-facets.test.ts +116 -0
  298. package/src/presentation/routes/list-trigger-event-facets.ts +29 -0
  299. package/src/presentation/routes/list-trigger-events.test.ts +379 -0
  300. package/src/presentation/routes/list-trigger-events.ts +68 -0
  301. package/src/presentation/subscribers/index.ts +5 -0
  302. package/src/presentation/subscribers/on-definition-deleted.test.ts +42 -0
  303. package/src/presentation/subscribers/on-definition-deleted.ts +6 -0
  304. package/src/presentation/subscribers/on-definition-resolved.test.ts +67 -0
  305. package/src/presentation/subscribers/on-definition-resolved.ts +11 -0
  306. package/src/presentation/subscribers/on-integration-event-received.test.ts +53 -0
  307. package/src/presentation/subscribers/on-integration-event-received.ts +21 -0
  308. package/src/presentation/subscribers/on-job-activated.ts +14 -0
  309. package/src/presentation/subscribers/on-job-terminated.ts +6 -0
  310. package/src/temporal/activities/drain-cron-batch.ts +20 -0
  311. package/src/temporal/activities/index.ts +15 -0
  312. package/src/temporal/activities/prune-trigger-events.test.ts +78 -0
  313. package/src/temporal/activities/prune-trigger-events.ts +9 -0
  314. package/src/temporal/constants.ts +7 -0
  315. package/src/temporal/workflows/cron-tick-cron.ts +56 -0
  316. package/src/temporal/workflows/index.ts +2 -0
  317. package/src/temporal/workflows/prune-trigger-events-cron.ts +17 -0
  318. package/test/env.ts +8 -0
  319. package/test/factories/cron-schedule.ts +34 -0
  320. package/test/factories/decision.ts +49 -0
  321. package/test/factories/job-listener-subscription.ts +42 -0
  322. package/test/factories/received-event.ts +49 -0
  323. package/test/factories/subscription.ts +38 -0
  324. package/test/globalSetup.ts +20 -0
  325. package/test/index.ts +5 -0
  326. package/test/setup.ts +17 -0
  327. package/tsconfig.build.json +9 -0
  328. package/tsconfig.build.tsbuildinfo +1 -0
  329. package/tsconfig.json +3 -0
  330. package/tsconfig.test.json +8 -0
  331. package/vitest.config.ts +11 -0
@@ -0,0 +1,33 @@
1
+ import {instanceMetrics} from '@shipfox/node-opentelemetry';
2
+
3
+ const meter = instanceMetrics.getMeter('triggers');
4
+
5
+ export const eventReceivedCount = meter.createCounter<{
6
+ provider: string;
7
+ }>('triggers_event_received', {
8
+ description: 'Trigger events received by provider (e.g. github, gitea, sentry, manual)',
9
+ });
10
+
11
+ export const subscriptionTriggeredCount = meter.createCounter<{
12
+ provider: string;
13
+ }>('triggers_subscription_triggered', {
14
+ description: 'Subscriptions that resulted in a workflow run, by provider',
15
+ });
16
+
17
+ export const eventOutcomeCount = meter.createCounter<{
18
+ provider: string;
19
+ outcome: 'discarded' | 'routed' | 'failed' | 'errored';
20
+ }>('triggers_event_outcome', {
21
+ description: 'Final outcomes of trigger events by provider and outcome',
22
+ });
23
+
24
+ export const cronFiredCount = meter.createCounter<{outcome: 'fired' | 'errored'}>(
25
+ 'triggers_cron_fired',
26
+ {description: 'Cron schedule slots consumed by the tick, by fire outcome'},
27
+ );
28
+
29
+ export const cronFireLag = meter.createHistogram<Record<string, never>>('triggers_cron_fire_lag', {
30
+ description: 'Delay between a cron scheduled slot and when the tick actually fired it',
31
+ unit: 'ms',
32
+ advice: {explicitBucketBoundaries: [0, 100, 500, 1000, 5000, 15000, 60000, 300000]},
33
+ });
@@ -0,0 +1,18 @@
1
+ import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';
2
+ import {countDueCronSchedules} from '#db/cron-schedules.js';
3
+
4
+ export function registerTriggersServiceMetrics(): void {
5
+ const meter = getServiceMetricsProvider().getMeter('triggers');
6
+
7
+ const cronBacklog = meter.createObservableGauge('triggers_cron_backlog', {
8
+ description:
9
+ 'Cron schedules past their next fire time and still pending a fire (including any a drain is mid-flight on). A sustained nonzero value means the minute tick is falling behind; raise TRIGGER_CRON_FANOUT, TRIGGER_CRON_CLAIM_BATCH, or pod count.',
10
+ });
11
+
12
+ meter.addBatchObservableCallback(
13
+ async (observer) => {
14
+ observer.observe(cronBacklog, await countDueCronSchedules());
15
+ },
16
+ [cronBacklog],
17
+ );
18
+ }
@@ -0,0 +1,144 @@
1
+ import type {TriggerDecision} from '#core/entities/decision.js';
2
+ import type {
3
+ TriggerReceivedEvent,
4
+ TriggerReceivedEventSummary,
5
+ } from '#core/entities/received-event.js';
6
+ import {
7
+ toTriggerDecisionDto,
8
+ toTriggerEventDto,
9
+ toTriggerEventListItemDto,
10
+ } from './trigger-events.js';
11
+
12
+ const baseSummary: TriggerReceivedEventSummary = {
13
+ id: '11111111-1111-1111-1111-111111111111',
14
+ eventRef: 'event-ref',
15
+ origin: 'manual',
16
+ workspaceId: '22222222-2222-2222-2222-222222222222',
17
+ provider: null,
18
+ source: 'manual',
19
+ event: 'fire',
20
+ deliveryId: null,
21
+ connectionId: null,
22
+ connectionName: null,
23
+ outcome: 'discarded',
24
+ matchedCount: 0,
25
+ receivedAt: new Date('2026-05-07T00:00:00.000Z'),
26
+ processedAt: null,
27
+ createdAt: new Date('2026-05-07T00:00:01.000Z'),
28
+ };
29
+
30
+ describe('trigger-events mappers', () => {
31
+ test('toTriggerEventListItemDto maps null fields, formats ISO dates, and omits payload', () => {
32
+ const dto = toTriggerEventListItemDto(baseSummary);
33
+
34
+ expect(dto).toEqual({
35
+ id: baseSummary.id,
36
+ event_ref: 'event-ref',
37
+ origin: 'manual',
38
+ workspace_id: baseSummary.workspaceId,
39
+ provider: null,
40
+ source: 'manual',
41
+ event: 'fire',
42
+ delivery_id: null,
43
+ connection_id: null,
44
+ outcome: 'discarded',
45
+ matched_count: 0,
46
+ received_at: '2026-05-07T00:00:00.000Z',
47
+ processed_at: null,
48
+ created_at: '2026-05-07T00:00:01.000Z',
49
+ });
50
+ expect(dto).not.toHaveProperty('payload');
51
+ });
52
+
53
+ test('toTriggerEventDto carries the payload (including null)', () => {
54
+ const withPayload: TriggerReceivedEvent = {...baseSummary, payload: {ref: 'main'}};
55
+ const withoutPayload: TriggerReceivedEvent = {...baseSummary, payload: null};
56
+
57
+ expect(toTriggerEventDto(withPayload).payload).toEqual({ref: 'main'});
58
+ expect(toTriggerEventDto(withoutPayload).payload).toBeNull();
59
+ });
60
+
61
+ test('toTriggerEventDto carries the connection name (including null)', () => {
62
+ const named: TriggerReceivedEvent = {
63
+ ...baseSummary,
64
+ connectionName: 'Acme Production',
65
+ payload: null,
66
+ };
67
+ const unnamed: TriggerReceivedEvent = {...baseSummary, connectionName: null, payload: null};
68
+
69
+ expect(toTriggerEventDto(named).connection_name).toBe('Acme Production');
70
+ expect(toTriggerEventDto(unnamed).connection_name).toBeNull();
71
+ });
72
+
73
+ test('toTriggerDecisionDto maps null run/reason fields', () => {
74
+ const decision: TriggerDecision = {
75
+ id: '33333333-3333-3333-3333-333333333333',
76
+ receivedEventId: '11111111-1111-1111-1111-111111111111',
77
+ subscriptionKind: 'trigger',
78
+ subscriptionId: '44444444-4444-4444-4444-444444444444',
79
+ subscriptionName: 'Deploy production',
80
+ workflowDefinitionId: '55555555-5555-5555-5555-555555555555',
81
+ projectId: '66666666-6666-6666-6666-666666666666',
82
+ workflowRunId: null,
83
+ jobId: null,
84
+ matcherKind: null,
85
+ matcherOrdinal: null,
86
+ decision: 'dispatch-error',
87
+ runId: null,
88
+ runName: null,
89
+ reason: 'boom',
90
+ createdAt: new Date('2026-05-07T00:00:02.000Z'),
91
+ };
92
+
93
+ expect(toTriggerDecisionDto(decision)).toEqual({
94
+ id: decision.id,
95
+ received_event_id: decision.receivedEventId,
96
+ subscription_kind: 'trigger',
97
+ subscription_id: decision.subscriptionId,
98
+ subscription_name: 'Deploy production',
99
+ workflow_definition_id: decision.workflowDefinitionId,
100
+ project_id: decision.projectId,
101
+ workflow_run_id: null,
102
+ job_id: null,
103
+ matcher_kind: null,
104
+ matcher_ordinal: null,
105
+ decision: 'dispatch-error',
106
+ run_id: null,
107
+ run_name: null,
108
+ reason: 'boom',
109
+ created_at: '2026-05-07T00:00:02.000Z',
110
+ });
111
+ });
112
+
113
+ test('toTriggerDecisionDto maps listener identity fields', () => {
114
+ const decision: TriggerDecision = {
115
+ id: '33333333-3333-3333-3333-333333333333',
116
+ receivedEventId: '11111111-1111-1111-1111-111111111111',
117
+ subscriptionKind: 'listener',
118
+ subscriptionId: '44444444-4444-4444-4444-444444444444',
119
+ subscriptionName: 'listener until[0] github/pull_request.closed',
120
+ workflowDefinitionId: null,
121
+ projectId: null,
122
+ workflowRunId: '55555555-5555-5555-5555-555555555555',
123
+ jobId: '66666666-6666-6666-6666-666666666666',
124
+ matcherKind: 'until',
125
+ matcherOrdinal: 0,
126
+ decision: 'triggered',
127
+ runId: null,
128
+ runName: null,
129
+ reason: null,
130
+ createdAt: new Date('2026-05-07T00:00:02.000Z'),
131
+ };
132
+
133
+ expect(toTriggerDecisionDto(decision)).toMatchObject({
134
+ subscription_kind: 'listener',
135
+ workflow_definition_id: null,
136
+ project_id: null,
137
+ workflow_run_id: decision.workflowRunId,
138
+ job_id: decision.jobId,
139
+ matcher_kind: 'until',
140
+ matcher_ordinal: 0,
141
+ decision: 'triggered',
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,60 @@
1
+ import type {
2
+ TriggerDecisionDto,
3
+ TriggerEventDto,
4
+ TriggerEventListItemDto,
5
+ } from '@shipfox/api-triggers-dto';
6
+ import type {TriggerDecision} from '#core/entities/decision.js';
7
+ import type {
8
+ TriggerReceivedEvent,
9
+ TriggerReceivedEventSummary,
10
+ } from '#core/entities/received-event.js';
11
+
12
+ export function toTriggerEventListItemDto(
13
+ event: TriggerReceivedEventSummary,
14
+ ): TriggerEventListItemDto {
15
+ return {
16
+ id: event.id,
17
+ event_ref: event.eventRef,
18
+ origin: event.origin,
19
+ workspace_id: event.workspaceId,
20
+ provider: event.provider,
21
+ source: event.source,
22
+ event: event.event,
23
+ delivery_id: event.deliveryId,
24
+ connection_id: event.connectionId,
25
+ outcome: event.outcome,
26
+ matched_count: event.matchedCount,
27
+ received_at: event.receivedAt.toISOString(),
28
+ processed_at: event.processedAt?.toISOString() ?? null,
29
+ created_at: event.createdAt.toISOString(),
30
+ };
31
+ }
32
+
33
+ export function toTriggerEventDto(event: TriggerReceivedEvent): TriggerEventDto {
34
+ return {
35
+ ...toTriggerEventListItemDto(event),
36
+ connection_name: event.connectionName,
37
+ payload: event.payload,
38
+ };
39
+ }
40
+
41
+ export function toTriggerDecisionDto(decision: TriggerDecision): TriggerDecisionDto {
42
+ return {
43
+ id: decision.id,
44
+ received_event_id: decision.receivedEventId,
45
+ subscription_kind: decision.subscriptionKind,
46
+ subscription_id: decision.subscriptionId,
47
+ subscription_name: decision.subscriptionName,
48
+ workflow_definition_id: decision.workflowDefinitionId,
49
+ project_id: decision.projectId,
50
+ workflow_run_id: decision.workflowRunId,
51
+ job_id: decision.jobId,
52
+ matcher_kind: decision.matcherKind,
53
+ matcher_ordinal: decision.matcherOrdinal,
54
+ decision: decision.decision,
55
+ run_id: decision.runId,
56
+ run_name: decision.runName,
57
+ reason: decision.reason,
58
+ created_at: decision.createdAt.toISOString(),
59
+ };
60
+ }
@@ -0,0 +1,6 @@
1
+ export {triggerRoutes as routes} from './routes/index.js';
2
+ export {
3
+ onDefinitionDeleted,
4
+ onDefinitionResolved,
5
+ onIntegrationEventReceived,
6
+ } from './subscribers/index.js';
@@ -0,0 +1,118 @@
1
+ import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
2
+ import type {WorkflowRun} from '@shipfox/api-workflows';
3
+ import type {FastifyInstance} from 'fastify';
4
+ import Fastify from 'fastify';
5
+ import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
6
+ import {triggerSubscriptionFactory} from '#test/index.js';
7
+
8
+ const fireManualSubscriptionMock = vi.hoisted(() => vi.fn());
9
+
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
+ vi.mock('#core/fire-manual.js', () => ({
31
+ fireManualSubscription: fireManualSubscriptionMock,
32
+ }));
33
+
34
+ import {InterpolationUnresolvableError} from '@shipfox/api-workflows';
35
+
36
+ const {fireManualTriggerRoute} = await import('./fire-manual.js');
37
+
38
+ describe('POST /:definitionId/fire-manual', () => {
39
+ let app: FastifyInstance;
40
+ let workspaceId: string;
41
+ let memberships: Array<{workspaceId: string; role: 'admin'}>;
42
+
43
+ beforeAll(async () => {
44
+ app = Fastify();
45
+ app.setValidatorCompiler(validatorCompiler);
46
+ app.setSerializerCompiler(serializerCompiler);
47
+ app.addHook('onRequest', (request, _reply, done) => {
48
+ setUserContext(
49
+ request,
50
+ buildUserContext({userId: crypto.randomUUID(), email: 'user@example.com', memberships}),
51
+ );
52
+ done();
53
+ });
54
+ app.post('/:definitionId/fire-manual', fireManualTriggerRoute);
55
+ await app.ready();
56
+ });
57
+
58
+ beforeEach(() => {
59
+ workspaceId = crypto.randomUUID();
60
+ memberships = [{workspaceId, role: 'admin'}];
61
+ fireManualSubscriptionMock.mockReset();
62
+ });
63
+
64
+ test('returns 201 with the created run id', async () => {
65
+ const definitionId = crypto.randomUUID();
66
+ const runId = crypto.randomUUID();
67
+ await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
68
+ fireManualSubscriptionMock.mockResolvedValue({id: runId} satisfies Pick<WorkflowRun, 'id'>);
69
+
70
+ const res = await app.inject({
71
+ method: 'POST',
72
+ url: `/${definitionId}/fire-manual`,
73
+ payload: {},
74
+ });
75
+
76
+ expect(res.statusCode).toBe(201);
77
+ expect(res.json()).toEqual({workflow_run_id: runId});
78
+ });
79
+
80
+ test('maps unresolvable workflow interpolation to 422', async () => {
81
+ const definitionId = crypto.randomUUID();
82
+ await triggerSubscriptionFactory.create({workspaceId, workflowDefinitionId: definitionId});
83
+ fireManualSubscriptionMock.mockRejectedValue(
84
+ new InterpolationUnresolvableError(definitionId, {
85
+ field: 'env',
86
+ source: 'event.ref',
87
+ envKey: 'REF',
88
+ }),
89
+ );
90
+
91
+ const res = await app.inject({
92
+ method: 'POST',
93
+ url: `/${definitionId}/fire-manual`,
94
+ payload: {},
95
+ });
96
+
97
+ expect(res.statusCode).toBe(422);
98
+ expect(res.json()).toMatchObject({
99
+ code: 'workflow-interpolation-unresolvable',
100
+ details: {
101
+ field: 'env',
102
+ source: 'event.ref',
103
+ env_key: 'REF',
104
+ },
105
+ });
106
+ });
107
+
108
+ test('returns 404 when the manual trigger is unavailable', async () => {
109
+ const res = await app.inject({
110
+ method: 'POST',
111
+ url: `/${crypto.randomUUID()}/fire-manual`,
112
+ payload: {},
113
+ });
114
+
115
+ expect(res.statusCode).toBe(404);
116
+ expect(res.json().code).toBe('manual-trigger-not-found');
117
+ });
118
+ });
@@ -0,0 +1,70 @@
1
+ import {requireUserContext} from '@shipfox/api-auth-context';
2
+ import {
3
+ fireManualTriggerBodySchema,
4
+ fireManualTriggerResponseSchema,
5
+ } from '@shipfox/api-triggers-dto';
6
+ import {InterpolationUnresolvableError} from '@shipfox/api-workflows';
7
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
8
+ import {z} from 'zod';
9
+ import {ManualTriggerNotFoundError} from '#core/errors.js';
10
+ import {fireManualSubscription} from '#core/fire-manual.js';
11
+ import {getManualSubscriptionByDefinitionId} from '#db/subscriptions.js';
12
+
13
+ export const fireManualTriggerRoute = defineRoute({
14
+ method: 'POST',
15
+ path: '/:definitionId/fire-manual',
16
+ description: 'Fire the manual trigger of a workflow definition, creating a workflow run.',
17
+ schema: {
18
+ params: z.object({
19
+ definitionId: z.string().uuid(),
20
+ }),
21
+ body: fireManualTriggerBodySchema,
22
+ response: {
23
+ 201: fireManualTriggerResponseSchema,
24
+ 422: z.object({
25
+ code: z.string(),
26
+ details: z.object({
27
+ field: z.string(),
28
+ source: z.string(),
29
+ env_key: z.string().optional(),
30
+ }),
31
+ }),
32
+ },
33
+ },
34
+ errorHandler: (error) => {
35
+ if (error instanceof ManualTriggerNotFoundError) {
36
+ throw new ClientError(error.message, 'manual-trigger-not-found', {status: 404});
37
+ }
38
+ if (error instanceof InterpolationUnresolvableError) {
39
+ throw new ClientError(error.message, 'workflow-interpolation-unresolvable', {
40
+ status: 422,
41
+ details: {
42
+ field: error.field,
43
+ source: error.source,
44
+ ...(error.envKey === undefined ? {} : {env_key: error.envKey}),
45
+ },
46
+ });
47
+ }
48
+ throw error;
49
+ },
50
+ handler: async (request, reply) => {
51
+ const {definitionId} = request.params;
52
+ const userContext = requireUserContext(request);
53
+
54
+ const subscription = await getManualSubscriptionByDefinitionId(definitionId);
55
+ // 404 covers both "no such manual trigger" and "not your workspace" to avoid leaking existence.
56
+ if (!subscription || !userContext.canAccess(subscription.workspaceId)) {
57
+ throw new ManualTriggerNotFoundError(definitionId);
58
+ }
59
+
60
+ const run = await fireManualSubscription({
61
+ subscriptionId: subscription.id,
62
+ callerWorkspaceId: subscription.workspaceId,
63
+ userId: userContext.userId,
64
+ inputs: request.body.inputs,
65
+ });
66
+
67
+ reply.status(201);
68
+ return {workflow_run_id: run.id};
69
+ },
70
+ });
@@ -0,0 +1,184 @@
1
+ import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
2
+ import type {FastifyInstance} from 'fastify';
3
+ import Fastify from 'fastify';
4
+ import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
5
+ import {db} from '#db/db.js';
6
+ import {type TriggerDecisionInsertDb, triggersDecisions} from '#db/schema/decisions.js';
7
+ import {decisionFactory, receivedEventFactory} from '#test/index.js';
8
+ import {getTriggerEventRoute} from './get-trigger-event.js';
9
+
10
+ describe('GET /trigger-events/:id', () => {
11
+ let app: FastifyInstance;
12
+ let workspaceId: string;
13
+ let memberships: Array<{workspaceId: string; role: 'admin'}>;
14
+
15
+ beforeAll(async () => {
16
+ app = Fastify();
17
+ app.setValidatorCompiler(validatorCompiler);
18
+ app.setSerializerCompiler(serializerCompiler);
19
+ app.addHook('onRequest', (request, _reply, done) => {
20
+ setUserContext(
21
+ request,
22
+ buildUserContext({userId: crypto.randomUUID(), email: 'user@example.com', memberships}),
23
+ );
24
+ done();
25
+ });
26
+ app.get('/trigger-events/:id', getTriggerEventRoute);
27
+ await app.ready();
28
+ });
29
+
30
+ beforeEach(() => {
31
+ workspaceId = crypto.randomUUID();
32
+ memberships = [{workspaceId, role: 'admin'}];
33
+ });
34
+
35
+ test('returns the event with its decisions and full payload', async () => {
36
+ const event = await receivedEventFactory.create({
37
+ workspaceId,
38
+ outcome: 'routed',
39
+ matchedCount: 2,
40
+ payload: {ref: 'refs/heads/main', headCommitSha: 'abc123'},
41
+ });
42
+ const triggered = await decisionFactory.create({
43
+ receivedEventId: event.id,
44
+ decision: 'triggered',
45
+ subscriptionName: 'Deploy production',
46
+ runName: 'deploy',
47
+ });
48
+ const errored = await decisionFactory.create({
49
+ receivedEventId: event.id,
50
+ decision: 'dispatch-error',
51
+ subscriptionName: 'Lint checks',
52
+ runId: null,
53
+ runName: null,
54
+ reason: 'boom',
55
+ });
56
+
57
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
58
+
59
+ expect(res.statusCode).toBe(200);
60
+ const body = res.json();
61
+ expect(body.id).toBe(event.id);
62
+ expect(body.matched_count).toBe(2);
63
+ expect(body.payload).toEqual({ref: 'refs/heads/main', headCommitSha: 'abc123'});
64
+ expect(body.decisions.map((decision: {id: string}) => decision.id)).toEqual([
65
+ triggered.id,
66
+ errored.id,
67
+ ]);
68
+ expect(
69
+ body.decisions.map((decision: {subscription_name: string}) => decision.subscription_name),
70
+ ).toEqual(['Deploy production', 'Lint checks']);
71
+ expect(body.decisions[1].decision).toBe('dispatch-error');
72
+ expect(body.decisions[1].run_id).toBeNull();
73
+ expect(body.decisions[1].reason).toBe('boom');
74
+ });
75
+
76
+ test('normalizes legacy errored decisions before serializing the response', async () => {
77
+ const event = await receivedEventFactory.create({
78
+ workspaceId,
79
+ outcome: 'errored',
80
+ matchedCount: 1,
81
+ });
82
+ const legacyDecision = {
83
+ receivedEventId: event.id,
84
+ subscriptionKind: 'trigger',
85
+ subscriptionId: crypto.randomUUID(),
86
+ subscriptionName: 'Deploy production',
87
+ workflowDefinitionId: crypto.randomUUID(),
88
+ projectId: crypto.randomUUID(),
89
+ decision: 'errored',
90
+ runId: null,
91
+ runName: null,
92
+ reason: 'legacy failure',
93
+ } as unknown as TriggerDecisionInsertDb;
94
+ await db().insert(triggersDecisions).values(legacyDecision);
95
+
96
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
97
+
98
+ expect(res.statusCode).toBe(200);
99
+ expect(res.json().decisions).toMatchObject([
100
+ {decision: 'dispatch-error', reason: 'legacy failure'},
101
+ ]);
102
+ });
103
+
104
+ test('returns mixed trigger and listener decisions', async () => {
105
+ const event = await receivedEventFactory.create({
106
+ workspaceId,
107
+ outcome: 'routed',
108
+ matchedCount: 2,
109
+ });
110
+ await decisionFactory.create({
111
+ receivedEventId: event.id,
112
+ subscriptionKind: 'trigger',
113
+ decision: 'triggered',
114
+ subscriptionName: 'Deploy production',
115
+ });
116
+ const listener = await decisionFactory.create({
117
+ receivedEventId: event.id,
118
+ subscriptionKind: 'listener',
119
+ subscriptionName: 'listener until[0] github/pull_request.closed',
120
+ workflowDefinitionId: null,
121
+ projectId: null,
122
+ workflowRunId: crypto.randomUUID(),
123
+ jobId: crypto.randomUUID(),
124
+ matcherKind: 'until',
125
+ matcherOrdinal: 0,
126
+ decision: 'triggered',
127
+ runId: null,
128
+ runName: null,
129
+ });
130
+
131
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
132
+
133
+ expect(res.statusCode).toBe(200);
134
+ expect(res.json().decisions).toEqual(
135
+ expect.arrayContaining([
136
+ expect.objectContaining({
137
+ subscription_kind: 'listener',
138
+ subscription_id: listener.subscriptionId,
139
+ workflow_definition_id: null,
140
+ project_id: null,
141
+ workflow_run_id: listener.workflowRunId,
142
+ job_id: listener.jobId,
143
+ matcher_kind: 'until',
144
+ matcher_ordinal: 0,
145
+ }),
146
+ ]),
147
+ );
148
+ });
149
+
150
+ test('returns an empty decisions list for a discarded event', async () => {
151
+ const event = await receivedEventFactory.create({
152
+ workspaceId,
153
+ outcome: 'discarded',
154
+ matchedCount: 0,
155
+ });
156
+
157
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
158
+
159
+ expect(res.statusCode).toBe(200);
160
+ expect(res.json().decisions).toEqual([]);
161
+ });
162
+
163
+ test('returns 404 for an unknown event id', async () => {
164
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${crypto.randomUUID()}`});
165
+
166
+ expect(res.statusCode).toBe(404);
167
+ expect(res.json().code).toBe('not-found');
168
+ });
169
+
170
+ test('returns 404 for an event in another workspace', async () => {
171
+ const event = await receivedEventFactory.create({workspaceId: crypto.randomUUID()});
172
+
173
+ const res = await app.inject({method: 'GET', url: `/trigger-events/${event.id}`});
174
+
175
+ expect(res.statusCode).toBe(404);
176
+ expect(res.json().code).toBe('not-found');
177
+ });
178
+
179
+ test('rejects a non-uuid id', async () => {
180
+ const res = await app.inject({method: 'GET', url: '/trigger-events/not-a-uuid'});
181
+
182
+ expect(res.statusCode).toBe(400);
183
+ });
184
+ });
@@ -0,0 +1,37 @@
1
+ import {requireUserContext} from '@shipfox/api-auth-context';
2
+ import {triggerEventDetailResponseSchema} from '@shipfox/api-triggers-dto';
3
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
+ import {z} from 'zod';
5
+ import {getTriggerEventById, listDecisionsByReceivedEventId} from '#db/index.js';
6
+ import {toTriggerDecisionDto, toTriggerEventDto} from '#presentation/dto/trigger-events.js';
7
+
8
+ export const getTriggerEventRoute = defineRoute({
9
+ method: 'GET',
10
+ path: '/:id',
11
+ description: 'Get a trigger event by ID with its routing decisions.',
12
+ schema: {
13
+ params: z.object({
14
+ id: z.string().uuid(),
15
+ }),
16
+ response: {
17
+ 200: triggerEventDetailResponseSchema,
18
+ },
19
+ },
20
+ handler: async (request) => {
21
+ const {id} = request.params;
22
+ const userContext = requireUserContext(request);
23
+
24
+ const event = await getTriggerEventById(id);
25
+ // 404 covers both "no such event" and "not your workspace" to avoid leaking existence.
26
+ if (!event || !userContext.canAccess(event.workspaceId)) {
27
+ throw new ClientError('Trigger event not found', 'not-found', {status: 404});
28
+ }
29
+
30
+ const decisions = await listDecisionsByReceivedEventId(event.id);
31
+
32
+ return {
33
+ ...toTriggerEventDto(event),
34
+ decisions: decisions.map(toTriggerDecisionDto),
35
+ };
36
+ },
37
+ });