Skip to content

Commit

Permalink
feat: implement ses and submission endpoint
Browse files Browse the repository at this point in the history
Merge pull request #3 from UKForeignOffice/implement-ses
  • Loading branch information
jenbutongit authored Nov 2, 2023
2 parents be8b9df + 26a593d commit 4f8ef8a
Show file tree
Hide file tree
Showing 67 changed files with 6,781 additions and 345 deletions.
62 changes: 37 additions & 25 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
docker:
- image: cimg/node:18.15.0-browsers

environment:
NODE_ENV: "test"

steps:
- checkout

Expand Down Expand Up @@ -193,7 +196,7 @@ jobs:

- kubernetes/update-container-image:
namespace: << parameters.namespace >>
container-image-updates: notarial-forms=523508197323.dkr.ecr.eu-west-2.amazonaws.com/fco/notarial-api:$DOCKER_TAG
container-image-updates: notarial-api=523508197323.dkr.ecr.eu-west-2.amazonaws.com/fco/notarial-api:$DOCKER_TAG
resource-name: deployment/notarial-api

- run:
Expand All @@ -206,27 +209,36 @@ workflows:
version: 2
build-deploy:
jobs:
- test
# - publish:
# name: publish-api
# app: api
# filters:
# branches:
# only:
# - deploy-test
# context:
# - VPN
# - AWS
#
# - deploy:
# name: deploy-test
# namespace: fco-forms-test
# context:
# - VPN
# - AWS
# requires:
# - publish-api
# filters:
# branches:
# only:
# - deploy-test
- test:
filters:
tags:
only: /.*/
branches:
only: /.*/
- publish:
name: publish-api
app: api
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
only:
- deploy-test
context:
- VPN
- AWS

- deploy:
name: deploy-test
namespace: fco-forms-test
context:
- VPN
- AWS
requires:
- publish-api
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
only:
- deploy-test
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
.yarn/build-state.yml
.yarn/install-state.gz
*.jest.*
*.test.*
jest
README.md
/api/src/__mocks__/
docs
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
.yarn/cache
.yarn/install-state.gz
**/dist
**/.env
**/.env
/api/coverage/
3 changes: 2 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"trailingComma": "es5",
"singleQuote": false
"singleQuote": false,
"printWidth": 160
}
2 changes: 2 additions & 0 deletions api/.jest/setEnvVars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
process.env.CNI_TEMPLATE_ID = "5678";
process.env.NODE_ENV = "test";
13 changes: 13 additions & 0 deletions api/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "18",
},
},
],
"@babel/preset-typescript",
],
};
7 changes: 6 additions & 1 deletion api/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"port": "PORT",
"env": "NODE_ENV"
"env": "NODE_ENV",
"documentPassword": "DOCUMENT_PASSWORD",
"affirmationTemplate": "AFFIRMATION_TEMPLATE_ID",
"cniTemplate": "CNI_TEMPLATE_ID",
"submissionEmail": "SUBMISSION_EMAIL_ADDRESS",
"senderEmail": "SENDER_EMAIL_ADDRESS"
}
3 changes: 3 additions & 0 deletions api/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ if (process.env.NODE_ENV !== "test") {
module.exports = {
port: "9000",
env: "development",
documentPassword: "Sup3rS3cr3tP4ssw0rd",
submissionAddress: "[email protected]",
senderEmail: "[email protected]",
};
3 changes: 3 additions & 0 deletions api/config/test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"env": "test"
}
4 changes: 4 additions & 0 deletions api/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"testMatch": ["**/__tests__/**/*.test.ts"],
"setupFiles": ["./.jest/setEnvVars.ts"]
}
19 changes: 18 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,43 @@
"version": "1.0.0",
"main": "dist/index.js",
"dependencies": {
"@aws-sdk/client-s3": "^3.427.0",
"@aws-sdk/client-ses": "^3.427.0",
"axios": "^1.5.1",
"bcrypt": "^5.1.1",
"config": "^3.3.9",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.0",
"handlebars": "^4.7.8",
"mimetext": "^3.0.16",
"nanoid": "3.3.4",
"pino": "^8.15.4",
"pino-http": "^8.5.0",
"typescript": "^5.2.2"
},
"devDependencies": {
"@babel/cli": "^7.22.10",
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@types/bcrypt": "^5.0.0",
"@types/config": "^3.3.1",
"@types/express": "^4.17.18",
"babel-jest": "^29.5.0",
"eslint": "^8.52.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.7.0",
"jest-mock-extended": "^3.0.5",
"nodemon": "^3.0.1",
"prettier": "^3.0.3",
"typescript": "^5.2.2"
},
"scripts": {
"build": "yarn tsc",
"start": "node dist/index.js",
"start:local": "yarn tsc -w & nodemon -q -w dist dist/index.js"
"start:local": "yarn tsc -w & nodemon -q -w dist dist/index.js",
"test": "jest --coverage"
}
}
41 changes: 41 additions & 0 deletions api/src/ApplicationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ErrorCode, ERRORS, ErrorTypes } from "./errors";

