Added the files.

This commit is contained in:
Batuhan Berk Başoğlu 2021-03-30 21:50:36 -04:00
commit 38ccdcbfe5
124 changed files with 32079 additions and 0 deletions

16
server/.babelrc Normal file
View file

@ -0,0 +1,16 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
],
"plugins": [
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-optional-chaining"
]
}

3
server/.cfignore Normal file
View file

@ -0,0 +1,3 @@
node_modules
/server
/public

8
server/.env Normal file
View file

@ -0,0 +1,8 @@
APP_ID=server
PORT=3001
LOG_LEVEL=debug
REQUEST_LIMIT=100kb
SESSION_SECRET=mySecret
OPENAPI_SPEC=/api/v1/spec
OPENAPI_ENABLE_RESPONSE_VALIDATION=false

5
server/.eslintignore Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
public/api-explorer/
*.yaml
*.yml

26
server/.eslintrc.json Normal file
View file

@ -0,0 +1,26 @@
{
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:node/recommended", "prettier"],
"env": {
"mocha": true
},
"plugins": ["prettier", "node"],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": 2,
"node/no-unsupported-features/es-syntax": 0,
"node/no-unpublished-import": ["off"],
"prettier/prettier": [
"error",
{
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"printWidth": 80
}
]
}
}

3
server/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
.DS_Store

7
server/.nodemonrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"watch": [
"server/**/*.*",
".env"
],
"ext": "js,json,mjs,yaml,yml"
}

117
server/README.md Normal file
View file

