@remit/account-worker 0.0.25 → 0.0.27
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 +2 -2
- package/src/cascade-enumerate.test.ts +3 -53
- package/src/cascade.ts +2 -55
- package/src/compose-relational.test.ts +1 -14
- package/src/compose-relational.ts +5 -6
- package/src/config.test.ts +0 -5
- package/src/config.ts +1 -6
- package/src/deletion-capabilities.ts +2 -2
- package/src/events.ts +1 -1
- package/src/handlers/account-export.test.ts +1 -1
- package/src/handlers/account-export.ts +1 -1
- package/src/handlers/account-fanout.test.ts +1 -2
- package/src/handlers/account-fanout.ts +7 -6
- package/src/handlers/account-finalize.ts +6 -8
- package/src/handlers/account-purge.test.ts +4 -50
- package/src/handlers/account-purge.ts +7 -99
- package/src/handlers/organize-job.test.ts +314 -16
- package/src/handlers/organize-job.ts +78 -26
- package/src/handlers/sender-category-backapply.test.ts +1 -1
- package/src/handlers/sender-category-backapply.ts +1 -1
- package/src/poller.ts +5 -1
- package/src/cascade.test.ts +0 -77
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/account-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dev": "node --import tsx src/poller.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@remit/logger": "*",
|
|
36
37
|
"@remit/data-ports": "*",
|
|
37
38
|
"@remit/logger-lambda": "*",
|
|
38
39
|
"@remit/drizzle-service": "*",
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
"@remit/sqs-client": "*",
|
|
41
42
|
"@remit/storage-service": "*",
|
|
42
43
|
"@remit/electrodb-entities": "*",
|
|
43
|
-
"@remit/search-index-worker": "*",
|
|
44
44
|
"@aws-sdk/client-sqs": "*",
|
|
45
45
|
"archiver": "*",
|
|
46
46
|
"electrodb": "*",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { MessageDescription } from "@remit/data-ports";
|
|
4
|
-
import { noopLogger } from "@remit/logger
|
|
4
|
+
import { noopLogger } from "@remit/logger";
|
|
5
5
|
import {
|
|
6
6
|
type CascadeEntity,
|
|
7
7
|
type CascadeServices,
|
|
@@ -18,11 +18,10 @@ const messageWithChildren = {
|
|
|
18
18
|
bodyPartParameter: [{ bodyPartParameterId: "bpp-1" }],
|
|
19
19
|
rawMessageStorage: [{ rawStorageId: "rms-1" }],
|
|
20
20
|
bodyPartStorage: [{ bodyPartStorageId: "bps-1" }],
|
|
21
|
-
bodyPartContent: [{ bodyPartContentId: "bpc-1" }],
|
|
22
21
|
} as unknown as MessageDescription;
|
|
23
22
|
|
|
24
23
|
describe("collectMessageChildEntities", () => {
|
|
25
|
-
it("expands the
|
|
24
|
+
it("expands the eight child entities of one message", () => {
|
|
26
25
|
const entities: CascadeEntity[] = [];
|
|
27
26
|
collectMessageChildEntities(entities, messageWithChildren);
|
|
28
27
|
|
|
@@ -36,13 +35,7 @@ describe("collectMessageChildEntities", () => {
|
|
|
36
35
|
"BodyPartParameter",
|
|
37
36
|
"RawMessageStorage",
|
|
38
37
|
"BodyPartStorage",
|
|
39
|
-
"BodyPartContent",
|
|
40
38
|
]);
|
|
41
|
-
assert.equal(
|
|
42
|
-
entities.find((e) => e.entityType === "BodyPartContent")?.key
|
|
43
|
-
.bodyPartContentId,
|
|
44
|
-
"bpc-1",
|
|
45
|
-
);
|
|
46
39
|
});
|
|
47
40
|
|
|
48
41
|
it("appends nothing when every child list is empty", () => {
|
|
@@ -56,7 +49,6 @@ describe("collectMessageChildEntities", () => {
|
|
|
56
49
|
bodyPartParameter: [],
|
|
57
50
|
rawMessageStorage: [],
|
|
58
51
|
bodyPartStorage: [],
|
|
59
|
-
bodyPartContent: [],
|
|
60
52
|
} as unknown as MessageDescription);
|
|
61
53
|
assert.equal(entities.length, 0);
|
|
62
54
|
});
|
|
@@ -96,13 +88,7 @@ const fullServices = (): CascadeServices =>
|
|
|
96
88
|
listByAccountConfig: async () => [{ accountSettingId: "set-1" }],
|
|
97
89
|
},
|
|
98
90
|
filterService: {
|
|
99
|
-
listByAccountConfig: async () => [
|
|
100
|
-
{ filterId: "flt-1", hasAnchor: true },
|
|
101
|
-
{ filterId: "flt-2", hasAnchor: false },
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
filterAnchorService: {
|
|
105
|
-
get: async () => ({ filterId: "flt-1" }),
|
|
91
|
+
listByAccountConfig: async () => [{ filterId: "flt-1" }],
|
|
106
92
|
},
|
|
107
93
|
labelService: {
|
|
108
94
|
listByAccountConfig: async () => [{ labelId: "lbl-1" }],
|
|
@@ -128,7 +114,6 @@ describe("enumerateCascadeEntities", () => {
|
|
|
128
114
|
"Mailbox",
|
|
129
115
|
"Message",
|
|
130
116
|
"MessageFlag",
|
|
131
|
-
"BodyPartContent",
|
|
132
117
|
"OutboxMessage",
|
|
133
118
|
"MailboxLock",
|
|
134
119
|
"MessagePlacementMove",
|
|
@@ -136,7 +121,6 @@ describe("enumerateCascadeEntities", () => {
|
|
|
136
121
|
"ThreadMessage",
|
|
137
122
|
"AccountSetting",
|
|
138
123
|
"Filter",
|
|
139
|
-
"FilterAnchor",
|
|
140
124
|
"Label",
|
|
141
125
|
"MessageLabel",
|
|
142
126
|
"Address",
|
|
@@ -148,40 +132,6 @@ describe("enumerateCascadeEntities", () => {
|
|
|
148
132
|
assert.equal(types[types.length - 1], "AccountConfig");
|
|
149
133
|
});
|
|
150
134
|
|
|
151
|
-
it("emits a Filter without a FilterAnchor when the filter has no anchor", async () => {
|
|
152
|
-
const services = fullServices();
|
|
153
|
-
services.filterService.listByAccountConfig = async () =>
|
|
154
|
-
[{ filterId: "flt-2", hasAnchor: false }] as never;
|
|
155
|
-
|
|
156
|
-
const { entities } = await enumerateCascadeEntities(
|
|
157
|
-
"cfg-1",
|
|
158
|
-
services,
|
|
159
|
-
noopLogger,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
assert.equal(entities.filter((e) => e.entityType === "Filter").length, 1);
|
|
163
|
-
assert.equal(
|
|
164
|
-
entities.filter((e) => e.entityType === "FilterAnchor").length,
|
|
165
|
-
0,
|
|
166
|
-
);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("skips the FilterAnchor when the anchor lookup returns null", async () => {
|
|
170
|
-
const services = fullServices();
|
|
171
|
-
services.filterAnchorService.get = async () => null as never;
|
|
172
|
-
|
|
173
|
-
const { entities } = await enumerateCascadeEntities(
|
|
174
|
-
"cfg-1",
|
|
175
|
-
services,
|
|
176
|
-
noopLogger,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
assert.equal(
|
|
180
|
-
entities.filter((e) => e.entityType === "FilterAnchor").length,
|
|
181
|
-
0,
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
135
|
it("yields only the AccountConfig row for an empty tenant", async () => {
|
|
186
136
|
const services = fullServices();
|
|
187
137
|
services.accountConfigService.describe = async () =>
|
package/src/cascade.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
IAccountSettingRepository,
|
|
6
6
|
IAddressRepository,
|
|
7
7
|
IEnvelopeRepository,
|
|
8
|
-
IFilterAnchorRepository,
|
|
9
8
|
IFilterRepository,
|
|
10
9
|
ILabelRepository,
|
|
11
10
|
IMailboxLockRepository,
|
|
@@ -19,7 +18,7 @@ import type {
|
|
|
19
18
|
IThreadMessageRepository,
|
|
20
19
|
MessageDescription,
|
|
21
20
|
} from "@remit/data-ports";
|
|
22
|
-
import type { Logger } from "@remit/logger
|
|
21
|
+
import type { Logger } from "@remit/logger";
|
|
23
22
|
|
|
24
23
|
export interface CascadeEntity {
|
|
25
24
|
entityType: string;
|
|
@@ -42,7 +41,6 @@ export interface CascadeServices {
|
|
|
42
41
|
accountExportRequestService: IAccountExportRequestRepository;
|
|
43
42
|
accountSettingService: IAccountSettingRepository;
|
|
44
43
|
filterService: IFilterRepository;
|
|
45
|
-
filterAnchorService: IFilterAnchorRepository;
|
|
46
44
|
labelService: ILabelRepository;
|
|
47
45
|
messageLabelService: IMessageLabelRepository;
|
|
48
46
|
}
|
|
@@ -111,12 +109,6 @@ export const collectMessageChildEntities = (
|
|
|
111
109
|
key: { bodyPartStorageId: bps.bodyPartStorageId },
|
|
112
110
|
});
|
|
113
111
|
}
|
|
114
|
-
for (const bpc of messageData.bodyPartContent) {
|
|
115
|
-
entities.push({
|
|
116
|
-
entityType: "BodyPartContent",
|
|
117
|
-
key: { bodyPartContentId: bpc.bodyPartContentId },
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
112
|
};
|
|
121
113
|
|
|
122
114
|
export const enumerateCascadeEntities = async (
|
|
@@ -138,7 +130,6 @@ export const enumerateCascadeEntities = async (
|
|
|
138
130
|
messageFlagPushService,
|
|
139
131
|
accountSettingService,
|
|
140
132
|
filterService,
|
|
141
|
-
filterAnchorService,
|
|
142
133
|
labelService,
|
|
143
134
|
messageLabelService,
|
|
144
135
|
} = services;
|
|
@@ -225,13 +216,6 @@ export const enumerateCascadeEntities = async (
|
|
|
225
216
|
key: { bodyPartStorageId: bps.bodyPartStorageId },
|
|
226
217
|
});
|
|
227
218
|
}
|
|
228
|
-
|
|
229
|
-
for (const bpc of messageData.bodyPartContent) {
|
|
230
|
-
entities.push({
|
|
231
|
-
entityType: "BodyPartContent",
|
|
232
|
-
key: { bodyPartContentId: bpc.bodyPartContentId },
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
219
|
}
|
|
236
220
|
}
|
|
237
221
|
|
|
@@ -292,7 +276,7 @@ export const enumerateCascadeEntities = async (
|
|
|
292
276
|
});
|
|
293
277
|
}
|
|
294
278
|
|
|
295
|
-
// Filter/
|
|
279
|
+
// Filter/Label/MessageLabel are accountConfig-scoped (RFC 034),
|
|
296
280
|
// not per sub-account, so — like AccountSetting and ThreadMessage above —
|
|
297
281
|
// they are enumerated once here rather than inside the per-account loop.
|
|
298
282
|
const filters = await filterService.listByAccountConfig(accountConfigId);
|
|
@@ -301,16 +285,6 @@ export const enumerateCascadeEntities = async (
|
|
|
301
285
|
entityType: "Filter",
|
|
302
286
|
key: { accountConfigId, filterId: filter.filterId },
|
|
303
287
|
});
|
|
304
|
-
if (!filter.hasAnchor) continue;
|
|
305
|
-
const anchor = await filterAnchorService.get(
|
|
306
|
-
accountConfigId,
|
|
307
|
-
filter.filterId,
|
|
308
|
-
);
|
|
309
|
-
if (!anchor) continue;
|
|
310
|
-
entities.push({
|
|
311
|
-
entityType: "FilterAnchor",
|
|
312
|
-
key: { accountConfigId, filterId: filter.filterId },
|
|
313
|
-
});
|
|
314
288
|
}
|
|
315
289
|
|
|
316
290
|
const labels = await labelService.listByAccountConfig(accountConfigId);
|
|
@@ -354,30 +328,3 @@ export const enumerateCascadeEntities = async (
|
|
|
354
328
|
|
|
355
329
|
return { entities, messageIds };
|
|
356
330
|
};
|
|
357
|
-
|
|
358
|
-
export const COVERED_ENTITY_TYPES = [
|
|
359
|
-
"Account",
|
|
360
|
-
"AccountConfig",
|
|
361
|
-
"AccountSetting",
|
|
362
|
-
"Address",
|
|
363
|
-
"BodyPart",
|
|
364
|
-
"BodyPartContent",
|
|
365
|
-
"BodyPartParameter",
|
|
366
|
-
"BodyPartStorage",
|
|
367
|
-
"Envelope",
|
|
368
|
-
"EnvelopeAddress",
|
|
369
|
-
"Filter",
|
|
370
|
-
"FilterAnchor",
|
|
371
|
-
"Label",
|
|
372
|
-
"Mailbox",
|
|
373
|
-
"MailboxLock",
|
|
374
|
-
"Message",
|
|
375
|
-
"MessageFlag",
|
|
376
|
-
"MessageFlagPush",
|
|
377
|
-
"MessageLabel",
|
|
378
|
-
"MessagePlacementMove",
|
|
379
|
-
"MessageReference",
|
|
380
|
-
"OutboxMessage",
|
|
381
|
-
"RawMessageStorage",
|
|
382
|
-
"ThreadMessage",
|
|
383
|
-
] as const;
|
|
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
-
import { noopLogger } from "@remit/logger
|
|
6
|
+
import { noopLogger } from "@remit/logger";
|
|
7
7
|
import { buildRelationalDeletionCapabilities } from "./compose-relational.js";
|
|
8
8
|
|
|
9
9
|
const exists = (path: string): Promise<boolean> =>
|
|
@@ -79,16 +79,3 @@ describe("relational deletion capabilities — filesystem storage cleanup", () =
|
|
|
79
79
|
await caps.deleteStoragePrefix("accounts/never-existed/", noopLogger);
|
|
80
80
|
});
|
|
81
81
|
});
|
|
82
|
-
|
|
83
|
-
describe("relational deletion capabilities — CDN and sign-out are no-ops", () => {
|
|
84
|
-
it("invalidateContent resolves without a CDN", async () => {
|
|
85
|
-
const caps = buildRelationalDeletionCapabilities();
|
|
86
|
-
await caps.invalidateContent("cfg-1", noopLogger);
|
|
87
|
-
await caps.invalidateContent("cfg-1/acc-1", noopLogger);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("signOut resolves without a federated session store", async () => {
|
|
91
|
-
const caps = buildRelationalDeletionCapabilities();
|
|
92
|
-
await caps.signOut("user-123", noopLogger);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -3,9 +3,9 @@ import { join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type CascadeDeleter,
|
|
5
5
|
createSqliteCascadeDeleter,
|
|
6
|
+
sqliteDatabasePathsFromEnv,
|
|
6
7
|
} from "@remit/drizzle-service";
|
|
7
|
-
import type { Logger } from "@remit/logger
|
|
8
|
-
import { env } from "expect-env";
|
|
8
|
+
import type { Logger } from "@remit/logger";
|
|
9
9
|
import type { DeletionCapabilities } from "./deletion-capabilities.js";
|
|
10
10
|
|
|
11
11
|
// Mirrors `@remit/storage-service`'s filesystem-backend default: bodies
|
|
@@ -30,14 +30,14 @@ const deleteStoragePrefix = async (
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
// The Drizzle cascade deleter is opened once, on first use, and reused: it
|
|
33
|
-
// opens the
|
|
33
|
+
// opens the app, mail and search databases (the native binding loads there,
|
|
34
34
|
// hence async). Built lazily so the fanout worker — which signs out but never
|
|
35
35
|
// cascade-deletes — does not open a database handle it will not use.
|
|
36
36
|
let deleterPromise: Promise<CascadeDeleter> | undefined;
|
|
37
37
|
|
|
38
38
|
const getDeleter = (): Promise<CascadeDeleter> => {
|
|
39
39
|
if (!deleterPromise)
|
|
40
|
-
deleterPromise = createSqliteCascadeDeleter(
|
|
40
|
+
deleterPromise = createSqliteCascadeDeleter(sqliteDatabasePathsFromEnv());
|
|
41
41
|
return deleterPromise;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -52,8 +52,7 @@ const getDeleter = (): Promise<CascadeDeleter> => {
|
|
|
52
52
|
* backend with no response cache (`deploy/vps/caddy/routes.caddy`), so there
|
|
53
53
|
* is nothing to invalidate.
|
|
54
54
|
* - `deleteStoragePrefix`: recursive filesystem removal.
|
|
55
|
-
* - `cascadeDelete`: the Drizzle cascade over SQLite.
|
|
56
|
-
* subtrees emit `message.removed` outbox rows the search-index worker relays.
|
|
55
|
+
* - `cascadeDelete`: the Drizzle cascade over SQLite.
|
|
57
56
|
*/
|
|
58
57
|
export const buildRelationalDeletionCapabilities =
|
|
59
58
|
(): DeletionCapabilities => ({
|
package/src/config.test.ts
CHANGED
|
@@ -51,9 +51,4 @@ describe("getImapWorkerQueueUrl", () => {
|
|
|
51
51
|
delete process.env.SQS_QUEUE_URL_IMAP_WORKER;
|
|
52
52
|
assert.equal(getImapWorkerQueueUrl(), undefined);
|
|
53
53
|
});
|
|
54
|
-
|
|
55
|
-
it("returns the configured value when set", () => {
|
|
56
|
-
process.env.SQS_QUEUE_URL_IMAP_WORKER = "http://queue/imap-worker";
|
|
57
|
-
assert.equal(getImapWorkerQueueUrl(), "http://queue/imap-worker");
|
|
58
|
-
});
|
|
59
54
|
});
|
package/src/config.ts
CHANGED
|
@@ -26,8 +26,6 @@ export const getSqsClient = (queueUrl: string): SQSClient => {
|
|
|
26
26
|
// no queue, so its Lambda doesn't carry the env vars. Eager evaluation here would
|
|
27
27
|
// crash finalize at module load — getters defer the read to the fanout-only call
|
|
28
28
|
// sites.
|
|
29
|
-
export const getSearchIndexQueueUrl = (): string =>
|
|
30
|
-
env.SQS_QUEUE_URL_SEARCH_INDEX;
|
|
31
29
|
// Optional, and read through `process.env` rather than expect-env so an unset
|
|
32
30
|
// var yields undefined instead of throwing. IMAP_WORKER_STOP is a no-op
|
|
33
31
|
// acknowledgement in the cascade contract — the account tombstone fence is what
|
|
@@ -48,7 +46,7 @@ let cascadeServicesPromise: Promise<CascadeServices> | null = null;
|
|
|
48
46
|
|
|
49
47
|
// Every cascade service is a `RemitClient` repository, so the whole enumeration
|
|
50
48
|
// runs on whatever backend `getClient()` resolved — DynamoDB (ElectroDB) or
|
|
51
|
-
// SQLite (Drizzle). Filter/
|
|
49
|
+
// SQLite (Drizzle). Filter/Label/MessageLabel (Smart Organize,
|
|
52
50
|
// RFC 034) are present on both via the client, so they need no
|
|
53
51
|
// backend-specific wiring here.
|
|
54
52
|
export const getCascadeServices = (): Promise<CascadeServices> => {
|
|
@@ -69,7 +67,6 @@ export const getCascadeServices = (): Promise<CascadeServices> => {
|
|
|
69
67
|
accountExportRequestService: remitClient.accountExportRequest,
|
|
70
68
|
accountSettingService: remitClient.accountSetting,
|
|
71
69
|
filterService: remitClient.filter,
|
|
72
|
-
filterAnchorService: remitClient.filterAnchor,
|
|
73
70
|
labelService: remitClient.label,
|
|
74
71
|
messageLabelService: remitClient.messageLabel,
|
|
75
72
|
}));
|
|
@@ -84,5 +81,3 @@ export const getStorageService = (): StorageService => {
|
|
|
84
81
|
}
|
|
85
82
|
return storageService;
|
|
86
83
|
};
|
|
87
|
-
|
|
88
|
-
export const dataBackend = process.env.DATA_BACKEND;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Logger } from "@remit/logger
|
|
1
|
+
import type { Logger } from "@remit/logger";
|
|
2
2
|
import type { CascadeEntity } from "./cascade.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -71,7 +71,7 @@ export const setDeletionCapabilities = (
|
|
|
71
71
|
const buildDeletionCapabilitiesFromEnv =
|
|
72
72
|
async (): Promise<DeletionCapabilities> => {
|
|
73
73
|
if (injectedCapabilities) return injectedCapabilities;
|
|
74
|
-
if (process.env.
|
|
74
|
+
if (process.env.SQLITE_APP_DB_PATH) {
|
|
75
75
|
const { buildRelationalDeletionCapabilities } = await import(
|
|
76
76
|
"./compose-relational.js"
|
|
77
77
|
);
|
package/src/events.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type AccountDeleteEvent = {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Per-account hard-delete. Purges ONE mail account's data (mailboxes,
|
|
12
|
-
* messages, body parts, thread messages, S3 objects
|
|
12
|
+
* messages, body parts, thread messages, S3 objects) while
|
|
13
13
|
* keeping the AccountConfig/tenant and any sibling accounts intact. Emitted
|
|
14
14
|
* by the API on `deleteAccount`; consumed by the fanout worker.
|
|
15
15
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
3
|
import { describe, it } from "node:test";
|
|
4
|
-
import { noopLogger } from "@remit/logger
|
|
4
|
+
import { noopLogger } from "@remit/logger";
|
|
5
5
|
import type { StorageService } from "@remit/storage-service";
|
|
6
6
|
import type { CascadeServices } from "../cascade.js";
|
|
7
7
|
import type { AccountExportEvent } from "../events.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PassThrough } from "node:stream";
|
|
2
|
-
import type { Logger } from "@remit/logger
|
|
2
|
+
import type { Logger } from "@remit/logger";
|
|
3
3
|
import type { StorageService } from "@remit/storage-service";
|
|
4
4
|
import { ZipArchive } from "archiver";
|
|
5
5
|
import type { CascadeServices } from "../cascade.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { afterEach, describe, it } from "node:test";
|
|
3
3
|
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
-
import { noopLogger } from "@remit/logger
|
|
4
|
+
import { noopLogger } from "@remit/logger";
|
|
5
5
|
import type { CascadeServices } from "../cascade.js";
|
|
6
6
|
import type { AccountDeleteEvent } from "../events.js";
|
|
7
7
|
import { processAccountFanout } from "./account-fanout.js";
|
|
@@ -32,7 +32,6 @@ const buildServices = (): CascadeServices =>
|
|
|
32
32
|
threadMessageService: { listAllByAccount: async () => [] },
|
|
33
33
|
accountSettingService: { listByAccountConfig: async () => [] },
|
|
34
34
|
filterService: { listByAccountConfig: async () => [] },
|
|
35
|
-
filterAnchorService: { get: async () => null },
|
|
36
35
|
labelService: { listByAccountConfig: async () => [] },
|
|
37
36
|
messageLabelService: { listByLabelId: async () => [] },
|
|
38
37
|
}) as unknown as CascadeServices;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
|
|
2
|
+
import { createLogger, type Logger } from "@remit/logger";
|
|
2
3
|
import {
|
|
3
|
-
createLogger,
|
|
4
|
-
type Logger,
|
|
5
4
|
queueNameFromEventSource,
|
|
6
5
|
recordQueueEvent,
|
|
7
|
-
|
|
8
|
-
} from "@remit/logger-lambda";
|
|
6
|
+
} from "@remit/logger-lambda/metrics";
|
|
7
|
+
import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
9
8
|
import type { SQSBatchResponse, SQSEvent, SQSHandler } from "aws-lambda";
|
|
10
9
|
import type { CascadeServices } from "../cascade.js";
|
|
11
10
|
import { enumerateCascadeEntities } from "../cascade.js";
|
|
@@ -30,6 +29,8 @@ import { processAccountDataPurge } from "./account-purge.js";
|
|
|
30
29
|
import { processOrganizeJob } from "./organize-job.js";
|
|
31
30
|
import { processSenderCategoryBackApply } from "./sender-category-backapply.js";
|
|
32
31
|
|
|
32
|
+
installNodeLogger();
|
|
33
|
+
|
|
33
34
|
export interface ProcessAccountFanoutDeps {
|
|
34
35
|
services?: CascadeServices;
|
|
35
36
|
sqs?: SQSClient;
|
|
@@ -41,8 +42,8 @@ export interface ProcessAccountFanoutDeps {
|
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* Fanout step of the account-deletion cascade. Read-only: enumerates the
|
|
44
|
-
* AccountConfig's data and dispatches downstream work —
|
|
45
|
-
*
|
|
45
|
+
* AccountConfig's data and dispatches downstream work — per-account
|
|
46
|
+
* imap-worker stops, a sign-out, and the finalize
|
|
46
47
|
* enqueue. Throws on any non-swallowable error so SQS partial batch-failure
|
|
47
48
|
* retries the whole record.
|
|
48
49
|
*/
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { inspect } from "node:util";
|
|
2
|
+
import { createLogger, type Logger } from "@remit/logger";
|
|
2
3
|
import {
|
|
3
|
-
createLogger,
|
|
4
|
-
type Logger,
|
|
5
4
|
queueNameFromEventSource,
|
|
6
5
|
recordQueueEvent,
|
|
7
|
-
|
|
8
|
-
} from "@remit/logger-lambda";
|
|
6
|
+
} from "@remit/logger-lambda/metrics";
|
|
7
|
+
import { installNodeLogger, withTelemetry } from "@remit/logger-lambda/node";
|
|
9
8
|
import type { SQSEvent, SQSHandler } from "aws-lambda";
|
|
10
9
|
import {
|
|
11
10
|
type CascadeEntity,
|
|
@@ -24,6 +23,8 @@ import type {
|
|
|
24
23
|
AccountFinalizeEvent,
|
|
25
24
|
} from "../events.js";
|
|
26
25
|
|
|
26
|
+
installNodeLogger();
|
|
27
|
+
|
|
27
28
|
export interface ProcessFinalizeDeps {
|
|
28
29
|
capabilities?: DeletionCapabilities;
|
|
29
30
|
cascadeServices?: CascadeServices;
|
|
@@ -131,10 +132,7 @@ export const processAccountFinalize = async (
|
|
|
131
132
|
* the work; this worker only ever consumes.
|
|
132
133
|
*
|
|
133
134
|
* Idempotency: the cascade on a missing key is a no-op, so a redelivered batch
|
|
134
|
-
* (or container) no-ops on already-gone rows.
|
|
135
|
-
* from here: on DynamoDB the fanout step enqueues a REMOVE per message id up
|
|
136
|
-
* front; on the relational backends the cascade emits a `message.removed` outbox
|
|
137
|
-
* row the search-index worker relays.
|
|
135
|
+
* (or container) no-ops on already-gone rows.
|
|
138
136
|
*/
|
|
139
137
|
export const processAccountDataPurgeFinalize = async (
|
|
140
138
|
event: AccountDataPurgeFinalizeEvent,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
-
import { noopLogger } from "@remit/logger
|
|
4
|
+
import { noopLogger } from "@remit/logger";
|
|
5
5
|
import type { CascadeServices } from "../cascade.js";
|
|
6
6
|
import type { AccountDataPurgeEvent } from "../events.js";
|
|
7
7
|
import {
|
|
@@ -12,28 +12,19 @@ import {
|
|
|
12
12
|
interface Sent {
|
|
13
13
|
queueUrl: string | undefined;
|
|
14
14
|
body: Record<string, unknown>;
|
|
15
|
-
entries?: Array<{ Id: string; MessageBody: string }>;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
const recordingSqs = (sent: Sent[]
|
|
17
|
+
const recordingSqs = (sent: Sent[]): SQSClient =>
|
|
19
18
|
({
|
|
20
19
|
send: async (command: {
|
|
21
|
-
input: {
|
|
22
|
-
QueueUrl?: string;
|
|
23
|
-
MessageBody?: string;
|
|
24
|
-
Entries?: Array<{ Id: string; MessageBody: string }>;
|
|
25
|
-
};
|
|
20
|
+
input: { QueueUrl?: string; MessageBody?: string };
|
|
26
21
|
}) => {
|
|
27
22
|
sent.push({
|
|
28
23
|
queueUrl: command.input.QueueUrl,
|
|
29
24
|
body: command.input.MessageBody
|
|
30
25
|
? JSON.parse(command.input.MessageBody)
|
|
31
26
|
: {},
|
|
32
|
-
entries: command.input.Entries,
|
|
33
27
|
});
|
|
34
|
-
if (command.input.Entries && failBatch) {
|
|
35
|
-
return { Failed: [{ Id: "0" }] };
|
|
36
|
-
}
|
|
37
28
|
return {};
|
|
38
29
|
},
|
|
39
30
|
}) as unknown as SQSClient;
|
|
@@ -73,19 +64,14 @@ const baseDeps = (
|
|
|
73
64
|
]),
|
|
74
65
|
sqs: recordingSqs(sent),
|
|
75
66
|
accountPurgeDeleteQueueUrl: "http://queue/purge",
|
|
76
|
-
searchIndexQueueUrl: "http://queue/search",
|
|
77
|
-
dataBackend: "dynamodb",
|
|
78
67
|
...overrides,
|
|
79
68
|
});
|
|
80
69
|
|
|
81
70
|
describe("processAccountDataPurge", () => {
|
|
82
|
-
it("enqueues
|
|
71
|
+
it("enqueues a subtrees batch and one container leftover", async () => {
|
|
83
72
|
const sent: Sent[] = [];
|
|
84
73
|
await processAccountDataPurge(event, noopLogger, baseDeps(sent));
|
|
85
74
|
|
|
86
|
-
const vectorBatch = sent.find((m) => m.queueUrl === "http://queue/search");
|
|
87
|
-
assert.equal(vectorBatch?.entries?.length, 2, "one entry per message");
|
|
88
|
-
|
|
89
75
|
const finalize = sent.filter((m) => m.queueUrl === "http://queue/purge");
|
|
90
76
|
const kinds = finalize.map((m) => m.body.kind);
|
|
91
77
|
assert.deepEqual(kinds, ["subtrees", "container"]);
|
|
@@ -96,27 +82,6 @@ describe("processAccountDataPurge", () => {
|
|
|
96
82
|
);
|
|
97
83
|
});
|
|
98
84
|
|
|
99
|
-
it("skips vector deletes on the relational backend, still enqueuing finalize", async () => {
|
|
100
|
-
const sent: Sent[] = [];
|
|
101
|
-
await processAccountDataPurge(
|
|
102
|
-
event,
|
|
103
|
-
noopLogger,
|
|
104
|
-
baseDeps(sent, { dataBackend: "sqlite" }),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
assert.equal(
|
|
108
|
-
sent.some((m) => m.queueUrl === "http://queue/search"),
|
|
109
|
-
false,
|
|
110
|
-
"no search-index enqueue on the relational backend",
|
|
111
|
-
);
|
|
112
|
-
assert.deepEqual(
|
|
113
|
-
sent
|
|
114
|
-
.filter((m) => m.queueUrl === "http://queue/purge")
|
|
115
|
-
.map((m) => m.body.kind),
|
|
116
|
-
["subtrees", "container"],
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
85
|
it("drops manifest rows outside the account's mailbox set", async () => {
|
|
121
86
|
const sent: Sent[] = [];
|
|
122
87
|
await processAccountDataPurge(event, noopLogger, {
|
|
@@ -167,15 +132,4 @@ describe("processAccountDataPurge", () => {
|
|
|
167
132
|
/dynamo throttled/,
|
|
168
133
|
);
|
|
169
134
|
});
|
|
170
|
-
|
|
171
|
-
it("throws when a vector-delete batch reports failed entries", async () => {
|
|
172
|
-
const sent: Sent[] = [];
|
|
173
|
-
await assert.rejects(
|
|
174
|
-
processAccountDataPurge(event, noopLogger, {
|
|
175
|
-
...baseDeps(sent),
|
|
176
|
-
sqs: recordingSqs(sent, true),
|
|
177
|
-
}),
|
|
178
|
-
/failed entries/,
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
135
|
});
|