Skip to content

Commit

Permalink
Implemented CreatePatient endpoint (#33)
Browse files Browse the repository at this point in the history
* Basic scaffolding for a new function app, including dev container. Will add testing and code architecture stuff with first function implementation.

* Added eslint

* Implemented crate patient on patient controller.

* Implemented create patient endpoint.

* Changed name of patient shardkey

* Updated readme and changed success response to created response.

* Added RetryCollection

* Added LoggingCollection to log mongo dependency calls to app insights. Still needs tests for the logging collection.

* Added tests for LoggingCollection

* close the connection in after function; otherwise mocha hangs and won't exit

* use eslint-plugin and parser 2.31.0 & fix lint issue

typescript-eslint/typescript-eslint#2009
typescript-eslint/typescript-eslint#2010

Co-authored-by: Hannes Nel <hannesne@microsoft.com>
Co-authored-by: Wenjun Zhou <176547141@qq.com>
  • Loading branch information
3 people committed May 12, 2020
1 parent 4210175 commit 96c38f7
Show file tree
Hide file tree
Showing 45 changed files with 1,628 additions and 30 deletions.
40 changes: 33 additions & 7 deletions src/PatientTestsApi/.eslintrc.json
@@ -1,25 +1,51 @@
{
"env": {
"browser": false,
"commonjs": true,
"es6": true
"node": true,
"es2017": true,
"mocha": true
},
"extends": [
"standard"
],
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018
"ecmaVersion": 2019,
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint"
],
"ignorePatterns": ["/Services/app-insights"],
"rules": {
"semi": "off",
"quotes": "off"
}
"@typescript-eslint/semi": ["error"],
"quotes": "off",
"@typescript-eslint/quotes": ["error", "double"],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error"
],
"@typescript-eslint/interface-name-prefix": ["error", { "prefixWithI": "always" } ],
"@typescript-eslint/no-non-null-assertion": "off"
},
"overrides": [
{
"files": ["*.spec.ts"],
"rules": {
"no-unused-expressions": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-use-before-define": "off"
}
}
]
}
6 changes: 6 additions & 0 deletions src/PatientTestsApi/.mocharc.json
@@ -0,0 +1,6 @@
{
"require": [
"source-map-support/register"
],
"recursive": true
}
39 changes: 22 additions & 17 deletions src/PatientTestsApi/.vscode/settings.json
Expand Up @@ -6,24 +6,29 @@
"debug.internalConsoleOptions": "neverOpen",
"azureFunctions.preDeployTask": "npm prune",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#3399ff",
"activityBar.activeBorder": "#bf0060",
"activityBar.background": "#3399ff",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#bf0060",
"activityBarBadge.foreground": "#e7e7e7",
"statusBar.background": "#007fff",
"statusBar.border": "#007fff",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#3399ff",
"titleBar.activeBackground": "#007fff",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.border": "#007fff",
"titleBar.inactiveBackground": "#007fff99",
"titleBar.inactiveForeground": "#e7e7e799"
"activityBar.activeBackground": "#3399ff",
"activityBar.activeBorder": "#bf0060",
"activityBar.background": "#3399ff",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#bf0060",
"activityBarBadge.foreground": "#e7e7e7",
"statusBar.background": "#007fff",
"statusBar.border": "#007fff",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#3399ff",
"titleBar.activeBackground": "#007fff",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.border": "#007fff",
"titleBar.inactiveBackground": "#007fff99",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#007fff",
"eslint.lintTask.enable": true,
"eslint.lintTask.options": ". --ext .ts"
"eslint.lintTask.options": ". --ext .ts",
"mochaExplorer.files": "dist/Test/**/*.spec.js",
"mochaExplorer.require": "source-map-support/register",
"typescript.validate.enable": true,
"editor.tabSize": 2,
"editor.detectIndentation": false,
}
45 changes: 45 additions & 0 deletions src/PatientTestsApi/Controllers/ControllerFactory.ts
@@ -0,0 +1,45 @@
import { PatientController } from "./PatientController";
import { Db, MongoClient } from "mongodb";
import { ISettings } from "../Models/ISettings";
import { ICollection } from "../Services/ICollection";
import { PatientDataService } from "../Services/PatientDataService";
import { EnvironmentSettings } from "../Models/EnvironmentSettings";
import { RetryCollection } from "../Services/RetryCollection";
import { LoggingCollection } from "../Services/LoggingCollection";
import { AppInsightsService } from "../Services/app-insights/app-insights-service";
import { TraceContext, HttpRequest } from "@azure/functions";