@ -0,0 +1,117 @@
# HMS server-side code
## Get Started
Get started developing...
```shell
# install deps
npm install
# run in development mode
npm run dev
# run tests
npm run test
```
## PLEASE READ THIS WHEN ADDING YOUR PART TO SERVER
There are two key files that enable you to customize and describe your API:
1. `server/routes.js` - This references the implementation of all of your routes. Add as many routes as you like and point each route your express handler functions.
2. `server/common/api.yaml` - This file contains your [OpenAPI spec](https://swagger.io/specification/). Describe your API here. It's recommended that you to declare any and all validation logic in this YAML. `express-no-stress-typescript` uses [express-openapi-validator](https://github.com/cdimascio/express-openapi-validator) to automatically handle all API validation based on what you've defined in the spec.
## Install Dependencies
Install all package dependencies (one time operation)
```shell
npm install
```
## Run It
#### Run in *development* mode:
Runs the application is development mode. Should not be used in production
```shell
npm run dev
```
or debug it
```shell
npm run dev:debug
```
#### Run in *production* mode:
Compiles the application and starts it in production production mode.
```shell
npm run compile
npm start
```
## Test It
Run the Mocha unit tests
```shell
npm test
```
or debug them
```shell
npm run test:debug
```
## Try It
* Open your browser to [http://localhost:3001](http://localhost:3001)
* Invoke the `/examples` endpoint
```shell
curl http://localhost:3001/api/v1/examples
```
## Debug It
#### Debug the server:
```
npm run dev:debug
```
#### Debug Tests
```
npm run test:debug
```
#### Debug with VSCode
Add these [contents](https://github.com/cdimascio/generator-express-no-stress/blob/next/assets/.vscode/launch.json) to your `.vscode/launch.json` file
## Lint It
View prettier linter output
```
npm run lint
```
Fix all prettier linter errors
```
npm run lint
```
## Deploy It
Deploy to CloudFoundry
```shell
cf push server
```

7557
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
server/package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "server",
"version": "1.0.0",
"description": "HMS server-side code",
"main": "index.js",
"scripts": {
"start": "node dist/index.js",
"compile": "babel server --out-dir dist --delete-dir-on-start --source-maps inline --copy-files",
"dev": "nodemon server --exec babel-node --config .nodemonrc.json | pino-pretty",
"dev:debug": "nodemon server --exec babel-node --config .nodemonrc.json --inspect | pino-pretty",
"test": "mocha --require @babel/register --exit",
"test:debug": "mocha --require @babel/register --inspect-brk --exit",
"lint": "eslint .",
"lint:fix": "eslint --fix ."
},
"dependencies": {
"bcrypt": "^5.0.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"continuation-local-storage": "^3.2.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-openapi-validator": "^4.5.0",
"jsonwebtoken": "^8.5.1",
"pg": "^8.5.1",
"pg-hstore": "^2.3.3",
"pino": "^6.7.0",
"sequelize": "^6.3.5",
"sequelize-cli": "^6.2.0",
"underscore": "^1.12.0",
"uuid": "^8.3.1"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/node": "^7.12.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/register": "^7.12.1",
"babel-eslint": "^10.1.0",
"chai": "^4.2.0",
"eslint": "^7.12.1",
"eslint-plugin-import": "^2.22.1",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.1.4",
"prettier": "^2.1.2",
"mocha": "^8.2.1",
"nodemon": "^2.0.6",
"pino-pretty": "^4.3.0",
"supertest": "^6.0.1"
},
"author": "Carmine DiMascio <cdimascio@gmail.com> (https://github.com/cdimascio)"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

View file

@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>server</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: window.location.origin + "/api/v1/spec",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View file

@ -0,0 +1,68 @@
<!doctype html>
<html lang="en-US">
<title>Swagger UI: OAuth2 Redirect</title>
<body onload="run()">
</body>
</html>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&")
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value)
}
) : {}
isValid = qp.state === sentState
if ((
oauth2.auth.schema.get("flow") === "accessCode"||
oauth2.auth.schema.get("flow") === "authorizationCode"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
server/public/index.html Normal file
View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
server
</title>
</head>
<body>
<h1>Welcome to
server
</h1>
<ul>
<li>
<h3><a href="/api-explorer/">Interactive API Doc!</a></h3>
</li>
</ul>
</body>
</html>

View file

@ -0,0 +1,149 @@
/* eslint-disable no-param-reassign */
/* eslint-disable consistent-return */
/* eslint-disable eqeqeq */
/* eslint-disable no-unused-vars */
const express = require('express');
const jwt = require('jsonwebtoken');
const config = require('../../../common/env');
const error = require('../../../common/error');
const { default: l } = require('../../../common/logger');
const { User } = require('../../../database');
const router = express.Router();
const TOKEN_EXPIRES = 7*86400; // 7 day in seconds
/**
* Middleware function that determines whether or not a user is authenticated
* and assigns the req.user object to their user info from the db
*
* @param req The request object
* @param res The response object
* @param next The next-middleware function
*/
export const authenticated = (req, res, next) => {
// We're looking for a header in the form of:
// Authorization: Bearer <TOKEN>
const referer = req.originalUrl;
// eslint-disable-next-line prettier/prettier
if (referer.includes('login') || referer.includes('signup') ) return next();
const authHeader = req.get('Authorization');
if (!authHeader) return next(new error.Unauthorized());
// authHead should be in the form of ['Bearer', '<TOKEN>']
const authHead = authHeader.split(' ');
if (
authHead.length != 2 ||
authHead[0] !== 'Bearer' ||
authHead[1].length < 1
)
return next(new error.Unauthorized());
const token = authHead[1];
jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => {
if (err) return next(new error.Unauthorized());
// if the user provided a valid token, use it to deserialize the UUID to
// an actual user object
User.findByUUID(decoded.uuid)
.then((user) => {
if (!user) throw new error.Unauthorized();
req.user = user;
})
.then(next)
.catch(next);
});
};
/**
* Login route.
*
* POST body should be in the format of { email, password }
* On success, this route will return a token
*/
router.post('/login', (req, res, next) => {
if (!req.body.email || req.body.email.length < 1)
return next(new error.BadRequest('Email must be provided'));
if (!req.body.password || req.body.password.length < 1)
return next(new error.BadRequest('Password must be provided'));
let userInfo = null;
User.findByEmail(req.body.email.toLowerCase())
.then((user) => {
if (!user) throw new error.UserError('Invalid email or password');
if (user.isPending()) {
throw new error.Unauthorized(
'Please activate your account. Check your email for an activation email'
);
}
if (user.isBlocked())
throw new error.Forbidden('Your account has been blocked');
return user
.verifyPassword(req.body.password)
.then((verified) => {
if (!verified) throw new error.UserError('Invalid email or password');
userInfo = user;
})
.then(
() =>
new Promise((resolve, reject) => {
// create a token with the user's ID and privilege level
jwt.sign(
{
uuid: user.getDataValue('uuid'),
admin: user.isAdmin(),
},
process.env.SESSION_SECRET,
{ expiresIn: TOKEN_EXPIRES },
(err, token) => (err ? reject(err) : resolve(token))
);
})
);
})
.then((token) => {
// respond with the token upon successful login
res.json({ error: null, token });
// register that the user logged in
})
.catch(next);
});
/**
* Registration route.
*
* POST body accepts a user object (see DB schema for user, sanitize function)
* Returns the created user on success
*/
router.post('/register', (req, res, next) => {
l.info(req.body);
if (!req.body.user)
return next(new error.BadRequest('User must be provided'));
if (!req.body.user.password)
return next(new error.BadRequest('Password must be provided'));
if (req.body.user.password.length < 4) {
return next(
new error.BadRequest('Password should be at least 10 characters long')
);
}
// get a sanitized version of the input
const userModel = User.sanitize(req.body.user);
// TODO: implement email auth instead of just active
userModel.state = 'ACTIVE';
// create the password hash
User.generateHash(req.body.user.password)
.then((hash) => {
userModel.hash = hash;
// add the user to the DB
return User.create(userModel);
})
.then((user) => {
// responsd with the newly created user
res.json({ error: null, user: user.getPublicProfile() });
})
.catch(next);
});
export default router;

View file

@ -0,0 +1,72 @@
import Division from '../../models/Division';
import AdmissionRequest from '../../models/AdmissionRequest';
import Patient from '../../models/Patient';
import Room from '../../models/Room';
import Repository from './repository';
module.exports = {
async getDivisionInfo(req, res) {
const { id } = req.body;
const division = await Repository.find(id);
return res.json(division);
},
async getDivisionAdmissionRequestList(req, res) {
const { id } = req.body;
const admissionList = Division.getAllAdmissionRequests(id);
return res.json(admissionList);
},
async createAdmissionRequest(req, res) {
const {divisionId, patientId, rationale, localDoctor} = req.body;
AdmissionRequest.create({divisionId, patientId, rationale, localDoctor});
let patient = await Patient.findById(patientId);
patient.isRequested = 'REQUESTED';
patient = await patient.save();
},
async admitPatient(req, res){
const { patientId, admissionRequestId } = req.body;
let admissionRequest = await AdmissionRequest.findById(admissionRequestId);
admissionRequest.approvalType = 'APPROVED';
admissionRequest = await admissionRequest.save();
let patient = await Patient.findById(patientId);
patient.isRequested = 'REQUEST_COMPLETED';
patient = await patient.save();
},
async admitPatientToRoom(req, res){
const {patientId, roomId, bedId} = req.body;
const room = await Room.findById(roomId);
let bed = await room.getBed(bedId);
bed.patientId = patientId;
bed.bedType = 'OCCUPIED';
bed = await bed.save();
let patient = await Patient.findById(patientId);
patient.isAdmitted = 'ADMITTED';
patient = await patient.save();
},
async dischargePatient(req, res){
const {roomId, bedId} = req.body;
const room = await Room.findById(roomId);
let bed = await room.getBed(bedId);
bed.patientId = 0;
bed.bedType = 'UNOCCUPIED';
bed = await bed.save();
let patient = await Patient.findById(patientId);
patient.isAdmitted = 'DISCHARGED';
patient = await patient.save();
},
};

View file

@ -0,0 +1,21 @@
import l from '../../../common/logger';
// EXAMPLE DATA - Will be replaced by data from DB
const division = ['division1', 'division2', 'division3', 'division4'];
const db = {
find: (id) => division[id],
save: (newDivision) => division.push(newDivision),
};
class DivisionRepository {
find(id) {
l.info(`${this.constructor.name}.byId(${id})`);
return db.find(id);
}
save(division) {
return db.save(division);
}
}
export default new DivisionRepository();

View file

@ -0,0 +1,22 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
/**
* Get division information
*/
router.route('/').get(facade.getDivisionInfo);
/**
* Get division admission request list, Post admission request, Post patient admittance
*/
router.route('/AdmissionRequest').get(facade.getDivisionAdmissionRequestList).post(facade.createAdmissionRequest).post(facade.admitPatient);
/**
* Post patient admittance to room, Post patient discharge from a room
*/
router.route('/Room/Bed').post(facade.admitPatientToRoom).post(facade.dischargePatient);
export default router;

View file

@ -0,0 +1,14 @@
import Medication from '../../models/Medication';
module.exports = {
async createPrescription(req, res, next){
const {drugName, unitsByDay, numberOfAdmins, adminTimes, methodOfAdmin, startDate, finishDate} = req.body;
Medication.create({drugName, unitsByDay, numberOfAdmins, adminTimes, methodOfAdmin, startDate, finishDate});
},
};

View file

@ -0,0 +1,13 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
router
.route('/')
/**
* Create a prescription for the patient
*/
.post(facade.createPrescription);
export default router;

View file

@ -0,0 +1,65 @@
import Patient from '../../models/Patient';
import Repository from './repository';
import Address from '../../models/Address';
module.exports = {
async registerPatient(req, res) {
const {
firstName,
lastName,
telephone,
dateOfBirth,
gender,
maritalStatus,
externalDoctor,
divisionId,
} = req.body;
Repository.save(
Patient.create({
firstName,
lastName,
telephone,
gender,
maritalStatus,
externalDoctor,
divisionId,
birthDate: dateOfBirth,
})
);
},
async getPatientFile(req, res) {
const { id } = req.body;
const patient = await Repository.find(id);
return res.json(patient);
},
async updatePatientFile(req, res) {
const {
firstName,
id,
lastName,
telephone,
dateOfBirth,
gender,
maritalStatus,
externalDoctor,
divisionId,
address,
} = req.body;
let patient = await Repository.find(id);
patient.firstName = firstName;
patient.lastName = lastName;
patient.telephone = telephone;
patient.dateOfBirth = dateOfBirth;
patient.gender = gender;
patient.maritalStatus = maritalStatus;
patient.externalDoctor = externalDoctor;
patient.divisionId = divisionId;
patient.address = address;
patient = await patient.save();
},
};

View file

@ -0,0 +1,21 @@
import l from '../../../common/logger';
// EXAMPLE DATA - Will be replaced by data from DB
const patient = ['patient'];
const db = {
find: (id) => patient[id],
save: (newPatient) => patient.push(newPatient),
};
class PatientRepository {
find(id) {
l.info(`${this.constructor.name}.byId(${id})`);
return db.find(id);
}
save(patient) {
return db.save(patient);
}
}
export default new PatientRepository();

View file

@ -0,0 +1,21 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
/**
* Get patient file
*/
router
.route('/')
.get(facade.getPatientFile)
/**
* Update patient file
*/
.patch(facade.updatePatientFile)
/**
* Update patient file
*/
.post(facade.registerPatient);
export default router;

View file

@ -0,0 +1,119 @@
const jwt = require('jsonwebtoken');
const error = require('../../../common/error');
const { default: l } = require('../../../common/logger');
const { User } = require('../../../database');
const TOKEN_EXPIRES = 7 * 86400; // 7 day in seconds
module.exports = {
async register(req, res, next) {
l.info(req.body);
if (!req.body.user)
return next(new error.BadRequest('User must be provided'));
if (!req.body.user.password)
return next(new error.BadRequest('Password must be provided'));
if (req.body.user.password.length < 4) {
return next(
new error.BadRequest('Password should be at least 10 characters long')
);
}
// get a sanitized version of the input
const userModel = User.sanitize(req.body.user);
userModel.state = 'ACTIVE';
// create the password hash
User.generateHash(req.body.user.password)
.then((hash) => {
userModel.hash = hash;
// add the user to the DB
return User.create(userModel);
})
.then((user) => {
// responsd with the newly created user
res.json({ error: null, user: user.getPublicProfile() });
})
.catch(next);
},
async login(req, res, next) {
if (!req.body.email || req.body.email.length < 1)
return next(new error.BadRequest('Email must be provided'));
if (!req.body.password || req.body.password.length < 1)
return next(new error.BadRequest('Password must be provided'));
User.findByEmail(req.body.email.toLowerCase())
.then((user) => {
if (!user) throw new error.UserError('Invalid email or password');
if (user.isPending()) {
throw new error.Unauthorized(
'Please activate your account. Check your email for an activation email'
);
}
if (user.isBlocked())
throw new error.Forbidden('Your account has been blocked');
return user
.verifyPassword(req.body.password)
.then((verified) => {
if (!verified)
throw new error.UserError('Invalid email or password');
})
.then(
() =>
new Promise((resolve, reject) => {
// create a token with the user's ID and privilege level
jwt.sign(
{
uuid: user.getDataValue('uuid'),
admin: user.isAdmin(),
},
process.env.SESSION_SECRET,
{ expiresIn: TOKEN_EXPIRES },
(err, token) => (err ? reject(err) : resolve(token))
);
})
);
})
.then((token) => {
// respond with the token upon successful login
res.json({ error: null, token });
// register that the user logged in
})
.catch(next);
},
async getStaff(req, res) {
res.json({ error: null, user: req.user.getUserProfile() });
},
async updateStaff(req, res, next) {
if (!req.body.user) return next(new error.BadRequest());
// construct new, sanitized object of update information
const updatedInfo = {};
// for each field { fistName, lastName, major, year }
// check that it is a valid input and it has changed
if (
req.body.user.firstName &&
req.body.user.firstName.length > 0 &&
req.body.user.firstName !== req.user.firstName
)
updatedInfo.firstName = req.body.user.firstName;
if (
req.body.user.lastName &&
req.body.user.lastName.length > 0 &&
req.body.user.lastName !== req.user.lastName
)
updatedInfo.lastName = req.body.user.lastName;
/*
update the user information normally
(with the given information, without any password changes)
*/
req.user
.update(updatedInfo)
.then((user) => {
// respond with the newly updated user profile
res.json({ error: null, user: user.getUserProfile() });
})
.catch(next);
},
};

View file

@ -0,0 +1,21 @@
import l from '../../../common/logger';
// EXAMPLE DATA - Will be replaced by data from DB
const staff = ['staff1', 'staff2', 'staff3', 'staff4'];
const db = {
find: (id) => staff[id],
save: (newStaff) => staff.push(newStaff),
};
class StaffRepository {
find(id) {
l.info(`${this.constructor.name}.byId(${id})`);
return db.find(id);
}
save(staff) {
return db.save(staff);
}
}
export default new StaffRepository();

View file

@ -0,0 +1,19 @@
import * as express from 'express';
import facade from './facade';
const router = express.Router();
/**
* Get user profile for current user
*/
router
.route('/')
.get(facade.getStaff)
/**
* Update user information given a 'user' object with fields to update and updated information
*/
.patch(facade.updateStaff);
router.post('/register', facade.register);
router.post('/login', facade.login);
export default router;

View file

@ -0,0 +1,5 @@
// eslint-disable-next-line no-unused-vars, no-shadow
export default function errorHandler(err, req, res, next) {
const errors = err.errors || [{ message: err.message }];
res.status(err.status || 500).json({ errors });
}

View file

@ -0,0 +1,98 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
module.exports = (Sequelize, db) => {
const AdmissionRequest = db.define(
'admissionRequest',
{
// admission request ID: main way of querying the admission request
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// division ID: main way of querying the division
divisionId: {
type: Sequelize.INTEGER,
autoIncrement: true,
},
// rationale name
rationale: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Rationale name must be at least 2 characters long',
},
notEmpty: {
msg: 'The rationale name is a required field',
},
},
},
// local doctor name
localDoctor: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Local Doctor must be at least 2 characters long',
},
notEmpty: {
msg: 'The local doctor is a required field',
},
},
},
// patient ID: main way of querying the patient
patientId: {
type: Sequelize.INTEGER,
},
// type of admission
// UNAPPROVED - not a approved admission
// APPROVED - a approved admission
approvalType: {
type: Sequelize.ENUM('UNAPPROVED', 'APPROVED'),
defaultValue: 'UNAPPROVED',
},
},
);
AdmissionRequest.findById = function(id){
return this.findOne({where: {id}})
}
AdmissionRequest.findByDivisionId = function(divisionId){
return this.findAll({where: {divisionId}})
}
AdmissionRequest.findByRationale = function (rationale) {
return this.findOne({ where: { rationale } });
};
AdmissionRequest.findByLocalDoctor = function (localDoctor) {
return this.findOne({ where: { localDoctor } });
};
AdmissionRequest.findByPatientId = function (patientId) {
return this.findOne({ where: { patientId } });
};
AdmissionRequest.prototype.isApproved = function () {
return this.getDataValue('approvalType') === 'APPROVED';
};
AdmissionRequest.prototype.isUnapproved = function () {
return this.getDataValue('approvalType') === 'UNAPPROVED';
};
return AdmissionRequest;
};

