Skip to content

Commit 19f3540

Browse files
committed
CCM-11192: Add bulk write and test data push
1 parent 0d8a9f9 commit 19f3540

File tree

6 files changed

+234
-8
lines changed

6 files changed

+234
-8
lines changed

internal/datastore/src/__test__/letter-repository.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,44 @@ describe('LetterRepository', () => {
255255
const result = await repo.getLetterIdsBySupplier('supplier1');
256256
expect(result).toEqual([]);
257257
});
258+
259+
test('should batch write letters to the database', async () => {
260+
const letters = [
261+
createLetter('supplier1', 'letter1'),
262+
createLetter('supplier1', 'letter2'),
263+
createLetter('supplier1', 'letter3')
264+
];
265+
266+
await letterRepository.putLetterBatch(letters);
267+
268+
await checkLetterExists('supplier1', 'letter1');
269+
await checkLetterExists('supplier1', 'letter2');
270+
await checkLetterExists('supplier1', 'letter3');
271+
});
272+
273+
test('should batch in calls upto 25', async () => {
274+
const letters = []
275+
for(let i=0; i<60; i++) {
276+
letters.push(createLetter('supplier1', `letter${i}`));
277+
}
278+
279+
const sendSpy = jest.spyOn(db.docClient, 'send');
280+
281+
await letterRepository.putLetterBatch(letters);
282+
283+
expect(sendSpy).toHaveBeenCalledTimes(3);
284+
285+
await checkLetterExists('supplier1', 'letter1');
286+
await checkLetterExists('supplier1', 'letter6');
287+
await checkLetterExists('supplier1', 'letter59');
288+
});
289+
290+
test('rethrows errors from DynamoDB when batch creating letter', async () => {
291+
const misconfiguredRepository = new LetterRepository(db.docClient, logger, {
292+
...db.config,
293+
lettersTableName: 'nonexistent-table'
294+
});
295+
await expect(misconfiguredRepository.putLetterBatch([createLetter('supplier1', 'letter1')]))
296+
.rejects.toThrow('Cannot do operations on a non-existent table');
297+
});
258298
});

