@remit/backend 0.0.59 → 0.0.61
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/dev-server/server.ts +47 -5
- package/dev-server/upload-handler.ts +132 -0
- package/dev-server/upload-route.test.ts +196 -0
- package/package.json +1 -1
- package/src/derive/contentSignature.test.ts +26 -0
- package/src/derive/contentSignature.ts +24 -41
- package/src/handlers/address.test.ts +132 -0
- package/src/handlers/index.ts +6 -1
- package/src/handlers/outbox-attachment.test.ts +763 -0
- package/src/handlers/outbox-attachment.ts +97 -0
- package/src/handlers/outbox.test.ts +247 -1
- package/src/handlers/outbox.ts +53 -4
- package/src/service/compose-sqlite.ts +2 -0
- package/src/service/create-remit-client.ts +15 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MintOutboxAttachmentInput,
|
|
3
|
+
MintOutboxAttachmentResponse,
|
|
4
|
+
OutboxAttachmentRejection,
|
|
5
|
+
OutboxAttachmentResponse,
|
|
6
|
+
} from "@remit/api-openapi-types";
|
|
7
|
+
import { OutboxAttachmentRejectionReason } from "@remit/domain-enums";
|
|
8
|
+
import type {
|
|
9
|
+
OutboxAttachmentRejectionDetail,
|
|
10
|
+
OutboxAttachmentRejectionReasonValue,
|
|
11
|
+
} from "@remit/mailbox-service";
|
|
12
|
+
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
13
|
+
import type { Context } from "openapi-backend";
|
|
14
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
15
|
+
import { getClient } from "../service/data-client.js";
|
|
16
|
+
|
|
17
|
+
const TOO_LARGE: ReadonlySet<OutboxAttachmentRejectionReasonValue> = new Set([
|
|
18
|
+
OutboxAttachmentRejectionReason.FileTooLarge,
|
|
19
|
+
OutboxAttachmentRejectionReason.MessageTooLarge,
|
|
20
|
+
OutboxAttachmentRejectionReason.TooManyAttachments,
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
interface RejectionResponse {
|
|
24
|
+
statusCode: number;
|
|
25
|
+
body: OutboxAttachmentRejection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const refuse = (
|
|
29
|
+
detail: OutboxAttachmentRejectionDetail,
|
|
30
|
+
): RejectionResponse => ({
|
|
31
|
+
statusCode: TOO_LARGE.has(detail.reason) ? 413 : 400,
|
|
32
|
+
body: {
|
|
33
|
+
reason: detail.reason,
|
|
34
|
+
message: detail.message,
|
|
35
|
+
limitBytes: detail.limitBytes,
|
|
36
|
+
usedBytes: detail.usedBytes,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reserve room on a draft and hand back somewhere to put the bytes. Small JSON
|
|
42
|
+
* in, small JSON out — the file itself never passes through here, which is what
|
|
43
|
+
* lets the upload live on its own fleet and keeps this operation under any
|
|
44
|
+
* payload ceiling the REST tier has.
|
|
45
|
+
*/
|
|
46
|
+
export const mintOutboxAttachment = async (
|
|
47
|
+
context: Context,
|
|
48
|
+
...args: unknown[]
|
|
49
|
+
): Promise<MintOutboxAttachmentResponse | RejectionResponse> => {
|
|
50
|
+
const event = args[0] as APIGatewayProxyEvent;
|
|
51
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
52
|
+
const { outboxMessageId } = context.request.params as {
|
|
53
|
+
outboxMessageId: string;
|
|
54
|
+
};
|
|
55
|
+
const input = context.request.requestBody as MintOutboxAttachmentInput;
|
|
56
|
+
|
|
57
|
+
const client = await getClient();
|
|
58
|
+
const result = await client.outboxAttachment.mint({
|
|
59
|
+
accountConfigId,
|
|
60
|
+
outboxMessageId,
|
|
61
|
+
filename: input.filename,
|
|
62
|
+
contentType: input.contentType,
|
|
63
|
+
sizeBytes: input.sizeBytes,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (result.outcome === "Rejected") return refuse(result.rejection);
|
|
67
|
+
|
|
68
|
+
return result.reservation;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Confirm the upload landed. The size in the response is read back from
|
|
73
|
+
* storage — on a deployment where the browser PUT straight to block storage
|
|
74
|
+
* this call is the first the API hears of the bytes, and the client's word for
|
|
75
|
+
* how many there are is not evidence.
|
|
76
|
+
*/
|
|
77
|
+
export const completeOutboxAttachment = async (
|
|
78
|
+
context: Context,
|
|
79
|
+
...args: unknown[]
|
|
80
|
+
): Promise<OutboxAttachmentResponse | RejectionResponse> => {
|
|
81
|
+
const event = args[0] as APIGatewayProxyEvent;
|
|
82
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
83
|
+
const { outboxMessageId, outboxAttachmentId } = context.request.params as {
|
|
84
|
+
outboxMessageId: string;
|
|
85
|
+
outboxAttachmentId: string;
|
|
86
|
+
};
|
|
87
|
+
const client = await getClient();
|
|
88
|
+
const result = await client.outboxAttachment.complete({
|
|
89
|
+
accountConfigId,
|
|
90
|
+
outboxMessageId,
|
|
91
|
+
outboxAttachmentId,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (result.outcome === "Rejected") return refuse(result.rejection);
|
|
95
|
+
|
|
96
|
+
return result.attachment;
|
|
97
|
+
};
|
|
@@ -17,15 +17,25 @@ import assert from "node:assert/strict";
|
|
|
17
17
|
import { afterEach, describe, it } from "node:test";
|
|
18
18
|
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
19
19
|
import type {
|
|
20
|
+
CreateOutboxAttachmentInput,
|
|
20
21
|
CreateOutboxMessageInput,
|
|
21
22
|
IAccountRepository,
|
|
23
|
+
IOutboxAttachmentRepository,
|
|
22
24
|
IOutboxMessageRepository,
|
|
25
|
+
OutboxAttachmentItem,
|
|
23
26
|
OutboxMessageItem,
|
|
24
27
|
UpdateOutboxMessageInput,
|
|
25
28
|
} from "@remit/data-ports";
|
|
26
29
|
import { NotFoundError } from "@remit/data-ports/errors";
|
|
27
30
|
import { OutboxMessageStatus } from "@remit/domain-enums";
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
OutboxAttachmentService,
|
|
33
|
+
OutboxQueueService,
|
|
34
|
+
} from "@remit/mailbox-service";
|
|
35
|
+
import {
|
|
36
|
+
createMockStorageService,
|
|
37
|
+
type StorageService,
|
|
38
|
+
} from "@remit/storage-service";
|
|
29
39
|
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
30
40
|
import type { Context } from "openapi-backend";
|
|
31
41
|
import { deriveAccountConfigId } from "../auth.js";
|
|
@@ -33,6 +43,7 @@ import { handleError } from "../error.js";
|
|
|
33
43
|
import { formatResponse } from "../response.js";
|
|
34
44
|
import {
|
|
35
45
|
_resetForTest,
|
|
46
|
+
getClient,
|
|
36
47
|
type RemitClient,
|
|
37
48
|
setClient,
|
|
38
49
|
} from "../service/data-client.js";
|
|
@@ -138,13 +149,83 @@ const accountRepository = {
|
|
|
138
149
|
}),
|
|
139
150
|
} as unknown as IAccountRepository;
|
|
140
151
|
|
|
152
|
+
let installedStorage: StorageService | null = null;
|
|
153
|
+
let attachmentRows = new Map<string, OutboxAttachmentItem>();
|
|
154
|
+
|
|
155
|
+
/** Rows only — this suite asserts what a discard takes, not how bytes move. */
|
|
156
|
+
const createAttachmentRepository = (
|
|
157
|
+
rows: Map<string, OutboxAttachmentItem>,
|
|
158
|
+
): IOutboxAttachmentRepository =>
|
|
159
|
+
({
|
|
160
|
+
reserve: async (input: CreateOutboxAttachmentInput) => {
|
|
161
|
+
const item = {
|
|
162
|
+
...input,
|
|
163
|
+
state: "Pending",
|
|
164
|
+
createdAt: 0,
|
|
165
|
+
updatedAt: 0,
|
|
166
|
+
} as OutboxAttachmentItem;
|
|
167
|
+
rows.set(item.outboxAttachmentId, item);
|
|
168
|
+
return { outcome: "Reserved", item };
|
|
169
|
+
},
|
|
170
|
+
listByOutboxMessage: async (
|
|
171
|
+
_accountConfigId: string,
|
|
172
|
+
outboxMessageId: string,
|
|
173
|
+
) =>
|
|
174
|
+
[...rows.values()].filter(
|
|
175
|
+
(row) => row.outboxMessageId === outboxMessageId,
|
|
176
|
+
),
|
|
177
|
+
deleteByOutboxMessage: async (
|
|
178
|
+
_accountConfigId: string,
|
|
179
|
+
outboxMessageId: string,
|
|
180
|
+
) => {
|
|
181
|
+
for (const row of [...rows.values()]) {
|
|
182
|
+
if (row.outboxMessageId === outboxMessageId) {
|
|
183
|
+
rows.delete(row.outboxAttachmentId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
deleteLapsedReservations: async (
|
|
188
|
+
accountConfigId: string,
|
|
189
|
+
outboxMessageId: string,
|
|
190
|
+
nowSeconds: number,
|
|
191
|
+
) => {
|
|
192
|
+
const gone: string[] = [];
|
|
193
|
+
for (const row of [...rows.values()]) {
|
|
194
|
+
if (
|
|
195
|
+
row.accountConfigId === accountConfigId &&
|
|
196
|
+
row.outboxMessageId === outboxMessageId &&
|
|
197
|
+
row.state === "Pending" &&
|
|
198
|
+
row.reservationExpiresAt < nowSeconds
|
|
199
|
+
) {
|
|
200
|
+
rows.delete(row.outboxAttachmentId);
|
|
201
|
+
gone.push(row.outboxAttachmentId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return gone;
|
|
205
|
+
},
|
|
206
|
+
deleteMany: async (_accountConfigId: string, ids: string[]) => {
|
|
207
|
+
for (const id of ids) rows.delete(id);
|
|
208
|
+
},
|
|
209
|
+
}) as unknown as IOutboxAttachmentRepository;
|
|
210
|
+
|
|
141
211
|
const installClient = (): void => {
|
|
142
212
|
const outboxMessage = createInMemoryOutboxRepository();
|
|
213
|
+
const storage = createMockStorageService();
|
|
214
|
+
attachmentRows = new Map<string, OutboxAttachmentItem>();
|
|
215
|
+
const outboxAttachmentService = new OutboxAttachmentService({
|
|
216
|
+
outboxMessageService: outboxMessage,
|
|
217
|
+
outboxAttachmentService: createAttachmentRepository(attachmentRows),
|
|
218
|
+
storage,
|
|
219
|
+
});
|
|
220
|
+
installedStorage = storage;
|
|
143
221
|
setClient({
|
|
144
222
|
outboxMessage,
|
|
223
|
+
storage,
|
|
145
224
|
account: accountRepository,
|
|
225
|
+
outboxAttachment: outboxAttachmentService,
|
|
146
226
|
outboxQueue: new OutboxQueueService({
|
|
147
227
|
outboxMessageService: outboxMessage,
|
|
228
|
+
outboxAttachmentService,
|
|
148
229
|
accountService: accountRepository,
|
|
149
230
|
sqsSmtpQueueUrl: "http://localhost:9324/queue/outbox-test",
|
|
150
231
|
sqsClient: acceptingSqsClient(),
|
|
@@ -292,3 +373,168 @@ describe("an outbox entry that has left draft (#604)", () => {
|
|
|
292
373
|
assert.equal(body.subject, "still editing");
|
|
293
374
|
});
|
|
294
375
|
});
|
|
376
|
+
|
|
377
|
+
describe("discarding a draft that carries files (#679)", () => {
|
|
378
|
+
it("takes the stored bytes with it, leaving nothing behind", async () => {
|
|
379
|
+
installClient();
|
|
380
|
+
const storage = installedStorage;
|
|
381
|
+
assert.ok(storage);
|
|
382
|
+
|
|
383
|
+
const draft = await createDraft(
|
|
384
|
+
requestContext({}),
|
|
385
|
+
authorizedEvent({
|
|
386
|
+
accountId: ACCOUNT_ID,
|
|
387
|
+
toAddresses: ["recipient@example.com"],
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
const outboxMessageId = String(draft.outboxMessageId);
|
|
391
|
+
|
|
392
|
+
const client = await getClient();
|
|
393
|
+
const minted = await client.outboxAttachment.mint({
|
|
394
|
+
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
395
|
+
outboxMessageId,
|
|
396
|
+
filename: "receipt.pdf",
|
|
397
|
+
contentType: "application/pdf",
|
|
398
|
+
sizeBytes: 16,
|
|
399
|
+
});
|
|
400
|
+
assert.equal(minted.outcome, "Minted");
|
|
401
|
+
// A mint reserves in the ledger; nothing is uploaded yet, so what a discard
|
|
402
|
+
// has to take with it is the reservation, not an object.
|
|
403
|
+
// A mint writes a row; nothing is uploaded yet, so what a discard has to
|
|
404
|
+
// take with it is the row, not an object.
|
|
405
|
+
assert.equal(attachmentRows.size, 1);
|
|
406
|
+
|
|
407
|
+
const response = await respond(() =>
|
|
408
|
+
deleteDraft(
|
|
409
|
+
requestContext({ params: { outboxMessageId } }),
|
|
410
|
+
authorizedEvent(),
|
|
411
|
+
),
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
assert.equal(response.statusCode, 204);
|
|
415
|
+
// Nothing else references these objects, so a row that goes without them
|
|
416
|
+
// leaves bytes no sweep collects.
|
|
417
|
+
assert.deepEqual(
|
|
418
|
+
await storage.listOutboxAttachments(
|
|
419
|
+
ACCOUNT_CONFIG_ID,
|
|
420
|
+
ACCOUNT_ID,
|
|
421
|
+
outboxMessageId,
|
|
422
|
+
),
|
|
423
|
+
[],
|
|
424
|
+
);
|
|
425
|
+
assert.equal(attachmentRows.size, 0);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe("attachmentIds on a draft update (#679)", () => {
|
|
430
|
+
const seed = async (): Promise<{
|
|
431
|
+
outboxMessageId: string;
|
|
432
|
+
ids: string[];
|
|
433
|
+
}> => {
|
|
434
|
+
const draft = await createDraft(
|
|
435
|
+
requestContext({}),
|
|
436
|
+
authorizedEvent({
|
|
437
|
+
accountId: ACCOUNT_ID,
|
|
438
|
+
toAddresses: ["recipient@example.com"],
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
const outboxMessageId = String(draft.outboxMessageId);
|
|
442
|
+
const client = await getClient();
|
|
443
|
+
const ids: string[] = [];
|
|
444
|
+
for (const filename of ["one.txt", "two.txt"]) {
|
|
445
|
+
const minted = await client.outboxAttachment.mint({
|
|
446
|
+
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
447
|
+
outboxMessageId,
|
|
448
|
+
filename,
|
|
449
|
+
contentType: "text/plain",
|
|
450
|
+
sizeBytes: 8,
|
|
451
|
+
});
|
|
452
|
+
assert.equal(minted.outcome, "Minted");
|
|
453
|
+
if (minted.outcome !== "Minted") throw new Error("unreachable");
|
|
454
|
+
ids.push(minted.reservation.outboxAttachmentId);
|
|
455
|
+
}
|
|
456
|
+
return { outboxMessageId, ids };
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const patch = (outboxMessageId: string, requestBody: unknown) =>
|
|
460
|
+
respond(() =>
|
|
461
|
+
updateDraft(
|
|
462
|
+
requestContext({ params: { outboxMessageId }, requestBody }),
|
|
463
|
+
authorizedEvent(),
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
it("absent leaves the files alone — a subject-only save keeps them", async () => {
|
|
468
|
+
installClient();
|
|
469
|
+
const { outboxMessageId } = await seed();
|
|
470
|
+
|
|
471
|
+
const response = await patch(outboxMessageId, { subject: "still typing" });
|
|
472
|
+
|
|
473
|
+
assert.equal(response.statusCode, 200);
|
|
474
|
+
const body = JSON.parse(response.body) as { attachments: unknown[] };
|
|
475
|
+
assert.equal(body.attachments.length, 2);
|
|
476
|
+
assert.equal(attachmentRows.size, 2);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("present and empty removes every file", async () => {
|
|
480
|
+
installClient();
|
|
481
|
+
const { outboxMessageId } = await seed();
|
|
482
|
+
|
|
483
|
+
const response = await patch(outboxMessageId, { attachmentIds: [] });
|
|
484
|
+
|
|
485
|
+
assert.equal(response.statusCode, 200);
|
|
486
|
+
assert.deepEqual(
|
|
487
|
+
(JSON.parse(response.body) as { attachments: unknown[] }).attachments,
|
|
488
|
+
[],
|
|
489
|
+
);
|
|
490
|
+
assert.equal(attachmentRows.size, 0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("present with ids keeps those and removes the rest, bytes included", async () => {
|
|
494
|
+
installClient();
|
|
495
|
+
const { outboxMessageId, ids } = await seed();
|
|
496
|
+
const storage = installedStorage;
|
|
497
|
+
assert.ok(storage);
|
|
498
|
+
await storage.storeOutboxAttachment({
|
|
499
|
+
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
500
|
+
accountId: ACCOUNT_ID,
|
|
501
|
+
outboxMessageId,
|
|
502
|
+
outboxAttachmentId: ids[1],
|
|
503
|
+
content: Buffer.from("dropped"),
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const response = await patch(outboxMessageId, {
|
|
507
|
+
attachmentIds: [ids[0]],
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
assert.equal(response.statusCode, 200);
|
|
511
|
+
const body = JSON.parse(response.body) as {
|
|
512
|
+
attachments: { outboxAttachmentId: string }[];
|
|
513
|
+
};
|
|
514
|
+
assert.deepEqual(
|
|
515
|
+
body.attachments.map((item) => item.outboxAttachmentId),
|
|
516
|
+
[ids[0]],
|
|
517
|
+
);
|
|
518
|
+
assert.equal(
|
|
519
|
+
await storage.statOutboxAttachment(
|
|
520
|
+
ACCOUNT_CONFIG_ID,
|
|
521
|
+
ACCOUNT_ID,
|
|
522
|
+
outboxMessageId,
|
|
523
|
+
ids[1],
|
|
524
|
+
),
|
|
525
|
+
null,
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("an unknown id is a no-op, not a reason to drop the known ones", async () => {
|
|
530
|
+
installClient();
|
|
531
|
+
const { outboxMessageId, ids } = await seed();
|
|
532
|
+
|
|
533
|
+
const response = await patch(outboxMessageId, {
|
|
534
|
+
attachmentIds: [...ids, "never-existed"],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
assert.equal(response.statusCode, 200);
|
|
538
|
+
assert.equal(attachmentRows.size, 2);
|
|
539
|
+
});
|
|
540
|
+
});
|
package/src/handlers/outbox.ts
CHANGED
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
OutboxMessageResponse,
|
|
4
4
|
UpdateOutboxMessageInput,
|
|
5
5
|
} from "@remit/api-openapi-types";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
OutboxAttachmentItem,
|
|
8
|
+
OutboxMessageItem,
|
|
9
|
+
} from "@remit/data-ports";
|
|
7
10
|
import { ForbiddenError } from "@remit/data-ports/errors";
|
|
8
11
|
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
9
12
|
import type { Context } from "openapi-backend";
|
|
@@ -11,13 +14,31 @@ import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
|
11
14
|
import { getClient } from "../service/data-client.js";
|
|
12
15
|
import type {
|
|
13
16
|
OperationHandler,
|
|
17
|
+
OutboxAttachmentOperationIds,
|
|
14
18
|
OutboxDetailOperationIds,
|
|
15
19
|
OutboxOperationIds,
|
|
16
20
|
} from "../types.js";
|
|
21
|
+
import {
|
|
22
|
+
completeOutboxAttachment,
|
|
23
|
+
mintOutboxAttachment,
|
|
24
|
+
} from "./outbox-attachment.js";
|
|
25
|
+
|
|
26
|
+
const toAttachmentResponse = (
|
|
27
|
+
item: OutboxAttachmentItem,
|
|
28
|
+
): OutboxMessageResponse["attachments"][number] => ({
|
|
29
|
+
outboxAttachmentId: item.outboxAttachmentId,
|
|
30
|
+
outboxMessageId: item.outboxMessageId,
|
|
31
|
+
filename: item.filename,
|
|
32
|
+
contentType: item.contentType,
|
|
33
|
+
sizeBytes: item.sizeBytes,
|
|
34
|
+
state: item.state,
|
|
35
|
+
});
|
|
17
36
|
|
|
18
37
|
const toOutboxMessageResponse = (
|
|
19
38
|
item: OutboxMessageItem,
|
|
39
|
+
attachments: OutboxAttachmentItem[] = [],
|
|
20
40
|
): OutboxMessageResponse => ({
|
|
41
|
+
attachments: attachments.map(toAttachmentResponse),
|
|
21
42
|
outboxMessageId: item.outboxMessageId,
|
|
22
43
|
accountId: item.accountId,
|
|
23
44
|
fromAddress: item.fromAddress,
|
|
@@ -118,7 +139,7 @@ export const OutboxOperations: Record<
|
|
|
118
139
|
});
|
|
119
140
|
|
|
120
141
|
return {
|
|
121
|
-
items: result.items.map(toOutboxMessageResponse),
|
|
142
|
+
items: result.items.map((item) => toOutboxMessageResponse(item)),
|
|
122
143
|
continuationToken: result.continuationToken,
|
|
123
144
|
};
|
|
124
145
|
},
|
|
@@ -145,7 +166,10 @@ export const OutboxDetailOperations: Record<
|
|
|
145
166
|
accountConfigId,
|
|
146
167
|
outboxMessageId,
|
|
147
168
|
);
|
|
148
|
-
return toOutboxMessageResponse(
|
|
169
|
+
return toOutboxMessageResponse(
|
|
170
|
+
outbox,
|
|
171
|
+
await client.outboxAttachment.listFor(accountConfigId, outboxMessageId),
|
|
172
|
+
);
|
|
149
173
|
},
|
|
150
174
|
|
|
151
175
|
OutboxDetailOperations_updateOutboxMessage: async (
|
|
@@ -176,7 +200,23 @@ export const OutboxDetailOperations: Record<
|
|
|
176
200
|
references: input.references,
|
|
177
201
|
},
|
|
178
202
|
);
|
|
179
|
-
|
|
203
|
+
|
|
204
|
+
// Absent means "leave the files alone" — a body that only carries a subject
|
|
205
|
+
// must not strip them. Present states exactly what the draft keeps, so an
|
|
206
|
+
// empty list is a real instruction to remove them all.
|
|
207
|
+
if (input.attachmentIds !== undefined) {
|
|
208
|
+
await client.outboxAttachment.retainOnly(
|
|
209
|
+
accountConfigId,
|
|
210
|
+
updated.accountId,
|
|
211
|
+
outboxMessageId,
|
|
212
|
+
input.attachmentIds,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return toOutboxMessageResponse(
|
|
217
|
+
updated,
|
|
218
|
+
await client.outboxAttachment.listFor(accountConfigId, outboxMessageId),
|
|
219
|
+
);
|
|
180
220
|
},
|
|
181
221
|
|
|
182
222
|
OutboxDetailOperations_deleteOutboxMessage: async (
|
|
@@ -215,4 +255,13 @@ export const OutboxDetailOperations: Record<
|
|
|
215
255
|
);
|
|
216
256
|
return toOutboxMessageResponse(sent);
|
|
217
257
|
},
|
|
258
|
+
|
|
259
|
+
OutboxDetailOperations_mintOutboxAttachment: mintOutboxAttachment,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const OutboxAttachmentOperations: Record<
|
|
263
|
+
OutboxAttachmentOperationIds,
|
|
264
|
+
OperationHandler<OutboxAttachmentOperationIds>
|
|
265
|
+
> = {
|
|
266
|
+
OutboxAttachmentOperations_completeOutboxAttachment: completeOutboxAttachment,
|
|
218
267
|
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
MessagePlacementMoveRepo,
|
|
23
23
|
messageDataSchema,
|
|
24
24
|
OrganizeJobRequestRepo,
|
|
25
|
+
OutboxAttachmentRepo,
|
|
25
26
|
OutboxMessageRepo,
|
|
26
27
|
QuarantineRepo,
|
|
27
28
|
} from "@remit/drizzle-service";
|
|
@@ -59,6 +60,7 @@ export const buildSqliteClient = async (): Promise<RemitClient> => {
|
|
|
59
60
|
message: new DrizzleMessageRepository(messageDataDb),
|
|
60
61
|
messageFlag: new DrizzleMessageFlagRepository(messageDataDb),
|
|
61
62
|
outboxMessage: new OutboxMessageRepo(genericDb),
|
|
63
|
+
outboxAttachment: new OutboxAttachmentRepo(genericDb),
|
|
62
64
|
threadMessage: new DrizzleThreadMessageRepository(genericDb),
|
|
63
65
|
envelope: new DrizzleEnvelopeRepository(messageDataDb),
|
|
64
66
|
accountExportRequest: new AccountExportRequestRepo(genericDb),
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
IMessagePlacementMoveRepository,
|
|
19
19
|
IMessageRepository,
|
|
20
20
|
IOrganizeJobRequestRepository,
|
|
21
|
+
IOutboxAttachmentRepository,
|
|
21
22
|
IOutboxMessageRepository,
|
|
22
23
|
IQuarantineRepository,
|
|
23
24
|
IThreadMessageRepository,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
type IImapConnection,
|
|
34
35
|
MailboxQueueService,
|
|
35
36
|
MessageMoveService,
|
|
37
|
+
OutboxAttachmentService,
|
|
36
38
|
OutboxQueueService,
|
|
37
39
|
PlacementMoveService,
|
|
38
40
|
SpamReportService,
|
|
@@ -144,6 +146,7 @@ export interface RemitClient {
|
|
|
144
146
|
flagQueue: FlagQueueService;
|
|
145
147
|
mailboxQueue: MailboxQueueService;
|
|
146
148
|
messageMove: MessageMoveService;
|
|
149
|
+
outboxAttachment: OutboxAttachmentService;
|
|
147
150
|
outboxQueue: OutboxQueueService;
|
|
148
151
|
|
|
149
152
|
// Report-spam / not-spam (block-and-move, unified from the old separate
|
|
@@ -166,6 +169,7 @@ export interface RemitClientRepositories {
|
|
|
166
169
|
message: IMessageRepository;
|
|
167
170
|
messageFlag: IMessageFlagRepository;
|
|
168
171
|
outboxMessage: IOutboxMessageRepository;
|
|
172
|
+
outboxAttachment: IOutboxAttachmentRepository;
|
|
169
173
|
threadMessage: IThreadMessageRepository;
|
|
170
174
|
envelope: IEnvelopeRepository;
|
|
171
175
|
accountExportRequest: IAccountExportRequestRepository;
|
|
@@ -356,6 +360,15 @@ export const createRemitClient = (deps: RemitClientDeps): RemitClient => {
|
|
|
356
360
|
logger,
|
|
357
361
|
});
|
|
358
362
|
|
|
363
|
+
// One instance, shared with the queue service below: the per-draft chain that
|
|
364
|
+
// makes the attachment cap safe under concurrency lives on it, and a discard
|
|
365
|
+
// racing an upload has to take the same lock the upload does.
|
|
366
|
+
const outboxAttachmentService = new OutboxAttachmentService({
|
|
367
|
+
outboxMessageService: repositories.outboxMessage,
|
|
368
|
+
outboxAttachmentService: repositories.outboxAttachment,
|
|
369
|
+
storage,
|
|
370
|
+
});
|
|
371
|
+
|
|
359
372
|
const bodySync = new BodySyncService(
|
|
360
373
|
repositories.message,
|
|
361
374
|
storage,
|
|
@@ -408,8 +421,10 @@ export const createRemitClient = (deps: RemitClientDeps): RemitClient => {
|
|
|
408
421
|
logger,
|
|
409
422
|
}),
|
|
410
423
|
messageMove: messageMoveService,
|
|
424
|
+
outboxAttachment: outboxAttachmentService,
|
|
411
425
|
outboxQueue: new OutboxQueueService({
|
|
412
426
|
outboxMessageService: repositories.outboxMessage,
|
|
427
|
+
outboxAttachmentService,
|
|
413
428
|
accountService: repositories.account,
|
|
414
429
|
sqsSmtpQueueUrl,
|
|
415
430
|
logger,
|
package/src/types.ts
CHANGED
|
@@ -63,6 +63,8 @@ export type OperationIds =
|
|
|
63
63
|
| "OutboxDetailOperations_updateOutboxMessage"
|
|
64
64
|
| "OutboxDetailOperations_deleteOutboxMessage"
|
|
65
65
|
| "OutboxDetailOperations_sendOutboxMessage"
|
|
66
|
+
| "OutboxDetailOperations_mintOutboxAttachment"
|
|
67
|
+
| "OutboxAttachmentOperations_completeOutboxAttachment"
|
|
66
68
|
| "AddressOperations_searchAddresses"
|
|
67
69
|
| "AddressDetailOperations_updateAddress";
|
|
68
70
|
|
|
@@ -159,6 +161,11 @@ export type OutboxDetailOperationIds = MatchPrefix<
|
|
|
159
161
|
OperationIds
|
|
160
162
|
>;
|
|
161
163
|
|
|
164
|
+
export type OutboxAttachmentOperationIds = MatchPrefix<
|
|
165
|
+
"OutboxAttachmentOperations_",
|
|
166
|
+
OperationIds
|
|
167
|
+
>;
|
|
168
|
+
|
|
162
169
|
export type AddressOperationIds = MatchPrefix<
|
|
163
170
|
"AddressOperations_",
|
|
164
171
|
OperationIds
|