@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
@@ -8,6 +8,7 @@ import type {
8
8
  import {
9
9
  and,
10
10
  asc,
11
+ type Column,
11
12
  eq,
12
13
  exists,
13
14
  gt,
@@ -19,6 +20,7 @@ import {
19
20
  type SQL,
20
21
  } from "drizzle-orm";
21
22
  import type { SqliteBatchItem, WriteBatch } from "../batch.js";
23
+ import { chunk, chunkRows } from "../bound-parameters.js";
22
24
  import type { RepositoryDb, SqliteStore } from "../db.js";
23
25
  import {
24
26
  CreateFailedConflictError,
@@ -26,7 +28,11 @@ import {
26
28
  NotFoundError,
27
29
  } from "../error.js";
28
30
  import {
31
+ bodyPartId as deriveBodyPartId,
32
+ bodyPartParameterId as deriveBodyPartParameterId,
33
+ envelopeAddressId as deriveEnvelopeAddressId,
29
34
  envelopeId as deriveEnvelopeId,
35
+ messageReferenceId as deriveMessageReferenceId,
30
36
  rootBodyPartId as deriveRootBodyPartId,
31
37
  } from "../id.js";
32
38
  import { decodeToken, resultList } from "../pagination.js";
@@ -45,7 +51,7 @@ import {
45
51
  } from "../schema/message-data.js";
46
52
  import { threadMessageTable } from "../schema/thread-message.js";
47
53
  import type { SearchIndex } from "../search/thread-message-search.js";
48
- import { inList } from "./in-list.js";
54
+ import { inList, pairInList } from "./in-list.js";
49
55
  import {
50
56
  toBodyPartItem,
51
57
  toBodyPartParameterItem,
@@ -55,6 +61,7 @@ import {
55
61
  toMessageReferenceItem,
56
62
  toRawMessageStorageItem,
57
63
  } from "./mappers.js";
64
+ import type { MessageObjects } from "./message-objects.js";
58
65
 
59
66
  type DB = RepositoryDb<Record<string, unknown>>;
60
67
 
@@ -112,59 +119,147 @@ function toMessageItem(row: typeof messageTable.$inferSelect): MessageItem {
112
119
  };
113
120
  }
114
121
 
115
- export type SubtreeDb = Pick<RepositoryDb<Record<string, unknown>>, "delete">;
122
+ export type SubtreeDb = Pick<
123
+ RepositoryDb<Record<string, unknown>>,
124
+ "delete" | "select"
125
+ >;
126
+
127
+ export type SubtreeGuard = (messageId: Column) => SQL;
128
+
129
+ export const MAIL_DELETE_BATCH_SIZE = 100;
116
130
 
117
131
  /**
118
132
  * The statements that delete a message and its whole per-message subtree by
119
133
  * message id: the child tables, then the message row. The caller runs them as
120
- * one batch so the subtree goes atomically; the last statement returns the
121
- * message ids it removed. `guard` rides every statement, so
122
- * a guard that reads the message row holds until the last statement removes it.
134
+ * one batch so the subtree goes atomically. `removed` and `removedParts` read
135
+ * the message and body-part rows the batch deletes; they go first, so they see
136
+ * them under the same guard. `guard` rides every statement on that
137
+ * statement's own `messageId` column, so a guard that reads the message row
138
+ * holds until the last statement removes it.
123
139
  */
124
140
  export function deleteMessageSubtree(
125
141
  db: SubtreeDb,
126
- messageIds: string[],
127
- guard?: SQL,
128
- ): SqliteBatchItem[] {
129
- if (messageIds.length === 0) return [];
130
- return [
142
+ messageIds: readonly string[],
143
+ guard?: SubtreeGuard,
144
+ ) {
145
+ const removed = db
146
+ .select({
147
+ messageId: messageTable.messageId,
148
+ bodyStorageKey: messageTable.bodyStorageKey,
149
+ })
150
+ .from(messageTable)
151
+ .where(
152
+ and(
153
+ inList(messageTable.messageId, messageIds),
154
+ guard?.(messageTable.messageId),
155
+ ),
156
+ );
157
+ const removedParts = db
158
+ .select({
159
+ messageId: bodyPartTable.messageId,
160
+ partPath: bodyPartTable.partPath,
161
+ })
162
+ .from(bodyPartTable)
163
+ .where(
164
+ and(
165
+ inList(bodyPartTable.messageId, messageIds),
166
+ guard?.(bodyPartTable.messageId),
167
+ ),
168
+ );
169
+ if (messageIds.length === 0) {
170
+ return { removed, removedParts, statements: [] };
171
+ }
172
+ const statements: SqliteBatchItem[] = [
131
173
  db
132
174
  .delete(messageFlagTable)
133
- .where(and(inList(messageFlagTable.messageId, messageIds), guard)),
175
+ .where(
176
+ and(
177
+ inList(messageFlagTable.messageId, messageIds),
178
+ guard?.(messageFlagTable.messageId),
179
+ ),
180
+ ),
134
181
  db
135
182
  .delete(messageReferenceTable)
136
- .where(and(inList(messageReferenceTable.messageId, messageIds), guard)),
183
+ .where(
184
+ and(
185
+ inList(messageReferenceTable.messageId, messageIds),
186
+ guard?.(messageReferenceTable.messageId),
187
+ ),
188
+ ),
137
189
  db
138
190
  .delete(envelopeAddressTable)
139
- .where(and(inList(envelopeAddressTable.messageId, messageIds), guard)),
191
+ .where(
192
+ and(
193
+ inList(envelopeAddressTable.messageId, messageIds),
194
+ guard?.(envelopeAddressTable.messageId),
195
+ ),
196
+ ),
140
197
  db
141
198
  .delete(bodyPartParameterTable)
142
- .where(and(inList(bodyPartParameterTable.messageId, messageIds), guard)),
199
+ .where(
200
+ and(
201
+ inList(bodyPartParameterTable.messageId, messageIds),
202
+ guard?.(bodyPartParameterTable.messageId),
203
+ ),
204
+ ),
143
205
  db
144
206
  .delete(bodyPartStorageTable)
145
- .where(and(inList(bodyPartStorageTable.messageId, messageIds), guard)),
207
+ .where(
208
+ and(
209
+ inList(bodyPartStorageTable.messageId, messageIds),
210
+ guard?.(bodyPartStorageTable.messageId),
211
+ ),
212
+ ),
146
213
  db
147
214
  .delete(rawMessageStorageTable)
148
- .where(and(inList(rawMessageStorageTable.messageId, messageIds), guard)),
215
+ .where(
216
+ and(
217
+ inList(rawMessageStorageTable.messageId, messageIds),
218
+ guard?.(rawMessageStorageTable.messageId),
219
+ ),
220
+ ),
149
221
  db
150
222
  .delete(bodyPartTable)
151
- .where(and(inList(bodyPartTable.messageId, messageIds), guard)),
223
+ .where(
224
+ and(
225
+ inList(bodyPartTable.messageId, messageIds),
226
+ guard?.(bodyPartTable.messageId),
227
+ ),
228
+ ),
152
229
  db
153
230
  .delete(bodyStructureRefetchTable)
154
231
  .where(
155
- and(inList(bodyStructureRefetchTable.messageId, messageIds), guard),
232
+ and(
233
+ inList(bodyStructureRefetchTable.messageId, messageIds),
234
+ guard?.(bodyStructureRefetchTable.messageId),
235
+ ),
156
236
  ),
157
237
  db
158
238
  .delete(messageSearchTextTable)
159
- .where(and(inList(messageSearchTextTable.messageId, messageIds), guard)),
239
+ .where(
240
+ and(
241
+ inList(messageSearchTextTable.messageId, messageIds),
242
+ guard?.(messageSearchTextTable.messageId),
243
+ ),
244
+ ),
160
245
  db
161
246
  .delete(envelopeTable)
162
- .where(and(inList(envelopeTable.messageId, messageIds), guard)),
247
+ .where(
248
+ and(
249
+ inList(envelopeTable.messageId, messageIds),
250
+ guard?.(envelopeTable.messageId),
251
+ ),
252
+ ),
163
253
  db
164
254
  .delete(messageTable)
165
- .where(and(inList(messageTable.messageId, messageIds), guard))
166
- .returning({ messageId: messageTable.messageId }),
255
+ .where(
256
+ and(
257
+ inList(messageTable.messageId, messageIds),
258
+ guard?.(messageTable.messageId),
259
+ ),
260
+ ),
167
261
  ];
262
+ return { removed, removedParts, statements };
168
263
  }
169
264
 
170
265
  /**
@@ -198,13 +293,16 @@ const placementTerms = (expected: PlacementPredicate): SQL[] => {
198
293
  export class DrizzleMessageRepository implements IMessageRepository {
199
294
  private readonly db: DB;
200
295
  private readonly batch: WriteBatch;
296
+ private readonly maxBoundParameters: number;
201
297
 
202
298
  constructor(
203
299
  store: SqliteStore<Record<string, unknown>>,
204
300
  private search: SearchIndex,
301
+ private messageObjects: MessageObjects,
205
302
  ) {
206
303
  this.db = store.db;
207
304
  this.batch = store.batch;
305
+ this.maxBoundParameters = store.maxBoundParameters;
208
306
  }
209
307
 
210
308
  private rowOf(input: CreateMessageInput) {
@@ -459,16 +557,172 @@ export class DrizzleMessageRepository implements IMessageRepository {
459
557
  });
460
558
  }
461
559
 
462
- async copySearchText(
560
+ async copyMessage(
463
561
  sourceMessageId: string,
464
- targetMessageId: string,
465
- ): Promise<void> {
466
- const [source] = await this.db
467
- .select({ text: messageSearchTextTable.text })
468
- .from(messageSearchTextTable)
469
- .where(eq(messageSearchTextTable.messageId, sourceMessageId));
470
- if (!source) return;
471
- await this.setSearchText(targetMessageId, source.text);
562
+ input: CreateMessageInput,
563
+ ): Promise<{ item: MessageItem; created: boolean }> {
564
+ const targetMessageId = input.messageId;
565
+ const [searchTexts, envelopes, references, addresses, parts, parameters] =
566
+ await Promise.all([
567
+ this.db
568
+ .select({ text: messageSearchTextTable.text })
569
+ .from(messageSearchTextTable)
570
+ .where(eq(messageSearchTextTable.messageId, sourceMessageId)),
571
+ this.db
572
+ .select()
573
+ .from(envelopeTable)
574
+ .where(eq(envelopeTable.messageId, sourceMessageId)),
575
+ this.db
576
+ .select()
577
+ .from(messageReferenceTable)
578
+ .where(eq(messageReferenceTable.messageId, sourceMessageId)),
579
+ this.db
580
+ .select()
581
+ .from(envelopeAddressTable)
582
+ .where(eq(envelopeAddressTable.messageId, sourceMessageId)),
583
+ this.db
584
+ .select()
585
+ .from(bodyPartTable)
586
+ .where(eq(bodyPartTable.messageId, sourceMessageId)),
587
+ this.db
588
+ .select()
589
+ .from(bodyPartParameterTable)
590
+ .where(eq(bodyPartParameterTable.messageId, sourceMessageId)),
591
+ ]);
592
+
593
+ const now = Date.now();
594
+ const envelopeId = deriveEnvelopeId(targetMessageId);
595
+ const copiedPartIds = new Map(
596
+ parts.map((part) => [
597
+ part.bodyPartId,
598
+ deriveBodyPartId(targetMessageId, part.partPath),
599
+ ]),
600
+ );
601
+ const partPaths = new Map(
602
+ parts.map((part) => [part.bodyPartId, part.partPath]),
603
+ );
604
+
605
+ const searchTextRows = searchTexts.map((row) => ({
606
+ messageId: targetMessageId,
607
+ text: row.text,
608
+ createdAt: now,
609
+ updatedAt: now,
610
+ }));
611
+ const envelopeRows = envelopes.map((row) => ({
612
+ ...row,
613
+ envelopeId,
614
+ messageId: targetMessageId,
615
+ createdAt: now,
616
+ updatedAt: now,
617
+ }));
618
+ const referenceRows = references.map((row) => ({
619
+ ...row,
620
+ messageReferenceId: deriveMessageReferenceId(
621
+ targetMessageId,
622
+ row.referenceType,
623
+ row.referenceOrder,
624
+ ),
625
+ messageId: targetMessageId,
626
+ envelopeId,
627
+ createdAt: now,
628
+ updatedAt: now,
629
+ }));
630
+ const addressRows = addresses.map((row) => ({
631
+ ...row,
632
+ envelopeAddressId: deriveEnvelopeAddressId(
633
+ targetMessageId,
634
+ row.addressRole,
635
+ row.addressOrder,
636
+ ),
637
+ messageId: targetMessageId,
638
+ createdAt: now,
639
+ updatedAt: now,
640
+ }));
641
+ const partRows = parts.map((row) => ({
642
+ ...row,
643
+ bodyPartId: deriveBodyPartId(targetMessageId, row.partPath),
644
+ messageId: targetMessageId,
645
+ parentBodyPartId:
646
+ row.parentBodyPartId === null
647
+ ? null
648
+ : (copiedPartIds.get(row.parentBodyPartId) ?? null),
649
+ createdAt: now,
650
+ updatedAt: now,
651
+ }));
652
+ const parameterRows = parameters.flatMap((row) => {
653
+ const partPath = partPaths.get(row.bodyPartId);
654
+ if (partPath === undefined) return [];
655
+ return [
656
+ {
657
+ ...row,
658
+ bodyPartParameterId: deriveBodyPartParameterId(
659
+ targetMessageId,
660
+ partPath,
661
+ row.parameterName,
662
+ ),
663
+ messageId: targetMessageId,
664
+ bodyPartId: deriveBodyPartId(targetMessageId, partPath),
665
+ createdAt: now,
666
+ updatedAt: now,
667
+ },
668
+ ];
669
+ });
670
+
671
+ const insertCopy = this.db
672
+ .insert(messageTable)
673
+ .values(this.rowOf(input))
674
+ .onConflictDoNothing({ target: messageTable.messageId })
675
+ .returning();
676
+ const clones: SqliteBatchItem[] = [
677
+ ...searchTextRows.map((row) =>
678
+ this.db
679
+ .insert(messageSearchTextTable)
680
+ .values(row)
681
+ .onConflictDoNothing({ target: messageSearchTextTable.messageId }),
682
+ ),
683
+ ...chunkRows(envelopeTable, envelopeRows, this.maxBoundParameters).map(
684
+ (slice) =>
685
+ this.db.insert(envelopeTable).values(slice).onConflictDoNothing(),
686
+ ),
687
+ ...chunkRows(
688
+ messageReferenceTable,
689
+ referenceRows,
690
+ this.maxBoundParameters,
691
+ ).map((slice) =>
692
+ this.db
693
+ .insert(messageReferenceTable)
694
+ .values(slice)
695
+ .onConflictDoNothing(),
696
+ ),
697
+ ...chunkRows(
698
+ envelopeAddressTable,
699
+ addressRows,
700
+ this.maxBoundParameters,
701
+ ).map((slice) =>
702
+ this.db
703
+ .insert(envelopeAddressTable)
704
+ .values(slice)
705
+ .onConflictDoNothing(),
706
+ ),
707
+ ...chunkRows(bodyPartTable, partRows, this.maxBoundParameters).map(
708
+ (slice) =>
709
+ this.db.insert(bodyPartTable).values(slice).onConflictDoNothing(),
710
+ ),
711
+ ...chunkRows(
712
+ bodyPartParameterTable,
713
+ parameterRows,
714
+ this.maxBoundParameters,
715
+ ).map((slice) =>
716
+ this.db
717
+ .insert(bodyPartParameterTable)
718
+ .values(slice)
719
+ .onConflictDoNothing(),
720
+ ),
721
+ ];
722
+ const [inserted] = await this.batch([insertCopy, ...clones]);
723
+ const [row] = inserted;
724
+ if (row) return { item: toMessageItem(row), created: true };
725
+ return { item: await this.get(targetMessageId), created: false };
472
726
  }
473
727
 
474
728
  async syncSearch(messageIds: string[]): Promise<void> {
@@ -529,24 +783,65 @@ export class DrizzleMessageRepository implements IMessageRepository {
529
783
  ),
530
784
  ),
531
785
  );
532
- const listings = await this.db
533
- .select({ threadMessageId: threadMessageTable.threadMessageId })
534
- .from(threadMessageTable)
535
- .where(eq(threadMessageTable.messageId, messageId));
536
- const results = await this.batch([
537
- this.db
538
- .delete(threadMessageTable)
539
- .where(and(eq(threadMessageTable.messageId, messageId), placed)),
540
- ...deleteMessageSubtree(this.db, [messageId], placed),
541
- ]);
542
- await this.search.sync(listings.map((row) => row.threadMessageId));
543
- const removed = results.at(-1);
544
- return Array.isArray(removed) && removed.length > 0;
786
+ return (await this.removeMessages([messageId], () => placed)) > 0;
787
+ }
788
+
789
+ async deleteManyIfPlacement(
790
+ mailboxId: string,
791
+ placements: ReadonlyArray<{ messageId: string; uid: number }>,
792
+ ): Promise<void> {
793
+ const placed = this.db
794
+ .select({ messageId: messageTable.messageId })
795
+ .from(messageTable)
796
+ .where(
797
+ and(
798
+ eq(messageTable.mailboxId, mailboxId),
799
+ pairInList(
800
+ messageTable.messageId,
801
+ messageTable.uid,
802
+ placements.map(({ messageId, uid }) => [messageId, uid] as const),
803
+ ),
804
+ ),
805
+ );
806
+ await this.removeMessages(
807
+ placements.map((placement) => placement.messageId),
808
+ (column) => inArray(column, placed),
809
+ );
545
810
  }
546
811
 
547
812
  async deleteMany(messageIds: string[]): Promise<void> {
548
- if (messageIds.length === 0) return;
549
- await this.batch(deleteMessageSubtree(this.db, messageIds));
813
+ await this.removeMessages(messageIds);
814
+ }
815
+
816
+ private async removeMessages(
817
+ messageIds: readonly string[],
818
+ guard?: SubtreeGuard,
819
+ ): Promise<number> {
820
+ let removedCount = 0;
821
+ for (const batch of chunk(messageIds, MAIL_DELETE_BATCH_SIZE)) {
822
+ const listings = await this.db
823
+ .select({ threadMessageId: threadMessageTable.threadMessageId })
824
+ .from(threadMessageTable)
825
+ .where(inList(threadMessageTable.messageId, batch));
826
+ const subtree = deleteMessageSubtree(this.db, batch, guard);
827
+ const [removed, removedParts] = await this.batch([
828
+ subtree.removed,
829
+ subtree.removedParts,
830
+ this.db
831
+ .delete(threadMessageTable)
832
+ .where(
833
+ and(
834
+ inList(threadMessageTable.messageId, batch),
835
+ guard?.(threadMessageTable.messageId),
836
+ ),
837
+ ),
838
+ ...subtree.statements,
839
+ ]);
840
+ await this.search.sync(listings.map((row) => row.threadMessageId));
841
+ await this.messageObjects.reclaim(removed, removedParts);
842
+ removedCount += removed.length;
843
+ }
844
+ return removedCount;
550
845
  }
551
846
 
552
847
  async listByMailbox(
@@ -16,6 +16,7 @@ import { messageSearchTextTable } from "../schema/message-data.js";
16
16
  import { threadMessageTable } from "../schema/thread-message.js";
17
17
  import { createSqliteRepositories } from "../sqlite-repositories.js";
18
18
  import { createSqliteTestDb } from "../test-db-sqlite.js";
19
+ import { messageObjectsInMemory } from "./message-objects.js";
19
20
  import type { DrizzleThreadMessageRepository } from "./thread-message.js";
20
21
 
21
22
  const ACCOUNT = "acct-seam";
@@ -68,6 +69,7 @@ describe("thread-message body and muted-sender predicates (sqlite)", () => {
68
69
  search: db,
69
70
  },
70
71
  BETTER_SQLITE3_LIMITS,
72
+ messageObjectsInMemory(),
71
73
  ).threadMessage;
72
74
  });
73
75
 
@@ -6,6 +6,7 @@ import { messageSearchTextTable } from "../schema/message-data.js";
6
6
  import { threadMessageTable } from "../schema/thread-message.js";
7
7
  import { createSqliteRepositories } from "../sqlite-repositories.js";
8
8
  import { createSqliteTestDb } from "../test-db-sqlite.js";
9
+ import { messageObjectsInMemory } from "./message-objects.js";
9
10
  import {
10
11
  type DrizzleThreadMessageRepository,
11
12
  THREAD_SEARCH_MAX_LIMIT,
@@ -67,6 +68,7 @@ describe("DrizzleThreadMessageRepository.listByFieldTerms (sqlite, #459)", () =>
67
68
  search: created.db,
68
69
  },
69
70
  BETTER_SQLITE3_LIMITS,
71
+ messageObjectsInMemory(),
70
72
  ).threadMessage;
71
73
 
72
74
  await repo.create(
@@ -13,6 +13,7 @@ import {
13
13
  LIVE_TOTALS,
14
14
  totalRows,
15
15
  } from "./category-fixture.js";
16
+ import { messageObjectsInMemory } from "./message-objects.js";
16
17
  import {
17
18
  type DrizzleThreadMessageRepository,
18
19
  THREAD_SEARCH_MAX_LIMIT,
@@ -68,6 +69,7 @@ describe("DrizzleThreadMessageRepository (sqlite)", () => {
68
69
  search: db,
69
70
  },
70
71
  BETTER_SQLITE3_LIMITS,
72
+ messageObjectsInMemory(),
71
73
  ).threadMessage;
72
74
  });
73
75
 
@@ -10,6 +10,7 @@ import { messageSearchTextTable } from "../schema/message-data.js";
10
10
  import { threadMessageTable } from "../schema/thread-message.js";
11
11
  import { createSqliteRepositories } from "../sqlite-repositories.js";
12
12
  import { createSqliteTestDb } from "../test-db-sqlite.js";
13
+ import { messageObjectsInMemory } from "./message-objects.js";
13
14
  import {
14
15
  clampThreadSearchLimit,
15
16
  type DrizzleThreadMessageRepository,
@@ -102,6 +103,7 @@ describe("DrizzleThreadMessageRepository.searchByMailboxWindow / countByMailbox"
102
103
  search: harness.db,
103
104
  },
104
105
  BETTER_SQLITE3_LIMITS,
106
+ messageObjectsInMemory(),
105
107
  ).threadMessage;
106
108
  });
107
109
 
@@ -411,6 +413,7 @@ describe("DrizzleThreadMessageRepository.searchByDate", () => {
411
413
  search: harness.db,
412
414
  },
413
415
  BETTER_SQLITE3_LIMITS,
416
+ messageObjectsInMemory(),
414
417
  );
415
418
  repo = repositories.threadMessage;
416
419
  messages = repositories.message;
@@ -949,6 +952,7 @@ describe("DrizzleThreadMessageRepository — native text search", () => {
949
952
  search: harness.db,
950
953
  },
951
954
  BETTER_SQLITE3_LIMITS,
955
+ messageObjectsInMemory(),
952
956
  ).threadMessage;
953
957
  });
954
958
 
@@ -1128,6 +1132,7 @@ describe("DrizzleThreadMessageRepository — core CRUD", () => {
1128
1132
  search: harness.db,
1129
1133
  },
1130
1134
  BETTER_SQLITE3_LIMITS,
1135
+ messageObjectsInMemory(),
1131
1136
  ).threadMessage;
1132
1137
  });
1133
1138
 
@@ -6,6 +6,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
6
6
  import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
7
7
  import { syncStore } from "../databases.js";
8
8
  import type { Db } from "../db.js";
9
+ import { messageObjectsInMemory } from "../repos/message-objects.js";
9
10
  import { messageSearchTextTable } from "../schema/message-data.js";
10
11
  import { createSqliteRepositories } from "../sqlite-repositories.js";
11
12
  import { applyMigrationSet } from "../test-shipped-sqlite-schema.js";
@@ -62,6 +63,7 @@ describe("the thread-message search index in its own database", () => {
62
63
  search: search.db,
63
64
  },
64
65
  BETTER_SQLITE3_LIMITS,
66
+ messageObjectsInMemory(),
65
67
  ).threadMessage;
66
68
 
67
69
  const found = async (query: string): Promise<string[]> =>
@@ -31,20 +31,29 @@ import { LabelRepo } from "./repos/label.js";
31
31
  import { DrizzleMessageRepository } from "./repos/message.js";
32
32
  import { DrizzleMessageFlagRepository } from "./repos/message-flag.js";
33
33
  import { MessageLabelRepo } from "./repos/message-label.js";
34
+ import {
35
+ type MessageObjectStore,
36
+ MessageObjects,
37
+ } from "./repos/message-objects.js";
34
38
  import { QuarantineRepo } from "./repos/quarantine.js";
35
39
  import { SyncWarningRepo } from "./repos/sync-warning.js";
36
40
  import { DrizzleThreadMessageRepository } from "./repos/thread-message.js";
37
41
  import { SearchIndex } from "./search/thread-message-search.js";
38
42
 
39
- export const composeSqliteRepositories = ({
40
- app,
41
- mail,
42
- search,
43
- }: SqliteStores) => {
43
+ export const composeSqliteRepositories = (
44
+ { app, mail, search }: SqliteStores,
45
+ objects: MessageObjectStore,
46
+ ) => {
44
47
  const searchIndex = new SearchIndex(mail.db, search);
48
+ const messageObjects = new MessageObjects(mail.db, objects);
45
49
  const accountSetting = new AccountSettingRepo(app.db);
46
50
  const address = new AddressRepo(mail.db);
47
- const mailbox = new MailboxRepo(mail, searchIndex);
51
+ const message = new DrizzleMessageRepository(
52
+ mail,
53
+ searchIndex,
54
+ messageObjects,
55
+ );
56
+ const mailbox = new MailboxRepo(mail, message);
48
57
  const account = new AccountRepo(app.db, mailbox);
49
58
  const calendarSuggestion = new CalendarSuggestionRepo(mail.db);
50
59
  return {
@@ -59,7 +68,7 @@ export const composeSqliteRepositories = ({
59
68
  accountSetting,
60
69
  ),
61
70
  mailboxLock: new MailboxLockRepo(mail.db),
62
- message: new DrizzleMessageRepository(mail, searchIndex),
71
+ message,
63
72
  messageFlag: new DrizzleMessageFlagRepository(mail.db),
64
73
  outboxMessage: new OutboxMessageRepo(mail.db),
65
74
  outboxAttachment: new OutboxAttachmentRepo(mail),
@@ -90,5 +99,6 @@ export type SqliteRepositories = ReturnType<typeof composeSqliteRepositories>;
90
99
  export const createSqliteRepositories = (
91
100
  databases: SqliteDatabases,
92
101
  limits: SqliteLimits,
102
+ objects: MessageObjectStore,
93
103
  ): SqliteRepositories =>
94
- composeSqliteRepositories(syncStores(databases, limits));
104
+ composeSqliteRepositories(syncStores(databases, limits), objects);