@remit/drizzle-service 0.0.54 → 0.0.56
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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { after, before, describe, test } from "node:test";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
composeFolderRoleAppointmentName,
|
|
6
|
+
meetsTrashAssurance,
|
|
7
|
+
} from "@remit/data-ports/folder-role";
|
|
5
8
|
import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
6
9
|
import {
|
|
7
10
|
accountSettingTable,
|
|
@@ -175,7 +178,9 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
|
|
|
175
178
|
makeMailboxInput(accountId, "[Gmail]/Trash"),
|
|
176
179
|
);
|
|
177
180
|
|
|
178
|
-
|
|
181
|
+
const proposal = await repo.resolveTrashRole(accountId);
|
|
182
|
+
assert.equal(proposal.kind, "proposed");
|
|
183
|
+
assert.equal(meetsTrashAssurance(proposal, "confirmed"), false);
|
|
179
184
|
|
|
180
185
|
await appoint(
|
|
181
186
|
accountConfigId,
|
|
@@ -183,9 +188,50 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
|
|
|
183
188
|
CanonicalMailboxRole.Trash,
|
|
184
189
|
trash.mailboxId,
|
|
185
190
|
);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
assert.deepEqual(await repo.resolveTrashRole(accountId), {
|
|
192
|
+
kind: "appointed",
|
|
193
|
+
mailbox: { mailboxId: trash.mailboxId, fullPath: "[Gmail]/Trash" },
|
|
194
|
+
});
|
|
195
|
+
assert.notEqual(trash.mailboxId, keepsakes.mailboxId);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("keeps a stale Trash appointment visible instead of answering the fallback", async () => {
|
|
199
|
+
// The appointed folder is gone — deleted by another client. `null` and the
|
|
200
|
+
// flagged folder both hide that; the resolution names the id the user chose
|
|
201
|
+
// so a refusal can offer the repair (#887).
|
|
202
|
+
const { accountId, accountConfigId } = await makeAccount();
|
|
203
|
+
await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
|
|
204
|
+
const flagged = await mailboxes.create(
|
|
205
|
+
makeMailboxInput(accountId, "[Gmail]/Trash"),
|
|
206
|
+
);
|
|
207
|
+
await repo.create(flagged.mailboxId, "Trash");
|
|
208
|
+
const appointed = randomUUID();
|
|
209
|
+
await appoint(
|
|
210
|
+
accountConfigId,
|
|
211
|
+
accountId,
|
|
212
|
+
CanonicalMailboxRole.Trash,
|
|
213
|
+
appointed,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
assert.deepEqual(await repo.resolveTrashRole(accountId), {
|
|
217
|
+
kind: "appointment_stale",
|
|
218
|
+
appointedMailboxId: appointed,
|
|
219
|
+
fallback: {
|
|
220
|
+
kind: "flagged",
|
|
221
|
+
mailbox: { mailboxId: flagged.mailboxId, fullPath: "[Gmail]/Trash" },
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// The flagged folder is where a delete files mail, and no evidence at all
|
|
226
|
+
// that the user meant it to be emptied.
|
|
227
|
+
assert.equal(
|
|
228
|
+
meetsTrashAssurance(await repo.resolveTrashRole(accountId), "confirmed"),
|
|
229
|
+
false,
|
|
230
|
+
);
|
|
231
|
+
assert.equal(
|
|
232
|
+
(await repo.findTrashMailbox(accountId))?.mailboxId,
|
|
233
|
+
flagged.mailboxId,
|
|
234
|
+
);
|
|
189
235
|
});
|
|
190
236
|
|
|
191
237
|
test("resolves an INBOX-nested Junk folder that advertises \\Junk", async () => {
|
|
@@ -231,7 +277,10 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
|
|
|
231
277
|
// Resolving the name widens where a delete FILES mail, never what an
|
|
232
278
|
// Empty Trash may expunge (#846): that still needs the flag or an
|
|
233
279
|
// appointment.
|
|
234
|
-
assert.equal(
|
|
280
|
+
assert.equal(
|
|
281
|
+
meetsTrashAssurance(await repo.resolveTrashRole(accountId), "confirmed"),
|
|
282
|
+
false,
|
|
283
|
+
);
|
|
235
284
|
});
|
|
236
285
|
|
|
237
286
|
test("resolves an INBOX-nested Archive folder that advertises no special use", async () => {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { describe, it } from "node:test";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The path a stale appointment last pointed at is a display-only sibling row
|
|
7
|
+
* (`FolderRoleAppointmentLabel`, #887). "Never read by resolution" is meant to
|
|
8
|
+
* be structural rather than a habit: this repository composes one setting name
|
|
9
|
+
* and has no way to reach the other. Read the source to prove it, because the
|
|
10
|
+
* guarantee is about what the file cannot do, not about what any one call
|
|
11
|
+
* returns.
|
|
12
|
+
*/
|
|
13
|
+
describe("MailboxSpecialUseRepo and the appointment label row", () => {
|
|
14
|
+
const source = readFileSync(
|
|
15
|
+
new URL("./i4-mailbox-special-use.ts", import.meta.url),
|
|
16
|
+
"utf8",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
it("never names the label setting", () => {
|
|
20
|
+
assert.equal(source.includes("FolderRoleAppointmentLabel"), false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("composes the appointment name in exactly one place", () => {
|
|
24
|
+
const uses = source.split("composeFolderRoleAppointmentName(").length - 1;
|
|
25
|
+
assert.equal(uses, 1);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
type CanonicalMailboxRoleValue,
|
|
8
8
|
composeFolderRoleAppointmentName,
|
|
9
9
|
type RoleMailboxCandidate,
|
|
10
|
-
|
|
10
|
+
type RoleResolution,
|
|
11
11
|
resolveMailboxForRole,
|
|
12
|
+
resolveRoleForAccount,
|
|
13
|
+
type UnappointedRoleResolution,
|
|
12
14
|
} from "@remit/data-ports/folder-role";
|
|
13
15
|
import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
14
16
|
import { eq, inArray } from "drizzle-orm";
|
|
@@ -24,6 +26,39 @@ interface RoleCandidate extends RoleMailboxCandidate {
|
|
|
24
26
|
fullPath: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
interface RoleMailbox {
|
|
30
|
+
mailboxId: string;
|
|
31
|
+
fullPath: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectMailbox = (candidate: RoleCandidate): RoleMailbox => ({
|
|
35
|
+
mailboxId: candidate.mailboxId,
|
|
36
|
+
fullPath: candidate.fullPath,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const projectUnappointed = (
|
|
40
|
+
resolution: UnappointedRoleResolution<RoleCandidate>,
|
|
41
|
+
): UnappointedRoleResolution<RoleMailbox> =>
|
|
42
|
+
resolution.kind === "none"
|
|
43
|
+
? resolution
|
|
44
|
+
: { kind: resolution.kind, mailbox: projectMailbox(resolution.mailbox) };
|
|
45
|
+
|
|
46
|
+
const projectResolution = (
|
|
47
|
+
resolution: RoleResolution<RoleCandidate>,
|
|
48
|
+
): RoleResolution<RoleMailbox> => {
|
|
49
|
+
if (resolution.kind === "appointment_stale") {
|
|
50
|
+
return {
|
|
51
|
+
kind: "appointment_stale",
|
|
52
|
+
appointedMailboxId: resolution.appointedMailboxId,
|
|
53
|
+
fallback: projectUnappointed(resolution.fallback),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (resolution.kind === "appointed") {
|
|
57
|
+
return { kind: "appointed", mailbox: projectMailbox(resolution.mailbox) };
|
|
58
|
+
}
|
|
59
|
+
return projectUnappointed(resolution);
|
|
60
|
+
};
|
|
61
|
+
|
|
27
62
|
function rowToSpecialUse(
|
|
28
63
|
row: typeof mailboxSpecialUseTable.$inferSelect,
|
|
29
64
|
): MailboxSpecialUseItem {
|
|
@@ -108,13 +143,21 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
|
|
|
108
143
|
return this.findMailboxForRole(accountId, CanonicalMailboxRole.Trash);
|
|
109
144
|
}
|
|
110
145
|
|
|
111
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Trash with its evidence, for the verbs that weigh it. Reads exactly what
|
|
148
|
+
* `findMailboxForRole` reads — a `null` answer is thrown away there, and this
|
|
149
|
+
* keeps it.
|
|
150
|
+
*/
|
|
151
|
+
async resolveTrashRole(
|
|
112
152
|
accountId: string,
|
|
113
|
-
): Promise<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
153
|
+
): Promise<RoleResolution<RoleMailbox>> {
|
|
154
|
+
const role = CanonicalMailboxRole.Trash;
|
|
155
|
+
const [candidates, appointedMailboxId] = await Promise.all([
|
|
156
|
+
this.roleCandidates(accountId),
|
|
157
|
+
this.appointedMailboxId(accountId, role),
|
|
158
|
+
]);
|
|
159
|
+
return projectResolution(
|
|
160
|
+
resolveRoleForAccount(role, candidates, appointedMailboxId),
|
|
118
161
|
);
|
|
119
162
|
}
|
|
120
163
|
|
|
@@ -140,13 +183,12 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
|
|
|
140
183
|
private async findMailboxForRole(
|
|
141
184
|
accountId: string,
|
|
142
185
|
role: CanonicalMailboxRoleValue,
|
|
143
|
-
resolve: typeof resolveMailboxForRole = resolveMailboxForRole,
|
|
144
186
|
): Promise<{ mailboxId: string; fullPath: string } | null> {
|
|
145
187
|
const [candidates, appointedMailboxId] = await Promise.all([
|
|
146
188
|
this.roleCandidates(accountId),
|
|
147
189
|
this.appointedMailboxId(accountId, role),
|
|
148
190
|
]);
|
|
149
|
-
const found =
|
|
191
|
+
const found = resolveMailboxForRole(role, candidates, appointedMailboxId);
|
|
150
192
|
return found
|
|
151
193
|
? { mailboxId: found.mailboxId, fullPath: found.fullPath }
|
|
152
194
|
: null;
|