@remit/backend 0.0.31 → 0.0.33

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/backend",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import type { MailboxItem } from "@remit/data-ports";
4
+ import { MailboxSyncStatus } from "@remit/domain-enums";
5
+ import { excludeDeletingMailboxes } from "./mailbox.js";
6
+
7
+ const mailbox = (
8
+ over: Partial<MailboxItem> & { mailboxId: string },
9
+ ): MailboxItem =>
10
+ ({ fullPath: over.mailboxId, ...over }) as unknown as MailboxItem;
11
+
12
+ describe("excludeDeletingMailboxes", () => {
13
+ it("drops a folder being deleted so it leaves the list before the worker reaps it", () => {
14
+ const items = [
15
+ mailbox({ mailboxId: "inbox", syncStatus: MailboxSyncStatus.synced }),
16
+ mailbox({ mailboxId: "gone", syncStatus: MailboxSyncStatus.deleting }),
17
+ mailbox({ mailboxId: "new", syncStatus: MailboxSyncStatus.pending }),
18
+ ];
19
+ assert.deepEqual(
20
+ excludeDeletingMailboxes(items).map((m) => m.mailboxId),
21
+ ["inbox", "new"],
22
+ );
23
+ });
24
+
25
+ it("keeps a folder whose delete failed and was restored off `deleting`", () => {
26
+ const items = [
27
+ mailbox({ mailboxId: "restored", syncStatus: MailboxSyncStatus.failed }),
28
+ ];
29
+ assert.deepEqual(
30
+ excludeDeletingMailboxes(items).map((m) => m.mailboxId),
31
+ ["restored"],
32
+ );
33
+ });
34
+ });
@@ -4,7 +4,7 @@ import type {
4
4
  } from "@remit/api-openapi-types";
5
5
  import type { IAccountSettingRepository, MailboxItem } from "@remit/data-ports";
6
6
  import { ForbiddenError, NotFoundError } from "@remit/data-ports/errors";
7
- import { MessageSystemFlag } from "@remit/domain-enums";
7
+ import { MailboxSyncStatus, MessageSystemFlag } from "@remit/domain-enums";
8
8
  import type { APIGatewayProxyEvent } from "aws-lambda";
9
9
  import { getAccountConfigIdFromEvent } from "../auth.js";
10
10
  import {
@@ -185,6 +185,24 @@ const toMailboxResponse = (
185
185
  updatedAt: mailbox.updatedAt,
186
186
  });
187
187
 
188
+ /**
189
+ * Drop folders the user has deleted but the imap-worker has not yet reaped.
190
+ *
191
+ * A delete is a soft delete: the row lives on with syncStatus=deleting until the
192
+ * worker confirms the IMAP delete and removes it (mailbox-queue.deleteMailbox).
193
+ * The list is the settings view a client refetches right after confirming, so a
194
+ * still-`deleting` row here would keep a deleted folder on screen until the
195
+ * worker finishes. Hiding it makes the folder leave the list the moment the
196
+ * delete is confirmed; a delete that fails restores the row off `deleting`,
197
+ * which brings the folder back on the next read.
198
+ */
199
+ export const excludeDeletingMailboxes = (
200
+ mailboxes: readonly MailboxItem[],
201
+ ): MailboxItem[] =>
202
+ mailboxes.filter(
203
+ (mailbox) => mailbox.syncStatus !== MailboxSyncStatus.deleting,
204
+ );
205
+
188
206
  export const MailboxOperations: Record<
189
207
  MailboxOperationIds,
190
208
  OperationHandler<MailboxOperationIds>
@@ -205,6 +223,8 @@ export const MailboxOperations: Record<
205
223
  continuationToken,
206
224
  });
207
225
 
226
+ const visibleItems = excludeDeletingMailboxes(result.items);
227
+
208
228
  // Overrides (mute / display-name / role) live in per-mailbox AccountSetting
209
229
  // rows (RFC 032). Load the whole config's set in one query and key it by
210
230
  // mailboxId so each mailbox surfaces its overrides without an N+1.
@@ -222,7 +242,7 @@ export const MailboxOperations: Record<
222
242
  accountId,
223
243
  );
224
244
  const items = applyPendingMoveCountPrediction(
225
- result.items,
245
+ visibleItems,
226
246
  pendingMoves,
227
247
  pendingUnseenFlagPushes,
228
248
  );
@@ -130,11 +130,14 @@ describe("GET /system/update", () => {
130
130
  assert.deepEqual(result, okState);
131
131
  });
132
132
 
133
- it("returns a disabled, empty resource when no state file exists", async () => {
133
+ it("reports an unknown version, not its own process env, when no state file exists", async () => {
134
+ // The updater owns the running version and writes it into state.json; with
135
+ // no state file the backend has nothing authoritative, so it says unknown
136
+ // rather than surfacing REMIT_TAG, which drifts from the real running tag.
134
137
  const result = await getUpdate(buildEvent(USER));
135
138
 
136
139
  assert.deepEqual(result, {
137
- currentVersion: "v1.4.1",
140
+ currentVersion: "unknown",
138
141
  check: { status: "disabled" },
139
142
  run: null,
140
143
  });
@@ -24,7 +24,17 @@ const controlDir = (): string =>
24
24
  const manifestUrl = (): string | undefined =>
25
25
  process.env.REMIT_UPDATE_MANIFEST_URL;
26
26
 
27
- const currentVersion = (): string => process.env.REMIT_TAG ?? "unknown";
27
+ /**
28
+ * The running version is a fact the updater owns: it reads the tag from `.env`
29
+ * and writes it into `state.json`, which is the only honest source across an
30
+ * update that rewrites that tag underneath a long-lived backend process. When no
31
+ * state file exists yet the backend has nothing authoritative to report, so it
32
+ * reports the version as unknown rather than fabricating one from its own
33
+ * process environment — a value captured at container start that drifts from the
34
+ * real running tag the moment an update lands. The first updater check writes a
35
+ * real `currentVersion` within seconds of the stack coming up.
36
+ */
37
+ const UNKNOWN_VERSION = "unknown";
28
38
 
29
39
  const notFound = (): APIGatewayProxyResult => ({
30
40
  statusCode: 404,
@@ -65,7 +75,7 @@ const readState = (): SystemUpdateResponse | null => {
65
75
  };
66
76
 
67
77
  const emptyResource = (): SystemUpdateResponse => ({
68
- currentVersion: currentVersion(),
78
+ currentVersion: UNKNOWN_VERSION,
69
79
  check: { status: "disabled" },
70
80
  run: null,
71
81
  });
@@ -115,7 +125,7 @@ const requestedResource = (
115
125
  targetVersion: string,
116
126
  requestedAt: string,
117
127
  ): SystemUpdateResponse => {
118
- const from = state?.currentVersion ?? currentVersion();
128
+ const from = state?.currentVersion ?? UNKNOWN_VERSION;
119
129
  return {
120
130
  currentVersion: from,
121
131
  check: state?.check ?? { status: "disabled" },