@crowdsource.you/core 1.2.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 (339) hide show
  1. package/README.md +571 -0
  2. package/dist/cases.d.ts +66 -0
  3. package/dist/cases.d.ts.map +1 -0
  4. package/dist/cases.js +79 -0
  5. package/dist/cases.js.map +1 -0
  6. package/dist/client.d.ts +90 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +125 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/communityNotes.d.ts +49 -0
  11. package/dist/communityNotes.d.ts.map +1 -0
  12. package/dist/communityNotes.js +104 -0
  13. package/dist/communityNotes.js.map +1 -0
  14. package/dist/credential.d.ts +58 -0
  15. package/dist/credential.d.ts.map +1 -0
  16. package/dist/credential.js +71 -0
  17. package/dist/credential.js.map +1 -0
  18. package/dist/defaults.d.ts +61 -0
  19. package/dist/defaults.d.ts.map +1 -0
  20. package/dist/defaults.js +82 -0
  21. package/dist/defaults.js.map +1 -0
  22. package/dist/digest.d.ts +44 -0
  23. package/dist/digest.d.ts.map +1 -0
  24. package/dist/digest.js +79 -0
  25. package/dist/digest.js.map +1 -0
  26. package/dist/envelope.d.ts +143 -0
  27. package/dist/envelope.d.ts.map +1 -0
  28. package/dist/envelope.js +258 -0
  29. package/dist/envelope.js.map +1 -0
  30. package/dist/errors.d.ts +90 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +151 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/esm/cases.js +74 -0
  35. package/dist/esm/cases.js.map +1 -0
  36. package/dist/esm/client.js +121 -0
  37. package/dist/esm/client.js.map +1 -0
  38. package/dist/esm/communityNotes.js +100 -0
  39. package/dist/esm/communityNotes.js.map +1 -0
  40. package/dist/esm/credential.js +67 -0
  41. package/dist/esm/credential.js.map +1 -0
  42. package/dist/esm/defaults.js +77 -0
  43. package/dist/esm/defaults.js.map +1 -0
  44. package/dist/esm/digest.js +74 -0
  45. package/dist/esm/digest.js.map +1 -0
  46. package/dist/esm/envelope.js +252 -0
  47. package/dist/esm/envelope.js.map +1 -0
  48. package/dist/esm/errors.js +140 -0
  49. package/dist/esm/errors.js.map +1 -0
  50. package/dist/esm/express/index.js +29 -0
  51. package/dist/esm/express/index.js.map +1 -0
  52. package/dist/esm/express/middleware.js +293 -0
  53. package/dist/esm/express/middleware.js.map +1 -0
  54. package/dist/esm/express/store.js +69 -0
  55. package/dist/esm/express/store.js.map +1 -0
  56. package/dist/esm/express/verify.js +133 -0
  57. package/dist/esm/express/verify.js.map +1 -0
  58. package/dist/esm/index.js +57 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/outbox/client.js +43 -0
  61. package/dist/esm/outbox/client.js.map +1 -0
  62. package/dist/esm/outbox/decision.js +153 -0
  63. package/dist/esm/outbox/decision.js.map +1 -0
  64. package/dist/esm/outbox/delivery.js +129 -0
  65. package/dist/esm/outbox/delivery.js.map +1 -0
  66. package/dist/esm/outbox/enforcement/executor.js +219 -0
  67. package/dist/esm/outbox/enforcement/executor.js.map +1 -0
  68. package/dist/esm/outbox/enforcement/planner.js +326 -0
  69. package/dist/esm/outbox/enforcement/planner.js.map +1 -0
  70. package/dist/esm/outbox/evidence.js +173 -0
  71. package/dist/esm/outbox/evidence.js.map +1 -0
  72. package/dist/esm/outbox/inbound.js +87 -0
  73. package/dist/esm/outbox/inbound.js.map +1 -0
  74. package/dist/esm/outbox/index.js +71 -0
  75. package/dist/esm/outbox/index.js.map +1 -0
  76. package/dist/esm/outbox/intake.js +140 -0
  77. package/dist/esm/outbox/intake.js.map +1 -0
  78. package/dist/esm/outbox/integration.js +116 -0
  79. package/dist/esm/outbox/integration.js.map +1 -0
  80. package/dist/esm/outbox/outbox/dispatcher.js +114 -0
  81. package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
  82. package/dist/esm/outbox/outbox/service.js +271 -0
  83. package/dist/esm/outbox/outbox/service.js.map +1 -0
  84. package/dist/esm/outbox/postgres/index.js +41 -0
  85. package/dist/esm/outbox/postgres/index.js.map +1 -0
  86. package/dist/esm/outbox/postgres/registries.js +155 -0
  87. package/dist/esm/outbox/postgres/registries.js.map +1 -0
  88. package/dist/esm/outbox/postgres/reportColumns.js +191 -0
  89. package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
  90. package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
  91. package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
  92. package/dist/esm/outbox/postgres/store/events.js +101 -0
  93. package/dist/esm/outbox/postgres/store/events.js.map +1 -0
  94. package/dist/esm/outbox/postgres/store/index.js +59 -0
  95. package/dist/esm/outbox/postgres/store/index.js.map +1 -0
  96. package/dist/esm/outbox/postgres/store/outbox.js +207 -0
  97. package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
  98. package/dist/esm/outbox/postgres/store/reports.js +231 -0
  99. package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
  100. package/dist/esm/outbox/postgres/store/transaction.js +29 -0
  101. package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
  102. package/dist/esm/outbox/postgres/tables.js +270 -0
  103. package/dist/esm/outbox/postgres/tables.js.map +1 -0
  104. package/dist/esm/outbox/reconciliation.js +157 -0
  105. package/dist/esm/outbox/reconciliation.js.map +1 -0
  106. package/dist/esm/outbox/reportStatus.js +22 -0
  107. package/dist/esm/outbox/reportStatus.js.map +1 -0
  108. package/dist/esm/outbox/retention.js +33 -0
  109. package/dist/esm/outbox/retention.js.map +1 -0
  110. package/dist/esm/outbox/store/types.js +30 -0
  111. package/dist/esm/outbox/store/types.js.map +1 -0
  112. package/dist/esm/outbox/types.js +23 -0
  113. package/dist/esm/outbox/types.js.map +1 -0
  114. package/dist/esm/outbox/webhook.js +145 -0
  115. package/dist/esm/outbox/webhook.js.map +1 -0
  116. package/dist/esm/package.json +1 -0
  117. package/dist/esm/reports.js +74 -0
  118. package/dist/esm/reports.js.map +1 -0
  119. package/dist/esm/testing/fixtures.js +132 -0
  120. package/dist/esm/testing/fixtures.js.map +1 -0
  121. package/dist/esm/testing/index.js +33 -0
  122. package/dist/esm/testing/index.js.map +1 -0
  123. package/dist/esm/testing/sandbox.js +337 -0
  124. package/dist/esm/testing/sandbox.js.map +1 -0
  125. package/dist/esm/testing/webhook-simulator.js +93 -0
  126. package/dist/esm/testing/webhook-simulator.js.map +1 -0
  127. package/dist/esm/transport.js +177 -0
  128. package/dist/esm/transport.js.map +1 -0
  129. package/dist/esm/webhookEndpoints.js +139 -0
  130. package/dist/esm/webhookEndpoints.js.map +1 -0
  131. package/dist/express/index.d.ts +32 -0
  132. package/dist/express/index.d.ts.map +1 -0
  133. package/dist/express/index.js +40 -0
  134. package/dist/express/index.js.map +1 -0
  135. package/dist/express/middleware.d.ts +97 -0
  136. package/dist/express/middleware.d.ts.map +1 -0
  137. package/dist/express/middleware.js +298 -0
  138. package/dist/express/middleware.js.map +1 -0
  139. package/dist/express/store.d.ts +45 -0
  140. package/dist/express/store.d.ts.map +1 -0
  141. package/dist/express/store.js +72 -0
  142. package/dist/express/store.js.map +1 -0
  143. package/dist/express/verify.d.ts +86 -0
  144. package/dist/express/verify.d.ts.map +1 -0
  145. package/dist/express/verify.js +138 -0
  146. package/dist/express/verify.js.map +1 -0
  147. package/dist/index.d.ts +67 -0
  148. package/dist/index.d.ts.map +1 -0
  149. package/dist/index.js +89 -0
  150. package/dist/index.js.map +1 -0
  151. package/dist/outbox/client.d.ts +35 -0
  152. package/dist/outbox/client.d.ts.map +1 -0
  153. package/dist/outbox/client.js +46 -0
  154. package/dist/outbox/client.js.map +1 -0
  155. package/dist/outbox/decision.d.ts +36 -0
  156. package/dist/outbox/decision.d.ts.map +1 -0
  157. package/dist/outbox/decision.js +159 -0
  158. package/dist/outbox/decision.js.map +1 -0
  159. package/dist/outbox/delivery.d.ts +50 -0
  160. package/dist/outbox/delivery.d.ts.map +1 -0
  161. package/dist/outbox/delivery.js +135 -0
  162. package/dist/outbox/delivery.js.map +1 -0
  163. package/dist/outbox/enforcement/executor.d.ts +26 -0
  164. package/dist/outbox/enforcement/executor.d.ts.map +1 -0
  165. package/dist/outbox/enforcement/executor.js +222 -0
  166. package/dist/outbox/enforcement/executor.js.map +1 -0
  167. package/dist/outbox/enforcement/planner.d.ts +50 -0
  168. package/dist/outbox/enforcement/planner.d.ts.map +1 -0
  169. package/dist/outbox/enforcement/planner.js +333 -0
  170. package/dist/outbox/enforcement/planner.js.map +1 -0
  171. package/dist/outbox/evidence.d.ts +114 -0
  172. package/dist/outbox/evidence.d.ts.map +1 -0
  173. package/dist/outbox/evidence.js +181 -0
  174. package/dist/outbox/evidence.js.map +1 -0
  175. package/dist/outbox/inbound.d.ts +78 -0
  176. package/dist/outbox/inbound.d.ts.map +1 -0
  177. package/dist/outbox/inbound.js +91 -0
  178. package/dist/outbox/inbound.js.map +1 -0
  179. package/dist/outbox/index.d.ts +79 -0
  180. package/dist/outbox/index.d.ts.map +1 -0
  181. package/dist/outbox/index.js +98 -0
  182. package/dist/outbox/index.js.map +1 -0
  183. package/dist/outbox/intake.d.ts +62 -0
  184. package/dist/outbox/intake.d.ts.map +1 -0
  185. package/dist/outbox/intake.js +145 -0
  186. package/dist/outbox/intake.js.map +1 -0
  187. package/dist/outbox/integration.d.ts +62 -0
  188. package/dist/outbox/integration.d.ts.map +1 -0
  189. package/dist/outbox/integration.js +119 -0
  190. package/dist/outbox/integration.js.map +1 -0
  191. package/dist/outbox/outbox/dispatcher.d.ts +26 -0
  192. package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
  193. package/dist/outbox/outbox/dispatcher.js +119 -0
  194. package/dist/outbox/outbox/dispatcher.js.map +1 -0
  195. package/dist/outbox/outbox/service.d.ts +124 -0
  196. package/dist/outbox/outbox/service.d.ts.map +1 -0
  197. package/dist/outbox/outbox/service.js +279 -0
  198. package/dist/outbox/outbox/service.js.map +1 -0
  199. package/dist/outbox/postgres/index.d.ts +44 -0
  200. package/dist/outbox/postgres/index.d.ts.map +1 -0
  201. package/dist/outbox/postgres/index.js +51 -0
  202. package/dist/outbox/postgres/index.js.map +1 -0
  203. package/dist/outbox/postgres/registries.d.ts +69 -0
  204. package/dist/outbox/postgres/registries.d.ts.map +1 -0
  205. package/dist/outbox/postgres/registries.js +159 -0
  206. package/dist/outbox/postgres/registries.js.map +1 -0
  207. package/dist/outbox/postgres/reportColumns.d.ts +154 -0
  208. package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
  209. package/dist/outbox/postgres/reportColumns.js +195 -0
  210. package/dist/outbox/postgres/reportColumns.js.map +1 -0
  211. package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
  212. package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
  213. package/dist/outbox/postgres/store/enforcement.js +161 -0
  214. package/dist/outbox/postgres/store/enforcement.js.map +1 -0
  215. package/dist/outbox/postgres/store/events.d.ts +46 -0
  216. package/dist/outbox/postgres/store/events.d.ts.map +1 -0
  217. package/dist/outbox/postgres/store/events.js +104 -0
  218. package/dist/outbox/postgres/store/events.js.map +1 -0
  219. package/dist/outbox/postgres/store/index.d.ts +27 -0
  220. package/dist/outbox/postgres/store/index.d.ts.map +1 -0
  221. package/dist/outbox/postgres/store/index.js +62 -0
  222. package/dist/outbox/postgres/store/index.js.map +1 -0
  223. package/dist/outbox/postgres/store/outbox.d.ts +8 -0
  224. package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
  225. package/dist/outbox/postgres/store/outbox.js +210 -0
  226. package/dist/outbox/postgres/store/outbox.js.map +1 -0
  227. package/dist/outbox/postgres/store/reports.d.ts +69 -0
  228. package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
  229. package/dist/outbox/postgres/store/reports.js +234 -0
  230. package/dist/outbox/postgres/store/reports.js.map +1 -0
  231. package/dist/outbox/postgres/store/transaction.d.ts +49 -0
  232. package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
  233. package/dist/outbox/postgres/store/transaction.js +32 -0
  234. package/dist/outbox/postgres/store/transaction.js.map +1 -0
  235. package/dist/outbox/postgres/tables.d.ts +737 -0
  236. package/dist/outbox/postgres/tables.d.ts.map +1 -0
  237. package/dist/outbox/postgres/tables.js +273 -0
  238. package/dist/outbox/postgres/tables.js.map +1 -0
  239. package/dist/outbox/reconciliation.d.ts +49 -0
  240. package/dist/outbox/reconciliation.d.ts.map +1 -0
  241. package/dist/outbox/reconciliation.js +162 -0
  242. package/dist/outbox/reconciliation.js.map +1 -0
  243. package/dist/outbox/reportStatus.d.ts +12 -0
  244. package/dist/outbox/reportStatus.d.ts.map +1 -0
  245. package/dist/outbox/reportStatus.js +25 -0
  246. package/dist/outbox/reportStatus.js.map +1 -0
  247. package/dist/outbox/retention.d.ts +33 -0
  248. package/dist/outbox/retention.d.ts.map +1 -0
  249. package/dist/outbox/retention.js +36 -0
  250. package/dist/outbox/retention.js.map +1 -0
  251. package/dist/outbox/store/types.d.ts +466 -0
  252. package/dist/outbox/store/types.d.ts.map +1 -0
  253. package/dist/outbox/store/types.js +31 -0
  254. package/dist/outbox/store/types.js.map +1 -0
  255. package/dist/outbox/types.d.ts +660 -0
  256. package/dist/outbox/types.d.ts.map +1 -0
  257. package/dist/outbox/types.js +24 -0
  258. package/dist/outbox/types.js.map +1 -0
  259. package/dist/outbox/webhook.d.ts +15 -0
  260. package/dist/outbox/webhook.d.ts.map +1 -0
  261. package/dist/outbox/webhook.js +148 -0
  262. package/dist/outbox/webhook.js.map +1 -0
  263. package/dist/reports.d.ts +73 -0
  264. package/dist/reports.d.ts.map +1 -0
  265. package/dist/reports.js +78 -0
  266. package/dist/reports.js.map +1 -0
  267. package/dist/testing/fixtures.d.ts +62 -0
  268. package/dist/testing/fixtures.d.ts.map +1 -0
  269. package/dist/testing/fixtures.js +137 -0
  270. package/dist/testing/fixtures.js.map +1 -0
  271. package/dist/testing/index.d.ts +36 -0
  272. package/dist/testing/index.d.ts.map +1 -0
  273. package/dist/testing/index.js +43 -0
  274. package/dist/testing/index.js.map +1 -0
  275. package/dist/testing/sandbox.d.ts +109 -0
  276. package/dist/testing/sandbox.d.ts.map +1 -0
  277. package/dist/testing/sandbox.js +342 -0
  278. package/dist/testing/sandbox.js.map +1 -0
  279. package/dist/testing/webhook-simulator.d.ts +78 -0
  280. package/dist/testing/webhook-simulator.d.ts.map +1 -0
  281. package/dist/testing/webhook-simulator.js +98 -0
  282. package/dist/testing/webhook-simulator.js.map +1 -0
  283. package/dist/transport.d.ts +59 -0
  284. package/dist/transport.d.ts.map +1 -0
  285. package/dist/transport.js +181 -0
  286. package/dist/transport.js.map +1 -0
  287. package/dist/webhookEndpoints.d.ts +147 -0
  288. package/dist/webhookEndpoints.d.ts.map +1 -0
  289. package/dist/webhookEndpoints.js +143 -0
  290. package/dist/webhookEndpoints.js.map +1 -0
  291. package/package.json +127 -0
  292. package/src/cases.ts +119 -0
  293. package/src/client.ts +202 -0
  294. package/src/communityNotes.ts +194 -0
  295. package/src/credential.ts +100 -0
  296. package/src/defaults.ts +87 -0
  297. package/src/digest.ts +91 -0
  298. package/src/envelope.ts +432 -0
  299. package/src/errors.ts +165 -0
  300. package/src/express/index.ts +43 -0
  301. package/src/express/middleware.ts +387 -0
  302. package/src/express/store.ts +85 -0
  303. package/src/express/verify.ts +166 -0
  304. package/src/index.ts +107 -0
  305. package/src/outbox/client.ts +75 -0
  306. package/src/outbox/decision.ts +209 -0
  307. package/src/outbox/delivery.ts +163 -0
  308. package/src/outbox/enforcement/executor.ts +288 -0
  309. package/src/outbox/enforcement/planner.ts +377 -0
  310. package/src/outbox/evidence.ts +231 -0
  311. package/src/outbox/inbound.ts +140 -0
  312. package/src/outbox/index.ts +160 -0
  313. package/src/outbox/intake.ts +175 -0
  314. package/src/outbox/integration.ts +205 -0
  315. package/src/outbox/outbox/dispatcher.ts +131 -0
  316. package/src/outbox/outbox/service.ts +444 -0
  317. package/src/outbox/postgres/index.ts +59 -0
  318. package/src/outbox/postgres/registries.ts +177 -0
  319. package/src/outbox/postgres/reportColumns.ts +281 -0
  320. package/src/outbox/postgres/store/enforcement.ts +186 -0
  321. package/src/outbox/postgres/store/events.ts +112 -0
  322. package/src/outbox/postgres/store/index.ts +72 -0
  323. package/src/outbox/postgres/store/outbox.ts +273 -0
  324. package/src/outbox/postgres/store/reports.ts +349 -0
  325. package/src/outbox/postgres/store/transaction.ts +64 -0
  326. package/src/outbox/postgres/tables.ts +339 -0
  327. package/src/outbox/reconciliation.ts +199 -0
  328. package/src/outbox/reportStatus.ts +24 -0
  329. package/src/outbox/retention.ts +34 -0
  330. package/src/outbox/store/types.ts +528 -0
  331. package/src/outbox/types.ts +753 -0
  332. package/src/outbox/webhook.ts +165 -0
  333. package/src/reports.ts +138 -0
  334. package/src/testing/fixtures.ts +183 -0
  335. package/src/testing/index.ts +57 -0
  336. package/src/testing/sandbox.ts +469 -0
  337. package/src/testing/webhook-simulator.ts +166 -0
  338. package/src/transport.ts +250 -0
  339. package/src/webhookEndpoints.ts +256 -0
