Skip to content

Commit 95fbcc3

Browse files
committed
CCM-12223: dynamodb error handling
1 parent d1869d5 commit 95fbcc3

File tree

4 files changed

+126
-49
lines changed

4 files changed

+126
-49
lines changed

lambdas/backend-api/src/__tests__/templates/infra/routing-config-repository/query.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@ describe('RoutingConfigRepo#query', () => {
225225

226226
expect(result.data).toEqual([]);
227227
});
228+
229+
test('handles exceptions from dynamodb', async () => {
230+
const { repo, mocks } = setup();
231+
232+
const e = new Error('oh no');
233+
234+
mocks.dynamo.on(QueryCommand).rejectsOnce(e);
235+
236+
const result = await repo.query(owner).list();
237+
238+
expect(result.error).toMatchObject({
239+
actualError: e,
240+
errorMeta: expect.objectContaining({ code: 500 }),
241+
});
242+
expect(result.data).toBeUndefined();
243+
});
228244
});
229245

230246
describe('count', () => {
@@ -280,5 +296,21 @@ describe('RoutingConfigRepo#query', () => {
280296

281297
expect(result.data).toEqual({ count: 0 });
282298
});
299+
300+
test('handles exceptions from dynamodb', async () => {
301+
const { repo, mocks } = setup();
302+
303+
const e = new Error('oh no');
304+
305+
mocks.dynamo.on(QueryCommand).rejectsOnce(e);
306+
307+
const result = await repo.query(owner).count();
308+
309+
expect(result.error).toMatchObject({
310+
actualError: e,
311+
errorMeta: expect.objectContaining({ code: 500 }),
312+
});
313+
expect(result.data).toBeUndefined();
314+
});
283315
});
284316
});

lambdas/backend-api/src/__tests__/templates/infra/routing-config-repository/repository.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';
22
import 'aws-sdk-client-mock-jest';
33
import { mockClient } from 'aws-sdk-client-mock';
4+
import { ZodError } from 'zod';
45
import { RoutingConfigRepository } from '@backend-api/templates/infra/routing-config-repository';
56
import { routingConfig } from '../../fixtures/routing-config';
67

@@ -83,7 +84,12 @@ describe('RoutingConfigRepository', () => {
8384
'nhs-notify-client-id'
8485
);
8586

86-
expect(result.error).not.toBeUndefined();
87+
expect(result.error).toMatchObject({
88+
actualError: expect.any(ZodError),
89+
errorMeta: expect.objectContaining({
90+
code: 500,
91+
}),
92+
});
8793
expect(result.data).toBeUndefined();
8894

8995
expect(mocks.dynamo).toHaveReceivedCommandWith(GetCommand, {
@@ -94,5 +100,27 @@ describe('RoutingConfigRepository', () => {
94100
},
95101
});
96102
});
103+
104+
test('returns errors if the database call fails', async () => {
105+
const { repo, mocks } = setup();
106+
107+
const e = new Error('Oh No');
108+
109+
mocks.dynamo.on(GetCommand).rejectsOnce(e);
110+
111+
const result = await repo.get(
112+
'b9b6d56b-421e-462f-9ce5-3012e3fdb27f',
113+
'nhs-notify-client-id'
114+
);
115+
116+
expect(result.error).toMatchObject({
117+
actualError: e,
118+
errorMeta: expect.objectContaining({
119+
code: 500,
120+
}),
121+
});
122+
123+
expect(result.data).toBeUndefined();
124+
});
97125
});
98126
});

