@heavybit/shared-lib 1.3.2 → 2.7.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 (366) hide show
  1. package/README.md +261 -0
  2. package/dist/cjs/access/index.js +9 -1
  3. package/dist/cjs/access/index.js.map +1 -1
  4. package/dist/cjs/approvals/index.js +137 -0
  5. package/dist/cjs/approvals/index.js.map +1 -0
  6. package/dist/cjs/audit/index.js +74 -0
  7. package/dist/cjs/audit/index.js.map +1 -0
  8. package/dist/cjs/cache/index.js +90 -16
  9. package/dist/cjs/cache/index.js.map +1 -1
  10. package/dist/cjs/cache/keyspace.js +101 -0
  11. package/dist/cjs/cache/keyspace.js.map +1 -0
  12. package/dist/cjs/common/circuit-breaker.js +74 -0
  13. package/dist/cjs/common/circuit-breaker.js.map +1 -0
  14. package/dist/cjs/common/errors.js +44 -5
  15. package/dist/cjs/common/errors.js.map +1 -1
  16. package/dist/cjs/common/index.js +4 -0
  17. package/dist/cjs/common/index.js.map +1 -1
  18. package/dist/cjs/common/internal-client.js +177 -0
  19. package/dist/cjs/common/internal-client.js.map +1 -0
  20. package/dist/cjs/common/internal-constants.js +12 -0
  21. package/dist/cjs/common/internal-constants.js.map +1 -0
  22. package/dist/cjs/common/internal-service-auth.js +160 -0
  23. package/dist/cjs/common/internal-service-auth.js.map +1 -0
  24. package/dist/cjs/common/money.js +215 -0
  25. package/dist/cjs/common/money.js.map +1 -0
  26. package/dist/cjs/common/phone.js +63 -11
  27. package/dist/cjs/common/phone.js.map +1 -1
  28. package/dist/cjs/common/response.js +78 -5
  29. package/dist/cjs/common/response.js.map +1 -1
  30. package/dist/cjs/common/time.js +28 -2
  31. package/dist/cjs/common/time.js.map +1 -1
  32. package/dist/cjs/config/index.js +140 -20
  33. package/dist/cjs/config/index.js.map +1 -1
  34. package/dist/cjs/context/index.js +49 -0
  35. package/dist/cjs/context/index.js.map +1 -0
  36. package/dist/cjs/contracts/index.js +0 -2
  37. package/dist/cjs/contracts/index.js.map +1 -1
  38. package/dist/cjs/contracts/notification-dispatch.js +87 -63
  39. package/dist/cjs/contracts/notification-dispatch.js.map +1 -1
  40. package/dist/cjs/database/db.js +37 -0
  41. package/dist/cjs/database/db.js.map +1 -0
  42. package/dist/cjs/database/index.js +22 -51
  43. package/dist/cjs/database/index.js.map +1 -1
  44. package/dist/cjs/database/pagination.js +88 -0
  45. package/dist/cjs/database/pagination.js.map +1 -0
  46. package/dist/cjs/database/pool.js +60 -0
  47. package/dist/cjs/database/pool.js.map +1 -0
  48. package/dist/cjs/database/scoped-repository.js +132 -0
  49. package/dist/cjs/database/scoped-repository.js.map +1 -0
  50. package/dist/cjs/database/scopes.js +166 -0
  51. package/dist/cjs/database/scopes.js.map +1 -0
  52. package/dist/cjs/database/table-names.js +14 -0
  53. package/dist/cjs/database/table-names.js.map +1 -0
  54. package/dist/cjs/database/tables.js +71 -0
  55. package/dist/cjs/database/tables.js.map +1 -0
  56. package/dist/cjs/express/auth.js +88 -41
  57. package/dist/cjs/express/auth.js.map +1 -1
  58. package/dist/cjs/express/index.js +3 -0
  59. package/dist/cjs/express/index.js.map +1 -1
  60. package/dist/cjs/express/internal-caller.js +52 -0
  61. package/dist/cjs/express/internal-caller.js.map +1 -0
  62. package/dist/cjs/express/middleware.js +154 -34
  63. package/dist/cjs/express/middleware.js.map +1 -1
  64. package/dist/cjs/express/respond.js +47 -0
  65. package/dist/cjs/express/respond.js.map +1 -0
  66. package/dist/cjs/express/step-up.js +41 -0
  67. package/dist/cjs/express/step-up.js.map +1 -0
  68. package/dist/cjs/http/index.js +3 -71
  69. package/dist/cjs/http/index.js.map +1 -1
  70. package/dist/cjs/identity/index.js +269 -67
  71. package/dist/cjs/identity/index.js.map +1 -1
  72. package/dist/cjs/identity/step-up.js +52 -0
  73. package/dist/cjs/identity/step-up.js.map +1 -0
  74. package/dist/cjs/index.js +7 -0
  75. package/dist/cjs/index.js.map +1 -1
  76. package/dist/cjs/knex/columns.js +36 -0
  77. package/dist/cjs/knex/columns.js.map +1 -0
  78. package/dist/cjs/knex/index.js +26 -43
  79. package/dist/cjs/knex/index.js.map +1 -1
  80. package/dist/cjs/knex/migration-helpers.js +57 -0
  81. package/dist/cjs/knex/migration-helpers.js.map +1 -0
  82. package/dist/cjs/logging/index.js +76 -40
  83. package/dist/cjs/logging/index.js.map +1 -1
  84. package/dist/cjs/logging/redact.js +112 -0
  85. package/dist/cjs/logging/redact.js.map +1 -0
  86. package/dist/cjs/messaging/base-consumer.js +36 -0
  87. package/dist/cjs/messaging/base-consumer.js.map +1 -0
  88. package/dist/cjs/messaging/consumer.js +142 -73
  89. package/dist/cjs/messaging/consumer.js.map +1 -1
  90. package/dist/cjs/messaging/envelope.js +53 -10
  91. package/dist/cjs/messaging/envelope.js.map +1 -1
  92. package/dist/cjs/messaging/idempotency.js +95 -8
  93. package/dist/cjs/messaging/idempotency.js.map +1 -1
  94. package/dist/cjs/messaging/index.js +3 -1
  95. package/dist/cjs/messaging/index.js.map +1 -1
  96. package/dist/cjs/messaging/outbox.js +97 -55
  97. package/dist/cjs/messaging/outbox.js.map +1 -1
  98. package/dist/cjs/messaging/publisher.js +86 -38
  99. package/dist/cjs/messaging/publisher.js.map +1 -1
  100. package/dist/cjs/messaging/staff-notifier.js +36 -44
  101. package/dist/cjs/messaging/staff-notifier.js.map +1 -1
  102. package/dist/cjs/messaging/staff-recipients.js +57 -0
  103. package/dist/cjs/messaging/staff-recipients.js.map +1 -0
  104. package/dist/cjs/messaging/topology.js +41 -5
  105. package/dist/cjs/messaging/topology.js.map +1 -1
  106. package/dist/cjs/rate-limit/index.js +57 -6
  107. package/dist/cjs/rate-limit/index.js.map +1 -1
  108. package/dist/cjs/rate-limit/keys.js +47 -0
  109. package/dist/cjs/rate-limit/keys.js.map +1 -0
  110. package/dist/cjs/socket/index.js +127 -16
  111. package/dist/cjs/socket/index.js.map +1 -1
  112. package/dist/cjs/socket/ticket.js +124 -0
  113. package/dist/cjs/socket/ticket.js.map +1 -0
  114. package/dist/cjs/testing/index.js +23 -14
  115. package/dist/cjs/testing/index.js.map +1 -1
  116. package/dist/cjs/validation/index.js +27 -3
  117. package/dist/cjs/validation/index.js.map +1 -1
  118. package/dist/esm/access/index.js +9 -1
  119. package/dist/esm/access/index.js.map +1 -1
  120. package/dist/esm/approvals/index.js +127 -0
  121. package/dist/esm/approvals/index.js.map +1 -0
  122. package/dist/esm/audit/index.js +70 -0
  123. package/dist/esm/audit/index.js.map +1 -0
  124. package/dist/esm/cache/index.js +70 -13
  125. package/dist/esm/cache/index.js.map +1 -1
  126. package/dist/esm/cache/keyspace.js +95 -0
  127. package/dist/esm/cache/keyspace.js.map +1 -0
  128. package/dist/esm/common/circuit-breaker.js +70 -0
  129. package/dist/esm/common/circuit-breaker.js.map +1 -0
  130. package/dist/esm/common/errors.js +42 -4
  131. package/dist/esm/common/errors.js.map +1 -1
  132. package/dist/esm/common/index.js +4 -0
  133. package/dist/esm/common/index.js.map +1 -1
  134. package/dist/esm/common/internal-client.js +173 -0
  135. package/dist/esm/common/internal-client.js.map +1 -0
  136. package/dist/esm/common/internal-constants.js +9 -0
  137. package/dist/esm/common/internal-constants.js.map +1 -0
  138. package/dist/esm/common/internal-service-auth.js +150 -0
  139. package/dist/esm/common/internal-service-auth.js.map +1 -0
  140. package/dist/esm/common/money.js +207 -0
  141. package/dist/esm/common/money.js.map +1 -0
  142. package/dist/esm/common/phone.js +63 -11
  143. package/dist/esm/common/phone.js.map +1 -1
  144. package/dist/esm/common/response.js +75 -5
  145. package/dist/esm/common/response.js.map +1 -1
  146. package/dist/esm/common/time.js +27 -2
  147. package/dist/esm/common/time.js.map +1 -1
  148. package/dist/esm/config/index.js +136 -19
  149. package/dist/esm/config/index.js.map +1 -1
  150. package/dist/esm/context/index.js +44 -0
  151. package/dist/esm/context/index.js.map +1 -0
  152. package/dist/esm/contracts/index.js +0 -2
  153. package/dist/esm/contracts/index.js.map +1 -1
  154. package/dist/esm/contracts/notification-dispatch.js +85 -62
  155. package/dist/esm/contracts/notification-dispatch.js.map +1 -1
  156. package/dist/esm/database/db.js +32 -0
  157. package/dist/esm/database/db.js.map +1 -0
  158. package/dist/esm/database/index.js +9 -45
  159. package/dist/esm/database/index.js.map +1 -1
  160. package/dist/esm/database/pagination.js +81 -0
  161. package/dist/esm/database/pagination.js.map +1 -0
  162. package/dist/esm/database/pool.js +51 -0
  163. package/dist/esm/database/pool.js.map +1 -0
  164. package/dist/esm/database/scoped-repository.js +128 -0
  165. package/dist/esm/database/scoped-repository.js.map +1 -0
  166. package/dist/esm/database/scopes.js +157 -0
  167. package/dist/esm/database/scopes.js.map +1 -0
  168. package/dist/esm/database/table-names.js +11 -0
  169. package/dist/esm/database/table-names.js.map +1 -0
  170. package/dist/esm/database/tables.js +61 -0
  171. package/dist/esm/database/tables.js.map +1 -0
  172. package/dist/esm/express/auth.js +87 -40
  173. package/dist/esm/express/auth.js.map +1 -1
  174. package/dist/esm/express/index.js +3 -0
  175. package/dist/esm/express/index.js.map +1 -1
  176. package/dist/esm/express/internal-caller.js +49 -0
  177. package/dist/esm/express/internal-caller.js.map +1 -0
  178. package/dist/esm/express/middleware.js +146 -27
  179. package/dist/esm/express/middleware.js.map +1 -1
  180. package/dist/esm/express/respond.js +42 -0
  181. package/dist/esm/express/respond.js.map +1 -0
  182. package/dist/esm/express/step-up.js +38 -0
  183. package/dist/esm/express/step-up.js.map +1 -0
  184. package/dist/esm/http/index.js +3 -70
  185. package/dist/esm/http/index.js.map +1 -1
  186. package/dist/esm/identity/index.js +246 -64
  187. package/dist/esm/identity/index.js.map +1 -1
  188. package/dist/esm/identity/step-up.js +47 -0
  189. package/dist/esm/identity/step-up.js.map +1 -0
  190. package/dist/esm/index.js +7 -0
  191. package/dist/esm/index.js.map +1 -1
  192. package/dist/esm/knex/columns.js +28 -0
  193. package/dist/esm/knex/columns.js.map +1 -0
  194. package/dist/esm/knex/index.js +8 -34
  195. package/dist/esm/knex/index.js.map +1 -1
  196. package/dist/esm/knex/migration-helpers.js +51 -0
  197. package/dist/esm/knex/migration-helpers.js.map +1 -0
  198. package/dist/esm/logging/index.js +68 -39
  199. package/dist/esm/logging/index.js.map +1 -1
  200. package/dist/esm/logging/redact.js +106 -0
  201. package/dist/esm/logging/redact.js.map +1 -0
  202. package/dist/esm/messaging/base-consumer.js +32 -0
  203. package/dist/esm/messaging/base-consumer.js.map +1 -0
  204. package/dist/esm/messaging/consumer.js +139 -71
  205. package/dist/esm/messaging/consumer.js.map +1 -1
  206. package/dist/esm/messaging/envelope.js +50 -10
  207. package/dist/esm/messaging/envelope.js.map +1 -1
  208. package/dist/esm/messaging/idempotency.js +91 -7
  209. package/dist/esm/messaging/idempotency.js.map +1 -1
  210. package/dist/esm/messaging/index.js +3 -1
  211. package/dist/esm/messaging/index.js.map +1 -1
  212. package/dist/esm/messaging/outbox.js +97 -54
  213. package/dist/esm/messaging/outbox.js.map +1 -1
  214. package/dist/esm/messaging/publisher.js +86 -38
  215. package/dist/esm/messaging/publisher.js.map +1 -1
  216. package/dist/esm/messaging/staff-notifier.js +36 -43
  217. package/dist/esm/messaging/staff-notifier.js.map +1 -1
  218. package/dist/esm/messaging/staff-recipients.js +53 -0
  219. package/dist/esm/messaging/staff-recipients.js.map +1 -0
  220. package/dist/esm/messaging/topology.js +39 -4
  221. package/dist/esm/messaging/topology.js.map +1 -1
  222. package/dist/esm/rate-limit/index.js +41 -6
  223. package/dist/esm/rate-limit/index.js.map +1 -1
  224. package/dist/esm/rate-limit/keys.js +43 -0
  225. package/dist/esm/rate-limit/keys.js.map +1 -0
  226. package/dist/esm/socket/index.js +112 -17
  227. package/dist/esm/socket/index.js.map +1 -1
  228. package/dist/esm/socket/ticket.js +117 -0
  229. package/dist/esm/socket/ticket.js.map +1 -0
  230. package/dist/esm/testing/index.js +24 -15
  231. package/dist/esm/testing/index.js.map +1 -1
  232. package/dist/esm/validation/index.js +27 -2
  233. package/dist/esm/validation/index.js.map +1 -1
  234. package/dist/types/access/index.d.ts +9 -3
  235. package/dist/types/access/index.d.ts.map +1 -1
  236. package/dist/types/alerting/index.d.ts +1 -1
  237. package/dist/types/alerting/index.d.ts.map +1 -1
  238. package/dist/types/approvals/index.d.ts +99 -0
  239. package/dist/types/approvals/index.d.ts.map +1 -0
  240. package/dist/types/audit/index.d.ts +82 -0
  241. package/dist/types/audit/index.d.ts.map +1 -0
  242. package/dist/types/cache/index.d.ts +64 -14
  243. package/dist/types/cache/index.d.ts.map +1 -1
  244. package/dist/types/cache/keyspace.d.ts +84 -0
  245. package/dist/types/cache/keyspace.d.ts.map +1 -0
  246. package/dist/types/common/circuit-breaker.d.ts +51 -0
  247. package/dist/types/common/circuit-breaker.d.ts.map +1 -0
  248. package/dist/types/common/errors.d.ts +48 -7
  249. package/dist/types/common/errors.d.ts.map +1 -1
  250. package/dist/types/common/index.d.ts +4 -0
  251. package/dist/types/common/index.d.ts.map +1 -1
  252. package/dist/types/common/internal-client.d.ts +116 -0
  253. package/dist/types/common/internal-client.d.ts.map +1 -0
  254. package/dist/types/common/internal-constants.d.ts +9 -0
  255. package/dist/types/common/internal-constants.d.ts.map +1 -0
  256. package/dist/types/common/internal-service-auth.d.ts +102 -0
  257. package/dist/types/common/internal-service-auth.d.ts.map +1 -0
  258. package/dist/types/common/money.d.ts +104 -0
  259. package/dist/types/common/money.d.ts.map +1 -0
  260. package/dist/types/common/phone.d.ts +45 -6
  261. package/dist/types/common/phone.d.ts.map +1 -1
  262. package/dist/types/common/response.d.ts +66 -5
  263. package/dist/types/common/response.d.ts.map +1 -1
  264. package/dist/types/common/time.d.ts +20 -2
  265. package/dist/types/common/time.d.ts.map +1 -1
  266. package/dist/types/config/index.d.ts +252 -28
  267. package/dist/types/config/index.d.ts.map +1 -1
  268. package/dist/types/context/index.d.ts +39 -0
  269. package/dist/types/context/index.d.ts.map +1 -0
  270. package/dist/types/contracts/index.d.ts +0 -2
  271. package/dist/types/contracts/index.d.ts.map +1 -1
  272. package/dist/types/contracts/notification-dispatch.d.ts +204 -86
  273. package/dist/types/contracts/notification-dispatch.d.ts.map +1 -1
  274. package/dist/types/database/db.d.ts +46 -0
  275. package/dist/types/database/db.d.ts.map +1 -0
  276. package/dist/types/database/index.d.ts +9 -22
  277. package/dist/types/database/index.d.ts.map +1 -1
  278. package/dist/types/database/pagination.d.ts +68 -0
  279. package/dist/types/database/pagination.d.ts.map +1 -0
  280. package/dist/types/database/pool.d.ts +39 -0
  281. package/dist/types/database/pool.d.ts.map +1 -0
  282. package/dist/types/database/scoped-repository.d.ts +92 -0
  283. package/dist/types/database/scoped-repository.d.ts.map +1 -0
  284. package/dist/types/database/scopes.d.ts +159 -0
  285. package/dist/types/database/scopes.d.ts.map +1 -0
  286. package/dist/types/database/table-names.d.ts +13 -0
  287. package/dist/types/database/table-names.d.ts.map +1 -0
  288. package/dist/types/database/tables.d.ts +748 -0
  289. package/dist/types/database/tables.d.ts.map +1 -0
  290. package/dist/types/express/auth.d.ts +63 -11
  291. package/dist/types/express/auth.d.ts.map +1 -1
  292. package/dist/types/express/index.d.ts +3 -0
  293. package/dist/types/express/index.d.ts.map +1 -1
  294. package/dist/types/express/internal-caller.d.ts +53 -0
  295. package/dist/types/express/internal-caller.d.ts.map +1 -0
  296. package/dist/types/express/middleware.d.ts +86 -7
  297. package/dist/types/express/middleware.d.ts.map +1 -1
  298. package/dist/types/express/respond.d.ts +41 -0
  299. package/dist/types/express/respond.d.ts.map +1 -0
  300. package/dist/types/express/step-up.d.ts +38 -0
  301. package/dist/types/express/step-up.d.ts.map +1 -0
  302. package/dist/types/http/index.d.ts +3 -31
  303. package/dist/types/http/index.d.ts.map +1 -1
  304. package/dist/types/identity/index.d.ts +159 -21
  305. package/dist/types/identity/index.d.ts.map +1 -1
  306. package/dist/types/identity/step-up.d.ts +41 -0
  307. package/dist/types/identity/step-up.d.ts.map +1 -0
  308. package/dist/types/index.d.ts +7 -0
  309. package/dist/types/index.d.ts.map +1 -1
  310. package/dist/types/knex/columns.d.ts +15 -0
  311. package/dist/types/knex/columns.d.ts.map +1 -0
  312. package/dist/types/knex/index.d.ts +3 -11
  313. package/dist/types/knex/index.d.ts.map +1 -1
  314. package/dist/types/knex/migration-helpers.d.ts +28 -0
  315. package/dist/types/knex/migration-helpers.d.ts.map +1 -0
  316. package/dist/types/logging/index.d.ts +30 -4
  317. package/dist/types/logging/index.d.ts.map +1 -1
  318. package/dist/types/logging/redact.d.ts +54 -0
  319. package/dist/types/logging/redact.d.ts.map +1 -0
  320. package/dist/types/messaging/base-consumer.d.ts +33 -0
  321. package/dist/types/messaging/base-consumer.d.ts.map +1 -0
  322. package/dist/types/messaging/consumer.d.ts +59 -21
  323. package/dist/types/messaging/consumer.d.ts.map +1 -1
  324. package/dist/types/messaging/envelope.d.ts +50 -11
  325. package/dist/types/messaging/envelope.d.ts.map +1 -1
  326. package/dist/types/messaging/idempotency.d.ts +60 -3
  327. package/dist/types/messaging/idempotency.d.ts.map +1 -1
  328. package/dist/types/messaging/index.d.ts +8 -0
  329. package/dist/types/messaging/index.d.ts.map +1 -1
  330. package/dist/types/messaging/outbox.d.ts +62 -22
  331. package/dist/types/messaging/outbox.d.ts.map +1 -1
  332. package/dist/types/messaging/publisher.d.ts +54 -21
  333. package/dist/types/messaging/publisher.d.ts.map +1 -1
  334. package/dist/types/messaging/staff-notifier.d.ts +69 -38
  335. package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
  336. package/dist/types/messaging/staff-recipients.d.ts +50 -0
  337. package/dist/types/messaging/staff-recipients.d.ts.map +1 -0
  338. package/dist/types/messaging/topology.d.ts +40 -8
  339. package/dist/types/messaging/topology.d.ts.map +1 -1
  340. package/dist/types/rate-limit/index.d.ts +40 -4
  341. package/dist/types/rate-limit/index.d.ts.map +1 -1
  342. package/dist/types/rate-limit/keys.d.ts +52 -0
  343. package/dist/types/rate-limit/keys.d.ts.map +1 -0
  344. package/dist/types/socket/index.d.ts +86 -2
  345. package/dist/types/socket/index.d.ts.map +1 -1
  346. package/dist/types/socket/ticket.d.ts +90 -0
  347. package/dist/types/socket/ticket.d.ts.map +1 -0
  348. package/dist/types/testing/index.d.ts +31 -10
  349. package/dist/types/testing/index.d.ts.map +1 -1
  350. package/dist/types/types/index.d.ts +62 -15
  351. package/dist/types/types/index.d.ts.map +1 -1
  352. package/dist/types/validation/index.d.ts +33 -2
  353. package/dist/types/validation/index.d.ts.map +1 -1
  354. package/package.json +3 -1
  355. package/dist/cjs/contracts/editorial-draft.js +0 -120
  356. package/dist/cjs/contracts/editorial-draft.js.map +0 -1
  357. package/dist/cjs/contracts/upload-import.js +0 -54
  358. package/dist/cjs/contracts/upload-import.js.map +0 -1
  359. package/dist/esm/contracts/editorial-draft.js +0 -117
  360. package/dist/esm/contracts/editorial-draft.js.map +0 -1
  361. package/dist/esm/contracts/upload-import.js +0 -51
  362. package/dist/esm/contracts/upload-import.js.map +0 -1
  363. package/dist/types/contracts/editorial-draft.d.ts +0 -610
  364. package/dist/types/contracts/editorial-draft.d.ts.map +0 -1
  365. package/dist/types/contracts/upload-import.d.ts +0 -96
  366. package/dist/types/contracts/upload-import.d.ts.map +0 -1
