Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default defineConfig([
eslintImportResolverTypescript.createTypeScriptImportResolver({
project: [
'lambdas/*/tsconfig.json',
'tests/test-team/tsconfig.json',
'tests/tsconfig.json',
'internal/*/tsconfig.json',
],
}),
Expand Down Expand Up @@ -221,6 +221,14 @@ export default defineConfig([
},
},

// No use before define relaxations
{
rules: {
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [ "error", {"functions": false}]
}
},

// misc rule overrides
{
rules: {
Expand Down
10 changes: 7 additions & 3 deletions lambdas/api-handler/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
{
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.925.0",
"@aws-sdk/client-s3": "^3.925.0",
"@aws-sdk/client-sqs": "^3.925.0",
"@aws-sdk/lib-dynamodb": "^3.925.0",
"@aws-sdk/s3-request-presigner": "^3.925.0",
"@internal/datastore": "*",
"@internal/helpers": "*",
"@types/aws-lambda": "^8.10.148",
"esbuild": "^0.25.11",
"pino": "^9.7.0"
"pino": "^9.7.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@aws-sdk/s3-request-presigner": "^3.901.0",
"@tsconfig/node22": "^22.0.2",
"@types/aws-lambda": "^8.10.148",
"@types/jest": "^29.5.14",
"jest": "^30.2.0",
"jest-mock-extended": "^3.0.7",
Expand Down
62 changes: 31 additions & 31 deletions lambdas/api-handler/src/config/__tests__/deps.test.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import type { Deps } from '../deps';

describe('createDependenciesContainer', () => {
import type { Deps } from "../deps";

describe("createDependenciesContainer", () => {
const env = {
LETTERS_TABLE_NAME: 'LettersTable',
LETTER_TTL_HOURS: 12960,
MI_TABLE_NAME: 'MITable',
LETTERS_TABLE_NAME: "LettersTable",
LETTER_TTL_HOURS: 12_960,
MI_TABLE_NAME: "MITable",
MI_TTL_HOURS: 2160,
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
DOWNLOAD_URL_TTL_SECONDS: 60
SUPPLIER_ID_HEADER: "nhsd-supplier-id",
APIM_CORRELATION_HEADER: "nhsd-correlation-id",
DOWNLOAD_URL_TTL_SECONDS: 60,
};

beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();

// pino
jest.mock('pino', () => ({
jest.mock("pino", () => ({
__esModule: true,
default: jest.fn(() => ({
info: jest.fn(),
Expand All @@ -27,36 +26,37 @@ describe('createDependenciesContainer', () => {
})),
}));

jest.mock('@aws-sdk/client-s3', () => ({
jest.mock("@aws-sdk/client-s3", () => ({
S3Client: jest.fn(),
}));

jest.mock('@aws-sdk/client-sqs', () => ({
jest.mock("@aws-sdk/client-sqs", () => ({
SQSClient: jest.fn(),
}));

// Repo client
jest.mock('@internal/datastore', () => ({
jest.mock("@internal/datastore", () => ({
LetterRepository: jest.fn(),
MIRepository: jest.fn(),
DBHealthcheck: jest.fn()
DBHealthcheck: jest.fn(),
}));

// Env
jest.mock('../env', () => ({envVars: env}));
jest.mock("../env", () => ({ envVars: env }));
});

test('constructs deps and wires repository config correctly', async () => {
test("constructs deps and wires repository config correctly", async () => {
// get current mock instances
const { S3Client } = jest.requireMock('@aws-sdk/client-s3') as { S3Client: jest.Mock };
const { SQSClient } = jest.requireMock('@aws-sdk/client-sqs') as { SQSClient: jest.Mock };
const pinoMock = jest.requireMock('pino') as { default: jest.Mock };
const { LetterRepository, MIRepository } = jest.requireMock('@internal/datastore') as {
LetterRepository: jest.Mock,
MIRepository: jest.Mock
};

const { createDependenciesContainer } = require('../deps');
const { S3Client } = jest.requireMock("@aws-sdk/client-s3");
const { SQSClient } = jest.requireMock("@aws-sdk/client-sqs");
const pinoMock = jest.requireMock("pino");
const { LetterRepository, MIRepository } = jest.requireMock(
"@internal/datastore",
);

// allow re-import of deps to leverage mocks
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { createDependenciesContainer } = require("../deps");
const deps: Deps = createDependenciesContainer();

expect(S3Client).toHaveBeenCalledTimes(1);
Expand All @@ -66,17 +66,17 @@ describe('createDependenciesContainer', () => {
expect(pinoMock.default).toHaveBeenCalledTimes(1);

expect(LetterRepository).toHaveBeenCalledTimes(1);
const letterRepoCtorArgs = (LetterRepository as jest.Mock).mock.calls[0];
const letterRepoCtorArgs = LetterRepository.mock.calls[0];
expect(letterRepoCtorArgs[2]).toEqual({
lettersTableName: 'LettersTable',
lettersTtlHours: 12960
lettersTableName: "LettersTable",
lettersTtlHours: 12_960,
});

expect(MIRepository).toHaveBeenCalledTimes(1);
const miRepoCtorArgs = (MIRepository as jest.Mock).mock.calls[0];
const miRepoCtorArgs = MIRepository.mock.calls[0];
expect(miRepoCtorArgs[2]).toEqual({
miTableName: 'MITable',
miTtlHours: 2160
miTableName: "MITable",
miTtlHours: 2160,
});

expect(deps.env).toEqual(env);
Expand Down
87 changes: 45 additions & 42 deletions lambdas/api-handler/src/config/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ZodError } from 'zod';
/* eslint-disable @typescript-eslint/no-require-imports */
/* Allow require imports to enable re-import of modules */

describe('lambdaEnv', () => {
import { ZodError } from "zod";

describe("lambdaEnv", () => {
const OLD_ENV = process.env;

beforeEach(() => {
Expand All @@ -12,64 +15,64 @@ describe('lambdaEnv', () => {
process.env = OLD_ENV; // Restore
});

it('should load all environment variables successfully', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
process.env.LETTERS_TABLE_NAME = 'letters-table';
process.env.MI_TABLE_NAME = 'mi-table';
process.env.LETTER_TTL_HOURS = '12960';
process.env.MI_TTL_HOURS = '2160';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
process.env.MAX_LIMIT = '2500';
process.env.QUEUE_URL = 'url';
it("should load all environment variables successfully", () => {
process.env.SUPPLIER_ID_HEADER = "nhsd-supplier-id";
process.env.APIM_CORRELATION_HEADER = "nhsd-correlation-id";
process.env.LETTERS_TABLE_NAME = "letters-table";
process.env.MI_TABLE_NAME = "mi-table";
process.env.LETTER_TTL_HOURS = "12960";
process.env.MI_TTL_HOURS = "2160";
process.env.DOWNLOAD_URL_TTL_SECONDS = "60";
process.env.MAX_LIMIT = "2500";
process.env.QUEUE_URL = "url";

const { envVars } = require('../env');
const { envVars } = require("../env");

expect(envVars).toEqual({
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'letters-table',
MI_TABLE_NAME: 'mi-table',
LETTER_TTL_HOURS: 12960,
SUPPLIER_ID_HEADER: "nhsd-supplier-id",
APIM_CORRELATION_HEADER: "nhsd-correlation-id",
LETTERS_TABLE_NAME: "letters-table",
MI_TABLE_NAME: "mi-table",
LETTER_TTL_HOURS: 12_960,
MI_TTL_HOURS: 2160,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: 2500,
QUEUE_URL: 'url'
QUEUE_URL: "url",
});
});

it('should throw if a required env var is missing', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
it("should throw if a required env var is missing", () => {
process.env.SUPPLIER_ID_HEADER = "nhsd-supplier-id";
process.env.APIM_CORRELATION_HEADER = "nhsd-correlation-id";
process.env.LETTERS_TABLE_NAME = undefined; // simulate missing var
process.env.MI_TABLE_NAME = 'mi-table';
process.env.LETTER_TTL_HOURS = '12960';
process.env.MI_TTL_HOURS = '2160';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
process.env.MI_TABLE_NAME = "mi-table";
process.env.LETTER_TTL_HOURS = "12960";
process.env.MI_TTL_HOURS = "2160";
process.env.DOWNLOAD_URL_TTL_SECONDS = "60";

expect(() => require('../env')).toThrow(ZodError);
expect(() => require("../env")).toThrow(ZodError);
});

it('should not throw if optional are not set', () => {
process.env.SUPPLIER_ID_HEADER = 'nhsd-supplier-id';
process.env.APIM_CORRELATION_HEADER = 'nhsd-correlation-id';
process.env.LETTERS_TABLE_NAME = 'letters-table';
process.env.MI_TABLE_NAME = 'mi-table';
process.env.LETTER_TTL_HOURS = '12960';
process.env.MI_TTL_HOURS = '2160';
process.env.DOWNLOAD_URL_TTL_SECONDS = '60';
it("should not throw if optional are not set", () => {
process.env.SUPPLIER_ID_HEADER = "nhsd-supplier-id";
process.env.APIM_CORRELATION_HEADER = "nhsd-correlation-id";
process.env.LETTERS_TABLE_NAME = "letters-table";
process.env.MI_TABLE_NAME = "mi-table";
process.env.LETTER_TTL_HOURS = "12960";
process.env.MI_TTL_HOURS = "2160";
process.env.DOWNLOAD_URL_TTL_SECONDS = "60";

const { envVars } = require('../env');
const { envVars } = require("../env");

expect(envVars).toEqual({
SUPPLIER_ID_HEADER: 'nhsd-supplier-id',
APIM_CORRELATION_HEADER: 'nhsd-correlation-id',
LETTERS_TABLE_NAME: 'letters-table',
MI_TABLE_NAME: 'mi-table',
LETTER_TTL_HOURS: 12960,
SUPPLIER_ID_HEADER: "nhsd-supplier-id",
APIM_CORRELATION_HEADER: "nhsd-correlation-id",
LETTERS_TABLE_NAME: "letters-table",
MI_TABLE_NAME: "mi-table",
LETTER_TTL_HOURS: 12_960,
MI_TTL_HOURS: 2160,
DOWNLOAD_URL_TTL_SECONDS: 60,
MAX_LIMIT: undefined
MAX_LIMIT: undefined,
});
});
});
46 changes: 26 additions & 20 deletions lambdas/api-handler/src/config/deps.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { S3Client } from "@aws-sdk/client-s3";
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { SQSClient } from "@aws-sdk/client-sqs";
import pino from 'pino';
import { LetterRepository, MIRepository, DBHealthcheck } from '@internal/datastore';
import { envVars, EnvVars } from "../config/env";
import pino from "pino";
import {
DBHealthcheck,
LetterRepository,
MIRepository,
} from "@internal/datastore";
import { EnvVars, envVars } from "./env";

export type Deps = {
s3Client: S3Client;
Expand All @@ -13,46 +17,48 @@ export type Deps = {
miRepo: MIRepository;
dbHealthcheck: DBHealthcheck;
logger: pino.Logger;
env: EnvVars
env: EnvVars;
};

function createDocumentClient(): DynamoDBDocumentClient {
const ddbClient = new DynamoDBClient({});
return DynamoDBDocumentClient.from(ddbClient);
}


function createLetterRepository(log: pino.Logger, envVars: EnvVars): LetterRepository {

function createLetterRepository(
log: pino.Logger,
environment: EnvVars,
): LetterRepository {
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
lettersTtlHours: envVars.LETTER_TTL_HOURS
lettersTableName: environment.LETTERS_TABLE_NAME,
lettersTtlHours: environment.LETTER_TTL_HOURS,
};

return new LetterRepository(createDocumentClient(), log, config);
}

function createDBHealthcheck(envVars: EnvVars): DBHealthcheck {
function createDBHealthcheck(environment: EnvVars): DBHealthcheck {
const config = {
lettersTableName: envVars.LETTERS_TABLE_NAME,
lettersTtlHours: envVars.LETTER_TTL_HOURS
lettersTableName: environment.LETTERS_TABLE_NAME,
lettersTtlHours: environment.LETTER_TTL_HOURS,
};

return new DBHealthcheck(createDocumentClient(), config);
}

function createMIRepository(log: pino.Logger, envVars: EnvVars): MIRepository {

function createMIRepository(
log: pino.Logger,
environment: EnvVars,
): MIRepository {
const config = {
miTableName: envVars.MI_TABLE_NAME,
miTtlHours: envVars.MI_TTL_HOURS
miTableName: environment.MI_TABLE_NAME,
miTtlHours: environment.MI_TTL_HOURS,
};

return new MIRepository(createDocumentClient(), log, config);
}

export function createDependenciesContainer(): Deps {

const log = pino();

return {
Expand All @@ -62,6 +68,6 @@ export function createDependenciesContainer(): Deps {
miRepo: createMIRepository(log, envVars),
dbHealthcheck: createDBHealthcheck(envVars),
logger: log,
env: envVars
env: envVars,
};
}
6 changes: 3 additions & 3 deletions lambdas/api-handler/src/config/env.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {z} from 'zod';
import { z } from "zod";

const EnvVarsSchema = z.object({
SUPPLIER_ID_HEADER: z.string(),
APIM_CORRELATION_HEADER: z.string(),
LETTERS_TABLE_NAME: z.string(),
MI_TABLE_NAME: z.string(),
LETTER_TTL_HOURS: z.coerce.number().int(),
MI_TTL_HOURS: z.coerce.number().int(),
MI_TTL_HOURS: z.coerce.number().int(),
DOWNLOAD_URL_TTL_SECONDS: z.coerce.number().int(),
MAX_LIMIT: z.coerce.number().int().optional(),
QUEUE_URL: z.coerce.string().optional()
QUEUE_URL: z.coerce.string().optional(),
});

export type EnvVars = z.infer<typeof EnvVarsSchema>;
Expand Down
Loading
Loading