@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.
- package/README.md +571 -0
- package/dist/cases.d.ts +66 -0
- package/dist/cases.d.ts.map +1 -0
- package/dist/cases.js +79 -0
- package/dist/cases.js.map +1 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +125 -0
- package/dist/client.js.map +1 -0
- package/dist/communityNotes.d.ts +49 -0
- package/dist/communityNotes.d.ts.map +1 -0
- package/dist/communityNotes.js +104 -0
- package/dist/communityNotes.js.map +1 -0
- package/dist/credential.d.ts +58 -0
- package/dist/credential.d.ts.map +1 -0
- package/dist/credential.js +71 -0
- package/dist/credential.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +82 -0
- package/dist/defaults.js.map +1 -0
- package/dist/digest.d.ts +44 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +79 -0
- package/dist/digest.js.map +1 -0
- package/dist/envelope.d.ts +143 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +258 -0
- package/dist/envelope.js.map +1 -0
- package/dist/errors.d.ts +90 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +151 -0
- package/dist/errors.js.map +1 -0
- package/dist/esm/cases.js +74 -0
- package/dist/esm/cases.js.map +1 -0
- package/dist/esm/client.js +121 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/communityNotes.js +100 -0
- package/dist/esm/communityNotes.js.map +1 -0
- package/dist/esm/credential.js +67 -0
- package/dist/esm/credential.js.map +1 -0
- package/dist/esm/defaults.js +77 -0
- package/dist/esm/defaults.js.map +1 -0
- package/dist/esm/digest.js +74 -0
- package/dist/esm/digest.js.map +1 -0
- package/dist/esm/envelope.js +252 -0
- package/dist/esm/envelope.js.map +1 -0
- package/dist/esm/errors.js +140 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/express/index.js +29 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +293 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/express/store.js +69 -0
- package/dist/esm/express/store.js.map +1 -0
- package/dist/esm/express/verify.js +133 -0
- package/dist/esm/express/verify.js.map +1 -0
- package/dist/esm/index.js +57 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/outbox/client.js +43 -0
- package/dist/esm/outbox/client.js.map +1 -0
- package/dist/esm/outbox/decision.js +153 -0
- package/dist/esm/outbox/decision.js.map +1 -0
- package/dist/esm/outbox/delivery.js +129 -0
- package/dist/esm/outbox/delivery.js.map +1 -0
- package/dist/esm/outbox/enforcement/executor.js +219 -0
- package/dist/esm/outbox/enforcement/executor.js.map +1 -0
- package/dist/esm/outbox/enforcement/planner.js +326 -0
- package/dist/esm/outbox/enforcement/planner.js.map +1 -0
- package/dist/esm/outbox/evidence.js +173 -0
- package/dist/esm/outbox/evidence.js.map +1 -0
- package/dist/esm/outbox/inbound.js +87 -0
- package/dist/esm/outbox/inbound.js.map +1 -0
- package/dist/esm/outbox/index.js +71 -0
- package/dist/esm/outbox/index.js.map +1 -0
- package/dist/esm/outbox/intake.js +140 -0
- package/dist/esm/outbox/intake.js.map +1 -0
- package/dist/esm/outbox/integration.js +116 -0
- package/dist/esm/outbox/integration.js.map +1 -0
- package/dist/esm/outbox/outbox/dispatcher.js +114 -0
- package/dist/esm/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/esm/outbox/outbox/service.js +271 -0
- package/dist/esm/outbox/outbox/service.js.map +1 -0
- package/dist/esm/outbox/postgres/index.js +41 -0
- package/dist/esm/outbox/postgres/index.js.map +1 -0
- package/dist/esm/outbox/postgres/registries.js +155 -0
- package/dist/esm/outbox/postgres/registries.js.map +1 -0
- package/dist/esm/outbox/postgres/reportColumns.js +191 -0
- package/dist/esm/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/esm/outbox/postgres/store/enforcement.js +158 -0
- package/dist/esm/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/esm/outbox/postgres/store/events.js +101 -0
- package/dist/esm/outbox/postgres/store/events.js.map +1 -0
- package/dist/esm/outbox/postgres/store/index.js +59 -0
- package/dist/esm/outbox/postgres/store/index.js.map +1 -0
- package/dist/esm/outbox/postgres/store/outbox.js +207 -0
- package/dist/esm/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/esm/outbox/postgres/store/reports.js +231 -0
- package/dist/esm/outbox/postgres/store/reports.js.map +1 -0
- package/dist/esm/outbox/postgres/store/transaction.js +29 -0
- package/dist/esm/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/esm/outbox/postgres/tables.js +270 -0
- package/dist/esm/outbox/postgres/tables.js.map +1 -0
- package/dist/esm/outbox/reconciliation.js +157 -0
- package/dist/esm/outbox/reconciliation.js.map +1 -0
- package/dist/esm/outbox/reportStatus.js +22 -0
- package/dist/esm/outbox/reportStatus.js.map +1 -0
- package/dist/esm/outbox/retention.js +33 -0
- package/dist/esm/outbox/retention.js.map +1 -0
- package/dist/esm/outbox/store/types.js +30 -0
- package/dist/esm/outbox/store/types.js.map +1 -0
- package/dist/esm/outbox/types.js +23 -0
- package/dist/esm/outbox/types.js.map +1 -0
- package/dist/esm/outbox/webhook.js +145 -0
- package/dist/esm/outbox/webhook.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/reports.js +74 -0
- package/dist/esm/reports.js.map +1 -0
- package/dist/esm/testing/fixtures.js +132 -0
- package/dist/esm/testing/fixtures.js.map +1 -0
- package/dist/esm/testing/index.js +33 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/testing/sandbox.js +337 -0
- package/dist/esm/testing/sandbox.js.map +1 -0
- package/dist/esm/testing/webhook-simulator.js +93 -0
- package/dist/esm/testing/webhook-simulator.js.map +1 -0
- package/dist/esm/transport.js +177 -0
- package/dist/esm/transport.js.map +1 -0
- package/dist/esm/webhookEndpoints.js +139 -0
- package/dist/esm/webhookEndpoints.js.map +1 -0
- package/dist/express/index.d.ts +32 -0
- package/dist/express/index.d.ts.map +1 -0
- package/dist/express/index.js +40 -0
- package/dist/express/index.js.map +1 -0
- package/dist/express/middleware.d.ts +97 -0
- package/dist/express/middleware.d.ts.map +1 -0
- package/dist/express/middleware.js +298 -0
- package/dist/express/middleware.js.map +1 -0
- package/dist/express/store.d.ts +45 -0
- package/dist/express/store.d.ts.map +1 -0
- package/dist/express/store.js +72 -0
- package/dist/express/store.js.map +1 -0
- package/dist/express/verify.d.ts +86 -0
- package/dist/express/verify.d.ts.map +1 -0
- package/dist/express/verify.js +138 -0
- package/dist/express/verify.js.map +1 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/outbox/client.d.ts +35 -0
- package/dist/outbox/client.d.ts.map +1 -0
- package/dist/outbox/client.js +46 -0
- package/dist/outbox/client.js.map +1 -0
- package/dist/outbox/decision.d.ts +36 -0
- package/dist/outbox/decision.d.ts.map +1 -0
- package/dist/outbox/decision.js +159 -0
- package/dist/outbox/decision.js.map +1 -0
- package/dist/outbox/delivery.d.ts +50 -0
- package/dist/outbox/delivery.d.ts.map +1 -0
- package/dist/outbox/delivery.js +135 -0
- package/dist/outbox/delivery.js.map +1 -0
- package/dist/outbox/enforcement/executor.d.ts +26 -0
- package/dist/outbox/enforcement/executor.d.ts.map +1 -0
- package/dist/outbox/enforcement/executor.js +222 -0
- package/dist/outbox/enforcement/executor.js.map +1 -0
- package/dist/outbox/enforcement/planner.d.ts +50 -0
- package/dist/outbox/enforcement/planner.d.ts.map +1 -0
- package/dist/outbox/enforcement/planner.js +333 -0
- package/dist/outbox/enforcement/planner.js.map +1 -0
- package/dist/outbox/evidence.d.ts +114 -0
- package/dist/outbox/evidence.d.ts.map +1 -0
- package/dist/outbox/evidence.js +181 -0
- package/dist/outbox/evidence.js.map +1 -0
- package/dist/outbox/inbound.d.ts +78 -0
- package/dist/outbox/inbound.d.ts.map +1 -0
- package/dist/outbox/inbound.js +91 -0
- package/dist/outbox/inbound.js.map +1 -0
- package/dist/outbox/index.d.ts +79 -0
- package/dist/outbox/index.d.ts.map +1 -0
- package/dist/outbox/index.js +98 -0
- package/dist/outbox/index.js.map +1 -0
- package/dist/outbox/intake.d.ts +62 -0
- package/dist/outbox/intake.d.ts.map +1 -0
- package/dist/outbox/intake.js +145 -0
- package/dist/outbox/intake.js.map +1 -0
- package/dist/outbox/integration.d.ts +62 -0
- package/dist/outbox/integration.d.ts.map +1 -0
- package/dist/outbox/integration.js +119 -0
- package/dist/outbox/integration.js.map +1 -0
- package/dist/outbox/outbox/dispatcher.d.ts +26 -0
- package/dist/outbox/outbox/dispatcher.d.ts.map +1 -0
- package/dist/outbox/outbox/dispatcher.js +119 -0
- package/dist/outbox/outbox/dispatcher.js.map +1 -0
- package/dist/outbox/outbox/service.d.ts +124 -0
- package/dist/outbox/outbox/service.d.ts.map +1 -0
- package/dist/outbox/outbox/service.js +279 -0
- package/dist/outbox/outbox/service.js.map +1 -0
- package/dist/outbox/postgres/index.d.ts +44 -0
- package/dist/outbox/postgres/index.d.ts.map +1 -0
- package/dist/outbox/postgres/index.js +51 -0
- package/dist/outbox/postgres/index.js.map +1 -0
- package/dist/outbox/postgres/registries.d.ts +69 -0
- package/dist/outbox/postgres/registries.d.ts.map +1 -0
- package/dist/outbox/postgres/registries.js +159 -0
- package/dist/outbox/postgres/registries.js.map +1 -0
- package/dist/outbox/postgres/reportColumns.d.ts +154 -0
- package/dist/outbox/postgres/reportColumns.d.ts.map +1 -0
- package/dist/outbox/postgres/reportColumns.js +195 -0
- package/dist/outbox/postgres/reportColumns.js.map +1 -0
- package/dist/outbox/postgres/store/enforcement.d.ts +33 -0
- package/dist/outbox/postgres/store/enforcement.d.ts.map +1 -0
- package/dist/outbox/postgres/store/enforcement.js +161 -0
- package/dist/outbox/postgres/store/enforcement.js.map +1 -0
- package/dist/outbox/postgres/store/events.d.ts +46 -0
- package/dist/outbox/postgres/store/events.d.ts.map +1 -0
- package/dist/outbox/postgres/store/events.js +104 -0
- package/dist/outbox/postgres/store/events.js.map +1 -0
- package/dist/outbox/postgres/store/index.d.ts +27 -0
- package/dist/outbox/postgres/store/index.d.ts.map +1 -0
- package/dist/outbox/postgres/store/index.js +62 -0
- package/dist/outbox/postgres/store/index.js.map +1 -0
- package/dist/outbox/postgres/store/outbox.d.ts +8 -0
- package/dist/outbox/postgres/store/outbox.d.ts.map +1 -0
- package/dist/outbox/postgres/store/outbox.js +210 -0
- package/dist/outbox/postgres/store/outbox.js.map +1 -0
- package/dist/outbox/postgres/store/reports.d.ts +69 -0
- package/dist/outbox/postgres/store/reports.d.ts.map +1 -0
- package/dist/outbox/postgres/store/reports.js +234 -0
- package/dist/outbox/postgres/store/reports.js.map +1 -0
- package/dist/outbox/postgres/store/transaction.d.ts +49 -0
- package/dist/outbox/postgres/store/transaction.d.ts.map +1 -0
- package/dist/outbox/postgres/store/transaction.js +32 -0
- package/dist/outbox/postgres/store/transaction.js.map +1 -0
- package/dist/outbox/postgres/tables.d.ts +737 -0
- package/dist/outbox/postgres/tables.d.ts.map +1 -0
- package/dist/outbox/postgres/tables.js +273 -0
- package/dist/outbox/postgres/tables.js.map +1 -0
- package/dist/outbox/reconciliation.d.ts +49 -0
- package/dist/outbox/reconciliation.d.ts.map +1 -0
- package/dist/outbox/reconciliation.js +162 -0
- package/dist/outbox/reconciliation.js.map +1 -0
- package/dist/outbox/reportStatus.d.ts +12 -0
- package/dist/outbox/reportStatus.d.ts.map +1 -0
- package/dist/outbox/reportStatus.js +25 -0
- package/dist/outbox/reportStatus.js.map +1 -0
- package/dist/outbox/retention.d.ts +33 -0
- package/dist/outbox/retention.d.ts.map +1 -0
- package/dist/outbox/retention.js +36 -0
- package/dist/outbox/retention.js.map +1 -0
- package/dist/outbox/store/types.d.ts +466 -0
- package/dist/outbox/store/types.d.ts.map +1 -0
- package/dist/outbox/store/types.js +31 -0
- package/dist/outbox/store/types.js.map +1 -0
- package/dist/outbox/types.d.ts +660 -0
- package/dist/outbox/types.d.ts.map +1 -0
- package/dist/outbox/types.js +24 -0
- package/dist/outbox/types.js.map +1 -0
- package/dist/outbox/webhook.d.ts +15 -0
- package/dist/outbox/webhook.d.ts.map +1 -0
- package/dist/outbox/webhook.js +148 -0
- package/dist/outbox/webhook.js.map +1 -0
- package/dist/reports.d.ts +73 -0
- package/dist/reports.d.ts.map +1 -0
- package/dist/reports.js +78 -0
- package/dist/reports.js.map +1 -0
- package/dist/testing/fixtures.d.ts +62 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +137 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +43 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/sandbox.d.ts +109 -0
- package/dist/testing/sandbox.d.ts.map +1 -0
- package/dist/testing/sandbox.js +342 -0
- package/dist/testing/sandbox.js.map +1 -0
- package/dist/testing/webhook-simulator.d.ts +78 -0
- package/dist/testing/webhook-simulator.d.ts.map +1 -0
- package/dist/testing/webhook-simulator.js +98 -0
- package/dist/testing/webhook-simulator.js.map +1 -0
- package/dist/transport.d.ts +59 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +181 -0
- package/dist/transport.js.map +1 -0
- package/dist/webhookEndpoints.d.ts +147 -0
- package/dist/webhookEndpoints.d.ts.map +1 -0
- package/dist/webhookEndpoints.js +143 -0
- package/dist/webhookEndpoints.js.map +1 -0
- package/package.json +127 -0
- package/src/cases.ts +119 -0
- package/src/client.ts +202 -0
- package/src/communityNotes.ts +194 -0
- package/src/credential.ts +100 -0
- package/src/defaults.ts +87 -0
- package/src/digest.ts +91 -0
- package/src/envelope.ts +432 -0
- package/src/errors.ts +165 -0
- package/src/express/index.ts +43 -0
- package/src/express/middleware.ts +387 -0
- package/src/express/store.ts +85 -0
- package/src/express/verify.ts +166 -0
- package/src/index.ts +107 -0
- package/src/outbox/client.ts +75 -0
- package/src/outbox/decision.ts +209 -0
- package/src/outbox/delivery.ts +163 -0
- package/src/outbox/enforcement/executor.ts +288 -0
- package/src/outbox/enforcement/planner.ts +377 -0
- package/src/outbox/evidence.ts +231 -0
- package/src/outbox/inbound.ts +140 -0
- package/src/outbox/index.ts +160 -0
- package/src/outbox/intake.ts +175 -0
- package/src/outbox/integration.ts +205 -0
- package/src/outbox/outbox/dispatcher.ts +131 -0
- package/src/outbox/outbox/service.ts +444 -0
- package/src/outbox/postgres/index.ts +59 -0
- package/src/outbox/postgres/registries.ts +177 -0
- package/src/outbox/postgres/reportColumns.ts +281 -0
- package/src/outbox/postgres/store/enforcement.ts +186 -0
- package/src/outbox/postgres/store/events.ts +112 -0
- package/src/outbox/postgres/store/index.ts +72 -0
- package/src/outbox/postgres/store/outbox.ts +273 -0
- package/src/outbox/postgres/store/reports.ts +349 -0
- package/src/outbox/postgres/store/transaction.ts +64 -0
- package/src/outbox/postgres/tables.ts +339 -0
- package/src/outbox/reconciliation.ts +199 -0
- package/src/outbox/reportStatus.ts +24 -0
- package/src/outbox/retention.ts +34 -0
- package/src/outbox/store/types.ts +528 -0
- package/src/outbox/types.ts +753 -0
- package/src/outbox/webhook.ts +165 -0
- package/src/reports.ts +138 -0
- package/src/testing/fixtures.ts +183 -0
- package/src/testing/index.ts +57 -0
- package/src/testing/sandbox.ts +469 -0
- package/src/testing/webhook-simulator.ts +166 -0
- package/src/transport.ts +250 -0
- 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"}
|