@remit/drizzle-service 0.0.101 → 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 (62) 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 +5 -4
  17. package/src/repos/cascade-delete.test.ts +3 -2
  18. package/src/repos/cascade-delete.ts +38 -35
  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 +19 -13
  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 +12 -13
  25. package/src/repos/i4-account-export-request.ts +2 -2
  26. package/src/repos/i4-account-setting.ts +6 -5
  27. package/src/repos/i4-account.test.ts +12 -8
  28. package/src/repos/i4-account.ts +10 -13
  29. package/src/repos/i4-address-junk-move.sqlite.test.ts +7 -2
  30. package/src/repos/i4-address.ts +17 -7
  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 +34 -44
  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 +19 -12
  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 +14 -10
  43. package/src/repos/i4-outbox-message.ts +7 -6
  44. package/src/repos/i4-sender-signer-standing.ts +2 -2
  45. package/src/repos/in-list.sqlite.test.ts +61 -0
  46. package/src/repos/in-list.ts +14 -0
  47. package/src/repos/label.ts +2 -2
  48. package/src/repos/message-flag.ts +8 -10
  49. package/src/repos/message-label.ts +5 -4
  50. package/src/repos/message.sqlite.test.ts +3 -3
  51. package/src/repos/message.test.ts +6 -5
  52. package/src/repos/message.ts +30 -24
  53. package/src/repos/quarantine.ts +2 -2
  54. package/src/repos/sync-warning.ts +5 -4
  55. package/src/repos/thread-message-body-muted.sqlite.test.ts +7 -3
  56. package/src/repos/thread-message-field-terms.sqlite.test.ts +7 -6
  57. package/src/repos/thread-message.sqlite.test.ts +7 -3
  58. package/src/repos/thread-message.test.ts +22 -18
  59. package/src/repos/thread-message.ts +16 -7
  60. package/src/search/thread-message-search.sqlite.test.ts +8 -3
  61. package/src/search/thread-message-search.ts +13 -8
  62. package/src/sqlite-repositories.ts +48 -32
@@ -1,9 +1,9 @@
1
- import { and, eq, inArray, or, type SQL } from "drizzle-orm";
2
- import { runBatch, type SqliteBatchItem } from "../batch.js";
1
+ import type { SqliteBatchItem } from "../batch.js";
3
2
  import {
4
3
  openSqliteDatabases,
5
4
  type SqliteDatabasePaths,
6
- type SqliteDatabases,
5
+ type SqliteStores,
6
+ syncStores,
7
7
  } from "../databases.js";
8
8
  import { accountTable } from "../schema/i4-account-config.js";
9
9
  import { accountSettingTable } from "../schema/i4-account-setting.js";
@@ -17,6 +17,7 @@ import { quarantineTable } from "../schema/quarantine.js";
17
17
  import { syncWarningTable } from "../schema/sync-warning.js";
18
18
  import { threadMessageTable } from "../schema/thread-message.js";
19
19
  import { SearchIndex } from "../search/thread-message-search.js";
20
+ import { inList, pairInList } from "./in-list.js";
20
21
  import { deleteMessageSubtree } from "./message.js";
21
22
 