View file

@ -0,0 +1,61 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
module.exports = (Sequelize, db) => {
const Bed = db.define(
'bed',
{
// Bed number
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// room ID: main way of querying the room
roomId: {
type: Sequelize.INTEGER,
autoIncrement: true,
},
// patient ID: main way of querying the patient
patientId: {
type: Sequelize.INTEGER,
},
// type of admission
// UNOCCUPIED - not a occupied admission
// OCCUPIED - a occupied admission
bedType: {
type: Sequelize.ENUM('UNOCCUPIED', 'OCCUPIED'),
defaultValue: 'UNOCCUPIED',
},
},
);
AdmissionRequest.findById = function (id) {
return this.findOne({ where: { id } });
};
AdmissionRequest.findByRoomId = function(roomId){
return this.findAll({where: {roomId}})
}
AdmissionRequest.findByPatientId = function (patientId) {
return this.findOne({ where: { patientId } });
};
AdmissionRequest.prototype.isOccupied = function () {
return this.getDataValue('bedType') === 'OCCUPIED';
};
AdmissionRequest.prototype.isUncompleted = function () {
return this.getDataValue('bedType') === 'UNOCCUPIED';
};
return Bed;
};

View file

@ -0,0 +1,127 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
const AdmissionRequest = require('./AdmissionRequest');
const Room = require('./Room');
module.exports = (Sequelize, db) => {
const Division = db.define('division', {
// division ID: main way of querying the division
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// division name
name: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Division name must be at least 2 characters long',
},
notEmpty: {
msg: 'The division name is a required field',
},
},
},
// location name
location: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Location name must be at least 2 characters long',
},
notEmpty: {
msg: 'The location name is a required field',
},
},
},
// number of beds
numberOfBeds: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Number of beds must be at least a character long',
},
notEmpty: {
msg: 'The number of beds is a required field',
},
},
},
// telephone
telephone: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Telephone must be at least a character long',
},
notEmpty: {
msg: 'The telephone is a required field',
},
},
},
// charge nurse ID: main way of querying the charge nurse
chargeNurseId: {
type: Sequelize.INTEGER,
},
// type of division
// UNCOMPLETED - not a completed division
// COMPLETED - a completed division
divisionType: {
type: Sequelize.ENUM('UNCOMPLETED', 'COMPLETED'),
defaultValue: 'UNCOMPLETED',
},
});
Division.findByName = function (name) {
return this.findOne({ where: { name } });
};
Division.findByLocation = function (location) {
return this.findOne({ where: { location } });
};
Division.findByNumberOfBeds = function (numberOfBeds) {
return this.findOne({ where: { numberOfBeds } });
};
Division.findByTelephone = function (telephone) {
return this.findOne({ where: { telephone } });
};
Division.findByChargeNurseID = function (chargeNurseId) {
return this.findOne({ where: { chargeNurseId } });
};
Division.getAllAdmissionRequests = function (id) {
AdmissionRequest.findByDivisionId(id);
};
Division.getAllRooms = function (id) {
Room.findByDivisionId(id);
};
Division.prototype.isCompleted = function () {
return this.getDataValue('divisionType') === 'COMPLETED';
};
Division.prototype.isUncompleted = function () {
return this.getDataValue('divisionType') === 'UNCOMPLETED';
};
return Division;
};

