@remit/storage-service 0.0.9 → 0.0.11

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/storage-service",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "scripts": {
26
26
  "test:typecheck": "tsgo --noEmit",
27
- "test:run": "node --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=87 --test 'src/**/*.test.ts'",
27
+ "test:run": "node $NODE_TEST_FLAGS --env-file=../../localhost-test-unit.env --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=87 --test 'src/**/*.test.ts'",
28
28
  "test": "npm run test:typecheck && npm run test:run"
29
29
  },
30
30
  "dependencies": {
@@ -400,6 +400,27 @@ export const createFilesystemStorageService = (
400
400
  return stats ? { sizeBytes: stats.size } : null;
401
401
  };
402
402
 
403
+ const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] = (
404
+ accountConfigId,
405
+ accountId,
406
+ outboxMessageId,
407
+ outboxAttachmentId,
408
+ ) =>
409
+ readFile(
410
+ join(
411
+ basePath,
412
+ buildOutboxAttachmentKey(
413
+ accountConfigId,
414
+ accountId,
415
+ outboxMessageId,
416
+ outboxAttachmentId,
417
+ ),
418
+ ),
419
+ ).catch((error: unknown) => {
420
+ if (isStorageNotFoundError(error)) return null;
421
+ throw error;
422
+ });
423
+
403
424
  // This backend has no presigned anything, so the URL addresses the
404
425
  // deployment's own upload route. Its authority is the same HMAC the read side
405
426
  // uses on /content, under the write label, covering the storage key, the
@@ -540,6 +561,7 @@ export const createFilesystemStorageService = (
540
561
  deleteOutboxAttachments,
541
562
  deleteOutboxAttachment,
542
563
  statOutboxAttachment,
564
+ retrieveOutboxAttachment,
543
565
  createOutboxAttachmentUploadUrl,
544
566
  storeDeduplicated,
545
567
  storeParsedBody,
@@ -467,6 +467,28 @@ export const createS3StorageService = (
467
467
  return { sizeBytes: head.ContentLength ?? 0 };
468
468
  };
469
469
 
470
+ const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] =
471
+ async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
472
+ const key = buildOutboxAttachmentKey(
473
+ accountConfigId,
474
+ accountId,
475
+ outboxMessageId,
476
+ outboxAttachmentId,
477
+ );
478
+ const response = await client
479
+ .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
480
+ .catch((error: unknown) => {
481
+ if (isStorageNotFoundError(error)) return null;
482
+ throw error;
483
+ });
484
+ if (!response) return null;
485
+ if (!response.Body) {
486
+ throw new Error(`Empty response body for outbox attachment: ${key}`);
487
+ }
488
+ const bytes = await response.Body.transformToByteArray();
489
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
490
+ };
491
+
470
492
  // The browser PUTs straight to the bucket and these bytes never touch the
471
493
  // API. `ContentLength` on the command hoists `content-length` into
472
494
  // X-Amz-SignedHeaders, so S3 recomputes the signature over the length it was
@@ -718,6 +740,7 @@ export const createS3StorageService = (
718
740
  deleteOutboxAttachments,
719
741
  deleteOutboxAttachment,
720
742
  statOutboxAttachment,
743
+ retrieveOutboxAttachment,
721
744
  createOutboxAttachmentUploadUrl,
722
745
  storeDeduplicated,
723
746
  storeParsedBody,
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { createFilesystemStorageService } from "./backends/filesystem.js";
7
+ import { createMockStorageService, type StorageService } from "./storage.js";
8
+
9
+ const OWNER = {
10
+ accountConfigId: "cfg-1",
11
+ accountId: "acc-1",
12
+ outboxMessageId: "ob-1",
13
+ };
14
+
15
+ const readsBackWhatWasStored = async (storage: StorageService) => {
16
+ const content = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00, 0xff]);
17
+ await storage.storeOutboxAttachment({
18
+ ...OWNER,
19
+ outboxAttachmentId: "att-1",
20
+ content,
21
+ });
22
+
23
+ assert.deepEqual(
24
+ await storage.retrieveOutboxAttachment(
25
+ OWNER.accountConfigId,
26
+ OWNER.accountId,
27
+ OWNER.outboxMessageId,
28
+ "att-1",
29
+ ),
30
+ content,
31
+ );
32
+ assert.equal(
33
+ await storage.retrieveOutboxAttachment(
34
+ OWNER.accountConfigId,
35
+ OWNER.accountId,
36
+ OWNER.outboxMessageId,
37
+ "att-missing",
38
+ ),
39
+ null,
40
+ );
41
+ };
42
+
43
+ describe("retrieveOutboxAttachment", () => {
44
+ it("reads back the exact bytes on the filesystem backend, and null for none", async () => {
45
+ const basePath = await mkdtemp(join(tmpdir(), "remit-outbox-retrieve-"));
46
+ try {
47
+ await readsBackWhatWasStored(createFilesystemStorageService(basePath));
48
+ } finally {
49
+ await rm(basePath, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ it("reads back the exact bytes on the mock backend, and null for none", async () => {
54
+ await readsBackWhatWasStored(createMockStorageService());
55
+ });
56
+ });
package/src/storage.ts CHANGED
@@ -266,6 +266,17 @@ export interface StorageService {
266
266
  outboxAttachmentId: string,
267
267
  ): Promise<{ sizeBytes: number } | null>;
268
268
 
269
+ /**
270
+ * The bytes of one attachment, for building the MIME part it goes out as.
271
+ * Null when nothing is stored under the key.
272
+ */
273
+ retrieveOutboxAttachment(
274
+ accountConfigId: string,
275
+ accountId: string,
276
+ outboxMessageId: string,
277
+ outboxAttachmentId: string,
278
+ ): Promise<Buffer | null>;
279
+
269
280
  /**
270
281
  * Mint somewhere to PUT one file. The backend decides what that is — block
271
282
  * storage direct, or this deployment's upload route — and the caller never
@@ -650,6 +661,12 @@ export const createMockStorageService = (): StorageService => {
650
661
  return content ? { sizeBytes: content.length } : null;
651
662
  };
652
663
 
664
+ const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] =
665
+ async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) =>
666
+ storage.get(
667
+ `mock://${buildOutboxAttachmentKey(accountConfigId, accountId, outboxMessageId, outboxAttachmentId)}`,
668
+ ) ?? null;
669
+
653
670
  const createOutboxAttachmentUploadUrl: StorageService["createOutboxAttachmentUploadUrl"] =
654
671
  async (params) => ({
655
672
  uploadUrl: `mock://upload/${buildOutboxAttachmentKey(params.accountConfigId, params.accountId, params.outboxMessageId, params.outboxAttachmentId)}?max=${params.sizeBytes}&exp=${params.expiresAt}`,
@@ -805,6 +822,7 @@ export const createMockStorageService = (): StorageService => {
805
822
  deleteOutboxAttachments,
806
823
  deleteOutboxAttachment,
807
824
  statOutboxAttachment,
825
+ retrieveOutboxAttachment,
808
826
  createOutboxAttachmentUploadUrl,
809
827
  storeDeduplicated,
810
828
  storeParsedBody,