@remit/storage-service 0.0.11 → 0.0.12

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.11",
3
+ "version": "0.0.12",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -29,9 +29,6 @@ import {
29
29
  buildBodyPartKey,
30
30
  buildDeduplicatedKey,
31
31
  buildExportArchiveKey,
32
- buildExtractedPrefix,
33
- buildExtractedSkippedKey,
34
- buildExtractedTextKey,
35
32
  buildMessageBodyKey,
36
33
  buildOutboxAttachmentKey,
37
34
  buildOutboxAttachmentPrefix,
@@ -174,105 +171,6 @@ export const createFilesystemStorageService = (
174
171
  : buffer;
175
172
  };
176
173
 
177
- const storeExtractedText: StorageService["storeExtractedText"] = (params) => {
178
- const { accountConfigId, accountId, messageId, partPath, text } = params;
179
- return storeInternal({
180
- key: buildExtractedTextKey(
181
- accountConfigId,
182
- accountId,
183
- messageId,
184
- partPath,
185
- ),
186
- content: Buffer.from(text, "utf8"),
187
- });
188
- };
189
-
190
- const storeExtractedSkipped: StorageService["storeExtractedSkipped"] = (
191
- params,
192
- ) => {
193
- const { accountConfigId, accountId, messageId, partPath, marker } = params;
194
- return storeInternal({
195
- key: buildExtractedSkippedKey(
196
- accountConfigId,
197
- accountId,
198
- messageId,
199
- partPath,
200
- ),
201
- content: Buffer.from(JSON.stringify(marker), "utf8"),
202
- compress: false,
203
- });
204
- };
205
-
206
- const extractedResultExists: StorageService["extractedResultExists"] = async (
207
- accountConfigId,
208
- accountId,
209
- messageId,
210
- partPath,
211
- ) => {
212
- const textPath = join(
213
- basePath,
214
- buildExtractedTextKey(accountConfigId, accountId, messageId, partPath),
215
- );
216
- const skippedPath = join(
217
- basePath,
218
- buildExtractedSkippedKey(accountConfigId, accountId, messageId, partPath),
219
- );
220
- const exists = (fullPath: string): Promise<boolean> =>
221
- stat(fullPath)
222
- .then(() => true)
223
- .catch(() => false);
224
-
225
- const [textExists, skippedExists] = await Promise.all([
226
- exists(textPath),
227
- exists(skippedPath),
228
- ]);
229
- return textExists || skippedExists;
230
- };
231
-
232
- const retrieveExtractedText: StorageService["retrieveExtractedText"] = async (
233
- accountConfigId,
234
- accountId,
235
- messageId,
236
- partPath,
237
- ) => {
238
- const fullPath = join(
239
- basePath,
240
- buildExtractedTextKey(accountConfigId, accountId, messageId, partPath),
241
- );
242
- const buffer = await readFile(fullPath).catch((error: unknown) => {
243
- if (isStorageNotFoundError(error)) return null;
244
- throw error;
245
- });
246
- if (!buffer) return null;
247
- const decoded =
248
- buffer[0] === 0x1f && buffer[1] === 0x8b ? gunzipSync(buffer) : buffer;
249
- return decoded.toString("utf8");
250
- };
251
-
252
- const listExtractedTexts: StorageService["listExtractedTexts"] = async (
253
- accountConfigId,
254
- accountId,
255
- messageId,
256
- ) => {
257
- const prefix = buildExtractedPrefix(accountConfigId, accountId, messageId);
258
- const suffix = ".txt.gz";
259
- const dirPath = join(basePath, prefix);
260
-
261
- const entries = await readdir(dirPath, { recursive: true }).catch(
262
- (error: unknown) => {
263
- if (isStorageNotFoundError(error)) return [];
264
- throw error;
265
- },
266
- );
267
-
268
- return entries
269
- .filter((entry) => entry.endsWith(suffix))
270
- .map((entry) => ({
271
- partPath: entry.slice(0, -suffix.length),
272
- key: `${prefix}${entry}`,
273
- }));
274
- };
275
-
276
174
  const storeOutboxAttachment: StorageService["storeOutboxAttachment"] = (
277
175
  params,
278
176
  ) => {
@@ -566,11 +464,6 @@ export const createFilesystemStorageService = (
566
464
  storeDeduplicated,
567
465
  storeParsedBody,
568
466
  retrieveParsedBody,
569
- storeExtractedText,
570
- storeExtractedSkipped,
571
- extractedResultExists,
572
- retrieveExtractedText,
573
- listExtractedTexts,
574
467
  retrieveMessageBody: async (accountConfigId, accountId, messageId) => {
575
468
  const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
576
469
  const fullPath = join(basePath, key);
@@ -367,160 +367,6 @@ describe("createS3StorageService", () => {
367
367
  );
368
368
  assert.strictEqual(miss, null);
369
369
  });
370
-
371
- test("storeExtractedText writes gzipped text keyed under .../extracted/{partPath}.txt.gz and round-trips", async () => {
372
- const { client, commands, stored } = createFakeS3Client();
373
- const storage = createS3StorageService(client, "bucket");
374
-
375
- const ref = await storage.storeExtractedText({
376
- accountConfigId: "cfg-x",
377
- accountId: "acc-x",
378
- messageId: "msg-x",
379
- partPath: "1.2",
380
- text: "extracted attachment text",
381
- });
382
-
383
- assert.strictEqual(
384
- ref.storageKey,
385
- "accounts/cfg-x/acc-x/messages/msg-x/extracted/1.2.txt.gz",
386
- );
387
- const put = commands.find((c) => c.name === "PutObjectCommand");
388
- assert.strictEqual(put?.input.ContentEncoding, "gzip");
389
- assert.strictEqual(put?.input.ContentType, "text/plain; charset=utf-8");
390
-
391
- const entry = stored.get(
392
- "accounts/cfg-x/acc-x/messages/msg-x/extracted/1.2.txt.gz",
393
- );
394
- assert.ok(entry);
395
- assert.strictEqual(
396
- gunzipSync(entry.body).toString("utf8"),
397
- "extracted attachment text",
398
- );
399
-
400
- const retrieved = await storage.retrieveExtractedText(
401
- "cfg-x",
402
- "acc-x",
403
- "msg-x",
404
- "1.2",
405
- );
406
- assert.strictEqual(retrieved, "extracted attachment text");
407
- });
408
-
409
- test("retrieveExtractedText returns null on miss", async () => {
410
- const { client } = createFakeS3Client();
411
- const storage = createS3StorageService(client, "bucket");
412
- const result = await storage.retrieveExtractedText(
413
- "cfg-x",
414
- "acc-x",
415
- "msg-x",
416
- "nope",
417
- );
418
- assert.strictEqual(result, null);
419
- });
420
-
421
- test("storeExtractedSkipped writes an uncompressed JSON marker keyed under .../extracted/{partPath}.skipped.json", async () => {
422
- const { client, commands, stored } = createFakeS3Client();
423
- const storage = createS3StorageService(client, "bucket");
424
-
425
- const ref = await storage.storeExtractedSkipped({
426
- accountConfigId: "cfg-s",
427
- accountId: "acc-s",
428
- messageId: "msg-s",
429
- partPath: "3",
430
- marker: { status: "failed", reason: "pdf: corrupt" },
431
- });
432
-
433
- assert.strictEqual(
434
- ref.storageKey,
435
- "accounts/cfg-s/acc-s/messages/msg-s/extracted/3.skipped.json",
436
- );
437
- const put = commands.find((c) => c.name === "PutObjectCommand");
438
- assert.strictEqual(put?.input.ContentEncoding, undefined);
439
- assert.strictEqual(put?.input.ContentType, "application/json");
440
-
441
- const entry = stored.get(
442
- "accounts/cfg-s/acc-s/messages/msg-s/extracted/3.skipped.json",
443
- );
444
- assert.ok(entry);
445
- assert.deepStrictEqual(JSON.parse(entry.body.toString("utf8")), {
446
- status: "failed",
447
- reason: "pdf: corrupt",
448
- });
449
- });
450
-
451
- test("extractedResultExists is true once either the text artifact or the skip marker is stored", async () => {
452
- const { client } = createFakeS3Client();
453
- const storage = createS3StorageService(client, "bucket");
454
-
455
- assert.strictEqual(
456
- await storage.extractedResultExists("cfg-e", "acc-e", "msg-e", "1"),
457
- false,
458
- );
459
-
460
- await storage.storeExtractedText({
461
- accountConfigId: "cfg-e",
462
- accountId: "acc-e",
463
- messageId: "msg-e",
464
- partPath: "1",
465
- text: "hi",
466
- });
467
- assert.strictEqual(
468
- await storage.extractedResultExists("cfg-e", "acc-e", "msg-e", "1"),
469
- true,
470
- );
471
-
472
- await storage.storeExtractedSkipped({
473
- accountConfigId: "cfg-e",
474
- accountId: "acc-e",
475
- messageId: "msg-e",
476
- partPath: "2",
477
- marker: { status: "skipped", reason: "too-large" },
478
- });
479
- assert.strictEqual(
480
- await storage.extractedResultExists("cfg-e", "acc-e", "msg-e", "2"),
481
- true,
482
- );
483
- });
484
-
485
- test("listExtractedTexts returns only .txt.gz artifacts for the given message, excluding skip markers and other messages", async () => {
486
- const { client } = createFakeS3Client();
487
- const storage = createS3StorageService(client, "bucket");
488
-
489
- await storage.storeExtractedText({
490
- accountConfigId: "cfg-l",
491
- accountId: "acc-l",
492
- messageId: "msg-l",
493
- partPath: "1",
494
- text: "one",
495
- });
496
- await storage.storeExtractedText({
497
- accountConfigId: "cfg-l",
498
- accountId: "acc-l",
499
- messageId: "msg-l",
500
- partPath: "2.1",
501
- text: "two",
502
- });
503
- await storage.storeExtractedSkipped({
504
- accountConfigId: "cfg-l",
505
- accountId: "acc-l",
506
- messageId: "msg-l",
507
- partPath: "3",
508
- marker: { status: "failed", reason: "doc: corrupt" },
509
- });
510
- await storage.storeExtractedText({
511
- accountConfigId: "cfg-l",
512
- accountId: "acc-l",
513
- messageId: "other-msg",
514
- partPath: "1",
515
- text: "unrelated",
516
- });
517
-
518
- const items = await storage.listExtractedTexts("cfg-l", "acc-l", "msg-l");
519
- assert.deepStrictEqual(items.map((i) => i.partPath).sort(), ["1", "2.1"]);
520
- for (const item of items) {
521
- assert.ok(item.key.endsWith(".txt.gz"));
522
- }
523
- });
524
370
  });
525
371
 
526
372
  describe("createS3StorageService: outbox attachments", () => {
@@ -15,7 +15,6 @@ import { Upload } from "@aws-sdk/lib-storage";
15
15
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
16
16
  import { ContentEncoding, StorageType } from "@remit/domain-enums";
17
17
  import type {
18
- ExtractedTextListItem,
19
18
  OutboxAttachmentListItem,
20
19
  ParsedBody,
21
20
  StorageReference,
@@ -25,9 +24,6 @@ import {
25
24
  buildBodyPartKey,
26
25
  buildDeduplicatedKey,
27
26
  buildExportArchiveKey,
28
- buildExtractedPrefix,
29
- buildExtractedSkippedKey,
30
- buildExtractedTextKey,
31
27
  buildMessageBodyKey,
32
28
  buildOutboxAttachmentKey,
33
29
  buildOutboxAttachmentPrefix,
@@ -201,134 +197,6 @@ export const createS3StorageService = (
201
197
  return response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
202
198
  };
203
199
 
204
- const storeExtractedText: StorageService["storeExtractedText"] = (params) => {
205
- const { accountConfigId, accountId, messageId, partPath, text } = params;
206
- return storeInternal({
207
- key: buildExtractedTextKey(
208
- accountConfigId,
209
- accountId,
210
- messageId,
211
- partPath,
212
- ),
213
- content: Buffer.from(text, "utf8"),
214
- contentType: "text/plain; charset=utf-8",
215
- });
216
- };
217
-
218
- const storeExtractedSkipped: StorageService["storeExtractedSkipped"] = (
219
- params,
220
- ) => {
221
- const { accountConfigId, accountId, messageId, partPath, marker } = params;
222
- return storeInternal({
223
- key: buildExtractedSkippedKey(
224
- accountConfigId,
225
- accountId,
226
- messageId,
227
- partPath,
228
- ),
229
- content: Buffer.from(JSON.stringify(marker), "utf8"),
230
- contentType: "application/json",
231
- compress: false,
232
- });
233
- };
234
-
235
- const extractedResultExists: StorageService["extractedResultExists"] = async (
236
- accountConfigId,
237
- accountId,
238
- messageId,
239
- partPath,
240
- ) => {
241
- const textKey = buildExtractedTextKey(
242
- accountConfigId,
243
- accountId,
244
- messageId,
245
- partPath,
246
- );
247
- const skippedKey = buildExtractedSkippedKey(
248
- accountConfigId,
249
- accountId,
250
- messageId,
251
- partPath,
252
- );
253
-
254
- const headExists = (key: string): Promise<boolean> =>
255
- client
256
- .send(new HeadObjectCommand({ Bucket: bucketName, Key: key }))
257
- .then(() => true)
258
- .catch(() => false);
259
-
260
- const [textExists, skippedExists] = await Promise.all([
261
- headExists(textKey),
262
- headExists(skippedKey),
263
- ]);
264
- return textExists || skippedExists;
265
- };
266
-
267
- const retrieveExtractedText: StorageService["retrieveExtractedText"] = async (
268
- accountConfigId,
269
- accountId,
270
- messageId,
271
- partPath,
272
- ) => {
273
- const key = buildExtractedTextKey(
274
- accountConfigId,
275
- accountId,
276
- messageId,
277
- partPath,
278
- );
279
-
280
- const response = await client
281
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
282
- .catch((error: unknown) => {
283
- if (isStorageNotFoundError(error)) return null;
284
- throw error;
285
- });
286
-
287
- if (!response) return null;
288
- if (!response.Body) {
289
- throw new Error(`Empty response body for extracted text: ${key}`);
290
- }
291
-
292
- const body = await response.Body.transformToByteArray();
293
- const buffer = Buffer.from(body);
294
- const decoded =
295
- response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
296
- return decoded.toString("utf8");
297
- };
298
-
299
- const listExtractedTexts: StorageService["listExtractedTexts"] = async (
300
- accountConfigId,
301
- accountId,
302
- messageId,
303
- ) => {
304
- const prefix = buildExtractedPrefix(accountConfigId, accountId, messageId);
305
- const suffix = ".txt.gz";
306
- const items: ExtractedTextListItem[] = [];
307
- let continuationToken: string | undefined;
308
-
309
- do {
310
- const response = await client.send(
311
- new ListObjectsV2Command({
312
- Bucket: bucketName,
313
- Prefix: prefix,
314
- ContinuationToken: continuationToken,
315
- }),
316
- );
317
- for (const object of response.Contents ?? []) {
318
- if (!object.Key || !object.Key.endsWith(suffix)) continue;
319
- items.push({
320
- partPath: object.Key.slice(prefix.length, -suffix.length),
321
- key: object.Key,
322
- });
323
- }
324
- continuationToken = response.IsTruncated
325
- ? response.NextContinuationToken
326
- : undefined;
327
- } while (continuationToken);
328
-
329
- return items;
330
- };
331
-
332
200
  const storeOutboxAttachment: StorageService["storeOutboxAttachment"] = (
333
201
  params,
334
202
  ) => {
@@ -745,11 +613,6 @@ export const createS3StorageService = (
745
613
  storeDeduplicated,
746
614
  storeParsedBody,
747
615
  retrieveParsedBody,
748
- storeExtractedText,
749
- storeExtractedSkipped,
750
- extractedResultExists,
751
- retrieveExtractedText,
752
- listExtractedTexts,
753
616
  retrieveMessageBody,
754
617
  retrieveMessageBodyStream,
755
618
  retrieve,
package/src/index.ts CHANGED
@@ -20,9 +20,6 @@ export {
20
20
  buildBodyPartKey,
21
21
  buildDeduplicatedKey,
22
22
  buildExportArchiveKey,
23
- buildExtractedPrefix,
24
- buildExtractedSkippedKey,
25
- buildExtractedTextKey,
26
23
  buildMessageBodyKey,
27
24
  buildOutboxAttachmentKey,
28
25
  buildOutboxAttachmentPrefix,
@@ -31,8 +28,6 @@ export {
31
28
  type CreateOutboxAttachmentUploadUrlParams,
32
29
  computeChecksum,
33
30
  createMockStorageService,
34
- type ExtractedTextListItem,
35
- type ExtractedTextMarker,
36
31
  isStorageNotFoundError,
37
32
  type OutboxAttachmentListItem,
38
33
  type OutboxAttachmentUploadTarget,
@@ -47,8 +42,6 @@ export {
47
42
  type StorageTypeValue,
48
43
  type StoreBodyPartParams,
49
44
  type StoreDeduplicatedParams,
50
- type StoreExtractedSkippedParams,
51
- type StoreExtractedTextParams,
52
45
  type StoreMessageBodyParams,
53
46
  type StoreOutboxAttachmentParams,
54
47
  type StoreParsedBodyParams,
@@ -10,9 +10,6 @@ import { createFilesystemStorageService } from "./backends/filesystem.js";
10
10
  import {
11
11
  buildBodyPartKey,
12
12
  buildDeduplicatedKey,
13
- buildExtractedPrefix,
14
- buildExtractedSkippedKey,
15
- buildExtractedTextKey,
16
13
  buildMessageBodyKey,
17
14
  buildOutboxAttachmentKey,
18
15
  buildOutboxAttachmentPrefix,
@@ -120,38 +117,6 @@ describe("path builders", () => {
120
117
  assert.strictEqual(segments[1], "CFG");
121
118
  assert.strictEqual(segments[2], "ACC");
122
119
  });
123
-
124
- test("buildExtractedTextKey nests accountId under accountConfigId and suffixes .txt.gz", () => {
125
- const key = buildExtractedTextKey("cfg1", "acc123", "msg456", "1.2");
126
- assert.strictEqual(
127
- key,
128
- "accounts/cfg1/acc123/messages/msg456/extracted/1.2.txt.gz",
129
- );
130
- });
131
-
132
- test("buildExtractedSkippedKey nests accountId under accountConfigId and suffixes .skipped.json", () => {
133
- const key = buildExtractedSkippedKey("cfg1", "acc123", "msg456", "1.2");
134
- assert.strictEqual(
135
- key,
136
- "accounts/cfg1/acc123/messages/msg456/extracted/1.2.skipped.json",
137
- );
138
- });
139
-
140
- test("buildExtractedTextKey and buildExtractedSkippedKey share the same prefix", () => {
141
- const prefix = buildExtractedPrefix("cfg1", "acc123", "msg456");
142
- assert.strictEqual(
143
- prefix,
144
- "accounts/cfg1/acc123/messages/msg456/extracted/",
145
- );
146
- assert.ok(
147
- buildExtractedTextKey("cfg1", "acc123", "msg456", "1").startsWith(prefix),
148
- );
149
- assert.ok(
150
- buildExtractedSkippedKey("cfg1", "acc123", "msg456", "1").startsWith(
151
- prefix,
152
- ),
153
- );
154
- });
155
120
  });
156
121
 
157
122
  describe("parseContentStorageKey", () => {
@@ -374,115 +339,6 @@ describe("createMockStorageService", () => {
374
339
  );
375
340
  assert.strictEqual(miss, null);
376
341
  });
377
-
378
- test("storeExtractedText / retrieveExtractedText round-trip", async () => {
379
- const storage = createMockStorageService();
380
-
381
- const ref = await storage.storeExtractedText({
382
- accountConfigId: "cfg1",
383
- accountId: "acc123",
384
- messageId: "msg456",
385
- partPath: "1.2",
386
- text: "extracted attachment text",
387
- });
388
-
389
- assert.strictEqual(
390
- ref.storageKey,
391
- "accounts/cfg1/acc123/messages/msg456/extracted/1.2.txt.gz",
392
- );
393
-
394
- const retrieved = await storage.retrieveExtractedText(
395
- "cfg1",
396
- "acc123",
397
- "msg456",
398
- "1.2",
399
- );
400
- assert.strictEqual(retrieved, "extracted attachment text");
401
- });
402
-
403
- test("retrieveExtractedText returns null on miss", async () => {
404
- const storage = createMockStorageService();
405
- const result = await storage.retrieveExtractedText(
406
- "cfg1",
407
- "acc123",
408
- "msg456",
409
- "nope",
410
- );
411
- assert.strictEqual(result, null);
412
- });
413
-
414
- test("extractedResultExists is true after either the text artifact or the skip marker is stored", async () => {
415
- const storage = createMockStorageService();
416
-
417
- assert.strictEqual(
418
- await storage.extractedResultExists("cfg1", "acc123", "msg456", "1"),
419
- false,
420
- );
421
-
422
- await storage.storeExtractedText({
423
- accountConfigId: "cfg1",
424
- accountId: "acc123",
425
- messageId: "msg456",
426
- partPath: "1",
427
- text: "hi",
428
- });
429
- assert.strictEqual(
430
- await storage.extractedResultExists("cfg1", "acc123", "msg456", "1"),
431
- true,
432
- );
433
-
434
- await storage.storeExtractedSkipped({
435
- accountConfigId: "cfg1",
436
- accountId: "acc123",
437
- messageId: "msg456",
438
- partPath: "2",
439
- marker: { status: "skipped", reason: "type-not-allowed" },
440
- });
441
- assert.strictEqual(
442
- await storage.extractedResultExists("cfg1", "acc123", "msg456", "2"),
443
- true,
444
- );
445
- });
446
-
447
- test("listExtractedTexts returns only .txt.gz artifacts, not skip markers", async () => {
448
- const storage = createMockStorageService();
449
-
450
- await storage.storeExtractedText({
451
- accountConfigId: "cfg1",
452
- accountId: "acc123",
453
- messageId: "msg456",
454
- partPath: "1",
455
- text: "first",
456
- });
457
- await storage.storeExtractedText({
458
- accountConfigId: "cfg1",
459
- accountId: "acc123",
460
- messageId: "msg456",
461
- partPath: "2.1",
462
- text: "second",
463
- });
464
- await storage.storeExtractedSkipped({
465
- accountConfigId: "cfg1",
466
- accountId: "acc123",
467
- messageId: "msg456",
468
- partPath: "3",
469
- marker: { status: "failed", reason: "pdf: corrupt" },
470
- });
471
- // A different message must not leak into the list.
472
- await storage.storeExtractedText({
473
- accountConfigId: "cfg1",
474
- accountId: "acc123",
475
- messageId: "other-msg",
476
- partPath: "1",
477
- text: "unrelated",
478
- });
479
-
480
- const items = await storage.listExtractedTexts("cfg1", "acc123", "msg456");
481
- assert.deepStrictEqual(items.map((i) => i.partPath).sort(), ["1", "2.1"]);
482
- for (const item of items) {
483
- assert.ok(item.key.endsWith(".txt.gz"));
484
- }
485
- });
486
342
  });
487
343
 
488
344
  describe("createFilesystemStorageService", () => {
@@ -670,102 +526,6 @@ describe("createFilesystemStorageService", () => {
670
526
  assert.strictEqual(miss, null);
671
527
  });
672
528
 
673
- test("storeExtractedText / retrieveExtractedText round-trip with gzip", async () => {
674
- const storage = createFilesystemStorageService(testBasePath);
675
-
676
- const ref = await storage.storeExtractedText({
677
- accountConfigId: "cfg1",
678
- accountId: "acc123",
679
- messageId: "msg-extracted-fs",
680
- partPath: "1",
681
- text: "fs extracted text",
682
- });
683
-
684
- assert.strictEqual(
685
- ref.storageKey,
686
- "accounts/cfg1/acc123/messages/msg-extracted-fs/extracted/1.txt.gz",
687
- );
688
- assert.strictEqual(ref.contentEncoding, ContentEncoding.Gzip);
689
-
690
- const retrieved = await storage.retrieveExtractedText(
691
- "cfg1",
692
- "acc123",
693
- "msg-extracted-fs",
694
- "1",
695
- );
696
- assert.strictEqual(retrieved, "fs extracted text");
697
- });
698
-
699
- test("storeExtractedSkipped writes an uncompressed JSON marker that satisfies extractedResultExists", async () => {
700
- const storage = createFilesystemStorageService(testBasePath);
701
-
702
- assert.strictEqual(
703
- await storage.extractedResultExists("cfg1", "acc123", "msg-skip-fs", "1"),
704
- false,
705
- );
706
-
707
- const ref = await storage.storeExtractedSkipped({
708
- accountConfigId: "cfg1",
709
- accountId: "acc123",
710
- messageId: "msg-skip-fs",
711
- partPath: "1",
712
- marker: { status: "skipped", reason: "too-large" },
713
- });
714
-
715
- assert.strictEqual(
716
- ref.storageKey,
717
- "accounts/cfg1/acc123/messages/msg-skip-fs/extracted/1.skipped.json",
718
- );
719
- assert.strictEqual(ref.contentEncoding, ContentEncoding.None);
720
- assert.strictEqual(
721
- await storage.extractedResultExists("cfg1", "acc123", "msg-skip-fs", "1"),
722
- true,
723
- );
724
- });
725
-
726
- test("listExtractedTexts lists only .txt.gz artifacts for the given message", async () => {
727
- const storage = createFilesystemStorageService(testBasePath);
728
-
729
- await storage.storeExtractedText({
730
- accountConfigId: "cfg1",
731
- accountId: "acc123",
732
- messageId: "msg-list-fs",
733
- partPath: "1",
734
- text: "one",
735
- });
736
- await storage.storeExtractedText({
737
- accountConfigId: "cfg1",
738
- accountId: "acc123",
739
- messageId: "msg-list-fs",
740
- partPath: "2.1",
741
- text: "two",
742
- });
743
- await storage.storeExtractedSkipped({
744
- accountConfigId: "cfg1",
745
- accountId: "acc123",
746
- messageId: "msg-list-fs",
747
- partPath: "3",
748
- marker: { status: "failed", reason: "doc: corrupt" },
749
- });
750
-
751
- const items = await storage.listExtractedTexts(
752
- "cfg1",
753
- "acc123",
754
- "msg-list-fs",
755
- );
756
- assert.deepStrictEqual(items.map((i) => i.partPath).sort(), ["1", "2.1"]);
757
- });
758
-
759
- test("listExtractedTexts returns an empty array when no extraction has happened yet", async () => {
760
- const storage = createFilesystemStorageService(testBasePath);
761
- const items = await storage.listExtractedTexts(
762
- "cfg1",
763
- "acc123",
764
- "msg-never-scanned",
765
- );
766
- assert.deepStrictEqual(items, []);
767
- });
768
-
769
529
  test("storeOutboxAttachment writes the bytes uncompressed so the size on disk is the size the cap counts", async () => {
770
530
  const storage = createFilesystemStorageService(testBasePath);
771
531
  // Highly compressible: gzip would make the stored size disagree with the
package/src/storage.ts CHANGED
@@ -128,41 +128,6 @@ export interface StoreParsedBodyParams {
128
128
  parsed: ParsedBody;
129
129
  }
130
130
 
131
- /** Parameters for storing extracted attachment text (attachment-scan worker, #450) */
132
- export interface StoreExtractedTextParams {
133
- accountConfigId: string;
134
- accountId: string;
135
- messageId: string;
136
- partPath: string;
137
- text: string;
138
- }
139
-
140
- /**
141
- * Marker written in place of extracted text when extraction is skipped or
142
- * fails, so the idempotency check terminates retries on a poison or
143
- * unsupported attachment instead of re-running extraction on every
144
- * redelivery.
145
- */
146
- export interface ExtractedTextMarker {
147
- status: "skipped" | "failed";
148
- reason: string;
149
- }
150
-
151
- /** Parameters for storing an extraction skip/failure marker */
152
- export interface StoreExtractedSkippedParams {
153
- accountConfigId: string;
154
- accountId: string;
155
- messageId: string;
156
- partPath: string;
157
- marker: ExtractedTextMarker;
158
- }
159
-
160
- /** One extracted-text artifact found under a message's `extracted/` prefix */
161
- export interface ExtractedTextListItem {
162
- partPath: string;
163
- key: string;
164
- }
165
-
166
131
  export interface StorageService {
167
132
  /** Store a message body (raw RFC822 content) */
168
133
  storeMessageBody(params: StoreMessageBodyParams): Promise<StorageReference>;
@@ -301,48 +266,6 @@ export interface StorageService {
301
266
  messageId: string,
302
267
  ): Promise<ParsedBody | null>;
303
268
 
304
- /** Store extracted attachment text (attachment-scan worker, #450) */
305
- storeExtractedText(
306
- params: StoreExtractedTextParams,
307
- ): Promise<StorageReference>;
308
-
309
- /** Store a skip/failure marker in place of extracted text */
310
- storeExtractedSkipped(
311
- params: StoreExtractedSkippedParams,
312
- ): Promise<StorageReference>;
313
-
314
- /**
315
- * Whether an extraction result — either the text artifact or the
316
- * skip/failure marker — already exists for this part. Makes extraction
317
- * idempotent: a redelivered SQS message short-circuits instead of
318
- * re-running extraction (#450).
319
- */
320
- extractedResultExists(
321
- accountConfigId: string,
322
- accountId: string,
323
- messageId: string,
324
- partPath: string,
325
- ): Promise<boolean>;
326
-
327
- /** Retrieve previously extracted text; returns null on NoSuchKey */
328
- retrieveExtractedText(
329
- accountConfigId: string,
330
- accountId: string,
331
- messageId: string,
332
- partPath: string,
333
- ): Promise<string | null>;
334
-
335
- /**
336
- * List every extracted-text artifact stored for a message — the
337
- * `.skipped.json` markers are excluded. Used by the search-index worker
338
- * (#452) to fold attachment text into a message's vectors.
339
- */
340
- listExtractedTexts(
341
- accountConfigId: string,
342
- accountId: string,
343
- messageId: string,
344
- ): Promise<ExtractedTextListItem[]>;
345
-
346
269
  /** Retrieve a raw message body (RFC822 .eml) by account/message id; returns null on NoSuchKey */
347
270
  retrieveMessageBody(
348
271
  accountConfigId: string,
@@ -420,29 +343,6 @@ export const buildBodyPartKey = (
420
343
  ): string =>
421
344
  `accounts/${accountConfigId}/${accountId}/messages/${messageId}/parts/${partPath}`;
422
345
 
423
- export const buildExtractedTextKey = (
424
- accountConfigId: string,
425
- accountId: string,
426
- messageId: string,
427
- partPath: string,
428
- ): string =>
429
- `${buildExtractedPrefix(accountConfigId, accountId, messageId)}${partPath}.txt.gz`;
430
-
431
- export const buildExtractedSkippedKey = (
432
- accountConfigId: string,
433
- accountId: string,
434
- messageId: string,
435
- partPath: string,
436
- ): string =>
437
- `${buildExtractedPrefix(accountConfigId, accountId, messageId)}${partPath}.skipped.json`;
438
-
439
- export const buildExtractedPrefix = (
440
- accountConfigId: string,
441
- accountId: string,
442
- messageId: string,
443
- ): string =>
444
- `accounts/${accountConfigId}/${accountId}/messages/${messageId}/extracted/`;
445
-
446
346
  export const buildOutboxAttachmentPrefix = (
447
347
  accountConfigId: string,
448
348
  accountId: string,
@@ -721,68 +621,6 @@ export const createMockStorageService = (): StorageService => {
721
621
  return storage.get(uri) ?? null;
722
622
  };
723
623
 
724
- const storeExtractedText: StorageService["storeExtractedText"] = async (
725
- params,
726
- ) => {
727
- const { accountConfigId, accountId, messageId, partPath, text } = params;
728
- return storeInternal(
729
- buildExtractedTextKey(accountConfigId, accountId, messageId, partPath),
730
- Buffer.from(text, "utf8"),
731
- );
732
- };
733
-
734
- const storeExtractedSkipped: StorageService["storeExtractedSkipped"] = async (
735
- params,
736
- ) => {
737
- const { accountConfigId, accountId, messageId, partPath, marker } = params;
738
- return storeInternal(
739
- buildExtractedSkippedKey(accountConfigId, accountId, messageId, partPath),
740
- Buffer.from(JSON.stringify(marker), "utf8"),
741
- );
742
- };
743
-
744
- const extractedResultExists: StorageService["extractedResultExists"] = async (
745
- accountConfigId,
746
- accountId,
747
- messageId,
748
- partPath,
749
- ) => {
750
- const textUri = `mock://${buildExtractedTextKey(accountConfigId, accountId, messageId, partPath)}`;
751
- const skippedUri = `mock://${buildExtractedSkippedKey(accountConfigId, accountId, messageId, partPath)}`;
752
- return storage.has(textUri) || storage.has(skippedUri);
753
- };
754
-
755
- const retrieveExtractedText: StorageService["retrieveExtractedText"] = async (
756
- accountConfigId,
757
- accountId,
758
- messageId,
759
- partPath,
760
- ) => {
761
- const uri = `mock://${buildExtractedTextKey(accountConfigId, accountId, messageId, partPath)}`;
762
- const content = storage.get(uri);
763
- if (!content) return null;
764
- return content.toString("utf8");
765
- };
766
-
767
- const listExtractedTexts: StorageService["listExtractedTexts"] = async (
768
- accountConfigId,
769
- accountId,
770
- messageId,
771
- ) => {
772
- const prefix = `mock://${buildExtractedPrefix(accountConfigId, accountId, messageId)}`;
773
- const suffix = ".txt.gz";
774
- const items: ExtractedTextListItem[] = [];
775
- for (const uri of storage.keys()) {
776
- if (!uri.startsWith(prefix) || !uri.endsWith(suffix)) continue;
777
- const key = uri.slice("mock://".length);
778
- items.push({
779
- partPath: uri.slice(prefix.length, -suffix.length),
780
- key,
781
- });
782
- }
783
- return items;
784
- };
785
-
786
624
  const retrieveMessageBody: StorageService["retrieveMessageBody"] = async (
787
625
  accountConfigId,
788
626
  accountId,
@@ -827,11 +665,6 @@ export const createMockStorageService = (): StorageService => {
827
665
  storeDeduplicated,
828
666
  storeParsedBody,
829
667
  retrieveParsedBody,
830
- storeExtractedText,
831
- storeExtractedSkipped,
832
- extractedResultExists,
833
- retrieveExtractedText,
834
- listExtractedTexts,
835
668
  retrieveMessageBody,
836
669
  retrieveMessageBodyStream,
837
670
  retrieve: async (uri) => {