@kelpie/server 0.12.0 → 0.14.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 (420) hide show
  1. package/dist/app.d.ts +13 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +8 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/boot.d.ts +9 -0
  6. package/dist/boot.d.ts.map +1 -1
  7. package/dist/boot.js +12 -2
  8. package/dist/boot.js.map +1 -1
  9. package/dist/index.d.ts +6 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/apiKeyScopes.d.ts.map +1 -1
  14. package/dist/lib/apiKeyScopes.js +6 -0
  15. package/dist/lib/apiKeyScopes.js.map +1 -1
  16. package/dist/lib/columns.d.ts +6 -0
  17. package/dist/lib/columns.d.ts.map +1 -1
  18. package/dist/lib/columns.js +5 -3
  19. package/dist/lib/columns.js.map +1 -1
  20. package/dist/lib/config.d.ts +17 -0
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/config.js +3 -0
  23. package/dist/lib/config.js.map +1 -1
  24. package/dist/lib/egress.d.ts.map +1 -1
  25. package/dist/lib/egress.js +8 -4
  26. package/dist/lib/egress.js.map +1 -1
  27. package/dist/lib/errors.d.ts +3 -3
  28. package/dist/lib/errors.d.ts.map +1 -1
  29. package/dist/lib/errors.js +13 -5
  30. package/dist/lib/errors.js.map +1 -1
  31. package/dist/lib/ids.d.ts +11 -0
  32. package/dist/lib/ids.d.ts.map +1 -1
  33. package/dist/lib/ids.js +11 -0
  34. package/dist/lib/ids.js.map +1 -1
  35. package/dist/lib/jobs.d.ts +110 -0
  36. package/dist/lib/jobs.d.ts.map +1 -0
  37. package/dist/lib/jobs.js +5 -0
  38. package/dist/lib/jobs.js.map +1 -0
  39. package/dist/lib/kelpieConfigFile.d.ts +15 -1
  40. package/dist/lib/kelpieConfigFile.d.ts.map +1 -1
  41. package/dist/lib/kelpieConfigFile.js +33 -1
  42. package/dist/lib/kelpieConfigFile.js.map +1 -1
  43. package/dist/modules/agent-tasks/catalog.d.ts.map +1 -1
  44. package/dist/modules/agent-tasks/catalog.js +32 -1
  45. package/dist/modules/agent-tasks/catalog.js.map +1 -1
  46. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  47. package/dist/modules/agent-tasks/index.js +1 -0
  48. package/dist/modules/agent-tasks/index.js.map +1 -1
  49. package/dist/modules/agent-tasks/resolve.d.ts.map +1 -1
  50. package/dist/modules/agent-tasks/resolve.js +5 -0
  51. package/dist/modules/agent-tasks/resolve.js.map +1 -1
  52. package/dist/modules/agent-tasks/routes.d.ts +2 -0
  53. package/dist/modules/agent-tasks/routes.d.ts.map +1 -1
  54. package/dist/modules/attachedRecords.d.ts +22 -8
  55. package/dist/modules/attachedRecords.d.ts.map +1 -1
  56. package/dist/modules/attachedRecords.js +42 -5
  57. package/dist/modules/attachedRecords.js.map +1 -1
  58. package/dist/modules/auth/index.d.ts.map +1 -1
  59. package/dist/modules/auth/index.js +1 -0
  60. package/dist/modules/auth/index.js.map +1 -1
  61. package/dist/modules/auth/repository.d.ts +14 -0
  62. package/dist/modules/auth/repository.d.ts.map +1 -1
  63. package/dist/modules/auth/repository.js +20 -2
  64. package/dist/modules/auth/repository.js.map +1 -1
  65. package/dist/modules/auth/routes.d.ts.map +1 -1
  66. package/dist/modules/auth/routes.js +30 -0
  67. package/dist/modules/auth/routes.js.map +1 -1
  68. package/dist/modules/auth/service.d.ts +28 -0
  69. package/dist/modules/auth/service.d.ts.map +1 -1
  70. package/dist/modules/auth/service.js +72 -4
  71. package/dist/modules/auth/service.js.map +1 -1
  72. package/dist/modules/companies/repository.d.ts.map +1 -1
  73. package/dist/modules/companies/repository.js +1 -1
  74. package/dist/modules/companies/repository.js.map +1 -1
  75. package/dist/modules/companies/routes.d.ts +2 -4
  76. package/dist/modules/companies/routes.d.ts.map +1 -1
  77. package/dist/modules/companies/routes.js +6 -11
  78. package/dist/modules/companies/routes.js.map +1 -1
  79. package/dist/modules/companies/schema.d.ts +13 -28
  80. package/dist/modules/companies/schema.d.ts.map +1 -1
  81. package/dist/modules/companies/schema.js +2 -2
  82. package/dist/modules/companies/schema.js.map +1 -1
  83. package/dist/modules/companies/service.d.ts +2 -3
  84. package/dist/modules/companies/service.d.ts.map +1 -1
  85. package/dist/modules/companies/service.js +4 -7
  86. package/dist/modules/companies/service.js.map +1 -1
  87. package/dist/modules/core.d.ts.map +1 -1
  88. package/dist/modules/core.js +2 -0
  89. package/dist/modules/core.js.map +1 -1
  90. package/dist/modules/custom-fields/repository.d.ts.map +1 -1
  91. package/dist/modules/custom-fields/repository.js +2 -0
  92. package/dist/modules/custom-fields/repository.js.map +1 -1
  93. package/dist/modules/custom-fields/routes.d.ts +1 -0
  94. package/dist/modules/custom-fields/routes.d.ts.map +1 -1
  95. package/dist/modules/dashboard/index.d.ts.map +1 -1
  96. package/dist/modules/dashboard/index.js +1 -0
  97. package/dist/modules/dashboard/index.js.map +1 -1
  98. package/dist/modules/dashboard/repository.d.ts +13 -0
  99. package/dist/modules/dashboard/repository.d.ts.map +1 -1
  100. package/dist/modules/dashboard/repository.js +36 -0
  101. package/dist/modules/dashboard/repository.js.map +1 -1
  102. package/dist/modules/dashboard/routes.d.ts.map +1 -1
  103. package/dist/modules/dashboard/routes.js +13 -0
  104. package/dist/modules/dashboard/routes.js.map +1 -1
  105. package/dist/modules/dashboard/service.d.ts +11 -0
  106. package/dist/modules/dashboard/service.d.ts.map +1 -1
  107. package/dist/modules/dashboard/service.js +24 -1
  108. package/dist/modules/dashboard/service.js.map +1 -1
  109. package/dist/modules/decisions/repository.d.ts.map +1 -1
  110. package/dist/modules/decisions/repository.js +2 -0
  111. package/dist/modules/decisions/repository.js.map +1 -1
  112. package/dist/modules/decisions/routes.d.ts +2 -0
  113. package/dist/modules/decisions/routes.d.ts.map +1 -1
  114. package/dist/modules/events/attendances.d.ts +35 -0
  115. package/dist/modules/events/attendances.d.ts.map +1 -0
  116. package/dist/modules/events/attendances.js +157 -0
  117. package/dist/modules/events/attendances.js.map +1 -0
  118. package/dist/modules/events/catalog.d.ts +35 -0
  119. package/dist/modules/events/catalog.d.ts.map +1 -0
  120. package/dist/modules/events/catalog.js +11 -0
  121. package/dist/modules/events/catalog.js.map +1 -0
  122. package/dist/modules/events/index.d.ts +10 -0
  123. package/dist/modules/events/index.d.ts.map +1 -0
  124. package/dist/modules/events/index.js +47 -0
  125. package/dist/modules/events/index.js.map +1 -0
  126. package/dist/modules/events/repository.d.ts +67 -0
  127. package/dist/modules/events/repository.d.ts.map +1 -0
  128. package/dist/modules/events/repository.js +212 -0
  129. package/dist/modules/events/repository.js.map +1 -0
  130. package/dist/modules/events/routes.d.ts +136 -0
  131. package/dist/modules/events/routes.d.ts.map +1 -0
  132. package/dist/modules/events/routes.js +280 -0
  133. package/dist/modules/events/routes.js.map +1 -0
  134. package/dist/modules/events/schema.d.ts +626 -0
  135. package/dist/modules/events/schema.d.ts.map +1 -0
  136. package/dist/modules/events/schema.js +93 -0
  137. package/dist/modules/events/schema.js.map +1 -0
  138. package/dist/modules/events/service.d.ts +68 -0
  139. package/dist/modules/events/service.d.ts.map +1 -0
  140. package/dist/modules/events/service.js +262 -0
  141. package/dist/modules/events/service.js.map +1 -0
  142. package/dist/modules/events/tools.d.ts +8 -0
  143. package/dist/modules/events/tools.d.ts.map +1 -0
  144. package/dist/modules/events/tools.js +80 -0
  145. package/dist/modules/events/tools.js.map +1 -0
  146. package/dist/modules/forms/applySubmissionFields.d.ts.map +1 -1
  147. package/dist/modules/forms/applySubmissionFields.js +7 -5
  148. package/dist/modules/forms/applySubmissionFields.js.map +1 -1
  149. package/dist/modules/forms/fillAddresses.d.ts +4 -0
  150. package/dist/modules/forms/fillAddresses.d.ts.map +1 -0
  151. package/dist/modules/forms/fillAddresses.js +93 -0
  152. package/dist/modules/forms/fillAddresses.js.map +1 -0
  153. package/dist/modules/forms/repository.d.ts +2 -2
  154. package/dist/modules/forms/repository.d.ts.map +1 -1
  155. package/dist/modules/forms/repository.js.map +1 -1
  156. package/dist/modules/forms/routes.d.ts +2 -0
  157. package/dist/modules/forms/routes.d.ts.map +1 -1
  158. package/dist/modules/forms/routes.js +2 -2
  159. package/dist/modules/forms/routes.js.map +1 -1
  160. package/dist/modules/forms/schema.js +2 -2
  161. package/dist/modules/forms/schema.js.map +1 -1
  162. package/dist/modules/forms/service.js +2 -2
  163. package/dist/modules/forms/service.js.map +1 -1
  164. package/dist/modules/forms/submission.d.ts +1 -0
  165. package/dist/modules/forms/submission.d.ts.map +1 -1
  166. package/dist/modules/forms/submission.js +27 -4
  167. package/dist/modules/forms/submission.js.map +1 -1
  168. package/dist/modules/hiring/roles.d.ts.map +1 -1
  169. package/dist/modules/hiring/roles.js +2 -1
  170. package/dist/modules/hiring/roles.js.map +1 -1
  171. package/dist/modules/import-export/addresses.d.ts +22 -0
  172. package/dist/modules/import-export/addresses.d.ts.map +1 -0
  173. package/dist/modules/import-export/addresses.js +83 -0
  174. package/dist/modules/import-export/addresses.js.map +1 -0
  175. package/dist/modules/import-export/drafts.d.ts +3 -4
  176. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  177. package/dist/modules/import-export/drafts.js +4 -5
  178. package/dist/modules/import-export/drafts.js.map +1 -1
  179. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  180. package/dist/modules/import-export/exportRows.js +3 -3
  181. package/dist/modules/import-export/exportRows.js.map +1 -1
  182. package/dist/modules/import-export/plan.d.ts.map +1 -1
  183. package/dist/modules/import-export/plan.js +17 -2
  184. package/dist/modules/import-export/plan.js.map +1 -1
  185. package/dist/modules/import-export/presets.d.ts.map +1 -1
  186. package/dist/modules/import-export/presets.js +52 -7
  187. package/dist/modules/import-export/presets.js.map +1 -1
  188. package/dist/modules/import-export/repository.d.ts +3 -4
  189. package/dist/modules/import-export/repository.d.ts.map +1 -1
  190. package/dist/modules/import-export/repository.js +2 -3
  191. package/dist/modules/import-export/repository.js.map +1 -1
  192. package/dist/modules/import-export/writes.d.ts.map +1 -1
  193. package/dist/modules/import-export/writes.js +8 -4
  194. package/dist/modules/import-export/writes.js.map +1 -1
  195. package/dist/modules/lists/routes.d.ts +4 -0
  196. package/dist/modules/lists/routes.d.ts.map +1 -1
  197. package/dist/modules/notes/routes.d.ts +2 -0
  198. package/dist/modules/notes/routes.d.ts.map +1 -1
  199. package/dist/modules/people/repository.d.ts.map +1 -1
  200. package/dist/modules/people/repository.js +1 -1
  201. package/dist/modules/people/repository.js.map +1 -1
  202. package/dist/modules/people/routes.d.ts +2 -2
  203. package/dist/modules/people/routes.js +6 -6
  204. package/dist/modules/people/routes.js.map +1 -1
  205. package/dist/modules/people/schema.d.ts +17 -14
  206. package/dist/modules/people/schema.d.ts.map +1 -1
  207. package/dist/modules/people/schema.js +6 -4
  208. package/dist/modules/people/schema.js.map +1 -1
  209. package/dist/modules/people/service.d.ts +2 -2
  210. package/dist/modules/people/service.d.ts.map +1 -1
  211. package/dist/modules/people/service.js +5 -4
  212. package/dist/modules/people/service.js.map +1 -1
  213. package/dist/modules/plans/repository.d.ts +2 -2
  214. package/dist/modules/plans/repository.d.ts.map +1 -1
  215. package/dist/modules/plans/routes.d.ts +1 -0
  216. package/dist/modules/plans/routes.d.ts.map +1 -1
  217. package/dist/modules/plans/routes.js +4 -4
  218. package/dist/modules/plans/routes.js.map +1 -1
  219. package/dist/modules/plans/schema.d.ts +2 -2
  220. package/dist/modules/plans/schema.d.ts.map +1 -1
  221. package/dist/modules/plans/schema.js +3 -3
  222. package/dist/modules/plans/schema.js.map +1 -1
  223. package/dist/modules/plans/service.d.ts +2 -2
  224. package/dist/modules/plans/service.d.ts.map +1 -1
  225. package/dist/modules/plans/service.js.map +1 -1
  226. package/dist/modules/plans/tools.js +2 -2
  227. package/dist/modules/plans/tools.js.map +1 -1
  228. package/dist/modules/recordTargets.d.ts.map +1 -1
  229. package/dist/modules/recordTargets.js +11 -0
  230. package/dist/modules/recordTargets.js.map +1 -1
  231. package/dist/modules/references.d.ts.map +1 -1
  232. package/dist/modules/references.js +2 -0
  233. package/dist/modules/references.js.map +1 -1
  234. package/dist/modules/sample-data/fixture.d.ts +26 -4
  235. package/dist/modules/sample-data/fixture.d.ts.map +1 -1
  236. package/dist/modules/sample-data/fixture.js +157 -23
  237. package/dist/modules/sample-data/fixture.js.map +1 -1
  238. package/dist/modules/sample-data/index.d.ts +5 -2
  239. package/dist/modules/sample-data/index.d.ts.map +1 -1
  240. package/dist/modules/sample-data/index.js +7 -2
  241. package/dist/modules/sample-data/index.js.map +1 -1
  242. package/dist/modules/sample-data/routes.d.ts.map +1 -1
  243. package/dist/modules/sample-data/routes.js +2 -0
  244. package/dist/modules/sample-data/routes.js.map +1 -1
  245. package/dist/modules/sample-data/service.d.ts +11 -3
  246. package/dist/modules/sample-data/service.d.ts.map +1 -1
  247. package/dist/modules/sample-data/service.js +452 -363
  248. package/dist/modules/sample-data/service.js.map +1 -1
  249. package/dist/modules/sample-data/tools.d.ts.map +1 -1
  250. package/dist/modules/sample-data/tools.js +4 -3
  251. package/dist/modules/sample-data/tools.js.map +1 -1
  252. package/dist/modules/search/index.d.ts +1 -1
  253. package/dist/modules/search/index.d.ts.map +1 -1
  254. package/dist/modules/search/index.js +2 -1
  255. package/dist/modules/search/index.js.map +1 -1
  256. package/dist/modules/search/repository.d.ts +1 -0
  257. package/dist/modules/search/repository.d.ts.map +1 -1
  258. package/dist/modules/search/repository.js +22 -0
  259. package/dist/modules/search/repository.js.map +1 -1
  260. package/dist/modules/search/service.d.ts +2 -2
  261. package/dist/modules/search/service.d.ts.map +1 -1
  262. package/dist/modules/search/service.js +4 -3
  263. package/dist/modules/search/service.js.map +1 -1
  264. package/dist/modules/smtp-email/index.d.ts +12 -4
  265. package/dist/modules/smtp-email/index.d.ts.map +1 -1
  266. package/dist/modules/smtp-email/index.js +25 -5
  267. package/dist/modules/smtp-email/index.js.map +1 -1
  268. package/dist/modules/workspace/repository.d.ts +2 -0
  269. package/dist/modules/workspace/repository.d.ts.map +1 -1
  270. package/dist/modules/workspace/repository.js +12 -0
  271. package/dist/modules/workspace/repository.js.map +1 -1
  272. package/dist/modules/workspace/routes.d.ts.map +1 -1
  273. package/dist/modules/workspace/routes.js +13 -0
  274. package/dist/modules/workspace/routes.js.map +1 -1
  275. package/dist/modules/workspace/service.d.ts +10 -0
  276. package/dist/modules/workspace/service.d.ts.map +1 -1
  277. package/dist/modules/workspace/service.js +34 -10
  278. package/dist/modules/workspace/service.js.map +1 -1
  279. package/dist/modules/workspace/starters.d.ts +17 -2
  280. package/dist/modules/workspace/starters.d.ts.map +1 -1
  281. package/dist/modules/workspace/starters.js +141 -14
  282. package/dist/modules/workspace/starters.js.map +1 -1
  283. package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
  284. package/dist/modules/workspace/workspaceAccessMiddleware.js +3 -4
  285. package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
  286. package/dist/runtime/events.d.ts +1 -1
  287. package/dist/runtime/events.d.ts.map +1 -1
  288. package/dist/runtime/events.js +2 -0
  289. package/dist/runtime/events.js.map +1 -1
  290. package/dist/runtime/jobs.d.ts +63 -0
  291. package/dist/runtime/jobs.d.ts.map +1 -0
  292. package/dist/runtime/jobs.js +231 -0
  293. package/dist/runtime/jobs.js.map +1 -0
  294. package/dist/runtime/module.d.ts +20 -0
  295. package/dist/runtime/module.d.ts.map +1 -1
  296. package/dist/runtime/registry.d.ts +8 -0
  297. package/dist/runtime/registry.d.ts.map +1 -1
  298. package/dist/runtime/registry.js +117 -7
  299. package/dist/runtime/registry.js.map +1 -1
  300. package/dist/runtime/transaction.d.ts +19 -0
  301. package/dist/runtime/transaction.d.ts.map +1 -1
  302. package/dist/runtime/transaction.js +7 -0
  303. package/dist/runtime/transaction.js.map +1 -1
  304. package/dist/schema/index.d.ts +1 -0
  305. package/dist/schema/index.d.ts.map +1 -1
  306. package/dist/schema/index.js +1 -0
  307. package/dist/schema/index.js.map +1 -1
  308. package/dist/testing/app.d.ts +15 -0
  309. package/dist/testing/app.d.ts.map +1 -1
  310. package/dist/testing/app.js +7 -1
  311. package/dist/testing/app.js.map +1 -1
  312. package/dist/testing/client.d.ts.map +1 -1
  313. package/dist/testing/client.js +9 -1
  314. package/dist/testing/client.js.map +1 -1
  315. package/dist/testing/database.d.ts +7 -0
  316. package/dist/testing/database.d.ts.map +1 -1
  317. package/dist/testing/database.js +26 -2
  318. package/dist/testing/database.js.map +1 -1
  319. package/dist/testing/services.d.ts +7 -0
  320. package/dist/testing/services.d.ts.map +1 -1
  321. package/dist/testing/services.js +8 -1
  322. package/dist/testing/services.js.map +1 -1
  323. package/dist/worker.d.ts +15 -0
  324. package/dist/worker.d.ts.map +1 -0
  325. package/dist/worker.js +19 -0
  326. package/dist/worker.js.map +1 -0
  327. package/migrations/0044_events.sql +79 -0
  328. package/migrations/0045_addresses.sql +66 -0
  329. package/migrations/0046_drop_company_website.sql +66 -0
  330. package/migrations/meta/_journal.json +21 -0
  331. package/package.json +4 -3
  332. package/src/app.ts +25 -1
  333. package/src/boot.ts +21 -2
  334. package/src/index.ts +17 -0
  335. package/src/lib/apiKeyScopes.ts +6 -0
  336. package/src/lib/columns.ts +12 -3
  337. package/src/lib/config.ts +21 -0
  338. package/src/lib/egress.ts +8 -4
  339. package/src/lib/errors.ts +16 -9
  340. package/src/lib/ids.ts +11 -0
  341. package/src/lib/jobs.ts +126 -0
  342. package/src/lib/kelpieConfigFile.ts +69 -1
  343. package/src/modules/agent-tasks/catalog.ts +33 -1
  344. package/src/modules/agent-tasks/index.ts +1 -0
  345. package/src/modules/agent-tasks/resolve.ts +11 -0
  346. package/src/modules/attachedRecords.ts +85 -13
  347. package/src/modules/auth/index.ts +1 -0
  348. package/src/modules/auth/repository.ts +34 -3
  349. package/src/modules/auth/routes.ts +44 -1
  350. package/src/modules/auth/service.ts +118 -4
  351. package/src/modules/companies/repository.ts +1 -0
  352. package/src/modules/companies/routes.ts +6 -11
  353. package/src/modules/companies/schema.ts +3 -3
  354. package/src/modules/companies/service.ts +6 -10
  355. package/src/modules/core.ts +2 -0
  356. package/src/modules/custom-fields/repository.ts +2 -0
  357. package/src/modules/dashboard/index.ts +1 -0
  358. package/src/modules/dashboard/repository.ts +64 -0
  359. package/src/modules/dashboard/routes.ts +15 -0
  360. package/src/modules/dashboard/service.ts +48 -0
  361. package/src/modules/decisions/repository.ts +2 -0
  362. package/src/modules/events/attendances.ts +245 -0
  363. package/src/modules/events/catalog.ts +36 -0
  364. package/src/modules/events/index.ts +54 -0
  365. package/src/modules/events/repository.ts +396 -0
  366. package/src/modules/events/routes.ts +405 -0
  367. package/src/modules/events/schema.ts +148 -0
  368. package/src/modules/events/service.ts +443 -0
  369. package/src/modules/events/tools.ts +118 -0
  370. package/src/modules/forms/applySubmissionFields.ts +7 -5
  371. package/src/modules/forms/fillAddresses.ts +185 -0
  372. package/src/modules/forms/repository.ts +3 -3
  373. package/src/modules/forms/routes.ts +2 -2
  374. package/src/modules/forms/schema.ts +2 -2
  375. package/src/modules/forms/service.ts +2 -2
  376. package/src/modules/forms/submission.ts +34 -4
  377. package/src/modules/hiring/roles.ts +2 -1
  378. package/src/modules/import-export/addresses.ts +150 -0
  379. package/src/modules/import-export/drafts.ts +7 -9
  380. package/src/modules/import-export/exportRows.ts +3 -3
  381. package/src/modules/import-export/plan.ts +30 -2
  382. package/src/modules/import-export/presets.ts +55 -7
  383. package/src/modules/import-export/repository.ts +5 -7
  384. package/src/modules/import-export/writes.ts +8 -4
  385. package/src/modules/people/repository.ts +1 -0
  386. package/src/modules/people/routes.ts +6 -6
  387. package/src/modules/people/schema.ts +7 -5
  388. package/src/modules/people/service.ts +7 -5
  389. package/src/modules/plans/repository.ts +2 -2
  390. package/src/modules/plans/routes.ts +6 -6
  391. package/src/modules/plans/schema.ts +4 -4
  392. package/src/modules/plans/service.ts +3 -3
  393. package/src/modules/plans/tools.ts +2 -2
  394. package/src/modules/recordTargets.ts +14 -0
  395. package/src/modules/references.ts +2 -0
  396. package/src/modules/sample-data/fixture.ts +208 -27
  397. package/src/modules/sample-data/index.ts +7 -2
  398. package/src/modules/sample-data/routes.ts +2 -0
  399. package/src/modules/sample-data/service.ts +166 -49
  400. package/src/modules/sample-data/tools.ts +4 -3
  401. package/src/modules/search/index.ts +2 -1
  402. package/src/modules/search/repository.ts +32 -1
  403. package/src/modules/search/service.ts +6 -4
  404. package/src/modules/smtp-email/index.ts +32 -10
  405. package/src/modules/workspace/repository.ts +18 -0
  406. package/src/modules/workspace/routes.ts +16 -0
  407. package/src/modules/workspace/service.ts +59 -14
  408. package/src/modules/workspace/starters.ts +275 -14
  409. package/src/modules/workspace/workspaceAccessMiddleware.ts +3 -4
  410. package/src/runtime/events.ts +2 -0
  411. package/src/runtime/jobs.ts +358 -0
  412. package/src/runtime/module.ts +20 -0
  413. package/src/runtime/registry.ts +146 -8
  414. package/src/runtime/transaction.ts +43 -0
  415. package/src/schema/index.ts +1 -0
  416. package/src/testing/app.ts +22 -1
  417. package/src/testing/client.ts +11 -1
  418. package/src/testing/database.ts +35 -2
  419. package/src/testing/services.ts +15 -1
  420. package/src/worker.ts +20 -0
