@remit/drizzle-service 0.0.106 → 0.0.108

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 (31) hide show
  1. package/package.json +4 -2
  2. package/src/id.ts +9 -0
  3. package/src/index.ts +4 -0
  4. package/src/repair/junk-only-address.sqlite.test.ts +2 -0
  5. package/src/repos/account.conformance.d1.test.ts +3 -1
  6. package/src/repos/account.conformance.sqlite.test.ts +2 -0
  7. package/src/repos/calendar-default.suite.ts +5 -1
  8. package/src/repos/calendar-put.suite.ts +5 -1
  9. package/src/repos/cascade-delete.ts +1 -1
  10. package/src/repos/envelope.sqlite.test.ts +2 -0
  11. package/src/repos/envelope.test.ts +2 -0
  12. package/src/repos/envelope.ts +5 -3
  13. package/src/repos/i4-account-config.suite.ts +5 -1
  14. package/src/repos/i4-account.test.ts +3 -0
  15. package/src/repos/i4-address-junk-move.sqlite.test.ts +2 -0
  16. package/src/repos/i4-mailbox-special-use.sqlite.test.ts +2 -0
  17. package/src/repos/i4-mailbox.sqlite.test.ts +3 -0
  18. package/src/repos/i4-mailbox.test.ts +4 -3
  19. package/src/repos/i4-mailbox.ts +7 -28
  20. package/src/repos/i4-outbox-attachment.test.ts +2 -0
  21. package/src/repos/message-objects.sqlite.test.ts +271 -0
  22. package/src/repos/message-objects.ts +70 -0
  23. package/src/repos/message.sqlite.test.ts +11 -17
  24. package/src/repos/message.test.ts +4 -6
  25. package/src/repos/message.ts +341 -46
  26. package/src/repos/thread-message-body-muted.sqlite.test.ts +2 -0
  27. package/src/repos/thread-message-field-terms.sqlite.test.ts +2 -0
  28. package/src/repos/thread-message.sqlite.test.ts +2 -0
  29. package/src/repos/thread-message.test.ts +5 -0
  30. package/src/search/thread-message-search.sqlite.test.ts +2 -0
  31. package/src/sqlite-repositories.ts +18 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.106",
3
+ "version": "0.0.108",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -36,7 +36,9 @@
36
36
  "uuid": "^13.0.2",
37
37
  "@remit/drizzle-sqlite-schema": "*",
38
38
  "better-sqlite3": "^12.11.1",
39
- "@types/better-sqlite3": "^7.6.13"
39
+ "@types/better-sqlite3": "^7.6.13",
40
+ "@remit/storage-service": "*",
41
+ "p-map": "^2.1.0"
40
42
  },
41
43
  "license": "MIT",
