@remit/drizzle-service 0.0.15 → 0.0.16
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
CHANGED
|
@@ -242,7 +242,7 @@ describe("runDrizzleCascadeDelete", () => {
|
|
|
242
242
|
quarantinedAt: NOW,
|
|
243
243
|
attempts: 3,
|
|
244
244
|
failureStage: "BodyParse",
|
|
245
|
-
failureCode: "
|
|
245
|
+
failureCode: "UnreadableBody",
|
|
246
246
|
failureMessage: "multipart boundary was never closed",
|
|
247
247
|
workerVersion: "worker 1.0.0",
|
|
248
248
|
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { deriveQuarantineId } from "@remit/data-ports/id";
|
|
3
4
|
import { quarantineTable } from "../schema/quarantine.js";
|
|
4
5
|
import { createTestDb, randomId, type TestDb } from "../test-db.js";
|
|
5
6
|
import { QuarantineRepo } from "./quarantine.js";
|
|
@@ -26,7 +27,7 @@ describe("QuarantineRepo", () => {
|
|
|
26
27
|
quarantinedAt: now,
|
|
27
28
|
attempts: 3,
|
|
28
29
|
failureStage: "BodyParse",
|
|
29
|
-
failureCode: "
|
|
30
|
+
failureCode: "UnreadableBody",
|
|
30
31
|
failureMessage: "multipart boundary was never closed",
|
|
31
32
|
workerVersion: "worker 1.0.0",
|
|
32
33
|
structure: [{ depth: 0, contentType: "multipart/mixed" }],
|
|
@@ -109,4 +110,83 @@ describe("QuarantineRepo", () => {
|
|
|
109
110
|
assert.equal(entry.sizeBytes, undefined);
|
|
110
111
|
assert.equal(entry.messageIdHash, undefined);
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
test("re-quarantining the same message rewrites one row, never adds another", async () => {
|
|
115
|
+
const accountConfigId = randomId();
|
|
116
|
+
const identity = {
|
|
117
|
+
accountConfigId,
|
|
118
|
+
accountId: randomId(),
|
|
119
|
+
mailboxId: randomId(),
|
|
120
|
+
uidValidity: 1_712_000_000,
|
|
121
|
+
uid: 40217,
|
|
122
|
+
mailboxPath: "INBOX",
|
|
123
|
+
attempts: 1,
|
|
124
|
+
failureStage: "BodyParse" as const,
|
|
125
|
+
failureCode: "UnreadableBody" as const,
|
|
126
|
+
failureMessage: "the parser said no",
|
|
127
|
+
workerVersion: "sha-abc",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
await repo.upsert({ ...identity, quarantinedAt: 1_000 });
|
|
131
|
+
await repo.upsert({ ...identity, quarantinedAt: 2_000, attempts: 4 });
|
|
132
|
+
|
|
133
|
+
const entries = await repo.listByAccountConfigId(accountConfigId);
|
|
134
|
+
|
|
135
|
+
assert.equal(entries.length, 1);
|
|
136
|
+
assert.equal(entries[0].attempts, 4);
|
|
137
|
+
assert.equal(entries[0].quarantinedAt, 2_000);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("keeps the id derived from the message, not the random column default", async () => {
|
|
141
|
+
const accountConfigId = randomId();
|
|
142
|
+
const identity = {
|
|
143
|
+
accountConfigId,
|
|
144
|
+
accountId: randomId(),
|
|
145
|
+
mailboxId: randomId(),
|
|
146
|
+
uidValidity: 1_712_000_000,
|
|
147
|
+
uid: 40217,
|
|
148
|
+
mailboxPath: "INBOX",
|
|
149
|
+
quarantinedAt: 1_000,
|
|
150
|
+
attempts: 1,
|
|
151
|
+
failureStage: "BodyParse" as const,
|
|
152
|
+
failureCode: "UnreadableBody" as const,
|
|
153
|
+
failureMessage: "the parser said no",
|
|
154
|
+
workerVersion: "sha-abc",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await repo.upsert(identity);
|
|
158
|
+
const [entry] = await repo.listByAccountConfigId(accountConfigId);
|
|
159
|
+
|
|
160
|
+
assert.equal(
|
|
161
|
+
entry.quarantineId,
|
|
162
|
+
deriveQuarantineId(
|
|
163
|
+
identity.accountId,
|
|
164
|
+
identity.mailboxId,
|
|
165
|
+
identity.uidValidity,
|
|
166
|
+
identity.uid,
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("defaults the MIME tree to empty when the message failed before its shape was read", async () => {
|
|
172
|
+
const accountConfigId = randomId();
|
|
173
|
+
await repo.upsert({
|
|
174
|
+
accountConfigId,
|
|
175
|
+
accountId: randomId(),
|
|
176
|
+
mailboxId: randomId(),
|
|
177
|
+
uidValidity: 1_712_000_000,
|
|
178
|
+
uid: 1,
|
|
179
|
+
mailboxPath: "INBOX",
|
|
180
|
+
quarantinedAt: 1_000,
|
|
181
|
+
attempts: 1,
|
|
182
|
+
failureStage: "BodyParse",
|
|
183
|
+
failureCode: "UnreadableBody",
|
|
184
|
+
failureMessage: "the parser said no",
|
|
185
|
+
workerVersion: "sha-abc",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const [entry] = await repo.listByAccountConfigId(accountConfigId);
|
|
189
|
+
|
|
190
|
+
assert.deepEqual(entry.structure, []);
|
|
191
|
+
});
|
|
112
192
|
});
|
package/src/repos/quarantine.ts
CHANGED
|
@@ -2,7 +2,9 @@ import type {
|
|
|
2
2
|
IQuarantineRepository,
|
|
3
3
|
QuarantineItem,
|
|
4
4
|
QuarantineMimeNodeItem,
|
|
5
|
+
QuarantineUpsertInput,
|
|
5
6
|
} from "@remit/data-ports";
|
|
7
|
+
import { deriveQuarantineId } from "@remit/data-ports/id";
|
|
6
8
|
import { desc, eq } from "drizzle-orm";
|
|
7
9
|
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
8
10
|
import { quarantineTable } from "../schema/quarantine.js";
|
|
@@ -38,8 +40,8 @@ function rowToItem(row: typeof quarantineTable.$inferSelect): QuarantineItem {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
+
* The message quarantine (issue #72). Written by the sync worker, read by the
|
|
44
|
+
* settings surface; nothing in the API process creates or clears a row.
|
|
43
45
|
*/
|
|
44
46
|
export class QuarantineRepo implements IQuarantineRepository {
|
|
45
47
|
constructor(private db: DB) {}
|
|
@@ -54,4 +56,48 @@ export class QuarantineRepo implements IQuarantineRepository {
|
|
|
54
56
|
.orderBy(desc(quarantineTable.quarantinedAt));
|
|
55
57
|
return rows.map(rowToItem);
|
|
56
58
|
};
|
|
59
|
+
|
|
60
|
+
upsert = async (input: QuarantineUpsertInput): Promise<void> => {
|
|
61
|
+
const quarantineId = deriveQuarantineId(
|
|
62
|
+
input.accountId,
|
|
63
|
+
input.mailboxId,
|
|
64
|
+
input.uidValidity,
|
|
65
|
+
input.uid,
|
|
66
|
+
);
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const columns = {
|
|
69
|
+
accountConfigId: input.accountConfigId,
|
|
70
|
+
accountId: input.accountId,
|
|
71
|
+
mailboxId: input.mailboxId,
|
|
72
|
+
uidValidity: input.uidValidity,
|
|
73
|
+
uid: input.uid,
|
|
74
|
+
mailboxRole: input.mailboxRole ?? null,
|
|
75
|
+
mailboxPath: input.mailboxPath,
|
|
76
|
+
quarantinedAt: input.quarantinedAt,
|
|
77
|
+
attempts: input.attempts,
|
|
78
|
+
failureStage: input.failureStage,
|
|
79
|
+
failureCode: input.failureCode,
|
|
80
|
+
failureMessage: input.failureMessage,
|
|
81
|
+
failurePartPath: input.failurePartPath ?? null,
|
|
82
|
+
workerVersion: input.workerVersion,
|
|
83
|
+
contentType: input.contentType ?? null,
|
|
84
|
+
transferEncoding: input.transferEncoding ?? null,
|
|
85
|
+
charset: input.charset ?? null,
|
|
86
|
+
sizeBytes: input.sizeBytes ?? null,
|
|
87
|
+
structure: input.structure ?? [],
|
|
88
|
+
messageIdHash: input.messageIdHash ?? null,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// `quarantinedAt` is deliberately part of the update set: a re-quarantine
|
|
92
|
+
// is the message being set aside again, and the list is ordered by it.
|
|
93
|
+
// `createdAt` is not, so the row keeps saying when the message first
|
|
94
|
+
// failed.
|
|
95
|
+
await this.db
|
|
96
|
+
.insert(quarantineTable)
|
|
97
|
+
.values({ quarantineId, ...columns, createdAt: now, updatedAt: now })
|
|
98
|
+
.onConflictDoUpdate({
|
|
99
|
+
target: quarantineTable.quarantineId,
|
|
100
|
+
set: { ...columns, updatedAt: now },
|
|
101
|
+
});
|
|
102
|
+
};
|
|
57
103
|
}
|