@remit/drizzle-service 0.0.35 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +3 -10
  2. package/src/db.ts +6 -6
  3. package/src/error.ts +2 -5
  4. package/src/index.ts +0 -1
  5. package/src/repair/thread-message-category-contract.ts +2 -4
  6. package/src/repair/thread-message-category.sqlite.test.ts +1 -2
  7. package/src/repair/thread-message-category.ts +15 -25
  8. package/src/repos/cascade-delete.ts +8 -22
  9. package/src/repos/category-fixture.ts +2 -19
  10. package/src/repos/filter-anchor.ts +4 -4
  11. package/src/repos/filter.ts +3 -3
  12. package/src/repos/i4-account-config.ts +2 -2
  13. package/src/repos/i4-account-export-request.ts +2 -2
  14. package/src/repos/i4-account-setting.ts +2 -2
  15. package/src/repos/i4-account.ts +2 -2
  16. package/src/repos/i4-mailbox-lock.ts +2 -2
  17. package/src/repos/i4-mailbox-special-use.ts +2 -2
  18. package/src/repos/i4-mailbox.ts +2 -2
  19. package/src/repos/i4-message-flag-push.test.ts +1 -1
  20. package/src/repos/i4-message-flag-push.ts +3 -3
  21. package/src/repos/i4-message-placement-move.test.ts +1 -1
  22. package/src/repos/i4-message-placement-move.ts +4 -3
  23. package/src/repos/i4-organize-job-request.ts +2 -2
  24. package/src/repos/i4-outbox-message.ts +2 -2
  25. package/src/repos/label.ts +2 -2
  26. package/src/repos/message-flag.ts +2 -2
  27. package/src/repos/message-label.ts +2 -2
  28. package/src/repos/message.ts +8 -10
  29. package/src/repos/quarantine.ts +2 -2
  30. package/src/repos/test-helpers.ts +11 -213
  31. package/src/repos/thread-message.sqlite.test.ts +35 -10
  32. package/src/repos/thread-message.test.ts +45 -105
  33. package/src/repos/thread-message.ts +11 -25
  34. package/src/repos/thread-search-predicates.ts +13 -41
  35. package/src/repos/unit-of-work.ts +1 -1
  36. package/src/schema/i4-account-config.ts +1 -1
  37. package/src/schema/i4-account-export-request.ts +1 -1
  38. package/src/schema/i4-account-setting.ts +1 -1
  39. package/src/schema/i4-address.ts +1 -1
  40. package/src/schema/i4-mailbox-lock.ts +1 -1
  41. package/src/schema/i4-mailbox.ts +1 -1
  42. package/src/schema/i4-message-flag-push.ts +1 -1
  43. package/src/schema/i4-message-placement-move.ts +1 -1
  44. package/src/schema/i4-organize-job-request.ts +1 -1
  45. package/src/schema/i4-outbox-message.ts +1 -1
  46. package/src/schema/message-data.ts +1 -1
  47. package/src/schema/outbox.ts +13 -56
  48. package/src/schema/quarantine.ts +1 -1
  49. package/src/schema/thread-message.ts +1 -1
  50. package/src/schema-full-sqlite.ts +8 -6
  51. package/src/schema.ts +4 -4
  52. package/src/sqlite-client.ts +4 -5
  53. package/src/test-db-sqlite.ts +4 -9
  54. package/src/test-db.ts +11 -66
  55. package/src/tx.ts +6 -12
  56. package/drizzle.config.ts +0 -15
  57. package/src/dialect.ts +0 -13
  58. package/src/repair/thread-message-category.test.ts +0 -229
  59. package/src/repos/thread-message-category.test.ts +0 -156
  60. package/src/schema/active-entities.ts +0 -19
  61. 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.35",
