Skip to content

Commit aac68f4

Browse files
committed
CCM-12614: add basic lambda function
1 parent 39b41f7 commit aac68f4

File tree

15 files changed

+409
-0
lines changed

15 files changed

+409
-0
lines changed

infrastructure/terraform/components/dl/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ No requirements.
4040
| <a name="module_lambda_apim_key_generation"></a> [lambda\_apim\_key\_generation](#module\_lambda\_apim\_key\_generation) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
4141
| <a name="module_lambda_lambda_apim_refresh_token"></a> [lambda\_lambda\_apim\_refresh\_token](#module\_lambda\_lambda\_apim\_refresh\_token) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
4242
| <a name="module_mesh_poll"></a> [mesh\_poll](#module\_mesh\_poll) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
43+
| <a name="module_poll_pdm"></a> [poll\_pdm](#module\_poll\_pdm) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip | n/a |
4344
| <a name="module_s3bucket_cf_logs"></a> [s3bucket\_cf\_logs](#module\_s3bucket\_cf\_logs) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
4445
| <a name="module_s3bucket_letters"></a> [s3bucket\_letters](#module\_s3bucket\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
4546
| <a name="module_s3bucket_static_assets"></a> [s3bucket\_static\_assets](#module\_s3bucket\_static\_assets) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-s3bucket.zip | n/a |
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
module "poll_pdm" {
2+
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-lambda.zip"
3+
4+
function_name = "poll-pdm"
5+
description = "A function for polling PDM document status"
6+
7+
aws_account_id = var.aws_account_id
8+
component = local.component
9+
environment = var.environment
10+
project = var.project
11+
region = var.region
12+
group = var.group
13+
14+
log_retention_in_days = var.log_retention_in_days
15+
kms_key_arn = module.kms.key_arn
16+
17+
iam_policy_document = {
18+
body = data.aws_iam_policy_document.poll_pdm_lambda.json
19+
}
20+
21+
function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22+
function_code_base_path = local.aws_lambda_functions_dir_path
23+
function_code_dir = "poll-pdm-lambda/dist"
24+
function_include_common = true
25+
handler_function_name = "handler"
26+
runtime = "nodejs22.x"
27+
memory = 128
28+
timeout = 360
29+
log_level = var.log_level
30+
31+
force_lambda_code_deploy = var.force_lambda_code_deploy
32+
enable_lambda_insights = false
33+
34+
send_to_firehose = true
35+
log_destination_arn = local.log_destination_arn
36+
log_subscription_role_arn = local.acct.log_subscription_role_arn
37+
38+
lambda_env_vars = {
39+
"APIM_BASE_URL" = var.apim_base_url
40+
"APIM_ACCESS_TOKEN_SSM_PARAMETER_NAME" = local.apim_access_token_ssm_parameter_name
41+
"EVENT_PUBLISHER_EVENT_BUS_ARN" = aws_cloudwatch_event_bus.main.arn
42+
"EVENT_PUBLISHER_DLQ_URL" = module.sqs_event_publisher_errors.sqs_queue_url
43+
}
44+
}
45+
46+
data "aws_iam_policy_document" "poll_pdm_lambda" {
47+
statement {
48+
sid = "PutEvents"
49+
effect = "Allow"
50+
51+
actions = [
52+
"events:PutEvents",
53+
]
54+
55+
resources = [
56+
aws_cloudwatch_event_bus.main.arn,
57+
]
58+
}
59+
60+
statement {
61+
sid = "SQSPermissionsDLQs"
62+
effect = "Allow"
63+
64+
actions = [
65+
"sqs:SendMessage",
66+
"sqs:SendMessageBatch",
67+
]
68+
69+
resources = [
70+
module.sqs_event_publisher_errors.sqs_queue_arn,
71+
]
72+
}
73+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { baseJestConfig } from '../../jest.config.base';
2+
3+
const config = baseJestConfig;
4+
5+
export default config;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"dependencies": {
3+
"aws-lambda": "^1.0.7",
4+
"lodash": "^4.17.21",
5+
"p-limit": "^3.1.0",
6+
"utils": "^0.0.1"
7+
},
8+
"devDependencies": {
9+
"@tsconfig/node22": "^22.0.2",
10+
"@types/aws-lambda": "^8.10.155",
11+
"@types/jest": "^29.5.14",
12+
"@types/lodash": "^4.17.20",
13+
"aws-sdk-client-mock": "^4.1.0",
14+
"aws-sdk-client-mock-jest": "^4.1.0",
15+
"jest": "^29.7.0",
16+
"jest-mock-extended": "^3.0.7",
17+
"typescript": "^5.9.3"
18+
},
19+
"name": "nhs-notify-digital-letters-poll-pdm-lambda",
20+
"private": true,
21+
"scripts": {
22+
"lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts",
23+
"lint": "eslint .",
24+
"lint:fix": "eslint . --fix",
25+
"test:unit": "jest",
26+
"typecheck": "tsc --noEmit"
27+
},
28+
"version": "0.0.1"
29+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createHandler } from 'apis/sqs-handler';
2+
import { Logger } from 'utils';
3+
import { SQSEvent, SQSRecord } from 'aws-lambda';
4+
import { mock } from 'jest-mock-extended';
5+
6+
const logger = mock<Logger>();
7+
8+
const event = {
9+
sourceEventId: 'test-event-id',
10+
};
11+
12+
const sqsRecord1: SQSRecord = {
13+
messageId: '1',
14+
receiptHandle: 'abc',
15+
body: JSON.stringify(event),
16+
attributes: {
17+
ApproximateReceiveCount: '1',
18+
SentTimestamp: '2025-07-03T14:23:30Z',
19+
SenderId: 'sender-id',
20+
ApproximateFirstReceiveTimestamp: '2025-07-03T14:23:30Z',
21+
},
22+
messageAttributes: {},
23+
md5OfBody: '',
24+
eventSource: 'aws:sqs',
25+
eventSourceARN: '',
26+
awsRegion: '',
27+
};
28+
29+
const singleRecordEvent: SQSEvent = {
30+
Records: [sqsRecord1],
31+
};
32+
33+
const handler = createHandler({
34+
logger,
35+
});
36+
37+
describe('SQS Handler', () => {
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
});
41+
42+
it('processes a single record', async () => {
43+
const response = await handler(singleRecordEvent);
44+
45+
expect(logger.info).toHaveBeenCalledWith(
46+
'Received SQS Event of 1 record(s)',
47+
);
48+
expect(logger.info).toHaveBeenCalledWith(
49+
'1 of 1 records processed successfully',
50+
);
51+
expect(response).toEqual({ batchItemFailures: [] });
52+
});
53+
54+
it('should return failed items to the queue if an error occurs while processing them', async () => {
55+
singleRecordEvent.Records[0].body = 'not-json';
56+
57+
const result = await handler(singleRecordEvent);
58+
59+
expect(logger.warn).toHaveBeenCalledWith({
60+
error: `Unexpected token 'o', "not-json" is not valid JSON`,
61+
description: 'Failed processing message',
62+
messageId: '1',
63+
});
64+
65+
expect(logger.info).toHaveBeenCalledWith(
66+
'0 of 1 records processed successfully',
67+
);
68+
69+
expect(result).toEqual({
70+
batchItemFailures: [{ itemIdentifier: '1' }],
71+
});
72+
});
73+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { createContainer } from 'container';
2+
3+
jest.mock('infra/config', () => ({
4+
loadConfig: jest.fn(() => ({
5+
apimBaseUrl: 'https://test-apim-url',
6+
apimAccessTokenSsmParameterName: 'test-ssm-parameter-name',
7+
eventPublisherDlqUrl: 'test-url',
8+
eventPublisherEventBusArn: 'test-arn',
9+
})),
10+
}));
11+
12+
jest.mock('utils', () => ({
13+
EventPublisher: jest.fn(() => ({})),
14+
eventBridgeClient: {},
15+
logger: {},
16+
sqsClient: {},
17+
ParameterStoreCache: jest.fn(() => ({})),
18+
createGetApimAccessToken: jest.fn(() => ({})),
19+
}));
20+
21+
describe('container', () => {
22+
it('should create container', () => {
23+
const container = createContainer();
24+
expect(container).toBeDefined();
25+
});
26+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { handler } from 'index';
2+
3+
jest.mock('apis/sqs-handler', () => ({
4+
createHandler: jest.fn(() => jest.fn()),
5+
}));
6+
7+
jest.mock('container', () => ({
8+
createContainer: jest.fn(() => ({})),
9+
}));
10+
11+
describe('index', () => {
12+
it('should export handler', () => {
13+
expect(handler).toBeDefined();
14+
});
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { loadConfig } from 'infra/config';
2+
3+
jest.mock('utils', () => ({
4+
defaultConfigReader: {
5+
getValue: jest.fn(),
6+
getInt: jest.fn(),
7+
},
8+
}));
9+
10+
describe('config', () => {
11+
it('should load config', () => {
12+
const config = loadConfig();
13+
expect(config).toBeDefined();
14+
});
15+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {
2+
SQSBatchItemFailure,
3+
SQSBatchResponse,
4+
SQSEvent,
5+
SQSRecord,
6+
} from 'aws-lambda';
7+
import { Logger } from 'utils';
8+
9+
export interface HandlerDependencies {
10+
logger: Logger;
11+
}
12+
13+
export const createHandler = ({ logger }: HandlerDependencies) =>
14+
async function handler(sqsEvent: SQSEvent): Promise<SQSBatchResponse> {
15+
const receivedItemCount = sqsEvent.Records.length;
16+
17+
logger.info(`Received SQS Event of ${receivedItemCount} record(s)`);
18+
19+
const batchItemFailures: SQSBatchItemFailure[] = [];
20+
21+
await Promise.all(
22+
sqsEvent.Records.map(async (sqsRecord: SQSRecord) => {
23+
try {
24+
logger.info({
25+
event: JSON.parse(sqsRecord.body),
26+
});
27+
} catch (error: any) {
28+
logger.warn({
29+
error: error.message,
30+
description: 'Failed processing message',
31+
messageId: sqsRecord.messageId,
32+
});
33+
batchItemFailures.push({ itemIdentifier: sqsRecord.messageId });
34+
}
35+
}),
36+
);
37+
38+
const processedItemCount = receivedItemCount - batchItemFailures.length;
39+
logger.info(
40+
`${processedItemCount} of ${receivedItemCount} records processed successfully`,
41+
);
42+
43+
return { batchItemFailures };
44+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { logger } from 'utils';
2+
import { HandlerDependencies } from 'apis/sqs-handler';
3+
4+
export const createContainer = (): HandlerDependencies => {
5+
return { logger };
6+
};
7+
8+
export default createContainer;

0 commit comments

Comments
 (0)