type ApplicationErrorOptions = {
isOperational: boolean;
exposeToClient: boolean;
};

const defaultOptions: ApplicationErrorOptions = {
isOperational: true,
exposeToClient: true,
};

export class ApplicationError extends Error {
/**
* HTTP status code to send the response as. You may use {@link `Axios#HttpStatusCode`}, or the raw value
*/
httpStatusCode: number;

/**
* Notarial-API error code to help identify the error and resolve it
*/
code: ErrorCode;

isOperational: boolean = true;

/**
* determines whether the error message will be included in the response to the client
*/
exposeToClient: boolean = true;

constructor(name: ErrorTypes, code: ErrorCode, httpStatusCode: number, message?: string, options?: Partial<ApplicationErrorOptions>) {
super(message);
this.name = name;
this.httpStatusCode = httpStatusCode;
this.code = code;
this.message = message ?? ERRORS[name][code];
const { isOperational, exposeToClient } = { ...defaultOptions, ...options };
this.isOperational = isOperational;
this.exposeToClient = exposeToClient;
}
}
5 changes: 5 additions & 0 deletions api/src/SESClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SESClient } from "@aws-sdk/client-ses";

export const ses = new SESClient({
region: "eu-west-2",
});
12 changes: 12 additions & 0 deletions api/src/__mocks__/SESClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ses as sesClient } from "../SESClient";
import { MockProxy } from "jest-mock-extended";
import { SESClient } from "@aws-sdk/client-ses";

jest.mock("../SESClient", () => ({
__esModule: true,
ses: {
send: jest.fn(),
},
}));

export const ses = sesClient as unknown as MockProxy<SESClient>;
89 changes: 89 additions & 0 deletions api/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* These errors are for use in {@link ApplicationError}.
* {@link ERRORS} can be imported and used anywhere.
*/

/**
* To add a new category of error (new {@link ErrorTypes})
* 1. Add a type to {@link ErrorTypes}
* 2. Create a new type, named <YourNewType>ErrorCode
* 3. Add error codes as a union, in snake case and capitalised, e.g. "SOME_ERROR_CODE"
* 4. Add <YourNewType>ErrorCode to the {@link ErrorCodes union}
* 5. Create a new const, named <YOUR_NEW_TYPE>, with the type {@link ErrorRecord}, e.g. `ErrorRecord<YourNewTypeErrorCode>`
* 6. Add a new property and value to `ErrorRecords`, the key will be the ErrorType, and the value will be the const created in (5)
* 7. Do the same for the const {@link ERRORS}
*/

/**
* Category of the error - this is likely to match the service it came from
*/
export type ErrorTypes = "WEBHOOK" | "FILE" | "SES" | "GENERIC";

