Skip to content

Commit 2350e78

Browse files
committed
Address co-pilot review comments
1 parent de56784 commit 2350e78

4 files changed

Lines changed: 118 additions & 27 deletions

File tree

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,21 +174,27 @@ describe("LetterQueueRepository", () => {
174174
});
175175

176176
it("returns letters in timestamp order", async () => {
177+
jest.useFakeTimers().setSystemTime(new Date());
177178
await letterQueueRepository.putLetter(
178179
createLetter({ letterId: "first-letter" }),
179180
);
181+
jest.advanceTimersByTime(1);
180182
await letterQueueRepository.putLetter(
181183
createLetter({ letterId: "second-letter" }),
182184
);
185+
jest.advanceTimersByTime(1);
183186
await letterQueueRepository.putLetter(
184187
createLetter({ letterId: "third-letter" }),
185188
);
189+
jest.advanceTimersByTime(1);
186190
await letterQueueRepository.putLetter(
187191
createLetter({ letterId: "fourth-letter" }),
188192
);
193+
jest.advanceTimersByTime(1);
189194
await letterQueueRepository.putLetter(
190195
createLetter({ letterId: "fifth-letter" }),
191196
);
197+
jest.advanceTimersByTime(1);
192198

193199
const letters = await letterQueueRepository.getLetters("supplier1", 5);
194200

@@ -218,6 +224,80 @@ describe("LetterQueueRepository", () => {
218224
expect(letters).toHaveLength(3);
219225
expect(letters[2].letterId).toBe("third-letter");
220226
});
227+
228+
it("applies the limit after filtering on supplier", async () => {
229+
await letterQueueRepository.putLetter(
230+
createLetter({ letterId: "first-letter" }),
231+
);
232+
await letterQueueRepository.putLetter(
233+
createLetter({ letterId: "second-letter", supplierId: "supplier2" }),
234+
);
235+
await letterQueueRepository.putLetter(
236+
createLetter({ letterId: "third-letter" }),
237+
);
238+
await letterQueueRepository.putLetter(
239+
createLetter({ letterId: "fourth-letter" }),
240+
);
241+
242+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
243+
244+
expect(letters).toHaveLength(3);
245+
expect(letters[2].letterId).toBe("fourth-letter");
246+
});
247+
248+
it("applies the limit after filtering on visibilityTimestamp", async () => {
249+
await letterQueueRepository.putLetter(
250+
createLetter({ letterId: "first-letter" }),
251+
);
252+
await letterQueueRepository.putLetter(
253+
createLetter({ letterId: "second-letter" }),
254+
);
255+
await letterQueueRepository.putLetter(
256+
createLetter({ letterId: "third-letter" }),
257+
);
258+
await letterQueueRepository.putLetter(
259+
createLetter({ letterId: "fourth-letter" }),
260+
);
261+
await letterQueueRepository.updateVisibilityTimestamp(
262+
createLetter({ letterId: "second-letter" }),
263+
new Date(Date.now() + 600_000),
264+
);
265+
266+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
267+
268+
expect(letters).toHaveLength(3);
269+
expect(letters[2].letterId).toBe("fourth-letter");
270+
});
271+
272+
it("paginates through multiple DynamoDB pages to reach the limit", async () => {
273+
await letterQueueRepository.putLetter(
274+
createLetter({ letterId: "first-letter" }),
275+
);
276+
await letterQueueRepository.putLetter(
277+
createLetter({ letterId: "second-letter" }),
278+
);
279+
await letterQueueRepository.putLetter(
280+
createLetter({ letterId: "third-letter" }),
281+
);
282+
283+
const pagedRepository = new LetterQueueRepository(db.docClient, logger, {
284+
...db.config,
285+
queryPageSize: 1,
286+
});
287+
288+
const letters = await pagedRepository.getLetters("supplier1", 3);
289+
290+
expect(letters).toHaveLength(3);
291+
expect(letters[0].letterId).toBe("first-letter");
292+
expect(letters[1].letterId).toBe("second-letter");
293+
expect(letters[2].letterId).toBe("third-letter");
294+
});
295+
296+
it("returns an empty array if no items found", async () => {
297+
const letters = await letterQueueRepository.getLetters("supplier1", 3);
298+
299+
expect(letters).toHaveLength(0);
300+
});
221301
});
222302

