@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,154 @@
1
+ import { type PgColumn, type PgTable } from 'drizzle-orm/pg-core';
2
+ export interface ModerationReportColumnOptions {
3
+ /**
4
+ * The application's reportable types. Constrains the stored value.
5
+ *
6
+ * This is NOT the set of DELIVERABLE types — a type with no subject provider is
7
+ * still reportable and still stored, it simply never leaves. Registering a
8
+ * provider is what makes a type deliverable, and the two lists are allowed to
9
+ * differ.
10
+ *
11
+ * Omitted, no CHECK is created. Supplying it later is a migration.
12
+ */
13
+ readonly reportedTypes?: readonly string[];
14
+ /** The application's report categories. Omitted, no CHECK is created. */
15
+ readonly categories?: readonly string[];
16
+ /** Maximum length of the reporter's free text. */
17
+ readonly detailsMaxLength?: number;
18
+ }
19
+ /**
20
+ * The columns this package reads and writes, to spread into the application's
21
+ * own `pgTable`.
22
+ *
23
+ * Every SQL name is written out. Drizzle would derive one from the property, and
24
+ * its derivation mangles digit- and capital-adjacent names — `crowdSourceReportId`
25
+ * becomes `crowd_source_report_id`, which is a working column with a name nobody
26
+ * chose and which no gate would notice.
27
+ */
28
+ export declare function moderationReportColumns(options?: ModerationReportColumnOptions): {
29
+ /**
30
+ * `text`, not `uuid`, and generated in the application.
31
+ *
32
+ * `generatedId()` holds a uuid v7 for a row created here and a 24-character
33
+ * ObjectId hex for every row that existed before a Mongo cutover, so one id
34
+ * space serves both. The consequence that matters downstream: a malformed id
35
+ * matches no rows instead of raising `22P02`, which is exactly the Mongo
36
+ * behaviour the delivery path already handles.
37
+ */
38
+ id: import("drizzle-orm").HasRuntimeDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"", [string, ...string[]]>>>>>;
39
+ reportedType: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"reported_type", [string, ...string[]]>>;
40
+ reportedId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"reported_id", [string, ...string[]]>>;
41
+ /**
42
+ * The reporting Oxy user id.
43
+ *
44
+ * The Oxy subject IS the identity binding proof, so there is no separate
45
+ * binding step for an application to implement — but it does mean this
46
+ * column must hold an Oxy user id and not an application-local one.
47
+ */
48
+ reporter: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"reporter", [string, ...string[]]>>;
49
+ /**
50
+ * `text[]`, written and read WHOLE.
51
+ *
52
+ * This package sets it at intake and reads it at delivery; it never queries
53
+ * by element, so an array column is the port rather than a join table. A
54
+ * containment CHECK constrains the values when the application declares them.
55
+ */
56
+ categories: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgArrayBuilder<{
57
+ name: "categories";
58
+ dataType: "array";
59
+ columnType: "PgArray";
60
+ data: string[];
61
+ driverParam: string | string[];
62
+ enumValues: [string, ...string[]];
63
+ size: undefined;
64
+ baseBuilder: {
65
+ name: "categories";
66
+ dataType: "string";
67
+ columnType: "PgText";
68
+ data: string;
69
+ enumValues: [string, ...string[]];
70
+ driverParam: string;
71
+ };
72
+ }, {
73
+ name: "categories";
74
+ dataType: "string";
75
+ columnType: "PgText";
76
+ data: string;
77
+ enumValues: [string, ...string[]];
78
+ driverParam: string;
79
+ }>>;
80
+ details: import("drizzle-orm/pg-core").PgVarcharBuilderInitial<"details", [string, ...string[]], number>;
81
+ localStatus: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"local_status", [string, ...string[]]>>>;
82
+ /**
83
+ * Why a report is not going anywhere, in words an operator can read.
84
+ *
85
+ * Stored rather than inferred from a missing outbox row. A missing row is
86
+ * also what a lost write looks like, and the two need to be distinguishable
87
+ * months later without re-deriving which types had providers at the time.
88
+ */
89
+ localStatusReason: import("drizzle-orm/pg-core").PgVarcharBuilderInitial<"local_status_reason", [string, ...string[]], 300>;
90
+ crowdSourceReportId: import("drizzle-orm/pg-core").PgTextBuilderInitial<"crowdsource_report_id", [string, ...string[]]>;
91
+ crowdSourceCaseId: import("drizzle-orm/pg-core").PgTextBuilderInitial<"crowdsource_case_id", [string, ...string[]]>;
92
+ crowdSourceMerged: import("drizzle-orm/pg-core").PgBooleanBuilderInitial<"crowdsource_merged">;
93
+ /** SHA-256 of the exact representation that was reviewed. */
94
+ contentSnapshotHash: import("drizzle-orm/pg-core").PgTextBuilderInitial<"content_snapshot_hash", [string, ...string[]]>;
95
+ submittedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"">;
96
+ lastDeliveryError: import("drizzle-orm/pg-core").PgVarcharBuilderInitial<"last_delivery_error", [string, ...string[]], 2000>;
97
+ decisionId: import("drizzle-orm/pg-core").PgTextBuilderInitial<"decision_id", [string, ...string[]]>;
98
+ /**
99
+ * The revision guard.
100
+ *
101
+ * Compared in the update's WHERE clause, so it is the database that refuses a
102
+ * stale write rather than a read-then-write in the application process.
103
+ */
104
+ decisionRevision: import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"decision_revision">;
105
+ decisionOutcome: import("drizzle-orm/pg-core").PgTextBuilderInitial<"decision_outcome", [string, ...string[]]>;
106
+ decisionStatus: import("drizzle-orm/pg-core").PgTextBuilderInitial<"decision_status", [string, ...string[]]>;
107
+ decidedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"">;
108
+ enforcedAction: import("drizzle-orm/pg-core").PgTextBuilderInitial<"enforced_action", [string, ...string[]]>;
109
+ enforcedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"">;
110
+ createdAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"">>>;
111
+ updatedAt: import("drizzle-orm").HasDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<"">>>>;
112
+ };
113
+ /** The column map, as the adopter's table carries it once built. */
114
+ export type ModerationReportColumns = ReturnType<typeof moderationReportColumns>;
115
+ /** Every column name the report store queries by. */
116
+ export type ModerationReportColumnName = keyof ModerationReportColumns;
117
+ /**
118
+ * The built columns, as `pgTable`'s third argument receives them.
119
+ *
120
+ * Deliberately NOT `ModerationReportTable`: drizzle calls that callback with the
121
+ * column map alone, which is not a table, so a parameter typed as one would
122
+ * refuse to be a `pgTable` callback at all.
123
+ */
124
+ export type ModerationReportBuiltColumns = Record<ModerationReportColumnName, PgColumn>;
125
+ /**
126
+ * A table carrying at least the moderation report columns.
127
+ *
128
+ * Structural on purpose: the adopter's table has its own name and its own extra
129
+ * columns, and a type derived from a concrete `pgTable` would name one table and
130
+ * therefore accept no other. What this asserts is the part that matters — that
131
+ * every column the report store reaches for is present.
132
+ */
133
+ export type ModerationReportTable = PgTable & Record<ModerationReportColumnName, PgColumn>;
134
+ /**
135
+ * The CHECKs and indexes the report table needs, as `pgTable`'s third argument.
136
+ *
137
+ * The three indexes are the same three `applyModerationReportIndexes` creates,
138
+ * and not optional: reconciliation scans `(local_status, created_at)` oldest-first
139
+ * on every sweep, and the decision worker looks a case up by
140
+ * `crowdsource_case_id` on every inbound decision. Without them both become
141
+ * sequential scans that grow with the table.
142
+ *
143
+ * The compound uniqueness of "one report per reporter per object" is the
144
+ * APPLICATION's to declare — some applications allow a reporter to file twice
145
+ * under different categories — so it is not created here. Intake's duplicate
146
+ * check reads `(reporter, reported_id, reported_type)`, so that one is indexed
147
+ * for it either way.
148
+ *
149
+ * Index names are derived from the adopter's own table name and length-checked,
150
+ * because two names that differ only past the 63rd byte are the same name to
151
+ * Postgres.
152
+ */
153
+ export declare function moderationReportTableExtras(options?: ModerationReportColumnOptions): (columns: ModerationReportBuiltColumns) => (import("drizzle-orm/pg-core").CheckBuilder | import("drizzle-orm/pg-core").IndexBuilder)[];
154
+ //# sourceMappingURL=reportColumns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reportColumns.d.ts","sourceRoot":"","sources":["../../../src/outbox/postgres/reportColumns.ts"],"names":[],"mappings":"AACA,OAAO,EAOL,KAAK,QAAQ,EACb,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AA+D7B,MAAM,WAAW,6BAA6B;IAC5C;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,yEAAyE;IACzE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,kDAAkD;IAClD,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,6BAAkC;IAE/E;;;;;;;;OAQG;;;;IAKH;;;;;;OAMG;;IAEH;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;IAOH;;;;;;OAMG;;;;;IAMH,6DAA6D;;;;;IAM7D;;;;;OAKG;;;;;;;;;EAWN;AAED,oEAAoE;AACpE,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAEjF,qDAAqD;AACrD,MAAM,MAAM,0BAA0B,GAAG,MAAM,uBAAuB,CAAC;AAEvE;;;;;;GAMG;AACH,MAAM,MAAM,4BAA4B,GAAG,MAAM,CAAC,0BAA0B,EAAE,QAAQ,CAAC,CAAC;AAExF;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,MAAM,CAAC,0BAA0B,EAAE,QAAQ,CAAC,CAAC;AAiB3F;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,6BAAkC,IAC7E,SAAS,4BAA4B,iGAgD9C"}
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.moderationReportColumns = moderationReportColumns;
4
+ exports.moderationReportTableExtras = moderationReportTableExtras;
5
+ const drizzle_orm_1 = require("drizzle-orm");
6
+ const pg_core_1 = require("drizzle-orm/pg-core");
7
+ const db_1 = require("@oxy.so/db");
8
+ /**
9
+ * The moderation half of an application's report table.
10
+ *
11
+ * The Postgres twin of `moderationReportSchemaFields`, and the split is the same
12
+ * one: the application owns the table — its name, its own extra columns, whatever
13
+ * verdict field it already had — and this package owns the SHAPE of the columns
14
+ * it queries, plus the indexes those queries depend on.
15
+ *
16
+ * ```ts
17
+ * const REPORT_MODERATION = {
18
+ * reportedTypes: ['listing', 'review'],
19
+ * categories: ['spam', 'harassment'],
20
+ * } as const;
21
+ *
22
+ * export const reports = pgTable(
23
+ * 'reports',
24
+ * {
25
+ * ...moderationReportColumns(REPORT_MODERATION),
26
+ * // …the application's own columns
27
+ * },
28
+ * moderationReportTableExtras(REPORT_MODERATION),
29
+ * );
30
+ * ```
31
+ *
32
+ * **`moderationReportTableExtras` takes the OPTIONS and returns the callback**,
33
+ * rather than taking the built columns directly. It has to: the CHECK on
34
+ * `reported_type` and the containment CHECK on `categories` are built from the
35
+ * same tuples that constrain the columns, and drizzle does not carry those
36
+ * tuples onto the built column (`enumValues` is `undefined` there — measured).
37
+ * Passing one options object to both calls is what keeps the column and its
38
+ * constraint from drifting apart.
39
+ */
40
+ /**
41
+ * The local statuses, as the CHECK renders them.
42
+ *
43
+ * Spelled out beside the DDL so it cannot acquire an ORM dependency. `satisfies`
44
+ * refuses a value outside the union; `postgresSchema.test.ts` asserts this tuple
45
+ * and `MODERATION_LOCAL_STATUSES` still agree, which is what catches an OMISSION
46
+ * — the direction a type cannot see.
47
+ */
48
+ const LOCAL_STATUSES = [
49
+ 'received',
50
+ 'queued',
51
+ 'submitted',
52
+ 'delivery_failed',
53
+ 'closed',
54
+ ];
55
+ /** Mongo's `detailsMaxLength ?? 2_000`, for the same reason: it bounds free text. */
56
+ const DEFAULT_DETAILS_MAX_LENGTH = 2000;
57
+ /**
58
+ * The columns this package reads and writes, to spread into the application's
59
+ * own `pgTable`.
60
+ *
61
+ * Every SQL name is written out. Drizzle would derive one from the property, and
62
+ * its derivation mangles digit- and capital-adjacent names — `crowdSourceReportId`
63
+ * becomes `crowd_source_report_id`, which is a working column with a name nobody
64
+ * chose and which no gate would notice.
65
+ */
66
+ function moderationReportColumns(options = {}) {
67
+ return {
68
+ /**
69
+ * `text`, not `uuid`, and generated in the application.
70
+ *
71
+ * `generatedId()` holds a uuid v7 for a row created here and a 24-character
72
+ * ObjectId hex for every row that existed before a Mongo cutover, so one id
73
+ * space serves both. The consequence that matters downstream: a malformed id
74
+ * matches no rows instead of raising `22P02`, which is exactly the Mongo
75
+ * behaviour the delivery path already handles.
76
+ */
77
+ id: (0, db_1.generatedId)(),
78
+ reportedType: (0, pg_core_1.text)('reported_type').notNull(),
79
+ reportedId: (0, pg_core_1.text)('reported_id').notNull(),
80
+ /**
81
+ * The reporting Oxy user id.
82
+ *
83
+ * The Oxy subject IS the identity binding proof, so there is no separate
84
+ * binding step for an application to implement — but it does mean this
85
+ * column must hold an Oxy user id and not an application-local one.
86
+ */
87
+ reporter: (0, pg_core_1.text)('reporter').notNull(),
88
+ /**
89
+ * `text[]`, written and read WHOLE.
90
+ *
91
+ * This package sets it at intake and reads it at delivery; it never queries
92
+ * by element, so an array column is the port rather than a join table. A
93
+ * containment CHECK constrains the values when the application declares them.
94
+ */
95
+ categories: (0, pg_core_1.text)('categories').array().notNull(),
96
+ details: (0, pg_core_1.varchar)('details', {
97
+ length: options.detailsMaxLength ?? DEFAULT_DETAILS_MAX_LENGTH,
98
+ }),
99
+ localStatus: (0, pg_core_1.text)('local_status').notNull().default('received'),
100
+ /**
101
+ * Why a report is not going anywhere, in words an operator can read.
102
+ *
103
+ * Stored rather than inferred from a missing outbox row. A missing row is
104
+ * also what a lost write looks like, and the two need to be distinguishable
105
+ * months later without re-deriving which types had providers at the time.
106
+ */
107
+ localStatusReason: (0, pg_core_1.varchar)('local_status_reason', { length: 300 }),
108
+ crowdSourceReportId: (0, pg_core_1.text)('crowdsource_report_id'),
109
+ crowdSourceCaseId: (0, pg_core_1.text)('crowdsource_case_id'),
110
+ crowdSourceMerged: (0, pg_core_1.boolean)('crowdsource_merged'),
111
+ /** SHA-256 of the exact representation that was reviewed. */
112
+ contentSnapshotHash: (0, pg_core_1.text)('content_snapshot_hash'),
113
+ submittedAt: (0, db_1.timestamptz)(),
114
+ lastDeliveryError: (0, pg_core_1.varchar)('last_delivery_error', { length: 2000 }),
115
+ decisionId: (0, pg_core_1.text)('decision_id'),
116
+ /**
117
+ * The revision guard.
118
+ *
119
+ * Compared in the update's WHERE clause, so it is the database that refuses a
120
+ * stale write rather than a read-then-write in the application process.
121
+ */
122
+ decisionRevision: (0, pg_core_1.integer)('decision_revision'),
123
+ decisionOutcome: (0, pg_core_1.text)('decision_outcome'),
124
+ decisionStatus: (0, pg_core_1.text)('decision_status'),
125
+ decidedAt: (0, db_1.timestamptz)(),
126
+ enforcedAction: (0, pg_core_1.text)('enforced_action'),
127
+ enforcedAt: (0, db_1.timestamptz)(),
128
+ createdAt: (0, db_1.createdAt)(),
129
+ updatedAt: (0, db_1.updatedAt)(),
130
+ };
131
+ }
132
+ /** Postgres truncates an identifier at 63 bytes, silently, and then names collide. */
133
+ const MAX_IDENTIFIER_BYTES = 63;
134
+ function identifier(parts) {
135
+ const name = parts.join('_');
136
+ if (Buffer.byteLength(name, 'utf8') > MAX_IDENTIFIER_BYTES) {
137
+ throw new Error(`The moderation report index name '${name}' is ${Buffer.byteLength(name, 'utf8')} bytes; ` +
138
+ `Postgres truncates identifiers at ${MAX_IDENTIFIER_BYTES} and two names that differ ` +
139
+ 'only past the cut then collide. Give the report table a shorter name.');
140
+ }
141
+ return name;
142
+ }
143
+ /**
144
+ * The CHECKs and indexes the report table needs, as `pgTable`'s third argument.
145
+ *
146
+ * The three indexes are the same three `applyModerationReportIndexes` creates,
147
+ * and not optional: reconciliation scans `(local_status, created_at)` oldest-first
148
+ * on every sweep, and the decision worker looks a case up by
149
+ * `crowdsource_case_id` on every inbound decision. Without them both become
150
+ * sequential scans that grow with the table.
151
+ *
152
+ * The compound uniqueness of "one report per reporter per object" is the
153
+ * APPLICATION's to declare — some applications allow a reporter to file twice
154
+ * under different categories — so it is not created here. Intake's duplicate
155
+ * check reads `(reporter, reported_id, reported_type)`, so that one is indexed
156
+ * for it either way.
157
+ *
158
+ * Index names are derived from the adopter's own table name and length-checked,
159
+ * because two names that differ only past the 63rd byte are the same name to
160
+ * Postgres.
161
+ */
162
+ function moderationReportTableExtras(options = {}) {
163
+ return (columns) => {
164
+ /**
165
+ * The adopter's table name, read off one of its own columns — `pgTable`
166
+ * calls this with the BUILT columns, each of which carries its table. That
167
+ * is what lets the index names belong to the adopter's table without being
168
+ * passed in a second time and drifting.
169
+ */
170
+ const prefix = (0, drizzle_orm_1.getTableName)(columns.localStatus.table);
171
+ return [
172
+ (0, pg_core_1.check)(identifier([prefix, 'local_status_check']), (0, drizzle_orm_1.sql) `${columns.localStatus} in (${drizzle_orm_1.sql.raw((0, db_1.inList)(LOCAL_STATUSES))})`),
173
+ ...(options.reportedTypes === undefined
174
+ ? []
175
+ : [
176
+ (0, pg_core_1.check)(identifier([prefix, 'reported_type_check']), (0, drizzle_orm_1.sql) `${columns.reportedType} in (${drizzle_orm_1.sql.raw((0, db_1.inList)(options.reportedTypes))})`),
177
+ ]),
178
+ ...(options.categories === undefined
179
+ ? []
180
+ : [
181
+ /**
182
+ * Containment, not equality: a report carries a SUBSET of the
183
+ * declared categories, and `<@` is trivially satisfied by an empty
184
+ * array — which the application's own validation refuses before this
185
+ * is ever reached.
186
+ */
187
+ (0, pg_core_1.check)(identifier([prefix, 'categories_check']), (0, drizzle_orm_1.sql) `${columns.categories} <@ ${drizzle_orm_1.sql.raw((0, db_1.textArrayLiteral)(options.categories))}`),
188
+ ]),
189
+ (0, pg_core_1.index)(identifier([prefix, 'local_status_created_at_idx'])).on(columns.localStatus, columns.createdAt),
190
+ (0, pg_core_1.index)(identifier([prefix, 'crowdsource_case_id_idx'])).on(columns.crowdSourceCaseId),
191
+ (0, pg_core_1.index)(identifier([prefix, 'reporter_object_idx'])).on(columns.reporter, columns.reportedId, columns.reportedType),
192
+ ];
193
+ };
194
+ }
195
+ //# sourceMappingURL=reportColumns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reportColumns.js","sourceRoot":"","sources":["../../../src/outbox/postgres/reportColumns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { ModerationEnforcementStore } from '../../store/types.js';
2
+ import type { ModerationTables } from '../tables.js';
3
+ import type { ModerationPgHandle } from './transaction.js';
4
+ /**
5
+ * The enforcement ledger, in Postgres.
6
+ *
7
+ * Two operations carry everything: the CLAIM that makes an action happen at most
8
+ * once, and the reversal LOOKUP that decides what a correction puts back. Two of
9
+ * this package's eleven proven mutations attack the second one, which makes its
10
+ * predicate the most load-bearing SQL in the Postgres half.
11
+ *
12
+ * ## The idempotency key IS the primary key
13
+ *
14
+ * `decision_id + decision_revision + action`. Mongo needed a surrogate `_id` plus
15
+ * a unique index on that triple; here the triple is the key, so
16
+ * `onConflictDoNothing()` needs no explicit `target` — there is only one
17
+ * constraint it could mean — and there is no second object to keep in step.
18
+ *
19
+ * `decision_revision` being IN the key is what lets a correction act: a new
20
+ * revision is a different row, so the restore it asks for is allowed to happen
21
+ * while still being impossible to apply twice itself.
22
+ *
23
+ * ## Addressed by the key, never by a record id
24
+ *
25
+ * Every write after the claim addresses the row by the same three values the
26
+ * claim used. PostgreSQL reaches it through the composite primary key, so no
27
+ * opaque record id crosses the port.
28
+ */
29
+ export declare function postgresEnforcementStore(input: {
30
+ db: ModerationPgHandle;
31
+ tables: ModerationTables;
32
+ }): ModerationEnforcementStore;
33
+ //# sourceMappingURL=enforcement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement.d.ts","sourceRoot":"","sources":["../../../../src/outbox/postgres/store/enforcement.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,0BAA0B,EAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,EAAE,EAAE,kBAAkB,CAAC;IACvB,MAAM,EAAE,gBAAgB,CAAC;CAC1B,GAAG,0BAA0B,CAqJ7B"}
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postgresEnforcementStore = postgresEnforcementStore;
4
+ const drizzle_orm_1 = require("drizzle-orm");
5
+ /**
6
+ * The enforcement ledger, in Postgres.
7
+ *
8
+ * Two operations carry everything: the CLAIM that makes an action happen at most
9
+ * once, and the reversal LOOKUP that decides what a correction puts back. Two of
10
+ * this package's eleven proven mutations attack the second one, which makes its
11
+ * predicate the most load-bearing SQL in the Postgres half.
12
+ *
13
+ * ## The idempotency key IS the primary key
14
+ *
15
+ * `decision_id + decision_revision + action`. Mongo needed a surrogate `_id` plus
16
+ * a unique index on that triple; here the triple is the key, so
17
+ * `onConflictDoNothing()` needs no explicit `target` — there is only one
18
+ * constraint it could mean — and there is no second object to keep in step.
19
+ *
20
+ * `decision_revision` being IN the key is what lets a correction act: a new
21
+ * revision is a different row, so the restore it asks for is allowed to happen
22
+ * while still being impossible to apply twice itself.
23
+ *
24
+ * ## Addressed by the key, never by a record id
25
+ *
26
+ * Every write after the claim addresses the row by the same three values the
27
+ * claim used. PostgreSQL reaches it through the composite primary key, so no
28
+ * opaque record id crosses the port.
29
+ */
30
+ function postgresEnforcementStore(input) {
31
+ const { db } = input;
32
+ const enforcements = input.tables.enforcements;
33
+ /** The primary key, as a predicate. Addresses at most one row, by construction. */
34
+ const keyFilter = (key) => (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(enforcements.decisionId, key.decisionId), (0, drizzle_orm_1.eq)(enforcements.decisionRevision, key.decisionRevision), (0, drizzle_orm_1.eq)(enforcements.action, key.action));
35
+ return {
36
+ async claim(row) {
37
+ /**
38
+ * The insert IS the check, and a lost race is zero rows rather than an
39
+ * error — the same shape as the event store's claim, and for the same
40
+ * reason: there is no catch block whose predicate could be widened into
41
+ * swallowing a real fault as "another delivery already handled it".
42
+ *
43
+ * `created_at` and `updated_at` are written from the caller's `now`. The
44
+ * Mongo store lets Mongoose own them; here nothing does, and the reversal
45
+ * lookup ORDERS BY `created_at`, so the clock that decides which row is
46
+ * "most recent" is the caller's rather than two different defaults'.
47
+ */
48
+ const rows = await db
49
+ .insert(enforcements)
50
+ .values({
51
+ decisionId: row.decisionId,
52
+ decisionRevision: row.decisionRevision,
53
+ action: row.action,
54
+ caseId: row.caseId,
55
+ subjectType: row.subjectType,
56
+ subjectId: row.subjectId,
57
+ outcome: row.outcome,
58
+ ...(row.recommendedAction === undefined
59
+ ? {}
60
+ : { recommendedAction: row.recommendedAction }),
61
+ reason: row.reason,
62
+ mode: row.mode,
63
+ applied: false,
64
+ createdAt: row.now,
65
+ updatedAt: row.now,
66
+ })
67
+ .onConflictDoNothing()
68
+ .returning({ decisionId: enforcements.decisionId });
69
+ return rows.length === 1;
70
+ },
71
+ async markSkipped(key, { skippedReason, recordedAs, now }) {
72
+ await db
73
+ .update(enforcements)
74
+ .set({
75
+ skippedReason,
76
+ // Absent means leave it: `recorded_as` only carries a value when the
77
+ // effect said the planned action amounted to something else.
78
+ ...(recordedAs === undefined ? {} : { recordedAs }),
79
+ updatedAt: now,
80
+ })
81
+ .where(keyFilter(key));
82
+ },
83
+ async markApplied(key, { appliedAt, previousState, now }) {
84
+ await db
85
+ .update(enforcements)
86
+ .set({
87
+ applied: true,
88
+ appliedAt,
89
+ /**
90
+ * Written as `jsonb`, so it round-trips as the object it was rather
91
+ * than as its `JSON.stringify` — a `text` column would hand a reversal
92
+ * a string, and `previousState?.status` on a string is `undefined`,
93
+ * which reads as "moderation displaced nothing" and restores a guess.
94
+ *
95
+ * Absent means leave it NULL: an action that changed state records what
96
+ * it displaced, and one that did not must not claim it displaced
97
+ * nothing-in-particular.
98
+ */
99
+ ...(previousState === undefined ? {} : { previousState }),
100
+ updatedAt: now,
101
+ })
102
+ .where(keyFilter(key));
103
+ },
104
+ async releaseClaim(key) {
105
+ await db.delete(enforcements).where(keyFilter(key));
106
+ },
107
+ async latestApplied({ subjectType, subjectId, actions }) {
108
+ /**
109
+ * The reversal lookup, and every clause in it is load-bearing.
110
+ *
111
+ * `eq(applied, true)` — a row that was RECORDED and never carried out
112
+ * describes a state change that never happened, and it carries no
113
+ * `previous_state` at all. Reading it hands a reversal nothing and the
114
+ * application's fallback publishes a draft moderation only ever hid.
115
+ *
116
+ * `inArray(action, actions)` — one action may reverse several. The most
117
+ * recent APPLIED row across the whole declared set wins, so `apply`
118
+ * receives whatever actually happened last rather than whichever action
119
+ * happens to be first in the array. Note the membership is POSITIVE: a
120
+ * negated set (`not in (…)`) would render as `<> ALL (…)`, which no btree
121
+ * can serve, and the supporting index would quietly stop being used.
122
+ *
123
+ * `created_at desc NULLS LAST` with `limit(1)` — newest first, and the
124
+ * NULLS placement is written out because drizzle's two spellings of "desc"
125
+ * DISAGREE and the difference is a blocking sort.
126
+ *
127
+ * Measured on Postgres 17. `.desc()` inside a drizzle INDEX emits
128
+ * `DESC NULLS LAST`; `desc(column)` in an ORDER BY emits plain `DESC`,
129
+ * which in Postgres means NULLS FIRST. Those two orderings do not match, so
130
+ * no index can satisfy the sort and the plan gains a `Sort` node — even
131
+ * though `created_at` is NOT NULL and the two orderings can differ by
132
+ * nothing at all. With `nulls last` the plan is a plain index scan under
133
+ * the Limit:
134
+ *
135
+ * desc -> Limit → Sort (created_at DESC) → Index Scan …
136
+ * desc nulls last -> Limit → Index Scan …
137
+ *
138
+ * Correct results either way, and a sort that grows with the number of
139
+ * enforcement rows for one subject. `postgresEnforcementStore.test.ts`
140
+ * asserts the absence of that `Sort` against the real planner.
141
+ */
142
+ const rows = await db
143
+ .select({
144
+ action: enforcements.action,
145
+ previousState: enforcements.previousState,
146
+ })
147
+ .from(enforcements)
148
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(enforcements.subjectType, subjectType), (0, drizzle_orm_1.eq)(enforcements.subjectId, subjectId), (0, drizzle_orm_1.inArray)(enforcements.action, [...actions]), (0, drizzle_orm_1.eq)(enforcements.applied, true)))
149
+ .orderBy((0, drizzle_orm_1.sql) `${enforcements.createdAt} desc nulls last`)
150
+ .limit(1);
151
+ const [row] = rows;
152
+ if (row === undefined)
153
+ return null;
154
+ return {
155
+ action: row.action,
156
+ ...(row.previousState === null ? {} : { previousState: row.previousState }),
157
+ };
158
+ },
159
+ };
160
+ }
161
+ //# sourceMappingURL=enforcement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enforcement.js","sourceRoot":"","sources":["../../../../src/outbox/postgres/store/enforcement.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { ModerationEventStore } from '../../store/types.js';
2
+ import type { ModerationTables } from '../tables.js';
3
+ import type { ModerationPgHandle } from './transaction.js';
4
+ /**
5
+ * The inbound webhook event log, in Postgres.
6
+ *
7
+ * ## The one place this backend is structurally better rather than equivalent
8
+ *
9
+ * Everything else in this port aims for parity. This claim does not: it removes a
10
+ * failure mode instead of handling it, and the difference is worth being precise
11
+ * about, because "better" is otherwise just an adjective.
12
+ *
13
+ * **Mongo's claim throws and catches.** It inserts, and reads `code === 11000` to
14
+ * tell "somebody else has this event" from a real fault; everything that is not
15
+ * 11000 is rethrown, so a lost connection or a failover answers non-2xx and the
16
+ * event stays on the sender's retry schedule. That property is correct — and it
17
+ * lives in a PREDICATE. `catch { return false }` is one keystroke away, it
18
+ * type-checks, and it turns a connection failure into "already processed": the
19
+ * receiver answers 200 and a decision is retired that nobody ever handled. The
20
+ * only thing standing between those two behaviours is a conditional somebody
21
+ * could widen, and a test can only catch that by injecting a driver failure.
22
+ *
23
+ * **Postgres's claim does not throw at all.** `ON CONFLICT DO NOTHING` plus
24
+ * `RETURNING` makes a duplicate a ROW COUNT rather than an error: one row means
25
+ * this call took the claim, zero means somebody else holds it. So there is no
26
+ * catch block here — and therefore
27
+ *
28
+ * - no predicate to widen,
29
+ * - no code path that can convert a fault into a negative answer,
30
+ * - and the "rethrow everything else" guarantee holds by the ABSENCE of code
31
+ * rather than by the presence of correct code.
32
+ *
33
+ * That is the mechanism: the Mongo version is a property of code that exists and
34
+ * can be edited wrongly; this one is a property of code that does not exist. Only
35
+ * the second cannot be broken by a well-meaning change.
36
+ *
37
+ * The consequence for the mutation suite, stated because its absence would
38
+ * otherwise look like a gap: there is nothing to DELETE on the insert side, so no
39
+ * mutation can attack it. The one that exists attacks the READ — `rows.length === 1`
40
+ * collapsing to `true`, which would hand the same event to two handlers.
41
+ */
42
+ export declare function postgresEventStore(input: {
43
+ db: ModerationPgHandle;
44
+ tables: ModerationTables;
45
+ }): ModerationEventStore<ModerationPgHandle>;
46
+ //# sourceMappingURL=events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../../src/outbox/postgres/store/events.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,EAAE,EAAE,kBAAkB,CAAC;IACvB,MAAM,EAAE,gBAAgB,CAAC;CAC1B,GAAG,oBAAoB,CAAC,kBAAkB,CAAC,CAiE3C"}