View file

@ -0,0 +1,133 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
module.exports = (Sequelize, db) => {
const Medication = db.define(
'medication',
{
// drug number for id
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// drug name
drugName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'The drug name must be at least 2 characters long',
},
notEmpty: {
msg: 'The drug name is a required field',
},
},
},
// number of units to take per day
unitsByDay: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Units must be at least a character long',
},
notEmpty: {
msg: 'The number of units is a required field',
},
},
},
// number of administrations of the drug
numberOfAdmins: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: '# of admins must be at least a character long',
},
notEmpty: {
msg: 'The number of admins is a required field',
},
},
},
// times of the drug administrations
adminTimes: {
type: Sequelize.TIME,
defaultValue: Sequelize.NOW,
},
// method of drug administration
methodOfAdmin: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'method of administration must be at least 2 characters long',
},
notEmpty: {
msg: 'The method of administration name is a required field',
},
},
},
// start date of drug administering
startDate: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
// when the prescription ends
finishDate: {
type: Sequelize.DATE,
},
},
);
Medication.findById = function (id) {
return this.findOne({ where: { id } });
};
Medication.findByDrugName = function (drugName) {
return this.findOne({ where: { drugName } });
};
Medication.findByUnitsByDay = function (unitsByDay) {
return this.findOne({ where: { unitsByDay } });
};
Medication.findByNumberOfAdmins = function (numberOfAdmins) {
return this.findOne({ where: { numberOfAdmins } });
};
Medication.findByAdminTimes = function (adminTimes) {
return this.findOne({ where: { adminTimes } });
};
Medication.findByMethodOfAdmin = function (methodOfAdmin) {
return this.findOne({ where: { methodOfAdmin } });
};
Medication.findByStartDate = function (startDate) {
return this.findOne({ where: { startDate } });
};
Medication.findByFinishDate = function (finishDate) {
return this.findOne({ where: { finishDate } });
};
return Medication;
};