internal/datastore/src/letter-repository.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {
22
DynamoDBDocumentClient,
33
GetCommand,
44
PutCommand,
5+
BatchWriteCommand,
6+
BatchWriteCommandOutput,
57
QueryCommand,
68
UpdateCommand,
79
UpdateCommandOutput
@@ -25,8 +27,8 @@ export type LetterRepositoryConfig = {
2527

2628
export class LetterRepository {
2729
constructor(readonly ddbClient: DynamoDBDocumentClient,
28-
readonly log: Logger,
29-
readonly config: LetterRepositoryConfig) {
30+
readonly log: Logger,
31+
readonly config: LetterRepositoryConfig) {
3032
}
3133

3234
async putLetter(letter: Omit<Letter, 'ttl' | 'supplierStatus'>): Promise<Letter> {
@@ -50,6 +52,33 @@ export class LetterRepository {
5052
return LetterSchema.parse(letterDb);
5153
}
5254

55+
async putLetterBatch(letters: Omit<Letter, 'ttl' | 'supplierStatus'>[]): Promise<void> {
56+
let lettersDb: Letter[] = [];
57+
for (let i = 0; i < letters.length; i++) {
58+
lettersDb.push({
59+
...letters[i],
60+
supplierStatus: `${letters[i].supplierId}#${letters[i].status}`,
61+
ttl: Math.floor(Date.now() / 1000 + 60 * 60 * this.config.ttlHours)
62+
});
63+
64+
if (lettersDb.length === 25 || i === letters.length - 1) {
65+
const input = {
66+
RequestItems: {
67+
[this.config.lettersTableName]: lettersDb.map((item: any) => ({
68+
PutRequest: {
69+
Item: item
70+
}
71+
}))
72+
}
73+
};
74+
75+
await this.ddbClient.send(new BatchWriteCommand(input));
76+
77+
lettersDb = [];
78+
}
79+
}
80+
}
81+
5382
async getLetterById(supplierId: string, letterId: string): Promise<Letter> {
5483
const result = await this.ddbClient.send(new GetCommand({
5584
TableName: this.config.lettersTableName,

scripts/test-data/src/__test__/helpers/create_letter_helpers.test.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { LetterRepository } from "../../../../../internal/datastore/src/letter-repository";
22
import { LetterStatusType } from "../../../../../internal/datastore/src/types";
3-
import { createLetter } from "../../helpers/create_letter_helpers";
3+
import { createLetter, createLetterDto } from "../../helpers/create_letter_helpers";
44
import { uploadFile } from "../../helpers/s3_helpers";
5-
import { mockDeep } from "jest-mock-extended";
65

76
jest.mock("../../helpers/s3_helpers");
87

@@ -57,4 +56,31 @@ describe("Create letter helpers", () => {
5756
url: "s3://bucketName/supplierId/targetFilename",
5857
});
5958
});
59+
60+
it("should create a letter DTO with correct fields", () => {
61+
jest.useFakeTimers();
62+
jest.setSystemTime(new Date(2020, 1, 1));
63+
64+
const params = {
65+
letterId: "testLetterId",
66+
supplierId: "testSupplierId",
67+
specificationId: "testSpecId",
68+
groupId: "testGroupId",
69+
status: "PENDING" as LetterStatusType,
70+
url: "s3://bucket/testSupplierId/testLetter.pdf",
71+
};
72+
73+
const result = createLetterDto(params);
74+
75+
expect(result).toEqual({
76+
id: "testLetterId",
77+
supplierId: "testSupplierId",
78+
specificationId: "testSpecId",
79+
groupId: "testGroupId",
80+
url: "s3://bucket/testSupplierId/testLetter.pdf",
81+
status: "PENDING",
82+
createdAt: "2020-02-01T00:00:00.000Z",
83+
updatedAt: "2020-02-01T00:00:00.000Z",
84+
});
85+
});
6086
});

scripts/test-data/src/cli/index.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {
44
LetterStatusType,
55
} from "../../../../internal/datastore/src/types";
66
import { randomUUID } from "crypto";
7-
import { createLetter } from "../helpers/create_letter_helpers";
7+
import { createLetter, createLetterDto } from "../helpers/create_letter_helpers";
88
import { createLetterRepository } from "../infrastructure/letter-repo-factory";
9+
import { uploadFile } from "../helpers/s3_helpers";
910

1011
async function main() {
1112
await yargs(hideBin(process.argv))
@@ -37,6 +38,11 @@ async function main() {
3738
type: "string",
3839
demandOption: false,
3940
},
41+
"ttl-hours": {
42+
type: "number",
43+
demandOption: false,
44+
default: 336,
45+
},
4046
status: {
4147
type: "string",
4248
demandOption: true,
@@ -67,7 +73,8 @@ async function main() {
6773
: randomUUID();
6874
const status = argv.status;
6975
const environment = argv.environment;
70-
const letterRepository = createLetterRepository(environment);
76+
const ttlHours = argv.ttlHours;
77+
const letterRepository = createLetterRepository(environment, ttlHours);
7178

7279
createLetter({
7380
letterId,
@@ -81,6 +88,99 @@ async function main() {
8188
});
8289
},
8390
)
91+
.command(
92+
"create-letter-batch",
93+
"Create a batch of letters",
94+
{
95+
"supplier-id": {
96+
type: "string",
97+
demandOption: true,
98+
},
99+
environment: {
100+
type: "string",
101+
demandOption: true,
102+
},
103+
awsAccountId: {
104+
type: "string",
105+
demandOption: true,
106+
},
107+
"group-id": {
108+
type: "string",
109+
demandOption: false,
110+
},
111+
"specification-id": {
112+
type: "string",
113+
demandOption: false,
114+
},
115+
"ttl-hours": {
116+
type: "number",
117+
demandOption: false,
118+
default: 336,
119+
},
120+
"count": {
121+
type: "number",
122+
demandOption: true,
123+
},
124+
status: {
125+
type: "string",
126+
demandOption: true,
127+
choices: [
128+
"PENDING",
129+
"ACCEPTED",
130+
"REJECTED",
131+
"PRINTED",
132+
"ENCLOSED",
133+
"CANCELLED",
134+
"DISPATCHED",
135+
"FAILED",
136+
"RETURNED",
137+
"DESTROYED",
138+
"FORWARDED",
139+
"DELIVERED",
140+
],
141+
},
142+
},
143+
async (argv) => {
144+
145+
// set batch ID
146+
const batchId = randomUUID();
147+
148+
// parse args
149+
const supplierId = argv.supplierId;
150+
const groupId = argv.groupId ? argv.groupId : randomUUID();
151+
const specificationId = argv.specificationId
152+
? argv.specificationId
153+
: randomUUID();
154+
const status = argv.status;
155+
const environment = argv.environment;
156+
const ttlHours = argv.ttlHours;
157+
const letterRepository = createLetterRepository(environment, ttlHours);
158+
const count = argv.count;
159+
160+
161+
// Upload a test file for this batch
162+
const bucketName = `nhs-${argv.awsAccountId}-eu-west-2-${argv.environment}-supapi-test-letters`;
163+
const targetFilename = `${batchId}-${status}.pdf`;
164+
const url = `s3://${bucketName}/${batchId}/${targetFilename}`;
165+
await uploadFile(bucketName, batchId, "../../test_letter.pdf", targetFilename);
166+
167+
// Create letter DTOs
168+
let letterDtos = [];
169+
for (let i = 0; i < count; i++) {
170+
letterDtos.push(createLetterDto({
171+
letterId: randomUUID(),
172+
supplierId,
173+
groupId,
174+
specificationId,
175+
status: status as LetterStatusType,
176+
url,
177+
}));
178+
};
179+
180+
// Upload Letters
181+
await letterRepository.putLetterBatch(letterDtos);
182+
},
183+
)
84184
.demandCommand(1)
85185
.parse();
86186
}

scripts/test-data/src/helpers/create_letter_helpers.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,34 @@ export async function createLetter(params: {
4747
const letterRecord = await letterRepository.putLetter(letter);
4848
console.log(letterRecord);
4949
}
50+
51+
export function createLetterDto(params: {
52+
letterId: string;
53+
supplierId: string;
54+
specificationId: string;
55+
groupId: string;
56+
status: LetterStatusType;
57+
url: string;
58+
}) {
59+
const {
60+
letterId,
61+
supplierId,
62+
specificationId,
63+
groupId,
64+
status,
65+
url,
66+
} = params;
67+
68+
const letter: Omit<Letter, "ttl" | "supplierStatus"> = {
69+
id: letterId,
70+
supplierId,
71+
specificationId,
72+
groupId,
73+
url: url,
74+
status: status,
75+
createdAt: new Date().toISOString(),
76+
updatedAt: new Date().toISOString(),
77+
};
78+
79+
return letter;
80+
}

scripts/test-data/src/infrastructure/letter-repo-factory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
33
import pino from 'pino';
44
import { LetterRepository } from '../../../../internal/datastore';
55

6-
export function createLetterRepository(environment: string): LetterRepository {
6+
export function createLetterRepository(environment: string, ttlHours:number): LetterRepository {
77
const ddbClient = new DynamoDBClient({});
88
const docClient = DynamoDBDocumentClient.from(ddbClient);
99
const log = pino();
1010
const config = {
1111
lettersTableName: `nhs-${environment}-supapi-letters`,
12-
ttlHours: 24,
12+
ttlHours: ttlHours,
1313
};
1414

1515
return new LetterRepository(docClient, log, config);

0 commit comments

Comments
 (0)