lambdas/backend-api/src/templates/infra/routing-config-repository/query.ts

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import {
44
type NativeAttributeValue,
55
type QueryCommandInput,
66
} from '@aws-sdk/lib-dynamodb';
7-
import { ApplicationResult, success } from '@backend-api/utils/result';
7+
import { ApplicationResult, failure, success } from '@backend-api/utils/result';
88
import {
99
$RoutingConfig,
10+
ErrorCase,
1011
type RoutingConfig,
1112
type RoutingConfigStatus,
1213
} from 'nhs-notify-backend-client';
@@ -37,49 +38,65 @@ export class RoutingConfigQuery {
3738

3839
/** Execute the query and return a list of all matching RoutingConfigs */
3940
async list(): Promise<ApplicationResult<RoutingConfig[]>> {
40-
this.returnCount = false;
41-
42-
const query = this.build();
43-
44-
const collected: RoutingConfig[] = [];
45-
46-
const paginator = paginateQuery({ client: this.docClient }, query);
47-
48-
for await (const page of paginator) {
49-
for (const item of page.Items ?? []) {
50-
const parsed = $RoutingConfig.safeParse(item);
51-
if (parsed.success) {
52-
collected.push(parsed.data);
53-
} else {
54-
logger.warn('Filtered out invalid RoutingConfig item', {
55-
owner: this.owner,
56-
id: item.id,
57-
issues: parsed.error.issues,
58-
});
41+
try {
42+
this.returnCount = false;
43+
44+
const query = this.build();
45+
46+
const collected: RoutingConfig[] = [];
47+
48+
const paginator = paginateQuery({ client: this.docClient }, query);
49+
50+
for await (const page of paginator) {
51+
for (const item of page.Items ?? []) {
52+
const parsed = $RoutingConfig.safeParse(item);
53+
if (parsed.success) {
54+
collected.push(parsed.data);
55+
} else {
56+
logger.warn('Filtered out invalid RoutingConfig item', {
57+
owner: this.owner,
58+
id: item.id,
59+
issues: parsed.error.issues,
60+
});
61+
}
5962
}
6063
}
61-
}
6264

63-
return success(collected);
65+
return success(collected);
66+
} catch (error) {
67+
return failure(
68+
ErrorCase.INTERNAL,
69+
'Error listing Routing Configs',
70+
error
71+
);
72+
}
6473
}
6574

6675
/** Execute the query and return a count of all matching RoutingConfigs */
6776
async count(): Promise<ApplicationResult<{ count: number }>> {
68-
this.returnCount = true;
77+
try {
78+
this.returnCount = true;
6979

70-
const query = this.build();
80+
const query = this.build();
7181

72-
let count = 0;
82+
let count = 0;
7383

74-
const paginator = paginateQuery({ client: this.docClient }, query);
84+
const paginator = paginateQuery({ client: this.docClient }, query);
7585

76-
for await (const page of paginator) {
77-
if (page.Count) {
78-
count += page.Count;
86+
for await (const page of paginator) {
87+
if (page.Count) {
88+
count += page.Count;
89+
}
7990
}
80-
}
8191

82-
return success({ count });
92+
return success({ count });
93+
} catch (error) {
94+
return failure(
95+
ErrorCase.INTERNAL,
96+
'Error counting Routing Configs',
97+
error
98+
);
99+
}
83100
}
84101

85102
private build() {

lambdas/backend-api/src/templates/infra/routing-config-repository/repository.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,31 @@ export class RoutingConfigRepository {
1717
id: string,
1818
owner: string
1919
): Promise<ApplicationResult<RoutingConfig>> {
20-
const result = await this.client.send(
21-
new GetCommand({
22-
TableName: this.tableName,
23-
Key: {
24-
id,
25-
owner,
26-
},
27-
})
28-
);
20+
try {
21+
const result = await this.client.send(
22+
new GetCommand({
23+
TableName: this.tableName,
24+
Key: {
25+
id,
26+
owner,
27+
},
28+
})
29+
);
2930

30-
if (!result.Item) {
31-
return failure(ErrorCase.NOT_FOUND, 'Routing Config not found');
32-
}
31+
if (!result.Item) {
32+
return failure(ErrorCase.NOT_FOUND, 'Routing Config not found');
33+
}
3334

34-
const parsed = $RoutingConfig.safeParse(result.Item);
35+
const parsed = $RoutingConfig.parse(result.Item);
3536

36-
if (!parsed.success) {
37+
return success(parsed);
38+
} catch (error) {
3739
return failure(
3840
ErrorCase.INTERNAL,
3941
'Error retrieving Routing Config',
40-
parsed.error
42+
error
4143
);
4244
}
43-
44-
return success(parsed.data);
4545
}
4646

4747
query(owner: string): RoutingConfigQuery {

0 commit comments

Comments
 (0)