@remit/backend 0.0.32 → 0.0.34
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 +1 -1
- package/scripts/backfill-list-id.ts +68 -0
- package/src/handlers/mailbox.test.ts +34 -0
- package/src/handlers/mailbox.ts +22 -2
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { logger } from "@remit/logger-lambda";
|
|
5
|
+
import {
|
|
6
|
+
backfillListIds,
|
|
7
|
+
type ListIdBackfillCheckpoint,
|
|
8
|
+
type ListIdBackfillCheckpointStore,
|
|
9
|
+
} from "@remit/mailbox-service";
|
|
10
|
+
import { getClient } from "../src/service/dynamodb.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One-time, full-corpus `ThreadMessage.listId` backfill (issue #263). Ships as
|
|
14
|
+
* an alternate entrypoint baked into the backend image — "the backend image
|
|
15
|
+
* with a command", the same shape `migrate.mjs` uses — rather than a service
|
|
16
|
+
* of its own. Unlike `migrate`, it is never wired into a compose one-shot: a
|
|
17
|
+
* full pass runs once per install, not on every restart. Invoke it by hand:
|
|
18
|
+
*
|
|
19
|
+
* docker compose -f docker-compose.sqlite.yml run --rm backend \
|
|
20
|
+
* node backfill-list-id.mjs
|
|
21
|
+
*
|
|
22
|
+
* Safe to interrupt: progress is checkpointed to disk after every page and
|
|
23
|
+
* picked back up on the next run, and every row it touches is read-only
|
|
24
|
+
* against the stored message, so a rerun after a partial pass is a no-op for
|
|
25
|
+
* everything already done.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const CHECKPOINT_PATH =
|
|
29
|
+
process.env.LIST_ID_BACKFILL_CHECKPOINT_PATH ??
|
|
30
|
+
"/data/sqlite/list-id-backfill-checkpoint.json";
|
|
31
|
+
|
|
32
|
+
const fileCheckpointStore: ListIdBackfillCheckpointStore = {
|
|
33
|
+
load: async () => {
|
|
34
|
+
if (!existsSync(CHECKPOINT_PATH)) return undefined;
|
|
35
|
+
const raw = await readFile(CHECKPOINT_PATH, "utf8");
|
|
36
|
+
return JSON.parse(raw) as ListIdBackfillCheckpoint;
|
|
37
|
+
},
|
|
38
|
+
save: async (checkpoint) => {
|
|
39
|
+
await mkdir(dirname(CHECKPOINT_PATH), { recursive: true });
|
|
40
|
+
await writeFile(CHECKPOINT_PATH, JSON.stringify(checkpoint));
|
|
41
|
+
},
|
|
42
|
+
clear: async () => {
|
|
43
|
+
await rm(CHECKPOINT_PATH, { force: true });
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const run = async (): Promise<void> => {
|
|
48
|
+
const client = await getClient();
|
|
49
|
+
|
|
50
|
+
const result = await backfillListIds(
|
|
51
|
+
{
|
|
52
|
+
accountConfigService: client.accountConfig,
|
|
53
|
+
threadMessageService: client.threadMessage,
|
|
54
|
+
messageService: client.message,
|
|
55
|
+
storageService: client.storage,
|
|
56
|
+
},
|
|
57
|
+
{ checkpointStore: fileCheckpointStore, logger },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
console.log("[backfill-list-id] done", result);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
run()
|
|
64
|
+
.then(() => process.exit(0))
|
|
65
|
+
.catch((error: unknown) => {
|
|
66
|
+
console.error("[backfill-list-id] failed", error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -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
|
+
});
|
package/src/handlers/mailbox.ts
CHANGED
|
@@ -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
|
-
|
|
245
|
+
visibleItems,
|
|
226
246
|
pendingMoves,
|
|
227
247
|
pendingUnseenFlagPushes,
|
|
228
248
|
);
|
package/tsconfig.json
CHANGED