@@ -0,0 +1,358 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import { PgBoss, fromDrizzle } from 'pg-boss'
3
+ import type { Job } from 'pg-boss'
4
+
5
+ import { describeThrown } from '../lib/errors.ts'
6
+ import type { Logger } from '../lib/logger.ts'
7
+ import type {
8
+ EnqueueOptions,
9
+ JobContext,
10
+ JobDefinition,
11
+ JobHandle,
12
+ JobRegistry,
13
+ } from '../lib/jobs.ts'
14
+ import { deadLetterQueueName } from '../lib/jobs.ts'
15
+ import type { Transaction } from './transaction.ts'
16
+
17
+ /**
18
+ * The pg-boss provider that binds the job port defined in `lib/jobs.ts` to a
19
+ * real Postgres-backed queue.
20
+ *
21
+ * Two phases:
22
+ *
23
+ * 1. Registration, at boot. Modules call `context.jobs.define(...)` during
24
+ * `register`. The registry records the definition (fails boot on a
25
+ * duplicate name) and returns the handle. Nothing touches the database
26
+ * yet.
27
+ * 2. `start()`, from the entry point. Opens the pg-boss instance, primes the
28
+ * queue cache, and calls `createQueue` for every registered handle plus
29
+ * its dead-letter queue. Idempotent (`create_queue` upserts).
30
+ *
31
+ * The API and the worker both call `start()`: the API needs a boss instance
32
+ * to insert jobs on request-scoped transactions; the worker adds a
33
+ * `startWorking()` step that runs `work()` per handle.
34
+ *
35
+ * @see modules.md for how jobs sit next to the event bus.
36
+ */
37
+
38
+ /**
39
+ * The pg-boss schema name. Fixed here rather than configurable per assembly:
40
+ * self-hosters run one Postgres per Kelpie install and would gain nothing
41
+ * from renaming the schema.
42
+ */
43
+ const PGBOSS_SCHEMA = 'pgboss'
44
+
45
+ const DEFAULT_LOCAL_CONCURRENCY = 5
46
+
47
+ interface RegisteredJob<Data> {
48
+ readonly definition: JobDefinition<Data>
49
+ readonly handle: JobHandle<Data>
50
+ }
51
+
52
+ export interface JobsRuntimeOptions {
53
+ readonly connectionString: string
54
+ readonly logger: Logger
55
+ /**
56
+ * Polling floor. LISTEN/NOTIFY wakes a worker immediately when a job is
57
+ * inserted via `send()`, but pg-boss's own retry re-insert (inside
58
+ * `fail()`) never notifies, so a retried job is only ever picked up by
59
+ * this poll — as is a fresh job if the listener never opened. Also used
60
+ * as pg-boss's `notifyPollingIntervalSeconds`, so NOTIFY-active queues
61
+ * poll no slower than this either. Default 2s in production; tests pass
62
+ * a shorter value so a retry runs within the test's own budget.
63
+ */
64
+ readonly pollingIntervalSeconds?: number
65
+ /**
66
+ * How many workers the runtime spins up per handle when a definition does
67
+ * not override `defaults.localConcurrency`. Matches pg-boss's default.
68
+ */
69
+ readonly defaultLocalConcurrency?: number
70
+ }
71
+
72
+ export interface JobsRuntime {
73
+ /** The port modules bind to at register time. */
74
+ readonly registry: JobRegistry
75
+ /** True once `register` has been called with this name. Used by tests. */
76
+ hasHandle(name: string): boolean
77
+ /**
78
+ * Applies pg-boss's own schema migration. Idempotent: safe to run on every
79
+ * boot. Uses a throwaway `PgBoss` in `migrate: true, createSchema: true`
80
+ * mode, matching the release-command flow the cloud runs.
81
+ */
82
+ migrate(): Promise<void>
83
+ /**
84
+ * Opens the persistent pg-boss instance and declares every registered
85
+ * queue plus its dead-letter counterpart. Must run before `enqueueOnTx`
86
+ * or `startWorking`. `migrate: false` here: `migrate()` above owns that
87
+ * step, so instances never race the migration.
88
+ */
89
+ start(): Promise<void>
90
+ /**
91
+ * Starts the `work()` loop for every registered handle. The worker entry
92
+ * point calls this; the API calls it too unless `--no-worker` is set.
93
+ * Handler errors are logged and rethrown so pg-boss retries or
94
+ * dead-letters the job.
95
+ */
96
+ startWorking(): Promise<void>
97
+ /**
98
+ * Enqueues a job on the caller's transaction via `fromDrizzle(tx, sql)`.
99
+ * The insert commits with the surrounding write; a rollback discards it.
100
+ * Rejects when no `handle` was registered for `name` (this would be a
101
+ * boot-time bug, not a runtime one).
102
+ */
103
+ enqueueOnTx<Data>(
104
+ tx: Transaction,
105
+ handle: JobHandle<Data>,
106
+ data: Data,
107
+ options?: EnqueueOptions,
108
+ ): Promise<string | null>
109
+ /**
110
+ * Drains in-flight work and closes the pg-boss instance. `offWork({wait:
111
+ * true})` awaits every handler mid-flight; `boss.stop` then tears down
112
+ * the connection pool.
113
+ */
114
+ stop(): Promise<void>
115
+ }
116
+
117
+ export function createJobsRuntime(options: JobsRuntimeOptions): JobsRuntime {
118
+ const registered = new Map<string, RegisteredJob<unknown>>()
119
+ // Tracks which handles have had `createQueue` run and which have had
120
+ // `work()` called, so a runtime that registers a fresh handle after
121
+ // `start()` (a test that adds one mid-suite) still gets its queue built
122
+ // and its worker started without redoing the ones already up.
123
+ const queuesCreated = new Set<string>()
124
+ const workersStarted = new Set<string>()
125
+ let boss: PgBoss | undefined
126
+
127
+ function requireBoss(): PgBoss {
128
+ if (boss === undefined) {
129
+ throw new Error('jobs runtime used before start(): call jobs.start() at boot')
130
+ }
131
+ return boss
132
+ }
133
+
134
+ const registry: JobRegistry = {
135
+ define<Data>(definition: JobDefinition<Data>): JobHandle<Data> {
136
+ if (registered.has(definition.name)) {
137
+ throw new Error(`job "${definition.name}" is defined twice`)
138
+ }
139
+
140
+ const handle: JobHandle<Data> = { name: definition.name }
141
+ registered.set(definition.name, {
142
+ definition: definition as JobDefinition<unknown>,
143
+ handle: handle as JobHandle<unknown>,
144
+ })
145
+
146
+ return handle
147
+ },
148
+ }
149
+
150
+ async function migrate(): Promise<void> {
151
+ // A throwaway instance in migrate/createSchema mode. Cheaper than
152
+ // vendoring the SQL: pg-boss owns its schema, and `getConstructionPlans`
153
+ // / `getMigrationPlans` produce statements we would just execute in the
154
+ // same order the library does itself. `supervise: false, schedule: false`
155
+ // keeps the instance from spawning any background work while it lives.
156
+ const migrator = new PgBoss({
157
+ connectionString: options.connectionString,
158
+ schema: PGBOSS_SCHEMA,
159
+ migrate: true,
160
+ createSchema: true,
161
+ supervise: false,
162
+ schedule: false,
163
+ })
164
+
165
+ try {
166
+ await migrator.start()
167
+ } finally {
168
+ await migrator.stop({ graceful: false, close: true, timeout: 5_000 })
169
+ }
170
+ }
171
+
172
+ async function ensureQueuesFor(instance: PgBoss): Promise<void> {
173
+ for (const { definition } of registered.values()) {
174
+ if (queuesCreated.has(definition.name)) {
175
+ continue
176
+ }
177
+
178
+ const deadLetter = deadLetterQueueName(definition.name)
179
+
180
+ // Dead letter queue must exist before the main queue references it.
181
+ await instance.createQueue(deadLetter, {
182
+ policy: 'standard',
183
+ notify: false,
184
+ })
185
+
186
+ await instance.createQueue(definition.name, {
187
+ policy: 'standard',
188
+ deadLetter,
189
+ notify: true,
190
+ ...(definition.defaults?.retryLimit === undefined
191
+ ? {}
192
+ : { retryLimit: definition.defaults.retryLimit }),
193
+ ...(definition.defaults?.retryDelay === undefined
194
+ ? {}
195
+ : { retryDelay: definition.defaults.retryDelay }),
196
+ ...(definition.defaults?.retryBackoff === undefined
197
+ ? {}
198
+ : { retryBackoff: definition.defaults.retryBackoff }),
199
+ ...(definition.defaults?.retryDelayMax === undefined
200
+ ? {}
201
+ : { retryDelayMax: definition.defaults.retryDelayMax }),
202
+ ...(definition.defaults?.expireInSeconds === undefined
203
+ ? {}
204
+ : { expireInSeconds: definition.defaults.expireInSeconds }),
205
+ })
206
+
207
+ queuesCreated.add(definition.name)
208
+ }
209
+ }
210
+
211
+ async function start(): Promise<void> {
212
+ if (boss === undefined) {
213
+ const instance = new PgBoss({
214
+ connectionString: options.connectionString,
215
+ schema: PGBOSS_SCHEMA,
216
+ migrate: false,
217
+ createSchema: false,
218
+ supervise: false,
219
+ schedule: false,
220
+ useListenNotify: true,
221
+ })
222
+
223
+ instance.on('error', (error: unknown) => {
224
+ options.logger.error('pg-boss error', { error: describeThrown(error) })
225
+ })
226
+
227
+ await instance.start()
228
+ boss = instance
229
+ }
230
+
231
+ await ensureQueuesFor(boss)
232
+ }
233
+
234
+ async function startWorking(): Promise<void> {
235
+ const instance = requireBoss()
236
+ const pollingIntervalSeconds = options.pollingIntervalSeconds ?? 2
237
+ const defaultLocalConcurrency = options.defaultLocalConcurrency ?? DEFAULT_LOCAL_CONCURRENCY
238
+
239
+ for (const { definition } of registered.values()) {
240
+ if (workersStarted.has(definition.name)) {
241
+ continue
242
+ }
243
+
244
+ const localConcurrency = definition.defaults?.localConcurrency ?? defaultLocalConcurrency
245
+
246
+ await instance.work(
247
+ definition.name,
248
+ {
249
+ batchSize: 1,
250
+ localConcurrency,
251
+ pollingIntervalSeconds,
252
+ notifyPollingIntervalSeconds: pollingIntervalSeconds,
253
+ },
254
+ async (batch: Job<unknown>[]) => {
255
+ const job = batch[0]
256
+ if (job === undefined) {
257
+ return
258
+ }
259
+
260
+ const parsed = definition.schema.safeParse(job.data)
261
+
262
+ if (!parsed.success) {
263
+ options.logger.error('job payload rejected by schema', {
264
+ job: definition.name,
265
+ id: job.id,
266
+ issues: parsed.error.issues,
267
+ })
268
+ throw new Error(
269
+ `job "${definition.name}" payload does not match its schema: ${parsed.error.issues
270
+ .map((issue) => issue.message)
271
+ .join('; ')}`,
272
+ )
273
+ }
274
+
275
+ const jobLogger = options.logger.child({ job: definition.name, jobId: job.id })
276
+ const context: JobContext<unknown> = {
277
+ id: job.id,
278
+ data: parsed.data,
279
+ log: jobLogger,
280
+ signal: job.signal,
281
+ }
282
+
283
+ try {
284
+ await definition.handler(context)
285
+ } catch (error: unknown) {
286
+ jobLogger.error('job handler failed', { error: describeThrown(error) })
287
+ throw error
288
+ }
289
+ },
290
+ )
291
+
292
+ workersStarted.add(definition.name)
293
+ }
294
+ }
295
+
296
+ async function enqueueOnTx<Data>(
297
+ tx: Transaction,
298
+ handle: JobHandle<Data>,
299
+ data: Data,
300
+ enqueueOptions: EnqueueOptions = {},
301
+ ): Promise<string | null> {
302
+ const instance = requireBoss()
303
+ const entry = registered.get(handle.name)
304
+
305
+ if (entry === undefined) {
306
+ throw new Error(`enqueue for unregistered job "${handle.name}"`)
307
+ }
308
+
309
+ return instance.send(
310
+ handle.name,
311
+ data as object,
312
+ {
313
+ db: fromDrizzle(tx, sql),
314
+ ...(enqueueOptions.retryLimit === undefined ? {} : { retryLimit: enqueueOptions.retryLimit }),
315
+ ...(enqueueOptions.retryBackoff === undefined
316
+ ? {}
317
+ : { retryBackoff: enqueueOptions.retryBackoff }),
318
+ ...(enqueueOptions.singletonKey === undefined
319
+ ? {}
320
+ : { singletonKey: enqueueOptions.singletonKey }),
321
+ ...(enqueueOptions.startAfter === undefined ? {} : { startAfter: enqueueOptions.startAfter }),
322
+ },
323
+ )
324
+ }
325
+
326
+ async function stop(): Promise<void> {
327
+ const instance = boss
328
+ if (instance === undefined) {
329
+ return
330
+ }
331
+
332
+ for (const name of workersStarted) {
333
+ try {
334
+ await instance.offWork(name, { wait: true })
335
+ } catch (error: unknown) {
336
+ options.logger.error('offWork failed', {
337
+ job: name,
338
+ error: describeThrown(error),
339
+ })
340
+ }
341
+ }
342
+ workersStarted.clear()
343
+
344
+ await instance.stop({ graceful: true, close: true, timeout: 30_000 })
345
+ boss = undefined
346
+ queuesCreated.clear()
347
+ }
348
+
349
+ return {
350
+ registry,
351
+ hasHandle: (name) => registered.has(name),
352
+ migrate,
353
+ start,
354
+ startWorking,
355
+ enqueueOnTx,
356
+ stop,
357
+ }
358
+ }
@@ -5,6 +5,7 @@ import type { Actor } from '../lib/actor.ts'
5
5
  import type { Database } from '../lib/database.ts'