export class ControllerFactory {

private static mongoDb: Promise<Db>;
private readonly settings: ISettings;

constructor () {
this.settings = new EnvironmentSettings();
}

public async createPatientController(functionContext: TraceContext, request: HttpRequest): Promise<PatientController> {
const appInsightsService = new AppInsightsService(functionContext, request);
const collection = await this.CreateCollection(this.settings.patientCollection, appInsightsService);
const dataService: PatientDataService = new PatientDataService(collection);
return new PatientController(dataService);
}

private async CreateCollection(collectionName: string, appInsightsService: AppInsightsService): Promise<ICollection> {
if (ControllerFactory.mongoDb == null) {
ControllerFactory.mongoDb = this.createMongoDb();
}
const mongoCollection = (await ControllerFactory.mongoDb).collection(collectionName);

const retryCollection = new RetryCollection(mongoCollection);
return new LoggingCollection(retryCollection, appInsightsService, collectionName, this.settings.patientTestDatabase);
}

private async createMongoDb(): Promise<Db> {
// connect and select database
const mongoClient = await MongoClient.connect(this.settings.mongoConnectionString,
{ useUnifiedTopology: true, useNewUrlParser: true, tlsAllowInvalidCertificates: this.settings.allowSelfSignedMongoCert });

return mongoClient.db(this.settings.patientTestDatabase);
}
}
39 changes: 39 additions & 0 deletions src/PatientTestsApi/Controllers/PatientController.ts
@@ -0,0 +1,39 @@
import { HttpRequest } from "@azure/functions";
import { IPatient, PatientSchema } from "../Models/IPatient";
import { IResponse } from "../Models/IResponse";
import { IPatientDataService } from "../Services/IPatientDataService";
import { CreatedResponse} from "../Models/CreatedResponse";
import { BadRequestResponse } from "../Models/BadRequestResponse";
import { v4 as uuidv4 } from "uuid";

export class PatientController {
public constructor(
private readonly patientDataService: IPatientDataService
) {}

public async createPatient(req: HttpRequest): Promise<IResponse> {
const validationResult = PatientSchema.validate(req.body);
if (validationResult.error != null) {
return new BadRequestResponse(validationResult.error.message);
}

if (req.body.id != null) {
return new BadRequestResponse("Id unexpected.");
}
if (req.body.lastUpdated != null) {
return new BadRequestResponse("lastUpdated unexpected.");
}

req.body.lastUpdated = new Date();



const patient = req.body as IPatient || {};
patient.id = uuidv4();
await this.patientDataService.insertPatient(patient);

return new CreatedResponse(patient);
}
}


20 changes: 20 additions & 0 deletions src/PatientTestsApi/CreatePatient/function.json
@@ -0,0 +1,20 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"post"
],
"route": "patient"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/CreatePatient/index.js"
}
12 changes: 12 additions & 0 deletions src/PatientTestsApi/CreatePatient/index.ts
@@ -0,0 +1,12 @@
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { ControllerFactory } from "../Controllers/ControllerFactory";


const controllerFactory = new ControllerFactory();

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
const controller = await controllerFactory.createPatientController(context.traceContext, req);
context.res = await controller.createPatient(req);
};

export default httpTrigger;
10 changes: 10 additions & 0 deletions src/PatientTestsApi/Models/BadRequestResponse.ts
@@ -0,0 +1,10 @@
import { IResponse } from "./IResponse";
export class BadRequestResponse extends Error implements IResponse {
public constructor(message: string) {
super ("Bad request: " + message);
this.body = message;
}
body: string;
headers = { "Content-Type": "application/json" };
status = 400;
}
7 changes: 7 additions & 0 deletions src/PatientTestsApi/Models/CreatedResponse.ts
@@ -0,0 +1,7 @@
import { IResponse } from "./IResponse";
export class CreatedResponse<T> implements IResponse {
public constructor(public body: T) {
}
headers = { "Content-Type": "application/json" };
status = 201;
}
9 changes: 9 additions & 0 deletions src/PatientTestsApi/Models/EnvironmentSettings.ts
@@ -0,0 +1,9 @@
import { ISettings } from "./ISettings";

