@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
@@ -9,8 +9,8 @@ import type {
9
9
  UpdateEnvelopeInput,
10
10
  } from "@remit/data-ports";
11
11
  import { eq } from "drizzle-orm";
12
- import { runBatch } from "../batch.js";
13
- import type { Db } from "../db.js";
12
+ import type { WriteBatch } from "../batch.js";
13
+ import type { RepositoryDb, SqliteStore } from "../db.js";
14
14
  import {
15
15
  CreateFailedConflictError,
16
16
  isUniqueViolation,
@@ -29,7 +29,6 @@ import {
29
29
  bodyStructureRefetchTable,
30
30
  envelopeAddressTable,
31
31
  envelopeTable,
32
- type MessageDataSchema,
33
32
  messageReferenceTable,
34
33
  messageTable,
35
34
  rawMessageStorageTable,
@@ -45,7 +44,7 @@ import {
45
44
  toRawMessageStorageItem,
46
45
  } from "./mappers.js";
47
46
 
48
- type DB = Db<MessageDataSchema>;
47
+ type DB = RepositoryDb<Record<string, unknown>>;
49
48
 
50
49
  function dedupeById<T>(rows: T[], getId: (row: T) => string): T[] {
51
50
  const byId = new Map<string, T>();
@@ -56,7 +55,13 @@ function dedupeById<T>(rows: T[], getId: (row: T) => string): T[] {
56
55
  }
57
56
 
58
57
  export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
59
- constructor(private db: DB) {}
58
+ private readonly db: DB;
59
+ private readonly batch: WriteBatch;
60
+
61
+ constructor(store: SqliteStore<Record<string, unknown>>) {
62
+ this.db = store.db;
63
+ this.batch = store.batch;
64
+ }
60
65
 
61
66
  async getMessageData(messageId: string): Promise<MessageData> {
62
67
  const [
@@ -183,10 +188,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
183
188
  set: { updatedAt: now },
184
189
  });
185
190
  if (dedupedParameterRows.length === 0) {
186
- await runBatch(this.db, [insertParts]);
191
+ await this.batch([insertParts]);
187
192
  return;
188
193
  }
189
- await runBatch(this.db, [
194
+ await this.batch([
190
195
  insertParts,
191
196
  this.db
192
197
  .insert(bodyPartParameterTable)
@@ -211,10 +216,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
211
216
  .delete(messageReferenceTable)
212
217
  .where(eq(messageReferenceTable.messageId, messageId));
213
218
  if (references.length === 0) {
214
- await runBatch(this.db, [clear]);
219
+ await this.batch([clear]);
215
220
  return;
216
221
  }
217
- await runBatch(this.db, [
222
+ await this.batch([
218
223
  clear,
219
224
  this.db.insert(messageReferenceTable).values(
220
225
  references.map((reference) => ({
@@ -12,13 +12,13 @@ import {
12
12
  FilterState,
13
13
  } from "@remit/domain-enums";
14
14
  import { and, asc, eq, gt, or } from "drizzle-orm";
15
- import type { Db } from "../db.js";
15
+ import type { RepositoryDb } from "../db.js";
16
16
  import { NotFoundError } from "../error.js";
17
17
  import { randomId } from "../id.js";
18
18
  import { decodeToken, resultList } from "../pagination.js";
19
19
  import { filterTable } from "../schema.js";
20
20
 
21
- type DB = Db<Record<string, unknown>>;
21
+ type DB = RepositoryDb<Record<string, unknown>>;
22
22
 
23
23
  const RULE_ASSERTION_FIELDS = [
24
24
  "matchOperator",
@@ -1,9 +1,8 @@
1
1
  import assert from "node:assert";
2
2
  import { after, before, describe, test } from "node:test";
3
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
3
4
  import { createTestDb, randomId, type TestDb } from "../test-db.js";
4
- import { AccountRepo } from "./i4-account.js";
5
- import { AccountConfigRepo } from "./i4-account-config.js";
6
- import { AddressRepo } from "./i4-address.js";
5
+ import type { AccountConfigRepo } from "./i4-account-config.js";
7
6
 
8
7
  describe("AccountConfigRepo", () => {
9
8
  let db: TestDb;
@@ -12,7 +11,11 @@ describe("AccountConfigRepo", () => {
12
11
 
13
12
  before(async () => {
14
13
  ({ db, close } = await createTestDb());
15
- repo = new AccountConfigRepo(db as never, db as never);
14
+ repo = createSqliteRepositories({
15
+ app: db,
16
+ mail: db,
17
+ search: db,
18
+ }).accountConfig;
16
19
  });
17
20
 
18
21
  after(async () => {
@@ -58,8 +61,9 @@ describe("AccountConfigRepo", () => {
58
61
  const userId = randomId();
59
62
  const cfg = await repo.create({ userId });
60
63
 
61
- const accountRepo = new AccountRepo(db as never, db as never);
62
- const addrRepo = new AddressRepo(db as never);
64
+ const repos = createSqliteRepositories({ app: db, mail: db, search: db });
65
+ const accountRepo = repos.account;
66
+ const addrRepo = repos.address;
63
67
 
64
68
  await accountRepo.create({
65
69
  accountConfigId: cfg.accountConfigId,
@@ -3,11 +3,12 @@ import type {
3
3
  AccountConfigItem,
4
4
  CreateAccountConfigInput,
5
5
  IAccountConfigRepository,
6
+ IAddressRepository,
6
7
  ResultList,
7
8
  UpdateAccountConfigInput,
8
9
  } from "@remit/data-ports";
9
10
  import { and, asc, eq, gt, or } from "drizzle-orm";
10
- import type { Db } from "../db.js";
11
+ import type { RepositoryDb } from "../db.js";
11
12
  import { NotFoundError } from "../error.js";
12
13
  import { randomId } from "../id.js";
13
14
  import { decodeToken, resultList } from "../pagination.js";
@@ -15,12 +16,12 @@ import {
15
16
  accountConfigTable,
16
17
  accountTable,
17
18
  } from "../schema/i4-account-config.js";
18
- import { addressTable } from "../schema/i4-address.js";
19
19
  import { rowToAccount } from "./i4-account.js";
20
- import { rowToAddress } from "./i4-address.js";
21
20
  import { inList } from "./in-list.js";
22
21
 
23
- type DB = Db<Record<string, unknown>>;
22
+ type DB = RepositoryDb<Record<string, unknown>>;
23
+
24
+ type AddressReader = Pick<IAddressRepository, "listAllByAccountConfig">;
24
25
 
25
26
  function rowToAccountConfig(
26
27
  row: typeof accountConfigTable.$inferSelect,
@@ -40,7 +41,7 @@ function rowToAccountConfig(
40
41
  export class AccountConfigRepo implements IAccountConfigRepository {
41
42
  constructor(
42
43
  private db: DB,
43
- private mail: DB,
44
+ private addresses: AddressReader,
44
45
  ) {}
45
46
 
46
47
  async create(input: CreateAccountConfigInput): Promise<AccountConfigItem> {
@@ -183,10 +184,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
183
184
  .select()
184
185
  .from(accountTable)
185
186
  .where(eq(accountTable.accountConfigId, accountConfigId)),
186
- this.mail
187
- .select()
188
- .from(addressTable)
189
- .where(eq(addressTable.accountConfigId, accountConfigId)),
187
+ this.addresses.listAllByAccountConfig(accountConfigId),
190
188
  ]);
191
189
  if (configs.length === 0) {
192
190
  throw new NotFoundError(`AccountConfig not found: ${accountConfigId}`);
@@ -194,7 +192,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
194
192
  return {
195
193
  accountConfig: configs.map(rowToAccountConfig),
196
194
  account: accounts.map(rowToAccount),
197
- address: addresses.map(rowToAddress),
195
+ address: addresses,
198
196
  };
199
197
  }
200
198
 
@@ -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 { Db } from "../db.js";
9
+ import type { RepositoryDb } 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 = Db<Record<string, unknown>>;
15
+ type DB = RepositoryDb<Record<string, unknown>>;
16
16
 
17
17
  function rowToItem(
18
18
  row: typeof accountExportRequestTable.$inferSelect,
@@ -6,11 +6,11 @@ import type {
6
6
  UpsertAccountSettingInput,
7
7
  } from "@remit/data-ports";
8
8
  import { and, eq } from "drizzle-orm";
9
- import type { Db } from "../db.js";
9
+ import type { RepositoryDb } from "../db.js";
10
10
  import { accountSettingTable } from "../schema/i4-account-setting.js";
11
11
  import { inList } from "./in-list.js";
12
12
 
13
- type DB = Db<Record<string, unknown>>;
13
+ type DB = RepositoryDb<Record<string, unknown>>;
14
14
 
15
15
  // The PG store derives its own deterministic id from the composite key. It is
16
16
  // stable within this backend but not byte-identical to the DDB base36 UUIDv5
@@ -1,9 +1,8 @@
1
1
  import assert from "node:assert";
2
2
  import { after, before, describe, test } from "node:test";
3
- import { SearchIndex } from "../search/thread-message-search.js";
3
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
4
4
  import { createTestDb, randomId, type TestDb } from "../test-db.js";
5
- import { AccountRepo } from "./i4-account.js";
6
- import { MailboxRepo } from "./i4-mailbox.js";
5
+ import type { AccountRepo } from "./i4-account.js";
7
6
  import { mailboxCreated } from "./test-helpers.js";
8
7
 
9
8
  function makeAccountInput(accountConfigId: string) {
@@ -27,7 +26,11 @@ describe("AccountRepo", () => {
27
26
 
28
27
  before(async () => {
29
28
  ({ db, close } = await createTestDb());
30
- repo = new AccountRepo(db as never, db as never);
29
+ repo = createSqliteRepositories({
30
+ app: db,
31
+ mail: db,
32
+ search: db,
33
+ }).account;
31
34
  });
32
35
 
33
36
  after(async () => {
@@ -161,10 +164,11 @@ describe("AccountRepo", () => {
161
164
  const accountConfigId = randomId();
162
165
  const account = await repo.create(makeAccountInput(accountConfigId));
163
166
 
164
- const mailboxRepo = new MailboxRepo(
165
- db as never,
166
- new SearchIndex(db as never, db as never),
167
- );
167
+ const mailboxRepo = createSqliteRepositories({
168
+ app: db,
169
+ mail: db,
170
+ search: db,
171
+ }).mailbox;
168
172
  mailboxCreated(
169
173
  await mailboxRepo.create({
170
174
  accountId: account.accountId,
@@ -4,22 +4,21 @@ import type {
4
4
  AccountSchedulerPage,
5
5
  CreateAccountInput,
6
6
  IAccountRepository,
7
+ IMailboxRepository,
7
8
  RemovableAccountField,
8
9
  ResultList,
9
10
  UpdateAccountInput,
10
11
  } from "@remit/data-ports";
11
12
  import { AccountService, SyncPhase } from "@remit/domain-enums";
12
13
  import { and, asc, eq, gt, like, or, sql } from "drizzle-orm";
13
- import type { Db } from "../db.js";
14
+ import type { RepositoryDb } from "../db.js";
14
15
  import { NotFoundError } from "../error.js";
15
16
  import { randomId } from "../id.js";
16
17
  import { decodeToken, encodeToken, resultList } from "../pagination.js";
17
18
  import { accountTable } from "../schema/i4-account-config.js";
18
- import { mailboxTable } from "../schema/i4-mailbox.js";
19
- import { rowToMailbox } from "./i4-mailbox.js";
20
19
  import { inList } from "./in-list.js";
21
20
 
22
- type DB = Db<Record<string, unknown>>;
21
+ type DB = RepositoryDb<Record<string, unknown>>;
23
22
 
24
23
  /**
25
24
  * A stored failure is a sentence the account card shows, not a transcript.
@@ -74,7 +73,7 @@ export function rowToAccount(
74
73
  export class AccountRepo implements IAccountRepository {
75
74
  constructor(
76
75
  private db: DB,
77
- private mail: DB,
76
+ private mailboxes: Pick<IMailboxRepository, "listAllByAccount">,
78
77
  ) {}
79
78
 
80
79
  async create(input: CreateAccountInput): Promise<AccountItem> {
@@ -269,17 +268,14 @@ export class AccountRepo implements IAccountRepository {
269
268
  .select()
270
269
  .from(accountTable)
271
270
  .where(eq(accountTable.accountId, accountId)),
272
- this.mail
273
- .select()
274
- .from(mailboxTable)
275
- .where(eq(mailboxTable.accountId, accountId)),
271
+ this.mailboxes.listAllByAccount(accountId),
276
272
  ]);
277
273
  if (accounts.length === 0) {
278
274
  throw new NotFoundError(`Account not found: ${accountId}`);
279
275
  }
280
276
  return {
281
277
  account: accounts.map(rowToAccount),
282
- mailbox: mailboxes.map(rowToMailbox),
278
+ mailbox: mailboxes,
283
279
  };
284
280
  }
285
281
 
@@ -1,9 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { after, before, beforeEach, describe, test } from "node:test";
3
3
  import type Database from "better-sqlite3";
4
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
4
5
  import { createTestDb, type TestDb } from "../test-db.js";
5
6
  import { AddressRepo } from "./i4-address.js";
6
- import { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
7
+ import type { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
7
8
 
8
9
  const CONFIG = "cfg-1";
9
10
 
@@ -124,7 +125,11 @@ describe("reconciling one message's addresses at the moment it moves", () => {
124
125
  before(async () => {
125
126
  ({ db, sqlite, close } = await createTestDb());
126
127
  repo = new AddressRepo(db as never);
127
- specialUse = new MailboxSpecialUseRepo(db as never, db as never);
128
+ specialUse = createSqliteRepositories({
129
+ app: db,
130
+ mail: db,
131
+ search: db,
132
+ }).mailboxSpecialUse;
128
133
  });
129
134
 
130
135
  /**
@@ -22,7 +22,7 @@ import {
22
22
  type SQL,
23
23
  sql,
24
24
  } from "drizzle-orm";
25
- import type { Db } from "../db.js";
25
+ import type { RepositoryDb } from "../db.js";
26
26
  import { NotFoundError } from "../error.js";
27
27
  import { envelopeAddressId } from "../id.js";
28
28
  import { decodeToken, resultList } from "../pagination.js";
@@ -44,7 +44,7 @@ import {
44
44
  } from "./address-search-predicates.js";
45
45
  import { inList } from "./in-list.js";
46
46
 
47
- type DB = Db<Record<string, unknown>>;
47
+ type DB = RepositoryDb<Record<string, unknown>>;
48
48
 
49
49
  type AddressUpdate = Partial<{
50
50
  [K in keyof typeof addressTable.$inferInsert]:
@@ -786,6 +786,16 @@ export class AddressRepo implements IAddressRepository {
786
786
  );
787
787
  }
788
788
 
789
+ async listAllByAccountConfig(
790
+ accountConfigId: string,
791
+ ): Promise<AddressItem[]> {
792
+ const rows = await this.db
793
+ .select()
794
+ .from(addressTable)
795
+ .where(eq(addressTable.accountConfigId, accountConfigId));
796
+ return rows.map(rowToAddress);
797
+ }
798
+
789
799
  async createEnvelopeAddress(
790
800
  input: CreateEnvelopeAddressInput,
791
801
  ): Promise<EnvelopeAddressItem> {
@@ -7,12 +7,12 @@ import {
7
7
  type UpdateConfigImportInput,
8
8
  } from "@remit/data-ports";
9
9
  import { desc, eq } from "drizzle-orm";
10
- import type { Db } from "../db.js";
10
+ import type { RepositoryDb } from "../db.js";
11
11
  import { NotFoundError } from "../error.js";
12
12
  import { randomId } from "../id.js";
13
13
  import { configImportTable } from "../schema/i4-config-import.js";
14
14
 
15
- type DB = Db<Record<string, unknown>>;
15
+ type DB = RepositoryDb<Record<string, unknown>>;
16
16
 
17
17
  type StoredRef = Omit<ConfigImportUnresolvedRefItem, "mailboxId"> & {
18
18
  mailboxId?: string;
@@ -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 { Db } from "../db.js";
7
+ import type { RepositoryDb } 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 = Db<Record<string, unknown>>;
11
+ type DB = RepositoryDb<Record<string, unknown>>;
12
12
 
13
13
  const TTL_SECONDS = 3600;
14
14
 
@@ -12,11 +12,11 @@ import {
12
12
  mailboxSpecialUseTable,
13
13
  mailboxTable,
14
14
  } from "../schema.js";
15
- import { SearchIndex } from "../search/thread-message-search.js";
15
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
16
16
  import { createSqliteTestDb } from "../test-db-sqlite.js";
17
- import { AccountSettingRepo } from "./i4-account-setting.js";
18
- import { MailboxRepo } from "./i4-mailbox.js";
19
- import { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
17
+ import type { AccountSettingRepo } from "./i4-account-setting.js";
18
+ import type { MailboxRepo } from "./i4-mailbox.js";
19
+ import type { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
20
20
  import { mailboxCreated } from "./test-helpers.js";
21
21
 
22
22
  const makeMailboxInput = (
@@ -57,12 +57,14 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
57
57
  });
58
58
  close = testDb.close;
59
59
  db = testDb.db;
60
- repo = new MailboxSpecialUseRepo(testDb.db as never, testDb.db as never);
61
- mailboxes = new MailboxRepo(
62
- testDb.db as never,
63
- new SearchIndex(testDb.db as never, testDb.db as never),
64
- );
65
- accountSettings = new AccountSettingRepo(testDb.db as never);
60
+ const repos = createSqliteRepositories({
61
+ app: testDb.db,
62
+ mail: testDb.db,
63
+ search: testDb.db,
64
+ });
65
+ repo = repos.mailboxSpecialUse;
66
+ mailboxes = repos.mailbox;
67
+ accountSettings = repos.accountSetting;
66
68
  });
67
69
 
68
70
  after(async () => {
@@ -1,4 +1,7 @@
1
1
  import type {
2
+ AccountItem,
3
+ IAccountRepository,
4
+ IAccountSettingRepository,
2
5
  IMailboxSpecialUseRepository,
3
6
  MailboxSpecialUseItem,
4
7
  MailboxSpecialUseValue,
@@ -15,14 +18,21 @@ import {
15
18
  } from "@remit/data-ports/folder-role";
16
19
  import { CanonicalMailboxRole } from "@remit/domain-enums";
17
20
  import { eq } from "drizzle-orm";
18
- import type { Db } from "../db.js";
21
+ import type { RepositoryDb } from "../db.js";
19
22
  import { randomId } from "../id.js";
20
- import { accountTable } from "../schema/i4-account-config.js";
21
23
  import { mailboxSpecialUseTable, mailboxTable } from "../schema/i4-mailbox.js";
22
- import { AccountSettingRepo } from "./i4-account-setting.js";
23
24
  import { inList } from "./in-list.js";
24
25
 
25
- type DB = Db<Record<string, unknown>>;
26
+ type DB = RepositoryDb<Record<string, unknown>>;
27
+
28
+ type AccountReader = Pick<
29
+ IAccountRepository,
30
+ "get" | "listAllByAccountConfig" | "listAll"
31
+ >;
32
+
33
+ type AppointmentReader = Pick<IAccountSettingRepository, "get" | "getMany">;
34
+
35
+ type AppointedAccount = Pick<AccountItem, "accountId" | "accountConfigId">;
26
36
 
27
37
  const JUNK_ROLES: readonly CanonicalMailboxRoleValue[] = [
28
38
  CanonicalMailboxRole.Junk,
@@ -89,14 +99,11 @@ function rowToSpecialUse(
89
99
  }
90
100
 
91
101
  export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
92
- private readonly accountSetting: AccountSettingRepo;
93
-
94
102
  constructor(
95
- private app: DB,
96
103
  private db: DB,
97
- ) {
98
- this.accountSetting = new AccountSettingRepo(app);
99
- }
104
+ private accounts: AccountReader,
105
+ private accountSetting: AppointmentReader,
106
+ ) {}
100
107
 
101
108
  async create(
102
109
  mailboxId: string,
@@ -203,11 +210,9 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
203
210
  async resolveJunkRolesForConfig(
204
211
  accountConfigId: string,
205
212
  ): Promise<JunkRoleMailboxes> {
206
- const accounts = await this.app
207
- .select({ accountId: accountTable.accountId })
208
- .from(accountTable)
209
- .where(eq(accountTable.accountConfigId, accountConfigId));
210
- return this.resolveJunkRoles(accounts.map((row) => row.accountId));
213
+ return this.resolveJunkRoles(
214
+ await this.accounts.listAllByAccountConfig(accountConfigId),
215
+ );
211
216
  }
212
217
 
213
218
  /**
@@ -217,21 +222,19 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
217
222
  * run one pass over the address table instead of one per config.
218
223
  */
219
224
  async resolveJunkRolesForInstance(): Promise<JunkRoleMailboxes> {
220
- const accounts = await this.app
221
- .select({ accountId: accountTable.accountId })
222
- .from(accountTable);
223
- return this.resolveJunkRoles(accounts.map((row) => row.accountId));
225
+ return this.resolveJunkRoles(await this.accounts.listAll());
224
226
  }
225
227
 
226
228
  private async resolveJunkRoles(
227
- accountIds: readonly string[],
229
+ accounts: readonly AppointedAccount[],
228
230
  ): Promise<JunkRoleMailboxes> {
229
- if (accountIds.length === 0) {
231
+ if (accounts.length === 0) {
230
232
  return { junkMailboxIds: [], trashMailboxIds: [] };
231
233
  }
234
+ const accountIds = accounts.map((account) => account.accountId);
232
235
  const [candidates, appointments] = await Promise.all([
233
236
  this.roleCandidatesFor(accountIds),
234
- this.appointedMailboxIds(accountIds, JUNK_ROLES),
237
+ this.appointedMailboxIds(accounts, JUNK_ROLES),
235
238
  ]);
236
239
 
237
240
  const junkMailboxIds: string[] = [];
@@ -255,24 +258,13 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
255
258
  }
256
259
 
257
260
  /**
258
- * Each account's appointment for each role, in two reads. Same precedence
259
- * input as `appointedMailboxId`, batched: an account row missing is a caller
260
- * racing a delete, and leaves the account with no appointment rather than
261
- * failing the lookup.
261
+ * Each account's appointment for each role, in one read. Same precedence
262
+ * input as `appointedMailboxId`, batched.
262
263
  */
263
264
  private async appointedMailboxIds(
264
- accountIds: readonly string[],
265
+ accounts: readonly AppointedAccount[],
265
266
  roles: readonly CanonicalMailboxRoleValue[],
266
267
  ): Promise<Map<string, string>> {
267
- const accounts = await this.app
268
- .select({
269
- accountId: accountTable.accountId,
270
- accountConfigId: accountTable.accountConfigId,
271
- })
272
- .from(accountTable)
273
- .where(inList(accountTable.accountId, accountIds));
274
- if (accounts.length === 0) return new Map();
275
-
276
268
  const wanted = new Map<string, string>();
277
269
  for (const account of accounts) {
278
270
  for (const role of roles) {
@@ -367,10 +359,7 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
367
359
  accountId: string,
368
360
  role: CanonicalMailboxRoleValue,
369
361
  ): Promise<string | undefined> {
370
- const [account] = await this.app
371
- .select({ accountConfigId: accountTable.accountConfigId })
372
- .from(accountTable)
373
- .where(eq(accountTable.accountId, accountId));
362
+ const [account] = await this.accounts.get([accountId]);
374
363
  if (!account) return undefined;
375
364
 
376
365
  const setting = await this.accountSetting.get(
@@ -5,13 +5,13 @@ import { MailboxSyncStatus } from "@remit/domain-enums";
5
5
  import Database from "better-sqlite3";
6
6
  import { drizzle } from "drizzle-orm/better-sqlite3";
7
7
  import { mailboxTable } from "../schema.js";
8
- import { SearchIndex } from "../search/thread-message-search.js";
8
+ import { createSqliteRepositories } from "../sqlite-repositories.js";
9
9
  import { createSqliteTestDb } from "../test-db-sqlite.js";
10
10
  import {
11
11
  applyMigrationSet,
12
12
  shippedTableDdl,
13
13
  } from "../test-shipped-sqlite-schema.js";
14
- import { MailboxRepo } from "./i4-mailbox.js";
14
+ import type { MailboxRepo } from "./i4-mailbox.js";
15
15
  import { mailboxCreated } from "./test-helpers.js";
16
16
 
17
17
  function makeMailboxInput(accountId: string, fullPath = "INBOX") {
@@ -41,10 +41,11 @@ describe("MailboxRepo (sqlite)", () => {
41
41
  before(async () => {
42
42
  const testDb = await createSqliteTestDb({ mailbox: mailboxTable });
43
43
  close = testDb.close;
44
- repo = new MailboxRepo(
45
- testDb.db as never,
46
- new SearchIndex(testDb.db as never, testDb.db as never),
47
- );
44
+ repo = createSqliteRepositories({
45
+ app: testDb.db,
46
+ mail: testDb.db,
47
+ search: testDb.db,
48
+ }).mailbox;
48
49
  });
49
50
 
50
51
  after(async () => {
@@ -210,10 +211,11 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
210
211
  const sqlite = new Database(":memory:");
211
212
  applyMigrationSet(sqlite, "mail");
212
213
  const db = drizzle(sqlite, { schema: { mailbox: mailboxTable } });
213
- repo = new MailboxRepo(
214
- db as never,
215
- new SearchIndex(db as never, db as never),
216
- );
214
+ repo = createSqliteRepositories({
215
+ app: db,
216
+ mail: db,
217
+ search: db,
218
+ }).mailbox;
217
219
  close = async () => {
218
220
  sqlite.close();
219
221
  };