Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented CreatePatient endpoint (#33)
* 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
1 parent
4210175
commit 96c38f7
Showing
45 changed files
with
1,628 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"require": [ | ||
"source-map-support/register" | ||
], | ||
"recursive": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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");} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export enum Gender { | ||
Male = "male", | ||
Female = "female", | ||
Other = "other", | ||
Unknown = "unknown" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/** Interface for http request headers */ | ||
export interface IHeaders { [key: string]: string } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export interface IResponse { | ||
body: unknown; | ||
headers: { | ||
[key: string]: string; | ||
}; | ||
status: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export interface ISettings { | ||
patientCollection: string; | ||
patientTestDatabase: string; | ||
mongoConnectionString: string; | ||
allowSelfSignedMongoCert: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export class InsertFailedError extends Error { | ||
constructor(){ | ||
super("Error inserting data record."); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>>; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { IPatient } from "../Models/IPatient"; | ||
|
||
export interface IPatientDataService { | ||
insertPatient(patient: IPatient): Promise<string>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/** | ||
* Interface for retry policy | ||
*/ | ||
export interface IRetryPolicy { | ||
retryAfterMilliSec: number; | ||
/** | ||
* Check if the operation should be retried | ||
*/ | ||
shouldRetry(): boolean; | ||
} |
Oops, something went wrong.