@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/drizzle-service",
3
- "version": "0.0.54",
3
+ "version": "0.0.56",
4
4
  "description": "Drizzle ORM service over SQLite",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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 { composeFolderRoleAppointmentName } from "@remit/data-ports/folder-role";
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
- assert.equal(await repo.findConfirmedTrashMailbox(accountId), null);
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
- const confirmed = await repo.findConfirmedTrashMailbox(accountId);
187
- assert.equal(confirmed?.mailboxId, trash.mailboxId);
188
- assert.notEqual(confirmed?.mailboxId, keepsakes.mailboxId);
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(await repo.findConfirmedTrashMailbox(accountId), null);
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
- resolveConfirmedMailboxForRole,
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
- findConfirmedTrashMailbox(
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<{ mailboxId: string; fullPath: string } | null> {
114
- return this.findMailboxForRole(
115
- accountId,
116
- CanonicalMailboxRole.Trash,
117
- resolveConfirmedMailboxForRole,
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 = resolve(role, candidates, appointedMailboxId);
191
+ const found = resolveMailboxForRole(role, candidates, appointedMailboxId);
150
192
  return found
151
193
  ? { mailboxId: found.mailboxId, fullPath: found.fullPath }
152
194
  : null;