Added the files.
This commit is contained in:
commit
38ccdcbfe5
124 changed files with 32079 additions and 0 deletions
16
server/.babelrc
Normal file
16
server/.babelrc
Normal 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
3
server/.cfignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
/server
|
||||
/public
|
8
server/.env
Normal file
8
server/.env
Normal 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
5
server/.eslintignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
public/api-explorer/
|
||||
*.yaml
|
||||
*.yml
|
26
server/.eslintrc.json
Normal file
26
server/.eslintrc.json
Normal 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
3
server/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
.DS_Store
|
7
server/.nodemonrc.json
Normal file
7
server/.nodemonrc.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"watch": [
|
||||
"server/**/*.*",
|
||||
".env"
|
||||
],
|
||||
"ext": "js,json,mjs,yaml,yml"
|
||||
}
|
117
server/README.md
Normal file
117
server/README.md
Normal 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
7557
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
57
server/package.json
Normal file
57
server/package.json
Normal 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)"
|
||||
}
|
BIN
server/public/api-explorer/favicon-16x16.png
Normal file
BIN
server/public/api-explorer/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 B |
BIN
server/public/api-explorer/favicon-32x32.png
Normal file
BIN
server/public/api-explorer/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 628 B |
60
server/public/api-explorer/index.html
Normal file
60
server/public/api-explorer/index.html
Normal 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>
|
68
server/public/api-explorer/oauth2-redirect.html
Normal file
68
server/public/api-explorer/oauth2-redirect.html
Normal 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>
|
134
server/public/api-explorer/swagger-ui-bundle.js
Normal file
134
server/public/api-explorer/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
1
server/public/api-explorer/swagger-ui-bundle.js.map
Normal file
1
server/public/api-explorer/swagger-ui-bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
22
server/public/api-explorer/swagger-ui-standalone-preset.js
Normal file
22
server/public/api-explorer/swagger-ui-standalone-preset.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
server/public/api-explorer/swagger-ui.css
Normal file
4
server/public/api-explorer/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
1
server/public/api-explorer/swagger-ui.css.map
Normal file
1
server/public/api-explorer/swagger-ui.css.map
Normal file
File diff suppressed because one or more lines are too long
9
server/public/api-explorer/swagger-ui.js
Normal file
9
server/public/api-explorer/swagger-ui.js
Normal file
File diff suppressed because one or more lines are too long
1
server/public/api-explorer/swagger-ui.js.map
Normal file
1
server/public/api-explorer/swagger-ui.js.map
Normal file
File diff suppressed because one or more lines are too long
22
server/public/index.html
Normal file
22
server/public/index.html
Normal 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>
|
149
server/server/api/contexts/auth/index.js
Normal file
149
server/server/api/contexts/auth/index.js
Normal 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;
|
72
server/server/api/contexts/division/facade.js
Normal file
72
server/server/api/contexts/division/facade.js
Normal 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();
|
||||
},
|
||||
|
||||
|
||||
};
|
21
server/server/api/contexts/division/repository.js
Normal file
21
server/server/api/contexts/division/repository.js
Normal 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();
|
22
server/server/api/contexts/division/router.js
Normal file
22
server/server/api/contexts/division/router.js
Normal 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;
|
14
server/server/api/contexts/medication/facade.js
Normal file
14
server/server/api/contexts/medication/facade.js
Normal 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});
|
||||
|
||||
},
|
||||
|
||||
|
||||
};
|
13
server/server/api/contexts/medication/router.js
Normal file
13
server/server/api/contexts/medication/router.js
Normal 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;
|
65
server/server/api/contexts/patient/facade.js
Normal file
65
server/server/api/contexts/patient/facade.js
Normal 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();
|
||||
},
|
||||
};
|
21
server/server/api/contexts/patient/repository.js
Normal file
21
server/server/api/contexts/patient/repository.js
Normal 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();
|
21
server/server/api/contexts/patient/router.js
Normal file
21
server/server/api/contexts/patient/router.js
Normal 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;
|
119
server/server/api/contexts/staff/facade.js
Normal file
119
server/server/api/contexts/staff/facade.js
Normal 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);
|
||||
},
|
||||
};
|
21
server/server/api/contexts/staff/repository.js
Normal file
21
server/server/api/contexts/staff/repository.js
Normal 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();
|
19
server/server/api/contexts/staff/router.js
Normal file
19
server/server/api/contexts/staff/router.js
Normal 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;
|
5
server/server/api/middlewares/error.handler.js
Normal file
5
server/server/api/middlewares/error.handler.js
Normal 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 });
|
||||
}
|
98
server/server/api/models/AdmissionRequest.js
Normal file
98
server/server/api/models/AdmissionRequest.js
Normal 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;
|
||||
};
|
61
server/server/api/models/Bed.js
Normal file
61
server/server/api/models/Bed.js
Normal 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;
|
||||
};
|
127
server/server/api/models/Division.js
Normal file
127
server/server/api/models/Division.js
Normal 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;
|
||||
};
|
133
server/server/api/models/Medication.js
Normal file
133
server/server/api/models/Medication.js
Normal 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;
|
||||
};
|
162
server/server/api/models/Patient.js
Normal file
162
server/server/api/models/Patient.js
Normal 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;
|
||||
};
|
49
server/server/api/models/Room.js
Normal file
49
server/server/api/models/Room.js
Normal 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;
|
||||
};
|
208
server/server/api/models/User.js
Normal file
208
server/server/api/models/User.js
Normal 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;
|
||||
};
|
111
server/server/api/models/address.js
Normal file
111
server/server/api/models/address.js
Normal 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;
|
||||
};
|
131
server/server/common/api.yml
Normal file
131
server/server/common/api.yml
Normal 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
|
3
server/server/common/env.js
Normal file
3
server/server/common/env.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
153
server/server/common/error.js
Normal file
153
server/server/common/error.js
Normal 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,
|
||||
};
|
8
server/server/common/logger.js
Normal file
8
server/server/common/logger.js
Normal 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;
|
98
server/server/common/server.js
Normal file
98
server/server/common/server.js
Normal 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;
|
||||
}
|
||||
}
|
38
server/server/database/dev-setup.js
Normal file
38
server/server/database/dev-setup.js
Normal 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
|
||||
}),
|
||||
]);
|
99
server/server/database/index.js
Normal file
99
server/server/database/index.js
Normal 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
5
server/server/index.js
Normal 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
7
server/server/routes.js
Normal 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);
|
||||
}
|
38
server/test/examples.controller.js
Normal file
38
server/test/examples.controller.js
Normal 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');
|
||||
}));
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue