Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB2-3295: EVL Feed Endpoint #14

Merged
merged 21 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
366fbc8
feat(cb2-3295): create new evl feed endpoint and sql view queries
May 26, 2022
b18bfa6
feat(cb2-3295): added IO service and fixed linting
May 27, 2022
d612ba6
feat(cb2-3295): fixed merge conflicts
May 27, 2022
9501932
Merge branch 'develop' into feature/CB2-3185
May 27, 2022
84f5be8
feat(cb2-3295): added data mapping and cvs file gen
May 27, 2022
01a7e10
feat(cb2-3295): added unit tests and Start of S3 upload
May 30, 2022
4441a5c
feat(cb2-3295): added param for S3 and created remove file function
May 31, 2022
d49b8e0
refactor(cb2-3295): added more logs and error messages
May 31, 2022
8c83672
refactor(cb2-3295): added more logs and error messages
May 31, 2022
b6b1ef5
refactor(cb2-3295): added remove to s3 upload callback
Jun 1, 2022
efbcb07
refactor(cb2-3295): added remove to s3 upload callback
Jun 1, 2022
b8a210e
refactor(cb2-3295): added remove to s3 upload callback
Jun 1, 2022
74a5ae9
refactor(cb2-3295): added read write to /tmp
Jun 6, 2022
d6014ac
feat(cb2-3295): add unit tests for evlFeedQueryFunctionFactory
Jun 7, 2022
2fff90d
refactor(cb2-3295): refactored s3 implementation and added s3 local
Jun 7, 2022
d814211
refactor(cb2-3295): remove code from index
Jun 7, 2022
8a2862f
refactor(cb2-3295): added response sent to callback on s3 upload
Jun 8, 2022
69f9427
feat(cb2-3295): bumped test coverage back up
Jun 17, 2022
57ef2db
feat(cb2-3295): change liquibase changelog to reflect nop changelog
Jun 22, 2022
442e90f
fix(cb2-3295): resolve vulnerabilities
Jun 22, 2022
3020c84
feat(cb2-3295): ammend commitlint config
Jun 23, 2022
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
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,6 @@ For further information about debugging, please refer to the following documenta