4
- "description": "Drizzle ORM service parameterized by dialect (Postgres / SQLite)",
3
+ "version": "0.0.37",
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:pg": "node --env-file=../../localhost-test-unit.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=81 --test 'src/**/!(*.sqlite).test.ts'",
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 { NodePgQueryResultHKT } from "drizzle-orm/node-postgres";
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>> = PgDatabase<
11
- NodePgQueryResultHKT,
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 pg repo is the
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
- // SQLSTATE / SQLite `code`) on `.cause`, so walk the cause chain — covers both
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
@@ -3,7 +3,6 @@ export {
3
3
  type CascadeDeleteLogger,
4
4
  type CascadeDeleter,
5
5
  type CascadeEntity,
6
- createCascadeDeleter,
7
6
  createSqliteCascadeDeleter,
8
7
  runDrizzleCascadeDelete,
9
8
  } from "./repos/cascade-delete.js";
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * The behaviour `thread_message.category`'s repair (#321) must have, asserted
3
- * against a real engine of each dialect from one place: the statement ships in
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
- "Postgres instance whose column was added",
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("sqlite")).run(),
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 per dialect. The value it copies is reachable through
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. On SQLite writers are serialized, so a
37
- * concurrent body-sync write lands wholly before or wholly after and both
38
- * orders converge on the same value. On Postgres READ COMMITTED an UPDATE
39
- * re-evaluates its WHERE clause against the version a concurrent
40
- * transaction just committed, but still reads other tables at its original
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, its `pg.Pool`, or a test
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 nowMillis = (dialect: RepairDialect): string =>
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 = (dialect: RepairDialect): string =>
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 <= ${nowMillis(dialect)}`;
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 = (dialect: RepairDialect): string =>
138
+ const residualSql = (): string =>
149
139
  `SELECT count(*) AS row_count,
150
- coalesce(sum(CASE WHEN t.updated_at > ${nowMillis(dialect)} THEN 1 ELSE 0 END), 0) AS ahead_of_clock
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
- // node-postgres hands back bigint aggregates as strings, so a count is a number
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(client.dialect));
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(client.dialect));
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 { drizzle, type NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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; Postgres deletes them set-wise by message id.
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
- * Postgres equivalent of the DynamoDB BatchWriteItem cascade: delete every
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
- * Bind a cascade deleter to one Postgres connection. The account worker builds
203
- * this once at module load on the `DATA_BACKEND=postgres` path and reuses it
204
- * across invocations; the underlying pool is shared with no per-purge setup.
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
- const cascadeDb = db as unknown as CascadeDb;
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 sqlite and
5
- * Postgres category suites (#304).
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
7
+ import type { Db } from "../db.js";
8
8
  import { filterAnchorTable } from "../schema.js";
9
9
 
10
- type DB = NodePgDatabase<Record<string, unknown>>;
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
- * Postgres counterpart to `FilterAnchorService` (remit-electrodb-service). The
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
- * pgvector store's per-message chunk vectors are read only by the search
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 {
@@ -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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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
- * Postgres has no TTL reaper, so an Expired Temporary row is never deleted by
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
9
+ import type { Db } from "../db.js";
10
10
  import { accountSettingTable } from "../schema/i4-account-setting.js";
11
11
 
12
- type DB = NodePgDatabase<Record<string, unknown>>;
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
@@ -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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
11
+ type DB = Db<Record<string, unknown>>;
12
12
 
13
13
  function rowToSpecialUse(
14
14
  row: typeof mailboxSpecialUseTable.$inferSelect,
@@ -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 = NodePgDatabase<Record<string, unknown>>;
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 (Postgres counterpart to MessageFlagPushService)", () => {
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { Db } from "../db.js";
3
3
  import { messageFlagPushTable } from "../schema/i4-message-flag-push.js";
4
4
 
5
- type DB = NodePgDatabase<Record<string, unknown>>;
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
- * Postgres counterpart to `MessageFlagPushService` (remit-electrodb-service).
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 (Postgres counterpart to MessagePlacementMoveService)", () => {
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import type { Db } from "../db.js";
3
3
  import { messagePlacementMoveTable } from "../schema/i4-message-placement-move.js";
4
4
 
5
- type DB = NodePgDatabase<Record<string, unknown>>;
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
- * Postgres counterpart to `MessagePlacementMoveService` (remit-electrodb-service).
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
14
+ type DB = Db<Record<string, unknown>>;
15
15
 
16
16
  function rowToOutboxMessage(
17
17
  row: typeof outboxMessageTable.$inferSelect,
@@ -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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<Record<string, unknown>>;
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 { NodePgDatabase } from "drizzle-orm/node-postgres";
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 = NodePgDatabase<MessageDataSchema>;
15
+ type DB = Db<MessageDataSchema>;
16
16
 
17
17
  function rowToMessageFlag(
18
18
  row: typeof messageFlagTable.$inferSelect,