@remit/drizzle-service 0.0.98 → 0.0.100
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/batch.sqlite.test.ts +78 -0
- package/src/batch.ts +45 -0
- package/src/databases.ts +70 -0
- package/src/db.ts +1 -4
- package/src/generated-enum.test.ts +8 -0
- package/src/generated-id.test.ts +19 -0
- package/src/id.test.ts +0 -8
- package/src/id.ts +0 -6
- package/src/index.ts +16 -6
- package/src/repair/address-display-name.sqlite.test.ts +3 -5
- package/src/repair/address-display-name.ts +8 -3
- package/src/repair/junk-only-address.sqlite.test.ts +8 -36
- package/src/repair/stranded-sent-outbox.sqlite.test.ts +2 -9
- package/src/repos/account.conformance.sqlite.test.ts +1 -1
- package/src/repos/calendar-default.sqlite.test.ts +6 -6
- package/src/repos/calendar-put.sqlite.test.ts +40 -39
- package/src/repos/calendar-store.ts +273 -0
- package/src/repos/cascade-delete.sqlite.test.ts +7 -9
- package/src/repos/cascade-delete.test.ts +6 -13
- package/src/repos/cascade-delete.ts +127 -97
- package/src/repos/envelope.test.ts +1 -17
- package/src/repos/envelope.ts +83 -67
- package/src/repos/filter.test.ts +22 -2
- package/src/repos/filter.ts +1 -5
- package/src/repos/i4-account-config.test.ts +2 -2
- package/src/repos/i4-account-config.ts +5 -2
- package/src/repos/i4-account.test.ts +6 -2
- package/src/repos/i4-account.ts +12 -56
- package/src/repos/i4-address-junk-move.sqlite.test.ts +1 -1
- package/src/repos/i4-address-merge-flags.sqlite.test.ts +1 -2
- package/src/repos/i4-address.ts +58 -90
- package/src/repos/i4-mailbox-special-use.sqlite.test.ts +6 -2
- package/src/repos/i4-mailbox-special-use.ts +9 -6
- package/src/repos/i4-mailbox.sqlite.test.ts +12 -14
- package/src/repos/i4-mailbox.test.ts +17 -62
- package/src/repos/i4-mailbox.ts +121 -94
- package/src/repos/i4-organize-job-request.ts +0 -4
- package/src/repos/i4-outbox-attachment.ts +62 -33
- package/src/repos/mappers.ts +0 -16
- package/src/repos/message.sqlite.test.ts +62 -102
- package/src/repos/message.test.ts +6 -167
- package/src/repos/message.ts +227 -218
- package/src/repos/sync-warning.ts +76 -0
- package/src/repos/thread-message-body-muted.sqlite.test.ts +5 -10
- package/src/repos/thread-message-field-terms.sqlite.test.ts +5 -1
- package/src/repos/thread-message.sqlite.test.ts +2 -1
- package/src/repos/thread-message.test.ts +17 -4
- package/src/repos/thread-message.ts +502 -285
- package/src/repos/thread-search-predicates.ts +24 -26
- package/src/schema/message-data.ts +4 -8
- package/src/schema/sync-warning.ts +3 -0
- package/src/schema-sets/app.ts +17 -0
- package/src/schema-sets/mail.ts +32 -0
- package/src/schema-sets/search.ts +5 -0
- package/src/schema-version-accounting.sqlite.test.ts +56 -43
- package/src/schema.ts +3 -4
- package/src/search/thread-message-search.sqlite.test.ts +131 -0
- package/src/search/thread-message-search.ts +271 -0
- package/src/sqlite-client.ts +5 -7
- package/src/sqlite-repositories.ts +75 -0
- package/src/test-db-sqlite.ts +5 -13
- package/src/test-shipped-sqlite-schema.ts +46 -45
- package/src/test-sqlite.ts +35 -11
- package/src/vps-migrations-drift.sqlite.test.ts +81 -15
- package/src/account-granted-scopes-migration.sqlite.test.ts +0 -66
- package/src/account-synced-services-migration.sqlite.test.ts +0 -56
- package/src/mailbox-path-dedupe.sqlite.test.ts +0 -462
- package/src/mailbox-sync-status-backfill.sqlite.test.ts +0 -118
- package/src/outbox-created-cleanup.sqlite.test.ts +0 -83
- package/src/repair/duplicate-mailbox-mail.ts +0 -98
- package/src/repair/thread-message-category-contract.ts +0 -505
- package/src/repair/thread-message-category.sqlite.test.ts +0 -100
- package/src/repair/thread-message-category.ts +0 -415
- package/src/repos/calendar-migrations.sqlite.test.ts +0 -345
- package/src/repos/calendar-unit-of-work.ts +0 -31
- package/src/repos/config-import-atomicity.sqlite.test.ts +0 -255
- package/src/repos/filter-anchor-transaction.test.ts +0 -136
- package/src/repos/filter-anchor-transaction.ts +0 -45
- package/src/repos/filter-anchor.test.ts +0 -131
- package/src/repos/filter-anchor.ts +0 -105
- package/src/repos/i4-mailbox-special-use.structure.test.ts +0 -27
- package/src/repos/message-placement-backfill.sqlite.test.ts +0 -190
- package/src/repos/search-index-shape.test.ts +0 -82
- package/src/repos/search-index-shape.ts +0 -56
- package/src/repos/serialized-writes.sqlite.test.ts +0 -199
- package/src/repos/unit-of-work.sqlite.test.ts +0 -138
- package/src/repos/unit-of-work.test.ts +0 -109
- package/src/repos/unit-of-work.ts +0 -31
- package/src/schema/outbox.ts +0 -46
- package/src/schema-full-sqlite.ts +0 -13
- package/src/tx.sqlite.test.ts +0 -83
- package/src/tx.ts +0 -233
package/src/repos/mappers.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
BodyPartContentItem,
|
|
3
2
|
BodyPartItem,
|
|
4
3
|
BodyPartParameterItem,
|
|
5
4
|
BodyPartStorageItem,
|
|
@@ -9,7 +8,6 @@ import type {
|
|
|
9
8
|
RawMessageStorageItem,
|
|
10
9
|
} from "@remit/data-ports";
|
|
11
10
|
import type {
|
|
12
|
-
bodyPartContentTable,
|
|
13
11
|
bodyPartParameterTable,
|
|
14
12
|
bodyPartStorageTable,
|
|
15
13
|
bodyPartTable,
|
|
@@ -155,17 +153,3 @@ export function toBodyPartStorageItem(
|
|
|
155
153
|
...(row.dedupHash !== null ? { dedupHash: row.dedupHash } : {}),
|
|
156
154
|
};
|
|
157
155
|
}
|
|
158
|
-
|
|
159
|
-
export function toBodyPartContentItem(
|
|
160
|
-
row: typeof bodyPartContentTable.$inferSelect,
|
|
161
|
-
): BodyPartContentItem {
|
|
162
|
-
return {
|
|
163
|
-
bodyPartContentId: row.bodyPartContentId,
|
|
164
|
-
messageId: row.messageId,
|
|
165
|
-
bodyPartId: row.bodyPartId,
|
|
166
|
-
content: row.content,
|
|
167
|
-
contentLength: row.contentLength,
|
|
168
|
-
createdAt: row.createdAt,
|
|
169
|
-
updatedAt: row.updatedAt,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
-
import { and, eq, isNull } from "drizzle-orm";
|
|
4
3
|
import {
|
|
5
4
|
envelopeId as deriveEnvelopeId,
|
|
6
5
|
rootBodyPartId as deriveRootBodyPartId,
|
|
7
6
|
} from "../id.js";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
outboxTable,
|
|
12
|
-
} from "../schema/message-data.js";
|
|
7
|
+
import { messageDataSchema } from "../schema/message-data.js";
|
|
8
|
+
import { threadMessageTable } from "../schema/thread-message.js";
|
|
9
|
+
import { SearchIndex } from "../search/thread-message-search.js";
|
|
13
10
|
import { createSqliteTestDb, type SqliteTestDb } from "../test-db-sqlite.js";
|
|
14
11
|
import { DrizzleMessageRepository } from "./message.js";
|
|
15
12
|
|
|
13
|
+
const SCHEMA = { ...messageDataSchema, threadMessages: threadMessageTable };
|
|
14
|
+
|
|
16
15
|
// Runs the ported message repo against a real better-sqlite3 database
|
|
17
|
-
// (RFC 036 D1). Exercises the SQLite-specific seams: the
|
|
18
|
-
//
|
|
19
|
-
// violation → CreateFailedConflictError mapping, and the JSON/boolean columns.
|
|
16
|
+
// (RFC 036 D1). Exercises the SQLite-specific seams: the batched delete, the
|
|
17
|
+
// unique-violation → CreateFailedConflictError mapping, and the JSON/boolean columns.
|
|
20
18
|
|
|
21
19
|
describe("DrizzleMessageRepository (sqlite)", () => {
|
|
22
|
-
let db: SqliteTestDb<
|
|
20
|
+
let db: SqliteTestDb<typeof SCHEMA>;
|
|
23
21
|
let close: () => Promise<void>;
|
|
24
22
|
let repo: DrizzleMessageRepository;
|
|
25
23
|
|
|
@@ -44,29 +42,19 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
44
42
|
};
|
|
45
43
|
|
|
46
44
|
before(async () => {
|
|
47
|
-
({ db, close } = await createSqliteTestDb(
|
|
48
|
-
repo = new DrizzleMessageRepository(db);
|
|
45
|
+
({ db, close } = await createSqliteTestDb(SCHEMA));
|
|
46
|
+
repo = new DrizzleMessageRepository(db, new SearchIndex(db, db));
|
|
49
47
|
});
|
|
50
48
|
|
|
51
49
|
after(async () => {
|
|
52
50
|
await close();
|
|
53
51
|
});
|
|
54
52
|
|
|
55
|
-
test("create returns a MessageItem
|
|
53
|
+
test("create returns a MessageItem", async () => {
|
|
56
54
|
const item = await repo.create(BASE_INPUT);
|
|
57
55
|
assert.equal(item.messageId, MESSAGE_ID);
|
|
58
56
|
assert.equal(item.status, "active");
|
|
59
57
|
assert.equal(item.hasListUnsubscribe, true);
|
|
60
|
-
|
|
61
|
-
const rows = await db
|
|
62
|
-
.select()
|
|
63
|
-
.from(outboxTable)
|
|
64
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
65
|
-
assert.equal(
|
|
66
|
-
rows.length,
|
|
67
|
-
0,
|
|
68
|
-
"a message with no body yet has nothing to index",
|
|
69
|
-
);
|
|
70
58
|
});
|
|
71
59
|
|
|
72
60
|
test("boolean and json columns round-trip", async () => {
|
|
@@ -122,26 +110,11 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
122
110
|
assert.equal(afterUpdate.placementVerdict?.action, "MoveToInbox");
|
|
123
111
|
});
|
|
124
112
|
|
|
125
|
-
test("duplicate messageId throws CreateFailedConflictError
|
|
126
|
-
const before = await db
|
|
127
|
-
.select()
|
|
128
|
-
.from(outboxTable)
|
|
129
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
130
|
-
|
|
113
|
+
test("duplicate messageId throws CreateFailedConflictError", async () => {
|
|
131
114
|
await assert.rejects(
|
|
132
115
|
() => repo.create(BASE_INPUT),
|
|
133
116
|
(err: Error) => err.name === "CreateFailedConflictError",
|
|
134
117
|
);
|
|
135
|
-
|
|
136
|
-
const afterRows = await db
|
|
137
|
-
.select()
|
|
138
|
-
.from(outboxTable)
|
|
139
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
140
|
-
assert.equal(
|
|
141
|
-
afterRows.length,
|
|
142
|
-
before.length,
|
|
143
|
-
"rolled-back create must not append an outbox row",
|
|
144
|
-
);
|
|
145
118
|
});
|
|
146
119
|
|
|
147
120
|
test("upsertWithStatus reports created=false for an existing row", async () => {
|
|
@@ -153,6 +126,25 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
153
126
|
// The state every inbound sync row is born in, and the one the re-point gate
|
|
154
127
|
// in mailbox-service has to accept (#1096): the sync path supplies no
|
|
155
128
|
// syncStatus and nothing later promotes what the repository writes here.
|
|
129
|
+
test("two concurrent saves of one message both succeed, one of them creating it", async () => {
|
|
130
|
+
const messageId = "00000000-0000-0000-1111-000000000099";
|
|
131
|
+
const input = {
|
|
132
|
+
...BASE_INPUT,
|
|
133
|
+
messageId,
|
|
134
|
+
envelopeId: deriveEnvelopeId(messageId),
|
|
135
|
+
rootBodyPartId: deriveRootBodyPartId(messageId),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const saves = await Promise.all([
|
|
139
|
+
repo.upsertWithStatus(input),
|
|
140
|
+
repo.upsertWithStatus(input),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
assert.deepEqual(saves.map((save) => save.created).sort(), [false, true]);
|
|
144
|
+
assert.equal(saves[0]?.item.messageId, messageId);
|
|
145
|
+
assert.equal(saves[1]?.item.messageId, messageId);
|
|
146
|
+
});
|
|
147
|
+
|
|
156
148
|
test("upsertWithStatus writes pending when the caller names no syncStatus", async () => {
|
|
157
149
|
const inboundId = "00000000-0000-0000-2222-000000000009";
|
|
158
150
|
const { item, created } = await repo.upsertWithStatus({
|
|
@@ -249,74 +241,42 @@ describe("DrizzleMessageRepository (sqlite)", () => {
|
|
|
249
241
|
});
|
|
250
242
|
});
|
|
251
243
|
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
db
|
|
255
|
-
.select()
|
|
256
|
-
.from(outboxTable)
|
|
257
|
-
.where(
|
|
258
|
-
and(
|
|
259
|
-
eq(outboxTable.messageId, MESSAGE_ID),
|
|
260
|
-
eq(outboxTable.event, "message.moved"),
|
|
261
|
-
isNull(outboxTable.processedAt),
|
|
262
|
-
),
|
|
263
|
-
);
|
|
264
|
-
const pendingBefore = (await movedRows()).length;
|
|
265
|
-
|
|
266
|
-
const queued = await repo.requestReembed([
|
|
267
|
-
MESSAGE_ID,
|
|
268
|
-
"00000000-0000-0000-2222-0000000000fe",
|
|
269
|
-
]);
|
|
244
|
+
describe("deleteIfPlacement", () => {
|
|
245
|
+
const GONE_ID = "00000000-0000-0000-2222-00000000000c";
|
|
270
246
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
247
|
+
before(async () => {
|
|
248
|
+
await repo.create({
|
|
249
|
+
...BASE_INPUT,
|
|
250
|
+
messageId: GONE_ID,
|
|
251
|
+
uid: 9,
|
|
252
|
+
envelopeId: deriveEnvelopeId(GONE_ID),
|
|
253
|
+
rootBodyPartId: deriveRootBodyPartId(GONE_ID),
|
|
254
|
+
});
|
|
255
|
+
});
|
|
279
256
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
.
|
|
298
|
-
|
|
299
|
-
.where(
|
|
300
|
-
and(
|
|
301
|
-
eq(outboxTable.messageId, MESSAGE_ID),
|
|
302
|
-
eq(outboxTable.event, "message.moved"),
|
|
303
|
-
isNull(outboxTable.processedAt),
|
|
304
|
-
),
|
|
305
|
-
);
|
|
306
|
-
assert.equal(pendingAfter.length, pendingBefore.length);
|
|
257
|
+
test("keeps the row when its placement no longer matches what was read", async () => {
|
|
258
|
+
const deleted = await repo.deleteIfPlacement(GONE_ID, {
|
|
259
|
+
mailboxId: MAILBOX_ID,
|
|
260
|
+
uid: 8,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
assert.equal(deleted, false);
|
|
264
|
+
assert.equal((await repo.get(GONE_ID)).uid, 9);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("deletes the row while its placement still matches", async () => {
|
|
268
|
+
const deleted = await repo.deleteIfPlacement(GONE_ID, {
|
|
269
|
+
mailboxId: MAILBOX_ID,
|
|
270
|
+
uid: 9,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
assert.equal(deleted, true);
|
|
274
|
+
await assert.rejects(() => repo.get(GONE_ID));
|
|
275
|
+
});
|
|
307
276
|
});
|
|
308
277
|
|
|
309
|
-
test("delete removes the message
|
|
278
|
+
test("delete removes the message", async () => {
|
|
310
279
|
await repo.delete(MESSAGE_ID);
|
|
311
280
|
await assert.rejects(() => repo.get(MESSAGE_ID));
|
|
312
|
-
|
|
313
|
-
const rows = await db
|
|
314
|
-
.select()
|
|
315
|
-
.from(outboxTable)
|
|
316
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
317
|
-
assert.ok(
|
|
318
|
-
rows.some((r) => r.event === "message.removed"),
|
|
319
|
-
"delete must append a message.removed outbox row",
|
|
320
|
-
);
|
|
321
281
|
});
|
|
322
282
|
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
envelopeId as deriveEnvelopeId,
|
|
5
5
|
rootBodyPartId as deriveRootBodyPartId,
|
|
6
6
|
} from "../id.js";
|
|
7
|
+
import { SearchIndex } from "../search/thread-message-search.js";
|
|
7
8
|
import { DrizzleEnvelopeRepository } from "./envelope.js";
|
|
8
9
|
import { DrizzleMessageRepository } from "./message.js";
|
|
9
10
|
import { createTestDb, type TestDb } from "./test-helpers.js";
|
|
@@ -36,7 +37,7 @@ describe("DrizzleMessageRepository", () => {
|
|
|
36
37
|
|
|
37
38
|
before(async () => {
|
|
38
39
|
({ db, stop } = await createTestDb());
|
|
39
|
-
messageRepo = new DrizzleMessageRepository(db);
|
|
40
|
+
messageRepo = new DrizzleMessageRepository(db, new SearchIndex(db, db));
|
|
40
41
|
envelopeRepo = new DrizzleEnvelopeRepository(db);
|
|
41
42
|
});
|
|
42
43
|
|
|
@@ -56,25 +57,7 @@ describe("DrizzleMessageRepository", () => {
|
|
|
56
57
|
assert.ok(typeof item.updatedAt === "number");
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
test("
|
|
60
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
61
|
-
const { eq } = await import("drizzle-orm");
|
|
62
|
-
const rows = await db
|
|
63
|
-
.select()
|
|
64
|
-
.from(outboxTable)
|
|
65
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
66
|
-
assert.equal(rows.length, 0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("duplicate messageId throws CreateFailedConflictError and writes no extra outbox row", async () => {
|
|
70
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
71
|
-
const { eq } = await import("drizzle-orm");
|
|
72
|
-
|
|
73
|
-
const before = await db
|
|
74
|
-
.select()
|
|
75
|
-
.from(outboxTable)
|
|
76
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
77
|
-
|
|
60
|
+
test("duplicate messageId throws CreateFailedConflictError", async () => {
|
|
78
61
|
await assert.rejects(
|
|
79
62
|
() => messageRepo.create(BASE_MESSAGE_INPUT),
|
|
80
63
|
(err: Error) => {
|
|
@@ -82,16 +65,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
82
65
|
return true;
|
|
83
66
|
},
|
|
84
67
|
);
|
|
85
|
-
|
|
86
|
-
const after = await db
|
|
87
|
-
.select()
|
|
88
|
-
.from(outboxTable)
|
|
89
|
-
.where(eq(outboxTable.messageId, MESSAGE_ID));
|
|
90
|
-
assert.equal(
|
|
91
|
-
after.length,
|
|
92
|
-
before.length,
|
|
93
|
-
"rolled-back create must not add an outbox row",
|
|
94
|
-
);
|
|
95
68
|
});
|
|
96
69
|
});
|
|
97
70
|
|
|
@@ -119,23 +92,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
119
92
|
});
|
|
120
93
|
});
|
|
121
94
|
|
|
122
|
-
test("returns all 10 MessageDescription members", async () => {
|
|
123
|
-
const desc = await messageRepo.describe(MESSAGE_ID);
|
|
124
|
-
const keys = Object.keys(desc).sort();
|
|
125
|
-
assert.deepStrictEqual(keys, [
|
|
126
|
-
"bodyPart",
|
|
127
|
-
"bodyPartContent",
|
|
128
|
-
"bodyPartParameter",
|
|
129
|
-
"bodyPartStorage",
|
|
130
|
-
"envelope",
|
|
131
|
-
"envelopeAddress",
|
|
132
|
-
"message",
|
|
133
|
-
"messageFlag",
|
|
134
|
-
"messageReference",
|
|
135
|
-
"rawMessageStorage",
|
|
136
|
-
]);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
95
|
test("message array has 1 row with correct fields", async () => {
|
|
140
96
|
const desc = await messageRepo.describe(MESSAGE_ID);
|
|
141
97
|
assert.equal(desc.message.length, 1);
|
|
@@ -160,7 +116,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
160
116
|
assert.equal(desc.bodyPartParameter.length, 0);
|
|
161
117
|
assert.equal(desc.rawMessageStorage.length, 0);
|
|
162
118
|
assert.equal(desc.bodyPartStorage.length, 0);
|
|
163
|
-
assert.equal(desc.bodyPartContent.length, 0);
|
|
164
119
|
});
|
|
165
120
|
|
|
166
121
|
test("throws NotFoundError for unknown messageId", async () => {
|
|
@@ -235,22 +190,7 @@ describe("DrizzleMessageRepository", () => {
|
|
|
235
190
|
);
|
|
236
191
|
});
|
|
237
192
|
|
|
238
|
-
test("
|
|
239
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
240
|
-
const { and, eq } = await import("drizzle-orm");
|
|
241
|
-
const rows = await db
|
|
242
|
-
.select()
|
|
243
|
-
.from(outboxTable)
|
|
244
|
-
.where(
|
|
245
|
-
and(
|
|
246
|
-
eq(outboxTable.messageId, MOVE_MESSAGE_ID),
|
|
247
|
-
eq(outboxTable.event, "message.moved"),
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
assert.equal(rows.length, 0);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("updateUid completes the move and emits a move reindex event", async () => {
|
|
193
|
+
test("updateUid completes the move", async () => {
|
|
254
194
|
const completed = await messageRepo.updateUid(
|
|
255
195
|
MOVE_MESSAGE_ID,
|
|
256
196
|
99,
|
|
@@ -265,21 +205,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
265
205
|
assert.equal(read.uid, 99);
|
|
266
206
|
assert.equal(read.status, "active");
|
|
267
207
|
assert.equal(read.syncStatus, "synced");
|
|
268
|
-
|
|
269
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
270
|
-
const { and, eq } = await import("drizzle-orm");
|
|
271
|
-
const rows = await db
|
|
272
|
-
.select()
|
|
273
|
-
.from(outboxTable)
|
|
274
|
-
.where(
|
|
275
|
-
and(
|
|
276
|
-
eq(outboxTable.messageId, MOVE_MESSAGE_ID),
|
|
277
|
-
eq(outboxTable.event, "message.moved"),
|
|
278
|
-
),
|
|
279
|
-
);
|
|
280
|
-
assert.equal(rows.length, 1, "one move reindex event was enqueued");
|
|
281
|
-
assert.deepStrictEqual(rows[0].payload, { messageId: MOVE_MESSAGE_ID });
|
|
282
|
-
assert.equal(rows[0].processedAt, null, "event starts undrained");
|
|
283
208
|
});
|
|
284
209
|
|
|
285
210
|
test("updateUid drops the source uid and keeps the folder Undo restores to", async () => {
|
|
@@ -304,7 +229,7 @@ describe("DrizzleMessageRepository", () => {
|
|
|
304
229
|
);
|
|
305
230
|
});
|
|
306
231
|
|
|
307
|
-
test("updateUid throws NotFoundError
|
|
232
|
+
test("updateUid throws NotFoundError for an unknown messageId", async () => {
|
|
308
233
|
const UNKNOWN = "00000000-0000-0000-9999-000000000002";
|
|
309
234
|
await assert.rejects(
|
|
310
235
|
() => messageRepo.updateUid(UNKNOWN, 1, DEST_MAILBOX_ID),
|
|
@@ -313,83 +238,10 @@ describe("DrizzleMessageRepository", () => {
|
|
|
313
238
|
return true;
|
|
314
239
|
},
|
|
315
240
|
);
|
|
316
|
-
|
|
317
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
318
|
-
const { eq } = await import("drizzle-orm");
|
|
319
|
-
const rows = await db
|
|
320
|
-
.select()
|
|
321
|
-
.from(outboxTable)
|
|
322
|
-
.where(eq(outboxTable.messageId, UNKNOWN));
|
|
323
|
-
assert.equal(rows.length, 0, "rolled-back move writes no outbox row");
|
|
324
241
|
});
|
|
325
242
|
});
|
|
326
243
|
|
|
327
|
-
describe("
|
|
328
|
-
const SYNC_MESSAGE_ID = "00000000-0000-0000-3333-000000000001";
|
|
329
|
-
const SYNC_MAILBOX_ID = "00000000-0000-0000-3333-000000000002";
|
|
330
|
-
|
|
331
|
-
const bodySyncedRows = async () => {
|
|
332
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
333
|
-
const { and, eq } = await import("drizzle-orm");
|
|
334
|
-
return db
|
|
335
|
-
.select()
|
|
336
|
-
.from(outboxTable)
|
|
337
|
-
.where(
|
|
338
|
-
and(
|
|
339
|
-
eq(outboxTable.messageId, SYNC_MESSAGE_ID),
|
|
340
|
-
eq(outboxTable.event, "message.body_synced"),
|
|
341
|
-
),
|
|
342
|
-
);
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
before(async () => {
|
|
346
|
-
await messageRepo.create({
|
|
347
|
-
messageId: SYNC_MESSAGE_ID,
|
|
348
|
-
mailboxId: SYNC_MAILBOX_ID,
|
|
349
|
-
uid: 11,
|
|
350
|
-
sequenceNumber: 1,
|
|
351
|
-
rfc822Size: 256,
|
|
352
|
-
internalDate: NOW,
|
|
353
|
-
envelopeId: deriveEnvelopeId(SYNC_MESSAGE_ID),
|
|
354
|
-
rootBodyPartId: deriveRootBodyPartId(SYNC_MESSAGE_ID),
|
|
355
|
-
status: "active" as const,
|
|
356
|
-
syncStatus: "synced" as const,
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
test("a body write appends one body_synced event", async () => {
|
|
361
|
-
assert.equal((await bodySyncedRows()).length, 0, "none before sync");
|
|
362
|
-
await messageRepo.update(SYNC_MESSAGE_ID, {
|
|
363
|
-
bodyStorageKey: "s3://bucket/body-a.eml",
|
|
364
|
-
});
|
|
365
|
-
const rows = await bodySyncedRows();
|
|
366
|
-
assert.equal(rows.length, 1, "one row for one body write");
|
|
367
|
-
assert.deepStrictEqual(rows[0].payload, { messageId: SYNC_MESSAGE_ID });
|
|
368
|
-
assert.equal(rows[0].processedAt, null, "event starts undrained");
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
test("each body write appends its own row (append-only, no suppression)", async () => {
|
|
372
|
-
await messageRepo.update(SYNC_MESSAGE_ID, {
|
|
373
|
-
bodyStorageKey: "s3://bucket/body-b.eml",
|
|
374
|
-
});
|
|
375
|
-
await messageRepo.update(SYNC_MESSAGE_ID, {
|
|
376
|
-
bodyStorageKey: "s3://bucket/body-c.eml",
|
|
377
|
-
});
|
|
378
|
-
assert.equal(
|
|
379
|
-
(await bodySyncedRows()).length,
|
|
380
|
-
3,
|
|
381
|
-
"three body writes leave three rows — coalescing is the worker's job",
|
|
382
|
-
);
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
test("a classification-only update (no bodyStorageKey) appends nothing", async () => {
|
|
386
|
-
const before = (await bodySyncedRows()).length;
|
|
387
|
-
await messageRepo.update(SYNC_MESSAGE_ID, { category: "personal" });
|
|
388
|
-
assert.equal((await bodySyncedRows()).length, before);
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
describe("deleteMany — subtree delete + message.removed outbox", () => {
|
|
244
|
+
describe("deleteMany — subtree delete", () => {
|
|
393
245
|
const DEL_MESSAGE_ID = "00000000-0000-0000-4444-000000000001";
|
|
394
246
|
const DEL_MAILBOX_ID = "00000000-0000-0000-4444-000000000002";
|
|
395
247
|
const DEL_ENVELOPE_ID = deriveEnvelopeId(DEL_MESSAGE_ID);
|
|
@@ -480,19 +332,6 @@ describe("DrizzleMessageRepository", () => {
|
|
|
480
332
|
);
|
|
481
333
|
});
|
|
482
334
|
|
|
483
|
-
test("leaves exactly one undrained message.removed outbox row", async () => {
|
|
484
|
-
const { outboxTable } = await import("../schema/message-data.js");
|
|
485
|
-
const { eq } = await import("drizzle-orm");
|
|
486
|
-
const rows = await db
|
|
487
|
-
.select()
|
|
488
|
-
.from(outboxTable)
|
|
489
|
-
.where(eq(outboxTable.messageId, DEL_MESSAGE_ID));
|
|
490
|
-
assert.equal(rows.length, 1, "prior CDC rows are cleared");
|
|
491
|
-
assert.equal(rows[0].event, "message.removed");
|
|
492
|
-
assert.equal(rows[0].processedAt, null, "removal starts undrained");
|
|
493
|
-
assert.deepStrictEqual(rows[0].payload, { messageId: DEL_MESSAGE_ID });
|
|
494
|
-
});
|
|
495
|
-
|
|
496
335
|
test("deleteMany([]) is a no-op", async () => {
|
|
497
336
|
await messageRepo.deleteMany([]);
|
|
498
337
|
});
|