View file

@ -0,0 +1,162 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
// const Address = require("./Address");
module.exports = (Sequelize, db) => {
const Patient = db.define('patient', {
// patient ID: main way of querying the patient
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// patient firstName
firstName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Patients first name must be at least 2 characters long',
},
notEmpty: {
msg: 'The patients first name is a required field',
},
},
},
// patient lastName
lastName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Patients last name must be at least 2 characters long',
},
notEmpty: {
msg: 'The patients last name is a required field',
},
},
},
// telephone
telephone: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Telephone must be at least a character long',
},
notEmpty: {
msg: 'The telephone is a required field',
},
},
},
// date of birth
birthDate: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
// patient gender
gender: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'gender last name must be at least 2 characters long',
},
notEmpty: {
msg: 'The patients gender is a required field',
},
},
},
//isMarried (Need an example of boolean type)
isMarried: {
type: Sequelize.ENUM('MARRIED', 'UNMARRIED'),
defaultValue: 'UNMARRIED',
},
//externalDoctor
externalDoctor: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Patients first name must be at least 2 characters long',
},
notEmpty: {
msg: 'The patients first name is a required field',
},
},
},
//divisionID
divisionId: {
type: Sequelize.INTEGER,
},
// address ID: main way of querying the address
addressId: {
type: Sequelize.INTEGER,
},
//isAdmitted
isAdmitted: {
type: Sequelize.ENUM('NOT_ADMITTED', 'ADMITTED', 'DISCHARGED'),
defaultValue: 'NOT_ADMITTED',
},
//isRequested
isRequested: {
type: Sequelize.ENUM('NOT_REQUESTED', 'REQUESTED', 'REQUEST_COMPLETED'),
defaultValue: 'NOT_REQUESTED',
},
});
Patient.findById = function (id) {
return this.findOne({ where: { id } });
};
Patient.findByfirstName = function (firstName) {
return this.findOne({ where: { firstName } });
};
Patient.findBylastName = function (lastName) {
return this.findOne({ where: { lastName } });
};
Patient.findByTelephone = function (telephone) {
return this.findOne({ where: { telephone } });
};
Patient.findBybirthDate = function (birthDate) {
return this.findOne({ where: { birthDate } });
};
Patient.findBygender = function (gender) {
return this.findOne({ where: { gender } });
};
Patient.findBymarried = function (isMarried) {
return this.findOne({ where: { isMarried } });
};
Patient.findByExDoctor = function (externalDoctor) {
return this.findOne({ where: { externalDoctor } });
};
Patient.findByDivisionId = function (divisionId) {
return this.findOne({ where: { divisionId } });
};
return Patient;
};

View file

@ -0,0 +1,49 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
const Bed = require("./Bed");
module.exports = (Sequelize, db) => {
const Room = db.define(
'room',
{
// Room number
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// division ID: main way of querying the division
divisionId: {
type: Sequelize.INTEGER,
autoIncrement: true,
},
// bed ID: main way of querying the bed
bedId: {
type: Sequelize.INTEGER,
},
},
);
Room.findById = function(id){
return this.findOne({where: {id}})
}
Room.findBydivisionId = function(divisionId){
return this.findAll({where: {divisionId}})
}
Room.getAllBeds = function(id){
Bed.findByRoomId(id);
}
Room.getBed = function(bedId){
Bed.findById(bedId);
}
return Room;
};

View file

