Skip to content

Commit a83ff2e

Browse files
CCM-13369: Add pagination logic into getLettersBySupplier (#352)
1 parent 06ed7e4 commit a83ff2e

2 files changed

Lines changed: 101 additions & 17 deletions

File tree

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ describe("LetterRepository", () => {
494494
await checkLetterStatus("supplier1", "letter59", "PENDING");
495495
});
496496

497+
// eslint-disable-next-line jest/expect-expect
497498
test("should skip array gaps", async () => {
498499
const letters = [];
499500
letters[0] = createLetter("supplier1", "letter1");
@@ -516,4 +517,71 @@ describe("LetterRepository", () => {
516517
]),
517518
).rejects.toThrow("Cannot do operations on a non-existent table");
518519
});
520+
521+
test("should paginate through multiple pages when fetching letters by supplier", async () => {
522+
const mockSend = jest
523+
.fn()
524+
// first call returns 30 items with a LastEvaluatedKey
525+
.mockResolvedValueOnce({
526+
Items: Array.from({ length: 30 }, (_, i) => ({
527+
id: `letter${(i + 1).toString().padStart(3, "0")}`,
528+
status: "PENDING",
529+
specificationId: "specification1",
530+
groupId: "group1",
531+
})),
532+
LastEvaluatedKey: { id: "letter030", supplierId: "supplier1" },
533+
})
534+
// second call returns remaining 20 items without LastEvaluatedKey
535+
.mockResolvedValueOnce({
536+
Items: Array.from({ length: 20 }, (_, i) => ({
537+
id: `letter${(i + 31).toString().padStart(3, "0")}`,
538+
status: "PENDING",
539+
specificationId: "specification1",
540+
groupId: "group1",
541+
})),
542+
LastEvaluatedKey: undefined,
543+
});
544+
545+
const mockDdbClient = { send: mockSend } as any;
546+
const repo = new LetterRepository(mockDdbClient, logger, db.config);
547+
548+
// request 50 letters - should require 2 DynamoDB queries due to mocked pagination
549+
const letters = await repo.getLettersBySupplier("supplier1", "PENDING", 50);
550+
551+
// verify all 50 letters were returned
552+
expect(letters).toHaveLength(50);
553+
554+
// verify two send calls were made (2 pages)
555+
expect(mockSend).toHaveBeenCalledTimes(2);
556+
557+
// verify the second call included the ExclusiveStartKey from first response
558+
const secondCallInput = mockSend.mock.calls[1][0].input;
559+
expect(secondCallInput.ExclusiveStartKey).toEqual({
560+
id: "letter030",
561+
supplierId: "supplier1",
562+
});
563+
});
564+
565+
test("should respect limit when fewer items available than requested", async () => {
566+
// create only 10 letters
567+
for (let i = 1; i <= 10; i++) {
568+
await letterRepository.putLetter(
569+
createLetter(
570+
"supplier1",
571+
`letter${i.toString().padStart(2, "0")}`,
572+
"PENDING",
573+
),
574+
);
575+
}
576+
577+
// request 50 letters but only 10 exist
578+
const letters = await letterRepository.getLettersBySupplier(
579+
"supplier1",
580+
"PENDING",
581+
50,
582+
);
583+
584+
expect(letters).toHaveLength(10);
585+
expect(letters.every((l) => l.status === "PENDING")).toBe(true);
586+
});
519587
});

internal/datastore/src/letter-repository.ts

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -230,23 +230,39 @@ export class LetterRepository {
230230
status: string,
231231
limit: number,
232232
): Promise<LetterBase[]> {
233+
const items: Record<string, any>[] = [];
234+
let ExclusiveStartKey: Record<string, any> | undefined;
233235
const supplierStatus = `${supplierId}#${status}`;
234-
const result = await this.ddbClient.send(
235-
new QueryCommand({
236-
TableName: this.config.lettersTableName,
237-
IndexName: "supplierStatus-index",
238-
KeyConditionExpression: "supplierStatus = :supplierStatus",
239-
Limit: limit,
240-
ExpressionAttributeNames: {
241-
"#status": "status", // reserved keyword
242-
},
243-
ExpressionAttributeValues: {
244-
":supplierStatus": supplierStatus,
245-
},
246-
ProjectionExpression:
247-
"id, #status, specificationId, groupId, reasonCode, reasonText",
248-
}),
249-
);
250-
return z.array(LetterSchemaBase).parse(result.Items ?? []);
236+
let res;
237+
238+
do {
239+
const remaining = limit - items.length;
240+
241+
res = await this.ddbClient.send(
242+
new QueryCommand({
243+
TableName: this.config.lettersTableName,
244+
IndexName: "supplierStatus-index",
245+
KeyConditionExpression: "supplierStatus = :supplierStatus",
246+
ExpressionAttributeNames: {
247+
"#status": "status", // reserved keyword
248+
},
249+
ExpressionAttributeValues: {
250+
":supplierStatus": supplierStatus,
251+
},
252+
ProjectionExpression:
253+
"id, #status, specificationId, groupId, reasonCode, reasonText",
254+
Limit: remaining, // limit is a per-page cap
255+
ExclusiveStartKey,
256+
}),
257+
);
258+
259+
if (res.Items?.length) {
260+
items.push(...res.Items);
261+
}
262+
263+
ExclusiveStartKey = res.LastEvaluatedKey;
264+
} while (res.LastEvaluatedKey && items.length < limit);
265+
266+
return z.array(LetterSchemaBase).parse(items);
251267
}
252268
}

0 commit comments

Comments
 (0)