22
23
  export interface CascadeEntity {
@@ -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()) {
@@ -107,7 +110,7 @@ export const runDrizzleCascadeDelete = async (
107
110
  items.push(
108
111
  mail
109
112
  .delete(threadMessageTable)
110
- .where(inArray(threadMessageTable.threadMessageId, threadMessageIds)),
113
+ .where(inList(threadMessageTable.threadMessageId, threadMessageIds)),
111
114
  );
112
115
  }
113
116
 
@@ -116,12 +119,12 @@ export const runDrizzleCascadeDelete = async (
116
119
  items.push(
117
120
  mail
118
121
  .delete(syncWarningTable)
119
- .where(inArray(syncWarningTable.mailboxId, mailboxIds)),
122
+ .where(inList(syncWarningTable.mailboxId, mailboxIds)),
120
123
  );
121
124
  items.push(
122
125
  mail
123
126
  .delete(mailboxTable)
124
- .where(inArray(mailboxTable.mailboxId, mailboxIds)),
127
+ .where(inList(mailboxTable.mailboxId, mailboxIds)),
125
128
  );
126
129
  }
127
130
 
@@ -132,19 +135,21 @@ export const runDrizzleCascadeDelete = async (
132
135
  items.push(
133
136
  mail
134
137
  .delete(outboxMessageTable)
135
- .where(inArray(outboxMessageTable.outboxMessageId, outboxMessageIds)),
138
+ .where(inList(outboxMessageTable.outboxMessageId, outboxMessageIds)),
136
139
  );
137
140
  }
138
141
 
139
142
  const lockKeys = grouped.get("MailboxLock") ?? [];
140
143
  if (lockKeys.length > 0) {
141
- const conditions: SQL[] = lockKeys.map((k) =>
142
- and(
143
- eq(mailboxLockTable.mailboxId, k.mailboxId),
144
- eq(mailboxLockTable.eventName, k.eventName),
144
+ items.push(
145
+ mail.delete(mailboxLockTable).where(
146
+ pairInList(
147
+ mailboxLockTable.mailboxId,
148
+ mailboxLockTable.eventName,
149
+ lockKeys.map((k) => [k.mailboxId, k.eventName]),
150
+ ),
145
151
  ),
146
- ) as SQL[];
147
- items.push(mail.delete(mailboxLockTable).where(or(...conditions)));
152
+ );
148
153
  }
149
154
 
150
155
  const accountSettingIds = (grouped.get("AccountSetting") ?? []).map(
@@ -154,9 +159,7 @@ export const runDrizzleCascadeDelete = async (
154
159
  appItems.push(
155
160
  app
156
161
  .delete(accountSettingTable)
157
- .where(
158
- inArray(accountSettingTable.accountSettingId, accountSettingIds),
159
- ),
162
+ .where(inList(accountSettingTable.accountSettingId, accountSettingIds)),
160
163
  );
161
164
  }
162
165
 
@@ -165,7 +168,7 @@ export const runDrizzleCascadeDelete = async (
165
168
  items.push(
166
169
  mail
167
170
  .delete(addressTable)
168
- .where(inArray(addressTable.addressId, addressIds)),
171
+ .where(inList(addressTable.addressId, addressIds)),
169
172
  );
170
173
  }
171
174
 
@@ -178,17 +181,17 @@ export const runDrizzleCascadeDelete = async (
178
181
  items.push(
179
182
  mail
180
183
  .delete(quarantineTable)
181
- .where(inArray(quarantineTable.accountId, accountIds)),
184
+ .where(inList(quarantineTable.accountId, accountIds)),
182
185
  );
183
186
  items.push(
184
187
  mail
185
188
  .delete(syncWarningTable)
186
- .where(inArray(syncWarningTable.accountId, accountIds)),
189
+ .where(inList(syncWarningTable.accountId, accountIds)),
187
190
  );
188
191
  appItems.push(
189
192
  app
190
193
  .delete(accountTable)
191
- .where(inArray(accountTable.accountId, accountIds)),
194
+ .where(inList(accountTable.accountId, accountIds)),
192
195
  );
193
196
  }
194
197
 
@@ -200,27 +203,26 @@ export const runDrizzleCascadeDelete = async (
200
203
  mail
201
204
  .delete(messagePlacementMoveTable)
202
205
  .where(
203
- inArray(messagePlacementMoveTable.messageId, placementMoveMessageIds),
206
+ inList(messagePlacementMoveTable.messageId, placementMoveMessageIds),
204
207
  ),
205
208
  );
206
209
  }
207
210
 
208
- // Composite key (messageId, flagName) — a message can carry an
209
- // independent pending read-state marker and pending star marker at once
210
- // (issue #1273), so a single-column inArray cannot target one row.
211
211
  const flagPushKeys = grouped.get("MessageFlagPush") ?? [];
212
212
  if (flagPushKeys.length > 0) {
213
- const conditions: SQL[] = flagPushKeys.map((k) =>
214
- and(
215
- eq(messageFlagPushTable.messageId, k.messageId),
216
- eq(messageFlagPushTable.flagName, k.flagName),
213
+ items.push(
214
+ mail.delete(messageFlagPushTable).where(
215
+ pairInList(
216
+ messageFlagPushTable.messageId,
217
+ messageFlagPushTable.flagName,
218
+ flagPushKeys.map((k) => [k.messageId, k.flagName]),
219
+ ),
217
220
  ),
218
- ) as SQL[];
219
- items.push(mail.delete(messageFlagPushTable).where(or(...conditions)));
221
+ );
220
222
  }
221
- await runBatch(mail, items);
222
- await runBatch(app, appItems);
223
- 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);
224
226
 
225
227
  log.info({ entityCount: entities.length }, "Drizzle cascade delete complete");
226
228
  };
@@ -239,5 +241,6 @@ export const createSqliteCascadeDeleter = async (
239
241
  paths: SqliteDatabasePaths,
240
242
  ): Promise<CascadeDeleter> => {
241
243
  const databases = await openSqliteDatabases(paths);
242
- return (entities, log) => runDrizzleCascadeDelete(databases, entities, log);
244
+ const stores = syncStores(databases);
245
+ return (entities, log) => runDrizzleCascadeDelete(stores, entities, log);
243
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,
@@ -8,9 +8,9 @@ import type {
8
8
  MessageReferenceInput,
9
9
  UpdateEnvelopeInput,
10
10
  } from "@remit/data-ports";
11
- import { eq, inArray } from "drizzle-orm";
12
- import { runBatch } from "../batch.js";
13
- import type { Db } from "../db.js";
11
+ import { eq } from "drizzle-orm";
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,11 +29,11 @@ import {
29
29
  bodyStructureRefetchTable,
30
30
  envelopeAddressTable,
31
31
  envelopeTable,
32
- type MessageDataSchema,
33
32
  messageReferenceTable,
34
33
  messageTable,
35
34
  rawMessageStorageTable,
36
35
  } from "../schema/message-data.js";
36
+ import { inList } from "./in-list.js";
37
37
  import {
38
38
  toBodyPartItem,
39
39
  toBodyPartParameterItem,
@@ -44,7 +44,7 @@ import {
44
44
  toRawMessageStorageItem,
45
45
  } from "./mappers.js";
46
46
 
47
- type DB = Db<MessageDataSchema>;
47
+ type DB = RepositoryDb<Record<string, unknown>>;
48
48
 
49
49
  function dedupeById<T>(rows: T[], getId: (row: T) => string): T[] {
50
50
  const byId = new Map<string, T>();
@@ -55,7 +55,13 @@ function dedupeById<T>(rows: T[], getId: (row: T) => string): T[] {
55
55
  }
56
56
 
57
57
  export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
58
- 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
+ }
59
65
 
60
66
  async getMessageData(messageId: string): Promise<MessageData> {
61
67
  const [
@@ -182,10 +188,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
182
188
  set: { updatedAt: now },
183
189
  });
184
190
  if (dedupedParameterRows.length === 0) {
185
- await runBatch(this.db, [insertParts]);
191
+ await this.batch([insertParts]);
186
192
  return;
187
193
  }
188
- await runBatch(this.db, [
194
+ await this.batch([
189
195
  insertParts,
190
196
  this.db
191
197
  .insert(bodyPartParameterTable)
@@ -210,10 +216,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
210
216
  .delete(messageReferenceTable)
211
217
  .where(eq(messageReferenceTable.messageId, messageId));
212
218
  if (references.length === 0) {
213
- await runBatch(this.db, [clear]);
219
+ await this.batch([clear]);
214
220
  return;
215
221
  }
216
- await runBatch(this.db, [
222
+ await this.batch([
217
223
  clear,
218
224
  this.db.insert(messageReferenceTable).values(
219
225
  references.map((reference) => ({
@@ -235,7 +241,7 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
235
241
 
236
242
  await this.db
237
243
  .delete(envelopeTable)
238
- .where(inArray(envelopeTable.envelopeId, envelopeIds));
244
+ .where(inList(envelopeTable.envelopeId, envelopeIds));
239
245
  }
240
246
 
241
247
  async createEnvelope(input: CreateEnvelopeInput): Promise<EnvelopeItem> {
@@ -293,7 +299,7 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
293
299
  const rows = await this.db
294
300
  .select()
295
301
  .from(envelopeTable)
296
- .where(inArray(envelopeTable.envelopeId, envelopeId));
302
+ .where(inList(envelopeTable.envelopeId, envelopeId));
297
303
  return rows.map(toEnvelopeItem);
298
304
  }
299
305
 
@@ -353,6 +359,6 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
353
359
  if (messageIds.length === 0) return;
354
360
  await this.db
355
361
  .delete(bodyStructureRefetchTable)
356
- .where(inArray(bodyStructureRefetchTable.messageId, messageIds));
362
+ .where(inList(bodyStructureRefetchTable.messageId, messageIds));
357
363
  }
358
364
  }
@@ -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
- import { and, asc, eq, gt, inArray, or } from "drizzle-orm";
10
- import type { Db } from "../db.js";
10
+ import { and, asc, eq, gt, or } from "drizzle-orm";
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,11 +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";
20
+ import { inList } from "./in-list.js";
21
21
 
22
- type DB = Db<Record<string, unknown>>;
22
+ type DB = RepositoryDb<Record<string, unknown>>;
23
+
24
+ type AddressReader = Pick<IAddressRepository, "listAllByAccountConfig">;
23
25
 
24
26
  function rowToAccountConfig(
25
27
  row: typeof accountConfigTable.$inferSelect,
@@ -39,7 +41,7 @@ function rowToAccountConfig(
39
41
  export class AccountConfigRepo implements IAccountConfigRepository {
40
42
  constructor(
41
43
  private db: DB,
42
- private mail: DB,
44
+ private addresses: AddressReader,
43
45
  ) {}
44
46
 
45
47
  async create(input: CreateAccountConfigInput): Promise<AccountConfigItem> {
@@ -71,7 +73,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
71
73
  const rows = await this.db
72
74
  .select()
73
75
  .from(accountConfigTable)
74
- .where(inArray(accountConfigTable.accountConfigId, accountConfigId));
76
+ .where(inList(accountConfigTable.accountConfigId, accountConfigId));
75
77
  return rows.map(rowToAccountConfig);
76
78
  }
77
79
  const [row] = await this.db
@@ -116,7 +118,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
116
118
  if (accountConfigIds.length === 0) return;
117
119
  await this.db
118
120
  .delete(accountConfigTable)
119
- .where(inArray(accountConfigTable.accountConfigId, accountConfigIds));
121
+ .where(inList(accountConfigTable.accountConfigId, accountConfigIds));
120
122
  }
121
123
 
122
124
  async listByUser(
@@ -182,10 +184,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
182
184
  .select()
183
185
  .from(accountTable)
184
186
  .where(eq(accountTable.accountConfigId, accountConfigId)),
185
- this.mail
186
- .select()
187
- .from(addressTable)
188
- .where(eq(addressTable.accountConfigId, accountConfigId)),
187
+ this.addresses.listAllByAccountConfig(accountConfigId),
189
188
  ]);
190
189
  if (configs.length === 0) {
191
190
  throw new NotFoundError(`AccountConfig not found: ${accountConfigId}`);
@@ -193,7 +192,7 @@ export class AccountConfigRepo implements IAccountConfigRepository {
193
192
  return {
194
193
  accountConfig: configs.map(rowToAccountConfig),
195
194
  account: accounts.map(rowToAccount),
196
- address: addresses.map(rowToAddress),
195
+ address: addresses,
197
196
  };
198
197
  }
199
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,
@@ -5,11 +5,12 @@ import type {
5
5
  IAccountSettingRepository,
6
6
  UpsertAccountSettingInput,
7
7
  } from "@remit/data-ports";
8
- import { and, eq, inArray } from "drizzle-orm";
9
- import type { Db } from "../db.js";
8
+ import { and, eq } from "drizzle-orm";
9
+ import type { RepositoryDb } from "../db.js";
10
10
  import { accountSettingTable } from "../schema/i4-account-setting.js";
11
+ import { inList } from "./in-list.js";
11
12
 
12
- type DB = Db<Record<string, unknown>>;
13
+ type DB = RepositoryDb<Record<string, unknown>>;
13
14
 
14
15
  // The PG store derives its own deterministic id from the composite key. It is
15
16
  // stable within this backend but not byte-identical to the DDB base36 UUIDv5
@@ -63,8 +64,8 @@ export class AccountSettingRepo implements IAccountSettingRepository {
63
64
  .from(accountSettingTable)
64
65
  .where(
65
66
  and(
66
- inArray(accountSettingTable.accountConfigId, [...accountConfigIds]),
67
- inArray(accountSettingTable.name, [...names]),
67
+ inList(accountSettingTable.accountConfigId, accountConfigIds),
68
+ inList(accountSettingTable.name, names),
68
69
  ),
69
70
  );
70
71
  return rows.map(rowToAccountSetting);
@@ -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,21 +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
- import { and, asc, eq, gt, inArray, like, or, sql } from "drizzle-orm";
13
- import type { Db } from "../db.js";
13
+ import { and, asc, eq, gt, like, or, sql } from "drizzle-orm";
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";
19
+ import { inList } from "./in-list.js";
20
20
 
21
- type DB = Db<Record<string, unknown>>;
21
+ type DB = RepositoryDb<Record<string, unknown>>;
22
22
 
23
23
  /**
24
24
  * A stored failure is a sentence the account card shows, not a transcript.
@@ -73,7 +73,7 @@ export function rowToAccount(
73
73
  export class AccountRepo implements IAccountRepository {
74
74
  constructor(
75
75
  private db: DB,
76
- private mail: DB,
76
+ private mailboxes: Pick<IMailboxRepository, "listAllByAccount">,
77
77
  ) {}
78
78
 
79
79
  async create(input: CreateAccountInput): Promise<AccountItem> {
@@ -129,7 +129,7 @@ export class AccountRepo implements IAccountRepository {
129
129
  const rows = await this.db
130
130
  .select()
131
131
  .from(accountTable)
132
- .where(inArray(accountTable.accountId, accountId));
132
+ .where(inList(accountTable.accountId, accountId));
133
133
  return rows.map(rowToAccount);
134
134
  }
135
135
  const [row] = await this.db
@@ -187,7 +187,7 @@ export class AccountRepo implements IAccountRepository {
187
187
  if (accountIds.length === 0) return;
188
188
  await this.db
189
189
  .delete(accountTable)
190
- .where(inArray(accountTable.accountId, accountIds));
190
+ .where(inList(accountTable.accountId, accountIds));
191
191
  }
192
192
 
193
193
  async list(
@@ -268,17 +268,14 @@ export class AccountRepo implements IAccountRepository {
268
268
  .select()
269
269
  .from(accountTable)
270
270
  .where(eq(accountTable.accountId, accountId)),
271
- this.mail
272
- .select()
273
- .from(mailboxTable)
274
- .where(eq(mailboxTable.accountId, accountId)),
271
+ this.mailboxes.listAllByAccount(accountId),
275
272
  ]);
276
273
  if (accounts.length === 0) {
277
274
  throw new NotFoundError(`Account not found: ${accountId}`);
278
275
  }
279
276
  return {
280
277
  account: accounts.map(rowToAccount),
281
- mailbox: mailboxes.map(rowToMailbox),
278
+ mailbox: mailboxes,
282
279
  };
283
280
  }
284
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
  /**