6
6
  import type { EmailSender } from '../lib/email.ts'
7
7
  import type { IdFactory } from '../lib/ids.ts'
8
+ import type { JobRegistry } from '../lib/jobs.ts'
8
9
  import type { Logger } from '../lib/logger.ts'
9
10
  import type { SecretEncryptionConfig } from '../lib/secrets.ts'
10
11
  import type { EntitlementRegistry } from './entitlements.ts'
@@ -133,6 +134,14 @@ export interface ModuleServices {
133
134
  * `secretEncryption`. Same fallback pattern as `appBaseUrl`.
134
135
  */
135
136
  readonly secretEncryption?: SecretEncryptionConfig | undefined
137
+ /**
138
+ * Whether the instance accepts a brand-new account, from `kelpie.config.ts`'s
139
+ * `signups` (or `SIGNUPS`). Undefined defaults to `true` (open), the same
140
+ * default `loadConfig` and `resolveKelpieConfig` use, so a caller that has
141
+ * not thought about it — a test, an older assembly — sees no change in
142
+ * behaviour.
143
+ */
144
+ readonly signupsEnabled?: boolean
136
145
  }
137
146
 
138
147
  export interface ModuleContext extends ModuleServices {
@@ -232,6 +241,17 @@ export interface ModuleContext extends ModuleServices {
232
241
  * commits, and must be idempotent.
233
242
  */
234
243
  readonly events: EventBus
244
+ /**
245
+ * Declares a background job. Modules call `context.jobs.define(...)` at
246
+ * register time and receive a typed handle to enqueue against. The insert
247
+ * lives inside the caller's transaction (`services.transaction`) so a
248
+ * rollback discards the job.
249
+ *
250
+ * A second `define` with the same name fails boot. Handlers run out of
251
+ * process on the worker entry point, or inline when the API runs its own
252
+ * worker.
253
+ */
254
+ readonly jobs: JobRegistry
235
255
  /**
236
256
  * Declare capabilities and check grants. Every check is granted and unlimited
237
257
  * until a module registers a provider.
@@ -7,6 +7,7 @@ import type { Environment } from '../lib/config.ts'
7
7
  import { createLogEmailSender } from '../lib/email.ts'
8
8
  import type { EmailMessage, EmailSender } from '../lib/email.ts'
9
9
  import { AppError, describeThrown, describeValidationIssue, toErrorDetails } from '../lib/errors.ts'
10
+ import type { JobRegistry } from '../lib/jobs.ts'
10
11
  import type { Logger } from '../lib/logger.ts'
11
12
  import { createEntitlementRegistry, requireCapability } from './entitlements.ts'
12
13
  import type { EntitlementRegistry } from './entitlements.ts'
@@ -87,6 +88,117 @@ function assertMountablePath(moduleId: string, kind: string, path: string): void
87
88
  }
88
89
  }
89
90
 
91
+ /** Verb methods a module can call on the router — full Hono set. */
92
+ const ROUTER_VERB_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'all'] as const
93
+ type RouterVerbMethod = (typeof ROUTER_VERB_METHODS)[number]
94
+
95
+ function isRouterVerbMethod(name: string): name is RouterVerbMethod {
96
+ return (ROUTER_VERB_METHODS as readonly string[]).includes(name)
97
+ }
98
+
99
+ /**
100
+ * Wraps a Hono router so every route the module registers runs `gate` first,
101
+ * and only that module's own routes. The straightforward alternative —
102
+ * `router.use('*', gate)` — attaches at `/v1/*` on the app once the router is
103
+ * mounted (Hono flattens sub-routers onto the parent prefix), so the gate
104
+ * fires for every module's routes, not just this module's. Prepending the gate
105
+ * to each declared route keeps the check local.
106
+ *
107
+ * The full router surface is covered so a future contributor cannot silently
108
+ * reintroduce the leak by reaching for a method that was not wrapped:
109
+ *
110
+ * - HTTP verbs and `on`: prepend `gate`, then the module's handlers.
111
+ * - `use(pattern, ...)` at a specific path: prepend `gate`, then the mw.
112
+ * - `route(subPath, subApp)`: recursively wrap `subApp` before mounting, so
113
+ * every route it contributes still runs the gate first.
114
+ *
115
+ * And the calls that cannot be made module-local through wrapping fail at
116
+ * boot with a message naming the module, rather than shipping a subtle leak:
117
+ *
118
+ * - `use()` with no path, `use('*')`, `use('/*')`, `use('/')`: any of these
119
+ * register a middleware at `/v1/*` on the parent app once the router is
120
+ * mounted — the exact bug this wrapper prevents. A module that wants to
121
+ * share middleware across many of its routes must name a specific path
122
+ * (`/things/*`) or attach the middleware per route.
123
+ * - `notFound` / `onError`: fire for every unmatched or failing `/v1`
124
+ * request once the router is flattened, so a module's handler would run
125
+ * for other modules' traffic. Handle failures inside the module's own
126
+ * route handlers instead.
127
+ */
128
+ function gateRouter(router: Hono, moduleId: string, gate: MiddlewareHandler): Hono {
129
+ const proxy: Hono = new Proxy(router, {
130
+ get(target, property, receiver) {
131
+ if (typeof property !== 'string') {
132
+ return Reflect.get(target, property, receiver)
133
+ }
134
+
135
+ if (isRouterVerbMethod(property)) {
136
+ return (path: string, ...handlers: Handler[]): Hono => {
137
+ const method = target[property] as (path: string, ...handlers: Handler[]) => Hono
138
+ method.call(target, path, gate, ...handlers)
139
+ return proxy
140
+ }
141
+ }
142
+
143
+ if (property === 'on') {
144
+ return (
145
+ method: string | string[],
146
+ path: string | string[],
147
+ ...handlers: Handler[]
148
+ ): Hono => {
149
+ target.on(method as never, path as never, gate, ...handlers)
150
+ return proxy
151
+ }
152
+ }
153
+
154
+ if (property === 'use') {
155
+ return (...args: unknown[]): Hono => {
156
+ const first = args[0]
157
+ const isAppWide =
158
+ typeof first !== 'string' || first === '*' || first === '/*' || first === '/'
159
+
160
+ if (isAppWide) {
161
+ throw new ModuleBootError([
162
+ `module "${moduleId}" called router.use() with an app-wide pattern (${
163
+ typeof first === 'string' ? `"${first}"` : 'no path'
164
+ }). Hono flattens the module router onto "/v1", so this would fire for every module's routes. Name a specific path ("/things/*") or attach middleware per route.`,
165
+ ])
166
+ }
167
+
168
+ const [pattern, ...handlers] = args as [string, ...MiddlewareHandler[]]
169
+ target.use(pattern, gate, ...handlers)
170
+ return proxy
171
+ }
172
+ }
173
+
174
+ if (property === 'route') {
175
+ return (path: string, subApp: Hono): Hono => {
176
+ // The sub-app's routes were registered on it before this call, so a
177
+ // proxy over `subApp` cannot retroactively prepend the gate to them.
178
+ // Instead attach the gate as middleware scoped to the sub-router's
179
+ // mount prefix on this router — once flattened onto `/v1`, the
180
+ // middleware covers exactly `/v1${path}/*`, which is the module's
181
+ // own routes. `use` must land before `route` so composition order
182
+ // runs the gate first.
183
+ target.use(`${path.replace(/\/+$/, '')}/*`, gate)
184
+ target.route(path, subApp)
185
+ return proxy
186
+ }
187
+ }
188
+
189
+ if (property === 'notFound' || property === 'onError') {
190
+ throw new ModuleBootError([
191
+ `module "${moduleId}" called router.${property}(). A module router's ${property} fires for every unmatched or failing "/v1" request once the router is mounted; handle failures inside the module's own route handlers instead.`,
192
+ ])
193
+ }
194
+
195
+ return Reflect.get(target, property, receiver)
196
+ },
197
+ }) as Hono
198
+
199
+ return proxy
200
+ }
201
+
90
202
  /**
91
203
  * What `registerModules` needs.
92
204
  *
@@ -130,6 +242,13 @@ export interface ModuleRuntimeOptions {
130
242
  * two modules had registered it.
131
243
  */
132
244
  readonly additionalEmailProviders?: ReadonlyMap<string, EmailSender> | undefined
245
+ /**
246
+ * The job registry modules bind to at register time. Bound to the pg-boss
247
+ * runtime in production (`runtime/jobs.ts`) and to a stub in unit tests
248
+ * that never define a job. A module that calls `context.jobs.define` when
249
+ * this is absent gets a boot-time error naming the module.
250
+ */
251
+ readonly jobs?: JobRegistry | undefined
133
252
  /**
134
253
  * The deploy-time module override (`lib/moduleConfig.ts`), parsed. A locked
135
254
  * module id wins over whatever a workspace's own settings say.
@@ -238,6 +357,21 @@ interface RegisteredProvider {
238
357
  readonly registeredBy: string
239
358
  }
240
359
 
360
+ /**
361
+ * Refuses `define` for a module that reached `context.jobs` when the runtime
362
+ * booted without a jobs registry. Named so a boot log points at the module
363
+ * whose declaration cannot be honoured, rather than a bare stack trace.
364
+ */
365
+ function createMissingJobsRegistry(moduleId: string): JobRegistry {
366
+ return {
367
+ define() {
368
+ throw new ModuleBootError([
369
+ `module "${moduleId}" defines a job but the assembly booted without a jobs runtime`,
370
+ ])
371
+ },
372
+ }
373
+ }
374
+
241
375
  function createModuleContext(
242
376
  module: KelpieModule,
243
377
  accumulator: Accumulator,
@@ -252,6 +386,7 @@ function createModuleContext(
252
386
  return {
253
387
  ...options.services,
254
388
  email: emailProxy,
389
+ jobs: options.jobs ?? createMissingJobsRegistry(module.id),
255
390
 
256
391
  provideExternalSignIn(handler) {
257
392
  externalSignIn.install(module.id, handler)
@@ -282,22 +417,25 @@ function createModuleContext(
282
417
  routes(mount) {
283
418
  const router = new Hono()
284
419
 
285
- // Gate first, so it runs before anything `mount` adds: Hono composes a
286
- // path's handlers in the order they were registered on this router,
287
- // middleware included, and registering after the routes it should guard
288
- // would run it too late to block them.
420
+ // Gate must run before anything `mount` adds. A `router.use('*', gate)`
421
+ // here would attach at `/v1/*` on the app after mounting (Hono flattens
422
+ // the sub-router onto the parent prefix), which would fire the gate for
423
+ // every module's routes, not just this one's. So the gate is prepended
424
+ // to each individual route through `gateRouter` instead.
289
425
  if (module.structural !== true && options.resolveActor !== undefined) {
290
426
  const { resolveActor } = options
291
-
292
- router.use('*', async (context, next) => {
427
+ const gate: MiddlewareHandler = async (context, next) => {
293
428
  const actor = await resolveActor(context)
294
429
 
295
430
  await requireCapability(entitlements, requireWorkspaceId(actor), moduleCapabilityName(module.id))
296
431
  await next()
297
- })
432
+ }
433
+
434
+ mount(gateRouter(router, module.id, gate))
435
+ } else {
436
+ mount(router)
298
437
  }
299
438
 
300
- mount(router)
301
439
  accumulator.routers.push({ moduleId: module.id, router })
302
440
  },
303
441
 
@@ -2,6 +2,11 @@ import type { EventActor, EventTarget, KelpieEvent } from '@kelpie/schemas'
2
2
 
3
3
  import type { Database } from '../lib/database.ts'
4
4
  import type { IdFactory } from '../lib/ids.ts'
5
+ import type {
6
+ EnqueueOptions,
7
+ JobHandle,
8
+ TransactionJobs,
9
+ } from '../lib/jobs.ts'
5
10
  import type { Logger } from '../lib/logger.ts'
6
11
  import { checkEventCycle, currentEventChain } from './events.ts'
7
12
  import type { EventBus, EventName, KelpieEventMap } from './events.ts'
@@ -43,6 +48,13 @@ export interface BufferedEvents {
43
48
  export interface TransactionContext {
44
49
  readonly tx: Transaction
45
50
  readonly events: BufferedEvents
51
+ /**
52
+ * Enqueues background work on the caller's transaction. The insert runs
53
+ * inside `tx` via pg-boss's Drizzle adapter, so a rolled-back scope
54
+ * discards the job with the write. Unlike `events`, this is not buffered:
55
+ * the return value carries the pg-boss job id, awaited inside the scope.
56
+ */
57
+ readonly jobs: TransactionJobs
46
58
  }
47
59
 
48
60
  export interface TransactionOptions {
@@ -69,6 +81,17 @@ interface BufferedEvent {
69
81
 
70
82
  const SYSTEM_ACTOR: EventActor = { kind: 'system' }
71
83
 
84
+ /**
85
+ * Inserts a background job on `tx`. Bound to the pg-boss provider in
86
+ * production (`runtime/jobs.ts`) and to a stub in tests that never enqueue.
87
+ */
88
+ export type EnqueueOnTransaction = <Data>(
89
+ tx: Transaction,
90
+ handle: JobHandle<Data>,
91
+ data: Data,
92
+ options?: EnqueueOptions,
93
+ ) => Promise<string | null>
94
+
72
95
  export interface TransactionScopeDependencies {
73
96
  readonly db: Database
74
97
  readonly bus: EventBus
@@ -78,12 +101,27 @@ export interface TransactionScopeDependencies {
78
101
  readonly now?: () => Date
79
102
  /** Chain-depth cap. Reads `KELPIE_EVENT_MAX_DEPTH`; otherwise 8. */
80
103
  readonly maxDepth?: number
104
+ /**
105
+ * How `tx.jobs.enqueue(...)` reaches pg-boss. Optional so unit tests that
106
+ * never enqueue can build the scope without a runtime; a call from a
107
+ * scope built without one rejects with a boot-time bug message.
108
+ */
109
+ readonly enqueueOnTx?: EnqueueOnTransaction
81
110
  }
82
111
 
83
112
  export function createTransactionScope(dependencies: TransactionScopeDependencies): TransactionScope {
84
113
  const now = dependencies.now ?? ((): Date => new Date())
85
114
  const maxDepth = dependencies.maxDepth ?? readMaxDepthFromEnv() ?? 8
86
115
 
116
+ const enqueueOnTx: EnqueueOnTransaction =
117
+ dependencies.enqueueOnTx ??
118
+ (() =>
119
+ Promise.reject(
120
+ new Error(
121
+ 'jobs.enqueue used but no jobs runtime was wired into the transaction scope',
122
+ ),
123
+ ))
124
+
87
125
  return async function runInTransaction(work, options) {
88
126
  const actor = options?.actor ?? SYSTEM_ACTOR
89
127
  const workspaceId = options?.workspaceId
@@ -92,6 +130,11 @@ export function createTransactionScope(dependencies: TransactionScopeDependencie
92
130
  const result = await dependencies.db.transaction((tx) =>
93
131
  work({
94
132
  tx,
133
+ jobs: {
134
+ enqueue(handle, data, enqueueOptions) {
135
+ return enqueueOnTx(tx, handle, data, enqueueOptions)
136
+ },
137
+ },
95
138
  events: {
96
139
  emit(name, target, data) {
97
140
  if (workspaceId === undefined || workspaceId.length === 0) {
@@ -20,6 +20,7 @@ export * from '../modules/opportunities/schema.ts'
20
20
  export * from '../modules/partnerships/schema.ts'
21
21
  export * from '../modules/raises/schema.ts'
22
22
  export * from '../modules/hiring/schema.ts'
23
+ export * from '../modules/events/schema.ts'
23
24
  export * from '../modules/plans/schema.ts'
24
25
  export * from '../modules/decisions/schema.ts'
25
26
  export * from '../modules/notes/schema.ts'