@@ -0,0 +1,81 @@
1
+ import { z } from 'zod';
2
+ import { ValidationError } from '../common/errors.js';
3
+ import { buildCursorPageMeta } from '../common/response.js';
4
+ import { zodIssuesToErrors } from '../validation/index.js';
5
+ /**
6
+ * Cursor (keyset) pagination (D6). Clients send `?cursor=&limit=`; the cursor is an opaque
7
+ * base64url token of `{ sortKey, id }` (the last row's sort value plus an id tie-breaker).
8
+ * Dependency-light: usable from controllers without the database subpath.
9
+ */
10
+ /** Largest page size a client may ask for. */
11
+ export const MAX_PAGE_LIMIT = 100;
12
+ /** Page size when the client sends none. */
13
+ export const DEFAULT_PAGE_LIMIT = 20;
14
+ const MAX_CURSOR_LENGTH = 512;
15
+ /** `{ cursor?, limit }` query schema: limit 1–100, default 20; unknown keys are stripped. */
16
+ export const cursorQuerySchema = z.object({
17
+ cursor: z.string().trim().min(1).max(MAX_CURSOR_LENGTH).optional(),
18
+ limit: z.coerce.number().int().min(1).max(MAX_PAGE_LIMIT).default(DEFAULT_PAGE_LIMIT),
19
+ });
20
+ /**
21
+ * Validates `{ cursor, limit }` from a query string.
22
+ *
23
+ * @throws ValidationError (422) with field errors when invalid.
24
+ * @example const { cursor, limit } = parseCursorQuery(req.query);
25
+ */
26
+ export function parseCursorQuery(input) {
27
+ const result = cursorQuerySchema.safeParse({ cursor: emptyToUndefined(input?.cursor), limit: emptyToUndefined(input?.limit) });
28
+ if (result.success)
29
+ return result.data;
30
+ throw new ValidationError(zodIssuesToErrors(result.error), 'Invalid pagination parameters');
31
+ }
32
+ function emptyToUndefined(value) {
33
+ return value === '' || value === null ? undefined : value;
34
+ }
35
+ /**
36
+ * Encodes a cursor. Date sort keys become ISO strings.
37
+ *
38
+ * @example encodeCursor({ sortKey: row.createdAt, id: row.id })
39
+ */
40
+ export function encodeCursor(cursor) {
41
+ const sortKey = cursor.sortKey instanceof Date ? cursor.sortKey.toISOString() : cursor.sortKey;
42
+ return Buffer.from(JSON.stringify({ k: sortKey, i: cursor.id }), 'utf8').toString('base64url');
43
+ }
44
+ /**
45
+ * Decodes a cursor produced by {@link encodeCursor}.
46
+ *
47
+ * @throws ValidationError (422, field `cursor`) for anything malformed or tampered with.
48
+ */
49
+ export function decodeCursor(token) {
50
+ const invalid = () => new ValidationError({ cursor: ['Invalid cursor'] }, 'Invalid cursor');
51
+ if (typeof token !== 'string' || token.length === 0 || token.length > MAX_CURSOR_LENGTH || !/^[A-Za-z0-9_-]+$/.test(token))
52
+ throw invalid();
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(Buffer.from(token, 'base64url').toString('utf8'));
56
+ }
57
+ catch {
58
+ throw invalid();
59
+ }
60
+ if (!parsed || typeof parsed !== 'object')
61
+ throw invalid();
62
+ const { k, i } = parsed;
63
+ const validKey = k === null || typeof k === 'string' || (typeof k === 'number' && Number.isFinite(k));
64
+ if (!validKey || typeof i !== 'string' || i.length === 0 || i.length > 64)
65
+ throw invalid();
66
+ return { sortKey: k, id: i };
67
+ }
68
+ /**
69
+ * Builds a page from `limit + 1` fetched rows: the extra row only signals that more exist.
70
+ *
71
+ * @param rows Rows fetched with `limit + 1`.
72
+ * @param limit The page size.
73
+ * @param cursorOf Extracts `{ sortKey, id }` from a row for the next cursor.
74
+ */
75
+ export function buildCursorPage(rows, limit, cursorOf) {
76
+ const data = rows.slice(0, limit);
77
+ const hasMore = rows.length > limit && data.length > 0;
78
+ const nextCursor = hasMore ? encodeCursor(cursorOf(data[data.length - 1])) : null;
79
+ return { data, meta: buildCursorPageMeta(limit, nextCursor) };
80
+ }
81
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.js","sourceRoot":"","sources":["../../../src/database/pagination.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import mysql from 'mysql2/promise';
2
+ /**
3
+ * mysql2 pool for Drizzle. Sessions run in UTC and utf8mb4 so timestamps and emoji round-trip
4
+ * unchanged regardless of server defaults.
5
+ */
6
+ export function createMysqlPool(options) {
7
+ return mysql.createPool({
8
+ host: options.host,
9
+ port: options.port,
10
+ user: options.user,
11
+ password: options.password,
12
+ database: options.database,
13
+ connectionLimit: options.poolMax ?? 10,
14
+ waitForConnections: true,
15
+ enableKeepAlive: true,
16
+ timezone: '+00:00',
17
+ charset: 'utf8mb4_unicode_ci',
18
+ dateStrings: false,
19
+ supportBigNumbers: true,
20
+ });
21
+ }
22
+ /**
23
+ * Knex config for migrations and seeds: mysql2, utf8mb4, UTC, `.cjs` files only. Services call it
24
+ * through `knexConfig(env)` in their `knexfile.js`.
25
+ */
26
+ export function createKnexConfig(options) {
27
+ return {
28
+ client: 'mysql2',
29
+ connection: {
30
+ host: options.host,
31
+ port: options.port,
32
+ user: options.user,
33
+ password: options.password,
34
+ database: options.database,
35
+ timezone: '+00:00',
36
+ charset: 'utf8mb4',
37
+ },
38
+ pool: { min: 0, max: options.poolMax ?? 10 },
39
+ migrations: { directory: options.migrationsDir ?? './migrations', tableName: 'knex_migrations', extension: 'cjs', loadExtensions: ['.cjs'] },
40
+ seeds: { directory: options.seedsDir ?? './seeds', extension: 'cjs', loadExtensions: ['.cjs'] },
41
+ };
42
+ }
43
+ /** Readiness probe helper. */
44
+ export async function pingMysql(pool) {
45
+ await pool.query('SELECT 1');
46
+ }
47
+ /** Escapes `%`, `_` and `\` for use inside a LIKE pattern. */
48
+ export function escapeLike(value) {
49
+ return value.replace(/[\\%_]/g, (ch) => `\\${ch}`);
50
+ }
51
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../../src/database/pool.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,gBAAgB,CAAC;AA0BnC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAA+B;IAC7D,OAAO,KAAK,CAAC,UAAU,CAAC;QACtB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,eAAe,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE;QACtC,kBAAkB,EAAE,IAAI;QACxB,eAAe,EAAE,IAAI;QACrB,QAAQ,EAAE,QAAQ;QAClB,OAAO,EAAE,oBAAoB;QAC7B,WAAW,EAAE,KAAK;QAClB,iBAAiB,EAAE,IAAI;KACxB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA0B;IACzD,OAAO;QACL,MAAM,EAAE,QAAQ;QAChB,UAAU,EAAE;YACV,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,QAAQ,EAAE,QAAQ;YAClB,OAAO,EAAE,SAAS;SACnB;QACD,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;QAC5C,UAAU,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,aAAa,IAAI,cAAc,EAAE,SAAS,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,MAAM,CAAC,EAAE;QAC5I,KAAK,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,QAAQ,IAAI,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,MAAM,CAAC,EAAE;KAChG,CAAC;AACJ,CAAC;AAED,8BAA8B;AAC9B,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAgB;IAC9C,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAC/B,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACrD,CAAC"}
@@ -0,0 +1,128 @@
1
+ import { and, asc, count, desc, eq, gt, lt, or } from 'drizzle-orm';
2
+ import { ForbiddenError } from '../common/errors.js';
3
+ import { buildCursorPage, decodeCursor, DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from './pagination.js';
4
+ import { resolveColumn } from './scopes.js';
5
+ /**
6
+ * Base for every repository over scoped data (Drizzle). Every read, count, update and delete is
7
+ * AND-ed with the scope's condition, and every insert is stamped with the scope value; there is no
8
+ * method that returns an unscoped query. Subclasses add queries only through {@link scoped} and
9
+ * {@link executor}.
10
+ *
11
+ * @example
12
+ * class NoteRepository extends ScopedRepository<typeof notes, ScopeContext<'workspaceId'>> {
13
+ * findByTitle(ctx: ScopeContext<'workspaceId'>, title: string) {
14
+ * return this.executor().select().from(this.table).where(this.scoped(ctx, eq(this.table.title, title)));
15
+ * }
16
+ * }
17
+ * const repo = new NoteRepository(db, notes, workspaceScope);
18
+ */
19
+ export class ScopedRepository {
20
+ #db;
21
+ table;
22
+ scope;
23
+ idColumn;
24
+ constructor(db, table, scope, options = {}) {
25
+ this.#db = db;
26
+ this.table = table;
27
+ this.scope = scope;
28
+ this.idColumn = resolveColumn(table, options.idColumn ?? 'id').column;
29
+ resolveColumn(table, scope.column); // fail fast when the table lacks the scope column
30
+ }
31
+ /** The row with `id` inside the caller's scope, or null (absent and out-of-scope look the same). */
32
+ async findById(ctx, id, options = {}) {
33
+ const rows = await this.executor(options.tx).select().from(this.table)
34
+ .where(this.scoped(ctx, eq(this.idColumn, id))).limit(1);
35
+ return rows[0] ?? null;
36
+ }
37
+ /** Rows inside the caller's scope, keyset-paginated by `orderBy` then id. */
38
+ async findMany(ctx, options = {}) {
39
+ return this.selectPage(ctx, options, clampLimit(options.limit));
40
+ }
41
+ /** Like {@link findMany}, returning `{ data, meta: { limit, nextCursor, hasMore } }`. */
42
+ async findPage(ctx, options = {}) {
43
+ const limit = clampLimit(options.limit);
44
+ const rows = await this.selectPage(ctx, options, limit + 1);
45
+ const { key: sortKey } = resolveColumn(this.table, options.orderBy?.column ?? this.idColumn.name);
46
+ const { key: idKey } = resolveColumn(this.table, this.idColumn.name);
47
+ return buildCursorPage(rows, limit, (row) => {
48
+ const record = row;
49
+ return { sortKey: record[sortKey], id: String(record[idKey]) };
50
+ });
51
+ }
52
+ /** Number of rows inside the caller's scope matching `where`. */
53
+ async count(ctx, where, options = {}) {
54
+ const rows = await this.executor(options.tx).select({ n: count() }).from(this.table).where(this.scoped(ctx, where));
55
+ return Number(rows[0]?.n ?? 0);
56
+ }
57
+ /**
58
+ * Inserts rows stamped with the caller's scope value.
59
+ *
60
+ * @throws ForbiddenError (`SCOPE_MISMATCH`) when a row names another scope.
61
+ * @returns The stamped rows as written.
62
+ */
63
+ async insert(ctx, rows, options = {}) {
64
+ const list = (Array.isArray(rows) ? rows : [rows]).map((row) => this.scope.stamp(this.table, row, ctx));
65
+ if (list.length === 0)
66
+ return [];
67
+ await this.executor(options.tx).insert(this.table).values(list);
68
+ return list;
69
+ }
70
+ /**
71
+ * Updates rows inside the caller's scope. Rows never move between scopes.
72
+ *
73
+ * @throws ForbiddenError (`SCOPE_IMMUTABLE`) when `values` sets the scope column.
74
+ * @returns Number of rows changed.
75
+ */
76
+ async update(ctx, where, values, options = {}) {
77
+ if (resolveColumn(this.table, this.scope.column).key in values) {
78
+ throw new ForbiddenError(`The ${this.scope.name} of a row cannot be changed`, { code: 'SCOPE_IMMUTABLE' });
79
+ }
80
+ const result = await this.executor(options.tx).update(this.table).set(values).where(this.scoped(ctx, where));
81
+ return affectedRows(result);
82
+ }
83
+ /**
84
+ * Deletes rows inside the caller's scope.
85
+ *
86
+ * @returns Number of rows deleted.
87
+ */
88
+ async delete(ctx, where, options = {}) {
89
+ const result = await this.executor(options.tx).delete(this.table).where(this.scoped(ctx, where));
90
+ return affectedRows(result);
91
+ }
92
+ /** The scope condition AND-ed with `conditions`. The only way subclasses may build a WHERE. */
93
+ scoped(ctx, ...conditions) {
94
+ return and(this.scope.condition(this.table, ctx), ...conditions);
95
+ }
96
+ /** The executor for a call: `tx` when given, otherwise the pool. Always pair with {@link scoped}. */
97
+ executor(tx) {
98
+ return tx ?? this.#db;
99
+ }
100
+ async selectPage(ctx, options, take) {
101
+ const sort = resolveColumn(this.table, options.orderBy?.column ?? this.idColumn.name).column;
102
+ const descending = (options.orderBy?.direction ?? 'desc') === 'desc';
103
+ const after = options.cursor ? this.afterCursor(options.cursor, sort, descending) : undefined;
104
+ const order = descending ? [desc(sort), desc(this.idColumn)] : [asc(sort), asc(this.idColumn)];
105
+ const rows = await this.executor(options.tx).select().from(this.table)
106
+ .where(this.scoped(ctx, options.where, after)).orderBy(...order).limit(take);
107
+ return rows;
108
+ }
109
+ /** Keyset condition: rows strictly after the cursor's (sortKey, id) in the sort direction. */
110
+ afterCursor(token, sort, descending) {
111
+ const { sortKey, id } = decodeCursor(token);
112
+ const past = descending ? lt : gt;
113
+ if (sort === this.idColumn)
114
+ return past(this.idColumn, id);
115
+ const value = sort.dataType === 'date' && typeof sortKey === 'string' ? new Date(sortKey) : sortKey;
116
+ return or(past(sort, value), and(eq(sort, value), past(this.idColumn, id)));
117
+ }
118
+ }
119
+ function clampLimit(limit) {
120
+ if (limit === undefined || !Number.isFinite(limit))
121
+ return DEFAULT_PAGE_LIMIT;
122
+ return Math.min(MAX_PAGE_LIMIT, Math.max(1, Math.trunc(limit)));
123
+ }
124
+ function affectedRows(result) {
125
+ const header = (Array.isArray(result) ? result[0] : result);
126
+ return Number(header?.affectedRows ?? 0);
127
+ }
128
+ //# sourceMappingURL=scoped-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped-repository.js","sourceRoot":"","sources":["../../../src/database/scoped-repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ import { eq, getTableColumns, isNotNull } from 'drizzle-orm';
2
+ import { ForbiddenError } from '../common/errors.js';
3
+ /**
4
+ * Finds a table column by its database name.
5
+ *
6
+ * @throws Error when the table has no such column (a wiring bug, caught at startup or in tests).
7
+ */
8
+ export function resolveColumn(table, dbName) {
9
+ const columns = getTableColumns(table);
10
+ for (const [key, column] of Object.entries(columns)) {
11
+ if (column.name === dbName)
12
+ return { key, column };
13
+ }
14
+ throw new Error(`Table has no column "${dbName}"`);
15
+ }
16
+ function requireValue(value, scopeName, code) {
17
+ if (typeof value !== 'string' || value.trim() === '') {
18
+ throw new ForbiddenError(`A ${scopeName} scope is required`, { code });
19
+ }
20
+ return value;
21
+ }
22
+ /** Sets `column` on `row` to `value`; refuses a row that carries a different value. */
23
+ function stampColumn(table, row, dbName, value, scopeName) {
24
+ const { key } = resolveColumn(table, dbName);
25
+ const current = row[key];
26
+ if (current !== undefined && current !== null && current !== value) {
27
+ throw new ForbiddenError(`Row belongs to another ${scopeName}`, { code: 'SCOPE_MISMATCH' });
28
+ }
29
+ return { ...row, [key]: value };
30
+ }
31
+ /**
32
+ * Defines a scope whose value comes from the request context (the gateway-signed identity, never a
33
+ * request body).
34
+ *
35
+ * @example const workspaceScope = defineScope({ name: 'workspace', column: 'workspace_id', contextKey: 'workspaceId' });
36
+ */
37
+ export function defineScope(options) {
38
+ const { name, column, contextKey } = options;
39
+ const valueOf = (ctx) => requireValue(ctx?.[contextKey], name, 'SCOPE_REQUIRED');
40
+ return Object.freeze({
41
+ name,
42
+ column,
43
+ contextKey,
44
+ valueOf,
45
+ condition: (table, ctx) => eq(resolveColumn(table, column).column, valueOf(ctx)),
46
+ stamp: (table, row, ctx) => stampColumn(table, row, column, valueOf(ctx), name),
47
+ });
48
+ }
49
+ /**
50
+ * Rows owned by one principal (e.g. a user's own records). Refuses a missing owner id, so an
51
+ * anonymous call can never fall through to "all owners".
52
+ *
53
+ * @example const ownerScope = new OwnerScope({ name: 'owner', column: 'user_id' });
54
+ */
55
+ export class OwnerScope {
56
+ /** Readable name (e.g. `owner`). */
57
+ name;
58
+ /** Database name of the owner column (e.g. `user_id`). */
59
+ column;
60
+ /** @param options `name` for messages, `column` = database name of the owner column. */
61
+ constructor(options) {
62
+ this.name = options.name;
63
+ this.column = options.column;
64
+ }
65
+ /** The owner id from `ctx`; throws ForbiddenError (`OWNER_REQUIRED`) when it is missing. */
66
+ ownerIdOf(ctx) {
67
+ return requireValue(ctx?.ownerId, this.name, 'OWNER_REQUIRED');
68
+ }
69
+ /** `owner column = ownerId`. @throws ForbiddenError (`OWNER_REQUIRED`) without an owner id. */
70
+ condition(table, ctx) {
71
+ return eq(resolveColumn(table, this.column).column, this.ownerIdOf(ctx));
72
+ }
73
+ /** Sets the owner column. @throws ForbiddenError (`OWNER_REQUIRED`, `SCOPE_MISMATCH`). */
74
+ stamp(table, row, ctx) {
75
+ return stampColumn(table, row, this.column, this.ownerIdOf(ctx), this.name);
76
+ }
77
+ }
78
+ const ISSUE = Symbol('PlatformScope.issue');
79
+ /**
80
+ * Proof that an identity may act across scopes. Only {@link PlatformScopeDefinition.authorize} can
81
+ * create one, and only when the definition's predicate approves the identity.
82
+ */
83
+ export class PlatformScope {
84
+ #definition;
85
+ /** @internal Use `definePlatformScope(...).authorize(identity)`. */
86
+ constructor(token, definition) {
87
+ if (token !== ISSUE)
88
+ throw new ForbiddenError('Platform scope must be authorized', { code: 'PLATFORM_SCOPE_REQUIRED' });
89
+ this.#definition = definition;
90
+ }
91
+ /** True when this grant was issued by `definition`. */
92
+ issuedBy(definition) {
93
+ return this.#definition === definition;
94
+ }
95
+ }
96
+ /**
97
+ * Cross-scope access for platform operators. Rows of every scope are reachable, but the query still
98
+ * filters on the scope column (`IS NOT NULL`) and inserts must name their scope explicitly.
99
+ */
100
+ export class PlatformScopeDefinition {
101
+ /** Readable name (e.g. `platform`). */
102
+ name;
103
+ /** Database name of the scope column the platform scope spans. */
104
+ column;
105
+ #predicate;
106
+ /** Prefer {@link definePlatformScope}. */
107
+ constructor(options) {
108
+ this.name = options.name;
109
+ this.column = options.column;
110
+ this.#predicate = options.predicate;
111
+ }
112
+ /**
113
+ * Issues a {@link PlatformScope} for `identity`.
114
+ *
115
+ * @throws ForbiddenError (`PLATFORM_SCOPE_DENIED`) when the predicate refuses (or throws).
116
+ */
117
+ authorize(identity) {
118
+ let allowed = false;
119
+ try {
120
+ allowed = identity != null && this.#predicate(identity);
121
+ }
122
+ catch {
123
+ allowed = false;
124
+ }
125
+ if (!allowed)
126
+ throw new ForbiddenError('Platform access is required', { code: 'PLATFORM_SCOPE_DENIED' });
127
+ return new PlatformScope(ISSUE, this);
128
+ }
129
+ /** `scope column IS NOT NULL`. @throws ForbiddenError (`PLATFORM_SCOPE_REQUIRED`) without a grant from this scope. */
130
+ condition(table, ctx) {
131
+ this.assertGrant(ctx);
132
+ return isNotNull(resolveColumn(table, this.column).column);
133
+ }
134
+ /** Requires the row to name its scope. @throws ForbiddenError (`PLATFORM_SCOPE_REQUIRED`, `SCOPE_REQUIRED`). */
135
+ stamp(table, row, ctx) {
136
+ this.assertGrant(ctx);
137
+ const { key } = resolveColumn(table, this.column);
138
+ requireValue(row[key], this.name, 'SCOPE_REQUIRED');
139
+ return row;
140
+ }
141
+ assertGrant(ctx) {
142
+ if (!(ctx instanceof PlatformScope) || !ctx.issuedBy(this)) {
143
+ throw new ForbiddenError('Platform access is required', { code: 'PLATFORM_SCOPE_REQUIRED' });
144
+ }
145
+ }
146
+ }
147
+ /**
148
+ * Defines the platform (cross-scope) scope.
149
+ *
150
+ * @example
151
+ * const platformScope = definePlatformScope({ name: 'platform', column: 'workspace_id', predicate: (u: AuthUser) => u.isSuperAdmin });
152
+ * const grant = platformScope.authorize(req.user); // throws 403 for anyone else
153
+ */
154
+ export function definePlatformScope(options) {
155
+ return new PlatformScopeDefinition(options);
156
+ }
157
+ //# sourceMappingURL=scopes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../../src/database/scopes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /** Default name of the transactional outbox table. */
2
+ export const DEFAULT_OUTBOX_TABLE = 'event_outbox';
3
+ /** Default name of the consumer idempotency ledger. */
4
+ export const DEFAULT_PROCESSED_EVENTS_TABLE = 'processed_events';
5
+ /** Default name of the scheduled-job runs table. */
6
+ export const DEFAULT_SCHEDULER_RUNS_TABLE = 'scheduler_runs';
7
+ /** Scheduler run states, shared by the Drizzle table and the Knex helper. */
8
+ export const SCHEDULER_RUN_STATUSES = ['running', 'succeeded', 'failed'];
9
+ /** Logical exchanges an outbox row can target: topic events or direct commands. */
10
+ export const OUTBOX_EXCHANGES = ['events', 'commands'];
11
+ //# sourceMappingURL=table-names.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-names.js","sourceRoot":"","sources":["../../../src/database/table-names.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,MAAM,CAAC,MAAM,oBAAoB,GAAG,cAAc,CAAC;AACnD,uDAAuD;AACvD,MAAM,CAAC,MAAM,8BAA8B,GAAG,kBAAkB,CAAC;AACjE,oDAAoD;AACpD,MAAM,CAAC,MAAM,4BAA4B,GAAG,gBAAgB,CAAC;AAC7D,6EAA6E;AAC7E,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,SAAS,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAClF,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAU,CAAC"}
@@ -0,0 +1,61 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { DEFAULT_OUTBOX_TABLE, DEFAULT_PROCESSED_EVENTS_TABLE, DEFAULT_SCHEDULER_RUNS_TABLE, OUTBOX_EXCHANGES, SCHEDULER_RUN_STATUSES } from './table-names.js';
3
+ import { char, datetime, index, int, json, mysqlEnum, mysqlTable, uniqueIndex, varchar } from 'drizzle-orm/mysql-core';
4
+ /**
5
+ * Drizzle definitions of the infrastructure tables every service may own. Each matches the Knex
6
+ * helper of the same name in `@heavybit/shared-lib/knex` (`outboxColumns`, `processedEventsColumns`,
7
+ * `schedulerRunsColumns`); a test keeps the two in step. Pass a custom name when a service needs one.
8
+ */
9
+ const ms = (name) => datetime(name, { mode: 'date', fsp: 3 });
10
+ const createdNow = (name) => ms(name).notNull().default(sql `CURRENT_TIMESTAMP(3)`);
11
+ export { DEFAULT_OUTBOX_TABLE, DEFAULT_PROCESSED_EVENTS_TABLE, DEFAULT_SCHEDULER_RUNS_TABLE, OUTBOX_EXCHANGES, SCHEDULER_RUN_STATUSES };
12
+ /** Transactional outbox (`event_outbox` by default): domain change and message in one transaction. */
13
+ export function outboxTable(name = DEFAULT_OUTBOX_TABLE) {
14
+ return mysqlTable(name, {
15
+ id: char('id', { length: 26 }).primaryKey(),
16
+ routingKey: varchar('routing_key', { length: 191 }).notNull(),
17
+ /** Logical exchange: `events` (topic fan-out) or `commands` (direct, one owner). */
18
+ exchange: mysqlEnum('exchange', OUTBOX_EXCHANGES).notNull().default('events'),
19
+ payload: json('payload').notNull(),
20
+ /** Correlation id of the request that staged the row; the relay sends it as `x-correlation-id`. */
21
+ correlationId: varchar('correlation_id', { length: 64 }),
22
+ attempts: int('attempts', { unsigned: true }).notNull().default(0),
23
+ lastError: varchar('last_error', { length: 500 }),
24
+ createdAt: createdNow('created_at'),
25
+ publishedAt: ms('published_at'),
26
+ }, (t) => [index(`${name}_published_at_id_index`).on(t.publishedAt, t.id)]);
27
+ }
28
+ /** Consumer idempotency ledger: one row per (consumer, event id). */
29
+ export function processedEventsTable(name = DEFAULT_PROCESSED_EVENTS_TABLE) {
30
+ return mysqlTable(name, {
31
+ id: char('id', { length: 26 }).primaryKey(),
32
+ consumer: varchar('consumer', { length: 191 }).notNull(),
33
+ eventId: varchar('event_id', { length: 64 }).notNull(),
34
+ processedAt: createdNow('processed_at'),
35
+ }, (t) => [
36
+ uniqueIndex(`${name}_consumer_event_id_unique`).on(t.consumer, t.eventId),
37
+ index(`${name}_processed_at_index`).on(t.processedAt),
38
+ ]);
39
+ }
40
+ /** Scheduled-job runs: one row per (job, run key), so a run is never executed twice. */
41
+ export function schedulerRunsTable(name = DEFAULT_SCHEDULER_RUNS_TABLE) {
42
+ return mysqlTable(name, {
43
+ id: char('id', { length: 26 }).primaryKey(),
44
+ job: varchar('job', { length: 191 }).notNull(),
45
+ runKey: varchar('run_key', { length: 191 }).notNull(),
46
+ status: mysqlEnum('status', SCHEDULER_RUN_STATUSES).notNull().default('running'),
47
+ error: varchar('error', { length: 500 }),
48
+ startedAt: createdNow('started_at'),
49
+ finishedAt: ms('finished_at'),
50
+ }, (t) => [
51
+ uniqueIndex(`${name}_job_run_key_unique`).on(t.job, t.runKey),
52
+ index(`${name}_job_started_at_index`).on(t.job, t.startedAt),
53
+ ]);
54
+ }
55
+ /** `event_outbox` with the default name. */
56
+ export const outbox = outboxTable();
57
+ /** `processed_events` with the default name. */
58
+ export const processedEvents = processedEventsTable();
59
+ /** `scheduler_runs` with the default name. */
60
+ export const schedulerRuns = schedulerRunsTable();
61
+ //# sourceMappingURL=tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tables.js","sourceRoot":"","sources":["../../../src/database/tables.ts"],"names":[],"mappings":"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"}