@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,191 @@
|
|
|
1
|
+
import { getTableName, sql } from 'drizzle-orm';
|
|
2
|
+
import { boolean, check, index, integer, text, varchar, } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { createdAt, generatedId, inList, textArrayLiteral, timestamptz, updatedAt, } from '@oxy.so/db';
|
|
4
|
+
/**
|
|
5
|
+
* The moderation half of an application's report table.
|
|
6
|
+
*
|
|
7
|
+
* The Postgres twin of `moderationReportSchemaFields`, and the split is the same
|
|
8
|
+
* one: the application owns the table — its name, its own extra columns, whatever
|
|
9
|
+
* verdict field it already had — and this package owns the SHAPE of the columns
|
|
10
|
+
* it queries, plus the indexes those queries depend on.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const REPORT_MODERATION = {
|
|
14
|
+
* reportedTypes: ['listing', 'review'],
|
|
15
|
+
* categories: ['spam', 'harassment'],
|
|
16
|
+
* } as const;
|
|
17
|
+
*
|
|
18
|
+
* export const reports = pgTable(
|
|
19
|
+
* 'reports',
|
|
20
|
+
* {
|
|
21
|
+
* ...moderationReportColumns(REPORT_MODERATION),
|
|
22
|
+
* // …the application's own columns
|
|
23
|
+
* },
|
|
24
|
+
* moderationReportTableExtras(REPORT_MODERATION),
|
|
25
|
+
* );
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* **`moderationReportTableExtras` takes the OPTIONS and returns the callback**,
|
|
29
|
+
* rather than taking the built columns directly. It has to: the CHECK on
|
|
30
|
+
* `reported_type` and the containment CHECK on `categories` are built from the
|
|
31
|
+
* same tuples that constrain the columns, and drizzle does not carry those
|
|
32
|
+
* tuples onto the built column (`enumValues` is `undefined` there — measured).
|
|
33
|
+
* Passing one options object to both calls is what keeps the column and its
|
|
34
|
+
* constraint from drifting apart.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* The local statuses, as the CHECK renders them.
|
|
38
|
+
*
|
|
39
|
+
* Spelled out beside the DDL so it cannot acquire an ORM dependency. `satisfies`
|
|
40
|
+
* refuses a value outside the union; `postgresSchema.test.ts` asserts this tuple
|
|
41
|
+
* and `MODERATION_LOCAL_STATUSES` still agree, which is what catches an OMISSION
|
|
42
|
+
* — the direction a type cannot see.
|
|
43
|
+
*/
|
|
44
|
+
const LOCAL_STATUSES = [
|
|
45
|
+
'received',
|
|
46
|
+
'queued',
|
|
47
|
+
'submitted',
|
|
48
|
+
'delivery_failed',
|
|
49
|
+
'closed',
|
|
50
|
+
];
|
|
51
|
+
/** Mongo's `detailsMaxLength ?? 2_000`, for the same reason: it bounds free text. */
|
|
52
|
+
const DEFAULT_DETAILS_MAX_LENGTH = 2000;
|
|
53
|
+
/**
|
|
54
|
+
* The columns this package reads and writes, to spread into the application's
|
|
55
|
+
* own `pgTable`.
|
|
56
|
+
*
|
|
57
|
+
* Every SQL name is written out. Drizzle would derive one from the property, and
|
|
58
|
+
* its derivation mangles digit- and capital-adjacent names — `crowdSourceReportId`
|
|
59
|
+
* becomes `crowd_source_report_id`, which is a working column with a name nobody
|
|
60
|
+
* chose and which no gate would notice.
|
|
61
|
+
*/
|
|
62
|
+
export function moderationReportColumns(options = {}) {
|
|
63
|
+
return {
|
|
64
|
+
/**
|
|
65
|
+
* `text`, not `uuid`, and generated in the application.
|
|
66
|
+
*
|
|
67
|
+
* `generatedId()` holds a uuid v7 for a row created here and a 24-character
|
|
68
|
+
* ObjectId hex for every row that existed before a Mongo cutover, so one id
|
|
69
|
+
* space serves both. The consequence that matters downstream: a malformed id
|
|
70
|
+
* matches no rows instead of raising `22P02`, which is exactly the Mongo
|
|
71
|
+
* behaviour the delivery path already handles.
|
|
72
|
+
*/
|
|
73
|
+
id: generatedId(),
|
|
74
|
+
reportedType: text('reported_type').notNull(),
|
|
75
|
+
reportedId: text('reported_id').notNull(),
|
|
76
|
+
/**
|
|
77
|
+
* The reporting Oxy user id.
|
|
78
|
+
*
|
|
79
|
+
* The Oxy subject IS the identity binding proof, so there is no separate
|
|
80
|
+
* binding step for an application to implement — but it does mean this
|
|
81
|
+
* column must hold an Oxy user id and not an application-local one.
|
|
82
|
+
*/
|
|
83
|
+
reporter: text('reporter').notNull(),
|
|
84
|
+
/**
|
|
85
|
+
* `text[]`, written and read WHOLE.
|
|
86
|
+
*
|
|
87
|
+
* This package sets it at intake and reads it at delivery; it never queries
|
|
88
|
+
* by element, so an array column is the port rather than a join table. A
|
|
89
|
+
* containment CHECK constrains the values when the application declares them.
|
|
90
|
+
*/
|
|
91
|
+
categories: text('categories').array().notNull(),
|
|
92
|
+
details: varchar('details', {
|
|
93
|
+
length: options.detailsMaxLength ?? DEFAULT_DETAILS_MAX_LENGTH,
|
|
94
|
+
}),
|
|
95
|
+
localStatus: text('local_status').notNull().default('received'),
|
|
96
|
+
/**
|
|
97
|
+
* Why a report is not going anywhere, in words an operator can read.
|
|
98
|
+
*
|
|
99
|
+
* Stored rather than inferred from a missing outbox row. A missing row is
|
|
100
|
+
* also what a lost write looks like, and the two need to be distinguishable
|
|
101
|
+
* months later without re-deriving which types had providers at the time.
|
|
102
|
+
*/
|
|
103
|
+
localStatusReason: varchar('local_status_reason', { length: 300 }),
|
|
104
|
+
crowdSourceReportId: text('crowdsource_report_id'),
|
|
105
|
+
crowdSourceCaseId: text('crowdsource_case_id'),
|
|
106
|
+
crowdSourceMerged: boolean('crowdsource_merged'),
|
|
107
|
+
/** SHA-256 of the exact representation that was reviewed. */
|
|
108
|
+
contentSnapshotHash: text('content_snapshot_hash'),
|
|
109
|
+
submittedAt: timestamptz(),
|
|
110
|
+
lastDeliveryError: varchar('last_delivery_error', { length: 2000 }),
|
|
111
|
+
decisionId: text('decision_id'),
|
|
112
|
+
/**
|
|
113
|
+
* The revision guard.
|
|
114
|
+
*
|
|
115
|
+
* Compared in the update's WHERE clause, so it is the database that refuses a
|
|
116
|
+
* stale write rather than a read-then-write in the application process.
|
|
117
|
+
*/
|
|
118
|
+
decisionRevision: integer('decision_revision'),
|
|
119
|
+
decisionOutcome: text('decision_outcome'),
|
|
120
|
+
decisionStatus: text('decision_status'),
|
|
121
|
+
decidedAt: timestamptz(),
|
|
122
|
+
enforcedAction: text('enforced_action'),
|
|
123
|
+
enforcedAt: timestamptz(),
|
|
124
|
+
createdAt: createdAt(),
|
|
125
|
+
updatedAt: updatedAt(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Postgres truncates an identifier at 63 bytes, silently, and then names collide. */
|
|
129
|
+
const MAX_IDENTIFIER_BYTES = 63;
|
|
130
|
+
function identifier(parts) {
|
|
131
|
+
const name = parts.join('_');
|
|
132
|
+
if (Buffer.byteLength(name, 'utf8') > MAX_IDENTIFIER_BYTES) {
|
|
133
|
+
throw new Error(`The moderation report index name '${name}' is ${Buffer.byteLength(name, 'utf8')} bytes; ` +
|
|
134
|
+
`Postgres truncates identifiers at ${MAX_IDENTIFIER_BYTES} and two names that differ ` +
|
|
135
|
+
'only past the cut then collide. Give the report table a shorter name.');
|
|
136
|
+
}
|
|
137
|
+
return name;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The CHECKs and indexes the report table needs, as `pgTable`'s third argument.
|
|
141
|
+
*
|
|
142
|
+
* The three indexes are the same three `applyModerationReportIndexes` creates,
|
|
143
|
+
* and not optional: reconciliation scans `(local_status, created_at)` oldest-first
|
|
144
|
+
* on every sweep, and the decision worker looks a case up by
|
|
145
|
+
* `crowdsource_case_id` on every inbound decision. Without them both become
|
|
146
|
+
* sequential scans that grow with the table.
|
|
147
|
+
*
|
|
148
|
+
* The compound uniqueness of "one report per reporter per object" is the
|
|
149
|
+
* APPLICATION's to declare — some applications allow a reporter to file twice
|
|
150
|
+
* under different categories — so it is not created here. Intake's duplicate
|
|
151
|
+
* check reads `(reporter, reported_id, reported_type)`, so that one is indexed
|
|
152
|
+
* for it either way.
|
|
153
|
+
*
|
|
154
|
+
* Index names are derived from the adopter's own table name and length-checked,
|
|
155
|
+
* because two names that differ only past the 63rd byte are the same name to
|
|
156
|
+
* Postgres.
|
|
157
|
+
*/
|
|
158
|
+
export function moderationReportTableExtras(options = {}) {
|
|
159
|
+
return (columns) => {
|
|
160
|
+
/**
|
|
161
|
+
* The adopter's table name, read off one of its own columns — `pgTable`
|
|
162
|
+
* calls this with the BUILT columns, each of which carries its table. That
|
|
163
|
+
* is what lets the index names belong to the adopter's table without being
|
|
164
|
+
* passed in a second time and drifting.
|
|
165
|
+
*/
|
|
166
|
+
const prefix = getTableName(columns.localStatus.table);
|
|
167
|
+
return [
|
|
168
|
+
check(identifier([prefix, 'local_status_check']), sql `${columns.localStatus} in (${sql.raw(inList(LOCAL_STATUSES))})`),
|
|
169
|
+
...(options.reportedTypes === undefined
|
|
170
|
+
? []
|
|
171
|
+
: [
|
|
172
|
+
check(identifier([prefix, 'reported_type_check']), sql `${columns.reportedType} in (${sql.raw(inList(options.reportedTypes))})`),
|
|
173
|
+
]),
|
|
174
|
+
...(options.categories === undefined
|
|
175
|
+
? []
|
|
176
|
+
: [
|
|
177
|
+
/**
|
|
178
|
+
* Containment, not equality: a report carries a SUBSET of the
|
|
179
|
+
* declared categories, and `<@` is trivially satisfied by an empty
|
|
180
|
+
* array — which the application's own validation refuses before this
|
|
181
|
+
* is ever reached.
|
|
182
|
+
*/
|
|
183
|
+
check(identifier([prefix, 'categories_check']), sql `${columns.categories} <@ ${sql.raw(textArrayLiteral(options.categories))}`),
|
|
184
|
+
]),
|
|
185
|
+
index(identifier([prefix, 'local_status_created_at_idx'])).on(columns.localStatus, columns.createdAt),
|
|
186
|
+
index(identifier([prefix, 'crowdsource_case_id_idx'])).on(columns.crowdSourceCaseId),
|
|
187
|
+
index(identifier([prefix, 'reporter_object_idx'])).on(columns.reporter, columns.reportedId, columns.reportedType),
|
|
188
|
+
];
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
//# 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,158 @@
|
|
|
1
|
+
import { and, eq, inArray, sql } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* The enforcement ledger, in Postgres.
|
|
4
|
+
*
|
|
5
|
+
* Two operations carry everything: the CLAIM that makes an action happen at most
|
|
6
|
+
* once, and the reversal LOOKUP that decides what a correction puts back. Two of
|
|
7
|
+
* this package's eleven proven mutations attack the second one, which makes its
|
|
8
|
+
* predicate the most load-bearing SQL in the Postgres half.
|
|
9
|
+
*
|
|
10
|
+
* ## The idempotency key IS the primary key
|
|
11
|
+
*
|
|
12
|
+
* `decision_id + decision_revision + action`. Mongo needed a surrogate `_id` plus
|
|
13
|
+
* a unique index on that triple; here the triple is the key, so
|
|
14
|
+
* `onConflictDoNothing()` needs no explicit `target` — there is only one
|
|
15
|
+
* constraint it could mean — and there is no second object to keep in step.
|
|
16
|
+
*
|
|
17
|
+
* `decision_revision` being IN the key is what lets a correction act: a new
|
|
18
|
+
* revision is a different row, so the restore it asks for is allowed to happen
|
|
19
|
+
* while still being impossible to apply twice itself.
|
|
20
|
+
*
|
|
21
|
+
* ## Addressed by the key, never by a record id
|
|
22
|
+
*
|
|
23
|
+
* Every write after the claim addresses the row by the same three values the
|
|
24
|
+
* claim used. PostgreSQL reaches it through the composite primary key, so no
|
|
25
|
+
* opaque record id crosses the port.
|
|
26
|
+
*/
|
|
27
|
+
export function postgresEnforcementStore(input) {
|
|
28
|
+
const { db } = input;
|
|
29
|
+
const enforcements = input.tables.enforcements;
|
|
30
|
+
/** The primary key, as a predicate. Addresses at most one row, by construction. */
|
|
31
|
+
const keyFilter = (key) => and(eq(enforcements.decisionId, key.decisionId), eq(enforcements.decisionRevision, key.decisionRevision), eq(enforcements.action, key.action));
|
|
32
|
+
return {
|
|
33
|
+
async claim(row) {
|
|
34
|
+
/**
|
|
35
|
+
* The insert IS the check, and a lost race is zero rows rather than an
|
|
36
|
+
* error — the same shape as the event store's claim, and for the same
|
|
37
|
+
* reason: there is no catch block whose predicate could be widened into
|
|
38
|
+
* swallowing a real fault as "another delivery already handled it".
|
|
39
|
+
*
|
|
40
|
+
* `created_at` and `updated_at` are written from the caller's `now`. The
|
|
41
|
+
* Mongo store lets Mongoose own them; here nothing does, and the reversal
|
|
42
|
+
* lookup ORDERS BY `created_at`, so the clock that decides which row is
|
|
43
|
+
* "most recent" is the caller's rather than two different defaults'.
|
|
44
|
+
*/
|
|
45
|
+
const rows = await db
|
|
46
|
+
.insert(enforcements)
|
|
47
|
+
.values({
|
|
48
|
+
decisionId: row.decisionId,
|
|
49
|
+
decisionRevision: row.decisionRevision,
|
|
50
|
+
action: row.action,
|
|
51
|
+
caseId: row.caseId,
|
|
52
|
+
subjectType: row.subjectType,
|
|
53
|
+
subjectId: row.subjectId,
|
|
54
|
+
outcome: row.outcome,
|
|
55
|
+
...(row.recommendedAction === undefined
|
|
56
|
+
? {}
|
|
57
|
+
: { recommendedAction: row.recommendedAction }),
|
|
58
|
+
reason: row.reason,
|
|
59
|
+
mode: row.mode,
|
|
60
|
+
applied: false,
|
|
61
|
+
createdAt: row.now,
|
|
62
|
+
updatedAt: row.now,
|
|
63
|
+
})
|
|
64
|
+
.onConflictDoNothing()
|
|
65
|
+
.returning({ decisionId: enforcements.decisionId });
|
|
66
|
+
return rows.length === 1;
|
|
67
|
+
},
|
|
68
|
+
async markSkipped(key, { skippedReason, recordedAs, now }) {
|
|
69
|
+
await db
|
|
70
|
+
.update(enforcements)
|
|
71
|
+
.set({
|
|
72
|
+
skippedReason,
|
|
73
|
+
// Absent means leave it: `recorded_as` only carries a value when the
|
|
74
|
+
// effect said the planned action amounted to something else.
|
|
75
|
+
...(recordedAs === undefined ? {} : { recordedAs }),
|
|
76
|
+
updatedAt: now,
|
|
77
|
+
})
|
|
78
|
+
.where(keyFilter(key));
|
|
79
|
+
},
|
|
80
|
+
async markApplied(key, { appliedAt, previousState, now }) {
|
|
81
|
+
await db
|
|
82
|
+
.update(enforcements)
|
|
83
|
+
.set({
|
|
84
|
+
applied: true,
|
|
85
|
+
appliedAt,
|
|
86
|
+
/**
|
|
87
|
+
* Written as `jsonb`, so it round-trips as the object it was rather
|
|
88
|
+
* than as its `JSON.stringify` — a `text` column would hand a reversal
|
|
89
|
+
* a string, and `previousState?.status` on a string is `undefined`,
|
|
90
|
+
* which reads as "moderation displaced nothing" and restores a guess.
|
|
91
|
+
*
|
|
92
|
+
* Absent means leave it NULL: an action that changed state records what
|
|
93
|
+
* it displaced, and one that did not must not claim it displaced
|
|
94
|
+
* nothing-in-particular.
|
|
95
|
+
*/
|
|
96
|
+
...(previousState === undefined ? {} : { previousState }),
|
|
97
|
+
updatedAt: now,
|
|
98
|
+
})
|
|
99
|
+
.where(keyFilter(key));
|
|
100
|
+
},
|
|
101
|
+
async releaseClaim(key) {
|
|
102
|
+
await db.delete(enforcements).where(keyFilter(key));
|
|
103
|
+
},
|
|
104
|
+
async latestApplied({ subjectType, subjectId, actions }) {
|
|
105
|
+
/**
|
|
106
|
+
* The reversal lookup, and every clause in it is load-bearing.
|
|
107
|
+
*
|
|
108
|
+
* `eq(applied, true)` — a row that was RECORDED and never carried out
|
|
109
|
+
* describes a state change that never happened, and it carries no
|
|
110
|
+
* `previous_state` at all. Reading it hands a reversal nothing and the
|
|
111
|
+
* application's fallback publishes a draft moderation only ever hid.
|
|
112
|
+
*
|
|
113
|
+
* `inArray(action, actions)` — one action may reverse several. The most
|
|
114
|
+
* recent APPLIED row across the whole declared set wins, so `apply`
|
|
115
|
+
* receives whatever actually happened last rather than whichever action
|
|
116
|
+
* happens to be first in the array. Note the membership is POSITIVE: a
|
|
117
|
+
* negated set (`not in (…)`) would render as `<> ALL (…)`, which no btree
|
|
118
|
+
* can serve, and the supporting index would quietly stop being used.
|
|
119
|
+
*
|
|
120
|
+
* `created_at desc NULLS LAST` with `limit(1)` — newest first, and the
|
|
121
|
+
* NULLS placement is written out because drizzle's two spellings of "desc"
|
|
122
|
+
* DISAGREE and the difference is a blocking sort.
|
|
123
|
+
*
|
|
124
|
+
* Measured on Postgres 17. `.desc()` inside a drizzle INDEX emits
|
|
125
|
+
* `DESC NULLS LAST`; `desc(column)` in an ORDER BY emits plain `DESC`,
|
|
126
|
+
* which in Postgres means NULLS FIRST. Those two orderings do not match, so
|
|
127
|
+
* no index can satisfy the sort and the plan gains a `Sort` node — even
|
|
128
|
+
* though `created_at` is NOT NULL and the two orderings can differ by
|
|
129
|
+
* nothing at all. With `nulls last` the plan is a plain index scan under
|
|
130
|
+
* the Limit:
|
|
131
|
+
*
|
|
132
|
+
* desc -> Limit → Sort (created_at DESC) → Index Scan …
|
|
133
|
+
* desc nulls last -> Limit → Index Scan …
|
|
134
|
+
*
|
|
135
|
+
* Correct results either way, and a sort that grows with the number of
|
|
136
|
+
* enforcement rows for one subject. `postgresEnforcementStore.test.ts`
|
|
137
|
+
* asserts the absence of that `Sort` against the real planner.
|
|
138
|
+
*/
|
|
139
|
+
const rows = await db
|
|
140
|
+
.select({
|
|
141
|
+
action: enforcements.action,
|
|
142
|
+
previousState: enforcements.previousState,
|
|
143
|
+
})
|
|
144
|
+
.from(enforcements)
|
|
145
|
+
.where(and(eq(enforcements.subjectType, subjectType), eq(enforcements.subjectId, subjectId), inArray(enforcements.action, [...actions]), eq(enforcements.applied, true)))
|
|
146
|
+
.orderBy(sql `${enforcements.createdAt} desc nulls last`)
|
|
147
|
+
.limit(1);
|
|
148
|
+
const [row] = rows;
|
|
149
|
+
if (row === undefined)
|
|
150
|
+
return null;
|
|
151
|
+
return {
|
|
152
|
+
action: row.action,
|
|
153
|
+
...(row.previousState === null ? {} : { previousState: row.previousState }),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//# 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,101 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
/**
|
|
3
|
+
* The inbound webhook event log, in Postgres.
|
|
4
|
+
*
|
|
5
|
+
* ## The one place this backend is structurally better rather than equivalent
|
|
6
|
+
*
|
|
7
|
+
* Everything else in this port aims for parity. This claim does not: it removes a
|
|
8
|
+
* failure mode instead of handling it, and the difference is worth being precise
|
|
9
|
+
* about, because "better" is otherwise just an adjective.
|
|
10
|
+
*
|
|
11
|
+
* **Mongo's claim throws and catches.** It inserts, and reads `code === 11000` to
|
|
12
|
+
* tell "somebody else has this event" from a real fault; everything that is not
|
|
13
|
+
* 11000 is rethrown, so a lost connection or a failover answers non-2xx and the
|
|
14
|
+
* event stays on the sender's retry schedule. That property is correct — and it
|
|
15
|
+
* lives in a PREDICATE. `catch { return false }` is one keystroke away, it
|
|
16
|
+
* type-checks, and it turns a connection failure into "already processed": the
|
|
17
|
+
* receiver answers 200 and a decision is retired that nobody ever handled. The
|
|
18
|
+
* only thing standing between those two behaviours is a conditional somebody
|
|
19
|
+
* could widen, and a test can only catch that by injecting a driver failure.
|
|
20
|
+
*
|
|
21
|
+
* **Postgres's claim does not throw at all.** `ON CONFLICT DO NOTHING` plus
|
|
22
|
+
* `RETURNING` makes a duplicate a ROW COUNT rather than an error: one row means
|
|
23
|
+
* this call took the claim, zero means somebody else holds it. So there is no
|
|
24
|
+
* catch block here — and therefore
|
|
25
|
+
*
|
|
26
|
+
* - no predicate to widen,
|
|
27
|
+
* - no code path that can convert a fault into a negative answer,
|
|
28
|
+
* - and the "rethrow everything else" guarantee holds by the ABSENCE of code
|
|
29
|
+
* rather than by the presence of correct code.
|
|
30
|
+
*
|
|
31
|
+
* That is the mechanism: the Mongo version is a property of code that exists and
|
|
32
|
+
* can be edited wrongly; this one is a property of code that does not exist. Only
|
|
33
|
+
* the second cannot be broken by a well-meaning change.
|
|
34
|
+
*
|
|
35
|
+
* The consequence for the mutation suite, stated because its absence would
|
|
36
|
+
* otherwise look like a gap: there is nothing to DELETE on the insert side, so no
|
|
37
|
+
* mutation can attack it. The one that exists attacks the READ — `rows.length === 1`
|
|
38
|
+
* collapsing to `true`, which would hand the same event to two handlers.
|
|
39
|
+
*/
|
|
40
|
+
export function postgresEventStore(input) {
|
|
41
|
+
const { db } = input;
|
|
42
|
+
const events = input.tables.events;
|
|
43
|
+
return {
|
|
44
|
+
async claim({ eventId, receivedAt, expiresAt }) {
|
|
45
|
+
/**
|
|
46
|
+
* The insert IS the claim, and a lost race is zero rows rather than an
|
|
47
|
+
* exception. `created_at`/`updated_at` are left to their column defaults:
|
|
48
|
+
* unlike the outbox, nothing here needs a repeated write to be provably a
|
|
49
|
+
* no-op, so the database's clock is the simpler authority.
|
|
50
|
+
*/
|
|
51
|
+
const rows = await db
|
|
52
|
+
.insert(events)
|
|
53
|
+
.values({ id: eventId, state: 'claimed', receivedAt, expiresAt })
|
|
54
|
+
.onConflictDoNothing({ target: events.id })
|
|
55
|
+
.returning({ id: events.id });
|
|
56
|
+
return rows.length === 1;
|
|
57
|
+
},
|
|
58
|
+
/** Give the claim back so a redelivery can be processed. */
|
|
59
|
+
async release(eventId) {
|
|
60
|
+
await db.delete(events).where(eq(events.id, eventId));
|
|
61
|
+
},
|
|
62
|
+
async markQueued({ eventId, type, caseId, payload, now }, tx) {
|
|
63
|
+
/**
|
|
64
|
+
* On the CALLER's transaction, which is the whole point: this row's
|
|
65
|
+
* completion and the outbox row that carries the work commit together, or a
|
|
66
|
+
* crash between them leaves an event permanently deduplicated with no work
|
|
67
|
+
* queued — a decision silently lost, with a row saying it arrived.
|
|
68
|
+
*/
|
|
69
|
+
await tx
|
|
70
|
+
.update(events)
|
|
71
|
+
.set({
|
|
72
|
+
type,
|
|
73
|
+
caseId,
|
|
74
|
+
payload,
|
|
75
|
+
state: 'queued',
|
|
76
|
+
queuedAt: now,
|
|
77
|
+
updatedAt: now,
|
|
78
|
+
})
|
|
79
|
+
.where(eq(events.id, eventId));
|
|
80
|
+
},
|
|
81
|
+
async markIgnored({ eventId, type, caseId, now }) {
|
|
82
|
+
await db
|
|
83
|
+
.update(events)
|
|
84
|
+
.set({
|
|
85
|
+
type,
|
|
86
|
+
/**
|
|
87
|
+
* Absent means LEAVE IT, not write something. An event type carrying no
|
|
88
|
+
* case id is ordinary — `case.created` before a case is linked — and
|
|
89
|
+
* `caseId: String(caseId)` would store the four characters `null` or
|
|
90
|
+
* the nine characters `undefined`, which then reads as a case id
|
|
91
|
+
* everywhere downstream. Postgres keeps the column NULL.
|
|
92
|
+
*/
|
|
93
|
+
...(caseId === undefined ? {} : { caseId }),
|
|
94
|
+
state: 'ignored',
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
})
|
|
97
|
+
.where(eq(events.id, eventId));
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../../../../../src/outbox/postgres/store/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAKjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAGlC;IACC,MAAM,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC;IACrB,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;IAEnC,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE;YAC5C;;;;;eAKG;YACH,MAAM,IAAI,GAAG,MAAM,EAAE;iBAClB,MAAM,CAAC,MAAM,CAAC;iBACd,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;iBAChE,mBAAmB,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;iBAC1C,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;YAEhC,OAAO,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC;QAC3B,CAAC;QAED,4DAA4D;QAC5D,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QACxD,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE;YAC1D;;;;;eAKG;YACH,MAAM,EAAE;iBACL,MAAM,CAAC,MAAM,CAAC;iBACd,GAAG,CAAC;gBACH,IAAI;gBACJ,MAAM;gBACN,OAAO;gBACP,KAAK,EAAE,QAAQ;gBACf,QAAQ,EAAE,GAAG;gBACb,SAAS,EAAE,GAAG;aACf,CAAC;iBACD,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QACnC,CAAC;QAED,KAAK,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;YAC9C,MAAM,EAAE;iBACL,MAAM,CAAC,MAAM,CAAC;iBACd,GAAG,CAAC;gBACH,IAAI;gBACJ;;;;;;mBAMG;gBACH,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;gBAC3C,KAAK,EAAE,SAAS;gBAChB,SAAS,EAAE,GAAG;aACf,CAAC;iBACD,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;QACnC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { postgresEnforcementStore } from './enforcement.js';
|
|
3
|
+
import { postgresEventStore } from './events.js';
|
|
4
|
+
import { postgresOutboxStore } from './outbox.js';
|
|
5
|
+
import { postgresReportStore } from './reports.js';
|
|
6
|
+
import { postgresTransactionRunner } from './transaction.js';
|
|
7
|
+
/**
|
|
8
|
+
* Everything this package writes, in Postgres.
|
|
9
|
+
*
|
|
10
|
+
* One factory rather than five, because the five members must share one handle: a
|
|
11
|
+
* report and its outbox row commit in the SAME transaction, and a store assembled
|
|
12
|
+
* from two handles would type-check perfectly and quietly lose that.
|
|
13
|
+
*
|
|
14
|
+
* Unlike the Mongo factory, this one CREATES NOTHING. There is no schema to
|
|
15
|
+
* register: the adopter's own migration created the tables, generated from the
|
|
16
|
+
* definitions this package exports, in the adopter's own journal. That asymmetry
|
|
17
|
+
* is deliberate and is the reason no migrations folder ships here — two journals
|
|
18
|
+
* against one `drizzle.__drizzle_migrations` table interleave, and the loser is
|
|
19
|
+
* skipped in silence with exit 0.
|
|
20
|
+
*/
|
|
21
|
+
export function postgresModerationStore(input) {
|
|
22
|
+
const { db, reportTable, tables } = input;
|
|
23
|
+
return {
|
|
24
|
+
transaction: postgresTransactionRunner(db),
|
|
25
|
+
outbox: postgresOutboxStore({ db, tables }),
|
|
26
|
+
events: postgresEventStore({ db, tables }),
|
|
27
|
+
enforcement: postgresEnforcementStore({ db, tables }),
|
|
28
|
+
reports: postgresReportStore({ db, reportTable }),
|
|
29
|
+
/**
|
|
30
|
+
* ASSERTS the schema, rather than creating it.
|
|
31
|
+
*
|
|
32
|
+
* The Mongo half calls `init()` and builds indexes; there is nothing here to
|
|
33
|
+
* build, so what remains is worth doing for its own sake: confirming the four
|
|
34
|
+
* tables this package queries exist BEFORE the first report is filed rather
|
|
35
|
+
* than at the first delivery.
|
|
36
|
+
*
|
|
37
|
+
* A missing table raises `42P01 undefined_table` and names it. `limit(0)`
|
|
38
|
+
* means Postgres parses and plans each statement — which is what proves the
|
|
39
|
+
* relation resolves — without reading a row.
|
|
40
|
+
*
|
|
41
|
+
* It cannot check COLUMNS. An adopter whose migration is a version behind
|
|
42
|
+
* gets a compile error from `ModerationReportTable` for a missing column and
|
|
43
|
+
* a `42703` from the query for a stale migration; both are loud, and neither
|
|
44
|
+
* is this function's job.
|
|
45
|
+
*/
|
|
46
|
+
async ensureSchema() {
|
|
47
|
+
const present = [
|
|
48
|
+
tables.outbox,
|
|
49
|
+
tables.events,
|
|
50
|
+
tables.enforcements,
|
|
51
|
+
reportTable,
|
|
52
|
+
];
|
|
53
|
+
for (const table of present) {
|
|
54
|
+
await db.select({ resolves: sql `1` }).from(table).limit(0);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/outbox/postgres/store/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAMlC,OAAO,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,yBAAyB,EAA2B,MAAM,kBAAkB,CAAC;AAEtF;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAyC,KAM/E;IACC,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAE1C,OAAO;QACL,WAAW,EAAE,yBAAyB,CAAC,EAAE,CAAC;QAC1C,MAAM,EAAE,mBAAmB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QAC3C,MAAM,EAAE,kBAAkB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QAC1C,WAAW,EAAE,wBAAwB,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QACrD,OAAO,EAAE,mBAAmB,CAAU,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC;QAE1D;;;;;;;;;;;;;;;;WAgBG;QACH,KAAK,CAAC,YAAY;YAChB,MAAM,OAAO,GAAuB;gBAClC,MAAM,CAAC,MAAM;gBACb,MAAM,CAAC,MAAM;gBACb,MAAM,CAAC,YAAY;gBACnB,WAAW;aACZ,CAAC;YACF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAA,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|