@remit/drizzle-service 0.0.102 → 0.0.103

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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/batch.sqlite.test.ts +4 -4
  3. package/src/batch.ts +47 -21
  4. package/src/databases.ts +24 -6
  5. package/src/db.ts +20 -5
  6. package/src/index.ts +12 -0
  7. package/src/repair/junk-only-address.sqlite.test.ts +7 -2
  8. package/src/repos/account.conformance.sqlite.test.ts +6 -2
  9. package/src/repos/calendar-collection.ts +2 -2
  10. package/src/repos/calendar-default.sqlite.test.ts +6 -2
  11. package/src/repos/calendar-event-index.ts +2 -2
  12. package/src/repos/calendar-feed-token.ts +2 -2
  13. package/src/repos/calendar-object.ts +2 -2
  14. package/src/repos/calendar-put.sqlite.test.ts +6 -2
  15. package/src/repos/calendar-store.ts +36 -40
  16. package/src/repos/calendar-suggestion.ts +2 -2
  17. package/src/repos/cascade-delete.test.ts +3 -2
  18. package/src/repos/cascade-delete.ts +11 -7
  19. package/src/repos/envelope.sqlite.test.ts +3 -2
  20. package/src/repos/envelope.test.ts +3 -2
  21. package/src/repos/envelope.ts +14 -9
  22. package/src/repos/filter.ts +2 -2
  23. package/src/repos/i4-account-config.test.ts +10 -6
  24. package/src/repos/i4-account-config.ts +8 -10
  25. package/src/repos/i4-account-export-request.ts +2 -2
  26. package/src/repos/i4-account-setting.ts +2 -2
  27. package/src/repos/i4-account.test.ts +12 -8
  28. package/src/repos/i4-account.ts +6 -10
  29. package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
  30. package/src/repos/i4-address.ts +12 -2
  31. package/src/repos/i4-config-import.ts +2 -2
  32. package/src/repos/i4-mailbox-lock.ts +2 -2
  33. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +12 -10
  34. package/src/repos/i4-mailbox-special-use.ts +29 -40
  35. package/src/repos/i4-mailbox.sqlite.test.ts +12 -10
  36. package/src/repos/i4-mailbox.test.ts +15 -14
  37. package/src/repos/i4-mailbox.ts +13 -7
  38. package/src/repos/i4-message-flag-push.ts +2 -2
  39. package/src/repos/i4-message-placement-move.ts +2 -2
  40. package/src/repos/i4-organize-job-request.ts +2 -2
  41. package/src/repos/i4-outbox-attachment.test.ts +7 -2
  42. package/src/repos/i4-outbox-attachment.ts +11 -5
  43. package/src/repos/i4-outbox-message.ts +2 -2
  44. package/src/repos/i4-sender-signer-standing.ts +2 -2
  45. package/src/repos/label.ts +2 -2
  46. package/src/repos/message-flag.ts +3 -6
  47. package/src/repos/message-label.ts +2 -2
  48. package/src/repos/message.sqlite.test.ts +3 -3
  49. package/src/repos/message.test.ts +6 -5
  50. package/src/repos/message.ts +15 -10
  51. package/src/repos/quarantine.ts +2 -2
  52. package/src/repos/sync-warning.ts +2 -2
  53. package/src/repos/thread-message-body-muted.sqlite.test.ts +7 -3
  54. package/src/repos/thread-message-field-terms.sqlite.test.ts +7 -6
  55. package/src/repos/thread-message.sqlite.test.ts +7 -3
  56. package/src/repos/thread-message.test.ts +22 -18
  57. package/src/repos/thread-message.ts +13 -5
  58. package/src/search/thread-message-search.sqlite.test.ts +8 -3
  59. package/src/search/thread-message-search.ts +13 -8
  60. package/src/sqlite-repositories.ts +48 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.102",
3
+ "version": "0.0.103",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import { eq } from "drizzle-orm";
4
- import { runBatch } from "./batch.js";
4
+ import { syncBatch } from "./batch.js";
5
5
  import { calendarTable } from "./schema.js";
6
6
  import { createSqliteTestDb, type SqliteTestDb } from "./test-db-sqlite.js";
7
7
 
