@remit/drizzle-service 0.0.107 → 0.0.109
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/cascade-delete.ts +0 -1
- package/src/repos/envelope.test.ts +0 -1
- package/src/repos/envelope.ts +5 -11
- package/src/repos/i4-mailbox.test.ts +0 -2
- package/src/repos/i4-mailbox.ts +7 -33
- package/src/repos/mappers.ts +0 -23
- 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 -7
- package/src/repos/message.ts +321 -62
- package/src/schema/message-data.ts +0 -3
- package/src/schema-sets/mail.ts +0 -1
- 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";
|
|
@@ -61,7 +61,6 @@ describe("DrizzleEnvelopeRepository", () => {
|
|
|
61
61
|
assert.equal(data.bodyPart.length, 0);
|
|
62
62
|
assert.equal(data.bodyPartParameter.length, 0);
|
|
63
63
|
assert.equal(data.rawMessageStorage.length, 0);
|
|
64
|
-
assert.equal(data.bodyPartStorage.length, 0);
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
test("throws NotFoundError for unknown messageId", async () => {
|
package/src/repos/envelope.ts
CHANGED
|
@@ -21,11 +21,10 @@ 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,
|
|
28
|
-
bodyPartStorageTable,
|
|
29
28
|
bodyPartTable,
|
|
30
29
|
bodyStructureRefetchTable,
|
|
31
30
|
envelopeAddressTable,
|
|
@@ -38,7 +37,6 @@ import { inList } from "./in-list.js";
|
|
|
38
37
|
import {
|
|
39
38
|
toBodyPartItem,
|
|
40
39
|
toBodyPartParameterItem,
|
|
41
|
-
toBodyPartStorageItem,
|
|
42
40
|
toEnvelopeAddressItem,
|
|
43
41
|
toEnvelopeItem,
|
|
44
42
|
toMessageReferenceItem,
|
|
@@ -74,7 +72,6 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
|
|
|
74
72
|
bodyParts,
|
|
75
73
|
bodyPartParameters,
|
|
76
74
|
rawMessageStorages,
|
|
77
|
-
bodyPartStorages,
|
|
78
75
|
] = await Promise.all([
|
|
79
76
|
this.db
|
|
80
77
|
.select()
|
|
@@ -100,10 +97,6 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
|
|
|
100
97
|
.select()
|
|
101
98
|
.from(rawMessageStorageTable)
|
|
102
99
|
.where(eq(rawMessageStorageTable.messageId, messageId)),
|
|
103
|
-
this.db
|
|
104
|
-
.select()
|
|
105
|
-
.from(bodyPartStorageTable)
|
|
106
|
-
.where(eq(bodyPartStorageTable.messageId, messageId)),
|
|
107
100
|
]);
|
|
108
101
|
|
|
109
102
|
if (envelopes.length === 0) {
|
|
@@ -117,7 +110,6 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
|
|
|
117
110
|
bodyPart: bodyParts.map(toBodyPartItem),
|
|
118
111
|
bodyPartParameter: bodyPartParameters.map(toBodyPartParameterItem),
|
|
119
112
|
rawMessageStorage: rawMessageStorages.map(toRawMessageStorageItem),
|
|
120
|
-
bodyPartStorage: bodyPartStorages.map(toBodyPartStorageItem),
|
|
121
113
|
};
|
|
122
114
|
}
|
|
123
115
|
|
|
@@ -229,8 +221,10 @@ export class DrizzleEnvelopeRepository implements IEnvelopeRepository {
|
|
|
229
221
|
.where(eq(messageReferenceTable.messageId, messageId));
|
|
230
222
|
const rows = references.map((reference) => ({
|
|
231
223
|
...reference,
|
|
232
|
-
messageReferenceId:
|
|
233
|
-
|
|
224
|
+
messageReferenceId: deriveMessageReferenceId(
|
|
225
|
+
messageId,
|
|
226
|
+
reference.referenceType,
|
|
227
|
+
reference.referenceOrder,
|
|
234
228
|
),
|
|
235
229
|
messageId,
|
|
236
230
|
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)
|
package/src/repos/mappers.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
BodyPartItem,
|
|
3
3
|
BodyPartParameterItem,
|
|
4
|
-
BodyPartStorageItem,
|
|
5
4
|
EnvelopeAddressItem,
|
|
6
5
|
EnvelopeItem,
|
|
7
6
|
MessageReferenceItem,
|
|
@@ -9,7 +8,6 @@ import type {
|
|
|
9
8
|
} from "@remit/data-ports";
|
|
10
9
|
import type {
|
|
11
10
|
bodyPartParameterTable,
|
|
12
|
-
bodyPartStorageTable,
|
|
13
11
|
bodyPartTable,
|
|
14
12
|
envelopeAddressTable,
|
|
15
13
|
envelopeTable,
|
|
@@ -132,24 +130,3 @@ export function toRawMessageStorageItem(
|
|
|
132
130
|
...(row.expiresAt !== null ? { expiresAt: row.expiresAt } : {}),
|
|
133
131
|
};
|
|
134
132
|
}
|
|
135
|
-
|
|
136
|
-
export function toBodyPartStorageItem(
|
|
137
|
-
row: typeof bodyPartStorageTable.$inferSelect,
|
|
138
|
-
): BodyPartStorageItem {
|
|
139
|
-
return {
|
|
140
|
-
bodyPartStorageId: row.bodyPartStorageId,
|
|
141
|
-
messageId: row.messageId,
|
|
142
|
-
bodyPartId: row.bodyPartId,
|
|
143
|
-
storageType: row.storageType,
|
|
144
|
-
storageLocation: row.storageLocation,
|
|
145
|
-
storageKey: row.storageKey,
|
|
146
|
-
decodedSizeBytes: row.decodedSizeBytes,
|
|
147
|
-
checksumSha256: row.checksumSha256,
|
|
148
|
-
contentEncoding: row.contentEncoding,
|
|
149
|
-
isDeduped: row.isDeduped,
|
|
150
|
-
storedAt: row.storedAt,
|
|
151
|
-
createdAt: row.createdAt,
|
|
152
|
-
updatedAt: row.updatedAt,
|
|
153
|
-
...(row.dedupHash !== null ? { dedupHash: row.dedupHash } : {}),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
@@ -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,
|
|
@@ -122,7 +122,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
122
122
|
assert.equal(desc.bodyPart.length, 0);
|
|
123
123
|
assert.equal(desc.bodyPartParameter.length, 0);
|
|
124
124
|
assert.equal(desc.rawMessageStorage.length, 0);
|
|
125
|
-
assert.equal(desc.bodyPartStorage.length, 0);
|
|
126
125
|
});
|
|
127
126
|
|
|
128
127
|
test("throws NotFoundError for unknown messageId", async () => {
|
|
@@ -149,8 +148,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
149
148
|
sequenceNumber: 1,
|
|
150
149
|
rfc822Size: 512,
|
|
151
150
|
internalDate: NOW,
|
|
152
|
-
envelopeId: deriveEnvelopeId(MOVE_MESSAGE_ID),
|
|
153
|
-
rootBodyPartId: deriveRootBodyPartId(MOVE_MESSAGE_ID),
|
|
154
151
|
status: "active" as const,
|
|
155
152
|
syncStatus: "synced" as const,
|
|
156
153
|
});
|
|
@@ -264,8 +261,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
264
261
|
sequenceNumber: 1,
|
|
265
262
|
rfc822Size: 10,
|
|
266
263
|
internalDate: NOW,
|
|
267
|
-
envelopeId: DEL_ENVELOPE_ID,
|
|
268
|
-
rootBodyPartId: DEL_ROOT_BODY_PART_ID,
|
|
269
264
|
status: "active",
|
|
270
265
|
syncStatus: "synced",
|
|
271
266
|
});
|
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,13 +28,16 @@ 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";
|
|
33
39
|
import {
|
|
34
40
|
bodyPartParameterTable,
|
|
35
|
-
bodyPartStorageTable,
|
|
36
41
|
bodyPartTable,
|
|
37
42
|
bodyStructureRefetchTable,
|
|
38
43
|
envelopeAddressTable,
|
|
@@ -45,11 +50,10 @@ import {
|
|
|
45
50
|
} from "../schema/message-data.js";
|
|
46
51
|
import { threadMessageTable } from "../schema/thread-message.js";
|
|
47
52
|
import type { SearchIndex } from "../search/thread-message-search.js";
|
|
48
|
-
import { inList } from "./in-list.js";
|
|
53
|
+
import { inList, pairInList } from "./in-list.js";
|
|
49
54
|
import {
|
|
50
55
|
toBodyPartItem,
|
|
51
56
|
toBodyPartParameterItem,
|
|
52
|
-
toBodyPartStorageItem,
|
|
53
57
|
toEnvelopeAddressItem,
|
|
54
58
|
toEnvelopeItem,
|
|
55
59
|
toMessageReferenceItem,
|
|
@@ -118,18 +122,23 @@ export type SubtreeDb = Pick<
|
|
|
118
122
|
"delete" | "select"
|
|
119
123
|
>;
|
|
120
124
|
|
|
125
|
+
export type SubtreeGuard = (messageId: Column) => SQL;
|
|
126
|
+
|
|
127
|
+
export const MAIL_DELETE_BATCH_SIZE = 100;
|
|
128
|
+
|
|
121
129
|
/**
|
|
122
130
|
* The statements that delete a message and its whole per-message subtree by
|
|
123
131
|
* 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
|
|
132
|
+
* one batch so the subtree goes atomically. `removed` and `removedParts` read
|
|
133
|
+
* the message and body-part rows the batch deletes; they go first, so they see
|
|
134
|
+
* them under the same guard. `guard` rides every statement on that
|
|
135
|
+
* statement's own `messageId` column, so a guard that reads the message row
|
|
136
|
+
* holds until the last statement removes it.
|
|
128
137
|
*/
|
|
129
138
|
export function deleteMessageSubtree(
|
|
130
139
|
db: SubtreeDb,
|
|
131
|
-
messageIds: string[],
|
|
132
|
-
guard?:
|
|
140
|
+
messageIds: readonly string[],
|
|
141
|
+
guard?: SubtreeGuard,
|
|
133
142
|
) {
|
|
134
143
|
const removed = db
|
|
135
144
|
.select({
|
|
@@ -137,46 +146,110 @@ export function deleteMessageSubtree(
|
|
|
137
146
|
bodyStorageKey: messageTable.bodyStorageKey,
|
|
138
147
|
})
|
|
139
148
|
.from(messageTable)
|
|
140
|
-
.where(
|
|
141
|
-
|
|
149
|
+
.where(
|
|
150
|
+
and(
|
|
151
|
+
inList(messageTable.messageId, messageIds),
|
|
152
|
+
guard?.(messageTable.messageId),
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const removedParts = db
|
|
156
|
+
.select({
|
|
157
|
+
messageId: bodyPartTable.messageId,
|
|
158
|
+
partPath: bodyPartTable.partPath,
|
|
159
|
+
})
|
|
160
|
+
.from(bodyPartTable)
|
|
161
|
+
.where(
|
|
162
|
+
and(
|
|
163
|
+
inList(bodyPartTable.messageId, messageIds),
|
|
164
|
+
guard?.(bodyPartTable.messageId),
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
if (messageIds.length === 0) {
|
|
168
|
+
return { removed, removedParts, statements: [] };
|
|
169
|
+
}
|
|
142
170
|
const statements: SqliteBatchItem[] = [
|
|
143
171
|
db
|
|
144
172
|
.delete(messageFlagTable)
|
|
145
|
-
.where(
|
|
173
|
+
.where(
|
|
174
|
+
and(
|
|
175
|
+
inList(messageFlagTable.messageId, messageIds),
|
|
176
|
+
guard?.(messageFlagTable.messageId),
|
|
177
|
+
),
|
|
178
|
+
),
|
|
146
179
|
db
|
|
147
180
|
.delete(messageReferenceTable)
|
|
148
|
-
.where(
|
|
181
|
+
.where(
|
|
182
|
+
and(
|
|
183
|
+
inList(messageReferenceTable.messageId, messageIds),
|
|
184
|
+
guard?.(messageReferenceTable.messageId),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
149
187
|
db
|
|
150
188
|
.delete(envelopeAddressTable)
|
|
151
|
-
.where(
|
|
189
|
+
.where(
|
|
190
|
+
and(
|
|
191
|
+
inList(envelopeAddressTable.messageId, messageIds),
|
|
192
|
+
guard?.(envelopeAddressTable.messageId),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
152
195
|
db
|
|
153
196
|
.delete(bodyPartParameterTable)
|
|
154
|
-
.where(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
197
|
+
.where(
|
|
198
|
+
and(
|
|
199
|
+
inList(bodyPartParameterTable.messageId, messageIds),
|
|
200
|
+
guard?.(bodyPartParameterTable.messageId),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
158
203
|
db
|
|
159
204
|
.delete(rawMessageStorageTable)
|
|
160
|
-
.where(
|
|
205
|
+
.where(
|
|
206
|
+
and(
|
|
207
|
+
inList(rawMessageStorageTable.messageId, messageIds),
|
|
208
|
+
guard?.(rawMessageStorageTable.messageId),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
161
211
|
db
|
|
162
212
|
.delete(bodyPartTable)
|
|
163
|
-
.where(
|
|
213
|
+
.where(
|
|
214
|
+
and(
|
|
215
|
+
inList(bodyPartTable.messageId, messageIds),
|
|
216
|
+
guard?.(bodyPartTable.messageId),
|
|
217
|
+
),
|
|
218
|
+
),
|
|
164
219
|
db
|
|
165
220
|
.delete(bodyStructureRefetchTable)
|
|
166
221
|
.where(
|
|
167
|
-
and(
|
|
222
|
+
and(
|
|
223
|
+
inList(bodyStructureRefetchTable.messageId, messageIds),
|
|
224
|
+
guard?.(bodyStructureRefetchTable.messageId),
|
|
225
|
+
),
|
|
168
226
|
),
|
|
169
227
|
db
|
|
170
228
|
.delete(messageSearchTextTable)
|
|
171
|
-
.where(
|
|
229
|
+
.where(
|
|
230
|
+
and(
|
|
231
|
+
inList(messageSearchTextTable.messageId, messageIds),
|
|
232
|
+
guard?.(messageSearchTextTable.messageId),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
172
235
|
db
|
|
173
236
|
.delete(envelopeTable)
|
|
174
|
-
.where(
|
|
237
|
+
.where(
|
|
238
|
+
and(
|
|
239
|
+
inList(envelopeTable.messageId, messageIds),
|
|
240
|
+
guard?.(envelopeTable.messageId),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
175
243
|
db
|
|
176
244
|
.delete(messageTable)
|
|
177
|
-
.where(
|
|
245
|
+
.where(
|
|
246
|
+
and(
|
|
247
|
+
inList(messageTable.messageId, messageIds),
|
|
248
|
+
guard?.(messageTable.messageId),
|
|
249
|
+
),
|
|
250
|
+
),
|
|
178
251
|
];
|
|
179
|
-
return { removed, statements };
|
|
252
|
+
return { removed, removedParts, statements };
|
|
180
253
|
}
|
|
181
254
|
|
|
182
255
|
/**
|
|
@@ -210,6 +283,7 @@ const placementTerms = (expected: PlacementPredicate): SQL[] => {
|
|
|
210
283
|
export class DrizzleMessageRepository implements IMessageRepository {
|
|
211
284
|
private readonly db: DB;
|
|
212
285
|
private readonly batch: WriteBatch;
|
|
286
|
+
private readonly maxBoundParameters: number;
|
|
213
287
|
|
|
214
288
|
constructor(
|
|
215
289
|
store: SqliteStore<Record<string, unknown>>,
|
|
@@ -218,6 +292,7 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
218
292
|
) {
|
|
219
293
|
this.db = store.db;
|
|
220
294
|
this.batch = store.batch;
|
|
295
|
+
this.maxBoundParameters = store.maxBoundParameters;
|
|
221
296
|
}
|
|
222
297
|
|
|
223
298
|
private rowOf(input: CreateMessageInput) {
|
|
@@ -322,7 +397,6 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
322
397
|
bodyParts,
|
|
323
398
|
bodyPartParameters,
|
|
324
399
|
rawMessageStorages,
|
|
325
|
-
bodyPartStorages,
|
|
326
400
|
] = await Promise.all([
|
|
327
401
|
this.db
|
|
328
402
|
.select()
|
|
@@ -356,10 +430,6 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
356
430
|
.select()
|
|
357
431
|
.from(rawMessageStorageTable)
|
|
358
432
|
.where(eq(rawMessageStorageTable.messageId, messageId)),
|
|
359
|
-
this.db
|
|
360
|
-
.select()
|
|
361
|
-
.from(bodyPartStorageTable)
|
|
362
|
-
.where(eq(bodyPartStorageTable.messageId, messageId)),
|
|
363
433
|
]);
|
|
364
434
|
|
|
365
435
|
if (messages.length === 0) {
|
|
@@ -382,7 +452,6 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
382
452
|
bodyPart: bodyParts.map(toBodyPartItem),
|
|
383
453
|
bodyPartParameter: bodyPartParameters.map(toBodyPartParameterItem),
|
|
384
454
|
rawMessageStorage: rawMessageStorages.map(toRawMessageStorageItem),
|
|
385
|
-
bodyPartStorage: bodyPartStorages.map(toBodyPartStorageItem),
|
|
386
455
|
};
|
|
387
456
|
}
|
|
388
457
|
|
|
@@ -472,16 +541,172 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
472
541
|
});
|
|
473
542
|
}
|
|
474
543
|
|
|
475
|
-
async
|
|
544
|
+
async copyMessage(
|
|
476
545
|
sourceMessageId: string,
|
|
477
|
-
|
|
478
|
-
): Promise<
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
546
|
+
input: CreateMessageInput,
|
|
547
|
+
): Promise<{ item: MessageItem; created: boolean }> {
|
|
548
|
+
const targetMessageId = input.messageId;
|
|
549
|
+
const [searchTexts, envelopes, references, addresses, parts, parameters] =
|
|
550
|
+
await Promise.all([
|
|
551
|
+
this.db
|
|
552
|
+
.select({ text: messageSearchTextTable.text })
|
|
553
|
+
.from(messageSearchTextTable)
|
|
554
|
+
.where(eq(messageSearchTextTable.messageId, sourceMessageId)),
|
|
555
|
+
this.db
|
|
556
|
+
.select()
|
|
557
|
+
.from(envelopeTable)
|
|
558
|
+
.where(eq(envelopeTable.messageId, sourceMessageId)),
|
|
559
|
+
this.db
|
|
560
|
+
.select()
|
|
561
|
+
.from(messageReferenceTable)
|
|
562
|
+
.where(eq(messageReferenceTable.messageId, sourceMessageId)),
|
|
563
|
+
this.db
|
|
564
|
+
.select()
|
|
565
|
+
.from(envelopeAddressTable)
|
|
566
|
+
.where(eq(envelopeAddressTable.messageId, sourceMessageId)),
|
|
567
|
+
this.db
|
|
568
|
+
.select()
|
|
569
|
+
.from(bodyPartTable)
|
|
570
|
+
.where(eq(bodyPartTable.messageId, sourceMessageId)),
|
|
571
|
+
this.db
|
|
572
|
+
.select()
|
|
573
|
+
.from(bodyPartParameterTable)
|
|
574
|
+
.where(eq(bodyPartParameterTable.messageId, sourceMessageId)),
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
const now = Date.now();
|
|
578
|
+
const envelopeId = deriveEnvelopeId(targetMessageId);
|
|
579
|
+
const copiedPartIds = new Map(
|
|
580
|
+
parts.map((part) => [
|
|
581
|
+
part.bodyPartId,
|
|
582
|
+
deriveBodyPartId(targetMessageId, part.partPath),
|
|
583
|
+
]),
|
|
584
|
+
);
|
|
585
|
+
const partPaths = new Map(
|
|
586
|
+
parts.map((part) => [part.bodyPartId, part.partPath]),
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const searchTextRows = searchTexts.map((row) => ({
|
|
590
|
+
messageId: targetMessageId,
|
|
591
|
+
text: row.text,
|
|
592
|
+
createdAt: now,
|
|
593
|
+
updatedAt: now,
|
|
594
|
+
}));
|
|
595
|
+
const envelopeRows = envelopes.map((row) => ({
|
|
596
|
+
...row,
|
|
597
|
+
envelopeId,
|
|
598
|
+
messageId: targetMessageId,
|
|
599
|
+
createdAt: now,
|
|
600
|
+
updatedAt: now,
|
|
601
|
+
}));
|
|
602
|
+
const referenceRows = references.map((row) => ({
|
|
603
|
+
...row,
|
|
604
|
+
messageReferenceId: deriveMessageReferenceId(
|
|
605
|
+
targetMessageId,
|
|
606
|
+
row.referenceType,
|
|
607
|
+
row.referenceOrder,
|
|
608
|
+
),
|
|
609
|
+
messageId: targetMessageId,
|
|
610
|
+
envelopeId,
|
|
611
|
+
createdAt: now,
|
|
612
|
+
updatedAt: now,
|
|
613
|
+
}));
|
|
614
|
+
const addressRows = addresses.map((row) => ({
|
|
615
|
+
...row,
|
|
616
|
+
envelopeAddressId: deriveEnvelopeAddressId(
|
|
617
|
+
targetMessageId,
|
|
618
|
+
row.addressRole,
|
|
619
|
+
row.addressOrder,
|
|
620
|
+
),
|
|
621
|
+
messageId: targetMessageId,
|
|
622
|
+
createdAt: now,
|
|
623
|
+
updatedAt: now,
|
|
624
|
+
}));
|
|
625
|
+
const partRows = parts.map((row) => ({
|
|
626
|
+
...row,
|
|
627
|
+
bodyPartId: deriveBodyPartId(targetMessageId, row.partPath),
|
|
628
|
+
messageId: targetMessageId,
|
|
629
|
+
parentBodyPartId:
|
|
630
|
+
row.parentBodyPartId === null
|
|
631
|
+
? null
|
|
632
|
+
: (copiedPartIds.get(row.parentBodyPartId) ?? null),
|
|
633
|
+
createdAt: now,
|
|
634
|
+
updatedAt: now,
|
|
635
|
+
}));
|
|
636
|
+
const parameterRows = parameters.flatMap((row) => {
|
|
637
|
+
const partPath = partPaths.get(row.bodyPartId);
|
|
638
|
+
if (partPath === undefined) return [];
|
|
639
|
+
return [
|
|
640
|
+
{
|
|
641
|
+
...row,
|
|
642
|
+
bodyPartParameterId: deriveBodyPartParameterId(
|
|
643
|
+
targetMessageId,
|
|
644
|
+
partPath,
|
|
645
|
+
row.parameterName,
|
|
646
|
+
),
|
|
647
|
+
messageId: targetMessageId,
|
|
648
|
+
bodyPartId: deriveBodyPartId(targetMessageId, partPath),
|
|
649
|
+
createdAt: now,
|
|
650
|
+
updatedAt: now,
|
|
651
|
+
},
|
|
652
|
+
];
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
const insertCopy = this.db
|
|
656
|
+
.insert(messageTable)
|
|
657
|
+
.values(this.rowOf(input))
|
|
658
|
+
.onConflictDoNothing({ target: messageTable.messageId })
|
|
659
|
+
.returning();
|
|
660
|
+
const clones: SqliteBatchItem[] = [
|
|
661
|
+
...searchTextRows.map((row) =>
|
|
662
|
+
this.db
|
|
663
|
+
.insert(messageSearchTextTable)
|
|
664
|
+
.values(row)
|
|
665
|
+
.onConflictDoNothing({ target: messageSearchTextTable.messageId }),
|
|
666
|
+
),
|
|
667
|
+
...chunkRows(envelopeTable, envelopeRows, this.maxBoundParameters).map(
|
|
668
|
+
(slice) =>
|
|
669
|
+
this.db.insert(envelopeTable).values(slice).onConflictDoNothing(),
|
|
670
|
+
),
|
|
671
|
+
...chunkRows(
|
|
672
|
+
messageReferenceTable,
|
|
673
|
+
referenceRows,
|
|
674
|
+
this.maxBoundParameters,
|
|
675
|
+
).map((slice) =>
|
|
676
|
+
this.db
|
|
677
|
+
.insert(messageReferenceTable)
|
|
678
|
+
.values(slice)
|
|
679
|
+
.onConflictDoNothing(),
|
|
680
|
+
),
|
|
681
|
+
...chunkRows(
|
|
682
|
+
envelopeAddressTable,
|
|
683
|
+
addressRows,
|
|
684
|
+
this.maxBoundParameters,
|
|
685
|
+
).map((slice) =>
|
|
686
|
+
this.db
|
|
687
|
+
.insert(envelopeAddressTable)
|
|
688
|
+
.values(slice)
|
|
689
|
+
.onConflictDoNothing(),
|
|
690
|
+
),
|
|
691
|
+
...chunkRows(bodyPartTable, partRows, this.maxBoundParameters).map(
|
|
692
|
+
(slice) =>
|
|
693
|
+
this.db.insert(bodyPartTable).values(slice).onConflictDoNothing(),
|
|
694
|
+
),
|
|
695
|
+
...chunkRows(
|
|
696
|
+
bodyPartParameterTable,
|
|
697
|
+
parameterRows,
|
|
698
|
+
this.maxBoundParameters,
|
|
699
|
+
).map((slice) =>
|
|
700
|
+
this.db
|
|
701
|
+
.insert(bodyPartParameterTable)
|
|
702
|
+
.values(slice)
|
|
703
|
+
.onConflictDoNothing(),
|
|
704
|
+
),
|
|
705
|
+
];
|
|
706
|
+
const [inserted] = await this.batch([insertCopy, ...clones]);
|
|
707
|
+
const [row] = inserted;
|
|
708
|
+
if (row) return { item: toMessageItem(row), created: true };
|
|
709
|
+
return { item: await this.get(targetMessageId), created: false };
|
|
485
710
|
}
|
|
486
711
|
|
|
487
712
|
async syncSearch(messageIds: string[]): Promise<void> {
|
|
@@ -542,31 +767,65 @@ export class DrizzleMessageRepository implements IMessageRepository {
|
|
|
542
767
|
),
|
|
543
768
|
),
|
|
544
769
|
);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
770
|
+
return (await this.removeMessages([messageId], () => placed)) > 0;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async deleteManyIfPlacement(
|
|
774
|
+
mailboxId: string,
|
|
775
|
+
placements: ReadonlyArray<{ messageId: string; uid: number }>,
|
|
776
|
+
): Promise<void> {
|
|
777
|
+
const placed = this.db
|
|
778
|
+
.select({ messageId: messageTable.messageId })
|
|
779
|
+
.from(messageTable)
|
|
780
|
+
.where(
|
|
781
|
+
and(
|
|
782
|
+
eq(messageTable.mailboxId, mailboxId),
|
|
783
|
+
pairInList(
|
|
784
|
+
messageTable.messageId,
|
|
785
|
+
messageTable.uid,
|
|
786
|
+
placements.map(({ messageId, uid }) => [messageId, uid] as const),
|
|
787
|
+
),
|
|
788
|
+
),
|
|
789
|
+
);
|
|
790
|
+
await this.removeMessages(
|
|
791
|
+
placements.map((placement) => placement.messageId),
|
|
792
|
+
(column) => inArray(column, placed),
|
|
793
|
+
);
|
|
560
794
|
}
|
|
561
795
|
|
|
562
796
|
async deleteMany(messageIds: string[]): Promise<void> {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
797
|
+
await this.removeMessages(messageIds);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private async removeMessages(
|
|
801
|
+
messageIds: readonly string[],
|
|
802
|
+
guard?: SubtreeGuard,
|
|
803
|
+
): Promise<number> {
|
|
804
|
+
let removedCount = 0;
|
|
805
|
+
for (const batch of chunk(messageIds, MAIL_DELETE_BATCH_SIZE)) {
|
|
806
|
+
const listings = await this.db
|
|
807
|
+
.select({ threadMessageId: threadMessageTable.threadMessageId })
|
|
808
|
+
.from(threadMessageTable)
|
|
809
|
+
.where(inList(threadMessageTable.messageId, batch));
|
|
810
|
+
const subtree = deleteMessageSubtree(this.db, batch, guard);
|
|
811
|
+
const [removed, removedParts] = await this.batch([
|
|
812
|
+
subtree.removed,
|
|
813
|
+
subtree.removedParts,
|
|
814
|
+
this.db
|
|
815
|
+
.delete(threadMessageTable)
|
|
816
|
+
.where(
|
|
817
|
+
and(
|
|
818
|
+
inList(threadMessageTable.messageId, batch),
|
|
819
|
+
guard?.(threadMessageTable.messageId),
|
|
820
|
+
),
|
|
821
|
+
),
|
|
822
|
+
...subtree.statements,
|
|
823
|
+
]);
|
|
824
|
+
await this.search.sync(listings.map((row) => row.threadMessageId));
|
|
825
|
+
await this.messageObjects.reclaim(removed, removedParts);
|
|
826
|
+
removedCount += removed.length;
|
|
827
|
+
}
|
|
828
|
+
return removedCount;
|
|
570
829
|
}
|
|
571
830
|
|
|
572
831
|
async listByMailbox(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as entities from "@remit/drizzle-sqlite-schema";
|
|
2
2
|
|
|
3
3
|
const bodyPartParameterTable = entities.bodyPartParameters;
|
|
4
|
-
const bodyPartStorageTable = entities.bodyPartStorages;
|
|
5
4
|
const bodyPartTable = entities.bodyParts;
|
|
6
5
|
const bodyStructureRefetchTable = entities.bodyStructureRefetches;
|
|
7
6
|
const envelopeAddressTable = entities.envelopeAddresses;
|
|
@@ -14,7 +13,6 @@ const rawMessageStorageTable = entities.rawMessageStorages;
|
|
|
14
13
|
|
|
15
14
|
export {
|
|
16
15
|
bodyPartParameterTable,
|
|
17
|
-
bodyPartStorageTable,
|
|
18
16
|
bodyPartTable,
|
|
19
17
|
bodyStructureRefetchTable,
|
|
20
18
|
envelopeAddressTable,
|
|
@@ -33,7 +31,6 @@ export const messageDataSchema = {
|
|
|
33
31
|
bodyPart: bodyPartTable,
|
|
34
32
|
bodyPartParameter: bodyPartParameterTable,
|
|
35
33
|
rawMessageStorage: rawMessageStorageTable,
|
|
36
|
-
bodyPartStorage: bodyPartStorageTable,
|
|
37
34
|
bodyStructureRefetch: bodyStructureRefetchTable,
|
|
38
35
|
message: messageTable,
|
|
39
36
|
messageFlag: messageFlagTable,
|
package/src/schema-sets/mail.ts
CHANGED
|
@@ -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),
|