@remit/storage-service 0.0.15 → 0.0.17
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/dist/backends/filesystem.js +26 -3
- package/dist/backends/s3.js +18 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/storage.d.ts +39 -0
- package/dist/storage.js +76 -21
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
|
-
import { mkdir, readdir, readFile, rm, stat, unlink, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readdir, readFile, rm, rmdir, stat, unlink, writeFile, } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
@@ -10,6 +10,17 @@ import { UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, } from "../outbox-upload
|
|
|
10
10
|
import { signStoragePath } from "../signed-path.js";
|
|
11
11
|
import { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, isStorageNotFoundError, } from "../storage.js";
|
|
12
12
|
const gzipAsync = promisify(gzip);
|
|
13
|
+
const DIRECTORY_IN_USE = new Set(["ENOENT", "ENOTEMPTY", "EEXIST"]);
|
|
14
|
+
const keepDirectoryInUse = (error) => {
|
|
15
|
+
if (typeof error === "object" &&
|
|
16
|
+
error !== null &&
|
|
17
|
+
"code" in error &&
|
|
18
|
+
typeof error.code === "string" &&
|
|
19
|
+
DIRECTORY_IN_USE.has(error.code)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
};
|
|
13
24
|
export const createFilesystemStorageService = (basePath, uploadUrls) => {
|
|
14
25
|
const storeInternal = async (params) => {
|
|
15
26
|
const { key, content, compress = true } = params;
|
|
@@ -168,8 +179,18 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
|
|
|
168
179
|
};
|
|
169
180
|
};
|
|
170
181
|
const deleteOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
await deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
|
|
183
|
+
};
|
|
184
|
+
const deletePrefix = async (prefix) => {
|
|
185
|
+
await rm(join(basePath, prefix), { recursive: true, force: true });
|
|
186
|
+
};
|
|
187
|
+
const deleteKeys = async (keys) => {
|
|
188
|
+
const paths = keys.map((key) => join(basePath, key));
|
|
189
|
+
await Promise.all(paths.map((path) => rm(path, { force: true })));
|
|
190
|
+
const directories = [...new Set(paths.map((path) => dirname(path)))].sort((a, b) => b.length - a.length);
|
|
191
|
+
for (const directory of directories) {
|
|
192
|
+
await rmdir(directory).catch(keepDirectoryInUse);
|
|
193
|
+
}
|
|
173
194
|
};
|
|
174
195
|
const storeDeduplicated = (params) => {
|
|
175
196
|
const { accountConfigId, accountId, content } = params;
|
|
@@ -242,6 +263,8 @@ export const createFilesystemStorageService = (basePath, uploadUrls) => {
|
|
|
242
263
|
listOutboxAttachments,
|
|
243
264
|
listOutboxDraftsWithAttachments,
|
|
244
265
|
deleteOutboxAttachments,
|
|
266
|
+
deletePrefix,
|
|
267
|
+
deleteKeys,
|
|
245
268
|
deleteOutboxAttachment,
|
|
246
269
|
statOutboxAttachment,
|
|
247
270
|
retrieveOutboxAttachment,
|
package/dist/backends/s3.js
CHANGED
|
@@ -7,6 +7,7 @@ import { ContentEncoding, StorageType } from "@remit/domain-enums";
|
|
|
7
7
|
import { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, isStorageNotFoundError, } from "../storage.js";
|
|
8
8
|
import { parseStorageUri } from "../uri.js";
|
|
9
9
|
const gzipAsync = promisify(gzip);
|
|
10
|
+
const STORAGE_DELETE_BATCH_SIZE = 1000;
|
|
10
11
|
export const createS3StorageService = (client, bucketName) => {
|
|
11
12
|
const storeInternal = async (params) => {
|
|
12
13
|
const { key, content, contentType, compress = true } = params;
|
|
@@ -194,8 +195,7 @@ export const createS3StorageService = (client, bucketName) => {
|
|
|
194
195
|
}), { expiresIn });
|
|
195
196
|
return { uploadUrl };
|
|
196
197
|
};
|
|
197
|
-
const
|
|
198
|
-
const prefix = buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId);
|
|
198
|
+
const deletePrefix = async (prefix) => {
|
|
199
199
|
let continuationToken;
|
|
200
200
|
do {
|
|
201
201
|
const response = await client.send(new ListObjectsV2Command({
|
|
@@ -217,6 +217,20 @@ export const createS3StorageService = (client, bucketName) => {
|
|
|
217
217
|
: undefined;
|
|
218
218
|
} while (continuationToken);
|
|
219
219
|
};
|
|
220
|
+
const deleteKeys = async (keys) => {
|
|
221
|
+
for (let start = 0; start < keys.length; start += STORAGE_DELETE_BATCH_SIZE) {
|
|
222
|
+
const batch = keys.slice(start, start + STORAGE_DELETE_BATCH_SIZE);
|
|
223
|
+
const response = await client.send(new DeleteObjectsCommand({
|
|
224
|
+
Bucket: bucketName,
|
|
225
|
+
Delete: { Objects: batch.map((Key) => ({ Key })), Quiet: true },
|
|
226
|
+
}));
|
|
227
|
+
const failed = response.Errors ?? [];
|
|
228
|
+
if (failed.length > 0) {
|
|
229
|
+
throw new Error(`S3 refused to delete ${failed.length} of ${batch.length} objects, first ${failed[0].Key}: ${failed[0].Code} ${failed[0].Message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const deleteOutboxAttachments = (accountConfigId, accountId, outboxMessageId) => deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
|
|
220
234
|
const storeDeduplicated = (params) => {
|
|
221
235
|
const { accountConfigId, accountId, content, contentType } = params;
|
|
222
236
|
const checksumSha256 = computeChecksum(content);
|
|
@@ -358,6 +372,8 @@ export const createS3StorageService = (client, bucketName) => {
|
|
|
358
372
|
listOutboxAttachments,
|
|
359
373
|
listOutboxDraftsWithAttachments,
|
|
360
374
|
deleteOutboxAttachments,
|
|
375
|
+
deletePrefix,
|
|
376
|
+
deleteKeys,
|
|
361
377
|
deleteOutboxAttachment,
|
|
362
378
|
statOutboxAttachment,
|
|
363
379
|
retrieveOutboxAttachment,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { type OutboxSweepResult, sweepAbandonedOutboxAttachments, } from "./outbox-sweep.js";
|
|
2
2
|
export { type AuthorizeUploadInput, type AuthorizeUploadResult, authorizeUploadRequest, UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, UPLOAD_URL_TTL_SECONDS, } from "./outbox-upload-url.js";
|
|
3
3
|
export { type SignedPathFailure, type SignedPathResult, signStoragePath, verifyStoragePath, } from "./signed-path.js";
|
|
4
|
-
export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, type ContentEncodingValue, type CreateOutboxAttachmentUploadUrlParams, computeChecksum, createMockStorageService, isStorageNotFoundError, type OutboxAttachmentListItem, type OutboxAttachmentUploadTarget, type ParsedAttachmentMeta, type ParsedBody, type ParsedContentStorageKey, type ParsedOutboxAttachmentKey, parseContentStorageKey, parseOutboxAttachmentKey, type StorageReference, type StorageService, type StorageTypeValue, type StoreBodyPartParams, type StoreDeduplicatedParams, type StoreMessageBodyParams, type StoreOutboxAttachmentParams, type StoreParsedBodyParams, } from "./storage.js";
|
|
4
|
+
export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, type ContentEncodingValue, type CreateOutboxAttachmentUploadUrlParams, computeChecksum, createMockStorageService, isStorageNotFoundError, MATERIALIZED_SENTINEL_PART_PATH, MATERIALIZED_SENTINEL_V2_PART_PATH, type MessageObjectKeys, messageObjectKeysOf, type OutboxAttachmentListItem, type OutboxAttachmentUploadTarget, type ParsedAttachmentMeta, type ParsedBody, type ParsedContentStorageKey, type ParsedOutboxAttachmentKey, parseContentStorageKey, parseOutboxAttachmentKey, type StorageReference, type StorageService, type StorageTypeValue, type StoreBodyPartParams, type StoreDeduplicatedParams, type StoreMessageBodyParams, type StoreOutboxAttachmentParams, type StoreParsedBodyParams, } from "./storage.js";
|
|
5
5
|
export { buildStorageUri, type ParsedStorageUri, parseStorageUri, } from "./uri.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { sweepAbandonedOutboxAttachments, } from "./outbox-sweep.js";
|
|
2
2
|
export { authorizeUploadRequest, UPLOAD_ROUTE_PREFIX, UPLOAD_URL_SIGNING_LABEL, UPLOAD_URL_TTL_SECONDS, } from "./outbox-upload-url.js";
|
|
3
3
|
export { signStoragePath, verifyStoragePath, } from "./signed-path.js";
|
|
4
|
-
export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, createMockStorageService, isStorageNotFoundError, parseContentStorageKey, parseOutboxAttachmentKey, } from "./storage.js";
|
|
4
|
+
export { buildBodyPartKey, buildDeduplicatedKey, buildExportArchiveKey, buildMessageBodyKey, buildOutboxAttachmentKey, buildOutboxAttachmentPrefix, buildParsedBodyKey, computeChecksum, createMockStorageService, isStorageNotFoundError, MATERIALIZED_SENTINEL_PART_PATH, MATERIALIZED_SENTINEL_V2_PART_PATH, messageObjectKeysOf, parseContentStorageKey, parseOutboxAttachmentKey, } from "./storage.js";
|
|
5
5
|
export { buildStorageUri, parseStorageUri, } from "./uri.js";
|
package/dist/storage.d.ts
CHANGED
|
@@ -190,6 +190,16 @@ export interface StorageService {
|
|
|
190
190
|
exists(uri: string): Promise<boolean>;
|
|
191
191
|
/** Delete content by URI */
|
|
192
192
|
delete(uri: string): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Delete every object whose key starts with `keyPrefix`, a key path ending
|
|
195
|
+
* in `/`. Deleting a prefix that holds nothing is a no-op.
|
|
196
|
+
*/
|
|
197
|
+
deletePrefix(keyPrefix: string): Promise<void>;
|
|
198
|
+
/**
|
|
199
|
+
* Delete the objects at `keys`, issuing one backend call per 1000 keys and
|
|
200
|
+
* no listing. A key that holds nothing is skipped.
|
|
201
|
+
*/
|
|
202
|
+
deleteKeys(keys: readonly string[]): Promise<void>;
|
|
193
203
|
/**
|
|
194
204
|
* Stream a finished export archive (already-compressed ZIP) to the export
|
|
195
205
|
* key for the given config + request, uncompressed at rest. The body is
|
|
@@ -240,6 +250,35 @@ export interface ParsedContentStorageKey {
|
|
|
240
250
|
* above). Returns null when the shape doesn't match.
|
|
241
251
|
*/
|
|
242
252
|
export declare const parseContentStorageKey: (storageKey: string) => ParsedContentStorageKey | null;
|
|
253
|
+
/**
|
|
254
|
+
* Sentinel object written under a message's parts prefix once lazy
|
|
255
|
+
* materialization has stored EVERY leaf. The read path checks it first with a
|
|
256
|
+
* single HEAD and early-returns, skipping the body.eml GET, the MIME re-parse,
|
|
257
|
+
* and the per-leaf HEADs on every warm re-open.
|
|
258
|
+
*
|
|
259
|
+
* Deliberately an S3-only marker (no Message-row flag). A leading dot keeps it
|
|
260
|
+
* out of any real IMAP section path (those are dot-separated digits).
|
|
261
|
+
*/
|
|
262
|
+
export declare const MATERIALIZED_SENTINEL_PART_PATH = ".materialized";
|
|
263
|
+
/**
|
|
264
|
+
* The sentinel for a message carrying a forwarded message. Those parts were
|
|
265
|
+
* once stored under a layout in which the forwarded message's root overwrote
|
|
266
|
+
* its own row, so a `.materialized` written then vouches for objects at the
|
|
267
|
+
* wrong paths. Its own name makes every such message materialize once more,
|
|
268
|
+
* overwriting what is there; no other message is touched.
|
|
269
|
+
*/
|
|
270
|
+
export declare const MATERIALIZED_SENTINEL_V2_PART_PATH = ".materialized.2";
|
|
271
|
+
export interface MessageObjectKeys {
|
|
272
|
+
own: string[];
|
|
273
|
+
shared: string[];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The storage keys a message's objects live at. `own` sit under the message's
|
|
277
|
+
* own id and no other message reads them. `shared` is the raw body
|
|
278
|
+
* `bodyStorageKey` names, which a copy shares with its source and every other
|
|
279
|
+
* copy, so it goes only once no row names it.
|
|
280
|
+
*/
|
|
281
|
+
export declare const messageObjectKeysOf: (bodyStorageKey: string, messageId: string, partPaths: Iterable<string>) => MessageObjectKeys;
|
|
243
282
|
export declare const computeChecksum: (content: Buffer) => string;
|
|
244
283
|
export declare const buildExportArchiveKey: (accountConfigId: string, exportRequestId: string) => string;
|
|
245
284
|
export declare const isStorageNotFoundError: (error: unknown) => boolean;
|
package/dist/storage.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
3
|
import { ContentEncoding, StorageType } from "@remit/domain-enums";
|
|
4
|
+
import { parseStorageUri } from "./uri.js";
|
|
4
5
|
// Path builders - centralized path formatting per RFC 011 + #224.
|
|
5
6
|
// The outer segment is `accountConfigId` (Cognito-derived tenant scope) so
|
|
6
7
|
// the edge can verify cross-tenant isolation at the URL prefix without
|
|
7
8
|
// loading any database record. The inner `accountId` distinguishes between
|
|
8
9
|
// multiple IMAP-account records owned by the same tenant.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const MESSAGE_BODY_OBJECT = "body.eml";
|
|
11
|
+
const PARSED_BODY_OBJECT = "parsed.json.gz";
|
|
12
|
+
const BODY_PARTS_PREFIX = "parts/";
|
|
13
|
+
export const buildMessageBodyKey = (accountConfigId, accountId, messageId) => `accounts/${accountConfigId}/${accountId}/messages/${messageId}/${MESSAGE_BODY_OBJECT}`;
|
|
14
|
+
export const buildParsedBodyKey = (accountConfigId, accountId, messageId) => `accounts/${accountConfigId}/${accountId}/messages/${messageId}/${PARSED_BODY_OBJECT}`;
|
|
15
|
+
export const buildBodyPartKey = (accountConfigId, accountId, messageId, partPath) => `accounts/${accountConfigId}/${accountId}/messages/${messageId}/${BODY_PARTS_PREFIX}${partPath}`;
|
|
12
16
|
export const buildOutboxAttachmentPrefix = (accountConfigId, accountId, outboxMessageId) => `accounts/${accountConfigId}/${accountId}/outbox/${outboxMessageId}/attachments/`;
|
|
13
17
|
export const buildOutboxAttachmentKey = (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => `${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}${outboxAttachmentId}`;
|
|
14
18
|
// Every segment is a generated id, so the shape is closed rather than
|
|
@@ -51,6 +55,50 @@ export const parseContentStorageKey = (storageKey) => {
|
|
|
51
55
|
messageId: match[3],
|
|
52
56
|
};
|
|
53
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* Sentinel object written under a message's parts prefix once lazy
|
|
60
|
+
* materialization has stored EVERY leaf. The read path checks it first with a
|
|
61
|
+
* single HEAD and early-returns, skipping the body.eml GET, the MIME re-parse,
|
|
62
|
+
* and the per-leaf HEADs on every warm re-open.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately an S3-only marker (no Message-row flag). A leading dot keeps it
|
|
65
|
+
* out of any real IMAP section path (those are dot-separated digits).
|
|
66
|
+
*/
|
|
67
|
+
export const MATERIALIZED_SENTINEL_PART_PATH = ".materialized";
|
|
68
|
+
/**
|
|
69
|
+
* The sentinel for a message carrying a forwarded message. Those parts were
|
|
70
|
+
* once stored under a layout in which the forwarded message's root overwrote
|
|
71
|
+
* its own row, so a `.materialized` written then vouches for objects at the
|
|
72
|
+
* wrong paths. Its own name makes every such message materialize once more,
|
|
73
|
+
* overwriting what is there; no other message is touched.
|
|
74
|
+
*/
|
|
75
|
+
export const MATERIALIZED_SENTINEL_V2_PART_PATH = ".materialized.2";
|
|
76
|
+
const MESSAGE_BODY_KEY = /(?:^|\/)(accounts\/[^/]+\/(?:[^/]+\/)?messages\/)([^/]+)\/([^/]+)$/;
|
|
77
|
+
/**
|
|
78
|
+
* The storage keys a message's objects live at. `own` sit under the message's
|
|
79
|
+
* own id and no other message reads them. `shared` is the raw body
|
|
80
|
+
* `bodyStorageKey` names, which a copy shares with its source and every other
|
|
81
|
+
* copy, so it goes only once no row names it.
|
|
82
|
+
*/
|
|
83
|
+
export const messageObjectKeysOf = (bodyStorageKey, messageId, partPaths) => {
|
|
84
|
+
const match = parseStorageUri(bodyStorageKey).storageKey.match(MESSAGE_BODY_KEY);
|
|
85
|
+
if (!match) {
|
|
86
|
+
throw new Error(`${bodyStorageKey} is not a message body key; its objects cannot be found to delete`);
|
|
87
|
+
}
|
|
88
|
+
const [, messages, ownerId, body] = match;
|
|
89
|
+
const own = `${messages}${messageId}/`;
|
|
90
|
+
return {
|
|
91
|
+
own: [
|
|
92
|
+
`${own}${PARSED_BODY_OBJECT}`,
|
|
93
|
+
...[
|
|
94
|
+
MATERIALIZED_SENTINEL_PART_PATH,
|
|
95
|
+
MATERIALIZED_SENTINEL_V2_PART_PATH,
|
|
96
|
+
...partPaths,
|
|
97
|
+
].map((partPath) => `${own}${BODY_PARTS_PREFIX}${partPath}`),
|
|
98
|
+
],
|
|
99
|
+
shared: [`${messages}${ownerId}/${body}`],
|
|
100
|
+
};
|
|
101
|
+
};
|
|
54
102
|
export const computeChecksum = (content) => createHash("sha256").update(content).digest("hex");
|
|
55
103
|
export const buildExportArchiveKey = (accountConfigId, exportRequestId) => `exports/${accountConfigId}/${exportRequestId}/export.zip`;
|
|
56
104
|
export const isStorageNotFoundError = (error) => {
|
|
@@ -65,15 +113,16 @@ export const isStorageNotFoundError = (error) => {
|
|
|
65
113
|
return true;
|
|
66
114
|
return false;
|
|
67
115
|
};
|
|
116
|
+
const MOCK_ROOT = "s3://mock/";
|
|
68
117
|
export const createMockStorageService = () => {
|
|
69
118
|
const storage = new Map();
|
|
70
119
|
const storeInternal = (key, content) => {
|
|
71
120
|
const checksumSha256 = computeChecksum(content);
|
|
72
|
-
const uri =
|
|
121
|
+
const uri = `${MOCK_ROOT}${key}`;
|
|
73
122
|
storage.set(uri, content);
|
|
74
123
|
return {
|
|
75
124
|
uri,
|
|
76
|
-
storageType: StorageType.
|
|
125
|
+
storageType: StorageType.S3,
|
|
77
126
|
storageLocation: "mock",
|
|
78
127
|
storageKey: key,
|
|
79
128
|
sizeBytes: content.length,
|
|
@@ -94,21 +143,21 @@ export const createMockStorageService = () => {
|
|
|
94
143
|
return storeInternal(buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId), content);
|
|
95
144
|
};
|
|
96
145
|
const listOutboxAttachments = async (accountConfigId, accountId, outboxMessageId) => {
|
|
97
|
-
const prefix =
|
|
146
|
+
const prefix = `${MOCK_ROOT}${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}`;
|
|
98
147
|
const items = [];
|
|
99
148
|
for (const [uri, content] of storage.entries()) {
|
|
100
149
|
if (!uri.startsWith(prefix))
|
|
101
150
|
continue;
|
|
102
151
|
items.push({
|
|
103
152
|
outboxAttachmentId: uri.slice(prefix.length),
|
|
104
|
-
key: uri.slice(
|
|
153
|
+
key: uri.slice(MOCK_ROOT.length),
|
|
105
154
|
sizeBytes: content.length,
|
|
106
155
|
});
|
|
107
156
|
}
|
|
108
157
|
return items;
|
|
109
158
|
};
|
|
110
159
|
const listOutboxDraftsWithAttachments = async (accountConfigId, accountId) => {
|
|
111
|
-
const root =
|
|
160
|
+
const root = `${MOCK_ROOT}accounts/${accountConfigId}/${accountId}/outbox/`;
|
|
112
161
|
const drafts = new Set();
|
|
113
162
|
for (const uri of storage.keys()) {
|
|
114
163
|
if (!uri.startsWith(root))
|
|
@@ -121,23 +170,27 @@ export const createMockStorageService = () => {
|
|
|
121
170
|
return [...drafts];
|
|
122
171
|
};
|
|
123
172
|
const deleteOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
|
|
124
|
-
storage.delete(
|
|
173
|
+
storage.delete(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
|
|
125
174
|
};
|
|
126
175
|
const statOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
|
|
127
|
-
const content = storage.get(
|
|
176
|
+
const content = storage.get(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`);
|
|
128
177
|
return content ? { sizeBytes: content.length } : null;
|
|
129
178
|
};
|
|
130
|
-
const retrieveOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => storage.get(
|
|
179
|
+
const retrieveOutboxAttachment = async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => storage.get(`${MOCK_ROOT}${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`) ?? null;
|
|
131
180
|
const createOutboxAttachmentUploadUrl = async (params) => ({
|
|
132
181
|
uploadUrl: `mock://upload/${buildOutboxAttachmentKey(params.accountConfigId, params.accountId, params.outboxMessageId, params.outboxAttachmentId)}?max=${params.sizeBytes}&exp=${params.expiresAt}`,
|
|
133
182
|
});
|
|
134
|
-
const
|
|
135
|
-
const prefix = `mock://${buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId)}`;
|
|
183
|
+
const deletePrefix = async (keyPrefix) => {
|
|
136
184
|
for (const uri of [...storage.keys()]) {
|
|
137
|
-
if (uri.startsWith(
|
|
185
|
+
if (uri.startsWith(`${MOCK_ROOT}${keyPrefix}`))
|
|
138
186
|
storage.delete(uri);
|
|
139
187
|
}
|
|
140
188
|
};
|
|
189
|
+
const deleteKeys = async (keys) => {
|
|
190
|
+
for (const key of keys)
|
|
191
|
+
storage.delete(`${MOCK_ROOT}${key}`);
|
|
192
|
+
};
|
|
193
|
+
const deleteOutboxAttachments = (accountConfigId, accountId, outboxMessageId) => deletePrefix(buildOutboxAttachmentPrefix(accountConfigId, accountId, outboxMessageId));
|
|
141
194
|
const storeDeduplicated = async (params) => {
|
|
142
195
|
const { accountConfigId, accountId, content } = params;
|
|
143
196
|
const checksumSha256 = computeChecksum(content);
|
|
@@ -149,29 +202,29 @@ export const createMockStorageService = () => {
|
|
|
149
202
|
return storeInternal(buildParsedBodyKey(accountConfigId, accountId, messageId), content);
|
|
150
203
|
};
|
|
151
204
|
const retrieveParsedBody = async (accountConfigId, accountId, messageId) => {
|
|
152
|
-
const uri =
|
|
205
|
+
const uri = `${MOCK_ROOT}${buildParsedBodyKey(accountConfigId, accountId, messageId)}`;
|
|
153
206
|
const content = storage.get(uri);
|
|
154
207
|
if (!content)
|
|
155
208
|
return null;
|
|
156
209
|
return JSON.parse(content.toString("utf8"));
|
|
157
210
|
};
|
|
158
211
|
const retrieveBodyPart = async (accountConfigId, accountId, messageId, partPath) => {
|
|
159
|
-
const uri =
|
|
212
|
+
const uri = `${MOCK_ROOT}${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
|
|
160
213
|
return storage.get(uri) ?? null;
|
|
161
214
|
};
|
|
162
215
|
const retrieveMessageBody = async (accountConfigId, accountId, messageId) => {
|
|
163
|
-
const uri =
|
|
216
|
+
const uri = `${MOCK_ROOT}${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
|
|
164
217
|
return storage.get(uri) ?? null;
|
|
165
218
|
};
|
|
166
219
|
const retrieveMessageBodyStream = async (accountConfigId, accountId, messageId) => {
|
|
167
|
-
const uri =
|
|
220
|
+
const uri = `${MOCK_ROOT}${buildMessageBodyKey(accountConfigId, accountId, messageId)}`;
|
|
168
221
|
const content = storage.get(uri);
|
|
169
222
|
if (!content)
|
|
170
223
|
return null;
|
|
171
224
|
return Readable.from(content);
|
|
172
225
|
};
|
|
173
226
|
const bodyPartExists = async (accountConfigId, accountId, messageId, partPath) => {
|
|
174
|
-
const uri =
|
|
227
|
+
const uri = `${MOCK_ROOT}${buildBodyPartKey(accountConfigId, accountId, messageId, partPath)}`;
|
|
175
228
|
return storage.has(uri);
|
|
176
229
|
};
|
|
177
230
|
return {
|
|
@@ -183,6 +236,8 @@ export const createMockStorageService = () => {
|
|
|
183
236
|
listOutboxAttachments,
|
|
184
237
|
listOutboxDraftsWithAttachments,
|
|
185
238
|
deleteOutboxAttachments,
|
|
239
|
+
deletePrefix,
|
|
240
|
+
deleteKeys,
|
|
186
241
|
deleteOutboxAttachment,
|
|
187
242
|
statOutboxAttachment,
|
|
188
243
|
retrieveOutboxAttachment,
|
|
@@ -211,11 +266,11 @@ export const createMockStorageService = () => {
|
|
|
211
266
|
for await (const chunk of body) {
|
|
212
267
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
213
268
|
}
|
|
214
|
-
storage.set(
|
|
269
|
+
storage.set(`${MOCK_ROOT}${key}`, Buffer.concat(chunks));
|
|
215
270
|
return key;
|
|
216
271
|
},
|
|
217
272
|
deleteExportArchive: async (accountConfigId, exportRequestId) => {
|
|
218
|
-
storage.delete(
|
|
273
|
+
storage.delete(`${MOCK_ROOT}${buildExportArchiveKey(accountConfigId, exportRequestId)}`);
|
|
219
274
|
},
|
|
220
275
|
getPresignedDownloadUrl: async (key, _expiresInSeconds) => `mock://presigned/${key}`,
|
|
221
276
|
};
|