- [Debug process section](https://www.serverless.com/plugins/serverless-offline#usage-with-webpack)

## Testing

[json-serverless](https://github.com/pharindoko/json-serverless) has been added to the repository should we wish to mock external services during development and can be used in conjunction with the `test` environment.

### Unit

Jest is used for unit testing.
Expand Down
2 changes: 1 addition & 1 deletion commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = {
2,
'always',
(parsed) => {
const ticketNumberFormat = /^cvsb-\d+$/;
const ticketNumberFormat = /^cb2-\d+$/;
// type(scope?): subject
// we want to ticket number to appear in the commit msg as well as scope when we release
if (
Expand Down
62,724 changes: 12,545 additions & 50,179 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"lint:report": "npm run lint:analyse -- -f json -o reports/eslint/eslint-report.json",
"lint": "npm-run-all lint:*",
"prettier": "prettier --write ./**/*.{js,ts}",
"security-checks": "git secrets --scan && git log -p | scanrepo",
"security-checks": "git secrets --scan",
"clean": "rimraf ./.build ./.artifact ./.serverless ./.webpack ./*.zip",
"compile": "tsc",
"build:dev": "cross-env API_VERSION=${npm_package_version} NODE_ENV=local serverless webpack",
Expand All @@ -52,11 +52,14 @@
"aws-lambda": "1.0.6",
"aws-sdk": "2.857.0",
"express": "4.17.1",
"moment": "2.29.3",
"mysql2": "^2.2.5",
"serverless-http": "2.6.0",
"source-map-support": "0.5.19"
"source-map-support": "0.5.19",
"zlib": "1.0.5"
},
"devDependencies": {
"@aws-sdk/types": "3.78.0",
"@commitlint/cli": "11.0.0",
"@commitlint/config-conventional": "11.0.0",
"@dvsa/eslint-config-ts": "2.0.0",
Expand All @@ -66,6 +69,7 @@
"@semantic-release/npm": "9.0.1",
"@serverless/typescript": "2.17.0",
"@types/aws-lambda": "8.10.70",
"@types/express": "4.17.13",
"@types/jest": "26.0.20",
"@types/node": "14.14.20",
"@types/supertest": "2.0.10",
Expand All @@ -82,15 +86,13 @@
"fork-ts-checker-webpack-plugin": "6.0.8",
"husky": "4.3.8",
"jest": "26.6.3",
"json-serverless": "1.6.14",
"npm-run-all": "4.1.5",
"prettier": "2.2.1",
"rimraf": "3.0.2",
"semantic-release": "19.0.2",
"serverless": "3.17.0",
"serverless-dotenv-plugin": "3.1.0",
"serverless-offline": "8.7.0",
"serverless-s3-local": "0.6.22",
"serverless-webpack": "5.3.5",
"supertest": "6.0.1",
"ts-jest": "26.4.4",
Expand Down
2 changes: 1 addition & 1 deletion serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins:
provider:
profile: ${env:AWS_PROVIDER_PROFILE, 'default'}
name: aws
runtime: nodejs10.x
runtime: nodejs14.x
apiGateway:
# https://www.serverless.com/framework/docs/deprecations/#LOAD_VARIABLES_FROM_ENV_FILES
shouldStartNameWithService: true
Expand Down
51 changes: 51 additions & 0 deletions src/app/databaseService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FieldPacket, RowDataPacket } from 'mysql2';
import * as technicalQueries from './queries/technicalRecord';
import * as testResultsQueries from './queries/testResults';
import { EVL_QUERY, EVL_VRM_QUERY } from './queries/evlQuery';
import DatabaseServiceInterface from '../interfaces/DatabaseService';
import ResultsEvent from '../interfaces/ResultsEvent';
import VehicleEvent from '../interfaces/VehicleEvent';
Expand All @@ -13,7 +14,9 @@ import Plate from '../interfaces/queryResults/technical/plate';
import TestResult from '../interfaces/queryResults/test/testResult';
import CustomDefect from '../interfaces/queryResults/test/customDefect';
import TestDefect from '../interfaces/queryResults/test/testDefect';
import EvlFeedData from '../interfaces/queryResults/evlFeedData';
import NotFoundError from '../errors/NotFoundError';
import EvlEvent from '../interfaces/EvlEvent';

async function getTechnicalRecordDetails(
technicalRecordQueryResult: TechnicalRecordQueryResult,
Expand Down Expand Up @@ -196,13 +199,61 @@ async function getTestResultsByTestId(
return [result];
}

function getEvlFeedByVrmDetails(
queryResult: [RowDataPacket[], FieldPacket[]],
): EvlFeedData {
const evlFeedQueryResult = queryResult[0][0] as EvlFeedData;
if (
evlFeedQueryResult === undefined
) {
throw new NotFoundError('Test not found');
}

return evlFeedQueryResult;
}

function getEvlFeedDetails(
queryResult: [RowDataPacket[], FieldPacket[]],
): EvlFeedData[] {
const evlFeedQueryResults = queryResult[0] as EvlFeedData[];

if (evlFeedQueryResults === undefined || evlFeedQueryResults.length === 0) {
throw new NotFoundError('No tests found');
}

return evlFeedQueryResults.map((evlFeedQueryResult: EvlFeedData) => evlFeedQueryResult);
}

async function getEvlFeedByVrm(
databaseService: DatabaseServiceInterface,
event: EvlEvent,
): Promise<EvlFeedData[]> {
console.info('Using getEvlFeedByVrm');
const queryResult = await databaseService.get(EVL_VRM_QUERY, [event.vrm_trm]);
const result = getEvlFeedByVrmDetails(queryResult);

return [result];
}

async function getEvlFeed(
databaseService: DatabaseServiceInterface,
): Promise<EvlFeedData[]> {
console.info('Using getEvlFeed');
const queryResult = await databaseService.get(EVL_QUERY, []);
return getEvlFeedDetails(queryResult);
}

export {
getVehicleDetailsByVrm,
getVehicleDetailsByVin,
getVehicleDetailsByTrailerId,
getTestResultsByVrm,
getTestResultsByVin,
getTestResultsByTestId,
getEvlFeed,
getEvlFeedDetails,
getEvlFeedByVrm,
getEvlFeedByVrmDetails,
};

interface VehicleQueryResult extends RowDataPacket {
Expand Down
18 changes: 18 additions & 0 deletions src/app/evlFeedQueryFunctionFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import DatabaseService from '../interfaces/DatabaseService';
import EvlEvent from '../interfaces/EvlEvent';
import { getEvlFeed, getEvlFeedByVrm } from './databaseService';
import EvlFeedData from '../interfaces/queryResults/evlFeedData';

export default (
event: EvlEvent,
): ((databaseService: DatabaseService, event: EvlEvent) => Promise<EvlFeedData[]>
) => {
if (event.vrm_trm) {
console.info('Using getEvlFeedByVrm');

return getEvlFeedByVrm;
}

console.info('Using getEvlFeed');
return getEvlFeed;
};
5 changes: 5 additions & 0 deletions src/app/queries/evlQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const EVL_QUERY = 'SELECT `testExpiryDate`, `vrm_trm`, `certificateNumber` FROM `evl_view`';

const EVL_VRM_QUERY = `${EVL_QUERY} WHERE vrm_trm = ?;`;

export { EVL_QUERY, EVL_VRM_QUERY };
14 changes: 13 additions & 1 deletion src/domain/enquiryService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import VehicleEvent from '../interfaces/VehicleEvent';
import { validateVehicleEvent, validateResultsEvent } from '../utils/validationService';
import evlFeedQueryFunctionFactory from '../app/evlFeedQueryFunctionFactory';
import vehicleQueryFunctionFactory from '../app/vehicleQueryFunctionFactory';
import testResultsQueryFunctionFactory from '../app/testResultsQueryFunctionFactory';
import DatabaseService from '../interfaces/DatabaseService';
import ResultsEvent from '../interfaces/ResultsEvent';
import EvlEvent from '../interfaces/EvlEvent';
import VehicleDetails from '../interfaces/queryResults/technical/vehicleDetails';
import TestResult from '../interfaces/queryResults/test/testResult';
import EvlFeedData from '../interfaces/queryResults/evlFeedData';

const getVehicleDetails = async (
event: VehicleEvent,
Expand All @@ -28,4 +31,13 @@ const getResultsDetails = async (
return query(dbService, event);
};

export { getVehicleDetails, getResultsDetails };
const getEvlFeedDetails = async (
event: EvlEvent,
queryFuncFactory: typeof evlFeedQueryFunctionFactory,
dbService: DatabaseService,
): Promise<EvlFeedData[]> => {
const query = queryFuncFactory(event);
return query(dbService, event);
};

export { getVehicleDetails, getResultsDetails, getEvlFeedDetails };
49 changes: 46 additions & 3 deletions src/infrastructure/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import AWS from 'aws-sdk';
import express, { Request } from 'express';
import express, { Request, Router } from 'express';
import mysql from 'mysql2/promise';
import moment from 'moment';
import vehicleQueryFunctionFactory from '../../app/vehicleQueryFunctionFactory';
import testResultsQueryFunctionFactory from '../../app/testResultsQueryFunctionFactory';
import { getResultsDetails, getVehicleDetails } from '../../domain/enquiryService';
import { getResultsDetails, getVehicleDetails, getEvlFeedDetails } from '../../domain/enquiryService';
import ParametersError from '../../errors/ParametersError';
import ResultsEvent from '../../interfaces/ResultsEvent';
import VehicleEvent from '../../interfaces/VehicleEvent';
import EvlEvent from '../../interfaces/EvlEvent';
import DatabaseService from '../databaseService';
import SecretsManagerService from '../secretsManagerService';
import NotFoundError from '../../errors/NotFoundError';
import SecretsManagerServiceInterface from '../../interfaces/SecretsManagerService';
import LocalSecretsManagerService from '../localSecretsManagerService';
import evlFeedQueryFunctionFactory from '../../app/evlFeedQueryFunctionFactory';
import { uploadToS3 } from '../s3BucketService';

const app = express();
const router = express.Router();
const router: Router = express.Router();

const { API_VERSION } = process.env;

Expand Down Expand Up @@ -101,6 +105,45 @@ router.get(
},
);

router.get(
'/evl',
(
request: Request<Record<string, unknown>, string | Record<string, unknown>, Record<string, unknown>, EvlEvent>,
res,
) => {
let secretsManager: SecretsManagerServiceInterface;
if (process.env.IS_OFFLINE === 'true') {
secretsManager = new LocalSecretsManagerService();
} else {
secretsManager = new SecretsManagerService(new AWS.SecretsManager());
}
const fileName = `EVL_GVT_${moment(Date.now()).format('YYYYMMDD')}.csv`;
DatabaseService.build(secretsManager, mysql)
.then((dbService) => getEvlFeedDetails(request.query, evlFeedQueryFunctionFactory, dbService))
.then((result) => {
console.log('Generating EVL File Data');
const evlFeedProcessedData: string = result.map(
(entry) => `${entry.vrm_trm},${entry.certificateNumber},${moment(entry.testExpiryDate).format('DD-MMM-YYYY')}`,
).join('\n');

uploadToS3(evlFeedProcessedData, fileName, () => {
res.status(200);
res.contentType('json').send();
});
})
.catch((e: Error) => {
if (e instanceof ParametersError) {
res.status(400);
} else if (e instanceof NotFoundError) {
res.status(404);
} else {
res.status(500);
}
res.send(`Error Generating EVL Feed Data: ${e.message}`);
});
},
);

router.all(/testResults|vehicle/, (_request, res) => {
res.status(405).send();
});
Expand Down
26 changes: 26 additions & 0 deletions src/infrastructure/s3BucketService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import AWS from 'aws-sdk';

export function uploadToS3(evlFeedProcessedData: string, fileName: string, callback: () => void) {
const s3 = configureS3();
const params = { Bucket: process.env.AWS_S3_BUCKET_NAME, Key: fileName, Body: evlFeedProcessedData };

console.log(`uploading ${fileName} to S3`);
s3.upload(params, (err) => {
if (err) {
console.log(err);
}
callback();
});
}

function configureS3() {
if (process.env.IS_OFFLINE === 'true') {
return new AWS.S3({
s3ForcePathStyle: true,
accessKeyId: 'S3RVER', // This specific key is required when working offline
secretAccessKey: 'S3RVER',
endpoint: 'http://localhost:4569',
});
}
return new AWS.S3();
}
3 changes: 3 additions & 0 deletions src/interfaces/EvlEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default interface EvlEvent {
vrm_trm?: string | undefined;
}
5 changes: 5 additions & 0 deletions src/interfaces/queryResults/evlFeedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default interface EvlFeedData {
certificateNumber?: string | undefined;
testExpiryDate?: string | undefined;
vrm_trm?: string | undefined;
}
2 changes: 2 additions & 0 deletions src/resources/changelog-master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
<include file="../../cvs-nop/sql/00000_base_database.sql" relativeToChangelogFile="true"/>
<include file="../../cvs-nop/sql/00001_base_database_adr.sql" relativeToChangelogFile="true"/>
<include file="data-seed.sql" relativeToChangelogFile="true"/>
<include file="../../cvs-nop/sql/00002_views.sql" relativeToChangelogFile="true"/>

</databaseChangeLog>
28 changes: 28 additions & 0 deletions tests/unit/app/evlFeedQueryFunctionFactory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { mocked } from 'ts-jest/utils';
import queryFunctionFactory from '../../../src/app/evlFeedQueryFunctionFactory';
import * as dbFunctions from '../../../src/app/databaseService';
import DatabaseService from '../../../src/infrastructure/databaseService';

jest.mock('../../../src/app/databaseService');
jest.mock('../../../src/infrastructure/databaseService');

const dbFunctionsMock = mocked(dbFunctions);
const dbServiceMock = (mocked(DatabaseService, true) as unknown) as DatabaseService;

describe('Query Function Factory', () => {
it('returns the correct function when passed a vin', () => {
dbFunctionsMock.getEvlFeedByVrm = jest.fn().mockReturnValue('Success');

const func = queryFunctionFactory({ vrm_trm: '1234' });

expect(func(dbServiceMock, { vrm_trm: '1234' })).toEqual('Success');
});

it('return the correct function when passed no parameter', () => {
dbFunctionsMock.getEvlFeed = jest.fn().mockReturnValue('Success');

const func = queryFunctionFactory({});

expect(func(dbServiceMock, {})).toEqual('Success');
});
});
Loading