@remit/drizzle-service 0.0.34 → 0.0.36
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/package.json +3 -10
- package/src/db.ts +6 -6
- package/src/error.ts +2 -5
- package/src/index.ts +0 -1
- package/src/repair/thread-message-category-contract.ts +2 -4
- package/src/repair/thread-message-category.sqlite.test.ts +1 -2
- package/src/repair/thread-message-category.ts +15 -25
- package/src/repos/cascade-delete.ts +8 -22
- package/src/repos/category-fixture.ts +2 -19
- package/src/repos/filter-anchor.ts +4 -4
- package/src/repos/filter.ts +3 -3
- package/src/repos/i4-account-config.ts +2 -2
- package/src/repos/i4-account-export-request.ts +2 -2
- package/src/repos/i4-account-setting.ts +2 -2
- package/src/repos/i4-account.ts +2 -2
- package/src/repos/i4-mailbox-lock.ts +2 -2
- package/src/repos/i4-mailbox-special-use.ts +2 -2
- package/src/repos/i4-mailbox.ts +2 -2
- package/src/repos/i4-message-flag-push.test.ts +1 -1
- package/src/repos/i4-message-flag-push.ts +3 -3
- package/src/repos/i4-message-placement-move.test.ts +1 -1
- package/src/repos/i4-message-placement-move.ts +4 -3
- package/src/repos/i4-organize-job-request.ts +2 -2
- package/src/repos/i4-outbox-message.ts +2 -2
- package/src/repos/label.ts +2 -2
- package/src/repos/message-flag.ts +2 -2
- package/src/repos/message-label.ts +2 -2
- package/src/repos/message.ts +8 -10
- package/src/repos/quarantine.ts +2 -2
- package/src/repos/test-helpers.ts +11 -213
- package/src/repos/thread-message.test.ts +35 -93
- package/src/repos/thread-message.ts +7 -19
- package/src/repos/thread-search-predicates.ts +13 -41
- package/src/repos/unit-of-work.ts +1 -1
- package/src/schema/i4-account-config.ts +1 -1
- package/src/schema/i4-account-export-request.ts +1 -1
- package/src/schema/i4-account-setting.ts +1 -1
- package/src/schema/i4-address.ts +1 -1
- package/src/schema/i4-mailbox-lock.ts +1 -1
- package/src/schema/i4-mailbox.ts +1 -1
- package/src/schema/i4-message-flag-push.ts +1 -1
- package/src/schema/i4-message-placement-move.ts +1 -1
- package/src/schema/i4-organize-job-request.ts +1 -1
- package/src/schema/i4-outbox-message.ts +1 -1
- package/src/schema/message-data.ts +1 -1
- package/src/schema/outbox.ts +13 -56
- package/src/schema/quarantine.ts +1 -1
- package/src/schema/thread-message.ts +1 -1
- package/src/schema-full-sqlite.ts +9 -7
- package/src/schema.ts +4 -4
- package/src/sqlite-client.ts +4 -5
- package/src/test-db-sqlite.ts +4 -9
- package/src/test-db.ts +11 -66
- package/src/tx.ts +6 -12
- package/src/vps-migrations-drift.sqlite.test.ts +3 -3
- package/drizzle.config.ts +0 -15
- package/src/dialect.ts +0 -13
- package/src/repair/thread-message-category.test.ts +0 -229
- package/src/repos/thread-message-category.test.ts +0 -156
- package/src/schema/active-entities.ts +0 -19
- package/src/schema-full.ts +0 -16
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/drizzle-service",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Drizzle ORM service
|
|
3
|
+
"version": "0.0.36",
|
|
4
|
+
"description": "Drizzle ORM service over SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -13,17 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"test:typecheck": "tsgo --noEmit -p tsconfig.json",
|
|
16
|
-
"test:run
|
|
17
|
-
"test:run:sqlite": "node --env-file=../../localhost-test-unit-sqlite.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=75 --test 'src/**/*.sqlite.test.ts'",
|
|
18
|
-
"test:run": "npm run test:run:pg && npm run test:run:sqlite",
|
|
16
|
+
"test:run": "node --env-file=../../localhost-test-unit-sqlite.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=90 --test 'src/**/*.test.ts'",
|
|
19
17
|
"test": "npm run test:typecheck && npm run test:run",
|
|
20
18
|
"fix": "biome check --write src"
|
|
21
19
|
},
|
|
22
20
|
"devDependencies": {
|
|
23
21
|
"@remit/api-zod-schemas": "*",
|
|
24
|
-
"@types/pg": "^8.0.0",
|
|
25
22
|
"drizzle-kit": "^0.31.1",
|
|
26
|
-
"embedded-postgres": "^18.4.0-beta.17",
|
|
27
23
|
"tsx": "*",
|
|
28
24
|
"zod": "*"
|
|
29
25
|
},
|
|
@@ -31,11 +27,8 @@
|
|
|
31
27
|
"@remit/data-ports": "*",
|
|
32
28
|
"@remit/domain-enums": "*",
|
|
33
29
|
"drizzle-orm": "^0.45.2",
|
|
34
|
-
"pg": "^8.0.0",
|
|
35
|
-
"postgres": "^3.4.5",
|
|
36
30
|
"short-uuid": "*",
|
|
37
31
|
"uuid": "*",
|
|
38
|
-
"@remit/drizzle-pg-schema": "*",
|
|
39
32
|
"@remit/drizzle-sqlite-schema": "*",
|
|
40
33
|
"better-sqlite3": "^12.11.1",
|
|
41
34
|
"@types/better-sqlite3": "^7.6.13"
|
package/src/db.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { PgDatabase } from "drizzle-orm/pg-core";
|
|
1
|
+
import type { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* A drizzle handle a repository can run on: the top-level database or a
|
|
6
5
|
* transaction (or savepoint) bound to one. Both share the query-builder API, so
|
|
7
6
|
* a repo constructed with either behaves the same — standalone, or enlisted in a
|
|
8
7
|
* unit-of-work transaction.
|
|
8
|
+
*
|
|
9
|
+
* Internal to this adapter: it names better-sqlite3, so it is not exported from
|
|
10
|
+
* the package index and never appears on a shared surface.
|
|
9
11
|
*/
|
|
10
|
-
export type Db<TSchema extends Record<string, unknown>> =
|
|
11
|
-
|
|
12
|
-
TSchema
|
|
13
|
-
>;
|
|
12
|
+
export type Db<TSchema extends Record<string, unknown>> =
|
|
13
|
+
BetterSQLite3Database<TSchema>;
|
package/src/error.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Re-export the shared error contract so an error thrown by a
|
|
1
|
+
// Re-export the shared error contract so an error thrown by a repo here is the
|
|
2
2
|
// same class the backend handlers and workers already catch (they import these
|
|
3
3
|
// from @remit/data-ports/errors). Defining parallel classes here made
|
|
4
4
|
// `instanceof NotFoundError` false across the adapter boundary — a fresh user's
|
|
@@ -14,7 +14,6 @@ export class NotImplementedError extends Error {
|
|
|
14
14
|
statusCode = 501;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const PG_UNIQUE_VIOLATION = "23505";
|
|
18
17
|
// better-sqlite3 raises a `SqliteError` whose `.code` is one of these for a
|
|
19
18
|
// primary-key or unique-index collision (RFC 036 D1).
|
|
20
19
|
const SQLITE_UNIQUE_VIOLATIONS = new Set([
|
|
@@ -23,13 +22,11 @@ const SQLITE_UNIQUE_VIOLATIONS = new Set([
|
|
|
23
22
|
]);
|
|
24
23
|
|
|
25
24
|
// Drizzle wraps the underlying driver error and carries the original (with its
|
|
26
|
-
//
|
|
27
|
-
// the pg and better-sqlite3 drivers.
|
|
25
|
+
// SQLite `code`) on `.cause`, so walk the cause chain.
|
|
28
26
|
export const isUniqueViolation = (error: unknown): boolean => {
|
|
29
27
|
let current: unknown = error;
|
|
30
28
|
while (current) {
|
|
31
29
|
const code = (current as { code?: string }).code;
|
|
32
|
-
if (code === PG_UNIQUE_VIOLATION) return true;
|
|
33
30
|
if (code !== undefined && SQLITE_UNIQUE_VIOLATIONS.has(code)) return true;
|
|
34
31
|
current = (current as { cause?: unknown }).cause;
|
|
35
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The behaviour `thread_message.category`'s repair (#321) must have, asserted
|
|
3
|
-
* against a real engine
|
|
4
|
-
* two dialects and an assertion that only ran on one of them is how a dialect
|
|
5
|
-
* quietly stops being repaired.
|
|
3
|
+
* against a real engine from one place.
|
|
6
4
|
*
|
|
7
5
|
* Every seed is raw SQL against the real column names, so it exercises the same
|
|
8
6
|
* statement text the migrator runs rather than a drizzle re-spelling of it.
|
|
@@ -493,7 +491,7 @@ export const describeRepairContract = (
|
|
|
493
491
|
for (const stale of [
|
|
494
492
|
"2026-07-08",
|
|
495
493
|
"drizzle push",
|
|
496
|
-
"
|
|
494
|
+
"instance whose column was added",
|
|
497
495
|
]) {
|
|
498
496
|
assert.ok(
|
|
499
497
|
!text.includes(stale),
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { describeRepairContract } from "./thread-message-category-contract.js";
|
|
16
16
|
|
|
17
17
|
const sqliteClient = (sqlite: Database.Database): RepairSqlClient => ({
|
|
18
|
-
dialect: "sqlite",
|
|
19
18
|
all: async (sql) => sqlite.prepare(sql).all(),
|
|
20
19
|
run: async (sql) => sqlite.prepare(sql).run().changes,
|
|
21
20
|
});
|
|
@@ -88,7 +87,7 @@ describe("thread_message.category repair — SQLite's write lock", () => {
|
|
|
88
87
|
writer.exec("BEGIN IMMEDIATE");
|
|
89
88
|
try {
|
|
90
89
|
assert.throws(
|
|
91
|
-
() => reader.prepare(repairStatement(
|
|
90
|
+
() => reader.prepare(repairStatement()).run(),
|
|
92
91
|
(error: unknown) =>
|
|
93
92
|
error instanceof Error &&
|
|
94
93
|
"code" in error &&
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* before that read path goes live — otherwise the filter under-returns mail
|
|
10
10
|
* with nothing masking it.
|
|
11
11
|
*
|
|
12
|
-
* One set-based statement
|
|
12
|
+
* One set-based statement. The value it copies is reachable through
|
|
13
13
|
* `message`'s primary key, so the write is single-valued by construction and
|
|
14
14
|
* there is no checkpointing, batching or resumability here: an interrupted run
|
|
15
15
|
* leaves a consistent table and the next start finishes the job.
|
|
@@ -33,14 +33,11 @@
|
|
|
33
33
|
* - It never overwrites a row that was written after the statement began. The
|
|
34
34
|
* migrate one-shot normally runs with every app service stopped, but
|
|
35
35
|
* `docker compose up -d` can restart it while workers still run, so the
|
|
36
|
-
* quiet window is not assumed.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* snapshot — so without the `updated_at` guard it could write a
|
|
42
|
-
* pre-classification value over the fresh one. The guard makes that row fail
|
|
43
|
-
* the re-check and the writer's value stands.
|
|
36
|
+
* quiet window is not assumed. Writers are serialized, so a concurrent
|
|
37
|
+
* body-sync write lands wholly before or wholly after and both orders
|
|
38
|
+
* converge on the same value. The `updated_at` guard is the backstop: a row
|
|
39
|
+
* a concurrent writer touched after the statement began fails the re-check,
|
|
40
|
+
* so the writer's value stands rather than being reverted.
|
|
44
41
|
*
|
|
45
42
|
* The guard covers a writer whose transaction begins after the statement. A
|
|
46
43
|
* writer that began before it and commits during it carries an older stamp and
|
|
@@ -75,15 +72,11 @@
|
|
|
75
72
|
* bumped it would both lie about the row and defeat its own guard.
|
|
76
73
|
*/
|
|
77
74
|
|
|
78
|
-
export type RepairDialect = "sqlite" | "postgres";
|
|
79
|
-
|
|
80
75
|
/**
|
|
81
76
|
* The smallest surface the repair needs, so this module imports nothing and can
|
|
82
|
-
* be driven by the migrator's `better-sqlite3` handle
|
|
83
|
-
* harness of either dialect.
|
|
77
|
+
* be driven by the migrator's `better-sqlite3` handle or a test harness.
|
|
84
78
|
*/
|
|
85
79
|
export interface RepairSqlClient {
|
|
86
|
-
readonly dialect: RepairDialect;
|
|
87
80
|
all(sql: string): Promise<unknown[]>;
|
|
88
81
|
run(sql: string): Promise<number>;
|
|
89
82
|
}
|
|
@@ -95,12 +88,9 @@ export interface RepairSqlClient {
|
|
|
95
88
|
*/
|
|
96
89
|
const PENDING = "uncategorized";
|
|
97
90
|
|
|
98
|
-
const
|
|
99
|
-
dialect === "sqlite"
|
|
100
|
-
? "CAST(unixepoch('subsec') * 1000 AS INTEGER)"
|
|
101
|
-
: "CAST(EXTRACT(EPOCH FROM now()) * 1000 AS bigint)";
|
|
91
|
+
const NOW_MILLIS = "CAST(unixepoch('subsec') * 1000 AS INTEGER)";
|
|
102
92
|
|
|
103
|
-
export const repairStatement = (
|
|
93
|
+
export const repairStatement = (): string =>
|
|
104
94
|
`UPDATE thread_message
|
|
105
95
|
SET category = (
|
|
106
96
|
SELECT m.category FROM message m WHERE m.message_id = thread_message.message_id
|
|
@@ -111,7 +101,7 @@ WHERE EXISTS (
|
|
|
111
101
|
AND m.category <> thread_message.category
|
|
112
102
|
AND m.category <> '${PENDING}'
|
|
113
103
|
)
|
|
114
|
-
AND thread_message.updated_at <= ${
|
|
104
|
+
AND thread_message.updated_at <= ${NOW_MILLIS}`;
|
|
115
105
|
|
|
116
106
|
const BUCKETS_SQL = `SELECT t.category AS row_category, m.category AS message_category, count(*) AS row_count
|
|
117
107
|
FROM thread_message t
|
|
@@ -145,9 +135,9 @@ FROM (
|
|
|
145
135
|
* every run until the stamp is corrected, which is a different thing to tell an
|
|
146
136
|
* operator and would otherwise be indistinguishable in the output.
|
|
147
137
|
*/
|
|
148
|
-
const residualSql = (
|
|
138
|
+
const residualSql = (): string =>
|
|
149
139
|
`SELECT count(*) AS row_count,
|
|
150
|
-
coalesce(sum(CASE WHEN t.updated_at > ${
|
|
140
|
+
coalesce(sum(CASE WHEN t.updated_at > ${NOW_MILLIS} THEN 1 ELSE 0 END), 0) AS ahead_of_clock
|
|
151
141
|
FROM thread_message t
|
|
152
142
|
JOIN message m ON m.message_id = t.message_id
|
|
153
143
|
WHERE m.category <> t.category
|
|
@@ -220,7 +210,7 @@ const columnOf = (row: unknown, column: string): unknown => {
|
|
|
220
210
|
return row[column];
|
|
221
211
|
};
|
|
222
212
|
|
|
223
|
-
//
|
|
213
|
+
// A driver may hand back a bigint aggregate as a string, so a count is a number
|
|
224
214
|
// or the decimal text of one, and anything else is a query that changed shape.
|
|
225
215
|
const countOf = (row: unknown, column: string): number => {
|
|
226
216
|
const value = columnOf(row, column);
|
|
@@ -328,14 +318,14 @@ export const repairThreadMessageCategory = async (
|
|
|
328
318
|
client: RepairSqlClient,
|
|
329
319
|
): Promise<RepairResult> => {
|
|
330
320
|
const startedAt = Date.now();
|
|
331
|
-
const rowsWritten = await client.run(repairStatement(
|
|
321
|
+
const rowsWritten = await client.run(repairStatement());
|
|
332
322
|
return { rowsWritten, elapsedMs: Date.now() - startedAt };
|
|
333
323
|
};
|
|
334
324
|
|
|
335
325
|
export const readResidual = async (
|
|
336
326
|
client: RepairSqlClient,
|
|
337
327
|
): Promise<CategoryResidual> => {
|
|
338
|
-
const [row] = await client.all(residualSql(
|
|
328
|
+
const [row] = await client.all(residualSql());
|
|
339
329
|
return {
|
|
340
330
|
repairable: countOf(row, "row_count"),
|
|
341
331
|
aheadOfClock: countOf(row, "ahead_of_clock"),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, eq, inArray, or, type SQL } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import type { Db } from "../db.js";
|
|
3
3
|
import { accountTable } from "../schema/i4-account-config.js";
|
|
4
4
|
import { accountSettingTable } from "../schema/i4-account-setting.js";
|
|
5
5
|
import { addressTable } from "../schema/i4-address.js";
|
|
@@ -24,13 +24,13 @@ export interface CascadeDeleteLogger {
|
|
|
24
24
|
info(obj: Record<string, unknown>, msg: string): void;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type CascadeDb =
|
|
27
|
+
type CascadeDb = Db<Record<string, unknown>>;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Per-message entity types. Their rows are removed with the message subtree by
|
|
31
31
|
* message id, so their individually-enumerated keys are not deleted one by one —
|
|
32
32
|
* the DynamoDB cascade enumerates them only because BatchWriteItem needs each
|
|
33
|
-
* full key;
|
|
33
|
+
* full key; the relational cascade deletes them set-wise by message id.
|
|
34
34
|
*/
|
|
35
35
|
const MESSAGE_CHILD_TYPES = new Set([
|
|
36
36
|
"MessageFlag",
|
|
@@ -70,7 +70,7 @@ const groupByType = (
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Relational equivalent of the DynamoDB BatchWriteItem cascade: delete every
|
|
74
74
|
* enumerated row for an account/tenant in one transaction. Message subtrees
|
|
75
75
|
* (message + nine child tables) are deleted set-wise by message id and emit a
|
|
76
76
|
* `message.removed` outbox row each for search-index cleanup; the remaining
|
|
@@ -199,22 +199,9 @@ export type CascadeDeleter = (
|
|
|
199
199
|
) => Promise<void>;
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*/
|
|
206
|
-
export const createCascadeDeleter = (connectionUrl: string): CascadeDeleter => {
|
|
207
|
-
const db = drizzle(connectionUrl) as CascadeDb;
|
|
208
|
-
return (entities, log) => runDrizzleCascadeDelete(db, entities, log);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* SQLite twin of {@link createCascadeDeleter} (RFC 036). Opens the shared SQLite
|
|
213
|
-
* file through the same serialized connection every writer uses and binds a
|
|
214
|
-
* cascade deleter to it. `runDrizzleCascadeDelete` deletes through the
|
|
215
|
-
* dialect-selected entity tables (`active-entities`, SQLite on this backend), so
|
|
216
|
-
* the same enumerated `CascadeEntity[]` drives an identical cascade over SQLite.
|
|
217
|
-
* Async because opening the SQLite handle loads the native binding lazily.
|
|
202
|
+
* Open the shared SQLite file through the same serialized connection every
|
|
203
|
+
* writer uses and bind a cascade deleter to it. Async because opening the
|
|
204
|
+
* SQLite handle loads the native binding lazily.
|
|
218
205
|
*/
|
|
219
206
|
export const createSqliteCascadeDeleter = async (
|
|
220
207
|
dbPath: string,
|
|
@@ -222,6 +209,5 @@ export const createSqliteCascadeDeleter = async (
|
|
|
222
209
|
const { db } = await createSqliteDatabase(messageDataSchema, {
|
|
223
210
|
filename: dbPath,
|
|
224
211
|
});
|
|
225
|
-
|
|
226
|
-
return (entities, log) => runDrizzleCascadeDelete(cascadeDb, entities, log);
|
|
212
|
+
return (entities, log) => runDrizzleCascadeDelete(db, entities, log);
|
|
227
213
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ThreadMessageItem } from "@remit/data-ports";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The measured category shape of the owner's INBOX, shared by the
|
|
5
|
-
*
|
|
4
|
+
* The measured category shape of the owner's INBOX, shared by the category
|
|
5
|
+
* suites (#304).
|
|
6
6
|
*
|
|
7
7
|
* It lives in one module because the shape is the load-bearing part of the
|
|
8
8
|
* regression: a filter resolved over the page the server returned is empty at
|
|
@@ -33,23 +33,6 @@ export const LIVE_TOTALS: CategoryTotals = {
|
|
|
33
33
|
social: 88,
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* The same shape with the four common categories trimmed, for the Postgres twin.
|
|
38
|
-
*
|
|
39
|
-
* The rare tail is untouched — `social` stays at its measured 88 — so every
|
|
40
|
-
* assertion the suites make about a rare category holds identically. What
|
|
41
|
-
* shrinks is the bulk that only exists to make the common categories common,
|
|
42
|
-
* which the Postgres suite does not assert on and which costs ~20s of seeding.
|
|
43
|
-
*/
|
|
44
|
-
export const TRIMMED_TOTALS: CategoryTotals = {
|
|
45
|
-
personal: 700,
|
|
46
|
-
marketing: 500,
|
|
47
|
-
automated: 400,
|
|
48
|
-
newsletter: 200,
|
|
49
|
-
transactional: 130,
|
|
50
|
-
social: 88,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
36
|
/**
|
|
54
37
|
* The newest 100 rows of that INBOX, as measured. Two of the 4,753 personal
|
|
55
38
|
* messages and two of the 88 social ones are in it, which is why selecting
|
|
@@ -4,10 +4,10 @@ import type {
|
|
|
4
4
|
IFilterAnchorRepository,
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { and, eq } from "drizzle-orm";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Db } from "../db.js";
|
|
8
8
|
import { filterAnchorTable } from "../schema.js";
|
|
9
9
|
|
|
10
|
-
type DB =
|
|
10
|
+
type DB = Db<Record<string, unknown>>;
|
|
11
11
|
|
|
12
12
|
function rowToFilterAnchor(
|
|
13
13
|
row: typeof filterAnchorTable.$inferSelect,
|
|
@@ -25,9 +25,9 @@ function rowToFilterAnchor(
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Relational counterpart to `FilterAnchorService` (remit-electrodb-service). The
|
|
29
29
|
* anchor vector is persisted inline on the row (RFC 034 Decision 2.1/2.3); the
|
|
30
|
-
*
|
|
30
|
+
* vector store's per-message chunk vectors are read only by the search
|
|
31
31
|
* service when it builds the anchor, never re-derived here at match time.
|
|
32
32
|
*/
|
|
33
33
|
export class FilterAnchorRepo implements IFilterAnchorRepository {
|
package/src/repos/filter.ts
CHANGED
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
FilterState,
|
|
12
12
|
} from "@remit/domain-enums";
|
|
13
13
|
import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
14
|
-
import type {
|
|
14
|
+
import type { Db } from "../db.js";
|
|
15
15
|
import { NotFoundError } from "../error.js";
|
|
16
16
|
import { randomId } from "../id.js";
|
|
17
17
|
import { decodeToken, resultList } from "../pagination.js";
|
|
18
18
|
import { filterTable } from "../schema.js";
|
|
19
19
|
|
|
20
|
-
type DB =
|
|
20
|
+
type DB = Db<Record<string, unknown>>;
|
|
21
21
|
|
|
22
22
|
const RULE_ASSERTION_FIELDS = [
|
|
23
23
|
"hasAnchor",
|
|
@@ -293,7 +293,7 @@ export class FilterRepo implements IFilterRepository {
|
|
|
293
293
|
* comparison. A no-op for a Standing filter (no `expiresAt`) or one already
|
|
294
294
|
* Expired.
|
|
295
295
|
*
|
|
296
|
-
*
|
|
296
|
+
* There is no TTL reaper here, so an Expired Temporary row is never deleted by
|
|
297
297
|
* a background sweep the way DynamoDB reaps it via the `ttl` attribute. That
|
|
298
298
|
* is correct by design: RFC 034 Decision 1.1 makes match-time correctness
|
|
299
299
|
* depend on the `expiresAt`/`now` comparison, never on the row's existence —
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
UpdateAccountConfigInput,
|
|
10
10
|
} from "@remit/data-ports";
|
|
11
11
|
import { and, asc, eq, gt, inArray, or } from "drizzle-orm";
|
|
12
|
-
import type {
|
|
12
|
+
import type { Db } from "../db.js";
|
|
13
13
|
import { NotFoundError } from "../error.js";
|
|
14
14
|
import { randomId } from "../id.js";
|
|
15
15
|
import { decodeToken, resultList } from "../pagination.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
} from "../schema/i4-account-config.js";
|
|
20
20
|
import { addressTable } from "../schema/i4-address.js";
|
|
21
21
|
|
|
22
|
-
type DB =
|
|
22
|
+
type DB = Db<Record<string, unknown>>;
|
|
23
23
|
|
|
24
24
|
function rowToAccountConfig(
|
|
25
25
|
row: typeof accountConfigTable.$inferSelect,
|
|
@@ -6,13 +6,13 @@ import type {
|
|
|
6
6
|
UpdateAccountExportRequestInput,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { eq } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Db } from "../db.js";
|
|
10
10
|
import { NotFoundError } from "../error.js";
|
|
11
11
|
import { randomId } from "../id.js";
|
|
12
12
|
import { decodeToken, resultList } from "../pagination.js";
|
|
13
13
|
import { accountExportRequestTable } from "../schema/i4-account-export-request.js";
|
|
14
14
|
|
|
15
|
-
type DB =
|
|
15
|
+
type DB = Db<Record<string, unknown>>;
|
|
16
16
|
|
|
17
17
|
function rowToItem(
|
|
18
18
|
row: typeof accountExportRequestTable.$inferSelect,
|
|
@@ -6,10 +6,10 @@ import type {
|
|
|
6
6
|
UpsertAccountSettingInput,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { eq } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Db } from "../db.js";
|
|
10
10
|
import { accountSettingTable } from "../schema/i4-account-setting.js";
|
|
11
11
|
|
|
12
|
-
type DB =
|
|
12
|
+
type DB = Db<Record<string, unknown>>;
|
|
13
13
|
|
|
14
14
|
// The PG store derives its own deterministic id from the composite key. It is
|
|
15
15
|
// stable within this backend but not byte-identical to the DDB base36 UUIDv5
|
package/src/repos/i4-account.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from "@remit/data-ports";
|
|
10
10
|
import { SyncPhase } from "@remit/domain-enums";
|
|
11
11
|
import { and, asc, eq, gt, inArray, or, sql } from "drizzle-orm";
|
|
12
|
-
import type {
|
|
12
|
+
import type { Db } from "../db.js";
|
|
13
13
|
import { NotFoundError } from "../error.js";
|
|
14
14
|
import { randomId } from "../id.js";
|
|
15
15
|
import { decodeToken, encodeToken, resultList } from "../pagination.js";
|
|
@@ -17,7 +17,7 @@ import { accountTable } from "../schema/i4-account-config.js";
|
|
|
17
17
|
import { mailboxTable } from "../schema/i4-mailbox.js";
|
|
18
18
|
import { rowToMailbox } from "./i4-mailbox.js";
|
|
19
19
|
|
|
20
|
-
type DB =
|
|
20
|
+
type DB = Db<Record<string, unknown>>;
|
|
21
21
|
|
|
22
22
|
export function rowToAccount(
|
|
23
23
|
row: typeof accountTable.$inferSelect,
|
|
@@ -4,11 +4,11 @@ import type {
|
|
|
4
4
|
WithMailboxLockResult,
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { and, eq, lt } from "drizzle-orm";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Db } from "../db.js";
|
|
8
8
|
import { randomId } from "../id.js";
|
|
9
9
|
import { mailboxLockTable } from "../schema/i4-mailbox-lock.js";
|
|
10
10
|
|
|
11
|
-
type DB =
|
|
11
|
+
type DB = Db<Record<string, unknown>>;
|
|
12
12
|
|
|
13
13
|
const TTL_SECONDS = 3600;
|
|
14
14
|
|
|
@@ -4,11 +4,11 @@ import type {
|
|
|
4
4
|
MailboxSpecialUseValue,
|
|
5
5
|
} from "@remit/data-ports";
|
|
6
6
|
import { eq } from "drizzle-orm";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Db } from "../db.js";
|
|
8
8
|
import { randomId } from "../id.js";
|
|
9
9
|
import { mailboxSpecialUseTable, mailboxTable } from "../schema/i4-mailbox.js";
|
|
10
10
|
|
|
11
|
-
type DB =
|
|
11
|
+
type DB = Db<Record<string, unknown>>;
|
|
12
12
|
|
|
13
13
|
function rowToSpecialUse(
|
|
14
14
|
row: typeof mailboxSpecialUseTable.$inferSelect,
|
package/src/repos/i4-mailbox.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type {
|
|
|
8
8
|
} from "@remit/data-ports";
|
|
9
9
|
import { MailboxCursorState, MailboxSyncStatus } from "@remit/domain-enums";
|
|
10
10
|
import { and, asc, eq, gt, inArray, or } from "drizzle-orm";
|
|
11
|
-
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
12
11
|
import shortUuid from "short-uuid";
|
|
12
|
+
import type { Db } from "../db.js";
|
|
13
13
|
import { NotFoundError } from "../error.js";
|
|
14
14
|
import { decodeToken, resultList } from "../pagination.js";
|
|
15
15
|
import { mailboxTable } from "../schema/i4-mailbox.js";
|
|
@@ -19,7 +19,7 @@ const base36Translator = shortUuid.createTranslator(
|
|
|
19
19
|
);
|
|
20
20
|
const generateMailboxId = () => base36Translator.fromUUID(randomUUID());
|
|
21
21
|
|
|
22
|
-
type DB =
|
|
22
|
+
type DB = Db<Record<string, unknown>>;
|
|
23
23
|
|
|
24
24
|
export function rowToMailbox(
|
|
25
25
|
row: typeof mailboxTable.$inferSelect,
|
|
@@ -3,7 +3,7 @@ import { after, before, describe, test } from "node:test";
|
|
|
3
3
|
import { createTestDb, randomId, type TestDb } from "../test-db.js";
|
|
4
4
|
import { MessageFlagPushRepo } from "./i4-message-flag-push.js";
|
|
5
5
|
|
|
6
|
-
describe("MessageFlagPushRepo (
|
|
6
|
+
describe("MessageFlagPushRepo (relational counterpart to MessageFlagPushService)", () => {
|
|
7
7
|
let db: TestDb;
|
|
8
8
|
let close: () => Promise<void>;
|
|
9
9
|
let repo: MessageFlagPushRepo;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Db } from "../db.js";
|
|
3
3
|
import { messageFlagPushTable } from "../schema/i4-message-flag-push.js";
|
|
4
4
|
|
|
5
|
-
type DB =
|
|
5
|
+
type DB = Db<Record<string, unknown>>;
|
|
6
6
|
|
|
7
7
|
export type FlagPushOperation = "add" | "remove";
|
|
8
8
|
export type MessageFlagPushState =
|
|
@@ -47,7 +47,7 @@ function rowToItem(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* Relational counterpart to `MessageFlagPushService` (remit-electrodb-service).
|
|
51
51
|
* Same public shape (`put`/`find`/`updateState`/`delete`/`listByAccountId`/
|
|
52
52
|
* `listByMailboxId`) so both backends satisfy the same structural contract
|
|
53
53
|
* for read-time unseenCount prediction (issue #1273, epic #1281 invariant 4)
|
|
@@ -3,7 +3,7 @@ import { after, before, describe, test } from "node:test";
|
|
|
3
3
|
import { createTestDb, randomId, type TestDb } from "../test-db.js";
|
|
4
4
|
import { MessagePlacementMoveRepo } from "./i4-message-placement-move.js";
|
|
5
5
|
|
|
6
|
-
describe("MessagePlacementMoveRepo (
|
|
6
|
+
describe("MessagePlacementMoveRepo (relational counterpart to MessagePlacementMoveService)", () => {
|
|
7
7
|
let db: TestDb;
|
|
8
8
|
let close: () => Promise<void>;
|
|
9
9
|
let repo: MessagePlacementMoveRepo;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { eq } from "drizzle-orm";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Db } from "../db.js";
|
|
3
3
|
import { messagePlacementMoveTable } from "../schema/i4-message-placement-move.js";
|
|
4
4
|
|
|
5
|
-
type DB =
|
|
5
|
+
type DB = Db<Record<string, unknown>>;
|
|
6
6
|
|
|
7
7
|
export type MessagePlacementMoveState =
|
|
8
8
|
| "pending"
|
|
@@ -44,7 +44,8 @@ function rowToItem(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Relational counterpart to `MessagePlacementMoveService`
|
|
48
|
+
* (remit-electrodb-service).
|
|
48
49
|
* Same public shape (`put`/`find`/`delete`/`listByAccountId`) so both backends
|
|
49
50
|
* satisfy the same structural contract for read-time count prediction
|
|
50
51
|
* (`mailbox.ts`'s `loadPendingMoves`, epic #1281 invariant 4).
|
|
@@ -6,13 +6,13 @@ import type {
|
|
|
6
6
|
UpdateOrganizeJobRequestInput,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { eq } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Db } from "../db.js";
|
|
10
10
|
import { NotFoundError } from "../error.js";
|
|
11
11
|
import { randomId } from "../id.js";
|
|
12
12
|
import { decodeToken, resultList } from "../pagination.js";
|
|
13
13
|
import { organizeJobRequestTable } from "../schema/i4-organize-job-request.js";
|
|
14
14
|
|
|
15
|
-
type DB =
|
|
15
|
+
type DB = Db<Record<string, unknown>>;
|
|
16
16
|
|
|
17
17
|
function rowToItem(
|
|
18
18
|
row: typeof organizeJobRequestTable.$inferSelect,
|
|
@@ -6,12 +6,12 @@ import type {
|
|
|
6
6
|
UpdateOutboxMessageInput,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { and, desc, eq, inArray, lt, or } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Db } from "../db.js";
|
|
10
10
|
import { ForbiddenError, NotFoundError } from "../error.js";
|
|
11
11
|
import { decodeToken, resultList } from "../pagination.js";
|
|
12
12
|
import { outboxMessageTable } from "../schema/i4-outbox-message.js";
|
|
13
13
|
|
|
14
|
-
type DB =
|
|
14
|
+
type DB = Db<Record<string, unknown>>;
|
|
15
15
|
|
|
16
16
|
function rowToOutboxMessage(
|
|
17
17
|
row: typeof outboxMessageTable.$inferSelect,
|
package/src/repos/label.ts
CHANGED
|
@@ -6,13 +6,13 @@ import type {
|
|
|
6
6
|
UpdateLabelInput,
|
|
7
7
|
} from "@remit/data-ports";
|
|
8
8
|
import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Db } from "../db.js";
|
|
10
10
|
import { NotFoundError } from "../error.js";
|
|
11
11
|
import { randomId } from "../id.js";
|
|
12
12
|
import { decodeToken, resultList } from "../pagination.js";
|
|
13
13
|
import { labelTable } from "../schema.js";
|
|
14
14
|
|
|
15
|
-
type DB =
|
|
15
|
+
type DB = Db<Record<string, unknown>>;
|
|
16
16
|
|
|
17
17
|
const normalize = (name: string): string => name.trim().toLowerCase();
|
|
18
18
|
|
|
@@ -5,14 +5,14 @@ import type {
|
|
|
5
5
|
MessageFlagItem,
|
|
6
6
|
} from "@remit/data-ports";
|
|
7
7
|
import { and, eq, inArray } from "drizzle-orm";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Db } from "../db.js";
|
|
9
9
|
import { NotFoundError } from "../error.js";
|
|
10
10
|
import {
|
|
11
11
|
type MessageDataSchema,
|
|
12
12
|
messageFlagTable,
|
|
13
13
|
} from "../schema/message-data.js";
|
|
14
14
|
|
|
15
|
-
type DB =
|
|
15
|
+
type DB = Db<MessageDataSchema>;
|
|
16
16
|
|
|
17
17
|
function rowToMessageFlag(
|
|
18
18
|
row: typeof messageFlagTable.$inferSelect,
|