42
44
  "publishConfig": {
package/src/id.ts CHANGED
@@ -42,3 +42,12 @@ export const envelopeAddressId = (
42
42
  order: number,
43
43
  ): string =>
44
44
  generateDeterministicId(`envelopeaddress:${messageId}:${role}:${order}`);
45
+
46
+ export const messageReferenceId = (
47
+ messageId: string,
48
+ referenceType: string,
49
+ referenceOrder: number,
50
+ ): string =>
51
+ deterministicBase36Id(
52
+ `msgref:${messageId}:${referenceType}:${referenceOrder}`,
53
+ );
package/src/index.ts CHANGED
@@ -74,6 +74,10 @@ export {
74
74
  } from "./repos/message.js";
75
75
  export { DrizzleMessageFlagRepository } from "./repos/message-flag.js";
76
76
  export { MessageLabelRepo } from "./repos/message-label.js";
77
+ export {
78
+ type MessageObjectStore,
79
+ messageObjectsInMemory,
80
+ } from "./repos/message-objects.js";
77
81
  export { QuarantineRepo } from "./repos/quarantine.js";
78
82
  export { SyncWarningRepo } from "./repos/sync-warning.js";
79
83
  export { DrizzleThreadMessageRepository } from "./repos/thread-message.js";
@@ -7,6 +7,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
7
7
  import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
8
8
  import { AccountSettingRepo } from "../repos/i4-account-setting.js";
9
9
  import type { MailboxSpecialUseRepo } from "../repos/i4-mailbox-special-use.js";
10
+ import { messageObjectsInMemory } from "../repos/message-objects.js";
10
11
  import { createSqliteRepositories } from "../sqlite-repositories.js";
11
12
  import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
12
13
  import {
@@ -190,6 +191,7 @@ describe("addresses standing only on mail in Junk", () => {
190
191
  search: db,
191
192
  },
192
193
  BETTER_SQLITE3_LIMITS,
194
+ messageObjectsInMemory(),
193
195
  ).mailboxSpecialUse;
194
196
  accountSetting = new AccountSettingRepo(db as never);
195
197
  applyMigrationSet(sqlite, "app");
@@ -3,6 +3,7 @@ import { NotFoundError } from "../error.js";
3
3
  import { randomId } from "../id.js";
4
4
  import { composeSqliteRepositories } from "../sqlite-repositories.js";
5
5
  import { openD1TestStores } from "../test-d1.js";
6
+ import { messageObjectsInMemory } from "./message-objects.js";
6
7
 
7
8
  let close: (() => Promise<void>) | undefined;
8
9
 
@@ -10,7 +11,8 @@ accountRepositoryConformance({
10
11
  async createRepository() {
11
12
  const opened = await openD1TestStores();
12
13
  close = opened.close;
13
- return composeSqliteRepositories(opened.stores).account;
14
+ return composeSqliteRepositories(opened.stores, messageObjectsInMemory())
15
+ .account;
14
16
  },
15
17
  teardown: async () => {
16
18
  await close?.();
@@ -5,6 +5,7 @@ import { randomId } from "../id.js";
5
5
  import { accountTable } from "../schema.js";
6
6
  import { createSqliteRepositories } from "../sqlite-repositories.js";
7
7
  import { createSqliteTestDb } from "../test-db-sqlite.js";
8
+ import { messageObjectsInMemory } from "./message-objects.js";
8
9
 
9
10
  let close: (() => Promise<void>) | undefined;
10
11
 
@@ -21,6 +22,7 @@ accountRepositoryConformance({
21
22
  search: db,
22
23
  },
23
24
  BETTER_SQLITE3_LIMITS,
25
+ messageObjectsInMemory(),
24
26
  ).account;
25
27
  },
26
28
  teardown: async () => {
@@ -6,6 +6,7 @@ import type { RepositoryDb } from "../db.js";
6
6
  import { composeSqliteRepositories } from "../sqlite-repositories.js";
7
7
  import type { OpenTestStores } from "../test-stores.js";
8
8
  import { CalendarCollectionRepo } from "./calendar-collection.js";
9
+ import { messageObjectsInMemory } from "./message-objects.js";
9
10
 
10
11
  const ACCOUNT_CONFIG_ID = "account-config-default";
11
12
 
@@ -19,7 +20,10 @@ export const calendarDefaultSuite = (open: OpenTestStores): void => {
19
20
  const opened = await open();
20
21
  db = opened.stores.app.db;
21
22
  close = opened.close;
22
- store = composeSqliteRepositories(opened.stores).calendarStore;
23
+ store = composeSqliteRepositories(
24
+ opened.stores,
25
+ messageObjectsInMemory(),
26
+ ).calendarStore;
23
27
  });
24
28
 
25
29
  after(() => close());
@@ -18,6 +18,7 @@ import {
18
18
  } from "../schema.js";
19
19
  import { composeSqliteRepositories } from "../sqlite-repositories.js";
20
20
  import type { OpenTestStores } from "../test-stores.js";
21
+ import { messageObjectsInMemory } from "./message-objects.js";
21
22
 
22
23
  const ACCOUNT_CONFIG_ID = "account-config-1";
23
24
 
@@ -74,7 +75,10 @@ export const calendarPutSuite = (open: OpenTestStores): void => {
74
75
  const opened = await open();
75
76
  db = opened.stores.app.db;
76
77
  close = opened.close;
77
- store = composeSqliteRepositories(opened.stores).calendarStore;
78
+ store = composeSqliteRepositories(
79
+ opened.stores,
80
+ messageObjectsInMemory(),
81
+ ).calendarStore;
78
82
  const collection = await provisionDefaultCalendar(
79
83
  store,
80
84
  ACCOUNT_CONFIG_ID,
@@ -102,7 +102,7 @@ export const runDrizzleCascadeDelete = async (
102
102
  const items: SqliteBatchItem[] = [];
103
103
  const appItems: SqliteBatchItem[] = [];
104
104
  const messageIds = (grouped.get("Message") ?? []).map((k) => k.messageId);
105
- items.push(...deleteMessageSubtree(mail, messageIds));
105
+ items.push(...deleteMessageSubtree(mail, messageIds).statements);
106
106
 
107
107
  const threadMessageIds = (grouped.get("ThreadMessage") ?? []).map(
108
108
  (k) => k.threadMessageId,
@@ -8,6 +8,7 @@ import {
8
8
  import { createSqliteRepositories } from "../sqlite-repositories.js";
9
9
  import { createSqliteTestDb, type SqliteTestDb } from "../test-db-sqlite.js";
10
10
  import type { DrizzleEnvelopeRepository } from "./envelope.js";
11
+ import { messageObjectsInMemory } from "./message-objects.js";
11
12
 
12
13
  // Envelope repo on sqlite (RFC 036 D1): the onConflictDoUpdate upsert path and
13
14
  // the transactional body-part upsert both run on better-sqlite3 unchanged.
@@ -24,6 +25,7 @@ describe("DrizzleEnvelopeRepository (sqlite)", () => {
24
25
  repo = createSqliteRepositories(
25
26
  { app: db, mail: db, search: db },
26
27
  BETTER_SQLITE3_LIMITS,
28
+ messageObjectsInMemory(),
27
29
  ).envelope;
28
30
  });
29
31
 
@@ -5,6 +5,7 @@ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
5
5
  import { envelopeId as deriveEnvelopeId } from "../id.js";
6
6
  import { createSqliteRepositories } from "../sqlite-repositories.js";
7
7
  import type { DrizzleEnvelopeRepository } from "./envelope.js";
8
+ import { messageObjectsInMemory } from "./message-objects.js";
8
9
  import { createTestDb, type TestDb } from "./test-helpers.js";
9
10
 
10
11
  describe("DrizzleEnvelopeRepository", () => {
@@ -23,6 +24,7 @@ describe("DrizzleEnvelopeRepository", () => {
23
24
  repo = createSqliteRepositories(
24
25
  { app: db, mail: db, search: db },
25
26
  BETTER_SQLITE3_LIMITS,
27
+ messageObjectsInMemory(),
26
28
  ).envelope;
27
29
 
28
30
  await repo.upsertEnvelope({
@@ -21,7 +21,7 @@ import {
21
21
  bodyPartId as deriveBodyPartId,
22
22
  bodyPartParameterId as deriveBodyPartParameterId,
23
23
  envelopeId as deriveEnvelopeId,
24
- deterministicBase36Id,
24
+ messageReferenceId as deriveMessageReferenceId,
25
25
  } from "../id.js";
26
26
  import {
27
27
  bodyPartParameterTable,
@@ -229,8 +229,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
229
229
  .where(eq(messageReferenceTable.messageId, messageId));
230
230
  const rows = references.map((reference) => ({
231
231
  ...reference,
232
- messageReferenceId: deterministicBase36Id(
233
- `msgref:${messageId}:${reference.referenceType}:${reference.referenceOrder}`,
232
+ messageReferenceId: deriveMessageReferenceId(
233
+ messageId,
234
+ reference.referenceType,
235
+ reference.referenceOrder,
234
236
  ),
235
237
  messageId,
236
238
  envelopeId,
@@ -7,6 +7,7 @@ import {
7
7
  } from "../sqlite-repositories.js";
8
8
  import type { OpenTestStores } from "../test-stores.js";
9
9
  import type { AccountConfigRepo } from "./i4-account-config.js";
10
+ import { messageObjectsInMemory } from "./message-objects.js";
10
11
 
11
12
  export const accountConfigRepoSuite = (open: OpenTestStores): void => {
12
13
  describe("AccountConfigRepo", () => {
@@ -17,7 +18,10 @@ export const accountConfigRepoSuite = (open: OpenTestStores): void => {
17
18
  before(async () => {
18
19
  const opened = await open();
19
20
  close = opened.close;
20
- repos = composeSqliteRepositories(opened.stores);
21
+ repos = composeSqliteRepositories(
22
+ opened.stores,
23
+ messageObjectsInMemory(),
24
+ );
21
25
  repo = repos.accountConfig;
22
26
  });
23
27
 
@@ -4,6 +4,7 @@ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
4
4
  import { createSqliteRepositories } from "../sqlite-repositories.js";
5
5
  import { createTestDb, randomId, type TestDb } from "../test-db.js";
6
6
  import type { AccountRepo } from "./i4-account.js";
7
+ import { messageObjectsInMemory } from "./message-objects.js";
7
8
  import { mailboxCreated } from "./test-helpers.js";
8
9
 
9
10
  function makeAccountInput(accountConfigId: string) {
@@ -34,6 +35,7 @@ describe("AccountRepo", () => {
34
35
  search: db,
35
36
  },
36
37
  BETTER_SQLITE3_LIMITS,
38
+ messageObjectsInMemory(),
37
39
  ).account;
38
40
  });
39
41
 
@@ -175,6 +177,7 @@ describe("AccountRepo", () => {
175
177
  search: db,
176
178
  },
177
179
  BETTER_SQLITE3_LIMITS,
180
+ messageObjectsInMemory(),
178
181
  ).mailbox;
179
182
  mailboxCreated(
180
183
  await mailboxRepo.create({
@@ -6,6 +6,7 @@ import { createSqliteRepositories } from "../sqlite-repositories.js";
6
6
  import { createTestDb, type TestDb } from "../test-db.js";
7
7
  import { AddressRepo } from "./i4-address.js";
8
8
  import type { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
9
+ import { messageObjectsInMemory } from "./message-objects.js";
9
10
 
10
11
  const CONFIG = "cfg-1";
11
12
 
@@ -133,6 +134,7 @@ describe("reconciling one message's addresses at the moment it moves", () => {
133
134
  search: db,
134
135
  },
135
136
  BETTER_SQLITE3_LIMITS,
137
+ messageObjectsInMemory(),
136
138
  ).mailboxSpecialUse;
137
139
  });
138
140
 
@@ -18,6 +18,7 @@ import { createSqliteTestDb } from "../test-db-sqlite.js";
18
18
  import type { AccountSettingRepo } from "./i4-account-setting.js";
19
19
  import type { MailboxRepo } from "./i4-mailbox.js";
20
20
  import type { MailboxSpecialUseRepo } from "./i4-mailbox-special-use.js";
21
+ import { messageObjectsInMemory } from "./message-objects.js";
21
22
  import { mailboxCreated } from "./test-helpers.js";
22
23
 
23
24
  const makeMailboxInput = (
@@ -65,6 +66,7 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
65
66
  search: testDb.db,
66
67
  },
67
68
  BETTER_SQLITE3_LIMITS,
69
+ messageObjectsInMemory(),
68
70
  );
69
71
  repo = repos.mailboxSpecialUse;
70
72
  mailboxes = repos.mailbox;
@@ -13,6 +13,7 @@ import {
13
13
  shippedTableDdl,
14
14
  } from "../test-shipped-sqlite-schema.js";
15
15
  import type { MailboxRepo } from "./i4-mailbox.js";
16
+ import { messageObjectsInMemory } from "./message-objects.js";
16
17
  import { mailboxCreated } from "./test-helpers.js";
17
18
 
18
19
  function makeMailboxInput(accountId: string, fullPath = "INBOX") {
@@ -49,6 +50,7 @@ describe("MailboxRepo (sqlite)", () => {
49
50
  search: testDb.db,
50
51
  },
51
52
  BETTER_SQLITE3_LIMITS,
53
+ messageObjectsInMemory(),
52
54
  ).mailbox;
53
55
  });
54
56
 
@@ -222,6 +224,7 @@ describe("MailboxRepo (sqlite, shipped migrations)", () => {
222
224
  search: db,
223
225
  },
224
226
  BETTER_SQLITE3_LIMITS,
227
+ messageObjectsInMemory(),
225
228
  ).mailbox;
226
229
  close = async () => {
227
230
  sqlite.close();
@@ -18,6 +18,7 @@ import { createSqliteRepositories } from "../sqlite-repositories.js";
18
18
  import { createTestDb, randomId, type TestDb } from "../test-db.js";
19
19
  import type { MailboxRepo } from "./i4-mailbox.js";
20
20
  import { deleteMessageSubtree } from "./message.js";
21
+ import { messageObjectsInMemory } from "./message-objects.js";
21
22
  import { mailboxCreated } from "./test-helpers.js";
22
23
 
23
24
  /** Every state a row can carry, for a transition that decides against none. */
@@ -62,6 +63,7 @@ describe("MailboxRepo", () => {
62
63
  search: db,
63
64
  },
64
65
  BETTER_SQLITE3_LIMITS,
66
+ messageObjectsInMemory(),
65
67
  ).mailbox;
66
68
  });
67
69
 
@@ -554,6 +556,7 @@ describe("MailboxRepo", () => {
554
556
  search: db,
555
557
  },
556
558
  BETTER_SQLITE3_LIMITS,
559
+ messageObjectsInMemory(),
557
560
  ).message.create({
558
561
  messageId,
559
562
  mailboxId,
@@ -561,8 +564,6 @@ describe("MailboxRepo", () => {
561
564
  sequenceNumber: uid,
562
565
  rfc822Size: 10,
563
566
  internalDate: 1700000000000,
564
- envelopeId: deriveEnvelopeId(messageId),
565
- rootBodyPartId: deriveRootBodyPartId(messageId),
566
567
  });
567
568
  await db.insert(threadMessageTable).values({
568
569
  threadMessageId: randomId(),
@@ -682,7 +683,7 @@ describe("MailboxRepo", () => {
682
683
 
683
684
  // The shape an interruption leaves: one message's rows already gone,
684
685
  // the mailbox row still there and still `deleting`.
685
- await syncBatch(db)(deleteMessageSubtree(db, [first]));
686
+ await syncBatch(db)(deleteMessageSubtree(db, [first]).statements);
686
687
  await repo.transition(accountId, mailbox.mailboxId, {
687
688
  from: EVERY_STATE,
688
689
  to: MailboxSyncStatus.deleting,
@@ -42,10 +42,8 @@ import { messageFlagPushTable } from "../schema/i4-message-flag-push.js";
42
42
  import { messagePlacementMoveTable } from "../schema/i4-message-placement-move.js";
43
43
  import { messageTable } from "../schema/message-data.js";
44
44
  import { syncWarningTable } from "../schema/sync-warning.js";
45
- import { threadMessageTable } from "../schema/thread-message.js";
46
- import type { SearchIndex } from "../search/thread-message-search.js";
47
45
  import { inList } from "./in-list.js";
48
- import { deleteMessageSubtree } from "./message.js";
46
+ import type { DrizzleMessageRepository } from "./message.js";
49
47
 
50
48
  const base36Translator = shortUuid.createTranslator(
51
49
  shortUuid.constants.uuid25Base36,
@@ -60,7 +58,6 @@ type DB = RepositoryDb<Record<string, unknown>>;
60
58
  * the file's write lock, so the bound is what keeps a large folder's delete from
61
59
  * parking every other writer behind it (D8).
62
60
  */
63
- const MAIL_DELETE_BATCH_SIZE = 100;
64
61
 
65
62
  /**
66
63
  * The folders under `path`. A flat namespace nests nothing: with no delimiter
@@ -154,7 +151,7 @@ export class MailboxRepo implements IMailboxRepository {
154
151
 
155
152
  constructor(
156
153
  store: SqliteStore<Record<string, unknown>>,
157
- private search: SearchIndex,
154
+ private messages: Pick<DrizzleMessageRepository, "deleteMany">,
158
155
  ) {
159
156
  this.db = store.db;
160
157
  this.batch = store.batch;
@@ -547,29 +544,11 @@ export class MailboxRepo implements IMailboxRepository {
547
544
  // Ordered, batched and resumable rather than one transaction (D8). The
548
545
  // caller keeps the row `deleting` until the last commit, so an interrupted
549
546
  // run re-enters here and continues against whatever is left.
550
- for (;;) {
551
- const rows = await this.db
552
- .select({ messageId: messageTable.messageId })
553
- .from(messageTable)
554
- .where(eq(messageTable.mailboxId, mailboxId))
555
- .limit(MAIL_DELETE_BATCH_SIZE);
556
- if (rows.length === 0) break;
557
- const messageIds = rows.map((row) => row.messageId);
558
- const listings = await this.db
559
- .select({ threadMessageId: threadMessageTable.threadMessageId })
560
- .from(threadMessageTable)
561
- .where(inList(threadMessageTable.messageId, messageIds));
562
-
563
- // The primitive the rest of the codebase deletes mail with: nine
564
- // per-message child tables. A bespoke table list would orphan them.
565
- await this.batch([
566
- ...deleteMessageSubtree(this.db, messageIds),
567
- this.db
568
- .delete(threadMessageTable)
569
- .where(inList(threadMessageTable.messageId, messageIds)),
570
- ]);
571
- await this.search.sync(listings.map((row) => row.threadMessageId));
572
- }
547
+ const rows = await this.db
548
+ .select({ messageId: messageTable.messageId })
549
+ .from(messageTable)
550
+ .where(eq(messageTable.mailboxId, mailboxId));
551
+ await this.messages.deleteMany(rows.map((row) => row.messageId));
573
552
 
574
553
  await this.db
575
554
  .delete(mailboxSpecialUseTable)
@@ -4,6 +4,7 @@ import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
4
4
  import { createSqliteRepositories } from "../sqlite-repositories.js";
5
5
  import { createTestDb, randomId } from "../test-db.js";
6
6
  import type { OutboxAttachmentRepo } from "./i4-outbox-attachment.js";
7
+ import { messageObjectsInMemory } from "./message-objects.js";
7
8
 
8
9
  const CAP = { maxTotalBytes: 1000, maxCount: 3, nowSeconds: 1_000_000 };
9
10
 
@@ -36,6 +37,7 @@ before(async () => {
36
37
  search: harness.db,
37
38
  },
38
39
  BETTER_SQLITE3_LIMITS,
40
+ messageObjectsInMemory(),
39
41
  ).outboxAttachment;
40
42
  });
41
43