@@ -0,0 +1,207 @@
1
+ import { and, asc, eq, gt, inArray, lte, or, sql } from 'drizzle-orm';
2
+ import { PgTransaction } from 'drizzle-orm/pg-core';
3
+ import { ModerationOutboxTransactionError } from '../../outbox/service.js';
4
+ export function postgresOutboxStore(input) {
5
+ const { db } = input;
6
+ const outbox = input.tables.outbox;
7
+ /**
8
+ * Due work, as one predicate.
9
+ *
10
+ * Either `pending` and past its `available_at`, or `processing` with an EXPIRED
11
+ * lease — the second arm is what makes a dead worker's event reclaimable rather
12
+ * than stranded. The two arms have an index each
13
+ * (`moderation_outbox_due_idx`, `moderation_outbox_lease_idx`).
14
+ */
15
+ const dueOrExpired = (now) => or(and(eq(outbox.status, 'pending'), lte(outbox.availableAt, now)), and(eq(outbox.status, 'processing'), lte(outbox.leaseUntil, now)));
16
+ /** The lease this caller claims to hold, still live. Shared by all three transitions. */
17
+ const heldLease = (eventId, leaseOwner, now) => and(eq(outbox.id, eventId), eq(outbox.status, 'processing'), eq(outbox.leaseOwner, leaseOwner), gt(outbox.leaseUntil, now));
18
+ const toEvent = (row) => ({
19
+ id: row.id,
20
+ kind: row.kind,
21
+ payload: row.payload,
22
+ attempts: row.attempts,
23
+ availableAt: row.availableAt,
24
+ // `null` is how Postgres stores "no lease"; the event type says absent.
25
+ ...(row.leaseOwner === null ? {} : { leaseOwner: row.leaseOwner }),
26
+ ...(row.leaseUntil === null ? {} : { leaseUntil: row.leaseUntil }),
27
+ expiresAt: row.expiresAt,
28
+ createdAt: row.createdAt,
29
+ });
30
+ return {
31
+ async enqueue(event, tx) {
32
+ /**
33
+ * The guard, and it is not the same mistake Mongo's guards against.
34
+ *
35
+ * Both `db` and `tx` are a `ModerationPgHandle`, so handing this the POOL
36
+ * handle type-checks perfectly — and then the row commits on its own
37
+ * connection, independently of the domain write it was supposed to be
38
+ * atomic with. That is "the report was answered 201 and never delivered",
39
+ * reached by a different route than a session nobody opened a transaction
40
+ * on, with the same silence.
41
+ *
42
+ * `PgTransaction` is a real runtime class in `drizzle-orm/pg-core`, so this
43
+ * is a genuine check rather than a duck-typed guess.
44
+ */
45
+ if (!(tx instanceof PgTransaction)) {
46
+ throw new ModerationOutboxTransactionError(event.eventId);
47
+ }
48
+ /**
49
+ * `ON CONFLICT DO NOTHING`, never `DO UPDATE`.
50
+ *
51
+ * A repeated enqueue is ORDINARY — a transaction retry, two concurrent
52
+ * duplicate submissions, a reconciliation sweep re-deriving an event — and
53
+ * the dispatcher is concurrently taking, renewing and completing leases on
54
+ * these same rows. `DO UPDATE` would make each repeat a real write, which
55
+ * conflicts with a live lease update and aborts the enclosing transaction:
56
+ * the exact defect the Mongo store's `timestamps: false` exists to prevent,
57
+ * reintroduced in a dialect where nothing forces it on you.
58
+ *
59
+ * One behavioural difference from Mongo, and Postgres has the better end of
60
+ * it: if a CONCURRENT UNCOMMITTED transaction holds this same key, Postgres
61
+ * WAITS for it and then proceeds (finding the row committed, and doing
62
+ * nothing), where Mongo raises `WriteConflict` (code 112) and aborts the
63
+ * enclosing transaction. Waiting is the outcome a caller wants.
64
+ *
65
+ * `created_at` and `updated_at` are written explicitly from the caller's
66
+ * clock rather than left to their defaults, so both backends stamp a row
67
+ * from one instant — and so a test can assert that a repeat changed
68
+ * NOTHING, which is a stronger claim than "no duplicate row".
69
+ */
70
+ await tx
71
+ .insert(outbox)
72
+ .values({
73
+ id: event.eventId,
74
+ kind: event.kind,
75
+ payload: event.payload,
76
+ status: 'pending',
77
+ attempts: 0,
78
+ availableAt: event.availableAt,
79
+ expiresAt: event.expiresAt,
80
+ createdAt: event.now,
81
+ updatedAt: event.now,
82
+ })
83
+ .onConflictDoNothing({ target: outbox.id });
84
+ },
85
+ async claim({ leaseOwner, leaseUntil, now, eventId }) {
86
+ /**
87
+ * `SKIP LOCKED` is load-bearing, not tuning.
88
+ *
89
+ * Without it, under READ COMMITTED the sub-select is evaluated once: the
90
+ * loser blocks on the head row and then returns ZERO rows. `claim` answers
91
+ * `null`, `dispatch` breaks out of its batch, and a deployment running N
92
+ * tasks silently drains at 1/N the rate with nothing failing anywhere.
93
+ *
94
+ * The `FOR UPDATE` lives in the SUB-SELECT because that is what locks the
95
+ * one row this claim intends to take, before the UPDATE touches it — the
96
+ * documented Postgres idiom for a work queue.
97
+ */
98
+ const due = db
99
+ .select({ id: outbox.id })
100
+ .from(outbox)
101
+ .where(eventId === undefined
102
+ ? dueOrExpired(now)
103
+ : and(eq(outbox.id, eventId), dueOrExpired(now)))
104
+ // Oldest first, so a backlog drains in the order it was filed rather than
105
+ // in whatever order the storage engine finds convenient.
106
+ .orderBy(asc(outbox.createdAt))
107
+ .limit(1)
108
+ .for('update', { skipLocked: true });
109
+ const rows = await db
110
+ .update(outbox)
111
+ .set({
112
+ status: 'processing',
113
+ leaseOwner,
114
+ leaseUntil,
115
+ // The COLUMN is interpolated, not a value: `attempts` is incremented by
116
+ // the database so two claimers cannot both read 3 and both write 4.
117
+ attempts: sql `${outbox.attempts} + 1`,
118
+ // The previous attempt's error, cleared with the claim so a stale
119
+ // message can never be read as this attempt's. `null` CLEARS in
120
+ // drizzle; `undefined` would leave it alone.
121
+ lastError: null,
122
+ updatedAt: now,
123
+ })
124
+ .where(inArray(outbox.id, due))
125
+ .returning({
126
+ id: outbox.id,
127
+ kind: outbox.kind,
128
+ payload: outbox.payload,
129
+ attempts: outbox.attempts,
130
+ availableAt: outbox.availableAt,
131
+ leaseOwner: outbox.leaseOwner,
132
+ leaseUntil: outbox.leaseUntil,
133
+ expiresAt: outbox.expiresAt,
134
+ createdAt: outbox.createdAt,
135
+ });
136
+ const [row] = rows;
137
+ return row === undefined ? null : toEvent(row);
138
+ },
139
+ /**
140
+ * ## Why all three transitions read `RETURNING`, and what that collapses
141
+ *
142
+ * Mongo's `complete` and `fail` answer `modifiedCount === 1` while its
143
+ * `renew` answers `matchedCount === 1`. `RETURNING` counts MATCHED rows, so
144
+ * this store answers the `matchedCount` question in all three places.
145
+ *
146
+ * That is equivalent HERE, and the argument is worth writing down because it
147
+ * is an argument rather than a test: the WHERE clause requires
148
+ * `status = 'processing'`, and `complete` and `fail` both write a different
149
+ * status, so a matched row is always a modified row. `renew` writes only
150
+ * `lease_until`/`updated_at` and Mongo already used `matchedCount` for it —
151
+ * a renewal that lands on the values already stored is still a lease this
152
+ * caller holds, and reporting it as lost would make a dispatcher abandon an
153
+ * event it still owns.
154
+ *
155
+ * If a later transition ever stops changing `status`, this equivalence stops
156
+ * holding and the difference becomes silent.
157
+ */
158
+ async complete({ eventId, leaseOwner, now }) {
159
+ const rows = await db
160
+ .update(outbox)
161
+ .set({
162
+ status: 'processed',
163
+ processedAt: now,
164
+ updatedAt: now,
165
+ leaseOwner: null,
166
+ leaseUntil: null,
167
+ lastError: null,
168
+ })
169
+ .where(heldLease(eventId, leaseOwner, now))
170
+ .returning({ id: outbox.id });
171
+ return rows.length === 1;
172
+ },
173
+ async renew({ eventId, leaseOwner, leaseUntil, now }) {
174
+ const rows = await db
175
+ .update(outbox)
176
+ .set({ leaseUntil, updatedAt: now })
177
+ .where(heldLease(eventId, leaseOwner, now))
178
+ .returning({ id: outbox.id });
179
+ return rows.length === 1;
180
+ },
181
+ async fail({ eventId, leaseOwner, status, availableAt, lastError, now }) {
182
+ const rows = await db
183
+ .update(outbox)
184
+ .set({
185
+ status,
186
+ availableAt,
187
+ lastError,
188
+ updatedAt: now,
189
+ leaseOwner: null,
190
+ leaseUntil: null,
191
+ })
192
+ .where(heldLease(eventId, leaseOwner, now))
193
+ .returning({ id: outbox.id });
194
+ return rows.length === 1;
195
+ },
196
+ async statusOf(eventId) {
197
+ const rows = await db
198
+ .select({ status: outbox.status })
199
+ .from(outbox)
200
+ .where(eq(outbox.id, eventId))
201
+ .limit(1);
202
+ const [row] = rows;
203
+ return row === undefined ? null : row.status;
204
+ },
205
+ };
206
+ }
207
+ //# sourceMappingURL=outbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.js","sourceRoot":"","sources":["../../../../../src/outbox/postgres/store/outbox.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,231 @@
1
+ import { and, asc, eq, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm';
2
+ const OPTIONAL_REPORT_FIELDS = [
3
+ 'details',
4
+ 'localStatusReason',
5
+ 'crowdSourceReportId',
6
+ 'crowdSourceCaseId',
7
+ 'crowdSourceMerged',
8
+ 'contentSnapshotHash',
9
+ 'submittedAt',
10
+ 'lastDeliveryError',
11
+ 'decisionId',
12
+ 'decisionRevision',
13
+ 'decisionOutcome',
14
+ 'decisionStatus',
15
+ 'decidedAt',
16
+ 'enforcedAction',
17
+ 'enforcedAt',
18
+ ];
19
+ /** Drop the package's own optional fields when the column is NULL. */
20
+ function absentWhereNull(row) {
21
+ const normalised = { ...row };
22
+ for (const field of OPTIONAL_REPORT_FIELDS) {
23
+ if (normalised[field] === null)
24
+ delete normalised[field];
25
+ }
26
+ return normalised;
27
+ }
28
+ /**
29
+ * The row, as the port declares it.
30
+ *
31
+ * An unchecked declaration, not a conversion — the same escape the Mongoose store
32
+ * takes with `.lean<TReport>()`, and for the same reason: a driver cannot know an
33
+ * adopter's row type, and `ModerationReportTable` deliberately erases column types
34
+ * so that any adopter's table is accepted.
35
+ *
36
+ * What bounds it is that both halves are checked elsewhere. Column PRESENCE is a
37
+ * compile error, gated by `scripts/test-report-table-type.mjs`. Column VALUES are
38
+ * asserted by round-trips against a real database in
39
+ * `postgresReportStore.test.ts`. What remains unchecked is a column declared with
40
+ * the wrong TYPE, which the migration and the schema test cover instead.
41
+ */
42
+ function asReport(row) {
43
+ return absentWhereNull(row);
44
+ }
45
+ export function postgresReportStore(input) {
46
+ const { db } = input;
47
+ const reports = input.reportTable;
48
+ /**
49
+ * `count(*)::int`, and the cast is load-bearing.
50
+ *
51
+ * `count(*)` is `bigint`, which postgres.js hands back as a STRING to avoid
52
+ * losing precision — so `sql<number>` without the cast is an assertion that
53
+ * quietly lies, and the caller's `number` is `'3'`. The `::int` makes the
54
+ * database do the conversion.
55
+ */
56
+ const total = sql `count(*)::int`;
57
+ return {
58
+ async findDuplicate({ reporter, reportedId, reportedType }, tx) {
59
+ const rows = await tx
60
+ .select()
61
+ .from(reports)
62
+ .where(and(eq(reports.reporter, reporter), eq(reports.reportedId, reportedId), eq(reports.reportedType, reportedType)))
63
+ .limit(1);
64
+ const [row] = rows;
65
+ return row === undefined ? null : asReport(row);
66
+ },
67
+ async insert(report, tx) {
68
+ const rows = await tx
69
+ .insert(reports)
70
+ .values({
71
+ /**
72
+ * The application's own columns FIRST, so a field this package owns can
73
+ * never be overwritten by one it knows nothing about. A report whose
74
+ * `localStatus` came from `extra` would be queued with nothing to
75
+ * deliver it, or received with a delivery event that tries anyway.
76
+ *
77
+ * A key that is not a column raises here — Postgres has no silent
78
+ * discard. Mongoose strict mode drops an undeclared path with no throw
79
+ * and no warning, which is why the Mongo half needs a standing test
80
+ * that every DTO field resolves to a schema path.
81
+ */
82
+ ...report.extra,
83
+ reportedType: report.reportedType,
84
+ reportedId: report.reportedId,
85
+ reporter: report.reporter,
86
+ categories: [...report.categories],
87
+ ...(report.details === undefined ? {} : { details: report.details }),
88
+ localStatus: report.localStatus,
89
+ ...(report.localStatusReason === undefined
90
+ ? {}
91
+ : { localStatusReason: report.localStatusReason }),
92
+ })
93
+ .returning();
94
+ const [row] = rows;
95
+ if (row === undefined) {
96
+ throw new Error('The moderation report insert returned no row.');
97
+ }
98
+ return asReport(row);
99
+ },
100
+ async findById(reportId) {
101
+ const rows = await db.select().from(reports).where(eq(reports.id, reportId)).limit(1);
102
+ const [row] = rows;
103
+ return row === undefined ? null : asReport(row);
104
+ },
105
+ async findByCaseId(caseId) {
106
+ const rows = await db
107
+ .select({
108
+ id: reports.id,
109
+ reportedType: reports.reportedType,
110
+ reportedId: reports.reportedId,
111
+ })
112
+ .from(reports)
113
+ .where(eq(reports.crowdSourceCaseId, caseId));
114
+ return rows.map((row) => ({
115
+ id: String(row.id),
116
+ reportedType: String(row.reportedType),
117
+ reportedId: String(row.reportedId),
118
+ }));
119
+ },
120
+ async applyDecision(reportId, update, maxRevision) {
121
+ const rows = await db
122
+ .update(reports)
123
+ .set(decisionSet(update))
124
+ .where(and(eq(reports.id, reportId),
125
+ /**
126
+ * The revision guard, in the WHERE clause. It is the DATABASE that
127
+ * refuses a stale write rather than a read-then-write in this
128
+ * process: deliveries overlap — CrowdSource retries for 24 hours, and
129
+ * a correction can arrive while the decision it supersedes is still
130
+ * being applied — and an older revision landing last would otherwise
131
+ * overwrite the current answer.
132
+ *
133
+ * `IS NULL` is the port of Mongo's `$exists: false`, because a report
134
+ * with no decision yet stores NULL rather than omitting the column.
135
+ * `<=` rather than `<` is deliberate: a redelivery of the SAME
136
+ * revision rewrites, which is harmless and keeps a partially-applied
137
+ * decision converging.
138
+ */
139
+ or(isNull(reports.decisionRevision), lte(reports.decisionRevision, maxRevision))))
140
+ .returning({ id: reports.id });
141
+ return rows.length === 1;
142
+ },
143
+ async markSubmitted(reportId, submission) {
144
+ await db
145
+ .update(reports)
146
+ .set({
147
+ localStatus: 'submitted',
148
+ crowdSourceReportId: submission.crowdSourceReportId,
149
+ crowdSourceCaseId: submission.crowdSourceCaseId,
150
+ crowdSourceMerged: submission.crowdSourceMerged,
151
+ contentSnapshotHash: submission.contentSnapshotHash,
152
+ submittedAt: submission.submittedAt,
153
+ // A report that has landed carries no failure and no reason it was
154
+ // going nowhere. `null` CLEARS in drizzle; `undefined` would leave the
155
+ // stale value in place, which is the port of Mongo's `$unset`.
156
+ lastDeliveryError: null,
157
+ localStatusReason: null,
158
+ })
159
+ .where(eq(reports.id, reportId));
160
+ },
161
+ async markDeliveryFailed(reportId, lastDeliveryError) {
162
+ await db
163
+ .update(reports)
164
+ .set({ localStatus: 'delivery_failed', lastDeliveryError })
165
+ .where(eq(reports.id, reportId));
166
+ },
167
+ async close(reportId, localStatusReason) {
168
+ await db
169
+ .update(reports)
170
+ .set({ localStatus: 'closed', localStatusReason })
171
+ .where(eq(reports.id, reportId));
172
+ },
173
+ async findPendingOldestFirst(limit) {
174
+ /**
175
+ * `queued` and `delivery_failed` only. `received` is excluded deliberately
176
+ * and the omission is the safety property, not an oversight: those reports
177
+ * have no subject provider, so an event re-derived for one would fail on
178
+ * its first attempt and dead-letter.
179
+ *
180
+ * Oldest first, and ASC needs no explicit NULLS placement: both drizzle and
181
+ * Postgres default ascending to NULLS LAST, so this matches the
182
+ * `(local_status, created_at)` index. A DESCENDING order would need the
183
+ * placement spelled out — see the enforcement store's reversal lookup for
184
+ * what that costs when it is not.
185
+ */
186
+ const rows = await db
187
+ .select({ id: reports.id })
188
+ .from(reports)
189
+ .where(inArray(reports.localStatus, ['queued', 'delivery_failed']))
190
+ .orderBy(asc(reports.createdAt))
191
+ .limit(limit);
192
+ return rows.map((row) => String(row.id));
193
+ },
194
+ async countAwaitingDecision(submittedBefore) {
195
+ const rows = await db
196
+ .select({ total })
197
+ .from(reports)
198
+ .where(and(eq(reports.localStatus, 'submitted'), lt(reports.submittedAt, submittedBefore)));
199
+ return rows[0]?.total ?? 0;
200
+ },
201
+ async countLocalOnly() {
202
+ const rows = await db
203
+ .select({ total })
204
+ .from(reports)
205
+ .where(eq(reports.localStatus, 'received'));
206
+ return rows[0]?.total ?? 0;
207
+ },
208
+ };
209
+ }
210
+ /**
211
+ * The `$set` a decision writes, as one object.
212
+ *
213
+ * `extra` FIRST, for the same reason it is first on insert: it is the adopter's
214
+ * legacy verdict field, and this package's own fields must win.
215
+ */
216
+ function decisionSet(update) {
217
+ return {
218
+ ...update.extra,
219
+ localStatus: update.localStatus,
220
+ decisionId: update.decisionId,
221
+ decisionRevision: update.decisionRevision,
222
+ decisionOutcome: update.decisionOutcome,
223
+ decisionStatus: update.decisionStatus,
224
+ decidedAt: update.decidedAt,
225
+ ...(update.enforcedAction === undefined
226
+ ? {}
227
+ : { enforcedAction: update.enforcedAction }),
228
+ ...(update.enforcedAt === undefined ? {} : { enforcedAt: update.enforcedAt }),
229
+ };
230
+ }
231
+ //# sourceMappingURL=reports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reports.js","sourceRoot":"","sources":["../../../../../src/outbox/postgres/store/reports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The transaction runner, in Postgres.
3
+ *
4
+ * READ COMMITTED, explicitly, and not `repeatable read`. Neither multi-statement
5
+ * transaction this package runs — a report insert plus an outbox upsert, an event
6
+ * update plus an outbox upsert — reads-then-decides in a way snapshot isolation
7
+ * protects, so `repeatable read` would import `40001` serialization failures and
8
+ * a retry loop for no benefit.
9
+ *
10
+ * What it deliberately does NOT promise: intake's duplicate-check-then-insert is
11
+ * not serialized at READ COMMITTED, and the "one report per reporter per object"
12
+ * unique index is explicitly the application's responsibility.
13
+ *
14
+ * The failure this runner exists to make impossible is subtler than Mongo's. A
15
+ * store call given `db` instead of `tx` inside the callback runs on a DIFFERENT
16
+ * pooled connection and commits on its own — no error, no warning, and the
17
+ * atomicity the outbox exists for is simply gone. `postgresOutboxStore.enqueue`
18
+ * refuses a handle that is not a transaction for exactly that reason.
19
+ */
20
+ export function postgresTransactionRunner(db) {
21
+ return {
22
+ async run(operation) {
23
+ return await db.transaction(async (tx) => await operation(tx), {
24
+ isolationLevel: 'read committed',
25
+ });
26
+ },
27
+ };
28
+ }
29
+ //# sourceMappingURL=transaction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../../../../src/outbox/postgres/store/transaction.ts"],"names":[],"mappings":"AAkCA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,yBAAyB,CACvC,EAAsB;IAEtB,OAAO;QACL,KAAK,CAAC,GAAG,CAAI,SAAiD;YAC5D,OAAO,MAAM,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,MAAM,SAAS,CAAC,EAAE,CAAC,EAAE;gBAC7D,cAAc,EAAE,gBAAgB;aACjC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC"}