@remit/drizzle-service 0.0.107 → 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.
- package/package.json +1 -1
- package/src/id.ts +9 -0
- package/src/index.ts +0 -1
- package/src/repos/envelope.ts +5 -3
- package/src/repos/i4-mailbox.test.ts +0 -2
- package/src/repos/i4-mailbox.ts +7 -33
- package/src/repos/message-objects.sqlite.test.ts +75 -21
- package/src/repos/message-objects.ts +37 -43
- package/src/repos/message.sqlite.test.ts +9 -17
- package/src/repos/message.test.ts +2 -6
- package/src/repos/message.ts +327 -52
- package/src/sqlite-repositories.ts +7 -2
package/package.json
CHANGED
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
|
@@ -75,7 +75,6 @@ export {
|
|
|
75
75
|
export { DrizzleMessageFlagRepository } from "./repos/message-flag.js";
|
|
76
76
|
export { MessageLabelRepo } from "./repos/message-label.js";
|
|
77
77
|
export {
|
|
78
|
-
DEFAULT_MESSAGE_OBJECT_DELETE_CONCURRENCY,
|
|
79
78
|
type MessageObjectStore,
|
|
80
79
|
messageObjectsInMemory,
|
|
81
80
|
} from "./repos/message-objects.js";
|
package/src/repos/envelope.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
bodyPartId as deriveBodyPartId,
|
|
22
22
|
bodyPartParameterId as deriveBodyPartParameterId,
|
|
23
23
|
envelopeId as deriveEnvelopeId,
|
|
24
|
-
|
|
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:
|
|
233
|
-
|
|
232
|
+
messageReferenceId: deriveMessageReferenceId(
|
|
233
|
+
messageId,
|
|
234
|
+
reference.referenceType,
|
|
235
|
+
reference.referenceOrder,
|
|
234
236
|
),
|
|
235
237
|
messageId,
|
|
236
238
|
envelopeId,
|
|
@@ -564,8 +564,6 @@ describe("MailboxRepo", () => {
|
|
|
564
564
|
sequenceNumber: uid,
|
|
565
565
|
rfc822Size: 10,
|
|
566
566
|
internalDate: 1700000000000,
|
|
567
|
-
envelopeId: deriveEnvelopeId(messageId),
|
|
568
|
-
rootBodyPartId: deriveRootBodyPartId(messageId),
|
|
569
567
|
});
|
|
570
568
|
await db.insert(threadMessageTable).values({
|
|
571
569
|
threadMessageId: randomId(),
|
package/src/repos/i4-mailbox.ts
CHANGED
|
@@ -42,11 +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 {
|
|
49
|
-
import type { MessageObjects } from "./message-objects.js";
|
|
46
|
+
import type { DrizzleMessageRepository } from "./message.js";
|
|
50
47
|
|
|
51
48
|
const base36Translator = shortUuid.createTranslator(
|
|
52
49
|
shortUuid.constants.uuid25Base36,
|
|
@@ -61,7 +58,6 @@ type DB = RepositoryDb<Record<string, unknown>>;
|
|
|
61
58
|
* the file's write lock, so the bound is what keeps a large folder's delete from
|
|
62
59
|
* parking every other writer behind it (D8).
|
|
63
60
|
*/
|
|
64
|
-
const MAIL_DELETE_BATCH_SIZE = 100;
|
|
65
61
|
|
|
66
62
|
/**
|
|
67
63
|
* The folders under `path`. A flat namespace nests nothing: with no delimiter
|
|
@@ -155,8 +151,7 @@ export class MailboxRepo implements IMailboxRepository {
|
|
|
155
151
|
|
|
156
152
|
constructor(
|
|
157
153
|
store: SqliteStore<Record<string, unknown>>,
|
|
158
|
-
private
|
|
159
|
-
private messageObjects: MessageObjects,
|
|
154
|
+
private messages: Pick<DrizzleMessageRepository, "deleteMany">,
|
|
160
155
|
) {
|
|
161
156
|
this.db = store.db;
|
|
162
157
|
this.batch = store.batch;
|
|
@@ -549,32 +544,11 @@ export class MailboxRepo implements IMailboxRepository {
|
|
|
549
544
|
// Ordered, batched and resumable rather than one transaction (D8). The
|
|
550
545
|
// caller keeps the row `deleting` until the last commit, so an interrupted
|
|
551
546
|
// run re-enters here and continues against whatever is left.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
.limit(MAIL_DELETE_BATCH_SIZE);
|
|
558
|
-
if (rows.length === 0) break;
|
|
559
|
-
const messageIds = rows.map((row) => row.messageId);
|
|
560
|
-
const listings = await this.db
|
|
561
|
-
.select({ threadMessageId: threadMessageTable.threadMessageId })
|
|
562
|
-
.from(threadMessageTable)
|
|
563
|
-
.where(inList(threadMessageTable.messageId, messageIds));
|
|
564
|
-
|
|
565
|
-
// The primitive the rest of the codebase deletes mail with: nine
|
|
566
|
-
// per-message child tables. A bespoke table list would orphan them.
|
|
567
|
-
const subtree = deleteMessageSubtree(this.db, messageIds);
|
|
568
|
-
const [removed] = await this.batch([
|
|
569
|
-
subtree.removed,
|
|
570
|
-
...subtree.statements,
|
|
571
|
-
this.db
|
|
572
|
-
.delete(threadMessageTable)
|
|
573
|
-
.where(inList(threadMessageTable.messageId, messageIds)),
|
|
574
|
-
]);
|
|
575
|
-
await this.search.sync(listings.map((row) => row.threadMessageId));
|
|
576
|
-
await this.messageObjects.reclaim(removed);
|
|
577
|
-
}
|
|
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));
|
|
578
552
|
|
|
579
553
|
await this.db
|
|
580
554
|
.delete(mailboxSpecialUseTable)
|
|
@@ -12,10 +12,6 @@ import { after, before, describe, test } from "node:test";
|
|
|
12
12
|
import type { StorageService } from "@remit/storage-service";
|
|
13
13
|
import { createFilesystemStorageService } from "@remit/storage-service/filesystem";
|
|
14
14
|
import { BETTER_SQLITE3_LIMITS } from "../bound-parameters.js";
|
|
15
|
-
import {
|
|
16
|
-
envelopeId as deriveEnvelopeId,
|
|
17
|
-
rootBodyPartId as deriveRootBodyPartId,
|
|
18
|
-
} from "../id.js";
|
|
19
15
|
import {
|
|
20
16
|
createSqliteRepositories,
|
|
21
17
|
type SqliteRepositories,
|
|
@@ -25,6 +21,17 @@ import { mailboxCreated } from "./test-helpers.js";
|
|
|
25
21
|
|
|
26
22
|
const ACCOUNT_CONFIG_ID = "cfg-objects";
|
|
27
23
|
|
|
24
|
+
const bodyPart = (partPath: string) => ({
|
|
25
|
+
partPath,
|
|
26
|
+
parentPartPath: null,
|
|
27
|
+
mediaType: "TEXT" as const,
|
|
28
|
+
mediaSubtype: "plain",
|
|
29
|
+
transferEncoding: "7BIT" as const,
|
|
30
|
+
sizeOctets: 4,
|
|
31
|
+
isMultipart: false,
|
|
32
|
+
parameters: [],
|
|
33
|
+
});
|
|
34
|
+
|
|
28
35
|
describe("message objects follow the last row that references them", () => {
|
|
29
36
|
let db: TestDb;
|
|
30
37
|
let close: () => Promise<void>;
|
|
@@ -32,26 +39,26 @@ describe("message objects follow the last row that references them", () => {
|
|
|
32
39
|
let storage: StorageService;
|
|
33
40
|
let failDeletesAfter = Number.POSITIVE_INFINITY;
|
|
34
41
|
let repos: SqliteRepositories;
|
|
42
|
+
let deleteCalls: number[] = [];
|
|
35
43
|
|
|
36
44
|
before(async () => {
|
|
37
45
|
({ db, close } = await createTestDb());
|
|
38
46
|
directory = mkdtempSync(join(tmpdir(), "message-objects-"));
|
|
39
47
|
storage = createFilesystemStorageService(directory);
|
|
48
|
+
const counted: Pick<StorageService, "deleteKeys"> = {
|
|
49
|
+
deleteKeys: (keys) => {
|
|
50
|
+
deleteCalls.push(keys.length);
|
|
51
|
+
if (failDeletesAfter <= 0) {
|
|
52
|
+
return Promise.reject(new Error("storage went away"));
|
|
53
|
+
}
|
|
54
|
+
failDeletesAfter -= 1;
|
|
55
|
+
return storage.deleteKeys(keys);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
40
58
|
repos = createSqliteRepositories(
|
|
41
59
|
{ app: db, mail: db, search: db },
|
|
42
60
|
BETTER_SQLITE3_LIMITS,
|
|
43
|
-
{
|
|
44
|
-
storage: {
|
|
45
|
-
deletePrefix: (prefix) => {
|
|
46
|
-
if (failDeletesAfter <= 0) {
|
|
47
|
-
return Promise.reject(new Error("storage went away"));
|
|
48
|
-
}
|
|
49
|
-
failDeletesAfter -= 1;
|
|
50
|
-
return storage.deletePrefix(prefix);
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
messageObjectDeleteConcurrency: 1,
|
|
54
|
-
},
|
|
61
|
+
{ storage: counted },
|
|
55
62
|
);
|
|
56
63
|
});
|
|
57
64
|
|
|
@@ -109,10 +116,9 @@ describe("message objects follow the last row that references them", () => {
|
|
|
109
116
|
sequenceNumber: uid,
|
|
110
117
|
rfc822Size: 10,
|
|
111
118
|
internalDate: 1700000000000,
|
|
112
|
-
envelopeId: deriveEnvelopeId(messageId),
|
|
113
|
-
rootBodyPartId: deriveRootBodyPartId(messageId),
|
|
114
119
|
bodyStorageKey: stored.uri,
|
|
115
120
|
});
|
|
121
|
+
await repos.envelope.upsertBodyParts(messageId, [bodyPart("1")]);
|
|
116
122
|
return { messageId, accountId, bodyStorageKey: stored.uri };
|
|
117
123
|
};
|
|
118
124
|
|
|
@@ -184,10 +190,9 @@ describe("message objects follow the last row that references them", () => {
|
|
|
184
190
|
sequenceNumber: 1,
|
|
185
191
|
rfc822Size: 10,
|
|
186
192
|
internalDate: 1700000000000,
|
|
187
|
-
envelopeId: deriveEnvelopeId(messageId),
|
|
188
|
-
rootBodyPartId: deriveRootBodyPartId(messageId),
|
|
189
193
|
bodyStorageKey: `file://${join(legacy, "body.eml")}`,
|
|
190
194
|
});
|
|
195
|
+
await repos.envelope.upsertBodyParts(messageId, [bodyPart("1")]);
|
|
191
196
|
|
|
192
197
|
await repos.message.delete(messageId);
|
|
193
198
|
|
|
@@ -201,7 +206,7 @@ describe("message objects follow the last row that references them", () => {
|
|
|
201
206
|
for (let uid = 1; uid <= 101; uid += 1) {
|
|
202
207
|
rows.push(await message(accountId, doomed.mailboxId, uid));
|
|
203
208
|
}
|
|
204
|
-
failDeletesAfter =
|
|
209
|
+
failDeletesAfter = 1;
|
|
205
210
|
|
|
206
211
|
await assert.rejects(
|
|
207
212
|
repos.mailbox.deleteMailboxWithMail(accountId, doomed.mailboxId),
|
|
@@ -214,4 +219,53 @@ describe("message objects follow the last row that references them", () => {
|
|
|
214
219
|
);
|
|
215
220
|
assert.equal(gone.filter((exists) => !exists).length, 100);
|
|
216
221
|
});
|
|
222
|
+
|
|
223
|
+
const gone = async (
|
|
224
|
+
rows: readonly {
|
|
225
|
+
accountId: string;
|
|
226
|
+
messageId: string;
|
|
227
|
+
bodyStorageKey: string;
|
|
228
|
+
}[],
|
|
229
|
+
) => {
|
|
230
|
+
for (const row of rows) {
|
|
231
|
+
assert.equal(await storage.exists(row.bodyStorageKey), false);
|
|
232
|
+
assert.equal(await part(row), null);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
test("deleting many messages makes one storage call per batch and keeps a body a copy shares", async () => {
|
|
237
|
+
const accountId = randomId();
|
|
238
|
+
const inbox = await folder(accountId, "INBOX");
|
|
239
|
+
const rows = [];
|
|
240
|
+
for (let uid = 1; uid <= 300; uid += 1) {
|
|
241
|
+
rows.push(await message(accountId, inbox.mailboxId, uid));
|
|
242
|
+
}
|
|
243
|
+
const [shared, ...unshared] = rows;
|
|
244
|
+
const copy = await message(accountId, inbox.mailboxId, 301, {
|
|
245
|
+
from: shared.messageId,
|
|
246
|
+
});
|
|
247
|
+
deleteCalls = [];
|
|
248
|
+
|
|
249
|
+
await repos.message.deleteMany(rows.map((row) => row.messageId));
|
|
250
|
+
|
|
251
|
+
assert.equal(deleteCalls.length, 3);
|
|
252
|
+
assert.ok(await storage.retrieve(copy.bodyStorageKey));
|
|
253
|
+
assert.ok(await part(copy));
|
|
254
|
+
await gone(unshared);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a folder delete makes one storage call per batch and removes every object", async () => {
|
|
258
|
+
const accountId = randomId();
|
|
259
|
+
const doomed = await folder(accountId, "Archive");
|
|
260
|
+
const rows = [];
|
|
261
|
+
for (let uid = 1; uid <= 250; uid += 1) {
|
|
262
|
+
rows.push(await message(accountId, doomed.mailboxId, uid));
|
|
263
|
+
}
|
|
264
|
+
deleteCalls = [];
|
|
265
|
+
|
|
266
|
+
await repos.mailbox.deleteMailboxWithMail(accountId, doomed.mailboxId);
|
|
267
|
+
|
|
268
|
+
assert.equal(deleteCalls.length, 3);
|
|
269
|
+
await gone(rows);
|
|
270
|
+
});
|
|
217
271
|
});
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMockStorageService,
|
|
3
|
-
|
|
3
|
+
messageObjectKeysOf,
|
|
4
4
|
type StorageService,
|
|
5
5
|
} from "@remit/storage-service";
|
|
6
|
-
import pMap from "p-map";
|
|
7
6
|
import type { RepositoryDb } from "../db.js";
|
|
8
7
|
import { messageTable } from "../schema/message-data.js";
|
|
9
8
|
import { inList } from "./in-list.js";
|
|
10
9
|
|
|
11
|
-
export const DEFAULT_MESSAGE_OBJECT_DELETE_CONCURRENCY = 8;
|
|
12
|
-
|
|
13
10
|
export interface MessageObjectStore {
|
|
14
|
-
storage: Pick<StorageService, "
|
|
15
|
-
messageObjectDeleteConcurrency: number;
|
|
11
|
+
storage: Pick<StorageService, "deleteKeys">;
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
export const messageObjectsInMemory = (): MessageObjectStore => ({
|
|
19
15
|
storage: createMockStorageService(),
|
|
20
|
-
messageObjectDeleteConcurrency: 1,
|
|
21
16
|
});
|
|
22
17
|
|
|
23
18
|
export interface RemovedMessageRow {
|
|
@@ -25,52 +20,51 @@ export interface RemovedMessageRow {
|
|
|
25
20
|
bodyStorageKey: string | null;
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
interface
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
export interface RemovedBodyPartRow {
|
|
24
|
+
messageId: string;
|
|
25
|
+
partPath: string;
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
const candidatesOf = (row: RemovedMessageRow): Candidate[] => {
|
|
34
|
-
const { messageId, bodyStorageKey } = row;
|
|
35
|
-
if (bodyStorageKey === null) return [];
|
|
36
|
-
const ownerPrefix = messageObjectPrefixOf(bodyStorageKey);
|
|
37
|
-
const ownPrefix = ownerPrefix.replace(/[^/]+\/$/, `${messageId}/`);
|
|
38
|
-
const owner = { prefix: ownerPrefix, bodyStorageKey };
|
|
39
|
-
if (ownPrefix === ownerPrefix) return [owner];
|
|
40
|
-
const at = bodyStorageKey.lastIndexOf(ownerPrefix);
|
|
41
|
-
const ownBodyStorageKey = `${bodyStorageKey.slice(0, at)}${ownPrefix}${bodyStorageKey.slice(at + ownerPrefix.length)}`;
|
|
42
|
-
return [owner, { prefix: ownPrefix, bodyStorageKey: ownBodyStorageKey }];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
28
|
export class MessageObjects {
|
|
46
29
|
constructor(
|
|
47
30
|
private readonly db: Pick<RepositoryDb<Record<string, unknown>>, "select">,
|
|
48
31
|
private readonly store: MessageObjectStore,
|
|
49
32
|
) {}
|
|
50
33
|
|
|
51
|
-
async reclaim(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
async reclaim(
|
|
35
|
+
removed: readonly RemovedMessageRow[],
|
|
36
|
+
removedParts: readonly RemovedBodyPartRow[],
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
const partPathsOf = new Map<string, string[]>();
|
|
39
|
+
for (const { messageId, partPath } of removedParts) {
|
|
40
|
+
partPathsOf.set(messageId, [
|
|
41
|
+
...(partPathsOf.get(messageId) ?? []),
|
|
42
|
+
partPath,
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
const own: string[] = [];
|
|
46
|
+
const sharedBy = new Map<string, string[]>();
|
|
47
|
+
for (const { messageId, bodyStorageKey } of removed) {
|
|
48
|
+
if (bodyStorageKey === null) continue;
|
|
49
|
+
const keys = messageObjectKeysOf(
|
|
50
|
+
bodyStorageKey,
|
|
51
|
+
messageId,
|
|
52
|
+
partPathsOf.get(messageId) ?? [],
|
|
53
|
+
);
|
|
54
|
+
own.push(...keys.own);
|
|
55
|
+
sharedBy.set(bodyStorageKey, keys.shared);
|
|
56
|
+
}
|
|
57
|
+
if (sharedBy.size === 0) return;
|
|
59
58
|
const referenced = await this.db
|
|
60
59
|
.select({ bodyStorageKey: messageTable.bodyStorageKey })
|
|
61
60
|
.from(messageTable)
|
|
62
|
-
.where(inList(messageTable.bodyStorageKey, keys));
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
(c) => !stillReferenced.has(c.bodyStorageKey),
|
|
71
|
-
),
|
|
72
|
-
(c) => this.store.storage.deletePrefix(c.prefix),
|
|
73
|
-
{ concurrency: this.store.messageObjectDeleteConcurrency },
|
|
74
|
-
);
|
|
61
|
+
.where(inList(messageTable.bodyStorageKey, [...sharedBy.keys()]));
|
|
62
|
+
for (const { bodyStorageKey } of referenced) {
|
|
63
|
+
if (bodyStorageKey !== null) sharedBy.delete(bodyStorageKey);
|
|
64
|
+
}
|
|
65
|
+
await this.store.storage.deleteKeys([
|
|
66
|
+
...own,
|
|
67
|
+
...[...sharedBy.values()].flat(),
|
|
68
|
+
]);
|
|
75
69
|
}
|
|
76
70
|
}
|
|
@@ -85,8 +85,6 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
85
85
|
const created = await repo.create({
|
|
86
86
|
...BASE_INPUT,
|
|
87
87
|
messageId: AUTO_MOVED_ID,
|
|
88
|
-
envelopeId: deriveEnvelopeId(AUTO_MOVED_ID),
|
|
89
|
-
rootBodyPartId: deriveRootBodyPartId(AUTO_MOVED_ID),
|
|
90
88
|
movedByRemit: true,
|
|
91
89
|
filterMove: filterMove as unknown as never,
|
|
92
90
|
});
|
|
@@ -160,8 +158,6 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
160
158
|
sequenceNumber: 2,
|
|
161
159
|
rfc822Size: 1024,
|
|
162
160
|
internalDate: NOW,
|
|
163
|
-
envelopeId: deriveEnvelopeId(inboundId),
|
|
164
|
-
rootBodyPartId: deriveRootBodyPartId(inboundId),
|
|
165
161
|
});
|
|
166
162
|
|
|
167
163
|
assert.equal(created, true);
|
|
@@ -182,8 +178,6 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
182
178
|
...BASE_INPUT,
|
|
183
179
|
messageId: LOCK_ID,
|
|
184
180
|
uid: 7,
|
|
185
|
-
envelopeId: deriveEnvelopeId(LOCK_ID),
|
|
186
|
-
rootBodyPartId: deriveRootBodyPartId(LOCK_ID),
|
|
187
181
|
status: "moving" as const,
|
|
188
182
|
syncStatus: "pending" as const,
|
|
189
183
|
});
|
|
@@ -255,8 +249,6 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
255
249
|
...BASE_INPUT,
|
|
256
250
|
messageId: GONE_ID,
|
|
257
251
|
uid: 9,
|
|
258
|
-
envelopeId: deriveEnvelopeId(GONE_ID),
|
|
259
|
-
rootBodyPartId: deriveRootBodyPartId(GONE_ID),
|
|
260
252
|
});
|
|
261
253
|
});
|
|
262
254
|
|
|
@@ -319,19 +311,19 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
319
311
|
);
|
|
320
312
|
});
|
|
321
313
|
|
|
322
|
-
test("
|
|
314
|
+
test("copyMessage gives a copy the source's search text", async () => {
|
|
323
315
|
const source = "00000000-0000-0000-4444-000000000001";
|
|
324
316
|
const copy = "00000000-0000-0000-4444-000000000002";
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
[copy, 202],
|
|
328
|
-
] as const) {
|
|
329
|
-
await repo.create({ ...BASE_INPUT, messageId, uid });
|
|
330
|
-
await repo.update(messageId, { bodyStorageKey: "file:///bodies/shared" });
|
|
331
|
-
}
|
|
317
|
+
await repo.create({ ...BASE_INPUT, messageId: source, uid: 201 });
|
|
318
|
+
await repo.update(source, { bodyStorageKey: "file:///bodies/shared" });
|
|
332
319
|
await repo.setSearchText(source, "the shared body");
|
|
333
320
|
|
|
334
|
-
await repo.
|
|
321
|
+
await repo.copyMessage(source, {
|
|
322
|
+
...BASE_INPUT,
|
|
323
|
+
messageId: copy,
|
|
324
|
+
uid: 202,
|
|
325
|
+
bodyStorageKey: "file:///bodies/shared",
|
|
326
|
+
});
|
|
335
327
|
|
|
336
328
|
const missing = await repo.listMissingSearchText("", 100);
|
|
337
329
|
assert.equal(
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
rootBodyPartId as deriveRootBodyPartId,
|
|
7
7
|
} from "../id.js";
|
|
8
8
|
import { createSqliteRepositories } from "../sqlite-repositories.js";
|
|
9
|
+
import { createTestDb, type TestDb } from "../test-db.js";
|
|
9
10
|
import type { DrizzleEnvelopeRepository } from "./envelope.js";
|
|
10
11
|
import type { DrizzleMessageRepository } from "./message.js";
|
|
11
12
|
import { messageObjectsInMemory } from "./message-objects.js";
|
|
12
|
-
import { createTestDb, type TestDb } from "./test-helpers.js";
|
|
13
13
|
|
|
14
14
|
describe("DrizzleMessageRepository", () => {
|
|
15
15
|
let db: TestDb;
|
|
@@ -38,7 +38,7 @@ describe("DrizzleMessageRepository", () => {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
before(async () => {
|
|
41
|
-
({ db, stop } = await createTestDb());
|
|
41
|
+
({ db, close: stop } = await createTestDb());
|
|
42
42
|
const repos = createSqliteRepositories(
|
|
43
43
|
{ app: db, mail: db, search: db },
|
|
44
44
|
BETTER_SQLITE3_LIMITS,
|
|
@@ -149,8 +149,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
149
149
|
sequenceNumber: 1,
|
|
150
150
|
rfc822Size: 512,
|
|
151
151
|
internalDate: NOW,
|
|
152
|
-
envelopeId: deriveEnvelopeId(MOVE_MESSAGE_ID),
|
|
153
|
-
rootBodyPartId: deriveRootBodyPartId(MOVE_MESSAGE_ID),
|
|
154
152
|
status: "active" as const,
|
|
155
153
|
syncStatus: "synced" as const,
|
|
156
154
|
});
|
|
@@ -264,8 +262,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
264
262
|
sequenceNumber: 1,
|
|
265
263
|
rfc822Size: 10,
|
|
266
264
|
internalDate: NOW,
|
|
267
|
-
envelopeId: DEL_ENVELOPE_ID,
|
|
268
|
-
rootBodyPartId: DEL_ROOT_BODY_PART_ID,
|
|
269
265
|
status: "active",
|
|
270
266
|
syncStatus: "synced",
|
|
271
267
|
});
|
package/src/repos/message.ts
CHANGED
|
@@ -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,
|
|
@@ -118,18 +124,23 @@ export type SubtreeDb = Pick<
|
|
|
118
124
|
"delete" | "select"
|
|
119
125
|
>;
|
|
120
126
|
|
|
127
|
+
export type SubtreeGuard = (messageId: Column) => SQL;
|
|
128
|
+
|
|
129
|
+
export const MAIL_DELETE_BATCH_SIZE = 100;
|
|
130
|
+
|
|
121
131
|
/**
|
|
122
132
|
* The statements that delete a message and its whole per-message subtree by
|
|
123
133
|
* message id: the child tables, then the message row. The caller runs them as
|
|
124
|
-
* one batch so the subtree goes atomically. `removed`
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* statement
|
|
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.
|
|
128
139
|
*/
|
|
129
140
|
export function deleteMessageSubtree(
|
|
130
141
|
db: SubtreeDb,
|
|
131
|
-
messageIds: string[],
|
|
132
|
-
guard?:
|
|
142
|
+
messageIds: readonly string[],
|
|
143
|
+
guard?: SubtreeGuard,
|
|
133
144
|
) {
|
|
134
145
|
const removed = db
|
|
135
146
|
.select({
|
|
@@ -137,46 +148,118 @@ export function deleteMessageSubtree(
|
|
|
137
148
|
bodyStorageKey: messageTable.bodyStorageKey,
|
|
138
149
|
})
|
|
139
150
|
.from(messageTable)
|
|
140
|
-
.where(
|
|
141
|
-
|
|
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
|
+
}
|
|
142
172
|
const statements: SqliteBatchItem[] = [
|
|
143
173
|
db
|
|
144
174
|
.delete(messageFlagTable)
|
|
145
|
-
.where(
|
|
175
|
+
.where(
|
|
176
|
+
and(
|
|
177
|
+
inList(messageFlagTable.messageId, messageIds),
|
|
178
|
+
guard?.(messageFlagTable.messageId),
|
|
179
|
+
),
|
|
180
|
+
),
|
|
146
181
|
db
|
|
147
182
|
.delete(messageReferenceTable)
|
|
148
|
-
.where(
|
|
183
|
+
.where(
|
|
184
|
+
and(
|
|
185
|
+
inList(messageReferenceTable.messageId, messageIds),
|
|
186
|
+
guard?.(messageReferenceTable.messageId),
|
|
187
|
+
),
|
|
188
|
+
),
|
|
149
189
|
db
|
|
150
190
|
.delete(envelopeAddressTable)
|
|
151
|
-
.where(
|
|
191
|
+
.where(
|
|
192
|
+
and(
|
|
193
|
+
inList(envelopeAddressTable.messageId, messageIds),
|
|
194
|
+
guard?.(envelopeAddressTable.messageId),
|
|
195
|
+
),
|
|
196
|
+
),
|
|
152
197
|
db
|
|
153
198
|
.delete(bodyPartParameterTable)
|
|
154
|
-
.where(
|
|
199
|
+
.where(
|
|
200
|
+
and(
|
|
201
|
+
inList(bodyPartParameterTable.messageId, messageIds),
|
|
202
|
+
guard?.(bodyPartParameterTable.messageId),
|
|
203
|
+
),
|
|
204
|
+
),
|
|
155
205
|
db
|
|
156
206
|
.delete(bodyPartStorageTable)
|
|
157
|
-
.where(
|
|
207
|
+
.where(
|
|
208
|
+
and(
|
|
209
|
+
inList(bodyPartStorageTable.messageId, messageIds),
|
|
210
|
+
guard?.(bodyPartStorageTable.messageId),
|
|
211
|
+
),
|
|
212
|
+
),
|
|
158
213
|
db
|
|
159
214
|
.delete(rawMessageStorageTable)
|
|
160
|
-
.where(
|
|
215
|
+
.where(
|
|
216
|
+
and(
|
|
217
|
+
inList(rawMessageStorageTable.messageId, messageIds),
|
|
218
|
+
guard?.(rawMessageStorageTable.messageId),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
161
221
|
db
|
|
162
222
|
.delete(bodyPartTable)
|
|
163
|
-
.where(
|
|
223
|
+
.where(
|
|
224
|
+
and(
|
|
225
|
+
inList(bodyPartTable.messageId, messageIds),
|
|
226
|
+
guard?.(bodyPartTable.messageId),
|
|
227
|
+
),
|
|
228
|
+
),
|
|
164
229
|
db
|
|
165
230
|
.delete(bodyStructureRefetchTable)
|
|
166
231
|
.where(
|
|
167
|
-
and(
|
|
232
|
+
and(
|
|
233
|
+
inList(bodyStructureRefetchTable.messageId, messageIds),
|
|
234
|
+
guard?.(bodyStructureRefetchTable.messageId),
|
|
235
|
+
),
|
|
168
236
|
),
|
|
169
237
|
db
|
|
170
238
|
.delete(messageSearchTextTable)
|
|
171
|
-
.where(
|
|
239
|
+
.where(
|
|
240
|
+
and(
|
|
241
|
+
inList(messageSearchTextTable.messageId, messageIds),
|
|
242
|
+
guard?.(messageSearchTextTable.messageId),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
172
245
|
db
|
|
173
246
|
.delete(envelopeTable)
|
|
174
|
-
.where(
|
|
247
|
+
.where(
|
|
248
|
+
and(
|
|
249
|
+
inList(envelopeTable.messageId, messageIds),
|
|
250
|
+
guard?.(envelopeTable.messageId),
|
|
251
|
+
),
|
|
252
|
+
),
|
|
175
253
|
db
|
|
176
254
|
.delete(messageTable)
|
|
177
|
-
.where(
|
|
255
|
+
.where(
|
|
256
|
+
and(
|
|
257
|
+
inList(messageTable.messageId, messageIds),
|
|
258
|
+
guard?.(messageTable.messageId),
|
|
259
|
+
),
|
|
260
|
+
),
|
|
178
261
|
];
|
|
179
|
-
return { removed, statements };
|
|
262
|
+
return { removed, removedParts, statements };
|
|
180
263
|
}
|
|
181
264
|
|
|
182
265
|
/**
|
|
@@ -210,6 +293,7 @@ const placementTerms = (expected: PlacementPredicate): SQL[] => {
|
|
|
210
293
|
export class DrizzleMessageRepository implements IMessageRepository {
|
|
211
294
|
private readonly db: DB;
|
|
212
295
|
private readonly batch: WriteBatch;
|
|
296
|
+
private readonly maxBoundParameters: number;
|
|
213
297
|
|
|
214
298
|
constructor(
|
|
215
299
|
store: SqliteStore<Record<string, unknown>>,
|
|
@@ -218,6 +302,7 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
218
302
|
) {
|
|
219
303
|
this.db = store.db;
|
|
220
304
|
this.batch = store.batch;
|
|
305
|
+
this.maxBoundParameters = store.maxBoundParameters;
|
|
221
306
|
}
|
|
222
307
|
|
|
223
308
|
private rowOf(input: CreateMessageInput) {
|
|
@@ -472,16 +557,172 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
472
557
|
});
|
|
473
558
|
}
|
|
474
559
|
|
|
475
|
-
async
|
|
560
|
+
async copyMessage(
|
|
476
561
|
sourceMessageId: string,
|
|
477
|
-
|
|
478
|
-
): Promise<
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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 };
|
|
485
726
|
}
|
|
486
727
|
|
|
487
728
|
async syncSearch(messageIds: string[]): Promise<void> {
|
|
@@ -542,31 +783,65 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
542
783
|
),
|
|
543
784
|
),
|
|
544
785
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
+
);
|
|
560
810
|
}
|
|
561
811
|
|
|
562
812
|
async deleteMany(messageIds: string[]): Promise<void> {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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;
|
|
570
845
|
}
|
|
571
846
|
|
|
572
847
|
async listByMailbox(
|
|
@@ -48,7 +48,12 @@ export const composeSqliteRepositories = (
|
|
|
48
48
|
const messageObjects = new MessageObjects(mail.db, objects);
|
|
49
49
|
const accountSetting = new AccountSettingRepo(app.db);
|
|
50
50
|
const address = new AddressRepo(mail.db);
|
|
51
|
-
const
|
|
51
|
+
const message = new DrizzleMessageRepository(
|
|
52
|
+
mail,
|
|
53
|
+
searchIndex,
|
|
54
|
+
messageObjects,
|
|
55
|
+
);
|
|
56
|
+
const mailbox = new MailboxRepo(mail, message);
|
|
52
57
|
const account = new AccountRepo(app.db, mailbox);
|
|
53
58
|
const calendarSuggestion = new CalendarSuggestionRepo(mail.db);
|
|
54
59
|
return {
|
|
@@ -63,7 +68,7 @@ export const composeSqliteRepositories = (
|
|
|
63
68
|
accountSetting,
|
|
64
69
|
),
|
|
65
70
|
mailboxLock: new MailboxLockRepo(mail.db),
|
|
66
|
-
message
|
|
71
|
+
message,
|
|
67
72
|
messageFlag: new DrizzleMessageFlagRepository(mail.db),
|
|
68
73
|
outboxMessage: new OutboxMessageRepo(mail.db),
|
|
69
74
|
outboxAttachment: new OutboxAttachmentRepo(mail),
|