@@ -36,7 +36,7 @@ describe("a batch on better-sqlite3", () => {
36
36
  const stored = () => db.select().from(calendarTable);
37
37
 
38
38
  test("answers each statement in order, reads seeing the writes before them", async () => {
39
- const [inserted, read] = await runBatch(db, [
39
+ const [inserted, read] = await syncBatch(db)([
40
40
  db.insert(calendarTable).values(collection("first")).returning(),
41
41
  db
42
42
  .select()
@@ -58,7 +58,7 @@ describe("a batch on better-sqlite3", () => {
58
58
  const before = await stored();
59
59
 
60
60
  await assert.rejects(
61
- runBatch(db, [
61
+ syncBatch(db)([
62
62
  db.insert(calendarTable).values(collection("second")),
63
63
  db
64
64
  .update(calendarTable)
@@ -73,6 +73,6 @@ describe("a batch on better-sqlite3", () => {
73
73
  });
74
74
 
75
75
  test("an empty batch writes nothing and answers nothing", async () => {
76
- assert.deepEqual(await runBatch(db, []), []);
76
+ assert.deepEqual(await syncBatch(db)([]), []);
77
77
  });
78
78
  });
package/src/batch.ts CHANGED
@@ -3,6 +3,26 @@ import type { Db } from "./db.js";
3
3
 
4
4
  export type SqliteBatchItem = BatchItem<"sqlite">;
5
5
 
6
+ /**
7
+ * The one write port a repository is handed for a write set, bound to one
8
+ * database. It has the shape of D1's `batch()`: every statement commits
9
+ * together or not at all, and none of them reads another's result mid-flight.
10
+ * A write that needs a value reads it before the batch and is written so a
11
+ * stale read loses safely (a conditional `WHERE`, an upsert, a deterministic
12
+ * id).
13
+ */
14
+ export type WriteBatch = <T extends readonly SqliteBatchItem[]>(
15
+ items: T,
16
+ ) => Promise<BatchResponse<T>>;
17
+
18
+ type NonEmptyBatch = readonly [SqliteBatchItem, ...SqliteBatchItem[]];
19
+
20
+ export interface BatchingDatabase {
21
+ batch<U extends SqliteBatchItem, T extends Readonly<[U, ...U[]]>>(
22
+ batch: T,
23
+ ): Promise<BatchResponse<T>>;
24
+ }
25
+
6
26
  interface SyncPreparable {
7
27
  _prepare(): { execute(): { sync(): unknown } };
8
28
  }
@@ -12,6 +32,10 @@ const isSyncPreparable = (value: unknown): value is SyncPreparable =>
12
32
  value !== null &&
13
33
  typeof Reflect.get(value, "_prepare") === "function";
14
34
 
35
+ const isNonEmpty = <T extends readonly SqliteBatchItem[]>(
36
+ items: T,
37
+ ): items is T & NonEmptyBatch => items.length > 0;
38
+
15
39
  const runSync = (item: SqliteBatchItem): unknown => {
16
40
  if (!isSyncPreparable(item)) {
17
41
  throw new TypeError("batch item is not a drizzle sqlite query");
@@ -19,28 +43,30 @@ const runSync = (item: SqliteBatchItem): unknown => {
19
43
  return item._prepare().execute().sync();
20
44
  };
21
45
 
46
+ const emptyResponse = <
47
+ T extends readonly SqliteBatchItem[],
48
+ >(): BatchResponse<T> => [] as unknown as BatchResponse<T>;
49
+
22
50
  /**
23
- * The shape of D1's `batch()`: every statement commits together or not at all,
24
- * and none of them reads another's result mid-flight. A write that needs a
25
- * value reads it before the batch and is written so a stale read loses safely
26
- * (a conditional `WHERE`, an upsert, a deterministic id).
27
- *
28
- * On better-sqlite3 the batch is one synchronous IMMEDIATE transaction: it
29
- * never yields the event loop, so no other write on the shared connection lands
30
- * inside it, and IMMEDIATE takes the file's write lock before the first
51
+ * A batch as one synchronous transaction. On better-sqlite3 it is IMMEDIATE:
52
+ * it never yields the event loop, so no other write on the shared connection
53
+ * lands inside it, and it takes the file's write lock before the first
31
54
  * statement so a writer in another process waits out the busy timeout. On a
32
55
  * Durable Object's `durable-sqlite` handle it is one `transactionSync`.
33
56
  */
34
- export const runBatch = async <
35
- TSchema extends Record<string, unknown>,
36
- T extends readonly SqliteBatchItem[],
37
- >(
38
- db: Db<TSchema>,
39
- items: T,
40
- ): Promise<BatchResponse<T>> => {
41
- if (items.length === 0) return [] as unknown as BatchResponse<T>;
42
- const results = db.transaction(() => items.map(runSync), {
43
- behavior: "immediate",
44
- });
45
- return results as unknown as BatchResponse<T>;
46
- };
57
+ export const syncBatch =
58
+ <TSchema extends Record<string, unknown>>(db: Db<TSchema>): WriteBatch =>
59
+ async (items) => {
60
+ if (items.length === 0) return emptyResponse();
61
+ const results = db.transaction(() => items.map(runSync), {
62
+ behavior: "immediate",
63
+ });
64
+ return results as unknown as BatchResponse<typeof items>;
65
+ };
66
+
67
+ export const asyncBatch =
68
+ (db: BatchingDatabase): WriteBatch =>
69
+ async (items) => {
70
+ if (!isNonEmpty(items)) return emptyResponse();
71
+ return db.batch(items);
72
+ };
package/src/databases.ts CHANGED
@@ -1,8 +1,6 @@
1
- import type { Db } from "./db.js";
2
- import {
3
- type MessageDataSchema,
4
- messageDataSchema,
5
- } from "./schema/message-data.js";
1
+ import { syncBatch } from "./batch.js";
2
+ import type { Db, SqliteStore } from "./db.js";
3
+ import { messageDataSchema } from "./schema/message-data.js";
6
4
  import { createSqliteDatabase } from "./sqlite-client.js";
7
5
 
8
6
  // The three databases a self-host instance runs on (#1420). The app database
@@ -12,10 +10,30 @@ import { createSqliteDatabase } from "./sqlite-client.js";
12
10
  // rebuilt from the mail database, so it never commits together with it.
13
11
  export interface SqliteDatabases {
14
12
  app: Db<Record<string, unknown>>;
15
- mail: Db<MessageDataSchema>;
13
+ mail: Db<Record<string, unknown>>;
16
14
  search: Db<Record<string, unknown>>;
17
15
  }
18
16
 
17
+ export interface SqliteStores {
18
+ app: SqliteStore<Record<string, unknown>>;
19
+ mail: SqliteStore<Record<string, unknown>>;
20
+ search: SqliteStore<Record<string, unknown>>;
21
+ }
22
+
23
+ export const syncStore = <TSchema extends Record<string, unknown>>(
24
+ db: Db<TSchema>,
25
+ ): SqliteStore<TSchema> => ({ db, batch: syncBatch(db) });
26
+
27
+ export const syncStores = ({
28
+ app,
29
+ mail,
30
+ search,
31
+ }: SqliteDatabases): SqliteStores => ({
32
+ app: syncStore(app),
33
+ mail: syncStore(mail),
34
+ search: syncStore(search),
35
+ });
36
+
19
37
  export interface SqliteDatabasePaths {
20
38
  app: string;
21
39
  mail: string;
package/src/db.ts CHANGED
@@ -1,14 +1,29 @@
1
1
  import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
2
+ import type { WriteBatch } from "./batch.js";
2
3
 
3
4
  /**
4
- * The drizzle handle a repository runs on: any synchronous SQLite driver.
5
- *
6
- * better-sqlite3 is the one this package opens; a Cloudflare Durable Object's
7
- * `durable-sqlite` handle satisfies it too, so its adapter constructs the same
8
- * repositories over its own storage.
5
+ * A synchronous SQLite handle: better-sqlite3, or a Cloudflare Durable
6
+ * Object's `durable-sqlite`. Only `syncBatch` and the better-sqlite3
7
+ * composition name it; a repository never does.
9
8
  */
10
9
  export type Db<TSchema extends Record<string, unknown>> = BaseSQLiteDatabase<
11
10
  "sync",
12
11
  unknown,
13
12
  TSchema
14
13
  >;
14
+
15
+ /**
16
+ * The handle a repository runs on: any SQLite driver, synchronous or not
17
+ * (better-sqlite3, Durable Object storage, D1, libsql). It has no
18
+ * `transaction` and no `batch`, so a repository writes a set through the
19
+ * injected `WriteBatch` and cannot open an interactive transaction (C5).
20
+ */
21
+ export type RepositoryDb<TSchema extends Record<string, unknown>> = Omit<
22
+ BaseSQLiteDatabase<"sync" | "async", unknown, TSchema>,
23
+ "transaction" | "batch"
24
+ >;
25
+
26
+ export interface SqliteStore<TSchema extends Record<string, unknown>> {
27
+ db: RepositoryDb<TSchema>;
28
+ batch: WriteBatch;
29
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,22 @@
1
+ export {
2
+ asyncBatch,
3
+ type BatchingDatabase,
4
+ type SqliteBatchItem,
5
+ syncBatch,
6
+ type WriteBatch,
7
+ } from "./batch.js";
1
8
  export {
2
9
  hasSqliteDatabaseEnv,
3
10
  openSqliteDatabases,
4
11
  SQLITE_DATABASE_ENV,
5
12
  type SqliteDatabasePaths,
6
13
  type SqliteDatabases,
14
+ type SqliteStores,
7
15
  sqliteDatabasePathsFromEnv,
16
+ syncStore,
17
+ syncStores,
8
18
  } from "./databases.js";
19
+ export type { RepositoryDb, SqliteStore } from "./db.js";
9
20
  export { CreateFailedConflictError, NotFoundError } from "./error.js";
10
21
  export { CalendarCollectionRepo } from "./repos/calendar-collection.js";
11
22
  export { CalendarEventIndexRepo } from "./repos/calendar-event-index.js";
@@ -79,6 +90,7 @@ export {
79
90
  type SqliteClientOptions,
80
91
  } from "./sqlite-client.js";
81
92
  export {
93
+ composeSqliteRepositories,
82
94
  createSqliteRepositories,
83
95
  type SqliteRepositories,
84
96
  } from "./sqlite-repositories.js";
@@ -5,7 +5,8 @@ import { CanonicalMailboxRole } from "@remit/domain-enums";
5
5
  import Database from "better-sqlite3";
6
6
  import { drizzle } from "drizzle-orm/better-sqlite3";
7
7
  import { AccountSettingRepo } from "../repos/i4-account-setting.js";
8
- import { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
8
+ import type { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
9
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
9
10
  import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
10
11
  import {
11
12
  type JunkOnlyRepairClient,
@@ -181,7 +182,11 @@ describe("addresses standing only on mail in Junk", () => {
181
182
  before(() => {
182
183
  sqlite = new Database(":memory:");
183
184
  const db = drizzle(sqlite);
184
- specialUse = new MailboxSpecialUseRepo(db as never, db as never);
185
+ specialUse = createSqliteRepositories({
186
+ app: db,
187
+ mail: db,
188
+ search: db,
189
+ }).mailboxSpecialUse;
185
190
  accountSetting = new AccountSettingRepo(db as never);
186
191
  applyMigrationSet(sqlite, "app");
187
192
  applyMigrationSet(sqlite, "mail");
@@ -2,8 +2,8 @@ import { accountRepositoryConformance } from "@remit/data-ports/conformance";
2
2
  import { NotFoundError } from "../error.js";
3
3
  import { randomId } from "../id.js";
4
4
  import { accountTable } from "../schema.js";
5
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
5
6
  import { createSqliteTestDb } from "../test-db-sqlite.js";
6
- import { AccountRepo } from "./i4-account.js";
7
7
 
8
8
  let close: (() => Promise<void>) | undefined;
9
9
 
@@ -13,7 +13,11 @@ accountRepositoryConformance({
13
13
  accounts: accountTable,
14
14
  });
15
15
  close = closeDb;
16
- return new AccountRepo(db as never, db as never);
16
+ return createSqliteRepositories({
17
+ app: db,
18
+ mail: db,
19
+ search: db,
20
+ }).account;
17
21
  },
18
22
  teardown: async () => {
19
23
  await close?.();
@@ -9,11 +9,11 @@ import {
9
9
  normalizeCalendarUrlSegment,
10
10
  } from "@remit/data-ports/id";
11
11
  import { and, asc, eq, sql } from "drizzle-orm";
12
- import type { Db } from "../db.js";
12
+ import type { RepositoryDb } from "../db.js";
13
13
  import { NotFoundError } from "../error.js";
14
14
  import { calendarTable } from "../schema.js";
15
15
 
16
- type DB = Db<Record<string, unknown>>;
16
+ type DB = RepositoryDb<Record<string, unknown>>;
17
17
 
18
18
  function rowToCalendar(
19
19
  row: typeof calendarTable.$inferSelect,
@@ -8,9 +8,9 @@ import {
8
8
  calendarObjectTable,
9
9
  calendarTable,
10
10
  } from "../schema.js";
11
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
11
12
  import { createSqliteTestDb } from "../test-db-sqlite.js";
12
13
  import { CalendarCollectionRepo } from "./calendar-collection.js";
13
- import { DrizzleCalendarStore } from "./calendar-store.js";
14
14
 
15
15
  const ACCOUNT_CONFIG_ID = "account-config-default";
16
16
 
@@ -27,7 +27,11 @@ describe("provisioning the default calendar", () => {
27
27
  });
28
28
  db = created.db as unknown as Db<Record<string, unknown>>;
29
29
  close = created.close;
30
- store = new DrizzleCalendarStore(db, db);
30
+ store = createSqliteRepositories({
31
+ app: db,
32
+ mail: db,
33
+ search: db,
34
+ }).calendarStore;
31
35
  });
32
36
 
33
37
  after(() => close());
@@ -4,10 +4,10 @@ import type {
4
4
  ICalendarEventIndexRepository,
5
5
  } from "@remit/data-ports";
6
6
  import { and, asc, eq, gte, lt } from "drizzle-orm";
7
- import type { Db } from "../db.js";
7
+ import type { RepositoryDb } from "../db.js";
8
8
  import { calendarEventIndexTable } from "../schema.js";
9
9
 
10
- type DB = Db<Record<string, unknown>>;
10
+ type DB = RepositoryDb<Record<string, unknown>>;
11
11
 
12
12
  function rowToOccurrence(
13
13
  row: typeof calendarEventIndexTable.$inferSelect,
@@ -5,10 +5,10 @@ import type {
5
5
  } from "@remit/data-ports";
6
6
  import { deriveCalendarFeedTokenId } from "@remit/data-ports/id";
7
7
  import { and, eq } from "drizzle-orm";
8
- import type { Db } from "../db.js";
8
+ import type { RepositoryDb } from "../db.js";
9
9
  import { calendarFeedTokenTable } from "../schema.js";
10
10
 
11
- type DB = Db<Record<string, unknown>>;
11
+ type DB = RepositoryDb<Record<string, unknown>>;
12
12
 
13
13
  function rowToFeedToken(
14
14
  row: typeof calendarFeedTokenTable.$inferSelect,
@@ -5,11 +5,11 @@ import type {
5
5
  } from "@remit/data-ports";
6
6
  import { deriveCalendarObjectId } from "@remit/data-ports/id";
7
7
  import { and, asc, eq, gt, gte, lt, ne } from "drizzle-orm";
8
- import type { Db } from "../db.js";
8
+ import type { RepositoryDb } from "../db.js";
9
9
  import { NotFoundError } from "../error.js";
10
10
  import { calendarObjectTable } from "../schema.js";
11
11
 
12
- type DB = Db<Record<string, unknown>>;
12
+ type DB = RepositoryDb<Record<string, unknown>>;
13
13
 
14
14
  function rowToCalendarObject(
15
15
  row: typeof calendarObjectTable.$inferSelect,
@@ -16,8 +16,8 @@ import {
16
16
  calendarObjectTable,
17
17
  calendarTable,
18
18
  } from "../schema.js";
19
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
19
20
  import { createSqliteTestDb } from "../test-db-sqlite.js";
20
- import { DrizzleCalendarStore } from "./calendar-store.js";
21
21
 
22
22
  const ACCOUNT_CONFIG_ID = "account-config-1";
23
23
 
@@ -77,7 +77,11 @@ describe("the calendar write path against sqlite", () => {
77
77
  });
78
78
  db = created.db as unknown as Db<Record<string, unknown>>;
79
79
  close = created.close;
80
- store = new DrizzleCalendarStore(db, db);
80
+ store = createSqliteRepositories({
81
+ app: db,
82
+ mail: db,
83
+ search: db,
84
+ }).calendarStore;
81
85
  const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
82
86
  calendarId = collection.calendarId;
83
87
  });
@@ -2,56 +2,67 @@ import type {
2
2
  CalendarRepositories,
3
3
  CalendarWrite,
4
4
  ICalendarStore,
5
+ ICalendarSuggestionRepository,
6
+ SettleCalendarSuggestionWrite,
5
7
  } from "@remit/data-ports";
6
8
  import { deriveCalendarObjectId } from "@remit/data-ports/id";
7
9
  import { and, eq, exists, sql } from "drizzle-orm";
8
- import { runBatch, type SqliteBatchItem } from "../batch.js";
9
- import type { Db } from "../db.js";
10
+ import type { SqliteBatchItem, WriteBatch } from "../batch.js";
11
+ import type { RepositoryDb, SqliteStore } from "../db.js";
10
12
  import {
11
13
  calendarEventIndexTable,
12
14
  calendarFeedTokenTable,
13
15
  calendarObjectTable,
14
- calendarSuggestionTable,
15
16
  calendarTable,
16
17
  } from "../schema.js";
17
18
  import { CalendarCollectionRepo } from "./calendar-collection.js";
18
19
  import { CalendarEventIndexRepo } from "./calendar-event-index.js";
19
20
  import { CalendarFeedTokenRepo } from "./calendar-feed-token.js";
20
21
  import { CalendarObjectRepo } from "./calendar-object.js";
21
- import { CalendarSuggestionRepo } from "./calendar-suggestion.js";
22
22
 
23
- type DB = Db<Record<string, unknown>>;
23
+ type DB = RepositoryDb<Record<string, unknown>>;
24
+
25
+ type CalendarBatchWrite = Exclude<CalendarWrite, SettleCalendarSuggestionWrite>;
26
+
27
+ const isSettlement = (
28
+ write: CalendarWrite,
29
+ ): write is SettleCalendarSuggestionWrite => write.kind === "SettleSuggestion";
30
+
31
+ const isBatchWrite = (write: CalendarWrite): write is CalendarBatchWrite =>
32
+ !isSettlement(write);
24
33
 
25
34
  export class DrizzleCalendarStore implements ICalendarStore {
26
35
  readonly repos: CalendarRepositories;
27
36
 
37
+ private readonly db: DB;
38
+ private readonly batch: WriteBatch;
39
+
28
40
  constructor(
29
- private db: DB,
30
- private mail: DB,
41
+ store: SqliteStore<Record<string, unknown>>,
42
+ calendarSuggestion: ICalendarSuggestionRepository,
31
43
  ) {
44
+ this.db = store.db;
45
+ this.batch = store.batch;
32
46
  this.repos = {
33
- calendarCollection: new CalendarCollectionRepo(db),
34
- calendarObject: new CalendarObjectRepo(db),
35
- calendarEventIndex: new CalendarEventIndexRepo(db),
36
- calendarSuggestion: new CalendarSuggestionRepo(mail),
37
- calendarFeedToken: new CalendarFeedTokenRepo(db),
47
+ calendarCollection: new CalendarCollectionRepo(store.db),
48
+ calendarObject: new CalendarObjectRepo(store.db),
49
+ calendarEventIndex: new CalendarEventIndexRepo(store.db),
50
+ calendarSuggestion,
51
+ calendarFeedToken: new CalendarFeedTokenRepo(store.db),
38
52
  };
39
53
  }
40
54
 
41
55
  async commit(writes: readonly CalendarWrite[]): Promise<void> {
42
- const settlements = writes.filter(
43
- (write) => write.kind === "SettleSuggestion",
44
- );
45
- await runBatch(
46
- this.db,
47
- writes
48
- .filter((write) => write.kind !== "SettleSuggestion")
49
- .flatMap((write) => this.statements(write)),
50
- );
51
- await runBatch(
52
- this.mail,
53
- settlements.flatMap((write) => this.statements(write)),
56
+ await this.batch(
57
+ writes.filter(isBatchWrite).flatMap((write) => this.statements(write)),
54
58
  );
59
+ for (const write of writes.filter(isSettlement)) {
60
+ await this.repos.calendarSuggestion.settle(
61
+ write.accountConfigId,
62
+ write.suggestionId,
63
+ write.settle,
64
+ );
65
+ }
55
66
  }
56
67
 
57
68
  private collection(accountConfigId: string, calendarId: string) {
@@ -97,7 +108,7 @@ export class DrizzleCalendarStore implements ICalendarStore {
97
108
  );
98
109
  }
99
110
 
100
- private statements(write: CalendarWrite): SqliteBatchItem[] {
111
+ private statements(write: CalendarBatchWrite): SqliteBatchItem[] {
101
112
  const now = Date.now();
102
113
  switch (write.kind) {
103
114
  case "PutObject": {
@@ -223,21 +234,6 @@ export class DrizzleCalendarStore implements ICalendarStore {
223
234
  ),
224
235
  this.bump(write.accountConfigId, write.calendarId),
225
236
  ];
226
- case "SettleSuggestion":
227
- return [
228
- this.mail
229
- .update(calendarSuggestionTable)
230
- .set({ ...write.settle, updatedAt: now })
231
- .where(
232
- and(
233
- eq(
234
- calendarSuggestionTable.accountConfigId,
235
- write.accountConfigId,
236
- ),
237
- eq(calendarSuggestionTable.suggestionId, write.suggestionId),
238
- ),
239
- ),
240
- ];
241
237
  case "UpdateCollection":
242
238
  return [
243
239
  this.db
@@ -8,13 +8,13 @@ import type {
8
8
  import { deriveCalendarSuggestionId } from "@remit/data-ports/id";
9
9
  import { CalendarSuggestionState } from "@remit/domain-enums";
10
10
  import { and, asc, desc, eq, gte, lt, or } from "drizzle-orm";
11
- import type { Db } from "../db.js";
11
+ import type { RepositoryDb } from "../db.js";
12
12
  import { NotFoundError } from "../error.js";
13
13
  import { decodeToken, resultList } from "../pagination.js";
14
14
  import { calendarSuggestionTable } from "../schema.js";
15
15
  import { inList } from "./in-list.js";
16
16
 
17
- type DB = Db<Record<string, unknown>>;
17
+ type DB = RepositoryDb<Record<string, unknown>>;
18
18
 
19
19
  function rowToCalendarSuggestion(
20
20
  row: typeof calendarSuggestionTable.$inferSelect,
@@ -2,6 +2,7 @@ import assert from "node:assert";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import { MailboxCursorState } from "@remit/domain-enums";
4
4
  import { eq } from "drizzle-orm";
5
+ import { syncStores } from "../databases.js";
5
6
  import {
6
7
  accountSettingTable,
7
8
  accountTable,
@@ -299,7 +300,7 @@ describe("runDrizzleCascadeDelete", () => {
299
300
 
300
301
  test("deletes every enumerated row across all tables", async () => {
301
302
  await runDrizzleCascadeDelete(
302
- { app: db, mail: db, search: db } as never,
303
+ syncStores({ app: db, mail: db, search: db } as never),
303
304
  entities,
304
305
  log,
305
306
  );
@@ -438,7 +439,7 @@ describe("runDrizzleCascadeDelete", () => {
438
439
  await assert.rejects(
439
440
  () =>
440
441
  runDrizzleCascadeDelete(
441
- { app: db, mail: db, search: db } as never,
442
+ syncStores({ app: db, mail: db, search: db } as never),
442
443
  [{ entityType: "Nonsense", key: { id: "x" } }],
443
444
  log,
444
445
  ),
@@ -1,8 +1,9 @@
1
- import { runBatch, type SqliteBatchItem } from "../batch.js";
1
+ import type { SqliteBatchItem } from "../batch.js";
2
2
  import {
3
3
  openSqliteDatabases,
4
4
  type SqliteDatabasePaths,
5
- type SqliteDatabases,
5
+ type SqliteStores,
6
+ syncStores,
6
7
  } from "../databases.js";
7
8
  import { accountTable } from "../schema/i4-account-config.js";
8
9
  import { accountSettingTable } from "../schema/i4-account-setting.js";
@@ -82,10 +83,12 @@ const groupByType = (
82
83
  * cascade-in-progress marker.
83
84
  */
84
85
  export const runDrizzleCascadeDelete = async (
85
- { app, mail, search }: SqliteDatabases,
86
+ stores: SqliteStores,
86
87
  entities: CascadeEntity[],
87
88
  log: CascadeDeleteLogger,
88
89
  ): Promise<void> => {
90
+ const app = stores.app.db;
91
+ const mail = stores.mail.db;
89
92
  const grouped = groupByType(entities);
90
93
 
91
94
  for (const entityType of grouped.keys()) {
@@ -217,9 +220,9 @@ export const runDrizzleCascadeDelete = async (
217
220
  ),
218
221
  );
219
222
  }
220
- await runBatch(mail, items);
221
- await runBatch(app, appItems);
222
- await new SearchIndex(mail, search).sync(threadMessageIds);
223
+ await stores.mail.batch(items);
224
+ await stores.app.batch(appItems);
225
+ await new SearchIndex(mail, stores.search).sync(threadMessageIds);
223
226
 
224
227
  log.info({ entityCount: entities.length }, "Drizzle cascade delete complete");
225
228
  };
@@ -238,5 +241,6 @@ export const createSqliteCascadeDeleter = async (
238
241
  paths: SqliteDatabasePaths,
239
242
  ): Promise<CascadeDeleter> => {
240
243
  const databases = await openSqliteDatabases(paths);
241
- return (entities, log) => runDrizzleCascadeDelete(databases, entities, log);
244
+ const stores = syncStores(databases);
245
+ return (entities, log) => runDrizzleCascadeDelete(stores, entities, log);
242
246
  };
@@ -4,8 +4,9 @@ import {
4
4
  type MessageDataSchema,
5
5
  messageDataSchema,
6
6
  } from "../schema/message-data.js";
7
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
7
8
  import { createSqliteTestDb, type SqliteTestDb } from "../test-db-sqlite.js";
8
- import { DrizzleEnvelopeRepository } from "./envelope.js";
9
+ import type { DrizzleEnvelopeRepository } from "./envelope.js";
9
10
 
10
11
  // Envelope repo on sqlite (RFC 036 D1): the onConflictDoUpdate upsert path and
11
12
  // the transactional body-part upsert both run on better-sqlite3 unchanged.
@@ -19,7 +20,7 @@ describe("DrizzleEnvelopeRepository (sqlite)", () => {
19
20
 
20
21
  before(async () => {
21
22
  ({ db, close } = await createSqliteTestDb(messageDataSchema));
22
- repo = new DrizzleEnvelopeRepository(db);
23
+ repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
23
24
  });
24
25
 
25
26
  after(async () => {
@@ -2,7 +2,8 @@ import assert from "node:assert/strict";
2
2
  import { after, before, describe, test } from "node:test";
3
3
  import type { BodyPartUpsertInput } from "@remit/data-ports";
4
4
  import { envelopeId as deriveEnvelopeId } from "../id.js";
5
- import { DrizzleEnvelopeRepository } from "./envelope.js";
5
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
6
+ import type { DrizzleEnvelopeRepository } from "./envelope.js";
6
7
  import { createTestDb, type TestDb } from "./test-helpers.js";
7
8
 
8
9
  describe("DrizzleEnvelopeRepository", () => {
@@ -18,7 +19,7 @@ describe("DrizzleEnvelopeRepository", () => {
18
19
 
19
20
  before(async () => {
20
21
  ({ db, stop } = await createTestDb());
21
- repo = new DrizzleEnvelopeRepository(db);
22
+ repo = createSqliteRepositories({ app: db, mail: db, search: db }).envelope;
22
23
 
23
24
  await repo.upsertEnvelope({
24
25
  envelopeId: ENVELOPE_ID,