export class EnvironmentSettings implements ISettings {
public get patientCollection(): string { return "patients";}
public get patientTestDatabase(): string { return process.env.patient_tests_database || ""; }
public get mongoConnectionString(): string { return process.env.mongo_connection_string || "";}
public get allowSelfSignedMongoCert(): boolean { return JSON.parse(process.env.allow_self_signed_mongo_cert || "false");}

}
6 changes: 6 additions & 0 deletions src/PatientTestsApi/Models/Gender.ts
@@ -0,0 +1,6 @@
export enum Gender {
Male = "male",
Female = "female",
Other = "other",
Unknown = "unknown"
}
2 changes: 2 additions & 0 deletions src/PatientTestsApi/Models/IHeaders.ts
@@ -0,0 +1,2 @@
/** Interface for http request headers */
export interface IHeaders { [key: string]: string }
40 changes: 40 additions & 0 deletions src/PatientTestsApi/Models/IPatient.ts
@@ -0,0 +1,40 @@
import { Gender } from "./Gender";
import Joi from "@hapi/joi";

export interface IPatient {
[key: string]: unknown;
id?: string;
firstName?: string;
lastName: string;
fullName?: string;
gender: Gender;
dateOfBirth: string;
postCode: string;
insuranceNumber?: string;
preferredContactNumber?: string;
lastUpdated?: Date;
}

const maxLengthNameField = 64;
const maxLengthFullNameField = 128;
const postcodeLength = 4;

const dateRegexString = /^([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?$/;

export const PatientSchema = Joi.object<IPatient>({
id: Joi.string().guid().optional(),
firstName: Joi.string().max(maxLengthNameField).required(),
lastName: Joi.string().max(maxLengthNameField),
fullName: Joi.string().max(maxLengthFullNameField),

gender: Joi.string()
.allow(Gender.Male, Gender.Female, Gender.Other, Gender.Unknown)
.only()
.required(),

dateOfBirth: Joi.string().pattern(dateRegexString, "date").required(),
postCode: Joi.string().length(postcodeLength).required(),
insuranceNumber: Joi.string(),
preferredContactNumber: Joi.string(),
lastUpdated: Joi.date().optional(),
});
7 changes: 7 additions & 0 deletions src/PatientTestsApi/Models/IResponse.ts
@@ -0,0 +1,7 @@
export interface IResponse {
body: unknown;
headers: {
[key: string]: string;
};
status: number;
}
6 changes: 6 additions & 0 deletions src/PatientTestsApi/Models/ISettings.ts
@@ -0,0 +1,6 @@
export interface ISettings {
patientCollection: string;
patientTestDatabase: string;
mongoConnectionString: string;
allowSelfSignedMongoCert: boolean;
}
5 changes: 5 additions & 0 deletions src/PatientTestsApi/Models/InsertFailedError.ts
@@ -0,0 +1,5 @@
export class InsertFailedError extends Error {
constructor(){
super("Error inserting data record.");
}
}
19 changes: 19 additions & 0 deletions src/PatientTestsApi/Services/DefaultRetryPolicy.ts
@@ -0,0 +1,19 @@
import { IRetryPolicy } from "./IRetryPolicy";
/**
* Default retry policy
*/
export class DefaultRetryPolicy implements IRetryPolicy {
private currentRetryCount = 0;
constructor(private readonly maxRetryCount: number = 10, public retryAfterMilliSec: number = 1000) { }

/**
* Check if the operation should be retried
*/
public shouldRetry(): boolean {
if (this.currentRetryCount < this.maxRetryCount) {
this.currentRetryCount++;
return true;
}
return false;
}
}
8 changes: 8 additions & 0 deletions src/PatientTestsApi/Services/ICollection.ts
@@ -0,0 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CollectionInsertOneOptions, InsertOneWriteOpResult } from "mongodb";

export interface ICollection {
insertOne(docs: any, options?: CollectionInsertOneOptions): Promise<InsertOneWriteOpResult<any>>;
}


5 changes: 5 additions & 0 deletions src/PatientTestsApi/Services/IPatientDataService.ts
@@ -0,0 +1,5 @@
import { IPatient } from "../Models/IPatient";

export interface IPatientDataService {
insertPatient(patient: IPatient): Promise<string>;
}
10 changes: 10 additions & 0 deletions src/PatientTestsApi/Services/IRetryPolicy.ts
@@ -0,0 +1,10 @@
/**
* Interface for retry policy
*/
export interface IRetryPolicy {
retryAfterMilliSec: number;
/**
* Check if the operation should be retried
*/
shouldRetry(): boolean;
}

0 comments on commit 96c38f7

Please sign in to comment.