Skip to content

Commit 287c292

Browse files
committed
CCM-11192: re-commit changes
1 parent e45e07d commit 287c292

File tree

14 files changed

+597
-1
lines changed

14 files changed

+597
-1
lines changed

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,59 @@ describe('LetterRepository', () => {
274274
const letters = await repo.getLettersBySupplier('supplier1', 'PENDING', 10);
275275
expect(letters).toEqual([]);
276276
});
277+
278+
test('should batch write letters to the database', async () => {
279+
const letters = [
280+
createLetter('supplier1', 'letter1'),
281+
createLetter('supplier1', 'letter2'),
282+
createLetter('supplier1', 'letter3')
283+
];
284+
285+
await letterRepository.putLetterBatch(letters);
286+
287+
await checkLetterExists('supplier1', 'letter1');
288+
await checkLetterExists('supplier1', 'letter2');
289+
await checkLetterExists('supplier1', 'letter3');
290+
});
291+
292+
test('should batch in calls upto 25', async () => {
293+
const letters = []
294+
for(let i=0; i<60; i++) {
295+
letters.push(createLetter('supplier1', `letter${i}`));
296+
}
297+
298+
const sendSpy = jest.spyOn(db.docClient, 'send');
299+
300+
await letterRepository.putLetterBatch(letters);
301+
302+
expect(sendSpy).toHaveBeenCalledTimes(3);
303+
304+
await checkLetterExists('supplier1', 'letter1');
305+
await checkLetterExists('supplier1', 'letter6');
306+
await checkLetterExists('supplier1', 'letter59');
307+
});
308+
309+
test('should skip array gaps', async () => {
310+
const letters = [
311+
createLetter('supplier1', 'letter1'),
312+
createLetter('supplier1', 'letter2'),
313+
createLetter('supplier1', 'letter3')
314+
];
315+
316+
delete letters[1];
317+
318+
await letterRepository.putLetterBatch(letters);
319+
320+
await checkLetterExists('supplier1', 'letter1');
321+
await checkLetterExists('supplier1', 'letter3');
322+
});
323+
324+
test('rethrows errors from DynamoDB when batch creating letter', async () => {
325+
const misconfiguredRepository = new LetterRepository(db.docClient, logger, {
326+
...db.config,
327+
lettersTableName: 'nonexistent-table'
328+
});
329+
await expect(misconfiguredRepository.putLetterBatch([createLetter('supplier1', 'letter1')]))
330+
.rejects.toThrow('Cannot do operations on a non-existent table');
331+
});
277332
});

internal/datastore/src/letter-repository.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
BatchWriteCommand,
23
DynamoDBDocumentClient,
34
GetCommand,
45
PutCommand,
@@ -52,6 +53,41 @@ export class LetterRepository {
5253
return LetterSchema.parse(letterDb);
5354
}
5455

56+
async putLetterBatch(letters: Omit<Letter, 'ttl' | 'supplierStatus'| 'supplierStatusSk'>[]): Promise<void> {
57+
let lettersDb: Letter[] = [];
58+
for (let i = 0; i < letters.length; i++) {
59+
60+
const letter = letters[i];
61+
62+
if(!letter){
63+
continue;
64+
}
65+
66+
lettersDb.push({
67+
...letter,
68+
supplierStatus: `${letter.supplierId}#${letter.status}`,
69+
supplierStatusSk: Date.now().toString(),
70+
ttl: Math.floor(Date.now() / 1000 + 60 * 60 * this.config.ttlHours)
71+
});
72+
73+
if (lettersDb.length === 25 || i === letters.length - 1) {
74+
const input = {
75+
RequestItems: {
76+
[this.config.lettersTableName]: lettersDb.map((item: any) => ({
77+
PutRequest: {
78+
Item: item
79+
}
80+
}))
81+
}
82+
};
83+
84+
await this.ddbClient.send(new BatchWriteCommand(input));
85+
86+
lettersDb = [];
87+
}
88+
}
89+
}
90+
5591
async getLetterById(supplierId: string, letterId: string): Promise<Letter> {
5692
const result = await this.ddbClient.send(new GetCommand({
5793
TableName: this.config.lettersTableName,
@@ -149,4 +185,5 @@ export class LetterRepository {
149185
}));
150186
return z.array(LetterSchemaBase).parse(result.Items ?? []);
151187
}
188+
152189
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"workspaces": [
7272
"lambdas/api-handler",
7373
"lambdas/authorizer",
74-
"internal/datastore"
74+
"internal/datastore",
75+
"scripts/test-data"
7576
]
7677
}

scripts/test-data/.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist

scripts/test-data/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
coverage
2+
node_modules
3+
dist
4+
.reports

scripts/test-data/README.md

Whitespace-only changes.