@ -0,0 +1,208 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
const _ = require('underscore');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const HASH_ROUNDS = 10;
module.exports = (Sequelize, db) => {
const User = db.define(
'user',
{
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// user ID: main way of querying the user
uuid: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
},
// email address of the user
email: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: {
msg: 'The email you entered is not valid',
},
notEmpty: {
msg: 'The email is a required field',
},
},
},
// type of account
// RESTRICTED - not used currently
// STANDARD - a regular member
// ADMIN - admin type user
accessType: {
type: Sequelize.ENUM('STAFF', 'MEDICAL', 'NURSE', 'ADMIN'),
defaultValue: 'STAFF',
},
// account state
// PENDING - account pending activation (newly created)
// ACTIVE - account activated and in good standing
// BLOCKED - account is blocked, login is denied
// PASSWORD_RESET - account has requested password reset
state: {
type: Sequelize.ENUM('PENDING', 'ACTIVE', 'BLOCKED', 'PASSWORD_RESET'),
defaultValue: 'PENDING',
},
// user's first name
firstName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Your first name must be at least 2 characters long',
},
notEmpty: {
msg: 'The first name is a required field',
},
},
},
// user's last name
lastName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Your last name must be at least 2 characters long',
},
notEmpty: {
msg: 'The last name is a required field',
},
},
},
// user's password hash
hash: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: {
msg: 'The password cannot be empty',
},
},
},
// date of last login
lastLogin: {
type: Sequelize.DATE,
defaultValue: Sequelize.NOW,
},
},
{
// creating indices on frequently accessed fields improves efficiency
indexes: [
// a hash index on the uuid makes lookup by UUID O(1)
{
unique: true,
fields: ['uuid'],
},
// a hash index on the email makes lookup by email O(1)
{
unique: true,
fields: ['email'],
},
],
}
);
User.findByUUID = function (uuid) {
return this.findOne({ where: { uuid } });
};
User.findByEmail = function (email) {
return this.findOne({ where: { email } });
};
User.generateHash = function (password) {
return bcrypt.hash(password, HASH_ROUNDS);
};
User.generateAccessCode = function () {
return new Promise((resolve, reject) => {
crypto.randomBytes(16, (err, data) => {
if (err) return reject(err);
resolve(data.toString('hex'));
});
});
};
User.sanitize = function (user) {
user = _.pick(user, ['email', 'firstName', 'lastName']);
if (user.email) user.email = user.email.toLowerCase();
return user;
};
User.prototype.getPublicProfile = function () {
return {
firstName: this.getDataValue('firstName'),
lastName: this.getDataValue('lastName'),
picture: this.getDataValue('picture'),
};
};
User.prototype.getUserProfile = function () {
const uuid = this.getDataValue('uuid');
return {
uuid,
firstName: this.getDataValue('firstName'),
lastName: this.getDataValue('lastName'),
accessType: this.getDataValue('accessType'),
picture: `https://www.gravatar.com/avatar/${uuid.replace(
/[^0-9a-f]/g,
''
)}?d=identicon&s=300`,
email: this.getDataValue('email'),
};
};
User.prototype.verifyPassword = function (password) {
return bcrypt.compare(password, this.getDataValue('hash'));
};
User.prototype.isAdmin = function () {
return this.getDataValue('accessType') === 'ADMIN';
};
User.prototype.isStandard = function () {
return this.getDataValue('accessType') === 'STANDARD';
};
User.prototype.isRestricted = function () {
return this.getDataValue('accessType') === 'RESTRICTED';
};
User.prototype.isActive = function () {
return this.getDataValue('state') === 'ACTIVE';
};
User.prototype.isPending = function () {
return this.getDataValue('state') === 'PENDING';
};
User.prototype.isBlocked = function () {
return this.getDataValue('state') === 'BLOCKED';
};
User.prototype.requestedPasswordReset = function () {
return this.getDataValue('state') === 'PASSWORD_RESET';
};
return User;
};

View file

@ -0,0 +1,111 @@
/* eslint-disable consistent-return */
/* eslint-disable no-param-reassign */
/* eslint-disable func-names */
module.exports = (Sequelize, db) => {
const Address = db.define('address', {
// address ID: main way of querying the address
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
// street name
streetNumber: {
type: Sequelize.INTEGER,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Street number must be at least a character long',
},
notEmpty: {
msg: 'The street number is a required field',
},
},
},
// street name
streetName: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Street name must be at least 2 characters long',
},
notEmpty: {
msg: 'The street name is a required field',
},
},
},
// city
city: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'City name must be at least 2 characters long',
},
notEmpty: {
msg: 'The city name is a required field',
},
},
},
//country
country: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [2, 255],
msg: 'Country name must be at least 2 characters long',
},
notEmpty: {
msg: 'The country name is a required field',
},
},
},
// zip
zip: {
type: Sequelize.STRING,
allowNull: false,
validate: {
len: {
args: [1, 255],
msg: 'Zip must be at least a character long',
},
notEmpty: {
msg: 'The zip is a required field',
},
},
},
// apartment number
aptNumber: {
type: Sequelize.INTEGER,
allowNull: true,
validate: {
len: {
args: [1, 255],
msg: 'Apartment number must be at least a character long',
},
notEmpty: {
msg: 'The apartment number is a required field',
},
},
},
//patientID
patientId: {
type: Sequelize.INTEGER,
},
});
return Address;
};

View file

@ -0,0 +1,131 @@
openapi: 3.0.1
info:
title: server
description: HMS server-side code
version: 1.0.0
servers:
- url: /api/v1
tags:
- name: Examples
description: Simple example endpoints
- name: Specification
description: The swagger API specification
paths:
/examples:
get:
tags:
- Examples
description: Fetch all examples
responses:
200:
description: Return the example with the specified id
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Example'
4XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
5XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
tags:
- Examples
description: Create a new example
requestBody:
description: an example
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleBody'
required: true
responses:
201:
description: Return the example with the specified id
content:
application/json:
schema:
$ref: '#/components/schemas/Example'
4XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
5XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/examples/{id}:
get:
tags:
- Examples
parameters:
- name: id
in: path
description: The id of the example to retrieve
required: true
schema:
type: integer
responses:
200:
description: Return the example with the specified id
content:
application/json:
schema:
$ref: '#/components/schemas/Example'
4XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
5XX:
description: Example not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/spec:
get:
tags:
- Specification
responses:
200:
description: Return the API specification
content: {}
components:
schemas:
Example:
type: object
properties:
id:
type: integer
example: 3
name:
type: string
example: example 3
Error:
type: object
additionalProperties: true
ExampleBody:
title: example
required:
- name
type: object
properties:
name:
type: string
example: no_stress

