From ff0b56e671429ce6fb9058d9633334a65cf57504 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 25 Mar 2018 21:25:04 +0100 Subject: [PATCH 01/35] working on refactor --- .gitignore | 7 +- .nvmrc | 1 + .travis.yml | 5 +- Dockerfile | 25 +- docker-compose.yml | 19 +- gulpfile.js | 89 - package-lock.json | 6354 ++++++++++++++++++++++ package.json | 102 +- pm2.json | 12 - src/configurations/config.dev.json | 16 - src/configurations/config.test.json | 15 - src/configurations/index.ts | 32 - src/database.ts | 30 - src/database/index.ts | 106 + src/database/migrations/m.ts | 19 + src/entities/index.ts | 2 + src/entities/task.ts | 7 + src/entities/user.ts | 6 + src/errors.ts | 0 src/index.ts | 66 +- src/plugins/interfaces.ts | 19 - src/plugins/jwt-auth/index.ts | 50 - src/plugins/logger/index.ts | 42 - src/plugins/swagger/index.ts | 52 - src/repositories/task-repository.ts | 0 src/repositories/user-repository.ts | 18 + src/server.ts | 54 - src/server/health/controller.ts | 7 + src/server/health/index.ts | 12 + src/server/index.ts | 17 + src/server/middlewares/authentication.ts | 0 src/server/middlewares/cache.ts | 0 src/server/middlewares/logger.ts | 0 src/server/middlewares/response.ts | 0 src/server/middlewares/validator.ts | 0 src/server/tasks/index.ts | 0 src/server/users/index.ts | 0 src/tasks/index.ts | 8 - src/tasks/routes.ts | 143 - src/tasks/task-controller.ts | 86 - src/tasks/task-validator.ts | 12 - src/tasks/task.ts | 21 - src/users/index.ts | 8 - src/users/routes.ts | 136 - src/users/user-controller.ts | 79 - src/users/user-validator.ts | 20 - src/users/user.ts | 58 - test/index.ts | 7 + test/tasks/task-controller-tests.ts | 177 - test/users/users-controller-tests.ts | 159 - test/utils.ts | 63 - tsconfig.json | 7 +- tslint.json | 75 +- 53 files changed, 6686 insertions(+), 1557 deletions(-) create mode 100644 .nvmrc delete mode 100644 gulpfile.js create mode 100644 package-lock.json delete mode 100644 pm2.json delete mode 100644 src/configurations/config.dev.json delete mode 100644 src/configurations/config.test.json delete mode 100644 src/configurations/index.ts delete mode 100644 src/database.ts create mode 100644 src/database/index.ts create mode 100644 src/database/migrations/m.ts create mode 100644 src/entities/index.ts create mode 100644 src/entities/task.ts create mode 100644 src/entities/user.ts create mode 100644 src/errors.ts delete mode 100644 src/plugins/interfaces.ts delete mode 100644 src/plugins/jwt-auth/index.ts delete mode 100644 src/plugins/logger/index.ts delete mode 100644 src/plugins/swagger/index.ts create mode 100644 src/repositories/task-repository.ts create mode 100644 src/repositories/user-repository.ts delete mode 100644 src/server.ts create mode 100644 src/server/health/controller.ts create mode 100644 src/server/health/index.ts create mode 100644 src/server/index.ts create mode 100644 src/server/middlewares/authentication.ts create mode 100644 src/server/middlewares/cache.ts create mode 100644 src/server/middlewares/logger.ts create mode 100644 src/server/middlewares/response.ts create mode 100644 src/server/middlewares/validator.ts create mode 100644 src/server/tasks/index.ts create mode 100644 src/server/users/index.ts delete mode 100644 src/tasks/index.ts delete mode 100644 src/tasks/routes.ts delete mode 100644 src/tasks/task-controller.ts delete mode 100644 src/tasks/task-validator.ts delete mode 100644 src/tasks/task.ts delete mode 100644 src/users/index.ts delete mode 100644 src/users/routes.ts delete mode 100644 src/users/user-controller.ts delete mode 100644 src/users/user-validator.ts delete mode 100644 src/users/user.ts create mode 100644 test/index.ts delete mode 100644 test/tasks/task-controller-tests.ts delete mode 100644 test/users/users-controller-tests.ts delete mode 100644 test/utils.ts diff --git a/.gitignore b/.gitignore index b7d9b93..f9c370d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,9 +32,6 @@ node_modules #VSCode .vscode -#Ignore build folder -build - -#Ignore typings -typings +#Ignore dist folder +dist diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..8d04a0f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +8.10 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 48d201e..82a60b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,5 @@ language: node_js node_js: - - "7" -before_script: - - npm install -g typescript - - npm install -g gulp + - "8" services: - mongodb \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index de1c42e..f0401ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,12 @@ -FROM mhart/alpine-node:6.9.1 +FROM node:8-alpine -MAINTAINER Talento90 - -# create a specific user to run this container -RUN adduser -S -D user-app - -# add files to container -ADD . /app +USER nobody # specify the working directory WORKDIR app -RUN chmod -R 777 . - -# build process -RUN npm install -RUN npm run build -RUN npm prune --production - -# run the container using a specific user -USER user-app - -EXPOSE 8080 +# expose server and debug port +EXPOSE 8080 5858 # run application -CMD ["npm", "start"] \ No newline at end of file +CMD ["node", "dist/src/index.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 5a7f918..18a6f50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,20 @@ -version: '2' +version: '2.1' services: app: build: . environment: - PORT=8080 - - MONGO_URL=mongodb:27017 - ports: + - MYSQL_URL=mysql + ports: - "8080:8080" + - "5858:5858" links: - - mongodb - mongodb: - image: mongo:latest + - mysql + volumes: + - .:/app/ + mysql: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: secret ports: - - "27017:27017" \ No newline at end of file + - "3306:3306" diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 7843a7a..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -const gulp = require('gulp'); -const rimraf = require('gulp-rimraf'); -const tslint = require('gulp-tslint'); -const mocha = require('gulp-mocha'); -const shell = require('gulp-shell'); -const env = require('gulp-env'); - -/** - * Remove build directory. - */ -gulp.task('clean', function () { - return gulp.src(outDir, { read: false }) - .pipe(rimraf()); -}); - -/** - * Lint all custom TypeScript files. - */ -gulp.task('tslint', () => { - return gulp.src('src/**/*.ts') - .pipe(tslint( { - formatter: 'prose' - })) - .pipe(tslint.report()); -}); - -/** - * Compile TypeScript. - */ - -function compileTS(args, cb) { - return exec(tscCmd + args, (err, stdout, stderr) => { - console.log(stdout); - - if (stderr) { - console.log(stderr); - } - cb(err); - }); -} - -gulp.task('compile', shell.task([ - 'npm run tsc', -])) - -/** - * Watch for changes in TypeScript - */ -gulp.task('watch', shell.task([ - 'npm run tsc-watch', -])) -/** - * Copy config files - */ -gulp.task('configs', (cb) => { - return gulp.src("src/configurations/*.json") - .pipe(gulp.dest('./build/src/configurations')); -}); - -/** - * Build the project. - */ -gulp.task('build', ['tslint', 'compile', 'configs'], () => { - console.log('Building the project ...'); -}); - -/** - * Run tests. - */ -gulp.task('test', ['build'], (cb) => { - const envs = env.set({ - NODE_ENV: 'test' - }); - - gulp.src(['build/test/**/*.js']) - .pipe(envs) - .pipe(mocha()) - .once('error', (error) => { - console.log(error); - process.exit(1); - }) - .once('end', () => { - process.exit(); - }); -}); - -gulp.task('default', ['build']); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b433c1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6354 @@ +{ + "name": "typescript-node", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "9.6.0" + } + }, + "@types/async": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.48.tgz", + "integrity": "sha512-yEyX/iGm9MWTV0JZ4oKEDf00lrehj7IkrZBhoBSNyQW8owSeV1Ule4ceo+lojao43v8grdEDekjNLghn7QyJWA==" + }, + "@types/bluebird": { + "version": "3.5.20", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.20.tgz", + "integrity": "sha512-Wk41MVdF+cHBfVXj/ufUHJeO3BlIQr1McbHZANErMykaCWeDSZbH5erGjNBw2/3UlRdSxZbLfSuQTzFmPOYFsA==", + "dev": true + }, + "@types/body-parser": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.16.8.tgz", + "integrity": "sha512-BdN2PXxOFnTXFcyONPW6t0fHjz2fvRZHVMFpaS0wYr+Y8fWEaNOs4V8LEu/fpzQlMx+ahdndgTaGTwPC+J/EeA==", + "dev": true, + "requires": { + "@types/express": "4.11.1", + "@types/node": "9.6.0" + } + }, + "@types/chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.2.tgz", + "integrity": "sha512-D8uQwKYUw2KESkorZ27ykzXgvkDJYXVEihGklgfp5I4HUP8D6IxtcdLTMB1emjQiWzV7WZ5ihm1cxIzVwjoleQ==", + "dev": true + }, + "@types/connect": { + "version": "3.4.31", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.31.tgz", + "integrity": "sha512-OPSxsP6XqA3984KWDUXq/u05Hu8VWa/2rUVlw/aDUOx87BptIep6xb3NdCxCpKLfLdjZcCE5jR+gouTul3gjdA==", + "dev": true, + "requires": { + "@types/node": "9.6.0" + } + }, + "@types/cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha512-ku6IvbucEyuC6i4zAVK/KnuzWNXdbFd1HkXlNLg/zhWDGTtQT5VhumiPruB/BHW34PWVFwyfwGftDQHfWNxu3Q==", + "dev": true, + "requires": { + "@types/connect": "3.4.31", + "@types/express": "4.11.1", + "@types/keygrip": "1.0.1", + "@types/node": "9.6.0" + } + }, + "@types/events": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", + "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "dev": true + }, + "@types/express": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.11.1.tgz", + "integrity": "sha512-ttWle8cnPA5rAelauSWeWJimtY2RsUf2aspYZs7xPHiWgOlPn6nnUfBMtrkcnjFJuIHJF4gNOdVvpLK2Zmvh6g==", + "dev": true, + "requires": { + "@types/body-parser": "1.16.8", + "@types/express-serve-static-core": "4.11.1", + "@types/serve-static": "1.13.1" + } + }, + "@types/express-serve-static-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz", + "integrity": "sha512-EehCl3tpuqiM8RUb+0255M8PhhSwTtLfmO7zBBdv0ay/VTd/zmrqDfQdZFsa5z/PVMbH2yCMZPXsnrImpATyIw==", + "dev": true, + "requires": { + "@types/events": "1.2.0", + "@types/node": "9.6.0" + } + }, + "@types/helmet": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.37.tgz", + "integrity": "sha512-E45vdnx+7+HIN5jsywhzfd+hUI/2yBFr6RT7tsMVrwp+uTvyVANBf4dyVUNW/+ZqAvcx23t2YtGTndQJR3tXIA==", + "dev": true, + "requires": { + "@types/express": "4.11.1" + } + }, + "@types/http-assert": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.2.2.tgz", + "integrity": "sha512-x1553BFcgBVOD6y3tC/2SOVcNaf6b9eH3BvqniAZZwmHWYhyr8ffXTYygQC/hQxNI4Yfc3q0gGUvyiAHV4tRNg==", + "dev": true + }, + "@types/joi": { + "version": "13.0.7", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.0.7.tgz", + "integrity": "sha512-x7VMOrIfpqo0pMi5bIuRE+3RwMNlzE3HZLrEpebW2JmuQXeIX69/G8R90Ibs1i/gb1YvBoSlO4pMwH0VUmclGw==", + "dev": true + }, + "@types/keygrip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", + "integrity": "sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg=", + "dev": true + }, + "@types/knex": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.14.9.tgz", + "integrity": "sha512-xRgDC4kowq5+LBLVZZoNCCK4WGmRwEswMVO0FTqjSNjgn1enfT05RGx6RWRBliql0NdLngTVa2PTuY92+Emfmw==", + "dev": true, + "requires": { + "@types/bluebird": "3.5.20", + "@types/events": "1.2.0", + "@types/node": "9.6.0" + } + }, + "@types/koa": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.0.44.tgz", + "integrity": "sha512-xOg6XLJdKmYriExAF0pV+HYhzftNJbpxplJgjyCwEM2LNLQPMgW4uSHXjgsqSPKsaAgM8CiR31vIeocRibFSjw==", + "dev": true, + "requires": { + "@types/accepts": "1.3.5", + "@types/cookies": "0.7.1", + "@types/events": "1.2.0", + "@types/http-assert": "1.2.2", + "@types/keygrip": "1.0.1", + "@types/koa-compose": "3.2.2", + "@types/node": "9.6.0" + } + }, + "@types/koa-bodyparser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz", + "integrity": "sha512-E0DCU2jBpWniwtjYVfNOyUwzJzEQUPH8tYm02SRjLI2j3xByonCv1sDr56xHgjRJqOOseTXiobsA2iwsYNbNaA==", + "dev": true, + "requires": { + "@types/koa": "2.0.44" + } + }, + "@types/koa-compose": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.2.tgz", + "integrity": "sha1-3BBuAAu/kqOskA91bfRzRIh+6Ec=", + "dev": true + }, + "@types/koa-helmet": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/koa-helmet/-/koa-helmet-3.1.2.tgz", + "integrity": "sha512-k2qE1UIFHO3MbXX527xh1KFy7sIGaCm6EG0qMiwbFtEQDGMU0QPe6cyNKbwlSHtndLzPnraJT5KylvdvZZkdKA==", + "dev": true, + "requires": { + "@types/helmet": "0.0.37", + "@types/koa": "2.0.44" + } + }, + "@types/koa-router": { + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/@types/koa-router/-/koa-router-7.0.27.tgz", + "integrity": "sha512-merzwkV5JD+X72K7Rvdoz6s62rIZFUAdld5cAnjmNEcJ7t/xZWI1+DxQJ79SfBXToPivHmYiUbD8e48EVMdXtw==", + "dev": true, + "requires": { + "@types/koa": "2.0.44" + } + }, + "@types/mime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.0.tgz", + "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", + "dev": true + }, + "@types/mocha": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.0.0.tgz", + "integrity": "sha512-ZS0vBV7Jn5Z/Q4T3VXauEKMDCV8nWOtJJg90OsDylkYJiQwcWtKuLzohWzrthBkerUF7DLMmJcwOPEP0i/AOXw==", + "dev": true + }, + "@types/node": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.0.tgz", + "integrity": "sha512-h3YZbOq2+ZoDFI1z8Zx0Ck/xRWkOESVaLdgLdd/c25mMQ1Y2CAkILu9ny5A15S5f32gGcQdaUIZ2jzYr8D7IFg==", + "dev": true + }, + "@types/pino": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-4.7.1.tgz", + "integrity": "sha512-jatsMBAonuhTB1NhJsbDjvw7p71q3AvHii276OifMQ1RAE9NaX1EEmcG1lxONA1bQIYKjJbTJIj5UHicnYYDPA==", + "dev": true, + "requires": { + "@types/events": "1.2.0", + "@types/node": "9.6.0" + } + }, + "@types/serve-static": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.1.tgz", + "integrity": "sha512-jDMH+3BQPtvqZVIcsH700Dfi8Q3MIcEx16g/VdxjoqiGR/NntekB10xdBpirMKnPe9z2C5cBmL0vte0YttOr3Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "4.11.1", + "@types/mime": "2.0.0" + } + }, + "@types/sinon": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.0.tgz", + "integrity": "sha512-rvgY5bK5ZBRJPuJF0vJI+NC2gt+lakobTa8pnDS/oRH2gk/tooeDEel8piZA8Ng6pxq0A5QGzilIFSyashP6jw==", + "dev": true + }, + "@types/superagent": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.5.7.tgz", + "integrity": "sha512-zvDPQHA/M8f8SqU3jgQ0PFgaO0FV+IarGwhRm8dy0CIPRi5on187IhkOJFTUT3O03C/vqjZ3jlhOichhw3RVng==", + "dev": true, + "requires": { + "@types/node": "9.6.0" + } + }, + "@types/supertest": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.4.tgz", + "integrity": "sha512-0TvOJ+6XVMSImgqc2ClNllfVffCxHQhFbsbwOGzGTjdFydoaG052LPqnP8SnmSlnokOcQiPPcbz+Yi30LxWPyA==", + "dev": true, + "requires": { + "@types/superagent": "3.5.7" + } + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "1.9.1" + } + }, + "ansicolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "requires": { + "lodash": "4.17.5" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", + "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + } + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", + "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "cardinal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "requires": { + "ansicolors": "0.2.1", + "redeyed": "1.0.1" + } + }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "ci-info": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", + "integrity": "sha512-SK/846h/Rcy8q9Z9CAwGBLfCJ6EkjJWdpelWDufQpqVDYq2Wnnv8zlSO6AMQap02jvhVruKKpEtQOufo3pFhLg==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-5.1.1.tgz", + "integrity": "sha1-2XeB0eM0S6SoIP0YBr3fg0FQUjY=", + "requires": { + "inflation": "2.0.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "1.6.16" + } + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-security-policy-builder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.0.0.tgz", + "integrity": "sha512-j+Nhmj1yfZAikJLImCvPJFE29x/UuBi+/MWqggGGc515JKaZrjuei2RhULJmy0MsstW3E3htl002bwmBNMKr7w==" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookiejar": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", + "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=", + "dev": true + }, + "cookies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.1.tgz", + "integrity": "sha1-fIphX1SBxhq58WyDNzG8uPZjuZs=", + "requires": { + "depd": "1.1.2", + "keygrip": "1.0.2" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, + "core-js": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", + "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-4.0.0.tgz", + "integrity": "sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==", + "dev": true, + "requires": { + "is-directory": "0.3.1", + "js-yaml": "3.11.0", + "parse-json": "4.0.0", + "require-from-string": "2.0.1" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "denque": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.2.3.tgz", + "integrity": "sha512-BOjyD1zPf7gqgXlXBCnCsz84cbRNfqpQNvWOUiw3Onu9s7a2afW2LyHzctoie/2KELfUoZkNHTnW02C3hCU20w==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "dns-prefetch-control": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.1.0.tgz", + "integrity": "sha1-YN20V3dOF48flBXwyrsOhbCzALI=" + }, + "dont-sniff-mimetype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.0.0.tgz", + "integrity": "sha1-WTKJDcn04vGeXrAqIAJuXl78j1g=" + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "1.4.0" + } + }, + "error-ex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-plugin-prettier": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.6.0.tgz", + "integrity": "sha512-floiaI4F7hRkTrFe8V2ItOK97QYrX75DjmdzmVITZoAP6Cn06oEDPQRsO6MlHEP/u2SxI3xQ52Kpjw6j5WGfeQ==", + "dev": true, + "requires": { + "fast-diff": "1.1.2", + "jest-docblock": "21.2.0" + } + }, + "esprima": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true, + "requires": { + "duplexer": "0.1.1", + "from": "0.1.7", + "map-stream": "0.1.0", + "pause-stream": "0.0.11", + "split": "0.3.3", + "stream-combiner": "0.0.4", + "through": "2.3.8" + } + }, + "execa": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz", + "integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "expect-ct": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.1.0.tgz", + "integrity": "sha1-UnNWeN4YUwiQ2Ne5XwrGNkCVgJQ=" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, + "fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" + }, + "fast-safe-stringify": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz", + "integrity": "sha512-QJYT/i0QYoiZBQ71ivxdyTqkwKkQ0oxACXHYxH2zYHJEgzi2LsbjgvtzTbLi1SZcF190Db2YP7I7eTsU2egOlw==" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "requires": { + "detect-file": "1.0.0", + "is-glob": "3.1.0", + "micromatch": "3.1.10", + "resolve-dir": "1.0.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.2" + } + }, + "flagged-respawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz", + "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=" + }, + "flatstr": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.5.tgz", + "integrity": "sha1-W0UbCMvUji6sVKK74L9GFlqhS+M=" + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "1.0.2" + } + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "formidable": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", + "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "dev": true + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "0.2.2" + } + }, + "frameguard": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.0.0.tgz", + "integrity": "sha1-e8rUae57lukdEs6zlZx4I1qScuk=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=" + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "requires": { + "global-prefix": "1.0.2", + "is-windows": "1.0.2", + "resolve-dir": "1.0.1" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "requires": { + "expand-tilde": "2.0.2", + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "1.0.2", + "which": "1.3.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "helmet": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.12.0.tgz", + "integrity": "sha512-CgkctpvreQLL6X3EL2Igs/92+75ZFIsrob9/Rdwf2hQCBGH/DxLk4xFPxAAl6jYnnus/YXfFEVXHEJf8TJTwlA==", + "requires": { + "dns-prefetch-control": "0.1.0", + "dont-sniff-mimetype": "1.0.0", + "expect-ct": "0.1.0", + "frameguard": "3.0.0", + "helmet-csp": "2.7.0", + "hide-powered-by": "1.0.0", + "hpkp": "2.0.0", + "hsts": "2.1.0", + "ienoopen": "1.0.0", + "nocache": "2.0.0", + "referrer-policy": "1.1.0", + "x-xss-protection": "1.1.0" + } + }, + "helmet-csp": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.7.0.tgz", + "integrity": "sha512-IGIAkWnxjRbgMXFA2/kmDqSIrIaSfZ6vhMHlSHw7jm7Gm9nVVXqwJ2B1YEpYrJsLrqY+w2Bbimk7snux9+sZAw==", + "requires": { + "camelize": "1.0.0", + "content-security-policy-builder": "2.0.0", + "dasherize": "2.0.0", + "lodash.reduce": "4.6.0", + "platform": "1.3.5" + } + }, + "hide-powered-by": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.0.0.tgz", + "integrity": "sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=" + }, + "hoek": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz", + "integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw==" + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "1.0.0" + } + }, + "hosted-git-info": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", + "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", + "dev": true + }, + "hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.1.0.tgz", + "integrity": "sha512-zXhh/DqgrTXJ7erTN6Fh5k/xjMhDGXCqdYN3wvxUvGUQvnxcFfUd8E+6vLg/nk3ss1TYMb+DhRl25fYABioTvA==" + }, + "http-assert": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.3.0.tgz", + "integrity": "sha1-oxpc+IyHPsu1eWkH1NbxMujAHko=", + "requires": { + "deep-equal": "1.0.1", + "http-errors": "1.6.2" + } + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.4.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + } + } + }, + "husky": { + "version": "0.15.0-rc.13", + "resolved": "https://registry.npmjs.org/husky/-/husky-0.15.0-rc.13.tgz", + "integrity": "sha512-J9bDyA3vllcIDPmYquNMuklEWKoHEhjqA3YG23Pic130ZueTks23JcjlVwMxWnf4dOjqEadwYFxG3svLFXZhYA==", + "dev": true, + "requires": { + "cosmiconfig": "4.0.0", + "execa": "0.9.0", + "is-ci": "1.1.0", + "pkg-dir": "2.0.0", + "pupa": "1.0.0", + "read-pkg": "3.0.0", + "run-node": "0.2.0", + "slash": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "ienoopen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.0.0.tgz", + "integrity": "sha1-NGpCj0dKrI9QzzeE6i0PFvYr2ms=" + }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "1.0.0", + "is-windows": "1.0.2" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-ci": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz", + "integrity": "sha512-c7TnwxLePuqIlxHgr7xtxzycJPegNHFuIrBkwbf8hc58//+Op1CqFkyS+xnIMkwn9UsJIwc174BIjkyBmSpjKg==", + "dev": true, + "requires": { + "ci-info": "1.1.3" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "6.0.2" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-odd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", + "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "1.0.0" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isemail": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.1.tgz", + "integrity": "sha512-mVjAjvdPkpwXW61agT2E9AkGoegZO7SdJGCezWwxnETL58f5KwJ4vSVAMBUL5idL6rTlYAIGkX3n4suiviMLNw==", + "requires": { + "punycode": "2.1.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "jest-docblock": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", + "integrity": "sha512-5IZ7sY9dBAYSV+YjQ0Ovb540Ku7AO9Z5o2Cg789xj167iQuZ2cG+z0f3Uct6WeYLbU6aQiM2pCs7sZ+4dotydw==", + "dev": true + }, + "joi": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.1.2.tgz", + "integrity": "sha512-bZZSQYW5lPXenOfENvgCBPb9+H6E6MeNWcMtikI04fKphj5tvFL9TOb+H2apJzbCrRw/jebjTH8z6IHLpBytGg==", + "requires": { + "hoek": "5.0.3", + "isemail": "3.1.1", + "topo": "3.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.11.0.tgz", + "integrity": "sha512-saJstZWv7oNeOyBh3+Dx1qWzhW0+e6/8eDzo7p5rDFqxntSztloLtuKu+Ejhtq82jsilwOIZYsCz+lIjthg1Hw==", + "dev": true, + "requires": { + "argparse": "1.0.10", + "esprima": "4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + } + } + }, + "json-parse-better-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", + "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", + "dev": true + }, + "just-extend": { + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", + "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", + "dev": true + }, + "keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "knex": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.14.4.tgz", + "integrity": "sha1-2XS34DVSRCYMcDcxyNkPZiv+eYo=", + "requires": { + "babel-runtime": "6.26.0", + "bluebird": "3.5.1", + "chalk": "2.3.0", + "commander": "2.15.1", + "debug": "3.1.0", + "inherits": "2.0.3", + "interpret": "1.1.0", + "liftoff": "2.5.0", + "lodash": "4.17.5", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "pg-connection-string": "2.0.0", + "readable-stream": "2.3.3", + "safe-buffer": "5.1.1", + "tarn": "1.1.4", + "tildify": "1.2.0", + "uuid": "3.2.1", + "v8flags": "3.0.2" + } + }, + "koa": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.5.0.tgz", + "integrity": "sha512-UkrbMW2mRNfoW/4I20knJEjtPAWCV3Iw6f4XdnPWjHsCN8iTeSh0eSutrYdL0fGF/G9on2eQ30EEQif0MarGJA==", + "requires": { + "accepts": "1.3.5", + "content-disposition": "0.5.2", + "content-type": "1.0.4", + "cookies": "0.7.1", + "debug": "3.1.0", + "delegates": "1.0.0", + "depd": "1.1.2", + "destroy": "1.0.4", + "error-inject": "1.0.0", + "escape-html": "1.0.3", + "fresh": "0.5.2", + "http-assert": "1.3.0", + "http-errors": "1.6.2", + "is-generator-function": "1.0.7", + "koa-compose": "4.0.0", + "koa-convert": "1.2.0", + "koa-is-json": "1.0.0", + "mime-types": "2.1.18", + "on-finished": "2.3.0", + "only": "0.0.2", + "parseurl": "1.3.2", + "statuses": "1.4.0", + "type-is": "1.6.16", + "vary": "1.1.2" + } + }, + "koa-bodyparser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.2.0.tgz", + "integrity": "sha1-vObgi8Zfhwm20fqpQRx/DYk4qlQ=", + "requires": { + "co-body": "5.1.1", + "copy-to": "2.0.1" + } + }, + "koa-compose": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.0.0.tgz", + "integrity": "sha1-KAClE9nDYe8NY4UrA45Pby1adzw=" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "4.6.0", + "koa-compose": "3.2.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "koa-helmet": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/koa-helmet/-/koa-helmet-3.3.0.tgz", + "integrity": "sha512-kuUjVpCy8gjJkXO0JYVX8tYvX8vTVOCdogLNHuPczRaITbmd8r7EBcNrk8GzBPVQE0ZOwDUqCSaNZ6vZwsL+wA==", + "requires": { + "helmet": "3.12.0" + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, + "koa-router": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", + "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", + "requires": { + "debug": "3.1.0", + "http-errors": "1.6.2", + "koa-compose": "3.2.1", + "methods": "1.1.2", + "path-to-regexp": "1.7.0", + "urijs": "1.19.1" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "1.3.0" + } + } + } + }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "requires": { + "extend": "3.0.1", + "findup-sync": "2.0.0", + "fined": "1.1.0", + "flagged-respawn": "1.0.0", + "is-plain-object": "2.0.4", + "object.map": "1.0.1", + "rechoir": "0.6.2", + "resolve": "1.6.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=" + }, + "lolex": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz", + "integrity": "sha512-A5pN2tkFj7H0dGIAM6MFvHKMJcPnjZsOMvR7ujCjfgW5TbV6H9vb1PgxLtHvjqNZTHsUolz+6/WEO0N1xNx2ng==", + "dev": true + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.0.tgz", + "integrity": "sha1-V7713IXSOSO6I3ZzJNjo+PPZaUs=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "1.0.1" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mocha": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.5.tgz", + "integrity": "sha512-3MM3UjZ5p8EJrYpG7s+29HAI9G7sTzKEe4+w37Dg0QP7qL4XGsV+Q2xet2cE37AqdgN1OtYQB6Vl98YiPV3PgA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + }, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "mysql2": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-1.5.3.tgz", + "integrity": "sha512-Oov36YQSeciNP9SeqE7je4eWgeGADOorXLmsqhtxOJmPGUOJSNJT0s6/eq1Byy4nhXTRQUvlMHsI4Q/eMEs88Q==", + "requires": { + "cardinal": "1.0.0", + "denque": "1.2.3", + "generate-function": "2.0.0", + "iconv-lite": "0.4.19", + "long": "4.0.0", + "lru-cache": "4.1.1", + "named-placeholders": "1.1.1", + "object-assign": "4.1.1", + "readable-stream": "2.3.5", + "safe-buffer": "5.1.1", + "seq-queue": "0.0.5", + "sqlstring": "2.3.1" + }, + "dependencies": { + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + } + } + }, + "named-placeholders": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.1.tgz", + "integrity": "sha1-O3oNJiA910s6nfTJz7gnsvuQfmQ=", + "requires": { + "lru-cache": "2.5.0" + }, + "dependencies": { + "lru-cache": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.0.tgz", + "integrity": "sha1-2COIrpyWC+y+oMc7uet5tsbOmus=" + } + } + }, + "nanomatch": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", + "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.3.2.tgz", + "integrity": "sha512-KPKb+wvETBiwb4eTwtR/OsA2+iijXP+VnlSFYJo3EHjm2yjek1NWxHOUQat3i7xNLm1Bm18UA5j5Wor0yO2GtA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "2.0.0", + "just-extend": "1.1.27", + "lolex": "2.3.2", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, + "nocache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", + "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "nyc": { + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.6.0.tgz", + "integrity": "sha512-ZaXCh0wmbk2aSBH2B5hZGGvK2s9aM8DIm2rVY+BG3Fx8tUS+bpJSswUVZqOD1YfCmnYRFSqgYJSr7UeeUcW0jg==", + "dev": true, + "requires": { + "archy": "1.0.0", + "arrify": "1.0.1", + "caching-transform": "1.0.1", + "convert-source-map": "1.5.1", + "debug-log": "1.0.1", + "default-require-extensions": "1.0.0", + "find-cache-dir": "0.1.1", + "find-up": "2.1.0", + "foreground-child": "1.5.6", + "glob": "7.1.2", + "istanbul-lib-coverage": "1.2.0", + "istanbul-lib-hook": "1.1.0", + "istanbul-lib-instrument": "1.10.1", + "istanbul-lib-report": "1.1.3", + "istanbul-lib-source-maps": "1.2.3", + "istanbul-reports": "1.3.0", + "md5-hex": "1.3.0", + "merge-source-map": "1.1.0", + "micromatch": "2.3.11", + "mkdirp": "0.5.1", + "resolve-from": "2.0.0", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "spawn-wrap": "1.4.2", + "test-exclude": "4.2.1", + "yargs": "11.1.0", + "yargs-parser": "8.1.0" + }, + "dependencies": { + "align-text": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "amdefine": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true, + "dev": true + }, + "append-transform": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "requires": { + "default-require-extensions": "1.0.0" + } + }, + "archy": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "arrify": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "async": { + "version": "1.5.2", + "bundled": true, + "dev": true + }, + "atob": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "babel-generator": { + "version": "6.26.1", + "bundled": true, + "dev": true, + "requires": { + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "detect-indent": "4.0.0", + "jsesc": "1.3.0", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.26.0" + } + }, + "babel-runtime": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "core-js": "2.5.3", + "regenerator-runtime": "0.11.1" + } + }, + "babel-template": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "lodash": "4.17.5" + } + }, + "babel-traverse": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "babel-messages": "6.23.0", + "babel-runtime": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "debug": "2.6.9", + "globals": "9.18.0", + "invariant": "2.2.3", + "lodash": "4.17.5" + } + }, + "babel-types": { + "version": "6.26.0", + "bundled": true, + "dev": true, + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.5", + "to-fast-properties": "1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "bundled": true, + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "base": { + "version": "0.11.2", + "bundled": true, + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.1", + "pascalcase": "0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "bundled": true, + "dev": true, + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true, + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "caching-transform": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "md5-hex": "1.3.0", + "mkdirp": "0.5.1", + "write-file-atomic": "1.3.4" + } + }, + "camelcase": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "class-utils": { + "version": "0.3.6", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "cliui": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, + "commondir": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "bundled": true, + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "convert-source-map": { + "version": "1.5.1", + "bundled": true, + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "core-js": { + "version": "2.5.3", + "bundled": true, + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "which": "1.3.0" + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "debug-log": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "decamelize": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "default-require-extensions": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "strip-bom": "2.0.0" + } + }, + "define-property": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "1.0.2", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "detect-indent": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "repeating": "2.0.1" + } + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "esutils": { + "version": "2.0.2", + "bundled": true, + "dev": true + }, + "execa": { + "version": "0.7.0", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "bundled": true, + "dev": true, + "requires": { + "lru-cache": "4.1.2", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "bundled": true, + "dev": true, + "requires": { + "fill-range": "2.2.3" + } + }, + "extend-shallow": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "filename-regex": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "bundled": true, + "dev": true, + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, + "find-cache-dir": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "requires": { + "commondir": "1.0.1", + "mkdirp": "0.5.1", + "pkg-dir": "1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "for-own": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "requires": { + "for-in": "1.0.2" + } + }, + "foreground-child": { + "version": "1.5.6", + "bundled": true, + "dev": true, + "requires": { + "cross-spawn": "4.0.2", + "signal-exit": "3.0.2" + } + }, + "fragment-cache": { + "version": "0.2.1", + "bundled": true, + "dev": true, + "requires": { + "map-cache": "0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "get-caller-file": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "get-value": { + "version": "2.0.6", + "bundled": true, + "dev": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "glob-base": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-glob": "2.0.1" + } + }, + "globals": { + "version": "9.18.0", + "bundled": true, + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "bundled": true, + "dev": true, + "requires": { + "async": "1.5.2", + "optimist": "0.6.1", + "source-map": "0.4.4", + "uglify-js": "2.8.29" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "amdefine": "1.0.1" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "has-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "hosted-git-info": { + "version": "2.6.0", + "bundled": true, + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "invariant": { + "version": "2.2.3", + "bundled": true, + "dev": true, + "requires": { + "loose-envify": "1.3.1" + } + }, + "invert-kv": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "is-descriptor": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extglob": "1.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-odd": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-number": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "bundled": true, + "dev": true + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "istanbul-lib-coverage": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "istanbul-lib-hook": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "append-transform": "0.4.0" + } + }, + "istanbul-lib-instrument": { + "version": "1.10.1", + "bundled": true, + "dev": true, + "requires": { + "babel-generator": "6.26.1", + "babel-template": "6.26.0", + "babel-traverse": "6.26.0", + "babel-types": "6.26.0", + "babylon": "6.18.0", + "istanbul-lib-coverage": "1.2.0", + "semver": "5.5.0" + } + }, + "istanbul-lib-report": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "requires": { + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "path-parse": "1.0.5", + "supports-color": "3.2.3" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "bundled": true, + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "1.2.3", + "bundled": true, + "dev": true, + "requires": { + "debug": "3.1.0", + "istanbul-lib-coverage": "1.2.0", + "mkdirp": "0.5.1", + "rimraf": "2.6.2", + "source-map": "0.5.7" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, + "istanbul-reports": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "handlebars": "4.0.11" + } + }, + "js-tokens": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "jsesc": { + "version": "1.3.0", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "bundled": true, + "dev": true, + "optional": true + }, + "lcid": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "invert-kv": "1.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "2.2.0", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "strip-bom": "2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "bundled": true, + "dev": true + } + } + }, + "lodash": { + "version": "4.17.5", + "bundled": true, + "dev": true + }, + "longest": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "loose-envify": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "js-tokens": "3.0.2" + } + }, + "lru-cache": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "map-cache": { + "version": "0.2.2", + "bundled": true, + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, + "md5-hex": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "md5-o-matic": "0.1.1" + } + }, + "md5-o-matic": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "mem": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "mimic-fn": "1.2.0" + } + }, + "merge-source-map": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "source-map": "0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "bundled": true, + "dev": true + } + } + }, + "micromatch": { + "version": "2.3.11", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, + "mimic-fn": { + "version": "1.2.0", + "bundled": true, + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "bundled": true, + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "nanomatch": { + "version": "1.2.9", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "fragment-cache": "0.2.1", + "is-odd": "2.0.0", + "is-windows": "1.0.2", + "kind-of": "6.0.2", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "dev": true, + "requires": { + "hosted-git-info": "2.6.0", + "is-builtin-module": "1.0.0", + "semver": "5.5.0", + "validate-npm-package-license": "3.0.3" + } + }, + "normalize-path": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "bundled": true, + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "object.omit": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "optimist": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8", + "wordwrap": "0.0.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "os-locale": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "execa": "0.7.0", + "lcid": "1.0.0", + "mem": "1.1.0" + } + }, + "p-finally": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "dev": true, + "requires": { + "error-ex": "1.3.1" + } + }, + "pascalcase": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "pinkie-promise": "2.0.1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "path-key": { + "version": "2.0.1", + "bundled": true, + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "bundled": true, + "dev": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true, + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true, + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "posix-character-classes": { + "version": "0.1.1", + "bundled": true, + "dev": true + }, + "preserve": { + "version": "0.2.0", + "bundled": true, + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "load-json-file": "1.1.0", + "normalize-package-data": "2.4.0", + "path-type": "1.1.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "find-up": "1.1.2", + "read-pkg": "1.1.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "requires": { + "path-exists": "2.1.0", + "pinkie-promise": "2.0.1" + } + } + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "bundled": true, + "dev": true + }, + "regex-cache": { + "version": "0.4.4", + "bundled": true, + "dev": true, + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "bundled": true, + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "bundled": true, + "dev": true + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-finite": "1.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "bundled": true, + "dev": true + }, + "ret": { + "version": "0.1.15", + "bundled": true, + "dev": true + }, + "right-align": { + "version": "0.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "align-text": "0.1.4" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-regex": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "requires": { + "ret": "0.1.15" + } + }, + "semver": { + "version": "5.5.0", + "bundled": true, + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "set-value": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true + }, + "slide": { + "version": "1.1.6", + "bundled": true, + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "source-map": { + "version": "0.5.7", + "bundled": true, + "dev": true + }, + "source-map-resolve": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "atob": "2.0.3", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "bundled": true, + "dev": true + }, + "spawn-wrap": { + "version": "1.4.2", + "bundled": true, + "dev": true, + "requires": { + "foreground-child": "1.5.6", + "mkdirp": "0.5.1", + "os-homedir": "1.0.2", + "rimraf": "2.6.2", + "signal-exit": "3.0.2", + "which": "1.3.0" + } + }, + "spdx-correct": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "bundled": true, + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "split-string": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + } + }, + "static-extend": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "string-width": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-utf8": "0.2.1" + } + }, + "strip-eof": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "test-exclude": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "requires": { + "arrify": "1.0.1", + "micromatch": "3.1.9", + "object-assign": "4.1.1", + "read-pkg-up": "1.0.1", + "require-main-filename": "1.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "bundled": true, + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "bundled": true, + "dev": true + }, + "braces": { + "version": "2.3.1", + "bundled": true, + "dev": true, + "requires": { + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "kind-of": "6.0.2", + "repeat-element": "1.1.2", + "snapdragon": "0.8.2", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "requires": { + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "bundled": true, + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "bundled": true, + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "bundled": true, + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + }, + "micromatch": { + "version": "3.1.9", + "bundled": true, + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.1", + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.9", + "object.pick": "1.3.0", + "regex-not": "1.0.2", + "snapdragon": "0.8.2", + "to-regex": "3.0.2" + } + } + } + }, + "to-fast-properties": { + "version": "1.0.3", + "bundled": true, + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "to-regex": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + } + } + }, + "trim-right": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + }, + "dependencies": { + "yargs": { + "version": "3.10.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "union-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "bundled": true, + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "bundled": true, + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "bundled": true, + "dev": true + }, + "isobject": { + "version": "3.0.1", + "bundled": true, + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "bundled": true, + "dev": true + }, + "use": { + "version": "3.1.0", + "bundled": true, + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "bundled": true, + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "which": { + "version": "1.3.0", + "bundled": true, + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "bundled": true, + "dev": true + }, + "window-size": { + "version": "0.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "0.0.3", + "bundled": true, + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "slide": "1.1.6" + } + }, + "y18n": { + "version": "3.2.1", + "bundled": true, + "dev": true + }, + "yallist": { + "version": "2.1.2", + "bundled": true, + "dev": true + }, + "yargs": { + "version": "11.1.0", + "bundled": true, + "dev": true, + "requires": { + "cliui": "4.0.0", + "decamelize": "1.2.0", + "find-up": "2.1.0", + "get-caller-file": "1.0.2", + "os-locale": "2.1.0", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "2.1.1", + "which-module": "2.0.0", + "y18n": "3.2.1", + "yargs-parser": "9.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "bundled": true, + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + }, + "cliui": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "wrap-ansi": "2.1.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "yargs-parser": { + "version": "9.0.2", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "8.1.0", + "bundled": true, + "dev": true, + "requires": { + "camelcase": "4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "bundled": true, + "dev": true + } + } + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "3.0.1" + } + }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "1.0.1", + "array-slice": "1.1.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + } + }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "requires": { + "for-own": "1.0.0", + "make-iterator": "1.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", + "integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==", + "dev": true, + "requires": { + "p-try": "1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.2.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "requires": { + "is-absolute": "1.0.0", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "0.1.2" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "pg-connection-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", + "integrity": "sha1-Pu/lmX4G2Ugh5NUC5CtqHHP434I=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pino": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-4.15.0.tgz", + "integrity": "sha512-nFKZPdlEabjARJTkK9wKaqN1rCNXJr0YG7zExjWDYaRrZP4pu46rMg9tiCTwOYDcHr7U2I/tQ9rqFlIr3iXn9A==", + "requires": { + "chalk": "2.3.2", + "fast-json-parse": "1.0.3", + "fast-safe-stringify": "1.2.3", + "flatstr": "1.0.5", + "pino-std-serializers": "1.0.0", + "pump": "3.0.0", + "quick-format-unescaped": "1.1.2", + "split2": "2.2.0" + }, + "dependencies": { + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "pino-std-serializers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-1.0.0.tgz", + "integrity": "sha512-VvrdU2+ixL4zYm1c4q5pdtI9chhV6T1keiVkbV6BVZC1ih8Fsp8pdOCiBXcPYcIMyyo83KxpRhjuX2B53O38iw==" + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + }, + "platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "prettier": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.11.1.tgz", + "integrity": "sha512-T/KD65Ot0PB97xTrG8afQ46x3oiVhnfGjGESSI9NWYcG92+OUPZKkwHqGWXH2t9jK1crnQjubECW0FuOth+hxw==", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true, + "requires": { + "event-stream": "3.3.4" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + }, + "pupa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz", + "integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y=", + "dev": true + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "quick-format-unescaped": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-1.1.2.tgz", + "integrity": "sha1-DKWB3jF0vs7yWsPC6JVjQjgdtpg=", + "requires": { + "fast-safe-stringify": "1.2.3" + } + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "requires": { + "resolve": "1.6.0" + } + }, + "redeyed": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "requires": { + "esprima": "3.0.0" + } + }, + "referrer-policy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz", + "integrity": "sha1-NXdOtzW/UPtsB46DM0tHI1AgfXk=" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "3.0.2", + "safe-regex": "1.1.0" + } + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "require-from-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", + "integrity": "sha1-xUUjPp19pmFunVmt+zn8n1iGdv8=", + "dev": true + }, + "resolve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", + "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", + "requires": { + "path-parse": "1.0.5" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "requires": { + "expand-tilde": "2.0.2", + "global-modules": "1.0.0" + } + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "run-node": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-0.2.0.tgz", + "integrity": "sha512-Zsnxrr+CMGfm7VFuCj96E8tOpFHTEuZS9EvlXcKapVr2RUvr+fxTMxNgK5fXi3TprSgWoxobtR/3TXZT4na/Ng==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "0.1.15" + } + }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true + }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + } + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.4.8.tgz", + "integrity": "sha512-EWZf/D5BN/BbDFPmwY2abw6wgELVmk361self+lcwEmVw0WWUxURp2S/YoDB2WG/xurFVzKQglMARweYRWM6Hw==", + "dev": true, + "requires": { + "@sinonjs/formatio": "2.0.0", + "diff": "3.5.0", + "lodash.get": "4.4.2", + "lolex": "2.3.2", + "nise": "1.3.2", + "supports-color": "5.3.0", + "type-detect": "4.0.8" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "1.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "requires": { + "atob": "2.0.3", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "3.0.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "2.1.0", + "spdx-license-ids": "3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "3.0.2" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "2.0.3" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "0.2.5", + "object-copy": "0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true, + "requires": { + "duplexer": "0.1.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "superagent": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", + "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", + "dev": true, + "requires": { + "component-emitter": "1.2.1", + "cookiejar": "2.1.1", + "debug": "3.1.0", + "extend": "3.0.1", + "form-data": "2.3.2", + "formidable": "1.2.1", + "methods": "1.1.2", + "mime": "1.6.0", + "qs": "6.5.1", + "readable-stream": "2.3.3" + } + }, + "supertest": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-3.0.0.tgz", + "integrity": "sha1-jUu2j9GDDuBwM7HFpamkAhyWUpY=", + "dev": true, + "requires": { + "methods": "1.1.2", + "superagent": "3.8.2" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "2.0.0" + } + }, + "tarn": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.4.tgz", + "integrity": "sha512-j4samMCQCP5+6Il9/cxCqBd3x4vvlLeVdoyGex0KixPKl4F8LpNbDSC6NDhjianZgUngElRr9UI1ryZqJDhwGg==" + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "2.3.3", + "xtend": "4.0.1" + } + }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "2.0.2", + "extend-shallow": "3.0.2", + "regex-not": "1.0.2", + "safe-regex": "1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "topo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", + "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", + "requires": { + "hoek": "5.0.3" + } + }, + "tsc-watch": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-1.0.17.tgz", + "integrity": "sha512-r7KWtcaxbKo/7SzyMglqgWmVVAq7M8JQnf24+fb9dce0XQXBKVT+QKQ+CB4vXqdJWOgBD3xEX8mJ9l6rkCJG/g==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "cross-spawn": "5.1.0", + "ps-tree": "1.1.0", + "typescript": "2.7.2" + } + }, + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tslint": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", + "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.0", + "commander": "2.15.1", + "diff": "3.5.0", + "glob": "7.1.2", + "js-yaml": "3.11.0", + "minimatch": "3.0.4", + "resolve": "1.6.0", + "semver": "5.5.0", + "tslib": "1.9.0", + "tsutils": "2.22.2" + } + }, + "tslint-config-prettier": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.10.0.tgz", + "integrity": "sha512-WghvT/oYVV6UGhkHyAVUjcJYmqnYJgw8nfYOmsnTeBdpNeyPfhQTwZ+qa63cni2VE/6J29VP/7ijWcuaVlVcqg==", + "dev": true + }, + "tslint-plugin-prettier": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tslint-plugin-prettier/-/tslint-plugin-prettier-1.3.0.tgz", + "integrity": "sha512-6UqeeV6EABp0RdQkW6eC1vwnAXcKMGJgPeJ5soXiKdSm2vv7c3dp+835CM8pjgx9l4uSa7tICm1Kli+SMsADDg==", + "dev": true, + "requires": { + "eslint-plugin-prettier": "2.6.0", + "tslib": "1.9.0" + } + }, + "tsutils": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.22.2.tgz", + "integrity": "sha512-u06FUSulCJ+Y8a2ftuqZN6kIGqdP2yJjUPEngXqmdPND4UQfb04igcotH+dw+IFr417yP6muCLE8/5/Qlfnx0w==", + "dev": true, + "requires": { + "tslib": "1.9.0" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.18" + } + }, + "typescript": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", + "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "dev": true + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "0.1.1" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "urijs": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", + "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", + "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", + "requires": { + "kind-of": "6.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, + "v8flags": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.2.tgz", + "integrity": "sha512-6sgSKoFw1UpUPd3cFdF7QGnrH6tDeBgW1F3v9gy8gLY0mlbiBXq8soy8aQpY6xeeCjH5K+JvC62Acp7gtl7wWA==", + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "validate-npm-package-license": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", + "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "dev": true, + "requires": { + "spdx-correct": "3.0.0", + "spdx-expression-parse": "3.0.0" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", + "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "requires": { + "isexe": "2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "x-xss-protection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.1.0.tgz", + "integrity": "sha512-rx3GzJlgEeZ08MIcDsU2vY2B1QEriUKJTSiNHHUIem6eg9pzVOr2TL3Y4Pd6TMAM5D5azGjcxqI62piITBDHVg==" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } +} diff --git a/package.json b/package.json index bc7e36a..b870c04 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "typescript-node", - "version": "1.2.0", - "description": "", + "version": "2.0.0", + "description": "TypeScript template for backend applications.", "license": "MIT", "repository": { "url": "https://github.com/Talento90/typescript-node.git" @@ -9,62 +9,62 @@ "author": "Talento90", "keywords": [ "typescript", - "project structure", - "nodejs" + "nodejs", + "backend" ], "scripts": { - "tsc": "tsc", - "tsc-watch": "tsc -w", - "build": "gulp build", - "start": "node build/src/index.js", - "setup": "npm install & typings install", - "test": "gulp test", - "watch": "nodemon --watch build/src build/src/index.js" + "build": "rm -rf dist && tsc", + "clean": "rm -rf node_modules testdata .sonar", + "coverage": "nyc --exclude dist/test --temp-directory coverage --check-coverage=false --report-dir coverage --reporter=lcov --reporter=html npm test", + "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", + "start": "node dist/src/index.js", + "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", + "test": "npm run build && mocha --recursive dist/test" }, "dependencies": { - "bcryptjs": "^2.4.3", - "boom": "^4.3.1", - "fs": "0.0.1-security", - "good": "^7.2.0", - "good-console": "^6.4.0", - "good-squeeze": "^5.0.2", - "hapi": "^16.1.1", - "hapi-auth-jwt2": "^7.2.4", - "hapi-swagger": "^7.7.0", - "inert": "^4.2.0", - "joi": "^10.5.0", - "jsonwebtoken": "^7.4.1", - "mongoose": "^4.10.0", - "nconf": "^0.8.4", - "path": "^0.12.7", - "vision": "^4.1.1" + "@types/async": "^2.0.48", + "async": "^2.6.0", + "joi": "^13.1.2", + "knex": "^0.14.4", + "koa": "^2.5.0", + "koa-bodyparser": "^4.2.0", + "koa-helmet": "^3.3.0", + "koa-router": "^7.4.0", + "mysql2": "^1.5.3", + "pino": "^4.15.0" }, "devDependencies": { - "@types/bcryptjs": "^2.4.0", - "@types/boom": "^4.3.2", - "@types/chai": "^3.5.2", - "@types/hapi": "^16.1.2", - "@types/joi": "^10.3.2", - "@types/jsonwebtoken": "^7.2.0", - "@types/mocha": "^2.2.41", - "@types/mongoose": "^4.7.13", - "@types/nconf": "0.0.34", - "@types/node": "^7.0.21", - "@types/sinon": "^2.2.2", - "chai": "^3.5.0", - "gulp": "^3.9.1", - "gulp-env": "^0.4.0", - "gulp-mocha": "^4.3.1", - "gulp-rimraf": "^0.2.1", - "gulp-shell": "^0.6.3", - "gulp-tslint": "^8.0.0", - "mocha": "^3.4.1", - "nodemon": "^1.11.0", - "sinon": "^2.2.0", - "tslint": "^5.2.0", - "typescript": "^2.3.2" + "@types/chai": "^4.1.2", + "@types/joi": "^13.0.7", + "@types/knex": "^0.14.9", + "@types/koa": "^2.0.44", + "@types/koa-bodyparser": "^4.2.0", + "@types/koa-helmet": "^3.1.2", + "@types/koa-router": "^7.0.27", + "@types/mocha": "^5.0.0", + "@types/node": "^9.6.0", + "@types/pino": "^4.7.1", + "@types/sinon": "^4.3.0", + "@types/supertest": "^2.0.4", + "chai": "^4.1.2", + "husky": "^0.15.0-rc.13", + "mocha": "^5.0.5", + "nyc": "^11.6.0", + "prettier": "^1.11.1", + "sinon": "^4.4.8", + "supertest": "^3.0.0", + "tsc-watch": "^1.0.17", + "tslint": "^5.9.1", + "tslint-config-prettier": "^1.10.0", + "tslint-plugin-prettier": "^1.3.0", + "typescript": "^2.7.2" }, "engines": { - "node": ">=7.6.0" + "node": ">=8.10.0" + }, + "husky": { + "hooks": { + "pre-commit": "npm run lint && npm test" + } } } diff --git a/pm2.json b/pm2.json deleted file mode 100644 index e0446b6..0000000 --- a/pm2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "apps": [{ - "exec_mode": "cluster", - "instances" : 4, - "script": "./build/src/index.js", - "name": "type-node", - "interpreter": "node", - "env": { - "NODE_ENV": "dev" - } - }] -} \ No newline at end of file diff --git a/src/configurations/config.dev.json b/src/configurations/config.dev.json deleted file mode 100644 index 4fe7647..0000000 --- a/src/configurations/config.dev.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "database": { - "connectionString": "mongodb://localhost:27017/taskdb-dev" - }, - "server": { - "port": 5000, - "jwtSecret": "random-secret-password", - "jwtExpiration": "1h", - "routePrefix": "/api", - "plugins": [ - "logger", - "jwt-auth", - "swagger" - ] - } -} \ No newline at end of file diff --git a/src/configurations/config.test.json b/src/configurations/config.test.json deleted file mode 100644 index 8807b8c..0000000 --- a/src/configurations/config.test.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "database": { - "connectionString": "mongodb://localhost:27017/taskdb-test" - }, - "server": { - "port": 5000, - "jwtSecret": "random-secret-password", - "jwtExpiration": "1h", - "routePrefix": "/api", - "plugins": [ - "logger", - "jwt-auth" - ] - } -} \ No newline at end of file diff --git a/src/configurations/index.ts b/src/configurations/index.ts deleted file mode 100644 index b4fa94c..0000000 --- a/src/configurations/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as nconf from "nconf"; -import * as path from "path"; - -//Read Configurations -const configs = new nconf.Provider({ - env: true, - argv: true, - store: { - type: 'file', - file: path.join(__dirname, `./config.${process.env.NODE_ENV || "dev"}.json`) - } -}); - -export interface IServerConfigurations { - port: number; - plugins: Array; - jwtSecret: string; - jwtExpiration: string; - routePrefix: string; -} - -export interface IDataConfiguration { - connectionString: string; -} - -export function getDatabaseConfig(): IDataConfiguration { - return configs.get("database"); -} - -export function getServerConfigs(): IServerConfigurations { - return configs.get("server"); -} \ No newline at end of file diff --git a/src/database.ts b/src/database.ts deleted file mode 100644 index 87e9aac..0000000 --- a/src/database.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Mongoose from "mongoose"; -import { IDataConfiguration } from "./configurations"; -import { IUser, UserModel } from "./users/user"; -import { ITask, TaskModel } from "./tasks/task"; - -export interface IDatabase { - userModel: Mongoose.Model; - taskModel: Mongoose.Model; -} - -export function init(config: IDataConfiguration): IDatabase { - - (Mongoose).Promise = Promise; - Mongoose.connect(process.env.MONGO_URL || config.connectionString); - - let mongoDb = Mongoose.connection; - - mongoDb.on('error', () => { - console.log(`Unable to connect to database: ${config.connectionString}`); - }); - - mongoDb.once('open', () => { - console.log(`Connected to database: ${config.connectionString}`); - }); - - return { - taskModel: TaskModel, - userModel: UserModel - }; -} \ No newline at end of file diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..7ecbcf3 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,106 @@ +import { AsyncResultCallback, retry } from 'async' +import * as knex from 'knex' +import * as path from 'path' + +export interface Configuration { + host: string + port: number + user: string + password: string + database: string + debug: boolean +} + +export class MySql { + private config: Configuration + private connection: knex | undefined + private retryDbConnectionPromise: Promise | undefined + + constructor(config: Configuration) { + this.config = config + } + + /** + * Retuns a connection to the database. + */ + public async getConnection(): Promise { + if (!this.connection) { + this.connection = await this.retryDbConnection() + } + + return this.connection + } + + public async closeDatabase(): Promise { + if (this.connection) { + await this.connection.destroy() + this.connection = undefined + } + } + + private async createConnection(): Promise { + const config: knex.Config = { + client: 'mysql', + connection: { + host: this.config.host, + port: this.config.port, + user: this.config.user, + password: this.config.password, + database: this.config.database + }, + debug: this.config.debug, + migrations: { + tableName: 'migrations' + } + } + + const db = knex(config) + + // Test database connectivity! + await db.raw('select 1').timeout(500) + + return db + } + + private retryDbConnection(): Promise { + if (this.retryDbConnectionPromise instanceof Promise) { + return this.retryDbConnectionPromise + } + + const methodToRetry = (cb: AsyncResultCallback) => { + this.createConnection() + .then((db: knex) => { + cb(undefined, db) + }) + .catch((err: Error) => { + cb(err, undefined) + }) + } + + this.retryDbConnectionPromise = new Promise((resolve, reject) => { + retry( + { times: 3, interval: 1000 }, + methodToRetry, + (err: Error | undefined, db: knex) => { + if (err) { + reject(err) + } else { + resolve(db) + } + + // After the connection succeeds or fails, we clear the promise so that we can + // attempt to retry to establish the db connection at a later time if required. + this.retryDbConnectionPromise = undefined + } + ) + }) + + return this.retryDbConnectionPromise + } + + private async schemaMigration() { + await this.connection.migrate.latest({ + directory: path.resolve(__dirname, './migrations') + }) + } +} diff --git a/src/database/migrations/m.ts b/src/database/migrations/m.ts new file mode 100644 index 0000000..52247b7 --- /dev/null +++ b/src/database/migrations/m.ts @@ -0,0 +1,19 @@ +// import * as knex from 'knex' + +// export function up(knex) { +// return knex.schema.createTable('user_mappings', table => { +// table.increments() +// table +// .integer('pd_user_id', 11) +// .notNullable() +// .unsigned() +// table.string('system_id', 50).notNullable() +// table.string('ext_ref', 50).notNullable() +// table.unique(['system_id', 'pd_user_id'], 'no_duplicate_marketplace_users') +// table.unique(['system_id', 'ext_ref'], 'no_duplicate_marketplace_refrences') +// }) +// } + +// export function down(knex) { +// return knex.schema.dropTable('user_mappings') +// } diff --git a/src/entities/index.ts b/src/entities/index.ts new file mode 100644 index 0000000..f0b0cc8 --- /dev/null +++ b/src/entities/index.ts @@ -0,0 +1,2 @@ +export { User } from './user' +export { Task } from './task' diff --git a/src/entities/task.ts b/src/entities/task.ts new file mode 100644 index 0000000..725f1ba --- /dev/null +++ b/src/entities/task.ts @@ -0,0 +1,7 @@ +export class Task { + public id: number + public name: string + public description: string + public done: boolean + public userId: number +} diff --git a/src/entities/user.ts b/src/entities/user.ts new file mode 100644 index 0000000..efb7fce --- /dev/null +++ b/src/entities/user.ts @@ -0,0 +1,6 @@ +export class User { + public id: number + public email: string + public firstName: string + public lastName: string +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/index.ts b/src/index.ts index a983d9a..d47ff77 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,28 +1,38 @@ -import * as Server from "./server"; -import * as Database from "./database"; -import * as Configs from "./configurations"; - -console.log(`Running enviroment ${process.env.NODE_ENV || "dev"}`); - -// Catch unhandling unexpected exceptions -process.on('uncaughtException', (error: Error) => { - console.error(`uncaughtException ${error.message}`); -}); - -// Catch unhandling rejected promises -process.on('unhandledRejection', (reason: any) => { - console.error(`unhandledRejection ${reason}`); -}); - -// Init Database -const dbConfigs = Configs.getDatabaseConfig(); -const database = Database.init(dbConfigs); - -// Starting Application Server -const serverConfigs = Configs.getServerConfigs(); - -Server.init(serverConfigs, database).then((server) => { - server.start(() => { - console.log('Server running at:', server.info.uri); - }); -}); \ No newline at end of file +import { Server } from 'http' +import * as pino from 'pino' +import { MySql } from './database' +import * as server from './server' + +export async function init() { + const logger = pino() + + try { + // Starting the HTTP server + logger.info('Starting HTTP server') + + const app = server.createServer().listen(process.env.PORT || 8080) + + // Register global process events + registerProcessEvents(logger, app) + } catch (e) { + logger.error(e, 'An error occurred while initializing application.') + } +} + +function registerProcessEvents(logger: pino.Logger, app: Server) { + process.on('uncaughtException', (error: Error) => { + logger.error('UncaughtException', error) + }) + + process.on('unhandledRejection', (reason: any, promise: any) => { + logger.info(reason, promise) + }) + + process.on('SIGTERM', () => { + logger.info('Starting graceful shutdown') + + app.close() + }) +} + +init() diff --git a/src/plugins/interfaces.ts b/src/plugins/interfaces.ts deleted file mode 100644 index 1eded69..0000000 --- a/src/plugins/interfaces.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as Hapi from "hapi"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - - -export interface IPluginOptions { - database: IDatabase; - serverConfigs: IServerConfigurations; -} - -export interface IPlugin { - register(server: Hapi.Server, options?: IPluginOptions): Promise; - info(): IPluginInfo; -} - -export interface IPluginInfo { - name: string; - version: string; -} \ No newline at end of file diff --git a/src/plugins/jwt-auth/index.ts b/src/plugins/jwt-auth/index.ts deleted file mode 100644 index 5a1536c..0000000 --- a/src/plugins/jwt-auth/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { IPlugin, IPluginOptions } from "../interfaces"; -import * as Hapi from "hapi"; -import { IUser, UserModel } from "../../users/user"; - -export default (): IPlugin => { - return { - register: (server: Hapi.Server, options: IPluginOptions): Promise => { - const database = options.database; - const serverConfig = options.serverConfigs; - - const validateUser = (decoded, request, cb) => { - database.userModel.findById(decoded.id).lean(true) - .then((user: IUser) => { - if (!user) { - return cb(null, false); - } - - return cb(null, true); - }); - }; - - return new Promise((resolve) => { - server.register({ - register: require('hapi-auth-jwt2') - }, (error) => { - if (error) { - console.log(`Error registering jwt plugin: ${error}`); - } else { - server.auth.strategy('jwt', 'jwt', false, - { - key: serverConfig.jwtSecret, - validateFunc: validateUser, - verifyOptions: { algorithms: ['HS256'] } - }); - } - - resolve(); - }); - }); - }, - info: () => { - return { - name: "JWT Authentication", - version: "1.0.0" - }; - } - }; -}; - - diff --git a/src/plugins/logger/index.ts b/src/plugins/logger/index.ts deleted file mode 100644 index eeb5d80..0000000 --- a/src/plugins/logger/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { IPlugin } from "../interfaces"; -import * as Hapi from "hapi"; - -export default (): IPlugin => { - return { - register: (server: Hapi.Server): Promise => { - const opts = { - ops: { - interval: 1000 - }, - reporters: { - consoleReporter: [{ - module: 'good-squeeze', - name: 'Squeeze', - args: [{ error: '*', log: '*', response: '*', request: '*' }] - }, { - module: 'good-console' - }, 'stdout'] - } - }; - - return new Promise((resolve) => { - server.register({ - register: require('good'), - options: opts - }, (error) => { - if (error) { - console.log(`Error registering logger plugin: ${error}`); - } - - resolve(); - }); - }); - }, - info: () => { - return { - name: "Good Logger", - version: "1.0.0" - }; - } - }; -}; \ No newline at end of file diff --git a/src/plugins/swagger/index.ts b/src/plugins/swagger/index.ts deleted file mode 100644 index 1f3eae6..0000000 --- a/src/plugins/swagger/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { IPlugin, IPluginInfo } from "../interfaces"; -import * as Hapi from "hapi"; - -export default (): IPlugin => { - return { - register: (server: Hapi.Server): Promise => { - - return new Promise((resolve) => { - server.register([ - require('inert'), - require('vision'), - { - register: require('hapi-swagger'), - options: { - info: { - title: 'Task Api', - description: 'Task Api Documentation', - version: '1.0' - }, - tags: [ - { - 'name': 'tasks', - 'description': 'Api tasks interface.' - }, - { - 'name': 'users', - 'description': 'Api users interface.' - } - ], - swaggerUI: true, - documentationPage: true, - documentationPath: '/docs' - } - } - ] - , (error) => { - if (error) { - console.log(`Error registering swagger plugin: ${error}`); - } - - resolve(); - }); - }); - }, - info: () => { - return { - name: "Swagger Documentation", - version: "1.0.0" - }; - } - }; -}; \ No newline at end of file diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts new file mode 100644 index 0000000..04223bf --- /dev/null +++ b/src/repositories/user-repository.ts @@ -0,0 +1,18 @@ +import * as knex from 'knex' +import { User } from '../entities' + +export class UserRepository { + private connection: knex + + constructor(conn: knex) { + this.connection = conn + } + + public async insert(user: User): Promise { + const inserted = await this.connection.insert({}) + + user.id = inserted + + return user + } +} diff --git a/src/server.ts b/src/server.ts deleted file mode 100644 index 44109d7..0000000 --- a/src/server.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as Hapi from "hapi"; -import * as Boom from "boom"; -import { IPlugin } from "./plugins/interfaces"; -import { IServerConfigurations } from "./configurations"; -import * as Tasks from "./tasks"; -import * as Users from "./users"; -import { IDatabase } from "./database"; - - -export function init(configs: IServerConfigurations, database: IDatabase): Promise { - - return new Promise(resolve => { - - const port = process.env.PORT || configs.port; - const server = new Hapi.Server(); - - server.connection({ - port: port, - routes: { - cors: true - } - }); - - if (configs.routePrefix) { - server.realm.modifiers.route.prefix = configs.routePrefix; - } - - // Setup Hapi Plugins - const plugins: Array = configs.plugins; - const pluginOptions = { - database: database, - serverConfigs: configs - }; - - let pluginPromises = []; - - plugins.forEach((pluginName: string) => { - var plugin: IPlugin = (require("./plugins/" + pluginName)).default(); - console.log(`Register Plugin ${plugin.info().name} v${plugin.info().version}`); - pluginPromises.push(plugin.register(server, pluginOptions)); - }); - - Promise.all(pluginPromises).then(() => { - console.log('All plugins registed successfully.'); - - console.log('Register Routes'); - Tasks.init(server, configs, database); - Users.init(server, configs, database); - console.log('Routes registed sucessfully.'); - - resolve(server); - }); - }); -} \ No newline at end of file diff --git a/src/server/health/controller.ts b/src/server/health/controller.ts new file mode 100644 index 0000000..7a489dd --- /dev/null +++ b/src/server/health/controller.ts @@ -0,0 +1,7 @@ +import { Context } from 'koa' + +export default class HealthController { + public getHealth(ctx: Context) { + ctx.status = 200 + } +} diff --git a/src/server/health/index.ts b/src/server/health/index.ts new file mode 100644 index 0000000..64163e4 --- /dev/null +++ b/src/server/health/index.ts @@ -0,0 +1,12 @@ +import * as Koa from 'koa' +import * as Router from 'koa-router' +import HealthController from './controller' + +export function init(server: Koa) { + const controller = new HealthController() + const router = new Router() + + router.get('/health', controller.getHealth.bind(this)) + + server.use(router.routes()) +} diff --git a/src/server/index.ts b/src/server/index.ts new file mode 100644 index 0000000..82dbfad --- /dev/null +++ b/src/server/index.ts @@ -0,0 +1,17 @@ +import * as Koa from 'koa' +import * as bodyParser from 'koa-bodyparser' +import * as helmet from 'koa-helmet' +import * as health from './health' + +export function createServer(): Koa { + const app = new Koa() + + // Register Middlewares + app.use(helmet()) + app.use(bodyParser()) + + // Register routes + health.init(app) + + return app +} diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/middlewares/cache.ts b/src/server/middlewares/cache.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/middlewares/logger.ts b/src/server/middlewares/logger.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/middlewares/response.ts b/src/server/middlewares/response.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/users/index.ts b/src/server/users/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/tasks/index.ts b/src/tasks/index.ts deleted file mode 100644 index b76d15e..0000000 --- a/src/tasks/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Hapi from "hapi"; -import Routes from "./routes"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - -export function init(server: Hapi.Server, configs: IServerConfigurations, database: IDatabase) { - Routes(server, configs, database); -} \ No newline at end of file diff --git a/src/tasks/routes.ts b/src/tasks/routes.ts deleted file mode 100644 index aa65fd7..0000000 --- a/src/tasks/routes.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as Hapi from "hapi"; -import * as Joi from "joi"; -import TaskController from "./task-controller"; -import * as TaskValidator from "./task-validator"; -import { jwtValidator } from "../users/user-validator"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - -export default function (server: Hapi.Server, configs: IServerConfigurations, database: IDatabase) { - - const taskController = new TaskController(configs, database); - server.bind(taskController); - - server.route({ - method: 'GET', - path: '/tasks/{id}', - config: { - handler: taskController.getTaskById, - auth: "jwt", - tags: ['api', 'tasks'], - description: 'Get task by id.', - validate: { - params: { - id: Joi.string().required() - }, - headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Task founded.' - }, - '404': { - 'description': 'Task does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'GET', - path: '/tasks', - config: { - handler: taskController.getTasks, - auth: "jwt", - tags: ['api', 'tasks'], - description: 'Get all tasks.', - validate: { - query: { - top: Joi.number().default(5), - skip: Joi.number().default(0) - }, - headers: jwtValidator - } - } - }); - - server.route({ - method: 'DELETE', - path: '/tasks/{id}', - config: { - handler: taskController.deleteTask, - auth: "jwt", - tags: ['api', 'tasks'], - description: 'Delete task by id.', - validate: { - params: { - id: Joi.string().required() - }, - headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted Task.', - }, - '404': { - 'description': 'Task does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'PUT', - path: '/tasks/{id}', - config: { - handler: taskController.updateTask, - auth: "jwt", - tags: ['api', 'tasks'], - description: 'Update task by id.', - validate: { - params: { - id: Joi.string().required() - }, - payload: TaskValidator.updateTaskModel, - headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Deleted Task.', - }, - '404': { - 'description': 'Task does not exists.' - } - } - } - } - } - }); - - server.route({ - method: 'POST', - path: '/tasks', - config: { - handler: taskController.createTask, - auth: "jwt", - tags: ['api', 'tasks'], - description: 'Create a task.', - validate: { - payload: TaskValidator.createTaskModel, - headers: jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '201': { - 'description': 'Created Task.' - } - } - } - } - } - }); -} \ No newline at end of file diff --git a/src/tasks/task-controller.ts b/src/tasks/task-controller.ts deleted file mode 100644 index 89170ca..0000000 --- a/src/tasks/task-controller.ts +++ /dev/null @@ -1,86 +0,0 @@ -import * as Hapi from "hapi"; -import * as Boom from "boom"; -import { ITask } from "./task"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - -export default class TaskController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.configs = configs; - this.database = database; - } - - public async createTask(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - let userId = request.auth.credentials.id; - var newTask: ITask = request.payload; - newTask.userId = userId; - - try { - let task: ITask = await this.database.taskModel.create(newTask); - return reply(task).code(201); - }catch (error) { - return reply(Boom.badImplementation(error)); - } - } - - public async updateTask(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - let userId = request.auth.credentials.id; - let id = request.params["id"]; - - try { - let task: ITask = await this.database.taskModel.findByIdAndUpdate( - { _id: id, userId: userId }, - { $set: request.payload }, - { new: true } - ); - - if (task) { - reply(task); - } else { - reply(Boom.notFound()); - } - - } catch (error) { - return reply(Boom.badImplementation(error)); - } - } - - public async deleteTask(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - let id = request.params["id"]; - let userId = request.auth.credentials.id; - - let deletedTask = await this.database.taskModel.findOneAndRemove({ _id: id, userId: userId }); - - if (deletedTask) { - return reply(deletedTask); - } else { - return reply(Boom.notFound()); - } - } - - public async getTaskById(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - let userId = request.auth.credentials.id; - let id = request.params["id"]; - - let task = await this.database.taskModel.findOne({ _id: id, userId: userId }).lean(true); - - if (task) { - reply(task); - } else { - reply(Boom.notFound()); - } - } - - public async getTasks(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - let userId = request.auth.credentials.id; - let top = request.query['top']; - let skip = request.query['skip']; - let tasks = await this.database.taskModel.find({ userId: userId }).lean(true).skip(skip).limit(top); - - return reply(tasks); - } -} \ No newline at end of file diff --git a/src/tasks/task-validator.ts b/src/tasks/task-validator.ts deleted file mode 100644 index 4446168..0000000 --- a/src/tasks/task-validator.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Joi from "joi"; - -export const createTaskModel = Joi.object().keys({ - name: Joi.string().required(), - description: Joi.string().required() -}); - -export const updateTaskModel = Joi.object().keys({ - name: Joi.string().required(), - description: Joi.string().required(), - completed: Joi.boolean() -}); \ No newline at end of file diff --git a/src/tasks/task.ts b/src/tasks/task.ts deleted file mode 100644 index 3874c6f..0000000 --- a/src/tasks/task.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Mongoose from "mongoose"; - -export interface ITask extends Mongoose.Document { - userId: string; - name: string; - description: string; - completed: boolean; - createdAt: Date; - updateAt: Date; -} - -export const TaskSchema = new Mongoose.Schema({ - userId: { type: String, required: true }, - name: { type: String, required: true }, - description: String, - completed: Boolean -}, { - timestamps: true - }); - -export const TaskModel = Mongoose.model('Task', TaskSchema); \ No newline at end of file diff --git a/src/users/index.ts b/src/users/index.ts deleted file mode 100644 index b76d15e..0000000 --- a/src/users/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Hapi from "hapi"; -import Routes from "./routes"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - -export function init(server: Hapi.Server, configs: IServerConfigurations, database: IDatabase) { - Routes(server, configs, database); -} \ No newline at end of file diff --git a/src/users/routes.ts b/src/users/routes.ts deleted file mode 100644 index 8bbdd29..0000000 --- a/src/users/routes.ts +++ /dev/null @@ -1,136 +0,0 @@ -import * as Hapi from "hapi"; -import * as Joi from "joi"; -import UserController from "./user-controller"; -import { UserModel } from "./user"; -import * as UserValidator from "./user-validator"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - -export default function (server: Hapi.Server, serverConfigs: IServerConfigurations, database: IDatabase) { - - const userController = new UserController(serverConfigs, database); - server.bind(userController); - - server.route({ - method: 'GET', - path: '/users/info', - config: { - handler: userController.infoUser, - auth: "jwt", - tags: ['api', 'users'], - description: 'Get user info.', - validate: { - headers: UserValidator.jwtValidator, - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'User founded.' - }, - '401': { - 'description': 'Please login.' - } - } - } - } - } - }); - - server.route({ - method: 'DELETE', - path: '/users', - config: { - handler: userController.deleteUser, - auth: "jwt", - tags: ['api', 'users'], - description: 'Delete current user.', - validate: { - headers: UserValidator.jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'User deleted.', - }, - '401': { - 'description': 'User does not have authorization.' - } - } - } - } - } - }); - - server.route({ - method: 'PUT', - path: '/users', - config: { - handler: userController.updateUser, - auth: "jwt", - tags: ['api', 'users'], - description: 'Update current user info.', - validate: { - payload: UserValidator.updateUserModel, - headers: UserValidator.jwtValidator - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'Updated info.', - }, - '401': { - 'description': 'User does not have authorization.' - } - } - } - } - } - }); - - server.route({ - method: 'POST', - path: '/users', - config: { - handler: userController.createUser, - tags: ['api', 'users'], - description: 'Create a user.', - validate: { - payload: UserValidator.createUserModel - }, - plugins: { - 'hapi-swagger': { - responses: { - '201': { - 'description': 'User created.' - } - } - } - } - } - }); - - server.route({ - method: 'POST', - path: '/users/login', - config: { - handler: userController.loginUser, - tags: ['api', 'users'], - description: 'Login a user.', - validate: { - payload: UserValidator.loginUserModel - }, - plugins: { - 'hapi-swagger': { - responses: { - '200': { - 'description': 'User logged in.' - } - } - } - } - } - }); -} \ No newline at end of file diff --git a/src/users/user-controller.ts b/src/users/user-controller.ts deleted file mode 100644 index 04ebe32..0000000 --- a/src/users/user-controller.ts +++ /dev/null @@ -1,79 +0,0 @@ -import * as Hapi from "hapi"; -import * as Boom from "boom"; -import * as Jwt from "jsonwebtoken"; -import { IUser } from "./user"; -import { IDatabase } from "../database"; -import { IServerConfigurations } from "../configurations"; - - -export default class UserController { - - private database: IDatabase; - private configs: IServerConfigurations; - - constructor(configs: IServerConfigurations, database: IDatabase) { - this.database = database; - this.configs = configs; - } - - private generateToken(user: IUser) { - const jwtSecret = this.configs.jwtSecret; - const jwtExpiration = this.configs.jwtExpiration; - - return Jwt.sign({ id: user._id }, jwtSecret, { expiresIn: jwtExpiration }); - } - - - public async loginUser(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - const email = request.payload.email; - const password = request.payload.password; - - let user: IUser = await this.database.userModel.findOne({ email: email }); - - if (!user) { - return reply(Boom.unauthorized("User does not exists.")); - } - - if (!user.validatePassword(password)) { - return reply(Boom.unauthorized("Password is invalid.")); - } - - reply({ - token: this.generateToken(user) - }); - } - - public async createUser(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - try { - let user: any = await this.database.userModel.create(request.payload); - return reply({ token: this.generateToken(user)}).code(201); - } catch (error) { - return reply(Boom.badImplementation(error)); - } - } - - public async updateUser(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - const id = request.auth.credentials.id; - - try { - let user: IUser = await this.database.userModel.findByIdAndUpdate(id, { $set: request.payload }, { new: true }); - return reply(user); - } catch (error) { - return reply(Boom.badImplementation(error)); - } - } - - public async deleteUser(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - const id = request.auth.credentials.id; - let user: IUser = await this.database.userModel.findByIdAndRemove(id); - - return reply(user); - } - - public async infoUser(request: Hapi.Request, reply: Hapi.ReplyNoContinue) { - const id = request.auth.credentials.id; - let user: IUser = await this.database.userModel.findById(id); - - reply(user); - } -} \ No newline at end of file diff --git a/src/users/user-validator.ts b/src/users/user-validator.ts deleted file mode 100644 index b1f3af0..0000000 --- a/src/users/user-validator.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Joi from "joi"; - -export const createUserModel = Joi.object().keys({ - email: Joi.string().email().trim().required(), - name: Joi.string().required(), - password: Joi.string().trim().required() -}); - -export const updateUserModel = Joi.object().keys({ - email: Joi.string().email().trim(), - name: Joi.string(), - password: Joi.string().trim() -}); - -export const loginUserModel = Joi.object().keys({ - email: Joi.string().email().required(), - password: Joi.string().trim().required() -}); - -export const jwtValidator = Joi.object({'authorization': Joi.string().required()}).unknown(); \ No newline at end of file diff --git a/src/users/user.ts b/src/users/user.ts deleted file mode 100644 index d3baf1a..0000000 --- a/src/users/user.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as Mongoose from "mongoose"; -import * as Bcrypt from "bcryptjs"; - -export interface IUser extends Mongoose.Document { - name: string; - email: string; - password: string; - createdAt: Date; - updateAt: Date; - validatePassword(requestPassword): boolean; -} - - -export const UserSchema = new Mongoose.Schema( - { - email: { type: String, unique: true, required: true }, - name: { type: String, required: true }, - password: { type: String, required: true } - }, - { - timestamps: true - }); - -function hashPassword(password: string): string { - if (!password) { - return null; - } - - return Bcrypt.hashSync(password, Bcrypt.genSaltSync(8)); -} - -UserSchema.methods.validatePassword = function (requestPassword) { - return Bcrypt.compareSync(requestPassword, this.password); -}; - -UserSchema.pre('save', function (next) { - const user = this; - - if (!user.isModified('password')) { - return next(); - } - - user.password = hashPassword(user.password); - - return next(); -}); - -UserSchema.pre('findOneAndUpdate', function () { - const password = hashPassword(this.getUpdate().$set.password); - - if (!password) { - return; - } - - this.findOneAndUpdate({}, { password: password }); -}); - -export const UserModel = Mongoose.model('User', UserSchema); diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..9646fe7 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,7 @@ +import { expect } from 'chai' + +describe('Just pass', () => { + it('must pass', () => { + expect(true) + }) +}) diff --git a/test/tasks/task-controller-tests.ts b/test/tasks/task-controller-tests.ts deleted file mode 100644 index 39c42e0..0000000 --- a/test/tasks/task-controller-tests.ts +++ /dev/null @@ -1,177 +0,0 @@ -import * as chai from "chai"; -import TaskController from "../../src/tasks/task-controller"; -import { ITask } from "../../src/tasks/task"; -import { IUser } from "../../src/users/user"; -import * as Configs from "../../src/configurations"; -import * as Server from "../../src/server"; -import * as Database from "../../src/database"; -import * as Utils from "../utils"; - -const configDb = Configs.getDatabaseConfig(); -const database = Database.init(configDb); -const assert = chai.assert; -const serverConfig = Configs.getServerConfigs(); - -describe("TastController Tests", () => { - - let server; - - before((done) => { - Server.init(serverConfig, database).then((s) => { - server = s; - done(); - }); - }); - - beforeEach((done) => { - Utils.createSeedTaskData(database, done); - }); - - afterEach((done) => { - Utils.clearDatabase(database, done); - }); - - it("Get tasks", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', url: serverConfig.routePrefix + '/users/login', payload: { - email: user.email, - password: user.password - } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - server.inject({ method: 'Get', url: serverConfig.routePrefix + '/tasks', headers: { "authorization": login.token } }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: Array = JSON.parse(res.payload); - assert.equal(3, responseBody.length); - done(); - }); - }); - }); - - it("Get single task", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', url: serverConfig.routePrefix + '/users/login', payload: { - email: user.email, - password: user.password - } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - database.taskModel.findOne({}).then((task) => { - server.inject({ - method: 'Get', - url: serverConfig.routePrefix + '/tasks/' + task._id, - headers: { "authorization": login.token } - }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: ITask = JSON.parse(res.payload); - assert.equal(task.name, responseBody.name); - done(); - }); - }); - }); - }); - - it("Create task", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { email: user.email, password: user.password } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - database.userModel.findOne({ email: user.email }).then((user: IUser) => { - var task = Utils.createTaskDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/tasks', - payload: task, - headers: { "authorization": login.token } - }, (res) => { - assert.equal(201, res.statusCode); - var responseBody: ITask = JSON.parse(res.payload); - assert.equal(task.name, responseBody.name); - assert.equal(task.description, responseBody.description); - done(); - }); - }); - }); - }); - - it("Update task", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { email: user.email, password: user.password } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - database.taskModel.findOne({}).then((task) => { - - var updateTask = { - completed: true, - name: task.name, - description: task.description - }; - - server.inject({ - method: 'PUT', - url: serverConfig.routePrefix + '/tasks/' + task._id, - payload: updateTask, - headers: { "authorization": login.token } - }, - (res) => { - assert.equal(200, res.statusCode); - console.log(res.payload); - var responseBody: ITask = JSON.parse(res.payload); - assert.isTrue(responseBody.completed); - done(); - }); - }); - }); - }); - - it("Delete single task", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { email: user.email, password: user.password } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - database.taskModel.findOne({}).then((task) => { - server.inject({ - method: 'DELETE', - url: serverConfig.routePrefix + '/tasks/' + task._id, - headers: { "authorization": login.token } - }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: ITask = JSON.parse(res.payload); - assert.equal(task.name, responseBody.name); - - database.taskModel.findById(responseBody._id).then((deletedTask) => { - assert.isNull(deletedTask); - done(); - }); - }); - }); - }); - }); -}); diff --git a/test/users/users-controller-tests.ts b/test/users/users-controller-tests.ts deleted file mode 100644 index 8c3be24..0000000 --- a/test/users/users-controller-tests.ts +++ /dev/null @@ -1,159 +0,0 @@ -import * as chai from "chai"; -import UserController from "../../src/users/user-controller"; -import { IUser } from "../../src/users/user"; -import * as Configs from "../../src/configurations"; -import * as Server from "../../src/server"; -import * as Database from "../../src/database"; -import * as Utils from "../utils"; - -const configDb = Configs.getDatabaseConfig(); -const database = Database.init(configDb); -const assert = chai.assert; -const serverConfig = Configs.getServerConfigs(); - -describe("UserController Tests", () => { - - let server; - - before((done) => { - Server.init(serverConfig, database).then((s) => { - server = s; - done(); - }); - }); - - beforeEach((done) => { - Utils.createSeedUserData(database, done); - }); - - afterEach((done) => { - Utils.clearDatabase(database, done); - }); - - it("Create user", (done) => { - var user = { - email: "user@mail.com", - name: "John Robot", - password: "123123" - }; - - server.inject({ method: 'POST', url: serverConfig.routePrefix + '/users', payload: user }, (res) => { - assert.equal(201, res.statusCode); - var responseBody: any = JSON.parse(res.payload); - assert.isNotNull(responseBody.token); - done(); - }); - }); - - it("Create user invalid data", (done) => { - var user = { - email: "user", - name: "John Robot", - password: "123123" - }; - - server.inject({ method: 'POST', url: serverConfig.routePrefix + '/users', payload: user }, (res) => { - assert.equal(400, res.statusCode); - done(); - }); - }); - - it("Create user with same email", (done) => { - server.inject({ method: 'POST', url: serverConfig.routePrefix + '/users', payload: Utils.createUserDummy() }, (res) => { - assert.equal(500, res.statusCode); - done(); - }); - }); - - it("Get user Info", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { - email: user.email, password: user.password - } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - server.inject({ - method: 'GET', - url: serverConfig.routePrefix + '/users/info', - headers: { "authorization": login.token } - }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: IUser = JSON.parse(res.payload); - assert.equal(user.email, responseBody.email); - done(); - }); - }); - }); - - it("Get User Info Unauthorized", (done) => { - server.inject({ - method: 'GET', - url: serverConfig.routePrefix + '/users/info', - headers: { "authorization": "dummy token" } - }, (res) => { - assert.equal(401, res.statusCode); - done(); - }); - }); - - - it("Delete user", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { email: user.email, password: user.password } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - - server.inject({ - method: 'DELETE', - url: serverConfig.routePrefix + '/users', - headers: { "authorization": login.token } - }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: IUser = JSON.parse(res.payload); - assert.equal(user.email, responseBody.email); - - database.userModel.findOne({ "email": user.email }).then((deletedUser) => { - assert.isNull(deletedUser); - done(); - }); - }); - }); - }); - - it("Update user info", (done) => { - var user = Utils.createUserDummy(); - - server.inject({ - method: 'POST', - url: serverConfig.routePrefix + '/users/login', - payload: { email: user.email, password: user.password } - }, (res) => { - assert.equal(200, res.statusCode); - var login: any = JSON.parse(res.payload); - var updateUser = { name: "New Name" }; - - server.inject({ - method: 'PUT', - url: serverConfig.routePrefix + '/users', - payload: updateUser, - headers: { "authorization": login.token } - }, (res) => { - assert.equal(200, res.statusCode); - var responseBody: IUser = JSON.parse(res.payload); - assert.equal("New Name", responseBody.name); - done(); - }); - }); - }); -}); \ No newline at end of file diff --git a/test/utils.ts b/test/utils.ts deleted file mode 100644 index 1827f59..0000000 --- a/test/utils.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Database from "../src/database"; - - -export function createTaskDummy(userId?: string, name?: string, description?: string) { - var user = { - name: name || "dummy task", - description: description || "I'm a dummy task!" - }; - - if (userId) { - user["userId"] = userId; - } - - return user; -} - -export function createUserDummy(email?: string) { - var user = { - email: email || "dummy@mail.com", - name: "Dummy Jones", - password: "123123" - }; - - return user; -} - - -export function clearDatabase(database: Database.IDatabase, done: MochaDone) { - var promiseUser = database.userModel.remove({}); - var promiseTask = database.taskModel.remove({}); - - Promise.all([promiseUser, promiseTask]).then(() => { - done(); - }).catch((error) => { - console.log(error); - }); -} - -export function createSeedTaskData(database: Database.IDatabase, done: MochaDone) { - return database.userModel.create(createUserDummy()) - .then((user) => { - return Promise.all([ - database.taskModel.create(createTaskDummy(user._id, "Task 1", "Some dummy data 1")), - database.taskModel.create(createTaskDummy(user._id, "Task 2", "Some dummy data 2")), - database.taskModel.create(createTaskDummy(user._id, "Task 3", "Some dummy data 3")), - ]); - }).then((task) => { - done(); - }).catch((error) => { - console.log(error); - }); -} - -export function createSeedUserData(database: Database.IDatabase, done: MochaDone) { - database.userModel.create(createUserDummy()) - .then((user) => { - done(); - }) - .catch((error) => { - console.log(error); - }); -} - diff --git a/tsconfig.json b/tsconfig.json index 83d7f75..0854cc2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,12 @@ { "compilerOptions": { - "outDir": "build", + "outDir": "dist", "target": "es6", "module": "commonjs", - "moduleResolution": "node", "sourceMap": true, - "typeRoots": ["node_modules/@types"] + "typeRoots": [ + "node_modules/@types" + ] }, "include": [ "src/**/**.ts", diff --git a/tslint.json b/tslint.json index 2ce3af6..925859b 100644 --- a/tslint.json +++ b/tslint.json @@ -1,59 +1,18 @@ { - "rules": { - "class-name": true, - "curly": true, - "eofline": false, - "forin": true, - "indent": [ - true, - 4 - ], - "label-position": true, - "max-line-length": [ - true, - 140 - ], - "no-arg": true, - "no-bitwise": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-construct": true, - "no-debugger": true, - "no-duplicate-variable": true, - "no-empty": false, - "no-eval": true, - "no-string-literal": false, - "no-trailing-whitespace": true, - "no-unused-variable": false, - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-else", - "check-whitespace" - ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator" - ] - } -} \ No newline at end of file + "defaultSeverity": "error", + "extends": ["tslint:recommended", "tslint-config-prettier"], + "jsRules": {}, + "rulesDirectory": ["tslint-plugin-prettier"], + "rules": { + "prettier": [ + true, + { + "semi": false, + "singleQuote": true + } + ], + "no-console": false, + "interface-name": false, + "object-literal-sort-keys": false + } +} From fb6369ae6c2a2823cdcc86ea3cc3852752731b6c Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Thu, 29 Mar 2018 17:32:34 +0100 Subject: [PATCH 02/35] working on refaactor --- package.json | 4 +- src/entities/task.ts | 14 ++++--- src/entities/user.ts | 12 +++--- src/managers/index.ts | 2 + src/managers/task-manager.ts | 26 +++++++++++++ src/managers/user-manager.ts | 22 +++++++++++ src/repositories/index.ts | 3 ++ src/repositories/task-repository.ts | 59 +++++++++++++++++++++++++++++ src/repositories/unit-of-work.ts | 41 ++++++++++++++++++++ src/repositories/user-repository.ts | 40 +++++++++++++++++-- src/server/index.ts | 2 + src/server/users/controller.ts | 19 ++++++++++ src/server/users/index.ts | 22 +++++++++++ 13 files changed, 249 insertions(+), 17 deletions(-) create mode 100644 src/managers/index.ts create mode 100644 src/managers/task-manager.ts create mode 100644 src/managers/user-manager.ts create mode 100644 src/repositories/index.ts create mode 100644 src/repositories/unit-of-work.ts create mode 100644 src/server/users/controller.ts diff --git a/package.json b/package.json index b870c04..85fec69 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "rm -rf dist && tsc", "clean": "rm -rf node_modules testdata .sonar", "coverage": "nyc --exclude dist/test --temp-directory coverage --check-coverage=false --report-dir coverage --reporter=lcov --reporter=html npm test", - "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", + "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts' --fix", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", "test": "npm run build && mocha --recursive dist/test" @@ -64,7 +64,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint && npm test" + "pre-commit": "" } } } diff --git a/src/entities/task.ts b/src/entities/task.ts index 725f1ba..3ea9526 100644 --- a/src/entities/task.ts +++ b/src/entities/task.ts @@ -1,7 +1,9 @@ -export class Task { - public id: number - public name: string - public description: string - public done: boolean - public userId: number +export interface Task { + id?: number + name: string + description: string + done: boolean + userId: number + created: Date + updated: Date } diff --git a/src/entities/user.ts b/src/entities/user.ts index efb7fce..0c525d4 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -1,6 +1,8 @@ -export class User { - public id: number - public email: string - public firstName: string - public lastName: string +export interface User { + id?: number + email: string + firstName: string + lastName: string + created: Date + updated: Date } diff --git a/src/managers/index.ts b/src/managers/index.ts new file mode 100644 index 0000000..a0007a1 --- /dev/null +++ b/src/managers/index.ts @@ -0,0 +1,2 @@ +export { UserManager } from './user-manager' +export { TaskManager } from './task-manager' diff --git a/src/managers/task-manager.ts b/src/managers/task-manager.ts new file mode 100644 index 0000000..aa11741 --- /dev/null +++ b/src/managers/task-manager.ts @@ -0,0 +1,26 @@ +import { Task } from '../entities' +import { TaskRepository } from '../repositories' + +export class TaskManager { + private repo: TaskRepository + + constructor(repo: TaskRepository) { + this.repo = repo + } + + public insert(task: Task): Promise { + return this.repo.insert(task) + } + + public update(task: Task): Promise { + return this.repo.update(task) + } + + public delete(taskId: number): Promise { + return this.repo.delete(taskId) + } + + public async findUserTasks(userId: number): Promise { + return this.repo.findByUser(userId) + } +} diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts new file mode 100644 index 0000000..3a1aed4 --- /dev/null +++ b/src/managers/user-manager.ts @@ -0,0 +1,22 @@ +import { User } from '../entities' +import { UserRepository } from '../repositories' + +export class UserManager { + private repo: UserRepository + + constructor(repo: UserRepository) { + this.repo = repo + } + + public insert(user: User): Promise { + return this.repo.insert(user) + } + + public update(user: User): Promise { + return this.repo.update(user) + } + + public async findByEmail(email: string): Promise { + return this.repo.find(email) + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts new file mode 100644 index 0000000..3eeabb5 --- /dev/null +++ b/src/repositories/index.ts @@ -0,0 +1,3 @@ +export { UserRepository } from './user-repository' +export { TaskRepository } from './task-repository' +export { UnitOfWork } from './unit-of-work' diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index e69de29..80980b1 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -0,0 +1,59 @@ +import * as knex from 'knex' +import { Task } from '../entities' + +export class TaskRepository { + private static TABLE: string = 'tasks' + private connection: knex + + constructor(connection: knex) { + this.connection = connection + } + + public async insert(task: Task): Promise { + task.created = new Date() + task.updated = new Date() + + const result = await this.connection.insert(task) + + task.id = result[0].insertId + + return task + } + + public async update(task: Task): Promise { + task.updated = new Date() + + const result = await this.connection.update({ + name: task.name, + description: task.description, + done: task.done + }) + + return task + } + + public async findByUser(userId: number): Promise { + const results = await this.connection + .select() + .from('') + .where({ user_id: userId }) + + return results.map((r: any) => this.transform(r)) + } + + public async delete(taskId: number): Promise { + await this.connection.delete().where({ id: taskId }) + } + + private transform(row: any): Task { + return { + id: row.id, + name: row.name, + description: row.description, + userId: row.user_id, + done: row.done, + created: row.created, + updated: row.updated + } + } +} diff --git a/src/repositories/unit-of-work.ts b/src/repositories/unit-of-work.ts new file mode 100644 index 0000000..2407cce --- /dev/null +++ b/src/repositories/unit-of-work.ts @@ -0,0 +1,41 @@ +import * as knex from 'knex' +import { MySql } from '../database' +import { TaskRepository, UserRepository } from './index' + +export class UnitOfWork { + private db: MySql + + constructor(db: MySql) { + this.db = db + } + + public async getConnection(): Promise { + return this.db.getConnection() + } + + public async getTransaction(): Promise { + const connection = await this.getConnection() + + return new Promise((resolve, reject) => { + try { + connection.transaction((trx: knex.Transaction) => { + resolve(trx) + }) + } catch (err) { + reject(err) + } + }) + } + + private async withinTransaction(execute: (trx: knex.Transaction) => Promise): Promise { + const trx = await this.getTransaction() + + try { + await execute(trx) + + trx.commit() + } catch (err) { + trx.rollback(err) + } + } +} diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 04223bf..427c56d 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -3,16 +3,48 @@ import { User } from '../entities' export class UserRepository { private connection: knex + private readonly TABLE: string = 'USER' - constructor(conn: knex) { - this.connection = conn + constructor(connection: knex) { + this.connection = connection } public async insert(user: User): Promise { - const inserted = await this.connection.insert({}) + user.created = new Date() + user.updated = new Date() - user.id = inserted + const result = await this.connection.insert(user) + + user.id = result[0].insertId + + return user + } + + public async update(user: User): Promise { + user.updated = new Date() + + const result = await this.connection.update({ + first_name: user.firstName, + last_name: user.lastName + }) return user } + + public async find(email: string): Promise { + const result = await this.connection.table(this.TABLE).first({ email }) + + return this.transform(result) + } + + private transform(row: any): User { + return { + id: row.id, + email: row.email, + firstName: row.first_name, + lastName: row.last_name, + created: row.created, + updated: row.updated + } + } } diff --git a/src/server/index.ts b/src/server/index.ts index 82dbfad..dbdddb6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -2,6 +2,7 @@ import * as Koa from 'koa' import * as bodyParser from 'koa-bodyparser' import * as helmet from 'koa-helmet' import * as health from './health' +import * as user from './users' export function createServer(): Koa { const app = new Koa() @@ -12,6 +13,7 @@ export function createServer(): Koa { // Register routes health.init(app) + user.init(app, null) return app } diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts new file mode 100644 index 0000000..063adde --- /dev/null +++ b/src/server/users/controller.ts @@ -0,0 +1,19 @@ +import { Context } from 'koa' +import { UserManager } from '../../managers' + +export class UserController { + private manager: UserManager + + constructor(manager: UserManager) { + this.manager = manager + } + + public async createUser(ctx: Context) { + const userDto = ctx.body + + const newUser = await this.manager.insert(userDto) + + ctx.body = newUser + ctx.status = 200 + } +} diff --git a/src/server/users/index.ts b/src/server/users/index.ts index e69de29..c44be4a 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -0,0 +1,22 @@ +import * as Joi from 'joi' +import * as Koa from 'koa' +import * as Router from 'koa-router' +// import * as middleware from '../middleware' +import { UserManager } from '../../managers' +import { UserController } from './controller' + +export function init(server: Koa, userManager: UserManager) { + const router = new Router({ prefix: '/api/v1/users' }) + + // router.use(middleware.validateSuperdataEnabled) + + const controller = new UserController(userManager) + + router.post( + '/', + // middleware.validate({ params: { id: Joi.number() } }), + controller.createUser.bind(this) + ) + + server.use(router.routes()) +} From b80da58bc32ed3fa228da24d8f1f306bf634e634 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Fri, 30 Mar 2018 19:34:35 +0100 Subject: [PATCH 03/35] add mysql --- db-scripts/create_database.sql | 1 + docker-compose.yml | 13 +++++- package.json | 8 ++-- src/container.ts | 30 ++++++++++++++ src/database/index.ts | 19 ++++----- .../20180330164632_create_schema.ts | 32 +++++++++++++++ src/database/migrations/m.ts | 19 --------- src/index.ts | 41 ++++++++++++++++--- src/repositories/task-repository.ts | 28 ++++++++----- src/repositories/user-repository.ts | 17 ++++---- src/server/index.ts | 35 +++++++++++++++- src/server/users/controller.ts | 20 ++++++++- src/server/users/index.ts | 7 ++-- 13 files changed, 206 insertions(+), 64 deletions(-) create mode 100644 db-scripts/create_database.sql create mode 100644 src/container.ts create mode 100644 src/database/migrations/20180330164632_create_schema.ts delete mode 100644 src/database/migrations/m.ts diff --git a/db-scripts/create_database.sql b/db-scripts/create_database.sql new file mode 100644 index 0000000..be7502a --- /dev/null +++ b/db-scripts/create_database.sql @@ -0,0 +1 @@ +CREATE DATABASE IF NOT EXISTS task_manager diff --git a/docker-compose.yml b/docker-compose.yml index 18a6f50..a0e7c3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,13 @@ version: '2.1' services: app: build: . + command: npm run start:dev environment: - PORT=8080 - - MYSQL_URL=mysql + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=secret ports: - "8080:8080" - "5858:5858" @@ -12,9 +16,14 @@ services: - mysql volumes: - .:/app/ + network_mode: bridge mysql: image: mysql:latest environment: - MYSQL_ROOT_PASSWORD: secret + - MYSQL_DATABASE=task_manager + - MYSQL_ROOT_PASSWORD=secret ports: - "3306:3306" + volumes: + - ./db-scripts:/docker-entrypoint-initdb.d + network_mode: bridge diff --git a/package.json b/package.json index 85fec69..5a5f0c8 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,14 @@ ], "scripts": { "build": "rm -rf dist && tsc", - "clean": "rm -rf node_modules testdata .sonar", - "coverage": "nyc --exclude dist/test --temp-directory coverage --check-coverage=false --report-dir coverage --reporter=lcov --reporter=html npm test", + "clean": "rm -rf node_modules coverage dist", + "coverage": "nyc --exclude dist/test --reporter=html npm test", "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts' --fix", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", "test": "npm run build && mocha --recursive dist/test" }, "dependencies": { - "@types/async": "^2.0.48", "async": "^2.6.0", "joi": "^13.1.2", "knex": "^0.14.4", @@ -34,6 +33,7 @@ "pino": "^4.15.0" }, "devDependencies": { + "@types/async": "^2.0.48", "@types/chai": "^4.1.2", "@types/joi": "^13.0.7", "@types/knex": "^0.14.9", @@ -64,7 +64,7 @@ }, "husky": { "hooks": { - "pre-commit": "" + "pre-commit": "npm run lint && npm test" } } } diff --git a/src/container.ts b/src/container.ts new file mode 100644 index 0000000..66a0ee1 --- /dev/null +++ b/src/container.ts @@ -0,0 +1,30 @@ +import { MySql } from './database' +import { TaskManager, UserManager } from './managers' +import { TaskRepository, UserRepository } from './repositories' + +export interface ServiceContainer { + repositories: { + task: TaskRepository + user: UserRepository + } + managers: { + task: TaskManager + user: UserManager + } +} + +export function createContainer(db: MySql): ServiceContainer { + const taskRepo = new TaskRepository(db) + const userRepo = new UserRepository(db) + + return { + repositories: { + task: taskRepo, + user: userRepo + }, + managers: { + task: new TaskManager(taskRepo), + user: new UserManager(userRepo) + } + } +} diff --git a/src/database/index.ts b/src/database/index.ts index 7ecbcf3..ca095d9 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -20,9 +20,6 @@ export class MySql { this.config = config } - /** - * Retuns a connection to the database. - */ public async getConnection(): Promise { if (!this.connection) { this.connection = await this.retryDbConnection() @@ -38,9 +35,17 @@ export class MySql { } } + public async schemaMigration() { + const connection = await this.getConnection() + + await connection.migrate.latest({ + directory: path.resolve(__dirname, './migrations') + }) + } + private async createConnection(): Promise { const config: knex.Config = { - client: 'mysql', + client: 'mysql2', connection: { host: this.config.host, port: this.config.port, @@ -97,10 +102,4 @@ export class MySql { return this.retryDbConnectionPromise } - - private async schemaMigration() { - await this.connection.migrate.latest({ - directory: path.resolve(__dirname, './migrations') - }) - } } diff --git a/src/database/migrations/20180330164632_create_schema.ts b/src/database/migrations/20180330164632_create_schema.ts new file mode 100644 index 0000000..a4cfe3d --- /dev/null +++ b/src/database/migrations/20180330164632_create_schema.ts @@ -0,0 +1,32 @@ +import * as knex from 'knex' + +export function up(db: knex) { + return db.schema + .createTable('user', table => { + table.increments('id').primary() + table.string('email', 50).notNullable() + table.string('first_name', 50).notNullable() + table.string('last_name', 50).notNullable() + table.dateTime('created').notNullable() + table.dateTime('updated').notNullable() + }) + .then(() => { + return db.schema.createTable('task', table => { + table.increments('id').primary() + table.string('name', 50).notNullable() + table.string('description').notNullable() + table.boolean('done').notNullable() + table.dateTime('created').notNullable() + table.dateTime('updated').notNullable() + table + .integer('user_id') + .unsigned() + .references('id') + .inTable('user') + }) + }) +} + +export function down(db: knex) { + return db.schema.dropTable('task').dropTable('user') +} diff --git a/src/database/migrations/m.ts b/src/database/migrations/m.ts deleted file mode 100644 index 52247b7..0000000 --- a/src/database/migrations/m.ts +++ /dev/null @@ -1,19 +0,0 @@ -// import * as knex from 'knex' - -// export function up(knex) { -// return knex.schema.createTable('user_mappings', table => { -// table.increments() -// table -// .integer('pd_user_id', 11) -// .notNullable() -// .unsigned() -// table.string('system_id', 50).notNullable() -// table.string('ext_ref', 50).notNullable() -// table.unique(['system_id', 'pd_user_id'], 'no_duplicate_marketplace_users') -// table.unique(['system_id', 'ext_ref'], 'no_duplicate_marketplace_refrences') -// }) -// } - -// export function down(knex) { -// return knex.schema.dropTable('user_mappings') -// } diff --git a/src/index.ts b/src/index.ts index d47ff77..06029f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import { Server } from 'http' import * as pino from 'pino' +import { createContainer } from './container' import { MySql } from './database' import * as server from './server' @@ -10,16 +11,32 @@ export async function init() { // Starting the HTTP server logger.info('Starting HTTP server') - const app = server.createServer().listen(process.env.PORT || 8080) + const db = new MySql({ + database: 'task_manager', + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT) || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + debug: process.env.ENV !== 'production' + }) - // Register global process events - registerProcessEvents(logger, app) + logger.info('Apply database migration') + await db.schemaMigration() + + const port = process.env.PORT || 8080 + const container = createContainer(db) + const app = server.createServer(container).listen(port) + + // Register global process events and graceful shutdown + registerProcessEvents(logger, app, db) + + logger.info(`Application running on port: ${port}`) } catch (e) { logger.error(e, 'An error occurred while initializing application.') } } -function registerProcessEvents(logger: pino.Logger, app: Server) { +function registerProcessEvents(logger: pino.Logger, app: Server, db: MySql) { process.on('uncaughtException', (error: Error) => { logger.error('UncaughtException', error) }) @@ -28,10 +45,22 @@ function registerProcessEvents(logger: pino.Logger, app: Server) { logger.info(reason, promise) }) - process.on('SIGTERM', () => { + process.on('SIGTERM', async () => { logger.info('Starting graceful shutdown') - app.close() + let exitCode = 0 + const shutdown = [server.closeServer(app), db.closeDatabase()] + + for (const s of shutdown) { + try { + await s + } catch (e) { + logger.error('Error in graceful shutdown ', e) + exitCode = 1 + } + } + + process.exit(exitCode) }) } diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index 80980b1..d227bfb 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -1,19 +1,20 @@ -import * as knex from 'knex' +import { MySql } from '../database' import { Task } from '../entities' export class TaskRepository { - private static TABLE: string = 'tasks' - private connection: knex + private readonly TABLE: string = 'tasks' + private db: MySql - constructor(connection: knex) { - this.connection = connection + constructor(db: MySql) { + this.db = db } public async insert(task: Task): Promise { task.created = new Date() task.updated = new Date() - const result = await this.connection.insert(task) + const conn = await this.db.getConnection() + const result = await conn.table(this.TABLE).insert(task) task.id = result[0].insertId @@ -23,7 +24,8 @@ export class TaskRepository { public async update(task: Task): Promise { task.updated = new Date() - const result = await this.connection.update({ + const conn = await this.db.getConnection() + const result = await conn.table(this.TABLE).update({ name: task.name, description: task.description, done: task.done @@ -33,16 +35,22 @@ export class TaskRepository { } public async findByUser(userId: number): Promise { - const results = await this.connection + const conn = await this.db.getConnection() + const results = await conn .select() - .from('') + .from(this.TABLE) .where({ user_id: userId }) return results.map((r: any) => this.transform(r)) } public async delete(taskId: number): Promise { - await this.connection.delete().where({ id: taskId }) + const conn = await this.db.getConnection() + + await conn + .from(this.TABLE) + .delete() + .where({ id: taskId }) } private transform(row: any): Task { diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 427c56d..add8ad6 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,19 +1,20 @@ -import * as knex from 'knex' +import { MySql } from '../database' import { User } from '../entities' export class UserRepository { - private connection: knex private readonly TABLE: string = 'USER' + private db: MySql - constructor(connection: knex) { - this.connection = connection + constructor(db: MySql) { + this.db = db } public async insert(user: User): Promise { user.created = new Date() user.updated = new Date() - const result = await this.connection.insert(user) + const conn = await this.db.getConnection() + const result = await conn.table(this.TABLE).insert(user) user.id = result[0].insertId @@ -23,7 +24,8 @@ export class UserRepository { public async update(user: User): Promise { user.updated = new Date() - const result = await this.connection.update({ + const conn = await this.db.getConnection() + const result = await conn.table(this.TABLE).update({ first_name: user.firstName, last_name: user.lastName }) @@ -32,7 +34,8 @@ export class UserRepository { } public async find(email: string): Promise { - const result = await this.connection.table(this.TABLE).first({ email }) + const conn = await this.db.getConnection() + const result = await conn.table(this.TABLE).first({ email }) return this.transform(result) } diff --git a/src/server/index.ts b/src/server/index.ts index dbdddb6..8c1f065 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,10 +1,13 @@ +import { ErrorCallback, retry } from 'async' +import { Server } from 'http' import * as Koa from 'koa' import * as bodyParser from 'koa-bodyparser' import * as helmet from 'koa-helmet' +import { ServiceContainer } from '../container' import * as health from './health' import * as user from './users' -export function createServer(): Koa { +export function createServer(container: ServiceContainer): Koa { const app = new Koa() // Register Middlewares @@ -13,7 +16,35 @@ export function createServer(): Koa { // Register routes health.init(app) - user.init(app, null) + user.init(app, container) return app } + +export function closeServer(server: Server): Promise { + const checkPendingRequests = (callback: ErrorCallback) => { + server.getConnections((err: Error | null, pendingRequests: number) => { + if (err) { + callback(err) + } else if (pendingRequests > 0) { + callback(Error(`Number of pending requests: ${pendingRequests}`)) + } else { + callback(undefined) + } + }) + } + + return new Promise((resolve, reject) => { + retry( + { times: 10, interval: 1000 }, + checkPendingRequests, + (error: Error | undefined) => { + if (error) { + server.close(() => reject(error)) + } else { + server.close(() => resolve()) + } + } + ) + }) +} diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index 063adde..c9b065b 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -8,7 +8,7 @@ export class UserController { this.manager = manager } - public async createUser(ctx: Context) { + public async create(ctx: Context) { const userDto = ctx.body const newUser = await this.manager.insert(userDto) @@ -16,4 +16,22 @@ export class UserController { ctx.body = newUser ctx.status = 200 } + + public async update(ctx: Context) { + const userDto = ctx.body + + const newUser = await this.manager.insert(userDto) + + ctx.body = newUser + ctx.status = 200 + } + + public async get(ctx: Context) { + const id: number = ctx.params.id + + const newUser = await this.manager.findByEmail('') + + ctx.body = newUser + ctx.status = 200 + } } diff --git a/src/server/users/index.ts b/src/server/users/index.ts index c44be4a..191b21a 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -2,20 +2,21 @@ import * as Joi from 'joi' import * as Koa from 'koa' import * as Router from 'koa-router' // import * as middleware from '../middleware' +import { ServiceContainer } from '../../container' import { UserManager } from '../../managers' import { UserController } from './controller' -export function init(server: Koa, userManager: UserManager) { +export function init(server: Koa, container: ServiceContainer) { const router = new Router({ prefix: '/api/v1/users' }) // router.use(middleware.validateSuperdataEnabled) - const controller = new UserController(userManager) + const controller = new UserController(container.managers.user) router.post( '/', // middleware.validate({ params: { id: Joi.number() } }), - controller.createUser.bind(this) + controller.create.bind(this) ) server.use(router.routes()) From 6d9d5f8529d7e0e47941684acbc60f9546a98712 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Fri, 30 Mar 2018 19:35:23 +0100 Subject: [PATCH 04/35] add mysql --- src/repositories/unit-of-work.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/repositories/unit-of-work.ts b/src/repositories/unit-of-work.ts index 2407cce..51ef7df 100644 --- a/src/repositories/unit-of-work.ts +++ b/src/repositories/unit-of-work.ts @@ -27,7 +27,9 @@ export class UnitOfWork { }) } - private async withinTransaction(execute: (trx: knex.Transaction) => Promise): Promise { + private async withinTransaction( + execute: (trx: knex.Transaction) => Promise + ): Promise { const trx = await this.getTransaction() try { From c406716ce89bde2d2b451e3618af05857c9f5756 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 31 Mar 2018 00:49:14 +0100 Subject: [PATCH 05/35] fix husky --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a5f0c8..e80a7f2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "rm -rf dist && tsc", "clean": "rm -rf node_modules coverage dist", "coverage": "nyc --exclude dist/test --reporter=html npm test", - "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts' --fix", + "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", "test": "npm run build && mocha --recursive dist/test" From b9597911e41bb151d7d0f8d5fba3111765b49f3c Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 1 Apr 2018 23:19:07 +0100 Subject: [PATCH 06/35] working on auth --- package-lock.json | 726 +++++++++++++++++- package.json | 4 + src/authentication/index.ts | 49 ++ .../20180330164632_create_schema.ts | 5 +- src/entities/user.ts | 3 + src/errors.ts | 15 + src/hasher/index.ts | 24 + src/repositories/user-repository.ts | 3 + src/server/middlewares/authentication.ts | 22 + src/server/middlewares/error-handler.ts | 9 + src/server/middlewares/log-request.ts | 25 + src/server/middlewares/logger.ts | 0 src/server/middlewares/response.ts | 0 src/server/middlewares/validator.ts | 18 + tslint.json | 3 +- 15 files changed, 876 insertions(+), 30 deletions(-) create mode 100644 src/authentication/index.ts create mode 100644 src/hasher/index.ts create mode 100644 src/server/middlewares/error-handler.ts create mode 100644 src/server/middlewares/log-request.ts delete mode 100644 src/server/middlewares/logger.ts delete mode 100644 src/server/middlewares/response.ts diff --git a/package-lock.json b/package-lock.json index b433c1f..a784201 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,14 @@ "@types/async": { "version": "2.0.48", "resolved": "https://registry.npmjs.org/@types/async/-/async-2.0.48.tgz", - "integrity": "sha512-yEyX/iGm9MWTV0JZ4oKEDf00lrehj7IkrZBhoBSNyQW8owSeV1Ule4ceo+lojao43v8grdEDekjNLghn7QyJWA==" + "integrity": "sha512-yEyX/iGm9MWTV0JZ4oKEDf00lrehj7IkrZBhoBSNyQW8owSeV1Ule4ceo+lojao43v8grdEDekjNLghn7QyJWA==", + "dev": true + }, + "@types/bcrypt": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-1.0.0.tgz", + "integrity": "sha1-LFI9oZHbfUHAbRfeI1M1yYXv/ps=", + "dev": true }, "@types/bluebird": { "version": "3.5.20", @@ -118,6 +125,15 @@ "integrity": "sha512-x7VMOrIfpqo0pMi5bIuRE+3RwMNlzE3HZLrEpebW2JmuQXeIX69/G8R90Ibs1i/gb1YvBoSlO4pMwH0VUmclGw==", "dev": true }, + "@types/jsonwebtoken": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.6.tgz", + "integrity": "sha512-SuCA16HtLqPy0yerKEvMdaEAeLRgm6zPUJE1sF7bwGq0hAO4xW9UJZxTcDBaBwr5rcz1HST5QC1+1qXQ1+R9yw==", + "dev": true, + "requires": { + "@types/node": "9.6.0" + } + }, "@types/keygrip": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.1.tgz", @@ -246,6 +262,11 @@ "@types/superagent": "3.5.7" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -255,11 +276,21 @@ "negotiator": "0.6.1" } }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "4.6.0", + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -279,6 +310,20 @@ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -318,6 +363,16 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -340,14 +395,23 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -398,8 +462,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -425,16 +488,61 @@ } } }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, + "bcrypt": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", + "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", + "requires": { + "nan": "2.6.2", + "node-pre-gyp": "0.6.36" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "2.0.3" + } + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, + "boom": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "requires": { + "hoek": "4.2.1" + }, + "dependencies": { + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -483,6 +591,11 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -524,6 +637,11 @@ "redeyed": "1.0.1" } }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, "chai": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", @@ -648,6 +766,11 @@ "type-is": "1.6.16" } }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -674,7 +797,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, "requires": { "delayed-stream": "1.0.0" } @@ -692,8 +814,12 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-disposition": { "version": "0.5.2", @@ -768,6 +894,37 @@ "which": "1.3.0" } }, + "cryptiles": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "requires": { + "boom": "5.2.0" + }, + "dependencies": { + "boom": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "requires": { + "hoek": "4.2.1" + } + }, + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "1.0.0" + } + }, "dasherize": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", @@ -800,6 +957,11 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" + }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -812,8 +974,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "delegates": { "version": "1.0.0", @@ -862,6 +1023,24 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "requires": { + "base64url": "2.0.0", + "safe-buffer": "5.1.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1111,6 +1290,16 @@ } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, "fast-diff": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", @@ -1122,6 +1311,11 @@ "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, "fast-safe-stringify": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz", @@ -1203,11 +1397,15 @@ "for-in": "1.0.2" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, "form-data": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", - "dev": true, "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.6", @@ -1247,8 +1445,43 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } }, "generate-function": { "version": "2.0.0", @@ -1272,11 +1505,18 @@ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "1.0.0" + } + }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -1311,8 +1551,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "growl": { "version": "1.10.3", @@ -1320,6 +1559,20 @@ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", "dev": true }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "requires": { + "ajv": "5.5.2", + "har-schema": "2.0.0" + } + }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1334,6 +1587,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -1363,6 +1621,24 @@ } } }, + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", + "requires": { + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.1", + "sntp": "2.1.0" + }, + "dependencies": { + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + } + } + }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", @@ -1461,6 +1737,16 @@ } } }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "1.0.0", + "jsprim": "1.4.1", + "sshpk": "1.14.1" + } + }, "husky": { "version": "0.15.0-rc.13", "resolved": "https://registry.npmjs.org/husky/-/husky-0.15.0-rc.13.tgz", @@ -1496,7 +1782,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -1597,6 +1882,14 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "1.0.1" + } + }, "is-generator-function": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", @@ -1665,6 +1958,11 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -1701,6 +1999,11 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, "jest-docblock": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", @@ -1741,18 +2044,95 @@ } } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, "json-parse-better-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", "dev": true }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonwebtoken": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz", + "integrity": "sha512-1Wxh8ADP3cNyPl8tZ95WtraHXCAyXupgc0AhMHjU9er98BV+UcKsO7OJUjfhIu0Uba9A40n1oSx8dbJYrm+EoQ==", + "requires": { + "jws": "3.1.4", + "lodash.includes": "4.3.0", + "lodash.isboolean": "3.0.3", + "lodash.isinteger": "4.0.4", + "lodash.isnumber": "3.0.3", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.once": "4.1.1", + "ms": "2.1.1", + "xtend": "4.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, "just-extend": { "version": "1.1.27", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", "dev": true }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "requires": { + "base64url": "2.0.0", + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.9", + "safe-buffer": "5.1.1" + } + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "requires": { + "base64url": "2.0.0", + "jwa": "1.1.5", + "safe-buffer": "5.1.1" + } + }, "keygrip": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", @@ -1936,6 +2316,41 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lodash.reduce": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", @@ -2051,7 +2466,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "1.1.11" } @@ -2190,6 +2604,11 @@ } } }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", @@ -2232,6 +2651,31 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" }, + "node-pre-gyp": { + "version": "0.6.36", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", + "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.6", + "request": "2.85.0", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "2.2.1", + "tar-pack": "3.4.1" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.5" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -2253,6 +2697,22 @@ "path-key": "2.0.1" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, "nyc": { "version": "11.6.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.6.0.tgz", @@ -5024,6 +5484,11 @@ } } }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5152,6 +5617,20 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -5226,8 +5705,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "2.0.1", @@ -5292,6 +5770,11 @@ "through": "2.3.8" } }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "pg-connection-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", @@ -5436,6 +5919,17 @@ "unpipe": "1.0.0" } }, + "rc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", + "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + } + }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -5506,6 +6000,35 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, + "request": { + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", + "requires": { + "aws-sign2": "0.7.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.3.2", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "2.1.0", + "qs": "6.5.1", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, "require-from-string": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", @@ -5539,6 +6062,14 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "7.1.2" + } + }, "run-node": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-0.2.0.tgz", @@ -5567,14 +6098,18 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", @@ -5619,8 +6154,7 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "sinon": { "version": "4.4.8", @@ -5790,6 +6324,21 @@ } } }, + "sntp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "requires": { + "hoek": "4.2.1" + }, + "dependencies": { + "hoek": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" + } + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5880,6 +6429,21 @@ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, + "sshpk": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -5972,11 +6536,25 @@ "safe-buffer": "5.1.1" } }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -5993,6 +6571,11 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, "superagent": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", @@ -6029,6 +6612,41 @@ "has-flag": "2.0.0" } }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "tarn": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.4.tgz", @@ -6109,6 +6727,21 @@ "hoek": "5.0.3" } }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "requires": { + "punycode": "1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, "tsc-watch": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-1.0.17.tgz", @@ -6172,6 +6805,20 @@ "tslib": "1.9.0" } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6193,6 +6840,11 @@ "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", "dev": true }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -6322,6 +6974,16 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + } + }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", @@ -6330,6 +6992,14 @@ "isexe": "2.0.0" } }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "requires": { + "string-width": "1.0.2" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index e80a7f2..6db7bf7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ }, "dependencies": { "async": "^2.6.0", + "bcrypt": "^1.0.3", "joi": "^13.1.2", + "jsonwebtoken": "^8.2.0", "knex": "^0.14.4", "koa": "^2.5.0", "koa-bodyparser": "^4.2.0", @@ -34,8 +36,10 @@ }, "devDependencies": { "@types/async": "^2.0.48", + "@types/bcrypt": "^1.0.0", "@types/chai": "^4.1.2", "@types/joi": "^13.0.7", + "@types/jsonwebtoken": "^7.2.6", "@types/knex": "^0.14.9", "@types/koa": "^2.0.44", "@types/koa-bodyparser": "^4.2.0", diff --git a/src/authentication/index.ts b/src/authentication/index.ts new file mode 100644 index 0000000..d1c7aea --- /dev/null +++ b/src/authentication/index.ts @@ -0,0 +1,49 @@ +import * as jwt from 'jsonwebtoken' +import { User } from '../entities' +import { UserManager } from '../managers' + +export interface AuthUser { + id: number + email: string + role: Role +} + +export enum Role { + user = 'user', + admin = 'admin' +} + +export interface Authenticator { + validate(token: string): Promise + authenticate(user: User) +} + +export class JWTAuthenticator implements Authenticator { + private userManager: UserManager + private secret: string + + constructor(userManager: UserManager) { + this.userManager = userManager + this.secret = process.env.SECRET_KEY || 'secret' + } + + public validate(token: string): Promise { + try { + const decode = jwt.verify(token, this.secret) + + return Promise.resolve({ + id: 1, + email: '', + role: Role.user + }) + } catch (err) { + throw err + } + } + + public authenticate(user: User): string { + return jwt.sign({ id: user.id, role: user.role }, this.secret, { + expiresIn: 60 * 60 + }) + } +} diff --git a/src/database/migrations/20180330164632_create_schema.ts b/src/database/migrations/20180330164632_create_schema.ts index a4cfe3d..1310883 100644 --- a/src/database/migrations/20180330164632_create_schema.ts +++ b/src/database/migrations/20180330164632_create_schema.ts @@ -4,7 +4,10 @@ export function up(db: knex) { return db.schema .createTable('user', table => { table.increments('id').primary() - table.string('email', 50).notNullable() + table.string('email', 50).unique() + table.string('password', 50).notNullable() + table.string('salt', 50).notNullable() + table.enum('role', ['user', 'admin']).notNullable() table.string('first_name', 50).notNullable() table.string('last_name', 50).notNullable() table.dateTime('created').notNullable() diff --git a/src/entities/user.ts b/src/entities/user.ts index 0c525d4..c6886ec 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -1,6 +1,9 @@ export interface User { id?: number email: string + password: string + salt: string + role: string firstName: string lastName: string created: Date diff --git a/src/errors.ts b/src/errors.ts index e69de29..8388851 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -0,0 +1,15 @@ +class AppError { + public code: number + public message: string + + constructor(code: number, message: string) { + this.code = code + this.message = message + } +} + +class ValidationError extends AppError { + constructor(code: number, message: string) { + super(code, message) + } +} diff --git a/src/hasher/index.ts b/src/hasher/index.ts new file mode 100644 index 0000000..73bff8f --- /dev/null +++ b/src/hasher/index.ts @@ -0,0 +1,24 @@ +import * as bcrypt from 'bcrypt' + +export interface Hasher { + hashPassword(password: string): Promise + verifyPassword(password: string, hash: string): Promise +} + +export interface HashPassword { + hash: string + salt: string +} + +class BasicHasher implements Hasher { + public async hashPassword(password: string): Promise { + const salt = '' + const hash = await bcrypt.hash('', salt) + + return { hash, salt } + } + + public verifyPassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash) + } +} diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index add8ad6..ee08a2d 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -44,6 +44,9 @@ export class UserRepository { return { id: row.id, email: row.email, + password: row.password, + salt: row.salt, + role: row.role, firstName: row.first_name, lastName: row.last_name, created: row.created, diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts index e69de29..7799161 100644 --- a/src/server/middlewares/authentication.ts +++ b/src/server/middlewares/authentication.ts @@ -0,0 +1,22 @@ +import * as jwt from 'jsonwebtoken' +import { Context } from 'koa' +import { IMiddleware } from 'koa-router' + +export function authentication(roles: string[]): IMiddleware { + return async (ctx: Context, next: () => Promise) => { + const token = ctx.headers.access_token + + try { + const decode = jwt.verify(token, process.env.SECRET_KEY || 'secret') + + ctx.state.user = { + id: 123, + role: '' + } + + await next() + } catch (err) { + throw err + } + } +} diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts new file mode 100644 index 0000000..02b000a --- /dev/null +++ b/src/server/middlewares/error-handler.ts @@ -0,0 +1,9 @@ +import { Context } from 'koa' + +export async function errorHandler(ctx: Context, next: () => Promise) { + try { + await next() + } catch (e) { + // TODO: sanitize error + } +} diff --git a/src/server/middlewares/log-request.ts b/src/server/middlewares/log-request.ts new file mode 100644 index 0000000..3955ba8 --- /dev/null +++ b/src/server/middlewares/log-request.ts @@ -0,0 +1,25 @@ +import { Context } from 'koa' +import { IMiddleware } from 'koa-router' +import { Logger } from 'pino' + +export function logRequest(logger: Logger): IMiddleware { + return async (ctx: Context, next: () => Promise) => { + const start = Date.now() + + await next() + + const message = `[${ctx.status}] ${ctx.method} ${ctx.path}` + const logData: any = { + method: ctx.method, + path: ctx.path, + statusCode: ctx.status, + timeMs: Date.now() - start + } + + if (ctx.status >= 400) { + logger.error(message, logData) + } else { + logger.info(message, logData) + } + } +} diff --git a/src/server/middlewares/logger.ts b/src/server/middlewares/logger.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/server/middlewares/response.ts b/src/server/middlewares/response.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index e69de29..0ed11f9 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -0,0 +1,18 @@ +import * as Joi from 'joi' +import { Context } from 'koa' +import { IMiddleware } from 'koa-router' + +export function validate(schema: Joi.SchemaMap): IMiddleware { + return async (ctx: Context, next: () => Promise) => { + const valResult = Joi.validate(ctx, schema, { + allowUnknown: true, + abortEarly: false + }) + + if (valResult.error) { + throw valResult.error + } + + await next() + } +} diff --git a/tslint.json b/tslint.json index 925859b..ac9ca43 100644 --- a/tslint.json +++ b/tslint.json @@ -13,6 +13,7 @@ ], "no-console": false, "interface-name": false, - "object-literal-sort-keys": false + "object-literal-sort-keys": false, + "max-classes-per-file": false } } From d2b844101a795e681a96a0fb6c9d59857484c189 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Tue, 3 Apr 2018 12:34:31 +0100 Subject: [PATCH 07/35] refactor lib --- package-lock.json | 16 ++--- package.json | 4 +- src/container.ts | 20 ++++++- src/index.ts | 4 +- src/{ => lib}/authentication/index.ts | 28 ++++----- src/{ => lib}/database/index.ts | 0 .../20180330164632_create_schema.ts | 0 src/{ => lib}/hasher/index.ts | 4 +- src/managers/user-manager.ts | 12 +++- src/repositories/index.ts | 1 - src/repositories/task-repository.ts | 2 +- src/repositories/unit-of-work.ts | 43 -------------- src/repositories/user-repository.ts | 2 +- src/server/index.ts | 2 - src/server/middlewares/authentication.ts | 14 ++--- src/server/middlewares/index.ts | 4 ++ src/server/middlewares/validator.ts | 2 +- src/server/users/controller.ts | 21 +++---- src/server/users/index.ts | 20 ++++++- src/server/users/model.ts | 39 +++++++++++++ test/index.ts | 7 --- test/integration/bootstrap.ts | 58 +++++++++++++++++++ .../server/users/create-user.test.ts | 9 +++ .../server/middlewares/log-request.test.ts | 0 24 files changed, 205 insertions(+), 107 deletions(-) rename src/{ => lib}/authentication/index.ts (55%) rename src/{ => lib}/database/index.ts (100%) rename src/{ => lib}/database/migrations/20180330164632_create_schema.ts (100%) rename src/{ => lib}/hasher/index.ts (83%) delete mode 100644 src/repositories/unit-of-work.ts create mode 100644 src/server/middlewares/index.ts create mode 100644 src/server/users/model.ts delete mode 100644 test/index.ts create mode 100644 test/integration/bootstrap.ts create mode 100644 test/integration/server/users/create-user.test.ts rename src/server/middlewares/cache.ts => test/unit/server/middlewares/log-request.test.ts (100%) diff --git a/package-lock.json b/package-lock.json index a784201..dd7c804 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6528,14 +6528,6 @@ "duplexer": "0.1.1" } }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -6546,6 +6538,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index 6db7bf7..15e5d74 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", - "test": "npm run build && mocha --recursive dist/test" + "test": "npm run build && mocha --recursive dist/test/unit/**/*.test.js", + "test:integration": "npm run build && mocha --recursive dist/test/integration/**/*.test.js --require dist/test/integration/bootstrap.js", + "test:all": "npm run build && mocha --recursive dist/test/**/*.test.js --require dist/test/integration/bootstrap.js" }, "dependencies": { "async": "^2.6.0", diff --git a/src/container.ts b/src/container.ts index 66a0ee1..6e47e40 100644 --- a/src/container.ts +++ b/src/container.ts @@ -1,8 +1,16 @@ -import { MySql } from './database' +import { Logger } from 'pino' +import { Authenticator, JWTAuthenticator } from './lib/authentication' +import { MySql } from './lib/database' +import { BCryptHasher, Hasher } from './lib/hasher' import { TaskManager, UserManager } from './managers' import { TaskRepository, UserRepository } from './repositories' export interface ServiceContainer { + logger: Logger + lib: { + hasher: Hasher + authenticator: Authenticator + } repositories: { task: TaskRepository user: UserRepository @@ -13,18 +21,24 @@ export interface ServiceContainer { } } -export function createContainer(db: MySql): ServiceContainer { +export function createContainer(db: MySql, logger: Logger): ServiceContainer { const taskRepo = new TaskRepository(db) const userRepo = new UserRepository(db) + const hasher = new BCryptHasher() return { + logger, + lib: { + hasher, + authenticator: new JWTAuthenticator(userRepo) + }, repositories: { task: taskRepo, user: userRepo }, managers: { task: new TaskManager(taskRepo), - user: new UserManager(userRepo) + user: new UserManager(userRepo, hasher) } } } diff --git a/src/index.ts b/src/index.ts index 06029f2..2bd7526 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import { Server } from 'http' import * as pino from 'pino' import { createContainer } from './container' -import { MySql } from './database' +import { MySql } from './lib/database' import * as server from './server' export async function init() { @@ -24,7 +24,7 @@ export async function init() { await db.schemaMigration() const port = process.env.PORT || 8080 - const container = createContainer(db) + const container = createContainer(db, logger) const app = server.createServer(container).listen(port) // Register global process events and graceful shutdown diff --git a/src/authentication/index.ts b/src/lib/authentication/index.ts similarity index 55% rename from src/authentication/index.ts rename to src/lib/authentication/index.ts index d1c7aea..c3b7d19 100644 --- a/src/authentication/index.ts +++ b/src/lib/authentication/index.ts @@ -1,6 +1,6 @@ import * as jwt from 'jsonwebtoken' -import { User } from '../entities' -import { UserManager } from '../managers' +import { User } from '../../entities' +import { UserRepository } from '../../repositories' export interface AuthUser { id: number @@ -19,24 +19,26 @@ export interface Authenticator { } export class JWTAuthenticator implements Authenticator { - private userManager: UserManager + private userRepo: UserRepository private secret: string - constructor(userManager: UserManager) { - this.userManager = userManager + constructor(userRepo: UserRepository) { + this.userRepo = userRepo this.secret = process.env.SECRET_KEY || 'secret' } - public validate(token: string): Promise { + public async validate(token: string): Promise { try { - const decode = jwt.verify(token, this.secret) - - return Promise.resolve({ - id: 1, - email: '', - role: Role.user - }) + const decode: any = jwt.verify(token, this.secret) + const user = await this.userRepo.find(decode.email) + + return { + id: user.id, + email: user.email, + role: user.role as Role + } } catch (err) { + // Throw correct error throw err } } diff --git a/src/database/index.ts b/src/lib/database/index.ts similarity index 100% rename from src/database/index.ts rename to src/lib/database/index.ts diff --git a/src/database/migrations/20180330164632_create_schema.ts b/src/lib/database/migrations/20180330164632_create_schema.ts similarity index 100% rename from src/database/migrations/20180330164632_create_schema.ts rename to src/lib/database/migrations/20180330164632_create_schema.ts diff --git a/src/hasher/index.ts b/src/lib/hasher/index.ts similarity index 83% rename from src/hasher/index.ts rename to src/lib/hasher/index.ts index 73bff8f..7cdb373 100644 --- a/src/hasher/index.ts +++ b/src/lib/hasher/index.ts @@ -10,10 +10,10 @@ export interface HashPassword { salt: string } -class BasicHasher implements Hasher { +export class BCryptHasher implements Hasher { public async hashPassword(password: string): Promise { const salt = '' - const hash = await bcrypt.hash('', salt) + const hash = await bcrypt.hash(password, salt) return { hash, salt } } diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index 3a1aed4..bb2bc87 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -1,14 +1,22 @@ import { User } from '../entities' +import { Hasher } from '../lib/hasher' import { UserRepository } from '../repositories' export class UserManager { private repo: UserRepository + private hasher: Hasher - constructor(repo: UserRepository) { + constructor(repo: UserRepository, hasher: Hasher) { this.repo = repo + this.hasher = hasher } - public insert(user: User): Promise { + public async create(user: User): Promise { + const hashPassword = await this.hasher.hashPassword(user.password) + + user.password = hashPassword.hash + user.salt = hashPassword.salt + return this.repo.insert(user) } diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 3eeabb5..c1eef02 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -1,3 +1,2 @@ export { UserRepository } from './user-repository' export { TaskRepository } from './task-repository' -export { UnitOfWork } from './unit-of-work' diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index d227bfb..720f7a0 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -1,5 +1,5 @@ -import { MySql } from '../database' import { Task } from '../entities' +import { MySql } from '../lib/database' export class TaskRepository { private readonly TABLE: string = 'tasks' diff --git a/src/repositories/unit-of-work.ts b/src/repositories/unit-of-work.ts deleted file mode 100644 index 51ef7df..0000000 --- a/src/repositories/unit-of-work.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as knex from 'knex' -import { MySql } from '../database' -import { TaskRepository, UserRepository } from './index' - -export class UnitOfWork { - private db: MySql - - constructor(db: MySql) { - this.db = db - } - - public async getConnection(): Promise { - return this.db.getConnection() - } - - public async getTransaction(): Promise { - const connection = await this.getConnection() - - return new Promise((resolve, reject) => { - try { - connection.transaction((trx: knex.Transaction) => { - resolve(trx) - }) - } catch (err) { - reject(err) - } - }) - } - - private async withinTransaction( - execute: (trx: knex.Transaction) => Promise - ): Promise { - const trx = await this.getTransaction() - - try { - await execute(trx) - - trx.commit() - } catch (err) { - trx.rollback(err) - } - } -} diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index ee08a2d..471a68b 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,5 +1,5 @@ -import { MySql } from '../database' import { User } from '../entities' +import { MySql } from '../lib/database' export class UserRepository { private readonly TABLE: string = 'USER' diff --git a/src/server/index.ts b/src/server/index.ts index 8c1f065..6c23a44 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,7 +1,6 @@ import { ErrorCallback, retry } from 'async' import { Server } from 'http' import * as Koa from 'koa' -import * as bodyParser from 'koa-bodyparser' import * as helmet from 'koa-helmet' import { ServiceContainer } from '../container' import * as health from './health' @@ -12,7 +11,6 @@ export function createServer(container: ServiceContainer): Koa { // Register Middlewares app.use(helmet()) - app.use(bodyParser()) // Register routes health.init(app) diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts index 7799161..6297af9 100644 --- a/src/server/middlewares/authentication.ts +++ b/src/server/middlewares/authentication.ts @@ -1,21 +1,21 @@ import * as jwt from 'jsonwebtoken' import { Context } from 'koa' import { IMiddleware } from 'koa-router' +import { Authenticator, Role } from '../../lib/authentication' -export function authentication(roles: string[]): IMiddleware { +export function authentication( + authenticator: Authenticator, + roles: Role[] +): IMiddleware { return async (ctx: Context, next: () => Promise) => { const token = ctx.headers.access_token try { - const decode = jwt.verify(token, process.env.SECRET_KEY || 'secret') - - ctx.state.user = { - id: 123, - role: '' - } + ctx.state.user = await authenticator.validate(token) await next() } catch (err) { + // TODO: Throw error throw err } } diff --git a/src/server/middlewares/index.ts b/src/server/middlewares/index.ts new file mode 100644 index 0000000..92edc83 --- /dev/null +++ b/src/server/middlewares/index.ts @@ -0,0 +1,4 @@ +export { authentication } from './authentication' +export { errorHandler } from './error-handler' +export { logRequest } from './log-request' +export { validate } from './validator' diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index 0ed11f9..c0f9760 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -2,7 +2,7 @@ import * as Joi from 'joi' import { Context } from 'koa' import { IMiddleware } from 'koa-router' -export function validate(schema: Joi.SchemaMap): IMiddleware { +export function validate(schema: Joi.ObjectSchema): IMiddleware { return async (ctx: Context, next: () => Promise) => { const valResult = Joi.validate(ctx, schema, { allowUnknown: true, diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index c9b065b..cb66725 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -1,5 +1,8 @@ import { Context } from 'koa' +import { User } from '../../entities' +import { AuthUser } from '../../lib/authentication' import { UserManager } from '../../managers' +import { CreateUser, UserModel } from './model' export class UserController { private manager: UserManager @@ -9,29 +12,27 @@ export class UserController { } public async create(ctx: Context) { - const userDto = ctx.body - - const newUser = await this.manager.insert(userDto) + const userDto: CreateUser = ctx.body + const newUser = await this.manager.create(userDto as User) - ctx.body = newUser - ctx.status = 200 + ctx.body = new UserModel(newUser) + ctx.status = 201 } public async update(ctx: Context) { const userDto = ctx.body - const newUser = await this.manager.insert(userDto) + const newUser = await this.manager.create(userDto) ctx.body = newUser ctx.status = 200 } public async get(ctx: Context) { - const id: number = ctx.params.id - - const newUser = await this.manager.findByEmail('') + const authUser: AuthUser = ctx.state.user + const user = await this.manager.findByEmail(authUser.email) - ctx.body = newUser + ctx.body = new UserModel(user) ctx.status = 200 } } diff --git a/src/server/users/index.ts b/src/server/users/index.ts index 191b21a..53a128d 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -1,23 +1,37 @@ import * as Joi from 'joi' import * as Koa from 'koa' +import * as bodyParser from 'koa-bodyparser' import * as Router from 'koa-router' -// import * as middleware from '../middleware' import { ServiceContainer } from '../../container' +import { Role } from '../../lib/authentication' import { UserManager } from '../../managers' +import * as middleware from '../middlewares' import { UserController } from './controller' +import { createUserModel } from './model' export function init(server: Koa, container: ServiceContainer) { const router = new Router({ prefix: '/api/v1/users' }) - // router.use(middleware.validateSuperdataEnabled) + router.use(middleware.logRequest(container.logger)) + router.use(middleware.errorHandler) const controller = new UserController(container.managers.user) router.post( '/', - // middleware.validate({ params: { id: Joi.number() } }), + bodyParser(), + middleware.validate(createUserModel), controller.create.bind(this) ) + router.get( + '/me', + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), + controller.get.bind(this) + ) + server.use(router.routes()) } diff --git a/src/server/users/model.ts b/src/server/users/model.ts new file mode 100644 index 0000000..abe670d --- /dev/null +++ b/src/server/users/model.ts @@ -0,0 +1,39 @@ +import * as Joi from 'joi' +import { User } from '../../entities' + +export interface CreateUser { + email: string + password: string + firstName: string + lastName: string +} + +export const createUserModel = Joi.object().keys({ + email: Joi.string() + .email() + .trim() + .required(), + password: Joi.string() + .trim() + .required(), + firstName: Joi.string().required(), + lastName: Joi.string().required() +}) + +export class UserModel { + public id: number + public email: string + public firstName: string + public lastName: string + public created: Date + public updated: Date + + constructor(user: User) { + this.id = user.id + this.email = user.email + this.firstName = user.firstName + this.lastName = user.lastName + this.created = user.created + this.updated = user.updated + } +} diff --git a/test/index.ts b/test/index.ts deleted file mode 100644 index 9646fe7..0000000 --- a/test/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect } from 'chai' - -describe('Just pass', () => { - it('must pass', () => { - expect(true) - }) -}) diff --git a/test/integration/bootstrap.ts b/test/integration/bootstrap.ts new file mode 100644 index 0000000..66d8481 --- /dev/null +++ b/test/integration/bootstrap.ts @@ -0,0 +1,58 @@ +import * as Koa from 'koa' +import * as pino from 'pino' +import { ServiceContainer } from '../../src/container' +import { Authenticator, JWTAuthenticator } from '../../src/lib/authentication' +import { Configuration, MySql } from '../../src/lib/database' +import { BCryptHasher, Hasher } from '../../src/lib/hasher' +import { TaskManager, UserManager } from '../../src/managers' +import { TaskRepository, UserRepository } from '../../src/repositories' +import { closeServer, createServer } from '../../src/server' + +const mysqlConfig: Configuration = { + database: 'task-manager-test', + host: 'mysql', + port: 3306, + user: 'root', + password: 'secret', + debug: true +} + +function createTestServer(logger: pino.Logger, db: MySql): Koa { + const taskRepo = new TaskRepository(db) + const userRepo = new UserRepository(db) + const hasher = new BCryptHasher() + + const container: ServiceContainer = { + logger, + lib: { + hasher, + authenticator: new JWTAuthenticator(userRepo) + }, + repositories: { + task: taskRepo, + user: userRepo + }, + managers: { + task: new TaskManager(taskRepo), + user: new UserManager(userRepo, hasher) + } + } + + return createServer(container) +} + +const log = pino({ name: 'test' }) +const database: MySql = new MySql(mysqlConfig) +const testServer = createTestServer(log, database).listen(1999) + +process.on('exit', async () => { + const shutdown = [closeServer(testServer), database.closeDatabase()] + + for (const s of shutdown) { + try { + await s + } catch (e) { + log.error('Error in graceful shutdown ', e) + } + } +}) diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts new file mode 100644 index 0000000..35ebcb1 --- /dev/null +++ b/test/integration/server/users/create-user.test.ts @@ -0,0 +1,9 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { createServer } from '../../../../src/server' + +describe('Create user', () => { + it('must pass', () => { + expect(true) + }) +}) diff --git a/src/server/middlewares/cache.ts b/test/unit/server/middlewares/log-request.test.ts similarity index 100% rename from src/server/middlewares/cache.ts rename to test/unit/server/middlewares/log-request.test.ts From a7e6e24ee5a893783a2766ade4eebd0d8adc07d0 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Wed, 4 Apr 2018 00:03:10 +0100 Subject: [PATCH 08/35] working on api --- Dockerfile | 2 +- package-lock.json | 620 ++---------------------- package.json | 4 +- src/lib/hasher/index.ts | 2 +- src/server/middlewares/error-handler.ts | 1 + src/server/middlewares/validator.ts | 3 +- src/server/users/controller.ts | 2 +- src/server/users/index.ts | 2 +- test/integration/bootstrap.ts | 2 +- 9 files changed, 45 insertions(+), 593 deletions(-) diff --git a/Dockerfile b/Dockerfile index f0401ea..f4f7bd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:8-alpine +FROM node:8.10-alpine USER nobody diff --git a/package-lock.json b/package-lock.json index dd7c804..400fc7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,10 +28,10 @@ "integrity": "sha512-yEyX/iGm9MWTV0JZ4oKEDf00lrehj7IkrZBhoBSNyQW8owSeV1Ule4ceo+lojao43v8grdEDekjNLghn7QyJWA==", "dev": true }, - "@types/bcrypt": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-1.0.0.tgz", - "integrity": "sha1-LFI9oZHbfUHAbRfeI1M1yYXv/ps=", + "@types/bcryptjs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.1.tgz", + "integrity": "sha512-CVJ8ExtzUQJzLJbEk/lWrHD3MTvstTodjWidcH23gCii5WSD0z1TPSLqSdtbn5eCDw+DxfKgoUALi+loe8ftXA==", "dev": true }, "@types/bluebird": { @@ -262,11 +262,6 @@ "@types/superagent": "3.5.7" } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, "accepts": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", @@ -276,21 +271,11 @@ "negotiator": "0.6.1" } }, - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.1.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -310,20 +295,6 @@ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.3" - } - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -363,16 +334,6 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -395,23 +356,14 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "atob": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -462,7 +414,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "base": { "version": "0.11.2", @@ -493,56 +446,21 @@ "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" }, - "bcrypt": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", - "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", - "requires": { - "nan": "2.6.2", - "node-pre-gyp": "0.6.36" - } - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.3" - } + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.1" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -637,11 +555,6 @@ "redeyed": "1.0.1" } }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, "chai": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", @@ -766,11 +679,6 @@ "type-is": "1.6.16" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -797,6 +705,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, "requires": { "delayed-stream": "1.0.0" } @@ -814,12 +723,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "content-disposition": { "version": "0.5.2", @@ -894,37 +799,6 @@ "which": "1.3.0" } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.1" - } - }, - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, "dasherize": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", @@ -957,11 +831,6 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, "define-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", @@ -974,7 +843,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "delegates": { "version": "1.0.0", @@ -1023,15 +893,6 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, "ecdsa-sig-formatter": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", @@ -1290,16 +1151,6 @@ } } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" - }, "fast-diff": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", @@ -1311,11 +1162,6 @@ "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, "fast-safe-stringify": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.3.tgz", @@ -1397,15 +1243,11 @@ "for-in": "1.0.2" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, "form-data": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "dev": true, "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.6", @@ -1445,43 +1287,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "generate-function": { "version": "2.0.0", @@ -1505,18 +1312,11 @@ "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -1551,7 +1351,8 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true }, "growl": { "version": "1.10.3", @@ -1559,20 +1360,6 @@ "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", "dev": true }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.2", - "har-schema": "2.0.0" - } - }, "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", @@ -1587,11 +1374,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -1621,24 +1403,6 @@ } } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.1", - "sntp": "2.1.0" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, "he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", @@ -1737,16 +1501,6 @@ } } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.14.1" - } - }, "husky": { "version": "0.15.0-rc.13", "resolved": "https://registry.npmjs.org/husky/-/husky-0.15.0-rc.13.tgz", @@ -1782,6 +1536,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -1882,14 +1637,6 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, "is-generator-function": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", @@ -1958,11 +1705,6 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", @@ -1999,11 +1741,6 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, "jest-docblock": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz", @@ -2044,33 +1781,12 @@ } } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, "json-parse-better-errors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, "jsonwebtoken": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.2.0.tgz", @@ -2095,17 +1811,6 @@ } } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, "just-extend": { "version": "1.1.27", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", @@ -2466,6 +2171,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, "requires": { "brace-expansion": "1.1.11" } @@ -2604,11 +2310,6 @@ } } }, - "nan": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", - "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" - }, "nanomatch": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", @@ -2651,31 +2352,6 @@ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" }, - "node-pre-gyp": { - "version": "0.6.36", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", - "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", - "requires": { - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.6", - "request": "2.85.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.5" - } - }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -2697,22 +2373,6 @@ "path-key": "2.0.1" } }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, "nyc": { "version": "11.6.0", "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.6.0.tgz", @@ -5484,11 +5144,6 @@ } } }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5617,20 +5272,6 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -5705,7 +5346,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "2.0.1", @@ -5770,11 +5412,6 @@ "through": "2.3.8" } }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, "pg-connection-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", @@ -5919,17 +5556,6 @@ "unpipe": "1.0.0" } }, - "rc": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.6.tgz", - "integrity": "sha1-6xiYnG1PTxYsOZ953dKfODVWgJI=", - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - } - }, "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -6000,35 +5626,6 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, - "request": { - "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", - "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.6", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.2", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.18", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.4", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, "require-from-string": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.1.tgz", @@ -6062,14 +5659,6 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "requires": { - "glob": "7.1.2" - } - }, "run-node": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/run-node/-/run-node-0.2.0.tgz", @@ -6098,18 +5687,14 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true }, "seq-queue": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=" }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", @@ -6154,7 +5739,8 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true }, "sinon": { "version": "4.4.8", @@ -6324,21 +5910,6 @@ } } }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.1" - }, - "dependencies": { - "hoek": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", - "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" - } - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6429,21 +6000,6 @@ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -6528,16 +6084,6 @@ "duplexer": "0.1.1" } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", @@ -6546,15 +6092,11 @@ "safe-buffer": "5.1.1" } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "2.1.1" } @@ -6571,11 +6113,6 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, "superagent": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", @@ -6612,41 +6149,6 @@ "has-flag": "2.0.0" } }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } - } - }, "tarn": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/tarn/-/tarn-1.1.4.tgz", @@ -6727,21 +6229,6 @@ "hoek": "5.0.3" } }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "requires": { - "punycode": "1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - } - } - }, "tsc-watch": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/tsc-watch/-/tsc-watch-1.0.17.tgz", @@ -6805,20 +6292,6 @@ "tslib": "1.9.0" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6840,11 +6313,6 @@ "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", "dev": true }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -6974,16 +6442,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", @@ -6992,14 +6450,6 @@ "isexe": "2.0.0" } }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "requires": { - "string-width": "1.0.2" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 15e5d74..531dc3d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "async": "^2.6.0", - "bcrypt": "^1.0.3", + "bcryptjs": "^2.4.3", "joi": "^13.1.2", "jsonwebtoken": "^8.2.0", "knex": "^0.14.4", @@ -38,7 +38,7 @@ }, "devDependencies": { "@types/async": "^2.0.48", - "@types/bcrypt": "^1.0.0", + "@types/bcryptjs": "^2.4.1", "@types/chai": "^4.1.2", "@types/joi": "^13.0.7", "@types/jsonwebtoken": "^7.2.6", diff --git a/src/lib/hasher/index.ts b/src/lib/hasher/index.ts index 7cdb373..3e36efe 100644 --- a/src/lib/hasher/index.ts +++ b/src/lib/hasher/index.ts @@ -1,4 +1,4 @@ -import * as bcrypt from 'bcrypt' +import * as bcrypt from 'bcryptjs' export interface Hasher { hashPassword(password: string): Promise diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts index 02b000a..c9e21ad 100644 --- a/src/server/middlewares/error-handler.ts +++ b/src/server/middlewares/error-handler.ts @@ -5,5 +5,6 @@ export async function errorHandler(ctx: Context, next: () => Promise) { await next() } catch (e) { // TODO: sanitize error + console.log(e) } } diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index c0f9760..41b9bfd 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -1,10 +1,11 @@ import * as Joi from 'joi' import { Context } from 'koa' +import * as bodyParser from 'koa-bodyparser' import { IMiddleware } from 'koa-router' export function validate(schema: Joi.ObjectSchema): IMiddleware { return async (ctx: Context, next: () => Promise) => { - const valResult = Joi.validate(ctx, schema, { + const valResult = Joi.validate(ctx.request.body, schema, { allowUnknown: true, abortEarly: false }) diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index cb66725..aefb7ae 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -12,7 +12,7 @@ export class UserController { } public async create(ctx: Context) { - const userDto: CreateUser = ctx.body + const userDto: CreateUser = ctx.request.body const newUser = await this.manager.create(userDto as User) ctx.body = new UserModel(newUser) diff --git a/src/server/users/index.ts b/src/server/users/index.ts index 53a128d..c32ddaa 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -21,7 +21,7 @@ export function init(server: Koa, container: ServiceContainer) { '/', bodyParser(), middleware.validate(createUserModel), - controller.create.bind(this) + controller.create.bind(controller) ) router.get( diff --git a/test/integration/bootstrap.ts b/test/integration/bootstrap.ts index 66d8481..4977288 100644 --- a/test/integration/bootstrap.ts +++ b/test/integration/bootstrap.ts @@ -9,7 +9,7 @@ import { TaskRepository, UserRepository } from '../../src/repositories' import { closeServer, createServer } from '../../src/server' const mysqlConfig: Configuration = { - database: 'task-manager-test', + database: 'task-manager', host: 'mysql', port: 3306, user: 'root', From 02af84641a08655901aa1da4789881a2644a4d50 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Wed, 4 Apr 2018 23:09:30 +0100 Subject: [PATCH 09/35] fix users routes --- src/container.ts | 5 ++-- src/entities/user.ts | 1 - src/lib/authentication/index.ts | 12 ++++++---- .../20180330164632_create_schema.ts | 12 +++++----- src/lib/hasher/index.ts | 14 ++++------- src/managers/user-manager.ts | 18 ++++++++++++--- src/repositories/task-repository.ts | 2 +- src/repositories/user-repository.ts | 18 +++++++++++---- src/server/middlewares/error-handler.ts | 2 ++ src/server/middlewares/validator.ts | 18 +++++++++++++-- src/server/users/controller.ts | 9 ++++++++ src/server/users/index.ts | 13 ++++++++--- src/server/users/model.ts | 13 ----------- src/server/users/validators.ts | 23 +++++++++++++++++++ test/integration/bootstrap.ts | 5 ++-- 15 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 src/server/users/validators.ts diff --git a/src/container.ts b/src/container.ts index 6e47e40..7964bcd 100644 --- a/src/container.ts +++ b/src/container.ts @@ -25,12 +25,13 @@ export function createContainer(db: MySql, logger: Logger): ServiceContainer { const taskRepo = new TaskRepository(db) const userRepo = new UserRepository(db) const hasher = new BCryptHasher() + const authenticator = new JWTAuthenticator(userRepo) return { logger, lib: { hasher, - authenticator: new JWTAuthenticator(userRepo) + authenticator }, repositories: { task: taskRepo, @@ -38,7 +39,7 @@ export function createContainer(db: MySql, logger: Logger): ServiceContainer { }, managers: { task: new TaskManager(taskRepo), - user: new UserManager(userRepo, hasher) + user: new UserManager(userRepo, hasher, authenticator) } } } diff --git a/src/entities/user.ts b/src/entities/user.ts index c6886ec..2913fbb 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -2,7 +2,6 @@ export interface User { id?: number email: string password: string - salt: string role: string firstName: string lastName: string diff --git a/src/lib/authentication/index.ts b/src/lib/authentication/index.ts index c3b7d19..8398006 100644 --- a/src/lib/authentication/index.ts +++ b/src/lib/authentication/index.ts @@ -15,7 +15,7 @@ export enum Role { export interface Authenticator { validate(token: string): Promise - authenticate(user: User) + authenticate(user: User): string } export class JWTAuthenticator implements Authenticator { @@ -44,8 +44,12 @@ export class JWTAuthenticator implements Authenticator { } public authenticate(user: User): string { - return jwt.sign({ id: user.id, role: user.role }, this.secret, { - expiresIn: 60 * 60 - }) + return jwt.sign( + { id: user.id, email: user.email, role: user.role }, + this.secret, + { + expiresIn: 60 * 60 + } + ) } } diff --git a/src/lib/database/migrations/20180330164632_create_schema.ts b/src/lib/database/migrations/20180330164632_create_schema.ts index 1310883..4875fc3 100644 --- a/src/lib/database/migrations/20180330164632_create_schema.ts +++ b/src/lib/database/migrations/20180330164632_create_schema.ts @@ -4,19 +4,19 @@ export function up(db: knex) { return db.schema .createTable('user', table => { table.increments('id').primary() - table.string('email', 50).unique() - table.string('password', 50).notNullable() - table.string('salt', 50).notNullable() + table.string('email', 64).unique() + table.string('password', 256).notNullable() + table.string('salt', 256).notNullable() table.enum('role', ['user', 'admin']).notNullable() - table.string('first_name', 50).notNullable() - table.string('last_name', 50).notNullable() + table.string('first_name', 64).notNullable() + table.string('last_name', 64).notNullable() table.dateTime('created').notNullable() table.dateTime('updated').notNullable() }) .then(() => { return db.schema.createTable('task', table => { table.increments('id').primary() - table.string('name', 50).notNullable() + table.string('name', 64).notNullable() table.string('description').notNullable() table.boolean('done').notNullable() table.dateTime('created').notNullable() diff --git a/src/lib/hasher/index.ts b/src/lib/hasher/index.ts index 3e36efe..45ad1d9 100644 --- a/src/lib/hasher/index.ts +++ b/src/lib/hasher/index.ts @@ -1,21 +1,15 @@ import * as bcrypt from 'bcryptjs' export interface Hasher { - hashPassword(password: string): Promise + hashPassword(password: string): Promise verifyPassword(password: string, hash: string): Promise } -export interface HashPassword { - hash: string - salt: string -} - export class BCryptHasher implements Hasher { - public async hashPassword(password: string): Promise { - const salt = '' - const hash = await bcrypt.hash(password, salt) + public async hashPassword(password: string): Promise { + const salt = bcrypt.genSaltSync(10) - return { hash, salt } + return bcrypt.hash(password, salt) } public verifyPassword(password: string, hash: string): Promise { diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index bb2bc87..2893667 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -1,25 +1,37 @@ import { User } from '../entities' +import { Authenticator } from '../lib/authentication' import { Hasher } from '../lib/hasher' import { UserRepository } from '../repositories' export class UserManager { private repo: UserRepository private hasher: Hasher + private auth: Authenticator - constructor(repo: UserRepository, hasher: Hasher) { + constructor(repo: UserRepository, hasher: Hasher, auth: Authenticator) { this.repo = repo this.hasher = hasher + this.auth = auth } public async create(user: User): Promise { const hashPassword = await this.hasher.hashPassword(user.password) - user.password = hashPassword.hash - user.salt = hashPassword.salt + user.password = hashPassword return this.repo.insert(user) } + public async login(email: string, password: string): Promise { + const user = await this.repo.find(email) + + if (this.hasher.verifyPassword(password, user.password)) { + return this.auth.authenticate(user) + } + + throw new Error('Wrong credentials') + } + public update(user: User): Promise { return this.repo.update(user) } diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index 720f7a0..b714284 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -2,7 +2,7 @@ import { Task } from '../entities' import { MySql } from '../lib/database' export class TaskRepository { - private readonly TABLE: string = 'tasks' + private readonly TABLE: string = 'task' private db: MySql constructor(db: MySql) { diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 471a68b..82e7e1c 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -2,7 +2,7 @@ import { User } from '../entities' import { MySql } from '../lib/database' export class UserRepository { - private readonly TABLE: string = 'USER' + private readonly TABLE: string = 'user' private db: MySql constructor(db: MySql) { @@ -14,7 +14,15 @@ export class UserRepository { user.updated = new Date() const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).insert(user) + const result = await conn.table(this.TABLE).insert({ + email: user.email, + password: user.password, + role: user.role, + first_name: user.firstName, + last_name: user.lastName, + created: user.created, + updated: user.updated + }) user.id = result[0].insertId @@ -35,7 +43,10 @@ export class UserRepository { public async find(email: string): Promise { const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).first({ email }) + const result = await conn + .table(this.TABLE) + .where({ email }) + .first() return this.transform(result) } @@ -45,7 +56,6 @@ export class UserRepository { id: row.id, email: row.email, password: row.password, - salt: row.salt, role: row.role, firstName: row.first_name, lastName: row.last_name, diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts index c9e21ad..13f06fc 100644 --- a/src/server/middlewares/error-handler.ts +++ b/src/server/middlewares/error-handler.ts @@ -6,5 +6,7 @@ export async function errorHandler(ctx: Context, next: () => Promise) { } catch (e) { // TODO: sanitize error console.log(e) + + throw e } } diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index 41b9bfd..8d086b2 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -3,9 +3,23 @@ import { Context } from 'koa' import * as bodyParser from 'koa-bodyparser' import { IMiddleware } from 'koa-router' -export function validate(schema: Joi.ObjectSchema): IMiddleware { +export interface SchemaMap { + params?: { [key: string]: Joi.SchemaLike } + + request?: { + body?: { [key: string]: Joi.SchemaLike } | Joi.ArraySchema + headers?: { [key: string]: Joi.SchemaLike } + } + + response?: { + body?: { [key: string]: Joi.SchemaLike } | Joi.ArraySchema + headers?: { [key: string]: Joi.SchemaLike } + } +} + +export function validate(schema: SchemaMap): IMiddleware { return async (ctx: Context, next: () => Promise) => { - const valResult = Joi.validate(ctx.request.body, schema, { + const valResult = Joi.validate(ctx, schema, { allowUnknown: true, abortEarly: false }) diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index aefb7ae..f69f9c6 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -19,6 +19,15 @@ export class UserController { ctx.status = 201 } + public async login(ctx: Context) { + ctx.body = { + token: await this.manager.login( + ctx.request.body.email, + ctx.request.body.password + ) + } + } + public async update(ctx: Context) { const userDto = ctx.body diff --git a/src/server/users/index.ts b/src/server/users/index.ts index c32ddaa..3a3b2a8 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -7,7 +7,7 @@ import { Role } from '../../lib/authentication' import { UserManager } from '../../managers' import * as middleware from '../middlewares' import { UserController } from './controller' -import { createUserModel } from './model' +import * as validators from './validators' export function init(server: Koa, container: ServiceContainer) { const router = new Router({ prefix: '/api/v1/users' }) @@ -20,17 +20,24 @@ export function init(server: Koa, container: ServiceContainer) { router.post( '/', bodyParser(), - middleware.validate(createUserModel), + middleware.validate({ request: { body: validators.createUser } }), controller.create.bind(controller) ) + router.post( + '/login', + bodyParser(), + middleware.validate({ request: { body: validators.login } }), + controller.login.bind(controller) + ) + router.get( '/me', middleware.authentication(container.lib.authenticator, [ Role.user, Role.admin ]), - controller.get.bind(this) + controller.get.bind(controller) ) server.use(router.routes()) diff --git a/src/server/users/model.ts b/src/server/users/model.ts index abe670d..750353c 100644 --- a/src/server/users/model.ts +++ b/src/server/users/model.ts @@ -1,4 +1,3 @@ -import * as Joi from 'joi' import { User } from '../../entities' export interface CreateUser { @@ -8,18 +7,6 @@ export interface CreateUser { lastName: string } -export const createUserModel = Joi.object().keys({ - email: Joi.string() - .email() - .trim() - .required(), - password: Joi.string() - .trim() - .required(), - firstName: Joi.string().required(), - lastName: Joi.string().required() -}) - export class UserModel { public id: number public email: string diff --git a/src/server/users/validators.ts b/src/server/users/validators.ts new file mode 100644 index 0000000..b66ab47 --- /dev/null +++ b/src/server/users/validators.ts @@ -0,0 +1,23 @@ +import * as Joi from 'joi' + +export const createUser: Joi.SchemaMap = { + email: Joi.string() + .email() + .trim() + .required(), + password: Joi.string() + .trim() + .required(), + firstName: Joi.string().required(), + lastName: Joi.string().required() +} + +export const login: Joi.SchemaMap = { + email: Joi.string() + .email() + .trim() + .required(), + password: Joi.string() + .trim() + .required() +} diff --git a/test/integration/bootstrap.ts b/test/integration/bootstrap.ts index 4977288..198efd3 100644 --- a/test/integration/bootstrap.ts +++ b/test/integration/bootstrap.ts @@ -21,12 +21,13 @@ function createTestServer(logger: pino.Logger, db: MySql): Koa { const taskRepo = new TaskRepository(db) const userRepo = new UserRepository(db) const hasher = new BCryptHasher() + const authenticator = new JWTAuthenticator(userRepo) const container: ServiceContainer = { logger, lib: { hasher, - authenticator: new JWTAuthenticator(userRepo) + authenticator }, repositories: { task: taskRepo, @@ -34,7 +35,7 @@ function createTestServer(logger: pino.Logger, db: MySql): Koa { }, managers: { task: new TaskManager(taskRepo), - user: new UserManager(userRepo, hasher) + user: new UserManager(userRepo, hasher, authenticator) } } From ec9ddf1c32698e6d36c40b2cc5941a850f055a93 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Thu, 5 Apr 2018 22:31:20 +0100 Subject: [PATCH 10/35] add users endpoints --- src/managers/user-manager.ts | 9 +++++++++ src/repositories/user-repository.ts | 17 ++++++++++++++++- src/server/users/controller.ts | 18 +++++++++++++++--- src/server/users/index.ts | 22 ++++++++++++++++++++++ src/server/users/validators.ts | 5 +++++ 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index 2893667..ed16695 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -36,6 +36,15 @@ export class UserManager { return this.repo.update(user) } + public async changePassword( + email: string, + newPassword: string + ): Promise { + const hashPassword = await this.hasher.hashPassword(newPassword) + + return this.repo.changePassword(email, newPassword) + } + public async findByEmail(email: string): Promise { return this.repo.find(email) } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 82e7e1c..108edc3 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -35,12 +35,27 @@ export class UserRepository { const conn = await this.db.getConnection() const result = await conn.table(this.TABLE).update({ first_name: user.firstName, - last_name: user.lastName + last_name: user.lastName, + password: user.password }) return user } + public async changePassword( + email: string, + newPassword: string + ): Promise { + const conn = await this.db.getConnection() + const result = await conn + .table(this.TABLE) + .update({ + password: newPassword, + updated: new Date() + }) + .where('email', email) + } + public async find(email: string): Promise { const conn = await this.db.getConnection() const result = await conn diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index f69f9c6..39494e7 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -29,14 +29,26 @@ export class UserController { } public async update(ctx: Context) { - const userDto = ctx.body + const userDto = ctx.request.body + const user = await this.manager.findByEmail(ctx.state.user.email) - const newUser = await this.manager.create(userDto) + user.firstName = userDto.firstName + user.lastName = userDto.lastName - ctx.body = newUser + const updatedUser = await this.manager.update(user) + + ctx.body = new UserModel(updatedUser) ctx.status = 200 } + public async changePassword(ctx: Context) { + const newPassword = ctx.request.body.password + + await this.manager.changePassword(ctx.state.user.email, newPassword) + + ctx.status = 204 + } + public async get(ctx: Context) { const authUser: AuthUser = ctx.state.user const user = await this.manager.findByEmail(authUser.email) diff --git a/src/server/users/index.ts b/src/server/users/index.ts index 3a3b2a8..3eb0eae 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -31,6 +31,28 @@ export function init(server: Koa, container: ServiceContainer) { controller.login.bind(controller) ) + router.put( + '/', + bodyParser(), + middleware.validate({ request: { body: validators.login } }), + controller.update.bind(controller) + ) + + router.put( + '/password', + bodyParser(), + middleware.validate({ + request: { + body: { + password: Joi.string() + .trim() + .required() + } + } + }), + controller.changePassword.bind(controller) + ) + router.get( '/me', middleware.authentication(container.lib.authenticator, [ diff --git a/src/server/users/validators.ts b/src/server/users/validators.ts index b66ab47..be1adea 100644 --- a/src/server/users/validators.ts +++ b/src/server/users/validators.ts @@ -12,6 +12,11 @@ export const createUser: Joi.SchemaMap = { lastName: Joi.string().required() } +export const updateUser: Joi.SchemaMap = { + firstName: Joi.string().required(), + lastName: Joi.string().required() +} + export const login: Joi.SchemaMap = { email: Joi.string() .email() From d1a4934ad89dc3c208f02338238f2ca48b81e87f Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Fri, 6 Apr 2018 10:20:57 +0100 Subject: [PATCH 11/35] working on error --- src/errors.ts | 28 +++++++++-- src/managers/task-manager.ts | 22 ++++++--- src/managers/user-manager.ts | 8 +-- src/repositories/task-repository.ts | 41 +++++++++++----- src/repositories/user-repository.ts | 20 ++++---- src/server/middlewares/error-handler.ts | 22 +++++++-- src/server/tasks/controller.ts | 65 +++++++++++++++++++++++++ src/server/tasks/index.ts | 64 ++++++++++++++++++++++++ src/server/tasks/model.ts | 24 +++++++++ src/server/tasks/validators.ts | 7 +++ src/server/users/index.ts | 18 +++---- 11 files changed, 269 insertions(+), 50 deletions(-) create mode 100644 src/server/tasks/controller.ts create mode 100644 src/server/tasks/model.ts create mode 100644 src/server/tasks/validators.ts diff --git a/src/errors.ts b/src/errors.ts index 8388851..b6ec5cb 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,15 +1,35 @@ -class AppError { +export class AppError { public code: number public message: string + public error: Error - constructor(code: number, message: string) { + constructor(code: number, message: string, error?: Error) { this.code = code this.message = message + this.error = error + } +} + +export class NotFoundError extends AppError { + constructor(message: string) { + super(20000, message) + } +} + +export class ValidationError extends AppError { + constructor(code: number, message: string, error: Error) { + super(30000, message, error) + } +} + +export class UnauthorizedError extends AppError { + constructor(code: number, message: string) { + super(30001, message) } } -class ValidationError extends AppError { +export class PermissionError extends AppError { constructor(code: number, message: string) { - super(code, message) + super(30002, message) } } diff --git a/src/managers/task-manager.ts b/src/managers/task-manager.ts index aa11741..7050d2f 100644 --- a/src/managers/task-manager.ts +++ b/src/managers/task-manager.ts @@ -8,7 +8,19 @@ export class TaskManager { this.repo = repo } - public insert(task: Task): Promise { + public find(id: number): Promise { + return this.repo.find(id) + } + + public async findUserTasks( + email: string, + limit: number, + offset: number + ): Promise { + return this.repo.findByUser(email, limit, offset) + } + + public create(task: Task): Promise { return this.repo.insert(task) } @@ -16,11 +28,7 @@ export class TaskManager { return this.repo.update(task) } - public delete(taskId: number): Promise { - return this.repo.delete(taskId) - } - - public async findUserTasks(userId: number): Promise { - return this.repo.findByUser(userId) + public delete(email: string, taskId: number): Promise { + return this.repo.delete(email, taskId) } } diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index ed16695..fc582f2 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -14,6 +14,10 @@ export class UserManager { this.auth = auth } + public async findByEmail(email: string): Promise { + return this.repo.find(email) + } + public async create(user: User): Promise { const hashPassword = await this.hasher.hashPassword(user.password) @@ -44,8 +48,4 @@ export class UserManager { return this.repo.changePassword(email, newPassword) } - - public async findByEmail(email: string): Promise { - return this.repo.find(email) - } } diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index b714284..e7fa97c 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -9,6 +9,34 @@ export class TaskRepository { this.db = db } + public async find(id: number): Promise { + const conn = await this.db.getConnection() + const row = await conn + .select() + .from(this.TABLE) + .where({ id }) + .first() + + return this.transform(row) + } + + public async findByUser( + email: string, + limit: number, + offset: number + ): Promise { + const conn = await this.db.getConnection() + const results = await conn + .select() + .from(this.TABLE) + .where({ email }) + .orderBy('updated', 'DESC') + .offset(offset) + .limit(limit) + + return results.map((r: any) => this.transform(r)) + } + public async insert(task: Task): Promise { task.created = new Date() task.updated = new Date() @@ -34,23 +62,14 @@ export class TaskRepository { return task } - public async findByUser(userId: number): Promise { - const conn = await this.db.getConnection() - const results = await conn - .select() - .from(this.TABLE) - .where({ user_id: userId }) - - return results.map((r: any) => this.transform(r)) - } - - public async delete(taskId: number): Promise { + public async delete(email: string, taskId: number): Promise { const conn = await this.db.getConnection() await conn .from(this.TABLE) .delete() .where({ id: taskId }) + .andWhere({ email }) } private transform(row: any): Task { diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 108edc3..86f17f8 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -9,6 +9,16 @@ export class UserRepository { this.db = db } + public async find(email: string): Promise { + const conn = await this.db.getConnection() + const row = await conn + .table(this.TABLE) + .where({ email }) + .first() + + return this.transform(row) + } + public async insert(user: User): Promise { user.created = new Date() user.updated = new Date() @@ -56,16 +66,6 @@ export class UserRepository { .where('email', email) } - public async find(email: string): Promise { - const conn = await this.db.getConnection() - const result = await conn - .table(this.TABLE) - .where({ email }) - .first() - - return this.transform(result) - } - private transform(row: any): User { return { id: row.id, diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts index 13f06fc..5cf01d9 100644 --- a/src/server/middlewares/error-handler.ts +++ b/src/server/middlewares/error-handler.ts @@ -1,12 +1,24 @@ import { Context } from 'koa' +import { AppError } from '../../errors' + +const httpCodes = { + 10000: 500, + 20000: 404, + 30000: 400, + 30001: 401, + 30002: 403 +} export async function errorHandler(ctx: Context, next: () => Promise) { try { await next() - } catch (e) { - // TODO: sanitize error - console.log(e) - - throw e + } catch (err) { + if (err instanceof AppError) { + ctx.body = err + ctx.status = httpCodes[err.code] ? httpCodes[err.code] : 500 + } else { + ctx.body = new AppError(9, 'Internal Error Server') + ctx.status = 500 + } } } diff --git a/src/server/tasks/controller.ts b/src/server/tasks/controller.ts new file mode 100644 index 0000000..697de3d --- /dev/null +++ b/src/server/tasks/controller.ts @@ -0,0 +1,65 @@ +import { Context } from 'koa' +import { Task } from '../../entities' +import { AuthUser } from '../../lib/authentication' +import { TaskManager } from '../../managers' +import { CreateTask, TaskModel } from './model' + +export class TaskController { + private manager: TaskManager + + constructor(manager: TaskManager) { + this.manager = manager + } + + public async get(ctx: Context) { + const task = await this.manager.find(ctx.params.id) + + ctx.body = new TaskModel(task) + ctx.status = 200 + } + + public async getAll(ctx: Context) { + const authUser: AuthUser = ctx.state.user + const limit = isNaN(ctx.query.limit) ? 10 : parseInt(ctx.query.limit, 10) + const offset = isNaN(ctx.query.offset) ? 10 : parseInt(ctx.query.offset, 10) + const tasks = await this.manager.findUserTasks( + authUser.email, + limit, + offset + ) + + ctx.body = tasks.map((t: Task) => new TaskModel(t)) + ctx.status = 200 + } + + public async create(ctx: Context) { + const taskDto: CreateTask = ctx.request.body + const newTask = await this.manager.create(taskDto as Task) + + ctx.body = new TaskModel(newTask) + ctx.status = 201 + } + + public async update(ctx: Context) { + const taskDto = ctx.request.body + const task = await this.manager.find(ctx.params.id) + + task.name = taskDto.name + task.description = taskDto.description + task.done = taskDto.done + + const updatedTask = await this.manager.update(task) + + ctx.body = new TaskModel(updatedTask) + ctx.status = 200 + } + + public async delete(ctx: Context) { + const authUser: AuthUser = ctx.state.user + const id: number = ctx.params.id + + await this.manager.delete(authUser.email, id) + + ctx.status = 204 + } +} diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts index e69de29..17861ee 100644 --- a/src/server/tasks/index.ts +++ b/src/server/tasks/index.ts @@ -0,0 +1,64 @@ +import * as Joi from 'joi' +import * as Koa from 'koa' +import * as bodyParser from 'koa-bodyparser' +import * as Router from 'koa-router' +import { ServiceContainer } from '../../container' +import { Role } from '../../lib/authentication' +import { UserManager } from '../../managers' +import * as middleware from '../middlewares' +import { TaskController } from './controller' +import * as validators from './validators' + +export function init(server: Koa, container: ServiceContainer) { + const router = new Router({ prefix: '/api/v1/tasks' }) + + router.use(middleware.logRequest(container.logger)) + router.use(middleware.errorHandler) + + const controller = new TaskController(container.managers.task) + + router.get( + '/:id', + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), + controller.get.bind(controller) + ) + + router.get( + '/', + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), + controller.getAll.bind(controller) + ) + + router.post( + '/', + bodyParser(), + middleware.validate({ request: { body: validators.task } }), + controller.create.bind(controller) + ) + + router.put( + '/:id', + bodyParser(), + middleware.validate({ + params: { id: Joi.number().required() }, + request: { + body: validators.task + } + }), + controller.update.bind(controller) + ) + + router.delete( + '/id', + middleware.validate({ params: { id: Joi.number().required() } }), + controller.delete.bind(controller) + ) + + server.use(router.routes()) +} diff --git a/src/server/tasks/model.ts b/src/server/tasks/model.ts new file mode 100644 index 0000000..32320bd --- /dev/null +++ b/src/server/tasks/model.ts @@ -0,0 +1,24 @@ +import { Task } from '../../entities' + +export interface CreateTask { + name: string + description: string +} + +export class TaskModel { + public id?: number + public name: string + public description: string + public done: boolean + public created: Date + public updated: Date + + constructor(task: Task) { + this.id = task.id + this.name = task.name + this.description = task.description + this.done = task.done + this.created = task.created + this.updated = task.updated + } +} diff --git a/src/server/tasks/validators.ts b/src/server/tasks/validators.ts new file mode 100644 index 0000000..b8d9ffd --- /dev/null +++ b/src/server/tasks/validators.ts @@ -0,0 +1,7 @@ +import * as Joi from 'joi' + +export const task: Joi.SchemaMap = { + name: Joi.string().required(), + description: Joi.string().required(), + done: Joi.boolean().required() +} diff --git a/src/server/users/index.ts b/src/server/users/index.ts index 3eb0eae..ae9a164 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -17,6 +17,15 @@ export function init(server: Koa, container: ServiceContainer) { const controller = new UserController(container.managers.user) + router.get( + '/me', + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), + controller.get.bind(controller) + ) + router.post( '/', bodyParser(), @@ -53,14 +62,5 @@ export function init(server: Koa, container: ServiceContainer) { controller.changePassword.bind(controller) ) - router.get( - '/me', - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), - controller.get.bind(controller) - ) - server.use(router.routes()) } From c0987803acda1d4192cb7e3b1c315934342b04c2 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 7 Apr 2018 00:54:01 +0100 Subject: [PATCH 12/35] fix endpoints --- src/errors.ts | 40 ++++++++++++++--- src/lib/authentication/index.ts | 4 +- .../20180330164632_create_schema.ts | 2 +- src/managers/task-manager.ts | 12 ++--- src/managers/user-manager.ts | 7 +-- src/repositories/task-repository.ts | 44 ++++++++++++------- src/repositories/user-repository.ts | 39 +++++++++++----- src/server/index.ts | 6 +++ src/server/middlewares/authentication.ts | 11 +++-- src/server/middlewares/error-handler.ts | 26 ++++++----- src/server/middlewares/index.ts | 1 + src/server/middlewares/log-request.ts | 2 +- src/server/middlewares/response-time.ts | 9 ++++ src/server/middlewares/validator.ts | 11 ++++- src/server/tasks/controller.ts | 26 ++++++----- src/server/tasks/index.ts | 18 +++++--- src/server/tasks/model.ts | 5 --- src/server/users/controller.ts | 2 +- src/server/users/index.ts | 14 +++--- 19 files changed, 192 insertions(+), 87 deletions(-) create mode 100644 src/server/middlewares/response-time.ts diff --git a/src/errors.ts b/src/errors.ts index b6ec5cb..3eb29c9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -8,6 +8,13 @@ export class AppError { this.message = message this.error = error } + + public toModel() { + return { + code: this.code, + message: this.message + } + } } export class NotFoundError extends AppError { @@ -17,19 +24,42 @@ export class NotFoundError extends AppError { } export class ValidationError extends AppError { - constructor(code: number, message: string, error: Error) { + constructor(message: string, error?: Error) { super(30000, message, error) } } +export class FieldValidationError extends AppError { + public fields: FieldError[] + + constructor(message: string, fields: FieldError[], error?: Error) { + super(30001, message, error) + this.fields = fields + } + + public toModel() { + return { + code: this.code, + message: this.message, + fields: this.fields + } + } +} + export class UnauthorizedError extends AppError { - constructor(code: number, message: string) { - super(30001, message) + constructor(error?: Error) { + super(30002, 'Unauthorized user') } } export class PermissionError extends AppError { - constructor(code: number, message: string) { - super(30002, message) + constructor(error?: Error) { + super(30003, 'Permission denied', error) } } + +export interface FieldError { + message: string + type: string + path: string[] +} diff --git a/src/lib/authentication/index.ts b/src/lib/authentication/index.ts index 8398006..1c4ae02 100644 --- a/src/lib/authentication/index.ts +++ b/src/lib/authentication/index.ts @@ -1,5 +1,6 @@ import * as jwt from 'jsonwebtoken' import { User } from '../../entities' +import { UnauthorizedError } from '../../errors' import { UserRepository } from '../../repositories' export interface AuthUser { @@ -38,8 +39,7 @@ export class JWTAuthenticator implements Authenticator { role: user.role as Role } } catch (err) { - // Throw correct error - throw err + throw new UnauthorizedError(err) } } diff --git a/src/lib/database/migrations/20180330164632_create_schema.ts b/src/lib/database/migrations/20180330164632_create_schema.ts index 4875fc3..b38b142 100644 --- a/src/lib/database/migrations/20180330164632_create_schema.ts +++ b/src/lib/database/migrations/20180330164632_create_schema.ts @@ -6,7 +6,6 @@ export function up(db: knex) { table.increments('id').primary() table.string('email', 64).unique() table.string('password', 256).notNullable() - table.string('salt', 256).notNullable() table.enum('role', ['user', 'admin']).notNullable() table.string('first_name', 64).notNullable() table.string('last_name', 64).notNullable() @@ -23,6 +22,7 @@ export function up(db: knex) { table.dateTime('updated').notNullable() table .integer('user_id') + .notNullable() .unsigned() .references('id') .inTable('user') diff --git a/src/managers/task-manager.ts b/src/managers/task-manager.ts index 7050d2f..952647d 100644 --- a/src/managers/task-manager.ts +++ b/src/managers/task-manager.ts @@ -8,16 +8,16 @@ export class TaskManager { this.repo = repo } - public find(id: number): Promise { - return this.repo.find(id) + public find(userId: number, id: number): Promise { + return this.repo.find(userId, id) } public async findUserTasks( - email: string, + userId: number, limit: number, offset: number ): Promise { - return this.repo.findByUser(email, limit, offset) + return this.repo.findByUser(userId, limit, offset) } public create(task: Task): Promise { @@ -28,7 +28,7 @@ export class TaskManager { return this.repo.update(task) } - public delete(email: string, taskId: number): Promise { - return this.repo.delete(email, taskId) + public delete(userId: number, taskId: number): Promise { + return this.repo.delete(userId, taskId) } } diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index fc582f2..797c41c 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -1,4 +1,5 @@ import { User } from '../entities' +import { ValidationError } from '../errors' import { Authenticator } from '../lib/authentication' import { Hasher } from '../lib/hasher' import { UserRepository } from '../repositories' @@ -29,11 +30,11 @@ export class UserManager { public async login(email: string, password: string): Promise { const user = await this.repo.find(email) - if (this.hasher.verifyPassword(password, user.password)) { + if (await this.hasher.verifyPassword(password, user.password)) { return this.auth.authenticate(user) } - throw new Error('Wrong credentials') + throw new ValidationError('Wrong credentials') } public update(user: User): Promise { @@ -46,6 +47,6 @@ export class UserManager { ): Promise { const hashPassword = await this.hasher.hashPassword(newPassword) - return this.repo.changePassword(email, newPassword) + return this.repo.changePassword(email, hashPassword) } } diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index e7fa97c..c73a112 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -1,4 +1,5 @@ import { Task } from '../entities' +import { NotFoundError } from '../errors' import { MySql } from '../lib/database' export class TaskRepository { @@ -9,19 +10,23 @@ export class TaskRepository { this.db = db } - public async find(id: number): Promise { + public async find(userId: number, id: number): Promise { const conn = await this.db.getConnection() const row = await conn .select() .from(this.TABLE) - .where({ id }) + .where({ id, user_id: userId }) .first() + if (!row) { + throw new NotFoundError('Task does not exist') + } + return this.transform(row) } public async findByUser( - email: string, + userId: number, limit: number, offset: number ): Promise { @@ -29,7 +34,7 @@ export class TaskRepository { const results = await conn .select() .from(this.TABLE) - .where({ email }) + .where({ user_id: userId }) .orderBy('updated', 'DESC') .offset(offset) .limit(limit) @@ -42,9 +47,16 @@ export class TaskRepository { task.updated = new Date() const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).insert(task) + const result = await conn.table(this.TABLE).insert({ + name: task.name, + description: task.name, + done: task.done, + created: task.created, + updated: task.updated, + user_id: task.userId + }) - task.id = result[0].insertId + task.id = result[0] return task } @@ -53,23 +65,25 @@ export class TaskRepository { task.updated = new Date() const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).update({ - name: task.name, - description: task.description, - done: task.done - }) + const result = await conn + .table(this.TABLE) + .update({ + name: task.name, + description: task.description, + done: task.done + }) + .where({ user_id: task.userId, id: task.id }) return task } - public async delete(email: string, taskId: number): Promise { + public async delete(userId: number, taskId: number): Promise { const conn = await this.db.getConnection() await conn .from(this.TABLE) .delete() - .where({ id: taskId }) - .andWhere({ email }) + .where({ id: taskId, user_id: userId }) } private transform(row: any): Task { @@ -78,7 +92,7 @@ export class TaskRepository { name: row.name, description: row.description, userId: row.user_id, - done: row.done, + done: row.done === 1, created: row.created, updated: row.updated } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 86f17f8..a91a139 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,4 +1,5 @@ import { User } from '../entities' +import { NotFoundError, ValidationError } from '../errors' import { MySql } from '../lib/database' export class UserRepository { @@ -16,6 +17,10 @@ export class UserRepository { .where({ email }) .first() + if (!row) { + throw new NotFoundError('User does not exist') + } + return this.transform(row) } @@ -24,19 +29,28 @@ export class UserRepository { user.updated = new Date() const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).insert({ - email: user.email, - password: user.password, - role: user.role, - first_name: user.firstName, - last_name: user.lastName, - created: user.created, - updated: user.updated - }) - user.id = result[0].insertId + try { + const result = await conn.table(this.TABLE).insert({ + email: user.email, + password: user.password, + role: user.role, + first_name: user.firstName, + last_name: user.lastName, + created: user.created, + updated: user.updated + }) - return user + user.id = result[0] + + return user + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + throw new ValidationError(`Email ${user.email} already exists`, err) + } + + throw err + } } public async update(user: User): Promise { @@ -57,7 +71,8 @@ export class UserRepository { newPassword: string ): Promise { const conn = await this.db.getConnection() - const result = await conn + + await conn .table(this.TABLE) .update({ password: newPassword, diff --git a/src/server/index.ts b/src/server/index.ts index 6c23a44..24335e1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,6 +4,8 @@ import * as Koa from 'koa' import * as helmet from 'koa-helmet' import { ServiceContainer } from '../container' import * as health from './health' +import * as middlewares from './middlewares' +import * as task from './tasks' import * as user from './users' export function createServer(container: ServiceContainer): Koa { @@ -11,10 +13,14 @@ export function createServer(container: ServiceContainer): Koa { // Register Middlewares app.use(helmet()) + app.use(middlewares.responseTime) + app.use(middlewares.logRequest(container.logger)) + app.use(middlewares.errorHandler(container.logger)) // Register routes health.init(app) user.init(app, container) + task.init(app, container) return app } diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts index 6297af9..9b2f6e2 100644 --- a/src/server/middlewares/authentication.ts +++ b/src/server/middlewares/authentication.ts @@ -1,6 +1,7 @@ import * as jwt from 'jsonwebtoken' import { Context } from 'koa' import { IMiddleware } from 'koa-router' +import { PermissionError } from '../../errors' import { Authenticator, Role } from '../../lib/authentication' export function authentication( @@ -8,14 +9,18 @@ export function authentication( roles: Role[] ): IMiddleware { return async (ctx: Context, next: () => Promise) => { - const token = ctx.headers.access_token + const token = ctx.headers.authorization try { - ctx.state.user = await authenticator.validate(token) + const user = await authenticator.validate(token) + if (roles.indexOf(user.role) < 0) { + throw new PermissionError() + } + + ctx.state.user = user await next() } catch (err) { - // TODO: Throw error throw err } } diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts index 5cf01d9..bb74bba 100644 --- a/src/server/middlewares/error-handler.ts +++ b/src/server/middlewares/error-handler.ts @@ -1,4 +1,6 @@ import { Context } from 'koa' +import { IMiddleware } from 'koa-router' +import { Logger } from 'pino' import { AppError } from '../../errors' const httpCodes = { @@ -9,16 +11,20 @@ const httpCodes = { 30002: 403 } -export async function errorHandler(ctx: Context, next: () => Promise) { - try { - await next() - } catch (err) { - if (err instanceof AppError) { - ctx.body = err - ctx.status = httpCodes[err.code] ? httpCodes[err.code] : 500 - } else { - ctx.body = new AppError(9, 'Internal Error Server') - ctx.status = 500 +export function errorHandler(logger: Logger): IMiddleware { + return async (ctx: Context, next: () => Promise) => { + try { + await next() + } catch (err) { + logger.error('Error Handler:', err) + + if (err instanceof AppError) { + ctx.body = err.toModel() + ctx.status = httpCodes[err.code] ? httpCodes[err.code] : 500 + } else { + ctx.body = new AppError(10000, 'Internal Error Server') + ctx.status = 500 + } } } } diff --git a/src/server/middlewares/index.ts b/src/server/middlewares/index.ts index 92edc83..ef3ee94 100644 --- a/src/server/middlewares/index.ts +++ b/src/server/middlewares/index.ts @@ -2,3 +2,4 @@ export { authentication } from './authentication' export { errorHandler } from './error-handler' export { logRequest } from './log-request' export { validate } from './validator' +export { responseTime } from './response-time' diff --git a/src/server/middlewares/log-request.ts b/src/server/middlewares/log-request.ts index 3955ba8..3a597ec 100644 --- a/src/server/middlewares/log-request.ts +++ b/src/server/middlewares/log-request.ts @@ -17,7 +17,7 @@ export function logRequest(logger: Logger): IMiddleware { } if (ctx.status >= 400) { - logger.error(message, logData) + logger.error(message, logData, ctx.body) } else { logger.info(message, logData) } diff --git a/src/server/middlewares/response-time.ts b/src/server/middlewares/response-time.ts new file mode 100644 index 0000000..0843961 --- /dev/null +++ b/src/server/middlewares/response-time.ts @@ -0,0 +1,9 @@ +import { Context } from 'koa' + +export async function responseTime(ctx: Context, next: () => Promise) { + const start = Date.now() + + await next() + + ctx.response.headers['X-Response-Time'] = Date.now() - start +} diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index 8d086b2..97a3c3b 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -2,6 +2,7 @@ import * as Joi from 'joi' import { Context } from 'koa' import * as bodyParser from 'koa-bodyparser' import { IMiddleware } from 'koa-router' +import { FieldValidationError } from '../../errors' export interface SchemaMap { params?: { [key: string]: Joi.SchemaLike } @@ -25,7 +26,15 @@ export function validate(schema: SchemaMap): IMiddleware { }) if (valResult.error) { - throw valResult.error + throw new FieldValidationError( + valResult.error.message, + valResult.error.details.map(f => ({ + message: f.message, + path: f.path, + type: f.type + })), + valResult.error + ) } await next() diff --git a/src/server/tasks/controller.ts b/src/server/tasks/controller.ts index 697de3d..a65e97e 100644 --- a/src/server/tasks/controller.ts +++ b/src/server/tasks/controller.ts @@ -2,7 +2,7 @@ import { Context } from 'koa' import { Task } from '../../entities' import { AuthUser } from '../../lib/authentication' import { TaskManager } from '../../managers' -import { CreateTask, TaskModel } from './model' +import { TaskModel } from './model' export class TaskController { private manager: TaskManager @@ -12,7 +12,8 @@ export class TaskController { } public async get(ctx: Context) { - const task = await this.manager.find(ctx.params.id) + const authUser: AuthUser = ctx.state.user + const task = await this.manager.find(authUser.id, ctx.params.id) ctx.body = new TaskModel(task) ctx.status = 200 @@ -21,20 +22,20 @@ export class TaskController { public async getAll(ctx: Context) { const authUser: AuthUser = ctx.state.user const limit = isNaN(ctx.query.limit) ? 10 : parseInt(ctx.query.limit, 10) - const offset = isNaN(ctx.query.offset) ? 10 : parseInt(ctx.query.offset, 10) - const tasks = await this.manager.findUserTasks( - authUser.email, - limit, - offset - ) + const offset = isNaN(ctx.query.offset) ? 0 : parseInt(ctx.query.offset, 10) + const tasks = await this.manager.findUserTasks(authUser.id, limit, offset) ctx.body = tasks.map((t: Task) => new TaskModel(t)) ctx.status = 200 } public async create(ctx: Context) { - const taskDto: CreateTask = ctx.request.body - const newTask = await this.manager.create(taskDto as Task) + const authUser: AuthUser = ctx.state.user + const task: Task = ctx.request.body + + task.userId = authUser.id + + const newTask = await this.manager.create(task) ctx.body = new TaskModel(newTask) ctx.status = 201 @@ -42,7 +43,8 @@ export class TaskController { public async update(ctx: Context) { const taskDto = ctx.request.body - const task = await this.manager.find(ctx.params.id) + const authUser: AuthUser = ctx.state.user + const task = await this.manager.find(authUser.id, ctx.params.id) task.name = taskDto.name task.description = taskDto.description @@ -58,7 +60,7 @@ export class TaskController { const authUser: AuthUser = ctx.state.user const id: number = ctx.params.id - await this.manager.delete(authUser.email, id) + await this.manager.delete(authUser.id, id) ctx.status = 204 } diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts index 17861ee..727cd13 100644 --- a/src/server/tasks/index.ts +++ b/src/server/tasks/index.ts @@ -11,10 +11,6 @@ import * as validators from './validators' export function init(server: Koa, container: ServiceContainer) { const router = new Router({ prefix: '/api/v1/tasks' }) - - router.use(middleware.logRequest(container.logger)) - router.use(middleware.errorHandler) - const controller = new TaskController(container.managers.task) router.get( @@ -38,6 +34,10 @@ export function init(server: Koa, container: ServiceContainer) { router.post( '/', bodyParser(), + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), middleware.validate({ request: { body: validators.task } }), controller.create.bind(controller) ) @@ -45,6 +45,10 @@ export function init(server: Koa, container: ServiceContainer) { router.put( '/:id', bodyParser(), + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), middleware.validate({ params: { id: Joi.number().required() }, request: { @@ -55,7 +59,11 @@ export function init(server: Koa, container: ServiceContainer) { ) router.delete( - '/id', + '/:id', + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), middleware.validate({ params: { id: Joi.number().required() } }), controller.delete.bind(controller) ) diff --git a/src/server/tasks/model.ts b/src/server/tasks/model.ts index 32320bd..d5ab891 100644 --- a/src/server/tasks/model.ts +++ b/src/server/tasks/model.ts @@ -1,10 +1,5 @@ import { Task } from '../../entities' -export interface CreateTask { - name: string - description: string -} - export class TaskModel { public id?: number public name: string diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index 39494e7..50b4c47 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -21,7 +21,7 @@ export class UserController { public async login(ctx: Context) { ctx.body = { - token: await this.manager.login( + accessToken: await this.manager.login( ctx.request.body.email, ctx.request.body.password ) diff --git a/src/server/users/index.ts b/src/server/users/index.ts index ae9a164..c0f74ef 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -11,10 +11,6 @@ import * as validators from './validators' export function init(server: Koa, container: ServiceContainer) { const router = new Router({ prefix: '/api/v1/users' }) - - router.use(middleware.logRequest(container.logger)) - router.use(middleware.errorHandler) - const controller = new UserController(container.managers.user) router.get( @@ -43,13 +39,21 @@ export function init(server: Koa, container: ServiceContainer) { router.put( '/', bodyParser(), - middleware.validate({ request: { body: validators.login } }), + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), + middleware.validate({ request: { body: validators.updateUser } }), controller.update.bind(controller) ) router.put( '/password', bodyParser(), + middleware.authentication(container.lib.authenticator, [ + Role.user, + Role.admin + ]), middleware.validate({ request: { body: { From 2e85eed143dcb0a493dcde8e724c4521b05084d5 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 7 Apr 2018 10:44:41 +0100 Subject: [PATCH 13/35] write readme --- README.md | 55 +++++++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 1af04d4..d4bb1c4 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,37 @@ # typescript-node [![Build Status](https://travis-ci.org/Talento90/typescript-node.svg?branch=master)](https://travis-ci.org/Talento90/typescript-node) -Boilerplate template for node and typescript services. +TypeBaked is a boilerplate template for building nodejs and typescript services. It supports the following features: -## Deprecated -Please for an updated version look at this repository: https://github.com/dwyl/hapi-typescript-example +***Features*** +* Language - [TypeScript](https://www.typescriptlang.org/) +* REST API - [koa2](http://koajs.com/) +* Graceful Shutdown - [Pattern](https://nemethgergely.com/nodejs-healthcheck-graceful-shutdown/) +* HealthCheck - [Patern /health](http://microservices.io/patterns/observability/health-check-api.html) +* SQL Database & Migrations - [knex](http://knexjs.org/) +* Authentication and Authorization - [JWT Tokens](https://github.com/auth0/node-jsonwebtoken) +* Validation - [Joi](https://github.com/hapijs/joi) +* Testing - [Mocha](https://mochajs.org/) [Chai](http://www.chaijs.com/) + [Sinon](http://sinonjs.org/) [Coverage](https://istanbul.js.org/) +* Code Style - [Prettier](https://prettier.io/) +* Git Hooks - [Husky](https://github.com/typicode/husky) -**Installation** +## Installation & Run -* *npm run setup* (install nuget packages & typings) +* *npm install* - Install dependencies +* *npm run start* - Start application (It needs a mysql database) -**Run** +### Running with Docker -* *gulp build* (build ts files) -* *gulp test* (run mocha tests) -* *gulp tslint* (run tslint) -* *gulp watch* (watch ts files) -* *npm run start* (start the application) -* *npm run watch* (restart the application when files change) -* *npm start* (run the application on http://localhost:5000/api/docs) +* *docker-compose up* (compose and run, it also creates the mysql database) +* *docker-compose down* (Destroy application and mysql containers) -**Docker** -* *docker-compose build* (compose images) -* *docker-compose up* (running containers) -* *browser: http://localhost:8080/docs* (have fun :) +## Useful npm commands -**Features** - -* *Project Structure - Feature oriented* -* *Hapijs - REST Api* -* *Swagger - documentation* -* *Jwt - authentication* -* *Mongoose - MongoDb* -* *nconf - configurations* -* *Unit Tests - chai + sinon + mocha* - -Have fun :) +* *npm run build* - Transpile TypeScript code +* *npm run clean* - Remove dist, node_modules, coverage folders +* *npm run coverage* - Run NYC coverage +* *npm run lint* - Lint your TypeScript code +* *npm run start:dev* - Run application in dev mode (debug & watch). Debug mode is running on port 5858 (open `chrome://inspect/#devices`). +* *npm run test* - Run unit tests +* *npm run test:integration* - Run integration tests +* *npm run test:all* - Run Unit and Integration tests From 778d3a9cf09026bc7674102fcf0b3f822a8452f4 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 8 Apr 2018 22:29:53 +0100 Subject: [PATCH 14/35] working on tests --- .travis.yml | 12 +++- src/lib/database/index.ts | 2 - test/integration/bootstrap.ts | 63 +++---------------- .../server/tasks/create-task.test.ts | 41 ++++++++++++ .../server/users/change-password.test.ts | 32 ++++++++++ .../server/users/create-user.test.ts | 34 +++++++++- test/integration/server/users/login.test.ts | 23 +++++++ .../server/users/update-user.test.ts | 30 +++++++++ test/integration/server/users/user-me.test.ts | 20 ++++++ 9 files changed, 196 insertions(+), 61 deletions(-) create mode 100644 test/integration/server/tasks/create-task.test.ts create mode 100644 test/integration/server/users/change-password.test.ts create mode 100644 test/integration/server/users/login.test.ts create mode 100644 test/integration/server/users/update-user.test.ts create mode 100644 test/integration/server/users/user-me.test.ts diff --git a/.travis.yml b/.travis.yml index 82a60b1..1f1f87f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,15 @@ language: node_js node_js: - "8" +env: + - PORT=8080 + - DB_HOST=127.0.0.1 + - DB_PORT=3306 + - DB_USER=root services: - - mongodb \ No newline at end of file + - mysql +before_install: + - mysql -e 'CREATE DATABASE IF NOT EXISTS task-manager-test;' +before_script: + - npm install +script: npm run test:all diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index ca095d9..e54ddc4 100644 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -93,8 +93,6 @@ export class MySql { resolve(db) } - // After the connection succeeds or fails, we clear the promise so that we can - // attempt to retry to establish the db connection at a later time if required. this.retryDbConnectionPromise = undefined } ) diff --git a/test/integration/bootstrap.ts b/test/integration/bootstrap.ts index 198efd3..ceee49c 100644 --- a/test/integration/bootstrap.ts +++ b/test/integration/bootstrap.ts @@ -1,59 +1,10 @@ -import * as Koa from 'koa' -import * as pino from 'pino' -import { ServiceContainer } from '../../src/container' -import { Authenticator, JWTAuthenticator } from '../../src/lib/authentication' -import { Configuration, MySql } from '../../src/lib/database' -import { BCryptHasher, Hasher } from '../../src/lib/hasher' -import { TaskManager, UserManager } from '../../src/managers' -import { TaskRepository, UserRepository } from '../../src/repositories' -import { closeServer, createServer } from '../../src/server' +import { init } from '../../src' -const mysqlConfig: Configuration = { - database: 'task-manager', - host: 'mysql', - port: 3306, - user: 'root', - password: 'secret', - debug: true -} +// tslint:disable-next-line:prettier +(async () => { + await init() +})() -function createTestServer(logger: pino.Logger, db: MySql): Koa { - const taskRepo = new TaskRepository(db) - const userRepo = new UserRepository(db) - const hasher = new BCryptHasher() - const authenticator = new JWTAuthenticator(userRepo) +// process.on('exit', async () => { - const container: ServiceContainer = { - logger, - lib: { - hasher, - authenticator - }, - repositories: { - task: taskRepo, - user: userRepo - }, - managers: { - task: new TaskManager(taskRepo), - user: new UserManager(userRepo, hasher, authenticator) - } - } - - return createServer(container) -} - -const log = pino({ name: 'test' }) -const database: MySql = new MySql(mysqlConfig) -const testServer = createTestServer(log, database).listen(1999) - -process.on('exit', async () => { - const shutdown = [closeServer(testServer), database.closeDatabase()] - - for (const s of shutdown) { - try { - await s - } catch (e) { - log.error('Error in graceful shutdown ', e) - } - } -}) +// }) diff --git a/test/integration/server/tasks/create-task.test.ts b/test/integration/server/tasks/create-task.test.ts new file mode 100644 index 0000000..35dfc9a --- /dev/null +++ b/test/integration/server/tasks/create-task.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { TaskModel } from '../../../../src/server/tasks/model' + +describe('Create user', () => { + it('Should create a task and return 201', async () => { + const task = { + name: 'dummy@gmail.com', + description: 'super', + done: false + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(201) + + expect(res.body).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const task = { + name: 'dummy@gmail.com' + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .post('/api/v1/tasks') + .expect(401) + }) +}) diff --git a/test/integration/server/users/change-password.test.ts b/test/integration/server/users/change-password.test.ts new file mode 100644 index 0000000..6b8d01b --- /dev/null +++ b/test/integration/server/users/change-password.test.ts @@ -0,0 +1,32 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { createServer } from '../../../../src/server' + +describe('Change user password', () => { + it('Should update user password', async () => { + const res = await supertest(100) + .put('/api/v1/users/password') + .set('Authorization', '') + .send({ password: 'newPassord' }) + .expect(200) + + expect(res.body.email).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const res = await supertest(100) + .put('/api/v1/users/password') + .send({}) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .get('/api/v1/users/me') + .expect(401) + }) +}) diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 35ebcb1..808c124 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -1,9 +1,39 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { createServer } from '../../../../src/server' +import { CreateUser } from '../../../../src/server/users/model' describe('Create user', () => { - it('must pass', () => { - expect(true) + it('Should create a valid user and return 201', async () => { + const user: CreateUser = { + email: 'dummy@gmail.com', + firstName: 'super', + lastName: 'test', + password: '123123123' + } + + const res = await supertest(100) + .post('/api/v1/users') + .send(user) + .expect(201) + + expect(res.body.email).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const user = { + email: 'dummy@gmail.com', + firstName: 'super', + lastName: 'test' + } + + const res = await supertest(100) + .post('/api/v1/users') + .send(user) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) }) }) diff --git a/test/integration/server/users/login.test.ts b/test/integration/server/users/login.test.ts new file mode 100644 index 0000000..4a63542 --- /dev/null +++ b/test/integration/server/users/login.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' + +describe('Login user', () => { + it('Should return a valid token', async () => { + const res = await supertest(100) + .post('/api/v1/users/login') + .send({ email: 'dude', password: 'test' }) + .expect(200) + + expect(res.body).keys(['accessToken']) + }) + + it('Should return 400 when missing body data', async () => { + const res = await supertest(100) + .post('/api/v1/users/login') + .send({ email: 'dude@mail.com' }) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) +}) diff --git a/test/integration/server/users/update-user.test.ts b/test/integration/server/users/update-user.test.ts new file mode 100644 index 0000000..fa31d7d --- /dev/null +++ b/test/integration/server/users/update-user.test.ts @@ -0,0 +1,30 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' + +describe('Update user information', () => { + it('Should update first and last name', async () => { + const res = await supertest(100) + .put('/api/v1/users') + .set('Authorization', '') + .send({ firstName: 'dude', lastName: 'test' }) + .expect(200) + + expect(res.body).eql([]) + }) + + it('Should return 400 when missing body data', async () => { + const res = await supertest(100) + .put('/api/v1/users/password') + .send({ firstName: 'dude' }) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .get('/api/v1/users/me') + .expect(401) + }) +}) diff --git a/test/integration/server/users/user-me.test.ts b/test/integration/server/users/user-me.test.ts new file mode 100644 index 0000000..55f4620 --- /dev/null +++ b/test/integration/server/users/user-me.test.ts @@ -0,0 +1,20 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { createServer } from '../../../../src/server' + +describe('Create user', () => { + it('Should return user information', async () => { + const res = await supertest(100) + .get('/api/v1/users/me') + .set('Authorization', '') + .expect(200) + + expect(res.body.email).eql({}) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .get('/api/v1/users/me') + .expect(401) + }) +}) From 5028b0d17d7872c3ac32f0df143e47480be19eb6 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 8 Apr 2018 22:35:28 +0100 Subject: [PATCH 15/35] fix travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1f1f87f..70586ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ env: services: - mysql before_install: - - mysql -e 'CREATE DATABASE IF NOT EXISTS task-manager-test;' + - mysql -e 'CREATE DATABASE IF NOT EXISTS task_manager;' before_script: - npm install script: npm run test:all From 18d9884c9956d5af9453af4bec74cc8141860abd Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 8 Apr 2018 23:52:49 +0100 Subject: [PATCH 16/35] fix travis --- .travis.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 70586ac..8614ad9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,13 @@ language: node_js node_js: - "8" env: - - PORT=8080 - - DB_HOST=127.0.0.1 - - DB_PORT=3306 - - DB_USER=root + global: + - PORT=8080 + - DB_HOST=127.0.0.1 + - DB_PORT=3306 + - DB_USER=root services: - mysql before_install: - mysql -e 'CREATE DATABASE IF NOT EXISTS task_manager;' -before_script: - - npm install script: npm run test:all From fa6f06fb2d409334d3b0315c60f725993c9a6511 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Mon, 9 Apr 2018 11:33:51 +0100 Subject: [PATCH 17/35] working on tests --- .editorconfig | 6 +++++ .travis.yml | 2 ++ README.md | 9 +++++++ package.json | 4 +-- test/integration/bootstrap.ts | 10 -------- .../server/users/create-user.test.ts | 14 ++++++++--- test/integration/test-utils.ts | 25 +++++++++++++++++++ test/unit/lib/authentication.test.ts | 0 test/unit/lib/database.test.ts | 0 test/unit/lib/hasher.test.ts | 0 10 files changed, 55 insertions(+), 15 deletions(-) delete mode 100644 test/integration/bootstrap.ts create mode 100644 test/integration/test-utils.ts create mode 100644 test/unit/lib/authentication.test.ts create mode 100644 test/unit/lib/database.test.ts create mode 100644 test/unit/lib/hasher.test.ts diff --git a/.editorconfig b/.editorconfig index 5760be5..ecc4b36 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,9 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 diff --git a/.travis.yml b/.travis.yml index 8614ad9..653f268 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,6 @@ services: - mysql before_install: - mysql -e 'CREATE DATABASE IF NOT EXISTS task_manager;' +before_script: + - npm run start script: npm run test:all diff --git a/README.md b/README.md index d4bb1c4..956b8ed 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,12 @@ TypeBaked is a boilerplate template for building nodejs and typescript services. * *npm run test* - Run unit tests * *npm run test:integration* - Run integration tests * *npm run test:all* - Run Unit and Integration tests + + +## Todo + +* Unit and Integration Tests +* Authorization & Authentication Middlewares +* Cache Middleware cache('url', data) & invalidateCache('url') + * set etag and last-modified headers +* Add location to create diff --git a/package.json b/package.json index 531dc3d..73f5772 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", "test": "npm run build && mocha --recursive dist/test/unit/**/*.test.js", - "test:integration": "npm run build && mocha --recursive dist/test/integration/**/*.test.js --require dist/test/integration/bootstrap.js", - "test:all": "npm run build && mocha --recursive dist/test/**/*.test.js --require dist/test/integration/bootstrap.js" + "test:integration": "npm run build && mocha --recursive dist/test/integration/**/*.test.js", + "test:all": "npm run build && mocha --recursive dist/test/**/*.test.js" }, "dependencies": { "async": "^2.6.0", diff --git a/test/integration/bootstrap.ts b/test/integration/bootstrap.ts deleted file mode 100644 index ceee49c..0000000 --- a/test/integration/bootstrap.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { init } from '../../src' - -// tslint:disable-next-line:prettier -(async () => { - await init() -})() - -// process.on('exit', async () => { - -// }) diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 808c124..082b787 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -2,9 +2,10 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { createServer } from '../../../../src/server' import { CreateUser } from '../../../../src/server/users/model' +import { SERVER_URL } from '../../test-utils' describe('Create user', () => { - it('Should create a valid user and return 201', async () => { + it.only('Should create a valid user and return 201', async () => { const user: CreateUser = { email: 'dummy@gmail.com', firstName: 'super', @@ -12,13 +13,20 @@ describe('Create user', () => { password: '123123123' } - const res = await supertest(100) + const res = await supertest(SERVER_URL) .post('/api/v1/users') .send(user) .expect(201) expect(res.body.email).equals('dummy@gmail.com') - expect(res.body).keys([]) + expect(res.body).eql({ + id: 1, + email: '', + firstName: '', + lastName: '', + created: '', + updated: '' + }) }) it('Should return 400 when missing body data', async () => { diff --git a/test/integration/test-utils.ts b/test/integration/test-utils.ts new file mode 100644 index 0000000..b04cfa1 --- /dev/null +++ b/test/integration/test-utils.ts @@ -0,0 +1,25 @@ +import * as supertest from 'supertest' +import { CreateUser, UserModel } from '../../src/server/users/model' + +export const SERVER_URL = `localhost:${process.env.PORT || 8080}` + +export async function createUserTest(user: CreateUser): Promise { + const res = await supertest(SERVER_URL) + .post('/api/v1/users') + .send(user) + .expect(201) + + return res.body +} + +export async function getLoginToken( + email: string, + password: string +): Promise { + const res = await supertest(SERVER_URL) + .post('/api/v1/users/login') + .send({ email, password }) + .expect(200) + + return res.body.accessToken +} diff --git a/test/unit/lib/authentication.test.ts b/test/unit/lib/authentication.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/lib/database.test.ts b/test/unit/lib/database.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/lib/hasher.test.ts b/test/unit/lib/hasher.test.ts new file mode 100644 index 0000000..e69de29 From 55d0b9e030df218afc862243c4c4470650c8c0df Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Mon, 9 Apr 2018 23:50:44 +0100 Subject: [PATCH 18/35] working on tests --- .gitignore | 3 -- .travis.yml | 4 +- .vscode/launch.json | 35 ++++++++++++++ src/server/middlewares/error-handler.ts | 5 +- .../server/users/create-user.test.ts | 46 ++++++++++++++----- test/integration/server/users/login.test.ts | 25 +++++++--- test/integration/server/users/user-me.test.ts | 44 +++++++++++++++--- 7 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index f9c370d..d648a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,9 +29,6 @@ build/Release # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules -#VSCode -.vscode - #Ignore dist folder dist diff --git a/.travis.yml b/.travis.yml index 653f268..495f550 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ env: services: - mysql before_install: - - mysql -e 'CREATE DATABASE IF NOT EXISTS task_manager;' + - mysql -u root --password="" < db-scripts/create_database.sql before_script: - - npm run start + - npm run build && npm run start script: npm run test:all diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c9d8efb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha Tests", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "${workspaceRoot}/dist/test/**/*.js" + ], + "internalConsoleOptions": "openOnSessionStart", + "remoteRoot": "/app", + "outFiles": [ + "${workspaceRoot}/dist/**/*.js" + ] + }, + { + "type": "node", + "request": "attach", + "name": "Docker: Attach to Node", + "port": 5858, + "address": "localhost", + "localRoot": "${workspaceFolder}", + "remoteRoot": "/app", + "protocol": "inspector", + "outFiles": [ + "${workspaceRoot}/dist/**/*.js" + ] + } + ] +} diff --git a/src/server/middlewares/error-handler.ts b/src/server/middlewares/error-handler.ts index bb74bba..dca65df 100644 --- a/src/server/middlewares/error-handler.ts +++ b/src/server/middlewares/error-handler.ts @@ -7,8 +7,9 @@ const httpCodes = { 10000: 500, 20000: 404, 30000: 400, - 30001: 401, - 30002: 403 + 30001: 400, + 30002: 401, + 30003: 403 } export function errorHandler(logger: Logger): IMiddleware { diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 082b787..7f93723 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -4,8 +4,8 @@ import { createServer } from '../../../../src/server' import { CreateUser } from '../../../../src/server/users/model' import { SERVER_URL } from '../../test-utils' -describe('Create user', () => { - it.only('Should create a valid user and return 201', async () => { +describe('POST /api/v1/users', () => { + it('Should create a valid user and return 201', async () => { const user: CreateUser = { email: 'dummy@gmail.com', firstName: 'super', @@ -19,29 +19,51 @@ describe('Create user', () => { .expect(201) expect(res.body.email).equals('dummy@gmail.com') + expect(res.body.firstName).equals('super') + expect(res.body.lastName).equals('test') + expect(res.body).keys([ + 'id', + 'email', + 'firstName', + 'lastName', + 'created', + 'updated' + ]) + }) + + it('Should return 400 when duplicated email', async () => { + const user: CreateUser = { + email: 'dummy@gmail.com', + firstName: 'super', + lastName: 'test', + password: '123123123' + } + + const res = await supertest(SERVER_URL) + .post('/api/v1/users') + .send(user) + .expect(400) + expect(res.body).eql({ - id: 1, - email: '', - firstName: '', - lastName: '', - created: '', - updated: '' + code: 30000, + message: 'Email dummy@gmail.com already exists' }) }) - it('Should return 400 when missing body data', async () => { + it('Should return 400 when missing fields', async () => { const user = { - email: 'dummy@gmail.com', + email: 'dummy1@gmail.com', firstName: 'super', lastName: 'test' } - const res = await supertest(100) + const res = await supertest(SERVER_URL) .post('/api/v1/users') .send(user) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"password" is required') }) }) diff --git a/test/integration/server/users/login.test.ts b/test/integration/server/users/login.test.ts index 4a63542..add6a21 100644 --- a/test/integration/server/users/login.test.ts +++ b/test/integration/server/users/login.test.ts @@ -1,23 +1,36 @@ import { expect } from 'chai' import * as supertest from 'supertest' +import { createUserTest, SERVER_URL } from '../../test-utils' + +describe('POST /api/v1/users/login', () => { + before(async () => { + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'test', + password: 'test' + } + + await createUserTest(user) + }) -describe('Login user', () => { it('Should return a valid token', async () => { - const res = await supertest(100) + const res = await supertest(SERVER_URL) .post('/api/v1/users/login') - .send({ email: 'dude', password: 'test' }) + .send({ email: 'dude@gmail.com', password: 'test' }) .expect(200) expect(res.body).keys(['accessToken']) }) - it('Should return 400 when missing body data', async () => { - const res = await supertest(100) + it('Should return 400 when missing password', async () => { + const res = await supertest(SERVER_URL) .post('/api/v1/users/login') .send({ email: 'dude@mail.com' }) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"password" is required') }) }) diff --git a/test/integration/server/users/user-me.test.ts b/test/integration/server/users/user-me.test.ts index 55f4620..9073d93 100644 --- a/test/integration/server/users/user-me.test.ts +++ b/test/integration/server/users/user-me.test.ts @@ -1,20 +1,50 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createServer } from '../../../../src/server' +import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' + +describe('GET /api/v1/users/me', () => { + before(async () => { + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'test', + password: 'test' + } + + await createUserTest(user) + }) -describe('Create user', () => { it('Should return user information', async () => { - const res = await supertest(100) + const token = await getLoginToken('dude@gmail.com', 'test') + const res = await supertest(SERVER_URL) .get('/api/v1/users/me') - .set('Authorization', '') + .set('Authorization', token) .expect(200) - expect(res.body.email).eql({}) + expect(res.body).keys([ + 'id', + 'email', + 'firstName', + 'lastName', + 'created', + 'updated' + ]) }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(SERVER_URL) .get('/api/v1/users/me') + .set('Authorization', 'wrong token') .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(SERVER_URL) + .get('/api/v1/users/me') + .expect(401) + + expect(res.body.code).equals(30002) }) }) From 7b368f9de190f4f5582fa06c93fd6cdfa5960e24 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Tue, 10 Apr 2018 09:47:28 +0100 Subject: [PATCH 19/35] working on tests --- src/managers/user-manager.ts | 13 +++- src/server/tasks/controller.ts | 1 + src/server/tasks/index.ts | 4 +- src/server/tasks/validators.ts | 7 +- src/server/users/controller.ts | 9 ++- src/server/users/index.ts | 6 +- src/server/users/validators.ts | 5 ++ .../server/tasks/delete-task.test.ts | 41 +++++++++++ .../server/tasks/get-all-tasks.test.ts | 41 +++++++++++ .../integration/server/tasks/get-task.test.ts | 41 +++++++++++ .../server/tasks/update-task.test.ts | 41 +++++++++++ .../server/users/change-password.test.ts | 72 +++++++++++++++---- .../server/users/update-user.test.ts | 57 ++++++++++++--- 13 files changed, 303 insertions(+), 35 deletions(-) create mode 100644 test/integration/server/tasks/delete-task.test.ts create mode 100644 test/integration/server/tasks/get-all-tasks.test.ts create mode 100644 test/integration/server/tasks/get-task.test.ts create mode 100644 test/integration/server/tasks/update-task.test.ts diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index 797c41c..c7f0a08 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -43,8 +43,19 @@ export class UserManager { public async changePassword( email: string, - newPassword: string + newPassword: string, + oldPassword: string ): Promise { + const user = await this.repo.find(email) + const validPassword = await this.hasher.verifyPassword( + oldPassword, + user.password + ) + + if (!validPassword) { + throw new ValidationError('Old password is not correct') + } + const hashPassword = await this.hasher.hashPassword(newPassword) return this.repo.changePassword(email, hashPassword) diff --git a/src/server/tasks/controller.ts b/src/server/tasks/controller.ts index a65e97e..dffc9a8 100644 --- a/src/server/tasks/controller.ts +++ b/src/server/tasks/controller.ts @@ -34,6 +34,7 @@ export class TaskController { const task: Task = ctx.request.body task.userId = authUser.id + task.done = false const newTask = await this.manager.create(task) diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts index 727cd13..6be96be 100644 --- a/src/server/tasks/index.ts +++ b/src/server/tasks/index.ts @@ -38,7 +38,7 @@ export function init(server: Koa, container: ServiceContainer) { Role.user, Role.admin ]), - middleware.validate({ request: { body: validators.task } }), + middleware.validate({ request: { body: validators.createTask } }), controller.create.bind(controller) ) @@ -52,7 +52,7 @@ export function init(server: Koa, container: ServiceContainer) { middleware.validate({ params: { id: Joi.number().required() }, request: { - body: validators.task + body: validators.updateTask } }), controller.update.bind(controller) diff --git a/src/server/tasks/validators.ts b/src/server/tasks/validators.ts index b8d9ffd..329cd1b 100644 --- a/src/server/tasks/validators.ts +++ b/src/server/tasks/validators.ts @@ -1,7 +1,12 @@ import * as Joi from 'joi' -export const task: Joi.SchemaMap = { +export const updateTask: Joi.SchemaMap = { name: Joi.string().required(), description: Joi.string().required(), done: Joi.boolean().required() } + +export const createTask: Joi.SchemaMap = { + name: Joi.string().required(), + description: Joi.string().required() +} diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index 50b4c47..ae54da5 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -42,9 +42,14 @@ export class UserController { } public async changePassword(ctx: Context) { - const newPassword = ctx.request.body.password + const newPassword = ctx.request.body.newPassword + const oldPassword = ctx.request.body.oldPassword - await this.manager.changePassword(ctx.state.user.email, newPassword) + await this.manager.changePassword( + ctx.state.user.email, + newPassword, + oldPassword + ) ctx.status = 204 } diff --git a/src/server/users/index.ts b/src/server/users/index.ts index c0f74ef..2b1ba46 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -56,11 +56,7 @@ export function init(server: Koa, container: ServiceContainer) { ]), middleware.validate({ request: { - body: { - password: Joi.string() - .trim() - .required() - } + body: validators.changePassword } }), controller.changePassword.bind(controller) diff --git a/src/server/users/validators.ts b/src/server/users/validators.ts index be1adea..62bf37e 100644 --- a/src/server/users/validators.ts +++ b/src/server/users/validators.ts @@ -17,6 +17,11 @@ export const updateUser: Joi.SchemaMap = { lastName: Joi.string().required() } +export const changePassword: Joi.SchemaMap = { + oldPassword: Joi.string().required(), + newPassword: Joi.string().required() +} + export const login: Joi.SchemaMap = { email: Joi.string() .email() diff --git a/test/integration/server/tasks/delete-task.test.ts b/test/integration/server/tasks/delete-task.test.ts new file mode 100644 index 0000000..35dfc9a --- /dev/null +++ b/test/integration/server/tasks/delete-task.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { TaskModel } from '../../../../src/server/tasks/model' + +describe('Create user', () => { + it('Should create a task and return 201', async () => { + const task = { + name: 'dummy@gmail.com', + description: 'super', + done: false + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(201) + + expect(res.body).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const task = { + name: 'dummy@gmail.com' + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .post('/api/v1/tasks') + .expect(401) + }) +}) diff --git a/test/integration/server/tasks/get-all-tasks.test.ts b/test/integration/server/tasks/get-all-tasks.test.ts new file mode 100644 index 0000000..35dfc9a --- /dev/null +++ b/test/integration/server/tasks/get-all-tasks.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { TaskModel } from '../../../../src/server/tasks/model' + +describe('Create user', () => { + it('Should create a task and return 201', async () => { + const task = { + name: 'dummy@gmail.com', + description: 'super', + done: false + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(201) + + expect(res.body).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const task = { + name: 'dummy@gmail.com' + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .post('/api/v1/tasks') + .expect(401) + }) +}) diff --git a/test/integration/server/tasks/get-task.test.ts b/test/integration/server/tasks/get-task.test.ts new file mode 100644 index 0000000..35dfc9a --- /dev/null +++ b/test/integration/server/tasks/get-task.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { TaskModel } from '../../../../src/server/tasks/model' + +describe('Create user', () => { + it('Should create a task and return 201', async () => { + const task = { + name: 'dummy@gmail.com', + description: 'super', + done: false + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(201) + + expect(res.body).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const task = { + name: 'dummy@gmail.com' + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .post('/api/v1/tasks') + .expect(401) + }) +}) diff --git a/test/integration/server/tasks/update-task.test.ts b/test/integration/server/tasks/update-task.test.ts new file mode 100644 index 0000000..35dfc9a --- /dev/null +++ b/test/integration/server/tasks/update-task.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { TaskModel } from '../../../../src/server/tasks/model' + +describe('Create user', () => { + it('Should create a task and return 201', async () => { + const task = { + name: 'dummy@gmail.com', + description: 'super', + done: false + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(201) + + expect(res.body).equals('dummy@gmail.com') + expect(res.body).keys([]) + }) + + it('Should return 400 when missing body data', async () => { + const task = { + name: 'dummy@gmail.com' + } + + const res = await supertest(100) + .post('/api/v1/tasks') + .send(task) + .expect(400) + + expect(res.body.code).equals(30001) + expect(res.body.fields).eql([]) + }) + + it('Should return unauthorized when user is not logged in', async () => { + const res = await supertest(100) + .post('/api/v1/tasks') + .expect(401) + }) +}) diff --git a/test/integration/server/users/change-password.test.ts b/test/integration/server/users/change-password.test.ts index 6b8d01b..0f3b0ef 100644 --- a/test/integration/server/users/change-password.test.ts +++ b/test/integration/server/users/change-password.test.ts @@ -1,32 +1,76 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createServer } from '../../../../src/server' +import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' -describe('Change user password', () => { - it('Should update user password', async () => { - const res = await supertest(100) +describe('PUT /api/v1/users/password', () => { + before(async () => { + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' + } + + await createUserTest(user) + }) + + it('Should update user password and login successfully', async () => { + const token = await getLoginToken('dude@gmail.com', 'test') + let res = await supertest(SERVER_URL) .put('/api/v1/users/password') - .set('Authorization', '') - .send({ password: 'newPassord' }) + .set('Authorization', token) + .send({ newPassword: 'newPassord', oldPassword: 'secret' }) + .expect(200) + + res = await supertest(SERVER_URL) + .post('/api/v1/users/login') + .send({ email: 'dude@gmail.com', password: 'newPassord' }) .expect(200) - expect(res.body.email).equals('dummy@gmail.com') - expect(res.body).keys([]) + expect(res.body).keys(['accessToken']) + }) + + it('Should update user password but fail on login', async () => { + const token = await getLoginToken('dude@gmail.com', 'test') + let res = await supertest(SERVER_URL) + .put('/api/v1/users/password') + .set('Authorization', token) + .send({ newPassword: 'newPassord', oldPassword: 'secret' }) + .expect(200) + + res = await supertest(SERVER_URL) + .post('/api/v1/users/login') + .send({ email: 'dude@gmail.com', password: 'secret' }) + .expect(401) + + expect(res.body.code).equals(30002) }) it('Should return 400 when missing body data', async () => { - const res = await supertest(100) + const res = await supertest(SERVER_URL) .put('/api/v1/users/password') - .send({}) + .send({ newPassword: 'newPassord' }) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"oldPassword" is required') }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .get('/api/v1/users/me') + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(SERVER_URL) + .put('/api/v1/users/password') + .set('Authorization', 'wrong token') .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(SERVER_URL) + .put('/api/v1/users/password') + .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/users/update-user.test.ts b/test/integration/server/users/update-user.test.ts index fa31d7d..d5ac577 100644 --- a/test/integration/server/users/update-user.test.ts +++ b/test/integration/server/users/update-user.test.ts @@ -1,30 +1,67 @@ import { expect } from 'chai' import * as supertest from 'supertest' +import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' + +describe('PUT /api/v1/users', () => { + before(async () => { + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'test' + } + + await createUserTest(user) + }) -describe('Update user information', () => { it('Should update first and last name', async () => { - const res = await supertest(100) + const token = await getLoginToken('dude@gmail.com', 'test') + let res = await supertest(SERVER_URL) .put('/api/v1/users') - .set('Authorization', '') + .set('Authorization', token) .send({ firstName: 'dude', lastName: 'test' }) .expect(200) - expect(res.body).eql([]) + res = await supertest(SERVER_URL) + .get('/api/v1/users/me') + .set('Authorization', token) + .expect(200) + + expect(res.body.firstName).equals('dude') + expect(res.body.lastName).equals('test') + + expect(res.body).include({ + firstName: 'dude', + lastName: 'test' + }) }) - it('Should return 400 when missing body data', async () => { - const res = await supertest(100) + it('Should return 400 when missing lastName data', async () => { + const res = await supertest(SERVER_URL) .put('/api/v1/users/password') + .set('Authorization', '') .send({ firstName: 'dude' }) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"lastName" is required') }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .get('/api/v1/users/me') + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(SERVER_URL) + .put('/api/v1/users/password') + .set('Authorization', 'wrong token') + .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(SERVER_URL) + .put('/api/v1/users/password') .expect(401) + + expect(res.body.code).equals(30002) }) }) From df6b00de8afab13405b84d4f1c9cc97931480b32 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Thu, 12 Apr 2018 23:21:38 +0100 Subject: [PATCH 20/35] working on tests --- .gitignore | 1 + docker-compose.yml | 34 +++---- package.json | 8 +- src/repositories/task-repository.ts | 8 +- src/server/tasks/model.ts | 5 ++ test/integration/database-utils.ts | 20 +++++ test/integration/global-hooks.test.ts | 22 +++++ test/integration/server-utils.ts | 47 ++++++++++ .../server/tasks/create-task.test.ts | 58 +++++++++--- .../server/tasks/delete-task.test.ts | 78 +++++++++++----- .../server/tasks/get-all-tasks.test.ts | 78 ++++++++++------ .../integration/server/tasks/get-task.test.ts | 79 ++++++++++++----- .../server/tasks/update-task.test.ts | 88 ++++++++++++++----- .../server/users/change-password.test.ts | 35 ++++---- .../server/users/create-user.test.ts | 32 ++++--- test/integration/server/users/login.test.ts | 11 ++- .../server/users/update-user.test.ts | 36 ++++---- test/integration/server/users/user-me.test.ts | 13 +-- test/integration/test-utils.ts | 25 ------ 19 files changed, 467 insertions(+), 211 deletions(-) create mode 100644 test/integration/database-utils.ts create mode 100644 test/integration/global-hooks.test.ts create mode 100644 test/integration/server-utils.ts delete mode 100644 test/integration/test-utils.ts diff --git a/.gitignore b/.gitignore index d648a2b..0d6eca5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage reports +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) diff --git a/docker-compose.yml b/docker-compose.yml index a0e7c3f..2b96989 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,22 @@ version: '2.1' services: - app: - build: . - command: npm run start:dev - environment: - - PORT=8080 - - DB_HOST=mysql - - DB_PORT=3306 - - DB_USER=root - - DB_PASSWORD=secret - ports: - - "8080:8080" - - "5858:5858" - links: - - mysql - volumes: - - .:/app/ - network_mode: bridge + # app: + # build: . + # command: npm run start:dev + # environment: + # - PORT=8080 + # - DB_HOST=mysql + # - DB_PORT=3306 + # - DB_USER=root + # - DB_PASSWORD=secret + # ports: + # - "8080:8080" + # - "5858:5858" + # links: + # - mysql + # volumes: + # - .:/app/ + # network_mode: bridge mysql: image: mysql:latest environment: diff --git a/package.json b/package.json index 73f5772..0a1290b 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ ], "scripts": { "build": "rm -rf dist && tsc", - "clean": "rm -rf node_modules coverage dist", - "coverage": "nyc --exclude dist/test --reporter=html npm test", + "clean": "rm -rf node_modules coverage dist .nyc_output", + "coverage": "nyc --exclude dist/test --reporter=html npm run test:all", "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", "test": "npm run build && mocha --recursive dist/test/unit/**/*.test.js", - "test:integration": "npm run build && mocha --recursive dist/test/integration/**/*.test.js", - "test:all": "npm run build && mocha --recursive dist/test/**/*.test.js" + "test:integration": "npm run build && mocha --exit --recursive dist/test/integration/**/*.test.js", + "test:all": "npm run build && mocha --exit --recursive dist/test/integration/**/*.test.js" }, "dependencies": { "async": "^2.6.0", diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index c73a112..9f9d1e9 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -49,7 +49,7 @@ export class TaskRepository { const conn = await this.db.getConnection() const result = await conn.table(this.TABLE).insert({ name: task.name, - description: task.name, + description: task.description, done: task.done, created: task.created, updated: task.updated, @@ -80,10 +80,14 @@ export class TaskRepository { public async delete(userId: number, taskId: number): Promise { const conn = await this.db.getConnection() - await conn + const result = await conn .from(this.TABLE) .delete() .where({ id: taskId, user_id: userId }) + + if (result === 0) { + throw new NotFoundError('Task does not exist') + } } private transform(row: any): Task { diff --git a/src/server/tasks/model.ts b/src/server/tasks/model.ts index d5ab891..32320bd 100644 --- a/src/server/tasks/model.ts +++ b/src/server/tasks/model.ts @@ -1,5 +1,10 @@ import { Task } from '../../entities' +export interface CreateTask { + name: string + description: string +} + export class TaskModel { public id?: number public name: string diff --git a/test/integration/database-utils.ts b/test/integration/database-utils.ts new file mode 100644 index 0000000..e13ec62 --- /dev/null +++ b/test/integration/database-utils.ts @@ -0,0 +1,20 @@ +import { Configuration, MySql } from '../../src/lib/database' + +const testMysqlConfig: Configuration = { + database: 'task_manager', + host: process.env.DB_HOST || '127.0.0.1', + port: Number(process.env.DB_PORT) || 3306, + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'secret', + debug: false +} + +export const database: MySql = new MySql(testMysqlConfig) + +export async function truncateTables(tables: string[]) { + const conn = await database.getConnection() + + for (const table of tables) { + await conn.raw(`DELETE FROM ${table}`) + } +} diff --git a/test/integration/global-hooks.test.ts b/test/integration/global-hooks.test.ts new file mode 100644 index 0000000..463451f --- /dev/null +++ b/test/integration/global-hooks.test.ts @@ -0,0 +1,22 @@ +import { closeServer } from '../../src/server' +import { database } from './database-utils' +import { testServer } from './server-utils' + +before(async () => { + console.info('Initializing database migration.') + await database.schemaMigration() +}) + +after(async () => { + const shutdowns = [closeServer(testServer), database.closeDatabase()] + + console.info('Start cleaning test resources.') + + for (const shutdown of shutdowns) { + try { + await shutdown + } catch (e) { + console.error('Error in graceful shutdown ', e) + } + } +}) diff --git a/test/integration/server-utils.ts b/test/integration/server-utils.ts new file mode 100644 index 0000000..f6331e6 --- /dev/null +++ b/test/integration/server-utils.ts @@ -0,0 +1,47 @@ +import * as pino from 'pino' +import * as supertest from 'supertest' +import { createContainer } from '../../src/container' +import { closeServer, createServer } from '../../src/server' +import { CreateTask, TaskModel } from '../../src/server/tasks/model' +import { CreateUser, UserModel } from '../../src/server/users/model' +import { database } from './database-utils' + +const logger = pino({ name: 'test', level: 'silent' }) +const container = createContainer(database, logger) +const port = process.env.PORT || 8080 + +export const testServer = createServer(container).listen(port) + +export async function createUserTest(user: CreateUser): Promise { + const res = await supertest(testServer) + .post('/api/v1/users') + .send(user) + .expect(201) + + return res.body +} + +export async function createTaskTest( + task: CreateTask, + token: string +): Promise { + const res = await supertest(testServer) + .post('/api/v1/tasks') + .set('Authorization', token) + .send(task) + .expect(201) + + return res.body +} + +export async function getLoginToken( + email: string, + password: string +): Promise { + const res = await supertest(testServer) + .post('/api/v1/users/login') + .send({ email, password }) + .expect(200) + + return res.body.accessToken +} diff --git a/test/integration/server/tasks/create-task.test.ts b/test/integration/server/tasks/create-task.test.ts index 35dfc9a..5c9052a 100644 --- a/test/integration/server/tasks/create-task.test.ts +++ b/test/integration/server/tasks/create-task.test.ts @@ -1,41 +1,75 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { TaskModel } from '../../../../src/server/tasks/model' +import { truncateTables } from '../../database-utils' +import { createUserTest, getLoginToken, testServer } from '../../server-utils' + +describe('POST /api/v1/tasks', () => { + let token: string + + before(async () => { + await truncateTables(['task', 'user']) + + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' + } + + await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') + }) -describe('Create user', () => { it('Should create a task and return 201', async () => { const task = { - name: 'dummy@gmail.com', - description: 'super', - done: false + name: 'Do homework', + description: 'Exercise 1 and 2' } - const res = await supertest(100) + const res = await supertest(testServer) .post('/api/v1/tasks') + .set('Authorization', token) .send(task) .expect(201) - expect(res.body).equals('dummy@gmail.com') - expect(res.body).keys([]) + expect(res.body).include({ + name: 'Do homework', + description: 'Exercise 1 and 2', + done: false + }) }) it('Should return 400 when missing body data', async () => { const task = { - name: 'dummy@gmail.com' + name: 'Do something' } - const res = await supertest(100) + const res = await supertest(testServer) .post('/api/v1/tasks') + .set('Authorization', token) .send(task) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"description" is required') }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) .post('/api/v1/tasks') + .set('Authorization', 'wrong token') .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .post('/api/v1/tasks') + .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/tasks/delete-task.test.ts b/test/integration/server/tasks/delete-task.test.ts index 35dfc9a..44954c4 100644 --- a/test/integration/server/tasks/delete-task.test.ts +++ b/test/integration/server/tasks/delete-task.test.ts @@ -1,41 +1,71 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { TaskModel } from '../../../../src/server/tasks/model' +import { truncateTables } from '../../database-utils' +import { + createTaskTest, + createUserTest, + getLoginToken, + testServer +} from '../../server-utils' -describe('Create user', () => { - it('Should create a task and return 201', async () => { - const task = { - name: 'dummy@gmail.com', - description: 'super', - done: false - } +describe('DELETE /api/v1/tasks/:id', () => { + let token: string + + before(async () => { + await truncateTables(['task', 'user']) - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(201) + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' + } - expect(res.body).equals('dummy@gmail.com') - expect(res.body).keys([]) + await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') }) - it('Should return 400 when missing body data', async () => { + it('Should delete a task and return 204', async () => { const task = { - name: 'dummy@gmail.com' + name: 'Do Something', + description: 'Some random description' } - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(400) + const createdTask = await createTaskTest(task, token) + + let res = await supertest(testServer) + .delete(`/api/v1/tasks/${createdTask.id}`) + .set('Authorization', token) + .expect(204) + + res = await supertest(testServer) + .get(`/api/v1/tasks/${createdTask.id}`) + .set('Authorization', token) + .expect(404) + }) + + it('Should return 404 when task does not exist', async () => { + await supertest(testServer) + .delete(`/api/v1/tasks/1000000`) + .set('Authorization', token) + .expect(404) + }) - expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) + .delete(`/api/v1/tasks/1000000`) + .set('Authorization', 'wrong token') + .expect(401) + + expect(res.body.code).equals(30002) }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .post('/api/v1/tasks') + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .delete(`/api/v1/tasks/1000000`) .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/tasks/get-all-tasks.test.ts b/test/integration/server/tasks/get-all-tasks.test.ts index 35dfc9a..7abe745 100644 --- a/test/integration/server/tasks/get-all-tasks.test.ts +++ b/test/integration/server/tasks/get-all-tasks.test.ts @@ -1,41 +1,69 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { TaskModel } from '../../../../src/server/tasks/model' +import { truncateTables } from '../../database-utils' +import { + createTaskTest, + createUserTest, + getLoginToken, + testServer +} from '../../server-utils' -describe('Create user', () => { - it('Should create a task and return 201', async () => { - const task = { - name: 'dummy@gmail.com', - description: 'super', - done: false - } +describe('GET /api/v1/tasks', () => { + let token: string + + before(async () => { + await truncateTables(['task', 'user']) - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(201) + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' + } - expect(res.body).equals('dummy@gmail.com') - expect(res.body).keys([]) + await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') }) - it('Should return 400 when missing body data', async () => { - const task = { - name: 'dummy@gmail.com' + it('Should return a list of tasks', async () => { + const task1 = { + name: 'Clean Room', + description: 'Mom said that I need to clean my room.' } - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(400) + const task2 = { + name: 'Do Homework', + description: 'Math homework.' + } + + await createTaskTest(task1, token) + await createTaskTest(task2, token) + + const res = await supertest(testServer) + .get('/api/v1/tasks') + .set('Authorization', token) + .expect(200) - expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.length).equals(2) + expect(res.body[0].name).equals('Clean Room') + expect(res.body[1].name).equals('Do Homework') }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .post('/api/v1/tasks') + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) + .get(`/api/v1/tasks`) + .set('Authorization', 'wrong token') .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .get(`/api/v1/tasks`) + .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/tasks/get-task.test.ts b/test/integration/server/tasks/get-task.test.ts index 35dfc9a..c6c24ad 100644 --- a/test/integration/server/tasks/get-task.test.ts +++ b/test/integration/server/tasks/get-task.test.ts @@ -1,41 +1,72 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { TaskModel } from '../../../../src/server/tasks/model' +import { truncateTables } from '../../database-utils' +import { + createTaskTest, + createUserTest, + getLoginToken, + testServer +} from '../../server-utils' -describe('Create user', () => { - it('Should create a task and return 201', async () => { - const task = { - name: 'dummy@gmail.com', - description: 'super', - done: false - } +describe('GET /api/v1/tasks/:id', () => { + let token: string - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(201) + before(async () => { + await truncateTables(['task', 'user']) - expect(res.body).equals('dummy@gmail.com') - expect(res.body).keys([]) + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' + } + + await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') }) - it('Should return 400 when missing body data', async () => { + it('Should return a single task', async () => { const task = { - name: 'dummy@gmail.com' + name: 'Clean Room', + description: 'Mom said that I need to clean my room.' } - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(400) + const createdTask = await createTaskTest(task, token) + + const res = await supertest(testServer) + .get(`/api/v1/tasks/${createdTask.id}`) + .set('Authorization', token) + .expect(200) + + expect(res.body).includes({ + name: 'Clean Room', + description: 'Mom said that I need to clean my room.', + done: false + }) + }) - expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + it('Should return 404 when task does not exist', async () => { + const res = await supertest(testServer) + .get(`/api/v1/tasks/111111111`) + .set('Authorization', token) + .expect(404) }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .post('/api/v1/tasks') + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) + .get('/api/v1/tasks/1') + .set('Authorization', 'wrong token') .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .get('/api/v1/tasks/1') + .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/tasks/update-task.test.ts b/test/integration/server/tasks/update-task.test.ts index 35dfc9a..639dc52 100644 --- a/test/integration/server/tasks/update-task.test.ts +++ b/test/integration/server/tasks/update-task.test.ts @@ -1,41 +1,85 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { TaskModel } from '../../../../src/server/tasks/model' +import { truncateTables } from '../../database-utils' +import { + createTaskTest, + createUserTest, + getLoginToken, + testServer +} from '../../server-utils' -describe('Create user', () => { - it('Should create a task and return 201', async () => { - const task = { - name: 'dummy@gmail.com', - description: 'super', - done: false +describe('PUT /api/v1/tasks/:id', () => { + let token: string + + before(async () => { + await truncateTables(['task', 'user']) + + const user = { + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'mocha', + password: 'secret' } - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) - .expect(201) + await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') + }) + + beforeEach(async () => { + await truncateTables(['task']) + }) + + it('Should update a task', async () => { + const task = await createTaskTest( + { name: 'Do homework', description: 'Exercise 1 and 2' }, + token + ) + + const res = await supertest(testServer) + .put(`/api/v1/tasks/${task.id}`) + .set('Authorization', token) + .send({ name: 'Do TPC', description: 'Some job', done: true }) + .expect(200) - expect(res.body).equals('dummy@gmail.com') - expect(res.body).keys([]) + expect(res.body).include({ + name: 'Do TPC', + description: 'Some job', + done: true + }) }) it('Should return 400 when missing body data', async () => { - const task = { - name: 'dummy@gmail.com' - } + const task = await createTaskTest( + { name: 'Do homework', description: 'Exercise 1 and 2' }, + token + ) - const res = await supertest(100) - .post('/api/v1/tasks') - .send(task) + const res = await supertest(testServer) + .put(`/api/v1/tasks/${task.id}`) + .set('Authorization', token) + .send({ name: 'Do TPC', description: 'Some job' }) .expect(400) expect(res.body.code).equals(30001) - expect(res.body.fields).eql([]) + expect(res.body.fields.length).equals(1) + expect(res.body.fields[0].message).eql('"done" is required') + }) + + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) + .put('/api/v1/tasks/1') + .set('Authorization', 'wrong token') + .expect(401) + + expect(res.body.code).equals(30002) }) - it('Should return unauthorized when user is not logged in', async () => { - const res = await supertest(100) - .post('/api/v1/tasks') + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .put('/api/v1/tasks/1') .expect(401) + + expect(res.body.code).equals(30002) }) }) diff --git a/test/integration/server/users/change-password.test.ts b/test/integration/server/users/change-password.test.ts index 0f3b0ef..4edeb1b 100644 --- a/test/integration/server/users/change-password.test.ts +++ b/test/integration/server/users/change-password.test.ts @@ -1,9 +1,14 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' +import { truncateTables } from '../../database-utils' +import { createUserTest, getLoginToken, testServer } from '../../server-utils' describe('PUT /api/v1/users/password', () => { - before(async () => { + let token: string + + beforeEach(async () => { + await truncateTables(['user']) + const user = { email: 'dude@gmail.com', firstName: 'super', @@ -12,17 +17,17 @@ describe('PUT /api/v1/users/password', () => { } await createUserTest(user) + token = await getLoginToken('dude@gmail.com', 'secret') }) it('Should update user password and login successfully', async () => { - const token = await getLoginToken('dude@gmail.com', 'test') - let res = await supertest(SERVER_URL) + let res = await supertest(testServer) .put('/api/v1/users/password') .set('Authorization', token) .send({ newPassword: 'newPassord', oldPassword: 'secret' }) - .expect(200) + .expect(204) - res = await supertest(SERVER_URL) + res = await supertest(testServer) .post('/api/v1/users/login') .send({ email: 'dude@gmail.com', password: 'newPassord' }) .expect(200) @@ -31,24 +36,24 @@ describe('PUT /api/v1/users/password', () => { }) it('Should update user password but fail on login', async () => { - const token = await getLoginToken('dude@gmail.com', 'test') - let res = await supertest(SERVER_URL) + let res = await supertest(testServer) .put('/api/v1/users/password') .set('Authorization', token) .send({ newPassword: 'newPassord', oldPassword: 'secret' }) - .expect(200) + .expect(204) - res = await supertest(SERVER_URL) + res = await supertest(testServer) .post('/api/v1/users/login') .send({ email: 'dude@gmail.com', password: 'secret' }) - .expect(401) + .expect(400) - expect(res.body.code).equals(30002) + expect(res.body.code).equals(30000) }) it('Should return 400 when missing body data', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .put('/api/v1/users/password') + .set('Authorization', token) .send({ newPassword: 'newPassord' }) .expect(400) @@ -58,7 +63,7 @@ describe('PUT /api/v1/users/password', () => { }) it('Should return unauthorized when token is not valid', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .put('/api/v1/users/password') .set('Authorization', 'wrong token') .expect(401) @@ -67,7 +72,7 @@ describe('PUT /api/v1/users/password', () => { }) it('Should return unauthorized when token is missing', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .put('/api/v1/users/password') .expect(401) diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 7f93723..8f41108 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -1,10 +1,14 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createServer } from '../../../../src/server' import { CreateUser } from '../../../../src/server/users/model' -import { SERVER_URL } from '../../test-utils' +import { truncateTables } from '../../database-utils' +import { createUserTest, getLoginToken, testServer } from '../../server-utils' describe('POST /api/v1/users', () => { + beforeEach(async () => { + await truncateTables(['user']) + }) + it('Should create a valid user and return 201', async () => { const user: CreateUser = { email: 'dummy@gmail.com', @@ -13,7 +17,7 @@ describe('POST /api/v1/users', () => { password: '123123123' } - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .post('/api/v1/users') .send(user) .expect(201) @@ -21,14 +25,11 @@ describe('POST /api/v1/users', () => { expect(res.body.email).equals('dummy@gmail.com') expect(res.body.firstName).equals('super') expect(res.body.lastName).equals('test') - expect(res.body).keys([ - 'id', - 'email', - 'firstName', - 'lastName', - 'created', - 'updated' - ]) + expect(res.body).includes({ + email: 'dummy@gmail.com', + firstName: 'super', + lastName: 'test' + }) }) it('Should return 400 when duplicated email', async () => { @@ -39,7 +40,12 @@ describe('POST /api/v1/users', () => { password: '123123123' } - const res = await supertest(SERVER_URL) + let res = await supertest(testServer) + .post('/api/v1/users') + .send(user) + .expect(201) + + res = await supertest(testServer) .post('/api/v1/users') .send(user) .expect(400) @@ -57,7 +63,7 @@ describe('POST /api/v1/users', () => { lastName: 'test' } - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .post('/api/v1/users') .send(user) .expect(400) diff --git a/test/integration/server/users/login.test.ts b/test/integration/server/users/login.test.ts index add6a21..2050840 100644 --- a/test/integration/server/users/login.test.ts +++ b/test/integration/server/users/login.test.ts @@ -1,9 +1,12 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createUserTest, SERVER_URL } from '../../test-utils' +import { truncateTables } from '../../database-utils' +import { createUserTest, testServer } from '../../server-utils' describe('POST /api/v1/users/login', () => { - before(async () => { + beforeEach(async () => { + await truncateTables(['user']) + const user = { email: 'dude@gmail.com', firstName: 'super', @@ -15,7 +18,7 @@ describe('POST /api/v1/users/login', () => { }) it('Should return a valid token', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .post('/api/v1/users/login') .send({ email: 'dude@gmail.com', password: 'test' }) .expect(200) @@ -24,7 +27,7 @@ describe('POST /api/v1/users/login', () => { }) it('Should return 400 when missing password', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .post('/api/v1/users/login') .send({ email: 'dude@mail.com' }) .expect(400) diff --git a/test/integration/server/users/update-user.test.ts b/test/integration/server/users/update-user.test.ts index d5ac577..f01d095 100644 --- a/test/integration/server/users/update-user.test.ts +++ b/test/integration/server/users/update-user.test.ts @@ -1,9 +1,14 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' +import { truncateTables } from '../../database-utils' +import { createUserTest, getLoginToken, testServer } from '../../server-utils' describe('PUT /api/v1/users', () => { - before(async () => { + let token: string + + beforeEach(async () => { + await truncateTables(['user']) + const user = { email: 'dude@gmail.com', firstName: 'super', @@ -12,24 +17,17 @@ describe('PUT /api/v1/users', () => { } await createUserTest(user) + + token = await getLoginToken('dude@gmail.com', 'test') }) it('Should update first and last name', async () => { - const token = await getLoginToken('dude@gmail.com', 'test') - let res = await supertest(SERVER_URL) + const res = await supertest(testServer) .put('/api/v1/users') .set('Authorization', token) .send({ firstName: 'dude', lastName: 'test' }) .expect(200) - res = await supertest(SERVER_URL) - .get('/api/v1/users/me') - .set('Authorization', token) - .expect(200) - - expect(res.body.firstName).equals('dude') - expect(res.body.lastName).equals('test') - expect(res.body).include({ firstName: 'dude', lastName: 'test' @@ -37,9 +35,9 @@ describe('PUT /api/v1/users', () => { }) it('Should return 400 when missing lastName data', async () => { - const res = await supertest(SERVER_URL) - .put('/api/v1/users/password') - .set('Authorization', '') + const res = await supertest(testServer) + .put('/api/v1/users') + .set('Authorization', token) .send({ firstName: 'dude' }) .expect(400) @@ -49,8 +47,8 @@ describe('PUT /api/v1/users', () => { }) it('Should return unauthorized when token is not valid', async () => { - const res = await supertest(SERVER_URL) - .put('/api/v1/users/password') + const res = await supertest(testServer) + .put('/api/v1/users') .set('Authorization', 'wrong token') .expect(401) @@ -58,8 +56,8 @@ describe('PUT /api/v1/users', () => { }) it('Should return unauthorized when token is missing', async () => { - const res = await supertest(SERVER_URL) - .put('/api/v1/users/password') + const res = await supertest(testServer) + .put('/api/v1/users') .expect(401) expect(res.body.code).equals(30002) diff --git a/test/integration/server/users/user-me.test.ts b/test/integration/server/users/user-me.test.ts index 9073d93..57578ab 100644 --- a/test/integration/server/users/user-me.test.ts +++ b/test/integration/server/users/user-me.test.ts @@ -1,9 +1,12 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { createUserTest, getLoginToken, SERVER_URL } from '../../test-utils' +import { truncateTables } from '../../database-utils' +import { createUserTest, getLoginToken, testServer } from '../../server-utils' describe('GET /api/v1/users/me', () => { - before(async () => { + beforeEach(async () => { + await truncateTables(['user']) + const user = { email: 'dude@gmail.com', firstName: 'super', @@ -16,7 +19,7 @@ describe('GET /api/v1/users/me', () => { it('Should return user information', async () => { const token = await getLoginToken('dude@gmail.com', 'test') - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .get('/api/v1/users/me') .set('Authorization', token) .expect(200) @@ -32,7 +35,7 @@ describe('GET /api/v1/users/me', () => { }) it('Should return unauthorized when token is not valid', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .get('/api/v1/users/me') .set('Authorization', 'wrong token') .expect(401) @@ -41,7 +44,7 @@ describe('GET /api/v1/users/me', () => { }) it('Should return unauthorized when token is missing', async () => { - const res = await supertest(SERVER_URL) + const res = await supertest(testServer) .get('/api/v1/users/me') .expect(401) diff --git a/test/integration/test-utils.ts b/test/integration/test-utils.ts deleted file mode 100644 index b04cfa1..0000000 --- a/test/integration/test-utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as supertest from 'supertest' -import { CreateUser, UserModel } from '../../src/server/users/model' - -export const SERVER_URL = `localhost:${process.env.PORT || 8080}` - -export async function createUserTest(user: CreateUser): Promise { - const res = await supertest(SERVER_URL) - .post('/api/v1/users') - .send(user) - .expect(201) - - return res.body -} - -export async function getLoginToken( - email: string, - password: string -): Promise { - const res = await supertest(SERVER_URL) - .post('/api/v1/users/login') - .send({ email, password }) - .expect(200) - - return res.body.accessToken -} From f8d76b8107a72f4bd2b25e4dd4c35f1494212860 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Fri, 13 Apr 2018 23:13:57 +0100 Subject: [PATCH 21/35] working on tests --- .travis.yml | 2 -- package.json | 6 +++--- test/unit/lib/hasher.test.ts | 7 +++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 495f550..1ea1c43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,4 @@ services: - mysql before_install: - mysql -u root --password="" < db-scripts/create_database.sql -before_script: - - npm run build && npm run start script: npm run test:all diff --git a/package.json b/package.json index 0a1290b..138022b 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", "start": "node dist/src/index.js", "start:dev": "tsc-watch --onSuccess 'node --inspect=0.0.0.0:5858 dist/src/index.js'", - "test": "npm run build && mocha --recursive dist/test/unit/**/*.test.js", - "test:integration": "npm run build && mocha --exit --recursive dist/test/integration/**/*.test.js", - "test:all": "npm run build && mocha --exit --recursive dist/test/integration/**/*.test.js" + "test": "npm run build && mocha --exit --recursive dist/test/unit", + "test:integration": "npm run build && mocha --exit --recursive dist/test/integration", + "test:all": "npm run build && mocha --exit --recursive dist/test" }, "dependencies": { "async": "^2.6.0", diff --git a/test/unit/lib/hasher.test.ts b/test/unit/lib/hasher.test.ts index e69de29..5a93f27 100644 --- a/test/unit/lib/hasher.test.ts +++ b/test/unit/lib/hasher.test.ts @@ -0,0 +1,7 @@ +import { expect } from 'chai' + +describe('BCryptHasher#hashPassword', () => { + it('Should return a hashed password', async () => { + expect(true) + }) +}) From f497ff33284f44858e3c15dd3fa28165a3f528bb Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 01:00:09 +0100 Subject: [PATCH 22/35] add health --- README.md | 2 - package-lock.json | 5 ++ package.json | 1 + src/container.ts | 4 + src/index.ts | 23 +++-- src/lib/health/index.ts | 29 ++++++ src/server/health/controller.ts | 12 ++- src/server/health/index.ts | 7 +- src/server/index.ts | 90 ++++++++++++------- src/server/middlewares/authentication.ts | 23 ++--- src/server/middlewares/authorization.ts | 16 ++++ src/server/middlewares/index.ts | 1 + src/server/middlewares/response-time.ts | 2 +- src/server/tasks/controller.ts | 1 + src/server/tasks/index.ts | 30 +++---- src/server/users/controller.ts | 1 + src/server/users/index.ts | 18 ++-- test/integration/global-hooks.test.ts | 8 +- test/integration/server-utils.ts | 7 +- .../server/tasks/create-task.test.ts | 1 + .../server/users/create-user.test.ts | 4 +- tslint.json | 17 +++- 22 files changed, 195 insertions(+), 107 deletions(-) create mode 100644 src/lib/health/index.ts create mode 100644 src/server/middlewares/authorization.ts diff --git a/README.md b/README.md index 956b8ed..d8a7428 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,5 @@ TypeBaked is a boilerplate template for building nodejs and typescript services. ## Todo * Unit and Integration Tests -* Authorization & Authentication Middlewares * Cache Middleware cache('url', data) & invalidateCache('url') * set etag and last-modified headers -* Add location to create diff --git a/package-lock.json b/package-lock.json index 400fc7a..af4a77e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2250,6 +2250,11 @@ } } }, + "moment": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.0.tgz", + "integrity": "sha512-1muXCh8jb1N/gHRbn9VDUBr0GYb8A/aVcHlII9QSB68a50spqEVLIGN6KVmCOnSvJrUhC0edGgKU5ofnGXdYdg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index 138022b..5d18247 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "koa-bodyparser": "^4.2.0", "koa-helmet": "^3.3.0", "koa-router": "^7.4.0", + "moment": "^2.22.0", "mysql2": "^1.5.3", "pino": "^4.15.0" }, diff --git a/src/container.ts b/src/container.ts index 7964bcd..81f69c8 100644 --- a/src/container.ts +++ b/src/container.ts @@ -2,10 +2,12 @@ import { Logger } from 'pino' import { Authenticator, JWTAuthenticator } from './lib/authentication' import { MySql } from './lib/database' import { BCryptHasher, Hasher } from './lib/hasher' +import { HealthMonitor } from './lib/health' import { TaskManager, UserManager } from './managers' import { TaskRepository, UserRepository } from './repositories' export interface ServiceContainer { + health: HealthMonitor logger: Logger lib: { hasher: Hasher @@ -26,8 +28,10 @@ export function createContainer(db: MySql, logger: Logger): ServiceContainer { const userRepo = new UserRepository(db) const hasher = new BCryptHasher() const authenticator = new JWTAuthenticator(userRepo) + const healthMonitor = new HealthMonitor() return { + health: healthMonitor, logger, lib: { hasher, diff --git a/src/index.ts b/src/index.ts index 2bd7526..685477d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,8 @@ import { Server } from 'http' import * as pino from 'pino' import { createContainer } from './container' import { MySql } from './lib/database' -import * as server from './server' +import { HealthMonitor } from './lib/health' +import { AppServer, createServer } from './server' export async function init() { const logger = pino() @@ -23,12 +24,15 @@ export async function init() { logger.info('Apply database migration') await db.schemaMigration() - const port = process.env.PORT || 8080 + const port = Number(process.env.PORT) || 8080 const container = createContainer(db, logger) - const app = server.createServer(container).listen(port) + const app = createServer(container) + const health = container.health + + app.listen(port) // Register global process events and graceful shutdown - registerProcessEvents(logger, app, db) + registerProcessEvents(logger, app, db, health) logger.info(`Application running on port: ${port}`) } catch (e) { @@ -36,7 +40,12 @@ export async function init() { } } -function registerProcessEvents(logger: pino.Logger, app: Server, db: MySql) { +function registerProcessEvents( + logger: pino.Logger, + app: AppServer, + db: MySql, + health: HealthMonitor +) { process.on('uncaughtException', (error: Error) => { logger.error('UncaughtException', error) }) @@ -48,8 +57,10 @@ function registerProcessEvents(logger: pino.Logger, app: Server, db: MySql) { process.on('SIGTERM', async () => { logger.info('Starting graceful shutdown') + health.shuttingDown() + let exitCode = 0 - const shutdown = [server.closeServer(app), db.closeDatabase()] + const shutdown = [app.closeServer(), db.closeDatabase()] for (const s of shutdown) { try { diff --git a/src/lib/health/index.ts b/src/lib/health/index.ts new file mode 100644 index 0000000..2ef3a4e --- /dev/null +++ b/src/lib/health/index.ts @@ -0,0 +1,29 @@ +import * as moment from 'moment' + +export interface Status { + startTime: string + upTime: string + isShuttingDown: boolean +} + +export class HealthMonitor { + private startTime: number + private isShuttingDown: boolean + + constructor() { + this.isShuttingDown = false + this.startTime = Date.now() + } + + public shuttingDown() { + this.isShuttingDown = true + } + + public getStatus(): Status { + return { + startTime: new Date(this.startTime).toISOString(), + upTime: moment(this.startTime).fromNow(true), + isShuttingDown: this.isShuttingDown + } + } +} diff --git a/src/server/health/controller.ts b/src/server/health/controller.ts index 7a489dd..6c17114 100644 --- a/src/server/health/controller.ts +++ b/src/server/health/controller.ts @@ -1,7 +1,17 @@ import { Context } from 'koa' +import { HealthMonitor } from '../../lib/health' export default class HealthController { + private health: HealthMonitor + + constructor(health: HealthMonitor) { + this.health = health + } + public getHealth(ctx: Context) { - ctx.status = 200 + const status = this.health.getStatus() + + ctx.body = status + ctx.status = status.isShuttingDown ? 503 : 200 } } diff --git a/src/server/health/index.ts b/src/server/health/index.ts index 64163e4..22dda79 100644 --- a/src/server/health/index.ts +++ b/src/server/health/index.ts @@ -1,12 +1,13 @@ import * as Koa from 'koa' import * as Router from 'koa-router' +import { ServiceContainer } from '../../container' import HealthController from './controller' -export function init(server: Koa) { - const controller = new HealthController() +export function init(server: Koa, container: ServiceContainer) { + const controller = new HealthController(container.health) const router = new Router() - router.get('/health', controller.getHealth.bind(this)) + router.get('/health', controller.getHealth.bind(controller)) server.use(router.routes()) } diff --git a/src/server/index.ts b/src/server/index.ts index 24335e1..9985151 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,13 +3,69 @@ import { Server } from 'http' import * as Koa from 'koa' import * as helmet from 'koa-helmet' import { ServiceContainer } from '../container' +import { AppError } from '../errors' import * as health from './health' import * as middlewares from './middlewares' import * as task from './tasks' import * as user from './users' -export function createServer(container: ServiceContainer): Koa { +export class AppServer { + private app: Koa + private server: Server + + constructor(app: Koa) { + this.app = app + } + + public listen(port: number): Server { + this.server = this.app.listen(port) + return this.server + } + + public getServer(): Server { + return this.server + } + + public closeServer(): Promise { + if (this.server === undefined) { + throw new AppError(10001, 'Server is not initialized.') + } + + const checkPendingRequests = ( + callback: ErrorCallback + ) => { + this.server.getConnections( + (err: Error | null, pendingRequests: number) => { + if (err) { + callback(err) + } else if (pendingRequests > 0) { + callback(Error(`Number of pending requests: ${pendingRequests}`)) + } else { + callback(undefined) + } + } + ) + } + + return new Promise((resolve, reject) => { + retry( + { times: 10, interval: 1000 }, + checkPendingRequests.bind(this), + ((error: Error | undefined) => { + if (error) { + this.server.close(() => reject(error)) + } else { + this.server.close(() => resolve()) + } + }).bind(this) + ) + }) + } +} + +export function createServer(container: ServiceContainer): AppServer { const app = new Koa() + const appSrv = new AppServer(app) // Register Middlewares app.use(helmet()) @@ -18,37 +74,9 @@ export function createServer(container: ServiceContainer): Koa { app.use(middlewares.errorHandler(container.logger)) // Register routes - health.init(app) + health.init(app, container) user.init(app, container) task.init(app, container) - return app -} - -export function closeServer(server: Server): Promise { - const checkPendingRequests = (callback: ErrorCallback) => { - server.getConnections((err: Error | null, pendingRequests: number) => { - if (err) { - callback(err) - } else if (pendingRequests > 0) { - callback(Error(`Number of pending requests: ${pendingRequests}`)) - } else { - callback(undefined) - } - }) - } - - return new Promise((resolve, reject) => { - retry( - { times: 10, interval: 1000 }, - checkPendingRequests, - (error: Error | undefined) => { - if (error) { - server.close(() => reject(error)) - } else { - server.close(() => resolve()) - } - } - ) - }) + return appSrv } diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts index 9b2f6e2..a936e5b 100644 --- a/src/server/middlewares/authentication.ts +++ b/src/server/middlewares/authentication.ts @@ -1,27 +1,14 @@ -import * as jwt from 'jsonwebtoken' import { Context } from 'koa' import { IMiddleware } from 'koa-router' import { PermissionError } from '../../errors' -import { Authenticator, Role } from '../../lib/authentication' +import { Authenticator } from '../../lib/authentication' -export function authentication( - authenticator: Authenticator, - roles: Role[] -): IMiddleware { +export function authentication(authenticator: Authenticator): IMiddleware { return async (ctx: Context, next: () => Promise) => { const token = ctx.headers.authorization + const user = await authenticator.validate(token) - try { - const user = await authenticator.validate(token) - - if (roles.indexOf(user.role) < 0) { - throw new PermissionError() - } - - ctx.state.user = user - await next() - } catch (err) { - throw err - } + ctx.state.user = user + await next() } } diff --git a/src/server/middlewares/authorization.ts b/src/server/middlewares/authorization.ts new file mode 100644 index 0000000..2b4ca43 --- /dev/null +++ b/src/server/middlewares/authorization.ts @@ -0,0 +1,16 @@ +import { Context } from 'koa' +import { IMiddleware } from 'koa-router' +import { PermissionError } from '../../errors' +import { AuthUser, Role } from '../../lib/authentication' + +export function authorization(roles: Role[]): IMiddleware { + return async (ctx: Context, next: () => Promise) => { + const user: AuthUser = ctx.state.user + + if (roles.indexOf(user.role) < 0) { + throw new PermissionError() + } + + await next() + } +} diff --git a/src/server/middlewares/index.ts b/src/server/middlewares/index.ts index ef3ee94..dd6b061 100644 --- a/src/server/middlewares/index.ts +++ b/src/server/middlewares/index.ts @@ -1,4 +1,5 @@ export { authentication } from './authentication' +export { authorization } from './authorization' export { errorHandler } from './error-handler' export { logRequest } from './log-request' export { validate } from './validator' diff --git a/src/server/middlewares/response-time.ts b/src/server/middlewares/response-time.ts index 0843961..27b7f98 100644 --- a/src/server/middlewares/response-time.ts +++ b/src/server/middlewares/response-time.ts @@ -5,5 +5,5 @@ export async function responseTime(ctx: Context, next: () => Promise) { await next() - ctx.response.headers['X-Response-Time'] = Date.now() - start + ctx.set('X-Response-Time', (Date.now() - start).toString()) } diff --git a/src/server/tasks/controller.ts b/src/server/tasks/controller.ts index dffc9a8..1079499 100644 --- a/src/server/tasks/controller.ts +++ b/src/server/tasks/controller.ts @@ -40,6 +40,7 @@ export class TaskController { ctx.body = new TaskModel(newTask) ctx.status = 201 + ctx.set('location', `/api/v1/tasks/${newTask.id}`) } public async update(ctx: Context) { diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts index 6be96be..cde1e16 100644 --- a/src/server/tasks/index.ts +++ b/src/server/tasks/index.ts @@ -15,29 +15,23 @@ export function init(server: Koa, container: ServiceContainer) { router.get( '/:id', - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), controller.get.bind(controller) ) router.get( '/', - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), controller.getAll.bind(controller) ) router.post( '/', bodyParser(), - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), middleware.validate({ request: { body: validators.createTask } }), controller.create.bind(controller) ) @@ -45,10 +39,8 @@ export function init(server: Koa, container: ServiceContainer) { router.put( '/:id', bodyParser(), - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), middleware.validate({ params: { id: Joi.number().required() }, request: { @@ -60,10 +52,8 @@ export function init(server: Koa, container: ServiceContainer) { router.delete( '/:id', - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), middleware.validate({ params: { id: Joi.number().required() } }), controller.delete.bind(controller) ) diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index ae54da5..fba128b 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -17,6 +17,7 @@ export class UserController { ctx.body = new UserModel(newUser) ctx.status = 201 + ctx.set('location', '/api/v1/users/me') } public async login(ctx: Context) { diff --git a/src/server/users/index.ts b/src/server/users/index.ts index 2b1ba46..a06eace 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -15,10 +15,8 @@ export function init(server: Koa, container: ServiceContainer) { router.get( '/me', - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), controller.get.bind(controller) ) @@ -39,10 +37,8 @@ export function init(server: Koa, container: ServiceContainer) { router.put( '/', bodyParser(), - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), middleware.validate({ request: { body: validators.updateUser } }), controller.update.bind(controller) ) @@ -50,10 +46,8 @@ export function init(server: Koa, container: ServiceContainer) { router.put( '/password', bodyParser(), - middleware.authentication(container.lib.authenticator, [ - Role.user, - Role.admin - ]), + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.user, Role.admin]), middleware.validate({ request: { body: validators.changePassword diff --git a/test/integration/global-hooks.test.ts b/test/integration/global-hooks.test.ts index 463451f..2e61b09 100644 --- a/test/integration/global-hooks.test.ts +++ b/test/integration/global-hooks.test.ts @@ -1,14 +1,14 @@ -import { closeServer } from '../../src/server' import { database } from './database-utils' -import { testServer } from './server-utils' +import { appServer } from './server-utils' -before(async () => { +before(async function() { + this.timeout(5000) console.info('Initializing database migration.') await database.schemaMigration() }) after(async () => { - const shutdowns = [closeServer(testServer), database.closeDatabase()] + const shutdowns = [appServer.closeServer(), database.closeDatabase()] console.info('Start cleaning test resources.') diff --git a/test/integration/server-utils.ts b/test/integration/server-utils.ts index f6331e6..bd526d7 100644 --- a/test/integration/server-utils.ts +++ b/test/integration/server-utils.ts @@ -1,16 +1,17 @@ import * as pino from 'pino' import * as supertest from 'supertest' import { createContainer } from '../../src/container' -import { closeServer, createServer } from '../../src/server' +import { createServer } from '../../src/server' import { CreateTask, TaskModel } from '../../src/server/tasks/model' import { CreateUser, UserModel } from '../../src/server/users/model' import { database } from './database-utils' const logger = pino({ name: 'test', level: 'silent' }) const container = createContainer(database, logger) -const port = process.env.PORT || 8080 +const port = Number(process.env.PORT) || 8080 -export const testServer = createServer(container).listen(port) +export const appServer = createServer(container) +export const testServer = appServer.listen(port) export async function createUserTest(user: CreateUser): Promise { const res = await supertest(testServer) diff --git a/test/integration/server/tasks/create-task.test.ts b/test/integration/server/tasks/create-task.test.ts index 5c9052a..5e32e18 100644 --- a/test/integration/server/tasks/create-task.test.ts +++ b/test/integration/server/tasks/create-task.test.ts @@ -33,6 +33,7 @@ describe('POST /api/v1/tasks', () => { .send(task) .expect(201) + expect(res.header.location).equals(`/api/v1/tasks/${res.body.id}`) expect(res.body).include({ name: 'Do homework', description: 'Exercise 1 and 2', diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 8f41108..5c24200 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -22,9 +22,7 @@ describe('POST /api/v1/users', () => { .send(user) .expect(201) - expect(res.body.email).equals('dummy@gmail.com') - expect(res.body.firstName).equals('super') - expect(res.body.lastName).equals('test') + expect(res.header.location).equals('/api/v1/users/me') expect(res.body).includes({ email: 'dummy@gmail.com', firstName: 'super', diff --git a/tslint.json b/tslint.json index ac9ca43..c1eaa66 100644 --- a/tslint.json +++ b/tslint.json @@ -1,8 +1,13 @@ { "defaultSeverity": "error", - "extends": ["tslint:recommended", "tslint-config-prettier"], + "extends": [ + "tslint:recommended", + "tslint-config-prettier" + ], "jsRules": {}, - "rulesDirectory": ["tslint-plugin-prettier"], + "rulesDirectory": [ + "tslint-plugin-prettier" + ], "rules": { "prettier": [ true, @@ -14,6 +19,12 @@ "no-console": false, "interface-name": false, "object-literal-sort-keys": false, - "max-classes-per-file": false + "max-classes-per-file": false, + "no-unused-variable": [ + true, + { + "ignore-pattern": "^_" + } + ] } } From 7c4201075ac8a5cc2b4c8b81ed8430833333cd84 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 01:02:58 +0100 Subject: [PATCH 23/35] add health --- tslint.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tslint.json b/tslint.json index c1eaa66..a2796a7 100644 --- a/tslint.json +++ b/tslint.json @@ -20,11 +20,6 @@ "interface-name": false, "object-literal-sort-keys": false, "max-classes-per-file": false, - "no-unused-variable": [ - true, - { - "ignore-pattern": "^_" - } - ] + "no-unused-variable": true } } From b613fbc2fb1331a7b432efa8f88f72a8d7b4faf3 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 10:43:25 +0100 Subject: [PATCH 24/35] add delete user --- .travis.yml | 4 +- package.json | 1 + src/lib/authentication/index.ts | 2 +- src/lib/database/index.ts | 14 +++ src/managers/user-manager.ts | 10 +- src/repositories/user-repository.ts | 23 +++- src/server/users/controller.ts | 6 + src/server/users/index.ts | 10 ++ test/integration/database-utils.ts | 10 ++ .../server/users/delete-user.test.ts | 108 ++++++++++++++++++ 10 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 test/integration/server/users/delete-user.test.ts diff --git a/.travis.yml b/.travis.yml index 1ea1c43..15e9e2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,9 @@ env: - PORT=8080 - DB_HOST=127.0.0.1 - DB_PORT=3306 - - DB_USER=root + - DB_USER=travis services: - mysql before_install: - - mysql -u root --password="" < db-scripts/create_database.sql + - mysql -u travis --password="" < db-scripts/create_database.sql script: npm run test:all diff --git a/package.json b/package.json index 5d18247..5402005 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ ], "scripts": { "build": "rm -rf dist && tsc", + "build:watch": "rm -rf dist && tsc -w", "clean": "rm -rf node_modules coverage dist .nyc_output", "coverage": "nyc --exclude dist/test --reporter=html npm run test:all", "lint": "tslint 'src/**/*.ts' 'test/**/*.test.ts'", diff --git a/src/lib/authentication/index.ts b/src/lib/authentication/index.ts index 1c4ae02..2699f93 100644 --- a/src/lib/authentication/index.ts +++ b/src/lib/authentication/index.ts @@ -31,7 +31,7 @@ export class JWTAuthenticator implements Authenticator { public async validate(token: string): Promise { try { const decode: any = jwt.verify(token, this.secret) - const user = await this.userRepo.find(decode.email) + const user = await this.userRepo.findByEmail(decode.email) return { id: user.id, diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index e54ddc4..94e312d 100644 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -28,6 +28,20 @@ export class MySql { return this.connection } + public async getTransaction(): Promise { + const connection = await this.getConnection() + + return new Promise((resolve, reject) => { + try { + connection.transaction((trx: knex.Transaction) => { + resolve(trx) + }) + } catch (err) { + reject(err) + } + }) + } + public async closeDatabase(): Promise { if (this.connection) { await this.connection.destroy() diff --git a/src/managers/user-manager.ts b/src/managers/user-manager.ts index c7f0a08..9aa72e6 100644 --- a/src/managers/user-manager.ts +++ b/src/managers/user-manager.ts @@ -16,7 +16,7 @@ export class UserManager { } public async findByEmail(email: string): Promise { - return this.repo.find(email) + return this.repo.findByEmail(email) } public async create(user: User): Promise { @@ -28,7 +28,7 @@ export class UserManager { } public async login(email: string, password: string): Promise { - const user = await this.repo.find(email) + const user = await this.repo.findByEmail(email) if (await this.hasher.verifyPassword(password, user.password)) { return this.auth.authenticate(user) @@ -46,7 +46,7 @@ export class UserManager { newPassword: string, oldPassword: string ): Promise { - const user = await this.repo.find(email) + const user = await this.repo.findByEmail(email) const validPassword = await this.hasher.verifyPassword( oldPassword, user.password @@ -60,4 +60,8 @@ export class UserManager { return this.repo.changePassword(email, hashPassword) } + + public delete(userId: number): Promise { + return this.repo.delete(userId) + } } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index a91a139..57eeba3 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -10,7 +10,7 @@ export class UserRepository { this.db = db } - public async find(email: string): Promise { + public async findByEmail(email: string): Promise { const conn = await this.db.getConnection() const row = await conn .table(this.TABLE) @@ -81,6 +81,27 @@ export class UserRepository { .where('email', email) } + public async delete(userId: number): Promise { + const trx = await this.db.getTransaction() + + try { + await trx + .from('task') + .delete() + .where({ user_id: userId }) + + await trx + .from(this.TABLE) + .delete() + .where({ id: userId }) + + await trx.commit() + } catch (error) { + trx.rollback(error) + throw error + } + } + private transform(row: any): User { return { id: row.id, diff --git a/src/server/users/controller.ts b/src/server/users/controller.ts index fba128b..d1a3153 100644 --- a/src/server/users/controller.ts +++ b/src/server/users/controller.ts @@ -62,4 +62,10 @@ export class UserController { ctx.body = new UserModel(user) ctx.status = 200 } + + public async delete(ctx: Context) { + await this.manager.delete(ctx.params.id) + + ctx.status = 204 + } } diff --git a/src/server/users/index.ts b/src/server/users/index.ts index a06eace..e820b65 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -56,5 +56,15 @@ export function init(server: Koa, container: ServiceContainer) { controller.changePassword.bind(controller) ) + router.delete( + '/:id', + middleware.authentication(container.lib.authenticator), + middleware.authorization([Role.admin]), + middleware.validate({ + params: { id: Joi.number().required() } + }), + controller.delete.bind(controller) + ) + server.use(router.routes()) } diff --git a/test/integration/database-utils.ts b/test/integration/database-utils.ts index e13ec62..a3266cd 100644 --- a/test/integration/database-utils.ts +++ b/test/integration/database-utils.ts @@ -1,3 +1,5 @@ +import { Task, User } from '../../src/entities' +import { Role } from '../../src/lib/authentication' import { Configuration, MySql } from '../../src/lib/database' const testMysqlConfig: Configuration = { @@ -18,3 +20,11 @@ export async function truncateTables(tables: string[]) { await conn.raw(`DELETE FROM ${table}`) } } + +export async function setAdminMode(email: string): Promise { + const conn = await database.getConnection() + + await conn.table('user').update({ + role: Role.admin + }) +} diff --git a/test/integration/server/users/delete-user.test.ts b/test/integration/server/users/delete-user.test.ts new file mode 100644 index 0000000..ec89b28 --- /dev/null +++ b/test/integration/server/users/delete-user.test.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { UserModel } from '../../../../src/server/users/model' +import { database, setAdminMode, truncateTables } from '../../database-utils' +import { + createTaskTest, + createUserTest, + getLoginToken, + testServer +} from '../../server-utils' + +describe('DELETE /api/v1/users/:id', () => { + beforeEach(async () => { + await truncateTables(['task', 'user']) + }) + + it('Should delete a user', async () => { + await createUserTest({ + email: 'god@gmail.com', + firstName: 'Jesus', + lastName: 'Christ', + password: 'godmode' + }) + + await setAdminMode('god@gmail.com') + const adminToken = await getLoginToken('god@gmail.com', 'godmode') + + const user = await createUserTest({ + email: 'user@gmail.com', + firstName: 'super', + lastName: 'test', + password: 'test' + }) + + const userToken = await getLoginToken('user@gmail.com', 'test') + await createTaskTest( + { + name: 'Do Something', + description: 'Some random description' + }, + userToken + ) + + await createTaskTest( + { + name: 'Do Something', + description: 'Some random description' + }, + userToken + ) + + await supertest(testServer) + .delete(`/api/v1/users/${user.id}`) + .set('Authorization', adminToken) + .expect(204) + + const conn = await database.getConnection() + + const users = await conn.from('user').select() + + expect(users.length).eql(1) + expect(users[0].email).eql('god@gmail.com') + + const tasks = await conn.from('task').count() + + expect(tasks[0]['count(*)']).eql(0) + }) + + it('Should return not allowed error', async () => { + await createUserTest({ + email: 'god@gmail.com', + firstName: 'Jesus', + lastName: 'Christ', + password: 'godmode' + }) + + const user = await createUserTest({ + email: 'dude@gmail.com', + firstName: 'super', + lastName: 'test', + password: 'test' + }) + + const token = await getLoginToken('god@gmail.com', 'godmode') + + await supertest(testServer) + .delete(`/api/v1/users/${user.id}`) + .set('Authorization', token) + .expect(403) + }) + + it('Should return unauthorized when token is not valid', async () => { + const res = await supertest(testServer) + .delete('/api/v1/users/${user.id}') + .set('Authorization', 'wrong token') + .expect(401) + + expect(res.body.code).equals(30002) + }) + + it('Should return unauthorized when token is missing', async () => { + const res = await supertest(testServer) + .delete('/api/v1/users/1') + .expect(401) + + expect(res.body.code).equals(30002) + }) +}) From edc1e7e8d9438c221f99a462cbc27da25a89b1a6 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 10:49:53 +0100 Subject: [PATCH 25/35] add delete user --- src/errors.ts | 2 +- src/index.ts | 1 - src/repositories/task-repository.ts | 3 ++- src/repositories/user-repository.ts | 3 ++- src/server/middlewares/authentication.ts | 1 - src/server/middlewares/validator.ts | 1 - src/server/tasks/index.ts | 1 - src/server/users/index.ts | 1 - test/integration/database-utils.ts | 10 ++++++---- test/integration/server/tasks/create-task.test.ts | 1 - test/integration/server/tasks/delete-task.test.ts | 5 ++--- test/integration/server/tasks/get-all-tasks.test.ts | 1 - test/integration/server/tasks/get-task.test.ts | 3 +-- test/integration/server/tasks/update-task.test.ts | 1 - test/integration/server/users/create-user.test.ts | 2 +- test/integration/server/users/delete-user.test.ts | 1 - tsconfig.json | 4 +++- tslint.json | 3 +-- 18 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 3eb29c9..43e0b34 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -48,7 +48,7 @@ export class FieldValidationError extends AppError { export class UnauthorizedError extends AppError { constructor(error?: Error) { - super(30002, 'Unauthorized user') + super(30002, 'Unauthorized user', error) } } diff --git a/src/index.ts b/src/index.ts index 685477d..2d21fa2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import { Server } from 'http' import * as pino from 'pino' import { createContainer } from './container' import { MySql } from './lib/database' diff --git a/src/repositories/task-repository.ts b/src/repositories/task-repository.ts index 9f9d1e9..00b587b 100644 --- a/src/repositories/task-repository.ts +++ b/src/repositories/task-repository.ts @@ -65,7 +65,8 @@ export class TaskRepository { task.updated = new Date() const conn = await this.db.getConnection() - const result = await conn + + await conn .table(this.TABLE) .update({ name: task.name, diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 57eeba3..3825097 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -57,7 +57,8 @@ export class UserRepository { user.updated = new Date() const conn = await this.db.getConnection() - const result = await conn.table(this.TABLE).update({ + + await conn.table(this.TABLE).update({ first_name: user.firstName, last_name: user.lastName, password: user.password diff --git a/src/server/middlewares/authentication.ts b/src/server/middlewares/authentication.ts index a936e5b..536cbf2 100644 --- a/src/server/middlewares/authentication.ts +++ b/src/server/middlewares/authentication.ts @@ -1,6 +1,5 @@ import { Context } from 'koa' import { IMiddleware } from 'koa-router' -import { PermissionError } from '../../errors' import { Authenticator } from '../../lib/authentication' export function authentication(authenticator: Authenticator): IMiddleware { diff --git a/src/server/middlewares/validator.ts b/src/server/middlewares/validator.ts index 97a3c3b..58a10b1 100644 --- a/src/server/middlewares/validator.ts +++ b/src/server/middlewares/validator.ts @@ -1,6 +1,5 @@ import * as Joi from 'joi' import { Context } from 'koa' -import * as bodyParser from 'koa-bodyparser' import { IMiddleware } from 'koa-router' import { FieldValidationError } from '../../errors' diff --git a/src/server/tasks/index.ts b/src/server/tasks/index.ts index cde1e16..926122d 100644 --- a/src/server/tasks/index.ts +++ b/src/server/tasks/index.ts @@ -4,7 +4,6 @@ import * as bodyParser from 'koa-bodyparser' import * as Router from 'koa-router' import { ServiceContainer } from '../../container' import { Role } from '../../lib/authentication' -import { UserManager } from '../../managers' import * as middleware from '../middlewares' import { TaskController } from './controller' import * as validators from './validators' diff --git a/src/server/users/index.ts b/src/server/users/index.ts index e820b65..dbbae8f 100644 --- a/src/server/users/index.ts +++ b/src/server/users/index.ts @@ -4,7 +4,6 @@ import * as bodyParser from 'koa-bodyparser' import * as Router from 'koa-router' import { ServiceContainer } from '../../container' import { Role } from '../../lib/authentication' -import { UserManager } from '../../managers' import * as middleware from '../middlewares' import { UserController } from './controller' import * as validators from './validators' diff --git a/test/integration/database-utils.ts b/test/integration/database-utils.ts index a3266cd..43112b8 100644 --- a/test/integration/database-utils.ts +++ b/test/integration/database-utils.ts @@ -1,4 +1,3 @@ -import { Task, User } from '../../src/entities' import { Role } from '../../src/lib/authentication' import { Configuration, MySql } from '../../src/lib/database' @@ -24,7 +23,10 @@ export async function truncateTables(tables: string[]) { export async function setAdminMode(email: string): Promise { const conn = await database.getConnection() - await conn.table('user').update({ - role: Role.admin - }) + await conn + .table('user') + .update({ + role: Role.admin + }) + .where({ email }) } diff --git a/test/integration/server/tasks/create-task.test.ts b/test/integration/server/tasks/create-task.test.ts index 5e32e18..ec4ac88 100644 --- a/test/integration/server/tasks/create-task.test.ts +++ b/test/integration/server/tasks/create-task.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { TaskModel } from '../../../../src/server/tasks/model' import { truncateTables } from '../../database-utils' import { createUserTest, getLoginToken, testServer } from '../../server-utils' diff --git a/test/integration/server/tasks/delete-task.test.ts b/test/integration/server/tasks/delete-task.test.ts index 44954c4..30cecd3 100644 --- a/test/integration/server/tasks/delete-task.test.ts +++ b/test/integration/server/tasks/delete-task.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { TaskModel } from '../../../../src/server/tasks/model' import { truncateTables } from '../../database-utils' import { createTaskTest, @@ -34,12 +33,12 @@ describe('DELETE /api/v1/tasks/:id', () => { const createdTask = await createTaskTest(task, token) - let res = await supertest(testServer) + await supertest(testServer) .delete(`/api/v1/tasks/${createdTask.id}`) .set('Authorization', token) .expect(204) - res = await supertest(testServer) + await supertest(testServer) .get(`/api/v1/tasks/${createdTask.id}`) .set('Authorization', token) .expect(404) diff --git a/test/integration/server/tasks/get-all-tasks.test.ts b/test/integration/server/tasks/get-all-tasks.test.ts index 7abe745..da54184 100644 --- a/test/integration/server/tasks/get-all-tasks.test.ts +++ b/test/integration/server/tasks/get-all-tasks.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { TaskModel } from '../../../../src/server/tasks/model' import { truncateTables } from '../../database-utils' import { createTaskTest, diff --git a/test/integration/server/tasks/get-task.test.ts b/test/integration/server/tasks/get-task.test.ts index c6c24ad..3e21923 100644 --- a/test/integration/server/tasks/get-task.test.ts +++ b/test/integration/server/tasks/get-task.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { TaskModel } from '../../../../src/server/tasks/model' import { truncateTables } from '../../database-utils' import { createTaskTest, @@ -47,7 +46,7 @@ describe('GET /api/v1/tasks/:id', () => { }) it('Should return 404 when task does not exist', async () => { - const res = await supertest(testServer) + await supertest(testServer) .get(`/api/v1/tasks/111111111`) .set('Authorization', token) .expect(404) diff --git a/test/integration/server/tasks/update-task.test.ts b/test/integration/server/tasks/update-task.test.ts index 639dc52..dafb3f2 100644 --- a/test/integration/server/tasks/update-task.test.ts +++ b/test/integration/server/tasks/update-task.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { TaskModel } from '../../../../src/server/tasks/model' import { truncateTables } from '../../database-utils' import { createTaskTest, diff --git a/test/integration/server/users/create-user.test.ts b/test/integration/server/users/create-user.test.ts index 5c24200..178a875 100644 --- a/test/integration/server/users/create-user.test.ts +++ b/test/integration/server/users/create-user.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import * as supertest from 'supertest' import { CreateUser } from '../../../../src/server/users/model' import { truncateTables } from '../../database-utils' -import { createUserTest, getLoginToken, testServer } from '../../server-utils' +import { testServer } from '../../server-utils' describe('POST /api/v1/users', () => { beforeEach(async () => { diff --git a/test/integration/server/users/delete-user.test.ts b/test/integration/server/users/delete-user.test.ts index ec89b28..86480ec 100644 --- a/test/integration/server/users/delete-user.test.ts +++ b/test/integration/server/users/delete-user.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai' import * as supertest from 'supertest' -import { UserModel } from '../../../../src/server/users/model' import { database, setAdminMode, truncateTables } from '../../database-utils' import { createTaskTest, diff --git a/tsconfig.json b/tsconfig.json index 0854cc2..796ae7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,8 @@ "target": "es6", "module": "commonjs", "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "typeRoots": [ "node_modules/@types" ] @@ -15,4 +17,4 @@ "exclude": [ "node_modules" ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index a2796a7..c710175 100644 --- a/tslint.json +++ b/tslint.json @@ -19,7 +19,6 @@ "no-console": false, "interface-name": false, "object-literal-sort-keys": false, - "max-classes-per-file": false, - "no-unused-variable": true + "max-classes-per-file": false } } From 96e48f8abaee0573410961fb27bfb4da1b646889 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 11:02:35 +0100 Subject: [PATCH 26/35] fix travis --- .travis.yml | 22 ++++++++++++++++++---- docker-compose.yml | 34 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 15e9e2d..8f28279 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,28 @@ +addons: + apt: + sources: + - mysql-5.7-trusty + packages: + - mysql-server + - mysql-client + language: node_js + node_js: - "8" + env: global: - PORT=8080 - DB_HOST=127.0.0.1 - DB_PORT=3306 - - DB_USER=travis -services: - - mysql + - DB_USER=root + - DB_PASSWORD=secret + before_install: - - mysql -u travis --password="" < db-scripts/create_database.sql + - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('secret') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" + - sudo mysql_upgrade + - sudo service mysql restart + - mysql -u root --password="secret" < db-scripts/create_database.sql + script: npm run test:all diff --git a/docker-compose.yml b/docker-compose.yml index 2b96989..a0e7c3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,22 @@ version: '2.1' services: - # app: - # build: . - # command: npm run start:dev - # environment: - # - PORT=8080 - # - DB_HOST=mysql - # - DB_PORT=3306 - # - DB_USER=root - # - DB_PASSWORD=secret - # ports: - # - "8080:8080" - # - "5858:5858" - # links: - # - mysql - # volumes: - # - .:/app/ - # network_mode: bridge + app: + build: . + command: npm run start:dev + environment: + - PORT=8080 + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=root + - DB_PASSWORD=secret + ports: + - "8080:8080" + - "5858:5858" + links: + - mysql + volumes: + - .:/app/ + network_mode: bridge mysql: image: mysql:latest environment: From d45b56d9a3aee5e5ffe79d4c1a867b723cbeb519 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 11:39:08 +0100 Subject: [PATCH 27/35] fix travis --- .travis.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8f28279..8b8865c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,19 @@ -addons: - apt: - sources: - - mysql-5.7-trusty - packages: - - mysql-server - - mysql-client - language: node_js - node_js: - "8" - env: global: - PORT=8080 - DB_HOST=127.0.0.1 - DB_PORT=3306 - - DB_USER=root + - DB_USER=travis - DB_PASSWORD=secret +services: + - mysql + before_install: - - sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('secret') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;" - - sudo mysql_upgrade - - sudo service mysql restart - - mysql -u root --password="secret" < db-scripts/create_database.sql + - mysql -u root -e "SET PASSWORD FOR 'travis'@'localhost' = PASSWORD('secret')" + - mysql -u travis --password="secret" < db-scripts/create_database.sql script: npm run test:all From 6293790f1db37931df54d521d28c41f1b0d14b31 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 11:59:13 +0100 Subject: [PATCH 28/35] add coverage --- .travis.yml | 8 +++++++- README.md | 12 +++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b8865c..5526935 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,10 @@ before_install: - mysql -u root -e "SET PASSWORD FOR 'travis'@'localhost' = PASSWORD('secret')" - mysql -u travis --password="secret" < db-scripts/create_database.sql -script: npm run test:all +install: + - npm install -g codecov + +script: + - npm run test:all + - ./node_modules/nyc/bin/nyc.js --exclude dist/test --reporter lcovonly npm run test:all + - codecov diff --git a/README.md b/README.md index d8a7428..8269ab8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# typescript-node [![Build Status](https://travis-ci.org/Talento90/typescript-node.svg?branch=master)](https://travis-ci.org/Talento90/typescript-node) +# typescript-node [![Build Status](https://travis-ci.org/Talento90/typescript-node.svg?branch=master)](https://travis-ci.org/Talento90/typescript-node) [![codecov](https://codecov.io/gh/Talento90/typescript-node/branch/master/graph/badge.svg)](https://codecov.io/gh/Talento90/typescript-node) -TypeBaked is a boilerplate template for building nodejs and typescript services. It supports the following features: + +Template for building nodejs and typescript services. The main goal of this boilerplate is to offer a good Developer Experience (eg: debugging, watch and recompile) by providing the following features out of the box: ***Features*** @@ -35,10 +36,3 @@ TypeBaked is a boilerplate template for building nodejs and typescript services. * *npm run test* - Run unit tests * *npm run test:integration* - Run integration tests * *npm run test:all* - Run Unit and Integration tests - - -## Todo - -* Unit and Integration Tests -* Cache Middleware cache('url', data) & invalidateCache('url') - * set etag and last-modified headers From d9c40b66b70cf30eeefebe6c42deb167e64b3ef1 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 12:01:29 +0100 Subject: [PATCH 29/35] add coverage --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5526935..53e4248 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,5 @@ install: - npm install -g codecov script: - - npm run test:all - ./node_modules/nyc/bin/nyc.js --exclude dist/test --reporter lcovonly npm run test:all - codecov From a360a98d665fd0576c54a69973a1252336d3cc15 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sat, 14 Apr 2018 12:04:21 +0100 Subject: [PATCH 30/35] add coverage --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 53e4248..07fc469 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_install: - mysql -u travis --password="secret" < db-scripts/create_database.sql install: + - npm install - npm install -g codecov script: From b4b56c91464a56b66d7974fdab320fed55bf9fad Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 15 Apr 2018 21:44:08 +0100 Subject: [PATCH 31/35] add health unit test --- test/unit/lib/health.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/unit/lib/health.test.ts diff --git a/test/unit/lib/health.test.ts b/test/unit/lib/health.test.ts new file mode 100644 index 0000000..0f55659 --- /dev/null +++ b/test/unit/lib/health.test.ts @@ -0,0 +1,17 @@ +import { expect } from 'chai' +import { HealthMonitor } from '../../../src/lib/health' + +describe('HealthMonitor#getStatus', () => { + it('Should return isShuttingDown true', async () => { + const health = new HealthMonitor() + let status = health.getStatus() + + expect(status.isShuttingDown).equals(false) + + health.shuttingDown() + + status = health.getStatus() + + expect(status.isShuttingDown).equals(true) + }) +}) From c2476564387c5cee947e7ea0e70b777646f9877f Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Sun, 15 Apr 2018 23:34:16 +0100 Subject: [PATCH 32/35] add integration tests in health --- test/integration/server-utils.ts | 4 ++++ .../server/health/health-check.test.ts | 23 +++++++++++++++++++ test/unit/lib/hasher.test.ts | 21 ++++++++++++++--- .../middlewares}/authentication.test.ts | 0 .../middlewares/authorization.test.ts} | 0 .../server/middlewares/error-handler.test.ts | 0 .../server/middlewares/response-time.test.ts | 0 .../unit/server/middlewares/validator.test.ts | 0 8 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 test/integration/server/health/health-check.test.ts rename test/unit/{lib => server/middlewares}/authentication.test.ts (100%) rename test/unit/{lib/database.test.ts => server/middlewares/authorization.test.ts} (100%) create mode 100644 test/unit/server/middlewares/error-handler.test.ts create mode 100644 test/unit/server/middlewares/response-time.test.ts create mode 100644 test/unit/server/middlewares/validator.test.ts diff --git a/test/integration/server-utils.ts b/test/integration/server-utils.ts index bd526d7..54ff9ce 100644 --- a/test/integration/server-utils.ts +++ b/test/integration/server-utils.ts @@ -22,6 +22,10 @@ export async function createUserTest(user: CreateUser): Promise { return res.body } +export function shuttingDown(): void { + container.health.shuttingDown() +} + export async function createTaskTest( task: CreateTask, token: string diff --git a/test/integration/server/health/health-check.test.ts b/test/integration/server/health/health-check.test.ts new file mode 100644 index 0000000..e298a19 --- /dev/null +++ b/test/integration/server/health/health-check.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'chai' +import * as supertest from 'supertest' +import { shuttingDown, testServer } from '../../server-utils' + +describe('GET /health', () => { + it('Should return 200 when server is running healthy', async () => { + const res = await supertest(testServer) + .get('/health') + .expect(200) + + expect(res.body.isShuttingDown).equals(false) + }) + + it('Should return 503 when server is shutting down', async () => { + shuttingDown() + + const res = await supertest(testServer) + .get('/health') + .expect(503) + + expect(res.body.isShuttingDown).equals(true) + }) +}) diff --git a/test/unit/lib/hasher.test.ts b/test/unit/lib/hasher.test.ts index 5a93f27..0e8ec46 100644 --- a/test/unit/lib/hasher.test.ts +++ b/test/unit/lib/hasher.test.ts @@ -1,7 +1,22 @@ import { expect } from 'chai' +import { BCryptHasher } from '../../../src/lib/hasher' -describe('BCryptHasher#hashPassword', () => { - it('Should return a hashed password', async () => { - expect(true) +describe('BCryptHasher', () => { + it('Should return validate password', async () => { + const hasher = new BCryptHasher() + const hashedPassword = await hasher.hashPassword('password') + + const verify = await hasher.verifyPassword('password', hashedPassword) + + expect(verify).equals(true) + }) + + it('Should return false when password is not valid', async () => { + const hasher = new BCryptHasher() + const hashedPassword = await hasher.hashPassword('password') + + const verify = await hasher.verifyPassword('password123', hashedPassword) + + expect(verify).equals(false) }) }) diff --git a/test/unit/lib/authentication.test.ts b/test/unit/server/middlewares/authentication.test.ts similarity index 100% rename from test/unit/lib/authentication.test.ts rename to test/unit/server/middlewares/authentication.test.ts diff --git a/test/unit/lib/database.test.ts b/test/unit/server/middlewares/authorization.test.ts similarity index 100% rename from test/unit/lib/database.test.ts rename to test/unit/server/middlewares/authorization.test.ts diff --git a/test/unit/server/middlewares/error-handler.test.ts b/test/unit/server/middlewares/error-handler.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/server/middlewares/response-time.test.ts b/test/unit/server/middlewares/response-time.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/server/middlewares/validator.test.ts b/test/unit/server/middlewares/validator.test.ts new file mode 100644 index 0000000..e69de29 From 59b1067e87c9d699d38b8c1ec72887a0ce857527 Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Mon, 16 Apr 2018 23:30:21 +0100 Subject: [PATCH 33/35] add more unit tests --- src/errors.ts | 6 +-- .../server/middlewares/log-request.test.ts | 36 +++++++++++++++++ .../server/middlewares/response-time.test.ts | 26 +++++++++++++ .../unit/server/middlewares/validator.test.ts | 39 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 43e0b34..f6b56cd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,11 +1,11 @@ -export class AppError { +export class AppError extends Error { public code: number - public message: string public error: Error constructor(code: number, message: string, error?: Error) { + super(message) + this.code = code - this.message = message this.error = error } diff --git a/test/unit/server/middlewares/log-request.test.ts b/test/unit/server/middlewares/log-request.test.ts index e69de29..45cd9c2 100644 --- a/test/unit/server/middlewares/log-request.test.ts +++ b/test/unit/server/middlewares/log-request.test.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai' +import * as pino from 'pino' +import * as sinon from 'sinon' +import { logRequest } from '../../../../src/server/middlewares' + +describe('logRequest', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.reset() + }) + + it('Should log info level when no errors', async () => { + const ctx: any = {} + const logger = pino({ name: 'test', level: 'silent' }) + const spy = sinon.spy(logger, 'info') + const logMiddleware = logRequest(logger) + + await logMiddleware(ctx, () => Promise.resolve()) + + expect(spy.calledOnce).equals(true) + expect(spy.args[0].length).equals(2) + }) + + it('Should log error level when status code is >= 400', async () => { + const ctx: any = { status: 500 } + const logger = pino({ name: 'test', level: 'silent' }) + const spy = sinon.spy(logger, 'error') + const logMiddleware = logRequest(logger) + + await logMiddleware(ctx, () => Promise.resolve()) + + expect(spy.calledOnce).equals(true) + expect(spy.args[0].length).equals(3) + }) +}) diff --git a/test/unit/server/middlewares/response-time.test.ts b/test/unit/server/middlewares/response-time.test.ts index e69de29..5830a4a 100644 --- a/test/unit/server/middlewares/response-time.test.ts +++ b/test/unit/server/middlewares/response-time.test.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai' +import * as sinon from 'sinon' +import { responseTime } from '../../../../src/server/middlewares' + +describe('responseTime', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.reset() + }) + + it('Should set header x-response-time', async () => { + const ctx: any = { + set: () => { + return + } + } + + const stub = sinon.stub(ctx, 'set') + + await responseTime(ctx, () => Promise.resolve()) + + expect(stub.calledOnce).equals(true) + expect(stub.args[0][0]).equals('X-Response-Time') + }) +}) diff --git a/test/unit/server/middlewares/validator.test.ts b/test/unit/server/middlewares/validator.test.ts index e69de29..543f43f 100644 --- a/test/unit/server/middlewares/validator.test.ts +++ b/test/unit/server/middlewares/validator.test.ts @@ -0,0 +1,39 @@ +import { expect } from 'chai' +import * as Joi from 'joi' +import { FieldValidationError } from '../../../../src/errors' +import { validate } from '../../../../src/server/middlewares' + +describe('validate', () => { + it('Should not throw an error when body valid', async () => { + const ctx: any = { + request: { + body: { name: 'test' } + } + } + + const schema = { request: { body: { name: Joi.string().required() } } } + + const validateMiddleware = validate(schema) + + await validateMiddleware(ctx, () => Promise.resolve()) + }) + + it('Should throw an error when body is not valid', async () => { + const ctx: any = { + request: { + body: {} + } + } + + const schema = { request: { body: { name: Joi.string().required() } } } + const validateMiddleware = validate(schema) + + try { + await validateMiddleware(ctx, () => Promise.resolve()) + expect.fail('Should not reach this point') + } catch (error) { + expect(error).instanceof(FieldValidationError) + expect(error.fields[0].message).equals('"name" is required') + } + }) +}) From 6b3a5e13dd000477b70333ac9a1cd9b6f71608fa Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Tue, 17 Apr 2018 21:28:30 +0100 Subject: [PATCH 34/35] add unit tests --- test/unit/lib/health.test.ts | 18 ++--- .../server/middlewares/authentication.test.ts | 68 +++++++++++++++++++ .../server/middlewares/authorization.test.ts | 53 +++++++++++++++ .../server/middlewares/error-handler.test.ts | 49 +++++++++++++ .../server/middlewares/log-request.test.ts | 2 +- .../server/middlewares/response-time.test.ts | 8 +-- 6 files changed, 185 insertions(+), 13 deletions(-) diff --git a/test/unit/lib/health.test.ts b/test/unit/lib/health.test.ts index 0f55659..2bc93b5 100644 --- a/test/unit/lib/health.test.ts +++ b/test/unit/lib/health.test.ts @@ -1,17 +1,19 @@ import { expect } from 'chai' import { HealthMonitor } from '../../../src/lib/health' -describe('HealthMonitor#getStatus', () => { - it('Should return isShuttingDown true', async () => { - const health = new HealthMonitor() - let status = health.getStatus() +describe('HealthMonitor', () => { + describe('getStatus', () => { + it('Should return isShuttingDown true', async () => { + const health = new HealthMonitor() + let status = health.getStatus() - expect(status.isShuttingDown).equals(false) + expect(status.isShuttingDown).equals(false) - health.shuttingDown() + health.shuttingDown() - status = health.getStatus() + status = health.getStatus() - expect(status.isShuttingDown).equals(true) + expect(status.isShuttingDown).equals(true) + }) }) }) diff --git a/test/unit/server/middlewares/authentication.test.ts b/test/unit/server/middlewares/authentication.test.ts index e69de29..a6613bb 100644 --- a/test/unit/server/middlewares/authentication.test.ts +++ b/test/unit/server/middlewares/authentication.test.ts @@ -0,0 +1,68 @@ +import { expect } from 'chai' +import * as sinon from 'sinon' +import { UnauthorizedError } from '../../../../src/errors' +import { Role } from '../../../../src/lib/authentication' +import { authentication } from '../../../../src/server/middlewares' + +describe.only('authentication', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.restore() + }) + + it('Should set context with the user data', async () => { + const ctx: any = { + headers: { + authorization: 'jwt token' + }, + state: {} + } + + const fakeAuthenticator: any = { + validate: sandbox.stub().returns({ + id: 1, + email: 'me@mail.com', + role: Role.admin + }) + } + + const spy = sandbox.spy() + const authenticationMiddleware = authentication(fakeAuthenticator) + + await authenticationMiddleware(ctx, spy) + + expect(fakeAuthenticator.validate.calledOnce).equals(true) + expect(ctx.state.user).eql({ + id: 1, + email: 'me@mail.com', + role: Role.admin + }) + expect(spy.calledOnce).eql(true) + }) + + it('Should throw UnauthorizedError', async () => { + const ctx: any = { + headers: { + authorization: 'jwt token' + }, + state: {} + } + + const fakeAuthenticator: any = { + validate: sandbox.stub().throws(new UnauthorizedError()) + } + + const spy = sandbox.spy() + const authenticationMiddleware = authentication(fakeAuthenticator) + + try { + await authenticationMiddleware(ctx, spy) + } catch (error) { + expect(error).instanceof(UnauthorizedError) + } + + expect(fakeAuthenticator.validate.calledOnce).equals(true) + expect(spy.calledOnce).eql(false) + }) +}) diff --git a/test/unit/server/middlewares/authorization.test.ts b/test/unit/server/middlewares/authorization.test.ts index e69de29..09b0a08 100644 --- a/test/unit/server/middlewares/authorization.test.ts +++ b/test/unit/server/middlewares/authorization.test.ts @@ -0,0 +1,53 @@ +import { expect } from 'chai' +import * as sinon from 'sinon' +import { PermissionError } from '../../../../src/errors' +import { Role } from '../../../../src/lib/authentication' +import { authorization } from '../../../../src/server/middlewares' + +describe('authorization', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.restore() + }) + + it('Should pass when user contains permission access', async () => { + const ctx: any = { + state: { + user: { + role: Role.user + } + } + } + + const authorizationMiddleware = authorization([Role.user, Role.admin]) + const spy = sandbox.spy() + + await authorizationMiddleware(ctx, spy) + + expect(spy.calledOnce).equals(true) + }) + + it('Should throw PermissionError when user is not allowed', async () => { + const ctx: any = { + state: { + user: { + role: Role.user + } + } + } + + const authorizationMiddleware = authorization([Role.admin]) + const spy = sandbox.spy() + + try { + await authorizationMiddleware(ctx, spy) + + expect.fail('Should throw an exception') + } catch (error) { + expect(error).instanceof(PermissionError) + } + + expect(spy.calledOnce).equals(false) + }) +}) diff --git a/test/unit/server/middlewares/error-handler.test.ts b/test/unit/server/middlewares/error-handler.test.ts index e69de29..7dbf7df 100644 --- a/test/unit/server/middlewares/error-handler.test.ts +++ b/test/unit/server/middlewares/error-handler.test.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai' +import * as pino from 'pino' +import * as sinon from 'sinon' +import { NotFoundError } from '../../../../src/errors' +import { errorHandler } from '../../../../src/server/middlewares' + +describe('errorHandler', () => { + const sandbox = sinon.createSandbox() + + afterEach(() => { + sandbox.restore() + }) + + it('Should create an Internal Error Server when error is a unknown error', async () => { + const ctx: any = {} + const logger = pino({ name: 'test', level: 'silent' }) + const spy = sinon.spy(logger, 'error') + const errorHandlerMiddleware = errorHandler(logger) + + await errorHandlerMiddleware(ctx, () => Promise.reject('Unknown error')) + + expect(spy.calledOnce).equals(true) + expect(spy.args[0][1]).equals('Unknown error') + expect(ctx.status).equals(500) + expect(ctx.body).includes({ + code: 10000, + message: 'Internal Error Server' + }) + }) + + it('Should handle the error when is a AppError', async () => { + const ctx: any = {} + const logger = pino({ name: 'test', level: 'silent' }) + const spy = sinon.spy(logger, 'error') + const errorHandlerMiddleware = errorHandler(logger) + + await errorHandlerMiddleware(ctx, () => + Promise.reject(new NotFoundError('Test Not Found')) + ) + + expect(spy.calledOnce).equals(true) + expect(spy.args[0][1]).instanceof(NotFoundError) + expect(ctx.status).equals(404) + expect(ctx.body).eql({ + code: 20000, + message: 'Test Not Found' + }) + }) +}) diff --git a/test/unit/server/middlewares/log-request.test.ts b/test/unit/server/middlewares/log-request.test.ts index 45cd9c2..8c6559c 100644 --- a/test/unit/server/middlewares/log-request.test.ts +++ b/test/unit/server/middlewares/log-request.test.ts @@ -7,7 +7,7 @@ describe('logRequest', () => { const sandbox = sinon.createSandbox() afterEach(() => { - sandbox.reset() + sandbox.restore() }) it('Should log info level when no errors', async () => { diff --git a/test/unit/server/middlewares/response-time.test.ts b/test/unit/server/middlewares/response-time.test.ts index 5830a4a..521b070 100644 --- a/test/unit/server/middlewares/response-time.test.ts +++ b/test/unit/server/middlewares/response-time.test.ts @@ -6,7 +6,7 @@ describe('responseTime', () => { const sandbox = sinon.createSandbox() afterEach(() => { - sandbox.reset() + sandbox.restore() }) it('Should set header x-response-time', async () => { @@ -16,11 +16,11 @@ describe('responseTime', () => { } } - const stub = sinon.stub(ctx, 'set') + const spy = sinon.spy(ctx, 'set') await responseTime(ctx, () => Promise.resolve()) - expect(stub.calledOnce).equals(true) - expect(stub.args[0][0]).equals('X-Response-Time') + expect(spy.calledOnce).equals(true) + expect(spy.args[0][0]).equals('X-Response-Time') }) }) From 02a9dd9a3b9490f8b5a4610aad7ae997e5509caa Mon Sep 17 00:00:00 2001 From: Marco Talento Date: Tue, 17 Apr 2018 21:29:10 +0100 Subject: [PATCH 35/35] add unit tests --- test/unit/server/middlewares/authentication.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/server/middlewares/authentication.test.ts b/test/unit/server/middlewares/authentication.test.ts index a6613bb..cf003e8 100644 --- a/test/unit/server/middlewares/authentication.test.ts +++ b/test/unit/server/middlewares/authentication.test.ts @@ -4,7 +4,7 @@ import { UnauthorizedError } from '../../../../src/errors' import { Role } from '../../../../src/lib/authentication' import { authentication } from '../../../../src/server/middlewares' -describe.only('authentication', () => { +describe('authentication', () => { const sandbox = sinon.createSandbox() afterEach(() => {