/**
* Error code for the matching ErrorType.
*/
type WebhookErrorCode = "EMPTY_PAYLOAD" | "EMPTY_TEMPLATE_DATA";
type FileErrorCode = "EMPTY_RES" | "API_ERROR" | "NOT_FOUND";
type SESErrorCode =
| "NO_TEMPLATE"
| "TEMPLATE_NOT_FOUND"
| "TEMPLATE_PART_MISSING"
| "TEMPLATE_VAR_MISSING"
| "EMPTY_RES"
| "BAD_REQUEST"
| "API_ERROR"
| "UNKNOWN";

type GenericErrorCode = "UNKNOWN" | "RATE_LIMIT_EXCEEDED";

/**
* Union of all the different ErrorCode.
*/
export type ErrorCode = WebhookErrorCode | FileErrorCode | SESErrorCode | GenericErrorCode;

/**
* {@ErrorRecord} uses `Record`, which means every key passed into the generic, must be implemented
* for example, if there is a new ErrorCode for WebhookErrorCode, then the const WEBHOOK needs to implement
* the new error code as a property.
*/
type ErrorRecord<T extends ErrorCode> = Record<T, string>;

const WEBHOOK: ErrorRecord<WebhookErrorCode> = {
EMPTY_PAYLOAD: "Malformed form data: No questions property found",
EMPTY_TEMPLATE_DATA: "No template data was returned",
};

const FILE: ErrorRecord<FileErrorCode> = {
EMPTY_RES: "The file server did not return a response",
API_ERROR: "There was an error returning this file",
NOT_FOUND: "The requested file could not be found",
};

const SES: ErrorRecord<SESErrorCode> = {
NO_TEMPLATE: "no template id was set for the specified form",
TEMPLATE_NOT_FOUND: "no template with the specified id could be found",
TEMPLATE_PART_MISSING: "the template subject line or body were missing",
TEMPLATE_VAR_MISSING: "a required variable was missing from the template data",
EMPTY_RES: "The email service did not return a response",
BAD_REQUEST: "The email data being sent was malformed",
API_ERROR: "The email service returned an error",
UNKNOWN: "There was an unknown error sending the email",
};

const GENERIC: ErrorRecord<GenericErrorCode> = {
UNKNOWN: "Unknown error",
RATE_LIMIT_EXCEEDED: "Rate limit exceeded",
};

type ErrorRecords = {
WEBHOOK: typeof WEBHOOK;
FILE: typeof FILE;
SES: typeof SES;
GENERIC: typeof GENERIC;
};
export const ERRORS: ErrorRecords = {
WEBHOOK,
FILE,
SES,
GENERIC,
};
7 changes: 7 additions & 0 deletions api/src/handlers/forms/helpers/flattenQuestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FormQuestion } from "../../../types/FormQuestion";

export function flattenQuestions(questions: FormQuestion[]) {
return questions.flatMap(({ fields }) => {
return fields;
});
}
Empty file.
1 change: 1 addition & 0 deletions api/src/handlers/forms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { post } from "./post";
15 changes: 15 additions & 0 deletions api/src/handlers/forms/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextFunction, Request, Response } from "express";

export async function post(req: Request, res: Response, next: NextFunction) {
const { submitService } = res.app.services;
try {
const { reference } = await submitService.submitForm(req.body);
res.status(200).send({
message: "Email sent successfully",
reference,
});
} catch (e) {
next(e);
return;
}
}
21 changes: 21 additions & 0 deletions api/src/handlers/helpers/buildEmailData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getAllInputsFromForm } from "./getAllInputsFromForm";
import { FormDataBody } from "../../types";
import { getTemplateDataFromInputs } from "./getTemplateDataFromInputs";

export type Errors = {
errors: Error;
};

export function buildEmailData(
formBody: FormDataBody,
formType: "cni" | "affirmation"
) {
const fields = getAllInputsFromForm(formBody);
if (!fields) {
return {
errors: new Error("Malformed form data: No questions property found"),
} as Errors;
}
const templateData = getTemplateDataFromInputs(fields, formType);
return templateData;
}
Loading

0 comments on commit 4f8ef8a

Please sign in to comment.