View file

@ -0,0 +1,3 @@
import dotenv from 'dotenv';
dotenv.config();

View file

@ -0,0 +1,153 @@
/* eslint-disable no-unused-vars */
/* eslint-disable no-param-reassign */
import l from './logger';
/**
* This file defines error classes based on their semantic meaning. It abstracts away
* HTTP status codes so they can be used in a RESTful way without worrying about a
* consistent error interface.
*
* These classes descend from the base Error class, so they also automatically capture
* stack traces -- useful for debugging.
*/
/**
* Base error class
*
* Supports HTTP status codes and a custom message
*/
class HTTPError extends Error {
constructor(name, status, message) {
if (message === undefined) {
message = status;
status = name;
name = undefined;
}
super(message);
this.name = name || this.constructor.name;
this.status = status;
this.message = message;
}
}
class UserError extends HTTPError {
constructor(message) {
super(200, message || 'User Error');
}
}
class BadRequest extends HTTPError {
constructor(message) {
super(400, message || 'Bad Request');
}
}
class Unauthorized extends HTTPError {
constructor(message) {
super(401, message || 'Unauthorized');
}
}
class Forbidden extends HTTPError {
constructor(message) {
super(403, message || 'Permission denied');
}
}
class NotFound extends HTTPError {
constructor(message) {
super(404, message || 'Resource not found');
}
}
class Unprocessable extends HTTPError {
constructor(message) {
super(422, message || 'Unprocessable request');
}
}
class TooManyRequests extends HTTPError {
constructor(message) {
super(429, message);
}
}
class InternalServerError extends HTTPError {
constructor(message) {
super(500, message || 'Internal server error');
}
}
class NotImplemented extends HTTPError {
constructor(message) {
super(501, message || 'Not Implemented');
}
}
class NotAvailable extends HTTPError {
constructor(message) {
super(503, message);
}
}
/**
* General error handler middleware. Attaches to express so that throwing or calling next() with
* an error ends up here and all errors are handled uniformly.
*/
const errorHandler = (err, req, res, next) => {
if (!err) err = new InternalServerError('An unknown error occurred');
if (!err.status) err = new InternalServerError(err.message);
if (err.status < 500) {
l.info(
`${new Date()} [Flow ${req.id}]: ${err.name} [${err.status}]: ${
err.message
}`
);
} else {
l.info(`${new Date()} [Flow ${req.id}]: \n${err.stack}`);
}
res.status(err.status).json({
error: {
status: err.status,
message: err.message,
},
});
};
/**
* 404 errors aren't triggered by an error object, so this is a catch-all middleware
* for requests that don't hit a route.
*/
const notFoundHandler = (req, res, next) => {
const err = new NotFound(`The resource ${req.url} was not found`);
l.info(
`${new Date()} [Flow ${req.id}]: ${err.name} [${err.status}]: ${
err.message
}`
);
res.status(err.status).json({
error: {
status: err.status,
message: err.message,
},
});
};
module.exports = {
HTTPError,
UserError,
BadRequest,
Unauthorized,
Forbidden,
NotFound,
TooManyRequests,
InternalServerError,
NotImplemented,
NotAvailable,
errorHandler,
notFoundHandler,
};

View file

@ -0,0 +1,8 @@
import pino from 'pino';
const l = pino({
name: process.env.APP_ID,
level: process.env.LOG_LEVEL,
});
export default l;

View file

@ -0,0 +1,98 @@
import Express from 'express';
import cookieParser from 'cookie-parser';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as http from 'http';
import * as os from 'os';
import * as uuid from 'uuid';
import l from './logger';
import * as OpenApiValidator from 'express-openapi-validator';
import error from './error';
const db = require('../database/index');
const app = new Express();
export default class Server {
constructor() {
const root = path.normalize(`${__dirname}/../..`);
const apiSpec = path.join(__dirname, 'api.yml');
l.info(process.env.OPENAPI_ENABLE_RESPONSE_VALIDATION);
// const validateResponses = !!(
// process.env.OPENAPI_ENABLE_RESPONSE_VALIDATION &&
// process.env.OPENAPI_ENABLE_RESPONSE_VALIDATION.toLowerCase() === 'true'
// );
// enable CORS in development
// eslint-disable-next-line no-constant-condition
if (true) {
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization'
);
res.header(
'Access-Control-Allow-Methods',
'GET, PUT, POST, PATCH, DELETE, OPTIONS'
);
if (req.method.toLowerCase() === 'options') res.status(200).end();
else next();
});
}
// Assign a unique ID to each request
app.use((req, res, next) => {
req.id = uuid.v4().split('-').pop();
res.set('X-Flow-Id', req.id);
next();
});
app.set('appPath', `${root}client`);
app.use(bodyParser.json({ limit: process.env.REQUEST_LIMIT || '100kb' }));
app.use(
bodyParser.urlencoded({
extended: true,
limit: process.env.REQUEST_LIMIT || '100kb',
})
);
app.use(bodyParser.text({ limit: process.env.REQUEST_LIMIT || '100kb' }));
app.use(cookieParser(process.env.SESSION_SECRET));
app.use(Express.static(`${root}/public`));
app.use(process.env.OPENAPI_SPEC || '/spec', Express.static(apiSpec));
// app.use(
// OpenApiValidator.middleware({
// apiSpec,
// validateResponses,
// ignorePaths: /.*\/spec(\/|$)/,
// })
// );
}
router(routes) {
routes(app);
app.use(db.errorHandler);
app.use(error.errorHandler);
app.use(error.notFoundHandler);
db.setup();
return this;
}
listen(port = process.env.PORT) {
const welcome = (p) => () =>
l.info(
`up and running in ${
process.env.NODE_ENV || 'development'
} @: ${os.hostname()} on port: ${p}}`
);
http.createServer(app).listen(port, welcome(port));
return app;
}
}

