@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,140 @@
1
+ import {paginateTimestampIdRows, timestampIdCursorWhere} from '@shipfox/node-drizzle';
2
+ import {and, asc, count, desc, eq, gte, inArray, lte, type SQL} from 'drizzle-orm';
3
+ import type {PgColumn} from 'drizzle-orm/pg-core';
4
+ import type {TriggerDecision} from '#core/entities/decision.js';
5
+ import type {
6
+ TriggerEventOutcome,
7
+ TriggerReceivedEvent,
8
+ TriggerReceivedEventSummary,
9
+ } from '#core/entities/received-event.js';
10
+ import {db} from './db.js';
11
+ import {toTriggerDecision, triggersDecisions} from './schema/decisions.js';
12
+ import {
13
+ toTriggerReceivedEvent,
14
+ toTriggerReceivedEventSummary,
15
+ triggerReceivedEventSummaryColumns,
16
+ triggersReceivedEvents,
17
+ } from './schema/received-events.js';
18
+
19
+ export interface TriggerEventCursor {
20
+ receivedAt: Date;
21
+ id: string;
22
+ }
23
+
24
+ export interface TriggerEventListFilters {
25
+ source?: string[] | undefined;
26
+ event?: string[] | undefined;
27
+ outcomes?: TriggerEventOutcome[] | undefined;
28
+ from?: Date | undefined;
29
+ to?: Date | undefined;
30
+ }
31
+
32
+ export interface ListTriggerEventsParams {
33
+ workspaceId: string;
34
+ limit: number;
35
+ cursor?: TriggerEventCursor | undefined;
36
+ filters?: TriggerEventListFilters | undefined;
37
+ }
38
+
39
+ export interface ListTriggerEventsResult {
40
+ events: TriggerReceivedEventSummary[];
41
+ nextCursor: TriggerEventCursor | null;
42
+ }
43
+
44
+ function listConditions(params: ListTriggerEventsParams): SQL[] {
45
+ const {workspaceId, cursor, filters} = params;
46
+ const conditions: SQL[] = [eq(triggersReceivedEvents.workspaceId, workspaceId)];
47
+ const cursorCondition = timestampIdCursorWhere({
48
+ timestampColumn: triggersReceivedEvents.receivedAt,
49
+ idColumn: triggersReceivedEvents.id,
50
+ cursor: cursor ? {createdAt: cursor.receivedAt, id: cursor.id} : undefined,
51
+ });
52
+ if (cursorCondition) conditions.push(cursorCondition);
53
+ if (filters?.source?.length)
54
+ conditions.push(inArray(triggersReceivedEvents.source, filters.source));
55
+ if (filters?.event?.length) conditions.push(inArray(triggersReceivedEvents.event, filters.event));
56
+ if (filters?.outcomes?.length)
57
+ conditions.push(inArray(triggersReceivedEvents.outcome, filters.outcomes));
58
+ if (filters?.from) conditions.push(gte(triggersReceivedEvents.receivedAt, filters.from));
59
+ if (filters?.to) conditions.push(lte(triggersReceivedEvents.receivedAt, filters.to));
60
+ return conditions;
61
+ }
62
+
63
+ export async function listTriggerEvents(
64
+ params: ListTriggerEventsParams,
65
+ ): Promise<ListTriggerEventsResult> {
66
+ const rows = await db()
67
+ .select(triggerReceivedEventSummaryColumns)
68
+ .from(triggersReceivedEvents)
69
+ .where(and(...listConditions(params)))
70
+ .orderBy(desc(triggersReceivedEvents.receivedAt), desc(triggersReceivedEvents.id))
71
+ .limit(params.limit + 1);
72
+
73
+ const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'receivedAt'});
74
+
75
+ return {
76
+ events: page.pageRows.map(toTriggerReceivedEventSummary),
77
+ nextCursor: page.nextCursor
78
+ ? {receivedAt: page.nextCursor.createdAt, id: page.nextCursor.id}
79
+ : null,
80
+ };
81
+ }
82
+
83
+ export async function getTriggerEventById(id: string): Promise<TriggerReceivedEvent | undefined> {
84
+ const [row] = await db()
85
+ .select()
86
+ .from(triggersReceivedEvents)
87
+ .where(eq(triggersReceivedEvents.id, id))
88
+ .limit(1);
89
+ return row ? toTriggerReceivedEvent(row) : undefined;
90
+ }
91
+
92
+ export interface TriggerEventFacet {
93
+ value: string;
94
+ count: number;
95
+ }
96
+
97
+ export interface ListTriggerEventFacetsResult {
98
+ sources: TriggerEventFacet[];
99
+ events: TriggerEventFacet[];
100
+ }
101
+
102
+ // Distinct filter values for a workspace, capped so one noisy integration can't flood
103
+ // the dropdown. Workspace-unfiltered by design: a stable option list, not one that
104
+ // shifts as the other filters change. The (workspace_id, source) / (workspace_id, event)
105
+ // indexes back the group-by. Ties break on the value so the order is deterministic.
106
+ const FACET_LIMIT = 50;
107
+
108
+ async function listFacet(workspaceId: string, column: PgColumn): Promise<TriggerEventFacet[]> {
109
+ const rows = await db()
110
+ .select({value: column, count: count()})
111
+ .from(triggersReceivedEvents)
112
+ .where(eq(triggersReceivedEvents.workspaceId, workspaceId))
113
+ .groupBy(column)
114
+ .orderBy(desc(count()), asc(column))
115
+ .limit(FACET_LIMIT);
116
+ return rows.map((row) => ({value: row.value as string, count: Number(row.count)}));
117
+ }
118
+
119
+ export async function listTriggerEventFacets(params: {
120
+ workspaceId: string;
121
+ }): Promise<ListTriggerEventFacetsResult> {
122
+ const [sources, events] = await Promise.all([
123
+ listFacet(params.workspaceId, triggersReceivedEvents.source),
124
+ listFacet(params.workspaceId, triggersReceivedEvents.event),
125
+ ]);
126
+ return {sources, events};
127
+ }
128
+
129
+ // `received_event_id` is a globally-unique uuid, so the parent-event lookup is the
130
+ // workspace gate; the decision query needs no separate workspace predicate.
131
+ export async function listDecisionsByReceivedEventId(
132
+ receivedEventId: string,
133
+ ): Promise<TriggerDecision[]> {
134
+ const rows = await db()
135
+ .select()
136
+ .from(triggersDecisions)
137
+ .where(eq(triggersDecisions.receivedEventId, receivedEventId))
138
+ .orderBy(asc(triggersDecisions.createdAt), asc(triggersDecisions.id));
139
+ return rows.map(toTriggerDecision);
140
+ }
@@ -0,0 +1,51 @@
1
+ import {dirname, resolve} from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+
4
+ export {
5
+ advanceCronSchedule,
6
+ claimDueCronSchedules,
7
+ countDueCronSchedules,
8
+ deleteCronScheduleForSubscription,
9
+ getCronScheduleBySubscriptionId,
10
+ syncCronSchedule,
11
+ } from './cron-schedules.js';
12
+ export {closeDb, db, type Executor, schema, type Tx} from './db.js';
13
+ export {
14
+ getTriggerEventById,
15
+ type ListTriggerEventFacetsResult,
16
+ type ListTriggerEventsParams,
17
+ type ListTriggerEventsResult,
18
+ listDecisionsByReceivedEventId,
19
+ listTriggerEventFacets,
20
+ listTriggerEvents,
21
+ type TriggerEventCursor,
22
+ type TriggerEventFacet,
23
+ type TriggerEventListFilters,
24
+ } from './event-queries.js';
25
+ export {
26
+ type FindMatchingJobListenerSubscriptionsParams,
27
+ findMatchingJobListenerSubscriptions,
28
+ type ListenerMatcher,
29
+ type ProjectJobListenerSubscriptionsParams,
30
+ projectJobListenerSubscriptions,
31
+ removeJobListenerSubscriptionsForJob,
32
+ } from './job-listener-subscriptions.js';
33
+ export {
34
+ type CronScheduleDb,
35
+ type CronScheduleInsertDb,
36
+ toCronSchedule,
37
+ triggersCronSchedules,
38
+ } from './schema/cron-schedules.js';
39
+ export {jobListenerSubscriptions} from './schema/job-listener-subscriptions.js';
40
+ export {triggersOutbox} from './schema/outbox.js';
41
+ export {triggerSubscriptions} from './schema/subscriptions.js';
42
+ export {
43
+ deleteSubscriptionsForDefinition,
44
+ findMatchingSubscriptions,
45
+ getManualSubscriptionByDefinitionId,
46
+ getTriggerSubscriptionById,
47
+ listSubscriptionsByWorkflowDefinitionIds,
48
+ projectDefinitionTriggers,
49
+ } from './subscriptions.js';
50
+
51
+ export const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');
@@ -0,0 +1,270 @@
1
+ import type {WorkflowsJobActivatedEventDto} from '@shipfox/api-workflows-dto';
2
+ import {eq} from 'drizzle-orm';
3
+ import {db} from '#db/db.js';
4
+ import {jobListenerSubscriptions} from '#db/schema/job-listener-subscriptions.js';
5
+ import {onJobActivated} from '#presentation/subscribers/on-job-activated.js';
6
+ import {onJobTerminated} from '#presentation/subscribers/on-job-terminated.js';
7
+ import {jobListenerSubscriptionFactory} from '#test/index.js';
8
+ import {findMatchingJobListenerSubscriptions} from './job-listener-subscriptions.js';
9
+
10
+ function buildActivatedPayload(
11
+ overrides: Partial<WorkflowsJobActivatedEventDto> = {},
12
+ ): WorkflowsJobActivatedEventDto {
13
+ const payload = {
14
+ jobId: crypto.randomUUID(),
15
+ workflowRunId: crypto.randomUUID(),
16
+ workspaceId: crypto.randomUUID(),
17
+ mode: 'listening',
18
+ on: [{source: 'github', event: 'pull_request_review', inputs: {state: 'approved'}}],
19
+ until: [{source: 'github', event: 'pull_request', filter: 'payload.action == "closed"'}],
20
+ ...overrides,
21
+ };
22
+ return payload as WorkflowsJobActivatedEventDto;
23
+ }
24
+
25
+ function listJobSubscriptions(jobId: string) {
26
+ return db()
27
+ .select()
28
+ .from(jobListenerSubscriptions)
29
+ .where(eq(jobListenerSubscriptions.jobId, jobId));
30
+ }
31
+
32
+ describe('job listener subscriptions', () => {
33
+ it('projects one row per activated listener matcher with its ordinal', async () => {
34
+ const payload = buildActivatedPayload({
35
+ on: [
36
+ {source: 'github', event: 'pull_request_review', inputs: {state: 'approved'}},
37
+ {source: 'github', event: 'check_suite'},
38
+ ],
39
+ until: [{source: 'github', event: 'pull_request'}],
40
+ });
41
+
42
+ await onJobActivated(payload);
43
+
44
+ const rows = await listJobSubscriptions(payload.jobId);
45
+ expect(rows).toHaveLength(3);
46
+ expect(rows.map((row) => [row.kind, row.matcherOrdinal]).sort()).toEqual([
47
+ ['on', 0],
48
+ ['on', 1],
49
+ ['until', 0],
50
+ ]);
51
+ expect(rows.find((row) => row.matcherOrdinal === 0 && row.kind === 'on')?.config).toEqual({
52
+ inputs: {state: 'approved'},
53
+ });
54
+ });
55
+
56
+ it('does not project subscriptions for one-shot jobs', async () => {
57
+ const payload = buildActivatedPayload({mode: 'one_shot'});
58
+
59
+ await onJobActivated(payload);
60
+
61
+ const rows = await listJobSubscriptions(payload.jobId);
62
+ expect(rows).toHaveLength(0);
63
+ });
64
+
65
+ it('is idempotent for the same activated listener payload', async () => {
66
+ const payload = buildActivatedPayload();
67
+
68
+ await onJobActivated(payload);
69
+ await onJobActivated(payload);
70
+
71
+ const rows = await listJobSubscriptions(payload.jobId);
72
+ expect(rows).toHaveLength(2);
73
+ });
74
+
75
+ it('prunes stale matcher ordinals when a listener is re-projected with fewer matchers', async () => {
76
+ const payload = buildActivatedPayload({
77
+ on: [
78
+ {source: 'github', event: 'pull_request_review', inputs: {state: 'approved'}},
79
+ {source: 'github', event: 'check_suite'},
80
+ ],
81
+ until: [{source: 'github', event: 'pull_request'}],
82
+ });
83
+
84
+ await onJobActivated(payload);
85
+ await onJobActivated({
86
+ ...payload,
87
+ on: [{source: 'github', event: 'pull_request_review', inputs: {state: 'approved'}}],
88
+ until: null,
89
+ });
90
+
91
+ const rows = await listJobSubscriptions(payload.jobId);
92
+ expect(rows).toHaveLength(1);
93
+ expect(rows[0]?.kind).toBe('on');
94
+ expect(rows[0]?.matcherOrdinal).toBe(0);
95
+ });
96
+
97
+ it('preserves duplicate source and event matchers by ordinal', async () => {
98
+ const payload = buildActivatedPayload({
99
+ on: [
100
+ {
101
+ source: 'github',
102
+ event: 'pull_request_review',
103
+ filter: 'payload.review.state == "approved"',
104
+ },
105
+ {
106
+ source: 'github',
107
+ event: 'pull_request_review',
108
+ filter: 'payload.review.state == "changes_requested"',
109
+ },
110
+ ],
111
+ until: null,
112
+ });
113
+
114
+ await onJobActivated(payload);
115
+
116
+ const rows = await listJobSubscriptions(payload.jobId);
117
+ expect(rows).toHaveLength(2);
118
+ expect(rows.map((row) => row.matcherOrdinal).sort()).toEqual([0, 1]);
119
+ expect(
120
+ rows.every((row) => row.source === 'github' && row.event === 'pull_request_review'),
121
+ ).toBe(true);
122
+ });
123
+
124
+ it('persists listener filter snapshots verbatim in matcher config', async () => {
125
+ const filterSnapshot = {jobs: {build: {outputs: {pr_number: 42}}}};
126
+ const payload = buildActivatedPayload({
127
+ on: [
128
+ {
129
+ source: 'github',
130
+ event: 'pull_request_review',
131
+ filter: 'jobs.build.outputs.pr_number == event.pull_request.number',
132
+ filter_snapshot: filterSnapshot,
133
+ },
134
+ ],
135
+ until: null,
136
+ });
137
+
138
+ await onJobActivated(payload);
139
+
140
+ const rows = await listJobSubscriptions(payload.jobId);
141
+ expect(rows).toHaveLength(1);
142
+ expect(rows[0]?.config).toEqual({
143
+ filter: 'jobs.build.outputs.pr_number == event.pull_request.number',
144
+ filter_snapshot: filterSnapshot,
145
+ });
146
+ });
147
+
148
+ it('omits filter snapshots from matcher config when absent', async () => {
149
+ const payload = buildActivatedPayload({
150
+ on: [{source: 'github', event: 'pull_request_review', filter: 'event.action == "created"'}],
151
+ until: null,
152
+ });
153
+
154
+ await onJobActivated(payload);
155
+
156
+ const rows = await listJobSubscriptions(payload.jobId);
157
+ expect(rows).toHaveLength(1);
158
+ expect(rows[0]?.config).toEqual({filter: 'event.action == "created"'});
159
+ });
160
+
161
+ it('updates existing matcher ordinals with replacement filter snapshots', async () => {
162
+ const payload = buildActivatedPayload({
163
+ on: [
164
+ {
165
+ source: 'github',
166
+ event: 'pull_request_review',
167
+ filter: 'jobs.build.outputs.pr_number == event.pull_request.number',
168
+ filter_snapshot: {jobs: {build: {outputs: {pr_number: 42}}}},
169
+ },
170
+ ],
171
+ until: null,
172
+ });
173
+
174
+ await onJobActivated(payload);
175
+ await onJobActivated({
176
+ ...payload,
177
+ on: [
178
+ {
179
+ source: 'github',
180
+ event: 'pull_request_review',
181
+ filter: 'jobs.build.outputs.pr_number == event.pull_request.number',
182
+ filter_snapshot: {jobs: {build: {outputs: {pr_number: 43}}}},
183
+ },
184
+ ],
185
+ });
186
+
187
+ const rows = await listJobSubscriptions(payload.jobId);
188
+ expect(rows).toHaveLength(1);
189
+ expect(rows[0]?.config).toEqual({
190
+ filter: 'jobs.build.outputs.pr_number == event.pull_request.number',
191
+ filter_snapshot: {jobs: {build: {outputs: {pr_number: 43}}}},
192
+ });
193
+ });
194
+
195
+ it('deletes all subscriptions for a terminated job', async () => {
196
+ const jobId = crypto.randomUUID();
197
+ await jobListenerSubscriptionFactory.create({jobId, kind: 'on', matcherOrdinal: 0});
198
+ await jobListenerSubscriptionFactory.create({jobId, kind: 'until', matcherOrdinal: 0});
199
+
200
+ await onJobTerminated({
201
+ jobId,
202
+ workflowRunId: crypto.randomUUID(),
203
+ workflowRunAttemptId: crypto.randomUUID(),
204
+ status: 'succeeded',
205
+ statusReason: null,
206
+ });
207
+
208
+ const rows = await listJobSubscriptions(jobId);
209
+ expect(rows).toHaveLength(0);
210
+ });
211
+
212
+ it('treats deleting subscriptions for an unknown job as a no-op', async () => {
213
+ const jobId = crypto.randomUUID();
214
+
215
+ await onJobTerminated({
216
+ jobId,
217
+ workflowRunId: crypto.randomUUID(),
218
+ workflowRunAttemptId: crypto.randomUUID(),
219
+ status: 'cancelled',
220
+ statusReason: null,
221
+ });
222
+
223
+ const rows = await listJobSubscriptions(jobId);
224
+ expect(rows).toHaveLength(0);
225
+ });
226
+
227
+ it('finds matching subscriptions by workspace, source, and event', async () => {
228
+ const workspaceId = crypto.randomUUID();
229
+ const matching = await jobListenerSubscriptionFactory.create({
230
+ workspaceId,
231
+ source: 'github',
232
+ event: 'push',
233
+ });
234
+ await jobListenerSubscriptionFactory.create({
235
+ workspaceId: crypto.randomUUID(),
236
+ source: 'github',
237
+ event: 'push',
238
+ });
239
+ await jobListenerSubscriptionFactory.create({
240
+ workspaceId,
241
+ source: 'github',
242
+ event: 'pull_request',
243
+ });
244
+
245
+ const rows = await findMatchingJobListenerSubscriptions({
246
+ workspaceId,
247
+ source: 'github',
248
+ event: 'push',
249
+ });
250
+
251
+ expect(rows.map((row) => row.id)).toEqual([matching.id]);
252
+ });
253
+
254
+ it('returns no subscriptions for a negative match', async () => {
255
+ const workspaceId = crypto.randomUUID();
256
+ await jobListenerSubscriptionFactory.create({
257
+ workspaceId,
258
+ source: 'github',
259
+ event: 'push',
260
+ });
261
+
262
+ const rows = await findMatchingJobListenerSubscriptions({
263
+ workspaceId,
264
+ source: 'github',
265
+ event: 'pull_request',
266
+ });
267
+
268
+ expect(rows).toHaveLength(0);
269
+ });
270
+ });
@@ -0,0 +1,133 @@
1
+ import {and, eq, notInArray} from 'drizzle-orm';
2
+ import type {
3
+ JobListenerMatcherKind,
4
+ JobListenerSubscription,
5
+ } from '#core/entities/job-listener-subscription.js';
6
+ import {db} from './db.js';
7
+ import {
8
+ jobListenerSubscriptions,
9
+ toJobListenerSubscription,
10
+ } from './schema/job-listener-subscriptions.js';
11
+
12
+ type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
13
+
14
+ export interface ListenerMatcher {
15
+ source: string;
16
+ event: string;
17
+ inputs?: Readonly<Record<string, unknown>> | undefined;
18
+ filter?: string | undefined;
19
+ filter_snapshot?: Readonly<Record<string, unknown>> | undefined;
20
+ }
21
+
22
+ export interface ProjectJobListenerSubscriptionsParams {
23
+ workspaceId: string;
24
+ workflowRunId: string;
25
+ jobId: string;
26
+ on: readonly ListenerMatcher[] | null;
27
+ until: readonly ListenerMatcher[] | null;
28
+ }
29
+
30
+ export async function projectJobListenerSubscriptions(
31
+ params: ProjectJobListenerSubscriptionsParams,
32
+ ): Promise<void> {
33
+ await db().transaction(async (tx) => {
34
+ await pruneStaleMatchers(tx, params.jobId, 'on', params.on ?? []);
35
+ await pruneStaleMatchers(tx, params.jobId, 'until', params.until ?? []);
36
+
37
+ for (const [kind, matchers] of [
38
+ ['on', params.on ?? []],
39
+ ['until', params.until ?? []],
40
+ ] as const) {
41
+ for (const [matcherOrdinal, matcher] of matchers.entries()) {
42
+ const config: Record<string, unknown> = {};
43
+ if (matcher.inputs !== undefined) config.inputs = matcher.inputs;
44
+ if (matcher.filter !== undefined) config.filter = matcher.filter;
45
+ if (matcher.filter_snapshot !== undefined) config.filter_snapshot = matcher.filter_snapshot;
46
+
47
+ await tx
48
+ .insert(jobListenerSubscriptions)
49
+ .values({
50
+ workspaceId: params.workspaceId,
51
+ workflowRunId: params.workflowRunId,
52
+ jobId: params.jobId,
53
+ kind,
54
+ matcherOrdinal,
55
+ source: matcher.source,
56
+ event: matcher.event,
57
+ config,
58
+ })
59
+ .onConflictDoUpdate({
60
+ target: [
61
+ jobListenerSubscriptions.jobId,
62
+ jobListenerSubscriptions.kind,
63
+ jobListenerSubscriptions.matcherOrdinal,
64
+ ],
65
+ set: {
66
+ workspaceId: params.workspaceId,
67
+ workflowRunId: params.workflowRunId,
68
+ source: matcher.source,
69
+ event: matcher.event,
70
+ config,
71
+ },
72
+ });
73
+ }
74
+ }
75
+ });
76
+ }
77
+
78
+ async function pruneStaleMatchers(
79
+ tx: Tx,
80
+ jobId: string,
81
+ kind: JobListenerMatcherKind,
82
+ matchers: readonly ListenerMatcher[],
83
+ ): Promise<void> {
84
+ const base = and(
85
+ eq(jobListenerSubscriptions.jobId, jobId),
86
+ eq(jobListenerSubscriptions.kind, kind),
87
+ );
88
+
89
+ if (matchers.length === 0) {
90
+ await tx.delete(jobListenerSubscriptions).where(base);
91
+ return;
92
+ }
93
+
94
+ await tx.delete(jobListenerSubscriptions).where(
95
+ and(
96
+ base,
97
+ notInArray(
98
+ jobListenerSubscriptions.matcherOrdinal,
99
+ matchers.map((_matcher, index) => index),
100
+ ),
101
+ ),
102
+ );
103
+ }
104
+
105
+ export async function removeJobListenerSubscriptionsForJob(jobId: string): Promise<number> {
106
+ const rows = await db()
107
+ .delete(jobListenerSubscriptions)
108
+ .where(eq(jobListenerSubscriptions.jobId, jobId))
109
+ .returning({id: jobListenerSubscriptions.id});
110
+ return rows.length;
111
+ }
112
+
113
+ export interface FindMatchingJobListenerSubscriptionsParams {
114
+ workspaceId: string;
115
+ source: string;
116
+ event: string;
117
+ }
118
+
119
+ export async function findMatchingJobListenerSubscriptions(
120
+ params: FindMatchingJobListenerSubscriptionsParams,
121
+ ): Promise<JobListenerSubscription[]> {
122
+ const rows = await db()
123
+ .select()
124
+ .from(jobListenerSubscriptions)
125
+ .where(
126
+ and(
127
+ eq(jobListenerSubscriptions.workspaceId, params.workspaceId),
128
+ eq(jobListenerSubscriptions.source, params.source),
129
+ eq(jobListenerSubscriptions.event, params.event),
130
+ ),
131
+ );
132
+ return rows.map(toJobListenerSubscription);
133
+ }
@@ -0,0 +1,16 @@
1
+ import {lt} from 'drizzle-orm';
2
+ import {db} from './db.js';
3
+ import {triggersReceivedEvents} from './schema/received-events.js';
4
+
5
+ export interface PruneTriggerEventsParams {
6
+ olderThan: Date;
7
+ }
8
+
9
+ export async function pruneTriggerEvents(
10
+ params: PruneTriggerEventsParams,
11
+ ): Promise<{deleted: number}> {
12
+ const result = await db()
13
+ .delete(triggersReceivedEvents)
14
+ .where(lt(triggersReceivedEvents.createdAt, params.olderThan));
15
+ return {deleted: result.rowCount ?? 0};
16
+ }
@@ -0,0 +1,3 @@
1
+ import {pgTableCreator} from 'drizzle-orm/pg-core';
2
+
3
+ export const pgTable = pgTableCreator((name) => `triggers_${name}`);
@@ -0,0 +1,37 @@
1
+ import {index, text, timestamp, uuid} from 'drizzle-orm/pg-core';
2
+ import type {CronSchedule} from '#core/entities/cron-schedule.js';
3
+ import {pgTable} from './common.js';
4
+ import {triggerSubscriptions} from './subscriptions.js';
5
+
6
+ export const triggersCronSchedules = pgTable(
7
+ 'cron_schedules',
8
+ {
9
+ subscriptionId: uuid('subscription_id')
10
+ .primaryKey()
11
+ .references(() => triggerSubscriptions.id, {onDelete: 'cascade'}),
12
+ workspaceId: uuid('workspace_id').notNull(),
13
+ cronExpression: text('cron_expression').notNull(),
14
+ timezone: text('timezone').notNull(),
15
+ nextFireAt: timestamp('next_fire_at', {withTimezone: true}).notNull(),
16
+ lastFiredAt: timestamp('last_fired_at', {withTimezone: true}),
17
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
18
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
19
+ },
20
+ (table) => [index('triggers_cron_schedules_next_fire_at_idx').on(table.nextFireAt)],
21
+ );
22
+
23
+ export type CronScheduleDb = typeof triggersCronSchedules.$inferSelect;
24
+ export type CronScheduleInsertDb = typeof triggersCronSchedules.$inferInsert;
25
+
26
+ export function toCronSchedule(row: CronScheduleDb): CronSchedule {
27
+ return {
28
+ subscriptionId: row.subscriptionId,
29
+ workspaceId: row.workspaceId,
30
+ cronExpression: row.cronExpression,
31
+ timezone: row.timezone,
32
+ nextFireAt: row.nextFireAt,
33
+ lastFiredAt: row.lastFiredAt,
34
+ createdAt: row.createdAt,
35
+ updatedAt: row.updatedAt,
36
+ };
37
+ }