@remit/drizzle-service 0.0.54 → 0.0.55

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -188,6 +188,34 @@ describe("MailboxSpecialUseRepo role lookups (sqlite)", () => {
188
188
  assert.notEqual(confirmed?.mailboxId, keepsakes.mailboxId);
189
189
  });
190
190
 
191
+ test("keeps a stale Trash appointment visible instead of answering the fallback", async () => {
192
+ // The appointed folder is gone — deleted by another client. `null` and the
193
+ // flagged folder both hide that; the resolution names the id the user chose
194
+ // so a refusal can offer the repair (#887).
195
+ const { accountId, accountConfigId } = await makeAccount();
196
+ await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
197
+ const flagged = await mailboxes.create(
198
+ makeMailboxInput(accountId, "[Gmail]/Trash"),
199
+ );
200
+ await repo.create(flagged.mailboxId, "Trash");
201
+ const appointed = randomUUID();
202
+ await appoint(
203
+ accountConfigId,
204
+ accountId,
205
+ CanonicalMailboxRole.Trash,
206
+ appointed,
207
+ );
208
+
209
+ assert.deepEqual(await repo.resolveTrashRole(accountId), {
210
+ kind: "appointment_stale",
211
+ appointedMailboxId: appointed,
212
+ fallback: {
213
+ kind: "flagged",
214
+ mailbox: { mailboxId: flagged.mailboxId, fullPath: "[Gmail]/Trash" },
215
+ },
216
+ });
217
+ });
218
+
191
219
  test("resolves an INBOX-nested Junk folder that advertises \\Junk", async () => {
192
220
  const { accountId } = await makeAccount();
193
221
  await mailboxes.create(makeMailboxInput(accountId, "INBOX"));
@@ -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,11 @@ import {
7
7
  type CanonicalMailboxRoleValue,
8
8
  composeFolderRoleAppointmentName,
9
9
  type RoleMailboxCandidate,
10
+ type RoleResolution,
10
11
  resolveConfirmedMailboxForRole,
11
12
  resolveMailboxForRole,
13
+ resolveRoleForAccount,
14
+ type UnappointedRoleResolution,
12
15
  } from "@remit/data-ports/folder-role";
13
16
  import { CanonicalMailboxRole } from "@remit/domain-enums";
14
17
  import { eq, inArray } from "drizzle-orm";
@@ -24,6 +27,39 @@ interface RoleCandidate extends RoleMailboxCandidate {
24
27
  fullPath: string;
25
28
  }
26
29
 
30
+ interface RoleMailbox {
31
+ mailboxId: string;
32
+ fullPath: string;
33
+ }
34
+
35
+ const projectMailbox = (candidate: RoleCandidate): RoleMailbox => ({
36
+ mailboxId: candidate.mailboxId,
37
+ fullPath: candidate.fullPath,
38
+ });
39
+
40
+ const projectUnappointed = (
41
+ resolution: UnappointedRoleResolution<RoleCandidate>,
42
+ ): UnappointedRoleResolution<RoleMailbox> =>
43
+ resolution.kind === "none"
44
+ ? resolution
45
+ : { kind: resolution.kind, mailbox: projectMailbox(resolution.mailbox) };
46
+
47
+ const projectResolution = (
48
+ resolution: RoleResolution<RoleCandidate>,
49
+ ): RoleResolution<RoleMailbox> => {
50
+ if (resolution.kind === "appointment_stale") {
51
+ return {
52
+ kind: "appointment_stale",
53
+ appointedMailboxId: resolution.appointedMailboxId,
54
+ fallback: projectUnappointed(resolution.fallback),
55
+ };
56
+ }
57
+ if (resolution.kind === "appointed") {
58
+ return { kind: "appointed", mailbox: projectMailbox(resolution.mailbox) };
59
+ }
60
+ return projectUnappointed(resolution);
61
+ };
62
+
27
63
  function rowToSpecialUse(
28
64
  row: typeof mailboxSpecialUseTable.$inferSelect,
29
65
  ): MailboxSpecialUseItem {
@@ -118,6 +154,24 @@ export class MailboxSpecialUseRepo implements IMailboxSpecialUseRepository {
118
154
  );
119
155
  }
120
156
 
157
+ /**
158
+ * Trash with its evidence, for the verbs that weigh it. Reads exactly what
159
+ * `findMailboxForRole` reads — a `null` answer is thrown away there, and this
160
+ * keeps it.
161
+ */
162
+ async resolveTrashRole(
163
+ accountId: string,
164
+ ): Promise<RoleResolution<RoleMailbox>> {
165
+ const role = CanonicalMailboxRole.Trash;
166
+ const [candidates, appointedMailboxId] = await Promise.all([
167
+ this.roleCandidates(accountId),
168
+ this.appointedMailboxId(accountId, role),
169
+ ]);
170
+ return projectResolution(
171
+ resolveRoleForAccount(role, candidates, appointedMailboxId),
172
+ );
173
+ }
174
+
121
175
  findArchiveMailbox(
122
176
  accountId: string,
123
177
  ): Promise<{ mailboxId: string; fullPath: string } | null> {