scripts/test-data/jest.config.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Config } from 'jest';
2+
3+
export const baseJestConfig: Config = {
4+
preset: 'ts-jest',
5+
6+
// Automatically clear mock calls, instances, contexts and results before every test
7+
clearMocks: true,
8+
9+
// Indicates whether the coverage information should be collected while executing the test
10+
collectCoverage: true,
11+
12+
// The directory where Jest should output its coverage files
13+
coverageDirectory: './.reports/unit/coverage',
14+
15+
// Indicates which provider should be used to instrument code for coverage
16+
coverageProvider: 'babel',
17+
18+
coverageThreshold: {
19+
global: {
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: -10,
24+
},
25+
},
26+
27+
coveragePathIgnorePatterns: ['/__tests__/'],
28+
transform: { '^.+\\.ts$': 'ts-jest' },
29+
testPathIgnorePatterns: ['.build'],
30+
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
31+
32+
// Use this configuration option to add custom reporters to Jest
33+
reporters: [
34+
'default',
35+
[
36+
'jest-html-reporter',
37+
{
38+
pageTitle: 'Test Report',
39+
outputPath: './.reports/unit/test-report.html',
40+
includeFailureMsg: true,
41+
},
42+
],
43+
],
44+
45+
// The test environment that will be used for testing
46+
testEnvironment: 'jsdom',
47+
};
48+
49+
const utilsJestConfig = {
50+
...baseJestConfig,
51+
52+
testEnvironment: 'node',
53+
54+
coveragePathIgnorePatterns: [
55+
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
56+
'cli/index.ts',
57+
'helpers/s3_helpers.ts',
58+
'letter-repo-factory.ts',
59+
],
60+
};
61+
62+
export default utilsJestConfig;

scripts/test-data/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"dependencies": {
3+
"@aws-sdk/client-s3": "^3.858.0",
4+
"esbuild": "^0.24.0",
5+
"pino": "^9.7.0",
6+
"yargs": "^17.7.2"
7+
},
8+
"devDependencies": {
9+
"@tsconfig/node22": "^22.0.2",
10+
"@types/jest": "^29.5.14",
11+
"jest": "^29.7.0",
12+
"jest-mock-extended": "^3.0.7",
13+
"typescript": "^5.8.2"
14+
},
15+
"license": "MIT",
16+
"main": "src/cli/index.ts",
17+
"name": "nhs-notify-supplier-api-data-generator",
18+
"private": true,
19+
"scripts": {
20+
"cli": "tsx ./src/cli/index.ts",
21+
"lint": "eslint .",
22+
"lint:fix": "eslint . --fix",
23+
"test:unit": "jest",
24+
"typecheck": "tsc --noEmit"
25+
},
26+
"version": "0.0.1"
27+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { LetterRepository } from "../../../../../internal/datastore/src/letter-repository";
2+
import { LetterStatusType } from "../../../../../internal/datastore/src/types";
3+
import { createLetter, createLetterDto } from "../../helpers/create_letter_helpers";
4+
import { uploadFile } from "../../helpers/s3_helpers";
5+
6+
jest.mock("../../helpers/s3_helpers");
7+
8+
describe("Create letter helpers", () => {
9+
beforeEach(() => {
10+
jest.resetAllMocks();
11+
});
12+
13+
it("create letter", async () => {
14+
jest.useFakeTimers();
15+
jest.setSystemTime(new Date(2020, 1, 1));
16+
17+
const mockPutLetter = jest.fn();
18+
const mockedLetterRepository = {
19+
putLetter: mockPutLetter,
20+
} as any as LetterRepository;
21+
const mockedUploadFile = uploadFile as jest.Mock;
22+
23+
const supplierId = "supplierId";
24+
const letterId = "letterId";
25+
const bucketName = "bucketName";
26+
const targetFilename = "targetFilename";
27+
const groupId = "groupId";
28+
const specificationId = "specificationId";
29+
const status = "PENDING" as LetterStatusType;
30+
31+
await createLetter({
32+
letterId,
33+
bucketName,
34+
supplierId,
35+
targetFilename,
36+
groupId,
37+
specificationId,
38+
status,
39+
letterRepository: mockedLetterRepository,
40+
});
41+
42+
expect(mockedUploadFile).toHaveBeenCalledWith(
43+
"bucketName",
44+
"supplierId",
45+
"../../test_letter.pdf",
46+
"targetFilename",
47+
);
48+
expect(mockPutLetter).toHaveBeenCalledWith({
49+
createdAt: "2020-02-01T00:00:00.000Z",
50+
groupId: "groupId",
51+
id: "letterId",
52+
specificationId: "specificationId",
53+
status: "PENDING",
54+
supplierId: "supplierId",
55+
updatedAt: "2020-02-01T00:00:00.000Z",
56+
url: "s3://bucketName/supplierId/targetFilename",
57+
});
58+
});
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+
});
86+
});

0 commit comments

Comments
 (0)