View file

@ -0,0 +1,38 @@
module.exports = (User) =>
Promise.all([
User.create({
email: 'admin@codehall.ca',
accessType: 'ADMIN',
state: 'ACTIVE',
firstName: 'Anthony',
lastName: 'Aoun',
hash: '$2a$10$db7eYhWGZ1LZl27gvyX/iOgb33ji1PHY5.pPzRyXaNlbctCFWMF9G', // test1234
}),
User.create({
email: 'carey@codehall.ca',
accessType: 'STANDARD',
state: 'ACTIVE',
firstName: 'Carey',
lastName: 'Nachenberg',
hash: '$2a$10$db7eYhWGZ1LZl27gvyX/iOgb33ji1PHY5.pPzRyXaNlbctCFWMF9G', // test1234
}),
User.create({
email: 'joebruin@codehall.ca',
accessType: 'STANDARD',
state: 'ACTIVE',
firstName: 'Joe',
lastName: 'Bruin',
hash: '$2a$10$db7eYhWGZ1LZl27gvyX/iOgb33ji1PHY5.pPzRyXaNlbctCFWMF9G', // test1234
}),
User.create({
email: 'ram@codehall.ca',
accessType: 'STANDARD',
state: 'ACTIVE',
firstName: 'Ram',
lastName: 'Goli',
hash: '$2a$10$db7eYhWGZ1LZl27gvyX/iOgb33ji1PHY5.pPzRyXaNlbctCFWMF9G', // test1234
}),
]);

View file

@ -0,0 +1,99 @@
const Sequelize = require('sequelize');
const cls = require('continuation-local-storage');
const error = require('../common/error');
const devSetup = require('./dev-setup');
// The transaction namespace to use for transactions
const transactionNamespace = cls.createNamespace('default-transaction-ns');
Sequelize.useCLS(transactionNamespace);
const connection = new Sequelize(
'postgres://zwkdknae:p-QIQup9VeqYckSDI0dCpZyCEI9OT0VW@salt.db.elephantsql.com:5432/zwkdknae'
);
const User = require('../api/models/User')(Sequelize, connection);
const Patient = require('../api/models/Patient')(Sequelize, connection);
const Address = require('../api/models/Address')(Sequelize, connection);
const Division = require('../api/models/Division')(Sequelize, connection);
/**
* DB setup function to sync tables and add admin if doesn't exist
*/
const setup = (force, dev) =>
(dev
? connection.sync({ force }).then(() => devSetup(User))
: connection.sync({ force })
).then(() => {
User.findOrCreate({
where: { email: 'aaoun723@gmail.com' },
defaults: {
email: 'aaoun723@gmail.com',
accessType: 'ADMIN',
state: 'ACTIVE',
firstName: 'Codehall',
lastName: 'Admin',
hash: '$2a$10$db7eYhWGZ1LZl27gvyX/iOgb33ji1PHY5.pPzRyXaNlbctCFWMF9G',
},
});
Patient.findOrCreate({
where: { id: 1 },
defaults: {
firstName: 'John',
lastName: 'Steven',
telephone: '1223123112',
dateOfBirth: '01/23/1976',
gender: 'Male',
maritalStatus: 'Single',
externalDoctor: 'Doctor Smith',
divisionId: 1,
address: 'address',
},
});
Address.findOrCreate({
where: { id: 1 },
defaults: {
streetNumber: 122,
streetName: 'Elgin',
city: 'Ottawa',
country: 'Canada',
zip: 'k1f3a2',
},
});
Division.findOrCreate({
where: { id: 1 },
defaults: {
name: 'Ward Pheloneus',
location: 'East Wing',
numberOfBeds: 35,
telephone: 1234322323,
chargeNurseId: 1,
},
});
});
/**
* Handles database errors (separate from the general error handler and the 404 error handler)
*
* Specifically, it intercepts validation errors and presents them to the user in a readable
* manner. All other errors it lets fall through to the general error handler middleware.
*/
const errorHandler = (err, req, res, next) => {
if (!err || !(err instanceof Sequelize.Error)) return next(err);
if (err instanceof Sequelize.ValidationError) {
const message = `Validation Error: ${err.errors
.map((e) => e.message)
.join('; ')}`;
return next(new error.HTTPError(err.name, 422, message));
}
return next(new error.HTTPError(err.name, 500, err.message));
};
module.exports = {
connection,
User,
setup,
errorHandler,
};

5
server/server/index.js Normal file
View file

@ -0,0 +1,5 @@
import './common/env';
import Server from './common/server';
import routes from './routes';
export default new Server().router(routes).listen(process.env.PORT);

7
server/server/routes.js Normal file
View file

@ -0,0 +1,7 @@
import staffRouter from './api/contexts/staff/router';
import authRouter, { authenticated } from './api/contexts/auth/index';
export default function routes(app) {
app.use('/api/v1/staff', authenticated, staffRouter);
app.use('/api/v1/auth', authRouter);
}

View file

@ -0,0 +1,38 @@
import chai from 'chai';
import request from 'supertest';
import Server from '../server';
const expect = chai.expect;
describe('Examples', () => {
it('should get all examples', () =>
request(Server)
.get('/api/v1/examples')
.expect('Content-Type', /json/)
.then((r) => {
expect(r.body).to.be.an.an('array').of.length(2);
}));
it('should add a new example', () =>
request(Server)
.post('/api/v1/examples')
.send({ name: 'test' })
.expect('Content-Type', /json/)
.then((r) => {
expect(r.body)
.to.be.an.an('object')
.that.has.property('name')
.equal('test');
}));
it('should get an example by id', () =>
request(Server)
.get('/api/v1/examples/2')
.expect('Content-Type', /json/)
.then((r) => {
expect(r.body)
.to.be.an.an('object')
.that.has.property('name')
.equal('test');
}));
});