223303
describe("updateVisibilityTimestamp", () => {

internal/datastore/src/letter-queue-repository.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { LetterDoesNotExistError } from "./letter-does-not-exist-error";
1919
type LetterQueueRepositoryConfig = {
2020
letterQueueTableName: string;
2121
letterQueueTtlHours: number;
22+
/** Maximum number of items to fetch per DynamoDB page. Defaults to 100. */
23+
queryPageSize?: number;
2224
};
2325

2426
export default class LetterQueueRepository {
@@ -95,21 +97,34 @@ export default class LetterQueueRepository {
9597
supplierId: string,
9698
limit: number,
9799
): Promise<PendingLetter[]> {
98-
const result = await this.ddbClient.send(
99-
new QueryCommand({
100-
TableName: this.config.letterQueueTableName,
101-
IndexName: "queueSortOrder-index",
102-
KeyConditionExpression: "supplierId = :supplierId",
103-
FilterExpression: "visibilityTimestamp < :now",
104-
ExpressionAttributeValues: {
105-
":supplierId": supplierId,
106-
":now": new Date().toISOString(),
107-
},
108-
Limit: limit,
109-
}),
110-
);
100+
const letters: PendingLetter[] = [];
101+
let lastEvaluatedKey: Record<string, unknown> | undefined;
102+
103+
do {
104+
const result = await this.ddbClient.send(
105+
new QueryCommand({
106+
TableName: this.config.letterQueueTableName,
107+
IndexName: "queueSortOrder-index",
108+
KeyConditionExpression: "supplierId = :supplierId",
109+
FilterExpression: "visibilityTimestamp < :now",
110+
ExpressionAttributeValues: {
111+
":supplierId": supplierId,
112+
":now": new Date().toISOString(),
113+
},
114+
// 1000 is a compromise - a smaller number might result in a lot of round trips, a larger one might
115+
// entail fetching and then throwing away a lot of data
116+
Limit: this.config.queryPageSize ?? 1000,
117+
ExclusiveStartKey: lastEvaluatedKey,
118+
}),
119+
);
120+
121+
const page = z.array(PendingLetterSchema).parse(result.Items);
122+
letters.push(...page);
123+
124+
lastEvaluatedKey = result.LastEvaluatedKey;
125+
} while (lastEvaluatedKey !== undefined && letters.length < limit);
111126

112-
return z.array(PendingLetterSchema).parse(result.Items);
127+
return letters.slice(0, limit);
113128
}
114129

115130
async updateVisibilityTimestamp(

internal/datastore/src/types.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,28 +78,28 @@ export type UpdateLetter = {
7878
reasonText?: string;
7979
};
8080

81-
export const PendingLetterSchema = z.object({
81+
export const PendingLetterSchemaBase = z.object({
8282
supplierId: idRef(SupplierSchema, "id"),
8383
letterId: idRef(LetterSchema, "id"),
84+
specificationId: z.string(),
85+
groupId: z.string(),
86+
});
87+
88+
export const PendingLetterSchema = PendingLetterSchemaBase.extend({
8489
queueTimestamp: z.string(),
8590
visibilityTimestamp: z.string(),
8691
queueSortOrderSk: z.string().describe("Secondary index SK"),
87-
specificationId: z.string(),
88-
groupId: z.string(),
8992
priority: z.int().min(0).max(99).optional(),
9093
ttl: z.int(),
9194
});
9295

93-
export const PendingLetterSchemaBase = z.object({
94-
supplierId: idRef(SupplierSchema, "id"),
95-
letterId: idRef(LetterSchema, "id"),
96-
specificationId: z.string(),
97-
groupId: z.string(),
96+
export const InsertPendingLetter = PendingLetterSchemaBase.extend({
97+
priority: z.int().min(0).max(99).optional(),
9898
});
9999

100100
export type PendingLetter = z.infer<typeof PendingLetterSchema>;
101101
export type PendingLetterBase = z.infer<typeof PendingLetterSchemaBase>;
102-
export type InsertPendingLetter = PendingLetterBase & { priority?: number };
102+
export type InsertPendingLetter = z.infer<typeof InsertPendingLetter>;
103103

104104
export const MISchemaBase = z.object({
105105
id: z.string(),

lambdas/api-handler/src/services/__tests__/letter-operations.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ afterEach(async () => {
4949
jest.useRealTimers();
5050
});
5151

52-
afterEach(async () => {
53-
jest.useRealTimers();
54-
});
55-
5652
describe("getPendingLetters", () => {
5753
beforeEach(() => {
5854
jest.clearAllMocks();

0 commit comments

Comments
 (0)