@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,46 @@
1
+ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
2
+ import {index, jsonb, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
3
+ import type {TriggerSubscription} from '#core/entities/subscription.js';
4
+ import {pgTable} from './common.js';
5
+
6
+ export const triggerSubscriptions = pgTable(
7
+ 'subscriptions',
8
+ {
9
+ id: uuidv7PrimaryKey(),
10
+ workspaceId: uuid('workspace_id').notNull(),
11
+ projectId: uuid('project_id').notNull(),
12
+ workflowDefinitionId: uuid('workflow_definition_id').notNull(),
13
+ name: text('name').notNull(),
14
+ source: text('source').notNull(),
15
+ event: text('event').notNull(),
16
+ config: jsonb('config').notNull(),
17
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
18
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
19
+ },
20
+ (table) => [
21
+ uniqueIndex('triggers_subscriptions_definition_name_unique').on(
22
+ table.workflowDefinitionId,
23
+ table.name,
24
+ ),
25
+ index('triggers_subscriptions_match_idx').on(table.workspaceId, table.source, table.event),
26
+ index('triggers_subscriptions_definition_idx').on(table.workflowDefinitionId),
27
+ ],
28
+ );
29
+
30
+ export type TriggerSubscriptionDb = typeof triggerSubscriptions.$inferSelect;
31
+ export type TriggerSubscriptionInsertDb = typeof triggerSubscriptions.$inferInsert;
32
+
33
+ export function toTriggerSubscription(row: TriggerSubscriptionDb): TriggerSubscription {
34
+ return {
35
+ id: row.id,
36
+ workspaceId: row.workspaceId,
37
+ projectId: row.projectId,
38
+ workflowDefinitionId: row.workflowDefinitionId,
39
+ name: row.name,
40
+ source: row.source,
41
+ event: row.event,
42
+ config: row.config as Record<string, unknown>,
43
+ createdAt: row.createdAt,
44
+ updatedAt: row.updatedAt,
45
+ };
46
+ }
@@ -0,0 +1,340 @@
1
+ import {eq} from 'drizzle-orm';
2
+ import {db} from './db.js';
3
+ import {triggerSubscriptions} from './schema/subscriptions.js';
4
+ import {
5
+ deleteSubscriptionsForDefinition,
6
+ findMatchingSubscriptions,
7
+ getManualSubscriptionByDefinitionId,
8
+ getTriggerSubscriptionById,
9
+ projectDefinitionTriggers,
10
+ } from './subscriptions.js';
11
+
12
+ describe('projectDefinitionTriggers', () => {
13
+ let workspaceId: string;
14
+ let projectId: string;
15
+ let workflowDefinitionId: string;
16
+
17
+ beforeEach(() => {
18
+ workspaceId = crypto.randomUUID();
19
+ projectId = crypto.randomUUID();
20
+ workflowDefinitionId = crypto.randomUUID();
21
+ });
22
+
23
+ test('inserts a row for each declared trigger', async () => {
24
+ await projectDefinitionTriggers({
25
+ workspaceId,
26
+ projectId,
27
+ workflowDefinitionId,
28
+ triggers: {
29
+ on_demand: {source: 'manual', event: 'fire'},
30
+ on_push: {
31
+ source: 'github',
32
+ event: 'push',
33
+ with: {branch: 'main'},
34
+ filter: 'event.ref == "refs/heads/main"',
35
+ },
36
+ },
37
+ });
38
+
39
+ const rows = await db()
40
+ .select()
41
+ .from(triggerSubscriptions)
42
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
43
+
44
+ expect(rows).toHaveLength(2);
45
+ const manual = rows.find((r) => r.name === 'on_demand');
46
+ expect(manual?.source).toBe('manual');
47
+ expect(manual?.event).toBe('fire');
48
+ expect(manual?.config).toEqual({});
49
+ const push = rows.find((r) => r.name === 'on_push');
50
+ expect(push?.source).toBe('github');
51
+ expect(push?.event).toBe('push');
52
+ expect(push?.config).toEqual({
53
+ with: {branch: 'main'},
54
+ filter: 'event.ref == "refs/heads/main"',
55
+ });
56
+ });
57
+
58
+ test('removes rows whose trigger name is no longer in the map', async () => {
59
+ await projectDefinitionTriggers({
60
+ workspaceId,
61
+ projectId,
62
+ workflowDefinitionId,
63
+ triggers: {
64
+ on_demand: {source: 'manual', event: 'fire'},
65
+ on_push: {source: 'github', event: 'push'},
66
+ },
67
+ });
68
+
69
+ await projectDefinitionTriggers({
70
+ workspaceId,
71
+ projectId,
72
+ workflowDefinitionId,
73
+ triggers: {
74
+ on_demand: {source: 'manual', event: 'fire'},
75
+ },
76
+ });
77
+
78
+ const rows = await db()
79
+ .select()
80
+ .from(triggerSubscriptions)
81
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
82
+
83
+ expect(rows.map((r) => r.name)).toEqual(['on_demand']);
84
+ });
85
+
86
+ test('updates rows in place when a trigger name persists across reconciliations', async () => {
87
+ await projectDefinitionTriggers({
88
+ workspaceId,
89
+ projectId,
90
+ workflowDefinitionId,
91
+ triggers: {
92
+ on_push: {source: 'github', event: 'push', with: {branch: 'main'}},
93
+ },
94
+ });
95
+ const before = await db()
96
+ .select()
97
+ .from(triggerSubscriptions)
98
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
99
+ const originalId = before[0]?.id;
100
+
101
+ await projectDefinitionTriggers({
102
+ workspaceId,
103
+ projectId,
104
+ workflowDefinitionId,
105
+ triggers: {
106
+ on_push: {source: 'github', event: 'push', with: {branch: 'develop'}},
107
+ },
108
+ });
109
+
110
+ const after = await db()
111
+ .select()
112
+ .from(triggerSubscriptions)
113
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
114
+ expect(after).toHaveLength(1);
115
+ expect(after[0]?.id).toBe(originalId);
116
+ expect(after[0]?.config).toEqual({with: {branch: 'develop'}});
117
+ });
118
+
119
+ test('empty triggers map removes all rows for the definition', async () => {
120
+ await projectDefinitionTriggers({
121
+ workspaceId,
122
+ projectId,
123
+ workflowDefinitionId,
124
+ triggers: {
125
+ on_demand: {source: 'manual', event: 'fire'},
126
+ },
127
+ });
128
+
129
+ await projectDefinitionTriggers({
130
+ workspaceId,
131
+ projectId,
132
+ workflowDefinitionId,
133
+ triggers: {},
134
+ });
135
+
136
+ const rows = await db()
137
+ .select()
138
+ .from(triggerSubscriptions)
139
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
140
+ expect(rows).toHaveLength(0);
141
+ });
142
+ });
143
+
144
+ describe('deleteSubscriptionsForDefinition', () => {
145
+ test('deletes every row for the definition', async () => {
146
+ const workflowDefinitionId = crypto.randomUUID();
147
+ await projectDefinitionTriggers({
148
+ workspaceId: crypto.randomUUID(),
149
+ projectId: crypto.randomUUID(),
150
+ workflowDefinitionId,
151
+ triggers: {
152
+ a: {source: 'manual', event: 'fire'},
153
+ b: {source: 'github', event: 'push'},
154
+ },
155
+ });
156
+
157
+ const deletedCount = await deleteSubscriptionsForDefinition({workflowDefinitionId});
158
+
159
+ expect(deletedCount).toBe(2);
160
+ const remaining = await db()
161
+ .select()
162
+ .from(triggerSubscriptions)
163
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
164
+ expect(remaining).toHaveLength(0);
165
+ });
166
+ });
167
+
168
+ describe('findMatchingSubscriptions', () => {
169
+ let workspaceId: string;
170
+ let projectId: string;
171
+
172
+ beforeEach(() => {
173
+ workspaceId = crypto.randomUUID();
174
+ projectId = crypto.randomUUID();
175
+ });
176
+
177
+ test('returns subscriptions matching workspace, source, event', async () => {
178
+ const workflowDefinitionId = crypto.randomUUID();
179
+ await projectDefinitionTriggers({
180
+ workspaceId,
181
+ projectId,
182
+ workflowDefinitionId,
183
+ triggers: {
184
+ on_push: {source: 'github', event: 'push'},
185
+ on_demand: {source: 'manual', event: 'fire'},
186
+ },
187
+ });
188
+
189
+ const matches = await findMatchingSubscriptions({
190
+ workspaceId,
191
+ source: 'github',
192
+ event: 'push',
193
+ });
194
+
195
+ expect(matches).toHaveLength(1);
196
+ expect(matches[0]?.name).toBe('on_push');
197
+ });
198
+
199
+ test('matches connection-specific sources, not the provider id', async () => {
200
+ await projectDefinitionTriggers({
201
+ workspaceId,
202
+ projectId,
203
+ workflowDefinitionId: crypto.randomUUID(),
204
+ triggers: {
205
+ prod_error: {source: 'sentry_prod', event: 'issue.created'},
206
+ staging_error: {source: 'sentry_staging', event: 'issue.created'},
207
+ },
208
+ });
209
+
210
+ const prodMatches = await findMatchingSubscriptions({
211
+ workspaceId,
212
+ source: 'sentry_prod',
213
+ event: 'issue.created',
214
+ });
215
+ const stagingMatches = await findMatchingSubscriptions({
216
+ workspaceId,
217
+ source: 'sentry_staging',
218
+ event: 'issue.created',
219
+ });
220
+ const providerMatches = await findMatchingSubscriptions({
221
+ workspaceId,
222
+ source: 'sentry',
223
+ event: 'issue.created',
224
+ });
225
+
226
+ expect(prodMatches.map((match) => match.name)).toEqual(['prod_error']);
227
+ expect(stagingMatches.map((match) => match.name)).toEqual(['staging_error']);
228
+ expect(providerMatches).toHaveLength(0);
229
+ });
230
+
231
+ test('matches across projects within the workspace, without project scoping', async () => {
232
+ await projectDefinitionTriggers({
233
+ workspaceId,
234
+ projectId,
235
+ workflowDefinitionId: crypto.randomUUID(),
236
+ triggers: {on_push: {source: 'github', event: 'push'}},
237
+ });
238
+ await projectDefinitionTriggers({
239
+ workspaceId,
240
+ projectId: crypto.randomUUID(),
241
+ workflowDefinitionId: crypto.randomUUID(),
242
+ triggers: {on_push: {source: 'github', event: 'push'}},
243
+ });
244
+
245
+ const matches = await findMatchingSubscriptions({
246
+ workspaceId,
247
+ source: 'github',
248
+ event: 'push',
249
+ });
250
+
251
+ expect(matches).toHaveLength(2);
252
+ });
253
+
254
+ test('does not return rows from other workspaces', async () => {
255
+ await projectDefinitionTriggers({
256
+ workspaceId,
257
+ projectId,
258
+ workflowDefinitionId: crypto.randomUUID(),
259
+ triggers: {on_push: {source: 'github', event: 'push'}},
260
+ });
261
+
262
+ const otherWorkspace = await findMatchingSubscriptions({
263
+ workspaceId: crypto.randomUUID(),
264
+ source: 'github',
265
+ event: 'push',
266
+ });
267
+
268
+ expect(otherWorkspace).toHaveLength(0);
269
+ });
270
+ });
271
+
272
+ describe('getTriggerSubscriptionById', () => {
273
+ test('returns the row when found', async () => {
274
+ const workspaceId = crypto.randomUUID();
275
+ const projectId = crypto.randomUUID();
276
+ const workflowDefinitionId = crypto.randomUUID();
277
+ await projectDefinitionTriggers({
278
+ workspaceId,
279
+ projectId,
280
+ workflowDefinitionId,
281
+ triggers: {on_demand: {source: 'manual', event: 'fire'}},
282
+ });
283
+ const [row] = await db()
284
+ .select()
285
+ .from(triggerSubscriptions)
286
+ .where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
287
+
288
+ const found = await getTriggerSubscriptionById(row?.id ?? '');
289
+
290
+ expect(found?.id).toBe(row?.id);
291
+ expect(found?.source).toBe('manual');
292
+ expect(found?.event).toBe('fire');
293
+ });
294
+
295
+ test('returns undefined when not found', async () => {
296
+ const found = await getTriggerSubscriptionById(crypto.randomUUID());
297
+
298
+ expect(found).toBeUndefined();
299
+ });
300
+ });
301
+
302
+ describe('getManualSubscriptionByDefinitionId', () => {
303
+ test('returns the manual subscription declared by a definition', async () => {
304
+ const workflowDefinitionId = crypto.randomUUID();
305
+ await projectDefinitionTriggers({
306
+ workspaceId: crypto.randomUUID(),
307
+ projectId: crypto.randomUUID(),
308
+ workflowDefinitionId,
309
+ triggers: {
310
+ on_demand: {source: 'manual', event: 'fire'},
311
+ on_push: {source: 'github', event: 'push'},
312
+ },
313
+ });
314
+
315
+ const found = await getManualSubscriptionByDefinitionId(workflowDefinitionId);
316
+
317
+ expect(found?.source).toBe('manual');
318
+ expect(found?.name).toBe('on_demand');
319
+ });
320
+
321
+ test('returns undefined when the definition has no manual trigger', async () => {
322
+ const workflowDefinitionId = crypto.randomUUID();
323
+ await projectDefinitionTriggers({
324
+ workspaceId: crypto.randomUUID(),
325
+ projectId: crypto.randomUUID(),
326
+ workflowDefinitionId,
327
+ triggers: {on_push: {source: 'github', event: 'push'}},
328
+ });
329
+
330
+ const found = await getManualSubscriptionByDefinitionId(workflowDefinitionId);
331
+
332
+ expect(found).toBeUndefined();
333
+ });
334
+
335
+ test('returns undefined when no rows exist for the definition', async () => {
336
+ const found = await getManualSubscriptionByDefinitionId(crypto.randomUUID());
337
+
338
+ expect(found).toBeUndefined();
339
+ });
340
+ });
@@ -0,0 +1,179 @@
1
+ import type {TriggerDto} from '@shipfox/api-definitions-dto';
2
+ import {and, eq, inArray, notInArray} from 'drizzle-orm';
3
+ import type {TriggerSubscription} from '#core/entities/subscription.js';
4
+ import {deleteCronScheduleForSubscription, syncCronSchedule} from './cron-schedules.js';
5
+ import {db, type Executor, type Tx} from './db.js';
6
+ import {toTriggerSubscription, triggerSubscriptions} from './schema/subscriptions.js';
7
+
8
+ export interface ProjectDefinitionTriggersParams {
9
+ tx?: Tx | undefined;
10
+ workspaceId: string;
11
+ projectId: string;
12
+ workflowDefinitionId: string;
13
+ triggers: Record<string, TriggerDto>;
14
+ }
15
+
16
+ export async function projectDefinitionTriggers(
17
+ params: ProjectDefinitionTriggersParams,
18
+ ): Promise<void> {
19
+ const work = async (tx: Tx): Promise<void> => {
20
+ const entries = Object.entries(params.triggers);
21
+ const keepNames = entries.map(([name]) => name);
22
+
23
+ if (keepNames.length === 0) {
24
+ await tx
25
+ .delete(triggerSubscriptions)
26
+ .where(eq(triggerSubscriptions.workflowDefinitionId, params.workflowDefinitionId));
27
+ return;
28
+ }
29
+
30
+ await tx
31
+ .delete(triggerSubscriptions)
32
+ .where(
33
+ and(
34
+ eq(triggerSubscriptions.workflowDefinitionId, params.workflowDefinitionId),
35
+ notInArray(triggerSubscriptions.name, keepNames),
36
+ ),
37
+ );
38
+
39
+ for (const [name, trigger] of entries) {
40
+ const config: Record<string, unknown> = {};
41
+ if (trigger.with !== undefined) config.with = trigger.with;
42
+ if (trigger.filter !== undefined) config.filter = trigger.filter;
43
+
44
+ const [upserted] = await tx
45
+ .insert(triggerSubscriptions)
46
+ .values({
47
+ workspaceId: params.workspaceId,
48
+ projectId: params.projectId,
49
+ workflowDefinitionId: params.workflowDefinitionId,
50
+ name,
51
+ source: trigger.source,
52
+ event: trigger.event,
53
+ config,
54
+ })
55
+ .onConflictDoUpdate({
56
+ target: [triggerSubscriptions.workflowDefinitionId, triggerSubscriptions.name],
57
+ set: {
58
+ workspaceId: params.workspaceId,
59
+ projectId: params.projectId,
60
+ source: trigger.source,
61
+ event: trigger.event,
62
+ config,
63
+ updatedAt: new Date(),
64
+ },
65
+ })
66
+ .returning({id: triggerSubscriptions.id});
67
+ if (!upserted) throw new Error('Trigger subscription upsert returned no rows');
68
+
69
+ if (trigger.source === 'cron') {
70
+ await syncCronSchedule({
71
+ tx,
72
+ subscriptionId: upserted.id,
73
+ workspaceId: params.workspaceId,
74
+ triggerConfig: trigger.config,
75
+ });
76
+ } else {
77
+ await deleteCronScheduleForSubscription({tx, subscriptionId: upserted.id});
78
+ }
79
+ }
80
+ };
81
+
82
+ if (params.tx) {
83
+ await work(params.tx);
84
+ return;
85
+ }
86
+ await db().transaction(work);
87
+ }
88
+
89
+ export interface DeleteSubscriptionsForDefinitionParams {
90
+ tx?: Tx | undefined;
91
+ workflowDefinitionId: string;
92
+ }
93
+
94
+ export async function deleteSubscriptionsForDefinition(
95
+ params: DeleteSubscriptionsForDefinitionParams,
96
+ ): Promise<number> {
97
+ const work = async (executor: Executor): Promise<number> => {
98
+ const rows = await executor
99
+ .delete(triggerSubscriptions)
100
+ .where(eq(triggerSubscriptions.workflowDefinitionId, params.workflowDefinitionId))
101
+ .returning({id: triggerSubscriptions.id});
102
+ return rows.length;
103
+ };
104
+ if (params.tx) return await work(params.tx);
105
+ return await db().transaction(work);
106
+ }
107
+
108
+ export async function getTriggerSubscriptionById(
109
+ id: string,
110
+ ): Promise<TriggerSubscription | undefined> {
111
+ const rows = await db()
112
+ .select()
113
+ .from(triggerSubscriptions)
114
+ .where(eq(triggerSubscriptions.id, id))
115
+ .limit(1);
116
+ const row = rows[0];
117
+ if (!row) return undefined;
118
+ return toTriggerSubscription(row);
119
+ }
120
+
121
+ export async function getManualSubscriptionByDefinitionId(
122
+ workflowDefinitionId: string,
123
+ ): Promise<TriggerSubscription | undefined> {
124
+ // limit(2) catches a broken parser invariant (>1 manual per definition) loudly instead of silently picking one.
125
+ const rows = await db()
126
+ .select()
127
+ .from(triggerSubscriptions)
128
+ .where(
129
+ and(
130
+ eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId),
131
+ eq(triggerSubscriptions.source, 'manual'),
132
+ ),
133
+ )
134
+ .limit(2);
135
+ if (rows.length > 1) {
136
+ throw new Error(
137
+ `Workflow definition ${workflowDefinitionId} has ${rows.length} manual triggers; expected at most 1`,
138
+ );
139
+ }
140
+ const row = rows[0];
141
+ if (!row) return undefined;
142
+ return toTriggerSubscription(row);
143
+ }
144
+
145
+ export interface FindMatchingSubscriptionsParams {
146
+ workspaceId: string;
147
+ source: string;
148
+ event: string;
149
+ }
150
+
151
+ // Matches at workspace scope: an inbound integration event is a workspace-level
152
+ // fact, not addressed to a project. Narrowing to a repo/project/branch is left to
153
+ // user-defined per-workflow filters, not inferred here.
154
+ export async function findMatchingSubscriptions(
155
+ params: FindMatchingSubscriptionsParams,
156
+ ): Promise<TriggerSubscription[]> {
157
+ const rows = await db()
158
+ .select()
159
+ .from(triggerSubscriptions)
160
+ .where(
161
+ and(
162
+ eq(triggerSubscriptions.workspaceId, params.workspaceId),
163
+ eq(triggerSubscriptions.source, params.source),
164
+ eq(triggerSubscriptions.event, params.event),
165
+ ),
166
+ );
167
+ return rows.map(toTriggerSubscription);
168
+ }
169
+
170
+ export async function listSubscriptionsByWorkflowDefinitionIds(
171
+ workflowDefinitionIds: string[],
172
+ ): Promise<TriggerSubscription[]> {
173
+ if (workflowDefinitionIds.length === 0) return [];
174
+ const rows = await db()
175
+ .select()
176
+ .from(triggerSubscriptions)
177
+ .where(inArray(triggerSubscriptions.workflowDefinitionId, workflowDefinitionIds));
178
+ return rows.map(toTriggerSubscription);
179
+ }
package/src/index.ts ADDED
@@ -0,0 +1,108 @@
1
+ import {dirname, resolve} from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {
4
+ DEFINITION_DELETED,
5
+ DEFINITION_RESOLVED,
6
+ type DefinitionsEventMap,
7
+ } from '@shipfox/api-definitions-dto';
8
+ import {
9
+ INTEGRATION_EVENT_RECEIVED,
10
+ type IntegrationsEventMap,
11
+ } from '@shipfox/api-integration-core-dto';
12
+ import {
13
+ WORKFLOWS_JOB_ACTIVATED,
14
+ WORKFLOWS_JOB_TERMINATED,
15
+ type WorkflowsEventMapDto,
16
+ } from '@shipfox/api-workflows-dto';
17
+ import {type ShipfoxModule, subscriberFactory} from '@shipfox/node-module';
18
+ import {db, migrationsPath, triggersOutbox} from '#db/index.js';
19
+ import {registerTriggersServiceMetrics} from '#metrics/index.js';
20
+ import {routes} from '#presentation/index.js';
21
+ import {
22
+ onDefinitionDeleted,
23
+ onDefinitionResolved,
24
+ onIntegrationEventReceived,
25
+ onJobActivated,
26
+ onJobTerminated,
27
+ } from '#presentation/subscribers/index.js';
28
+ import {
29
+ createTriggersCronActivities,
30
+ createTriggersMaintenanceActivities,
31
+ } from '#temporal/activities/index.js';
32
+ import {TRIGGERS_CRON_TASK_QUEUE, TRIGGERS_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';
33
+
34
+ export type {
35
+ JobListenerMatcherKind,
36
+ JobListenerSubscription,
37
+ } from '#core/entities/job-listener-subscription.js';
38
+ export type {TriggerSubscription} from '#core/entities/subscription.js';
39
+ export {
40
+ fireCronSubscription,
41
+ fireManualSubscription,
42
+ ManualTriggerNotFoundError,
43
+ TriggerSubscriptionNotCronError,
44
+ TriggerSubscriptionNotFoundError,
45
+ TriggerSubscriptionNotManualError,
46
+ TriggerWorkspaceMismatchError,
47
+ } from '#core/index.js';
48
+ export {
49
+ db,
50
+ findMatchingJobListenerSubscriptions,
51
+ findMatchingSubscriptions,
52
+ getManualSubscriptionByDefinitionId,
53
+ getTriggerSubscriptionById,
54
+ jobListenerSubscriptions,
55
+ listSubscriptionsByWorkflowDefinitionIds,
56
+ migrationsPath,
57
+ projectJobListenerSubscriptions,
58
+ removeJobListenerSubscriptionsForJob,
59
+ triggersOutbox,
60
+ } from '#db/index.js';
61
+
62
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
63
+ const temporalWorkflowsPath = resolve(packageRoot, 'dist/temporal/workflows/index.js');
64
+
65
+ const subscriber = subscriberFactory<
66
+ DefinitionsEventMap & IntegrationsEventMap & WorkflowsEventMapDto
67
+ >();
68
+
69
+ export const triggersModule: ShipfoxModule = {
70
+ name: 'triggers',
71
+ database: {db, migrationsPath},
72
+ routes,
73
+ metrics: registerTriggersServiceMetrics,
74
+ publishers: [{name: 'triggers', table: triggersOutbox, db}],
75
+ subscribers: [
76
+ subscriber(DEFINITION_RESOLVED, onDefinitionResolved),
77
+ subscriber(DEFINITION_DELETED, onDefinitionDeleted),
78
+ subscriber(INTEGRATION_EVENT_RECEIVED, onIntegrationEventReceived),
79
+ subscriber(WORKFLOWS_JOB_ACTIVATED, onJobActivated),
80
+ subscriber(WORKFLOWS_JOB_TERMINATED, onJobTerminated),
81
+ ],
82
+ workers: [
83
+ {
84
+ taskQueue: TRIGGERS_MAINTENANCE_TASK_QUEUE,
85
+ workflowsPath: temporalWorkflowsPath,
86
+ activities: createTriggersMaintenanceActivities,
87
+ workflows: [
88
+ {
89
+ name: 'pruneTriggerEventsCron',
90
+ id: 'triggers-prune-trigger-events',
91
+ cronSchedule: '0 * * * *',
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ taskQueue: TRIGGERS_CRON_TASK_QUEUE,
97
+ workflowsPath: temporalWorkflowsPath,
98
+ activities: createTriggersCronActivities,
99
+ workflows: [
100
+ {
101
+ name: 'cronTickCron',
102
+ id: 'triggers-cron-tick',
103
+ cronSchedule: '* * * * *',
104
+ },
105
+ ],
106
+ },
107
+ ],
108
+ };
@@ -0,0 +1,8 @@
1
+ export {
2
+ cronFiredCount,
3
+ cronFireLag,
4
+ eventOutcomeCount,
5
+ eventReceivedCount,
6
+ subscriptionTriggeredCount,
7
+ } from './instance.js';
8
+ export {registerTriggersServiceMetrics} from './service.js';