diff --git a/.env.docker b/.env.docker index 0ec71b8..5dffa0a 100644 --- a/.env.docker +++ b/.env.docker @@ -1,15 +1,17 @@ # Application -APP_NAME='Express API ES6 Starter' +APP_NAME='Node JS Starter' APP_VERSION='1.0.0' APP_PORT='8848' APP_HOST='0.0.0.0' # Log -LOGGING_DIR='logs' -LOGGING_LEVEL='debug' +LOG_DIR='logs' +LOG_LEVEL='debug' +LOG_RETENTION_PERIOD='' +ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database -DB_CLIENT='pg' +DB_CLIENT='mysql' # App Environment DB_PORT='5432' diff --git a/.env.example b/.env.example index 417b55f..975553d 100644 --- a/.env.example +++ b/.env.example @@ -1,22 +1,31 @@ # Application -APP_NAME='Express API ES6 Starter' +APP_NAME='Node JS Starter' APP_VERSION='1.0.0' APP_PORT='8848' APP_HOST='127.0.0.1' # Log +LOG_DIR='logs' LOG_LEVEL='debug' +LOG_RETENTION_PERIOD='' +ENABLE_FILE_LOG_TRANSPORT='TRUE' # Database -DB_CLIENT='pg' +DB_CLIENT='mysql' + # App Environment -DB_PORT='5432' +DB_PORT='3306' DB_HOST='localhost' DB_NAME='express' DB_USER='username' DB_PASSWORD='password' + +# Authenication parameters +AUTH_URL='http://localhost:5000/' +AUTH_CLIENT_ID='secret-client-id' + # Sentry # https://docs.sentry.io/quickstart SENTRY_DSN='' diff --git a/.prettierrc b/.prettierrc index 8d7bf5f..4ecea61 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,3 +1,4 @@ tabWidth: 2 printWidth: 120 singleQuote: true +arrowParens: avoid diff --git a/.travis.yml b/.travis.yml index d5c084f..7cc5cab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ node_js: - lts/dubnium services: - - postgresql + - mysql branches: only: @@ -14,13 +14,13 @@ branches: env: > NODE_ENV=test - APP_NAME='Express API ES6 Starter' + APP_NAME='Node JS Starter' APP_VERSION='1.0.0' TEST_APP_PORT='9945' - TEST_DB_NAME='express_test' + TEST_DB_NAME='app_test' TEST_DB_PASSWORD='' - TEST_DB_PORT='5432' - TEST_DB_USER='postgres' + TEST_DB_PORT='3306' + TEST_DB_USER='mysql' before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash @@ -28,7 +28,6 @@ before_install: before_script: - cp .env.example .env - - psql -c 'create database express_test;' -U postgres - yarn migrate script: diff --git a/README.md b/README.md index 90a9d85..d98090f 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,6 @@ Example, $ yarn make:migration create_tags_table $ yarn make:seeder 02_insert_tags -## Using Docker - -### Using docker-compose - -Use [docker-compose](https://docs.docker.com/compose/) to quickly bring up a stack with pre-configured Postgres database container. Data is ephemeral and containers will disappear when stack is removed. - -Specific configuration for Docker is in `.env.docker` - -- `0.0.0.0` as `$APP_HOST` to expose app on Docker network interface -- Pre-configured Postgres settings - can be updated to point to another Postgres host - -Bring up stack, - - $ docker-compose up - -Navigate to http://localhost:8848/api-docs/ to verify application is running from docker. - -Bring down stack, - - $ docker-compose down - ### Multi-stage docker builds There are multiple build targets available for different stages. These images can be used to deploy or run jobs in different container based cloud infrastructure like Kubernetes, AWS ECS, Fargate, GCP Cloud Run etc. @@ -100,4 +79,3 @@ To run the tests you need to create a separate test database. Don't forget to up Run tests with coverage. $ yarn test:coverage - diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bb1d2db..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3.4' -services: - express-api-es6-starter: - build: - context: . - target: dev - volumes: - - .env.docker:/app/.env - ports: - - '8848:8848' - depends_on: - - pg - - pg_test - links: - - pg - - pg_test - - migration: - build: - context: . - target: migrate - volumes: - - .env.docker:/app/.env - depends_on: - - pg - - pg_test - links: - - pg - - pg_test - pg: - image: postgres:11-alpine - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=mysecretpassword - - POSTGRES_DB=express - pg_test: - image: postgres:11-alpine - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=mysecretpassword - - POSTGRES_DB=express_test diff --git a/package.json b/package.json index 39ce622..6b5dcd6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "express-api-es6-starter", + "name": "@leapfrogtechnology/nodejs-starter", "version": "1.0.0", - "description": "Express API ES6 Starter", + "description": "NodeJS Starter", "scripts": { "start": "node dist", "prestart": "yarn build", @@ -47,10 +47,10 @@ "dependencies": { "@hapi/boom": "^9.1.0", "@hapi/joi": "^17.1.1", + "@leapfrogtechnology/async-store": "^1.2.0", "@sentry/node": "^5.15.0", + "axios": "^0.19.2", "body-parser": "^1.19.0", - "bookshelf": "^1.1.0", - "bookshelf-virtuals-plugin": "^0.1.1", "compression": "^1.7.4", "cors": "^2.8.5", "dotenv": "^8.2.0", @@ -60,7 +60,7 @@ "knex": "^0.20.12", "lodash": "^4.17.13", "morgan": "^1.10.0", - "pg": "^7.18.2", + "mysql": "^2.18.1", "serve-favicon": "^2.5.0", "swagger-jsdoc": "^3.5.0", "swagger-ui-dist": "^3.25.0", diff --git a/src/auth/index.js b/src/auth/index.js new file mode 100644 index 0000000..bc7f490 --- /dev/null +++ b/src/auth/index.js @@ -0,0 +1,70 @@ +import * as store from '@leapfrogtechnology/async-store'; + +import { http } from '../utils/http'; +import TokenError from '../errors/token'; + +/** + * Extract token from headers in http request. + * + * @param {Object} headers + * @returns {Object} + */ +function extractTokenFromHeaders(headers = {}) { + const { authorization = '' } = headers; + + const [tokenType, token] = authorization.split(' ').filter(Boolean); + + if (tokenType !== 'Bearer' || !token) { + return { + ok: false, + }; + } + + return { + token, + }; +} + +/** + * Fetch user from auth server using token. + * + * @param {String} token + * @throws {NetworkError} + * @returns {Promise} + */ +async function fetchUserByToken(token) { + const { data } = await http.get(`${process.env.AUTH_URL}/userinfo`, { + headers: { + accessToken: token, + clientId: process.env.AUTH_CLIENT_ID, + }, + }); + + return data; +} + +/** + * Validate token received in header. + * + * @param {Object} req + * @param {Object} res + * @param {Object} next + */ +async function authenticateUser(req, res, next) { + try { + const { ok, token } = extractTokenFromHeaders(req.headers); + + if (!ok) { + throw new TokenError('Invalid token'); + } + + const user = await fetchUserByToken(token); + + store.set(user); + next(); + } catch (err) { + next(err); + } +} + +export default authenticateUser; diff --git a/src/controllers/user.js b/src/controllers/user.js new file mode 100644 index 0000000..efeeedc --- /dev/null +++ b/src/controllers/user.js @@ -0,0 +1,37 @@ +import HttpStatus from 'http-status-codes'; + +import * as userService from '../services/user'; + +/** + * Get all users. + * + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +export async function fetch(req, res, next) { + try { + const data = await userService.fetch(); + + res.json({ data }); + } catch (err) { + next(err); + } +} + +/** + * Create a new user. + * + * @param {Object} req + * @param {Object} res + * @param {Function} next + */ +export async function create(req, res, next) { + try { + const data = await userService.create(); + + res.status(HttpStatus.CREATED).json({ data }); + } catch (err) { + next(err); + } +} diff --git a/src/controllers/users.js b/src/controllers/users.js deleted file mode 100644 index 65fdb40..0000000 --- a/src/controllers/users.js +++ /dev/null @@ -1,73 +0,0 @@ -import HttpStatus from 'http-status-codes'; - -import * as userService from '../services/userService'; - -/** - * Get all users. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function fetchAll(req, res, next) { - userService - .getAllUsers() - .then(data => res.json({ data })) - .catch(err => next(err)); -} - -/** - * Get a user by its id. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function fetchById(req, res, next) { - userService - .getUser(req.params.id) - .then(data => res.json({ data })) - .catch(err => next(err)); -} - -/** - * Create a new user. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function create(req, res, next) { - userService - .createUser(req.body) - .then(data => res.status(HttpStatus.CREATED).json({ data })) - .catch(err => next(err)); -} - -/** - * Update a user. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function update(req, res, next) { - userService - .updateUser(req.params.id, req.body) - .then(data => res.json({ data })) - .catch(err => next(err)); -} - -/** - * Delete a user. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - */ -export function deleteUser(req, res, next) { - userService - .deleteUser(req.params.id) - .then(data => res.status(HttpStatus.NO_CONTENT).json({ data })) - .catch(err => next(err)); -} diff --git a/src/db.js b/src/db.js index ea3279b..ed8592f 100644 --- a/src/db.js +++ b/src/db.js @@ -1,5 +1,4 @@ import knexJs from 'knex'; -import bookshelfJs from 'bookshelf'; import knexConfig from './knexfile'; @@ -7,8 +6,5 @@ import knexConfig from './knexfile'; * Database connection. */ const knex = knexJs(knexConfig); -const bookshelf = bookshelfJs(knex); -bookshelf.plugin(['bookshelf-virtuals-plugin']); - -export default bookshelf; +export default knex; diff --git a/src/errors/database.js b/src/errors/database.js new file mode 100644 index 0000000..42af01d --- /dev/null +++ b/src/errors/database.js @@ -0,0 +1,28 @@ +import BaseError from './error'; + +/** + * Error class for database failure and error. + */ +class DatabaseError extends BaseError { + /** + * Constructor for DatabaseError. + * + * @param {String} message + * @returns {DatabaseError} + */ + constructor(message) { + super(message); + this.name = 'DatabaseError'; + } + + /** + * Returns the formatted string representation of error. + * + * @returns {String} + */ + toString() { + return `Database Error: ${this.message}`; + } +} + +export default DatabaseError; diff --git a/src/errors/error.js b/src/errors/error.js new file mode 100644 index 0000000..bc060bc --- /dev/null +++ b/src/errors/error.js @@ -0,0 +1,18 @@ +/** + * Base class for error. + */ +class BaseError extends Error { + /** + * Constructor method for BaseError. + * + * @param {String} message + * @returns {BaseError} + */ + constructor(message = '') { + super(message); + // This flag is used to distinguished from other error types such as joi, boom, etc. + this.isCustom = true; + } +} + +export default BaseError; diff --git a/src/errors/network.js b/src/errors/network.js new file mode 100644 index 0000000..1adb1b3 --- /dev/null +++ b/src/errors/network.js @@ -0,0 +1,28 @@ +import BaseError from './error'; + +/** + * Error class for Network error. + */ +class NetworkError extends BaseError { + /** + * Constructor for NetworkError. + * + * @param {String} message + * @returns {NetworkError} + */ + constructor(message) { + super(message); + this.name = 'NetworkError'; + } + + /** + * Returns the formatted string representation of error. + * + * @returns {String} + */ + toString() { + return `Network Error: ${this.message}`; + } +} + +export default NetworkError; diff --git a/src/errors/token.js b/src/errors/token.js new file mode 100644 index 0000000..f0d36ed --- /dev/null +++ b/src/errors/token.js @@ -0,0 +1,28 @@ +import BaseError from './error'; + +/** + * Error class for Token Error. + */ +class TokenError extends BaseError { + /** + * Constructor for TokenError. + * + * @param {String} message + * @returns {TokenError} + */ + constructor(message) { + super(message); + this.name = 'TokenError'; + } + + /** + * Returns the formatted string representation of error. + * + * @returns {String} + */ + toString() { + return `Token Error: ${this.message}`; + } +} + +export default TokenError; diff --git a/src/index.js b/src/index.js index f47e567..242f99d 100644 --- a/src/index.js +++ b/src/index.js @@ -11,10 +11,11 @@ import favicon from 'serve-favicon'; import bodyParser from 'body-parser'; import compression from 'compression'; import * as Sentry from '@sentry/node'; +import * as store from '@leapfrogtechnology/async-store'; -import routes from './routes'; import json from './middlewares/json'; import logger, { logStream } from './utils/logger'; +import { publicRouter, privateRouter } from './routes'; import * as errorHandler from './middlewares/errorHandler'; // Initialize Sentry @@ -38,6 +39,9 @@ app.locals.version = process.env.APP_VERSION; // This request handler must be the first middleware on the app app.use(Sentry.Handlers.requestHandler()); +// For context propagation of request-response http roundtrip +app.use(store.initializeMiddleware()); + app.use(favicon(path.join(__dirname, '/../public', 'favicon.ico'))); app.use(cors()); app.use(helmet()); @@ -48,7 +52,8 @@ app.use(errorHandler.bodyParser); app.use(json); // API Routes -app.use('/api', routes); +app.use('/api', publicRouter); +app.use('/api', privateRouter); // Swagger UI // Workaround for changing the default URL in swagger.json @@ -58,8 +63,8 @@ const swaggerIndexContent = fs .toString() .replace('https://petstore.swagger.io/v2/swagger.json', '/api/swagger.json'); -app.get('/api-docs/index.html', (req, res) => res.send(swaggerIndexContent)); -app.get('/api-docs', (req, res) => res.redirect('/api-docs/index.html')); +app.get('/api-docs/index.html', (_, res) => res.send(swaggerIndexContent)); +app.get('/api-docs', (_, res) => res.redirect('/api-docs/index.html')); app.use('/api-docs', express.static(pathToSwaggerUi)); // This error handler must be before any other error middleware diff --git a/src/knexfile.js b/src/knexfile.js index 489572e..b4cec0b 100644 --- a/src/knexfile.js +++ b/src/knexfile.js @@ -8,7 +8,7 @@ let connection = { password: process.env.DB_PASSWORD, database: process.env.DB_NAME, charset: 'utf8', - timezone: 'UTC' + timezone: 'UTC', }; // For test environment @@ -19,7 +19,7 @@ if (process.env.NODE_ENV === 'test') { host: process.env.TEST_DB_HOST, user: process.env.TEST_DB_USER, password: process.env.TEST_DB_PASSWORD, - database: process.env.TEST_DB_NAME + database: process.env.TEST_DB_NAME, }; } @@ -32,10 +32,10 @@ module.exports = { migrations: { tableName: 'migrations', directory: './migrations', - stub: './stubs/migration.stub' + stub: './stubs/migration.stub', }, seeds: { directory: './seeds', - stub: './stubs/seed.stub' - } + stub: './stubs/seed.stub', + }, }; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js index 121a4b6..a348359 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.js @@ -13,8 +13,8 @@ export function notFound(req, res) { res.status(HttpStatus.NOT_FOUND).json({ error: { code: HttpStatus.NOT_FOUND, - message: HttpStatus.getStatusText(HttpStatus.NOT_FOUND) - } + message: HttpStatus.getStatusText(HttpStatus.NOT_FOUND), + }, }); } @@ -29,8 +29,8 @@ export function methodNotAllowed(req, res) { res.status(HttpStatus.METHOD_NOT_ALLOWED).json({ error: { code: HttpStatus.METHOD_NOT_ALLOWED, - message: HttpStatus.getStatusText(HttpStatus.METHOD_NOT_ALLOWED) - } + message: HttpStatus.getStatusText(HttpStatus.METHOD_NOT_ALLOWED), + }, }); } @@ -49,8 +49,8 @@ export function bodyParser(err, req, res, next) { res.status(err.status).json({ error: { code: err.status, - message: HttpStatus.getStatusText(err.status) - } + message: HttpStatus.getStatusText(err.status), + }, }); } diff --git a/src/middlewares/json.js b/src/middlewares/json.js index e7bead1..f0ef96a 100644 --- a/src/middlewares/json.js +++ b/src/middlewares/json.js @@ -5,10 +5,10 @@ import _isEmpty from 'lodash/isEmpty'; * Middleware to handle empty JSON body requests and other edge cases if any. * * @param {Object} request - * @param {Object} response + * @param {Object} _ * @param {Function} next */ -export default function json(request, response, next) { +export default function json(request, _, next) { const { body, method } = request; const disallowedHttpHeaders = ['PUT', 'POST', 'PATCH']; diff --git a/src/migrations/20170107202211_create_users_table.js b/src/migrations/20170107202211_create_users_table.js index 60efabd..b2753da 100644 --- a/src/migrations/20170107202211_create_users_table.js +++ b/src/migrations/20170107202211_create_users_table.js @@ -7,12 +7,7 @@ export function up(knex) { return knex.schema.createTable('users', table => { table.increments(); - table - .timestamp('created_at') - .notNull() - .defaultTo(knex.raw('now()')); - table.timestamp('updated_at').notNull(); - table.string('name').notNull(); + table.string('name'); }); } diff --git a/src/models/model.js b/src/models/model.js new file mode 100644 index 0000000..8ff1436 --- /dev/null +++ b/src/models/model.js @@ -0,0 +1,47 @@ +import db from '../db'; + +import { clamp } from '../utils/math'; + +/** + * Base class that is extended by domain models. + */ +class Model { + /** + * This method is required by the domain class. + * + * @returns {String} + */ + getTable() { + throw new Error('Not implemented'); + } + + /** + * This method persists the payload object to underlying database. + * NOTE: Rollback triggers with rejected promise. + * + * @param {Object} payload + * @returns {Promise} + */ + save(payload = {}) { + return db.transaction(trx => { + return db(this.getTable()).transacting(trx).insert(payload); + }); + } + + /** + * This method fetches rows from database provided offset and limit. + * + * @param {Object} params + * @returns {Promise} + */ + fetch(params) { + // Clamp the limit of the pagination to 100 exclusive + const limit = clamp(params.limit, 0, 100); + // Only positive offset allowed + const offset = Math.max(0, params.offset); + + return db(this.getTable()).limit(limit).offset(offset); + } +} + +export default Model; diff --git a/src/models/user.js b/src/models/user.js index d032e7d..3897d5b 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -1,24 +1,17 @@ -import bookshelf from '../db'; - -const TABLE_NAME = 'users'; +import Model from './model'; /** - * User model. + * User model representing user entity. */ -class User extends bookshelf.Model { - /** - * Get table name. - */ - get tableName() { - return TABLE_NAME; - } - +class User extends Model { /** - * Table has timestamps. + * Returns table name associated with User model. + * + * @returns {String} */ - get hasTimestamps() { - return true; + getTable() { + return 'users'; } } -export default User; +export default new User(); diff --git a/src/routes.js b/src/routes.js index 0e997d4..ff06474 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,30 +1,41 @@ import { Router } from 'express'; +import authenticateUser from './auth'; +import userRoutes from './routes/user'; import swaggerSpec from './utils/swagger'; -import userRoutes from './routes/userRoutes'; /** - * Contains all API routes for the application. + * Contains public API routes for the application. */ -const router = Router(); +const publicRouter = Router(); /** * GET /api/swagger.json */ -router.get('/swagger.json', (req, res) => { +publicRouter.get('/swagger.json', (_, res) => { res.json(swaggerSpec); }); /** * GET /api */ -router.get('/', (req, res) => { +publicRouter.get('/', (req, res) => { res.json({ app: req.app.locals.title, - apiVersion: req.app.locals.version + apiVersion: req.app.locals.version, }); }); -router.use('/users', userRoutes); +/** + * Contains secured API routes for the application. + */ +const privateRouter = Router(); + +/** + * Authentication middleware for private routes. + */ +privateRouter.use(authenticateUser); + +privateRouter.use('/users', userRoutes); -export default router; +export { publicRouter, privateRouter }; diff --git a/src/routes/user.js b/src/routes/user.js new file mode 100644 index 0000000..9df2e41 --- /dev/null +++ b/src/routes/user.js @@ -0,0 +1,17 @@ +import { Router } from 'express'; + +import * as userController from '../controllers/user'; + +const router = Router(); + +/** + * GET /api/users + */ +router.get('/', userController.fetch); + +/** + * POST /api/users + */ +router.post('/', userController.create); + +export default router; diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js deleted file mode 100644 index eb60806..0000000 --- a/src/routes/userRoutes.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from 'express'; - -import * as userController from '../controllers/users'; -import { findUser, userValidator } from '../validators/userValidator'; - -const router = Router(); - -/** - * GET /api/users - */ -router.get('/', userController.fetchAll); - -/** - * GET /api/users/:id - */ -router.get('/:id', userController.fetchById); - -/** - * POST /api/users - */ -router.post('/', userValidator, userController.create); - -/** - * PUT /api/users/:id - */ -router.put('/:id', findUser, userValidator, userController.update); - -/** - * DELETE /api/users/:id - */ -router.delete('/:id', findUser, userController.deleteUser); - -export default router; diff --git a/src/seeds/01_insert_users.js b/src/seeds/01_insert_users.js index ea49b1e..b9351aa 100644 --- a/src/seeds/01_insert_users.js +++ b/src/seeds/01_insert_users.js @@ -9,14 +9,10 @@ export function seed(knex) { .del() .then(() => { return knex('users').insert([ - { - name: 'Saugat Acharya', - updated_at: new Date() - }, { name: 'John Doe', - updated_at: new Date() - } + updated_at: new Date(), + }, ]); }); } diff --git a/src/services/user.js b/src/services/user.js new file mode 100644 index 0000000..b13882b --- /dev/null +++ b/src/services/user.js @@ -0,0 +1,32 @@ +import * as store from '@leapfrogtechnology/async-store'; + +import User from '../models/user'; +import logger from '../utils/logger'; + +/** + * Get all users. + * + * @returns {Promise} + */ +export function fetch() { + // Example: should retrieve from the query parameter + return User.fetch({ limit: 3, offset: 4 }); +} + +/** + * Create a new user. + * + * @returns {Promise} + */ +export async function create() { + // Example for retrieving the user from async-store in service. + const user = store.get('user'); + + const [id] = await User.save(user); + + logger.info(`User created: ${user}`); + + return { + id, + }; +} diff --git a/src/services/userService.js b/src/services/userService.js deleted file mode 100644 index 76440ec..0000000 --- a/src/services/userService.js +++ /dev/null @@ -1,58 +0,0 @@ -import Boom from '@hapi/boom'; - -import User from '../models/user'; - -/** - * Get all users. - * - * @returns {Promise} - */ -export function getAllUsers() { - return User.fetchAll(); -} - -/** - * Get a user. - * - * @param {Number|String} id - * @returns {Promise} - */ -export function getUser(id) { - return new User({ id }) - .fetch() - .then(user => user) - .catch(User.NotFoundError, () => { - throw Boom.notFound('User not found'); - }); -} - -/** - * Create new user. - * - * @param {Object} user - * @returns {Promise} - */ -export function createUser(user) { - return new User({ name: user.name }).save(); -} - -/** - * Update a user. - * - * @param {Number|String} id - * @param {Object} user - * @returns {Promise} - */ -export function updateUser(id, user) { - return new User({ id }).save({ name: user.name }); -} - -/** - * Delete a user. - * - * @param {Number|String} id - * @returns {Promise} - */ -export function deleteUser(id) { - return new User({ id }).fetch().then(user => user.destroy()); -} diff --git a/src/utils/buildError.js b/src/utils/buildError.js index 9a37bce..ecc3afc 100644 --- a/src/utils/buildError.js +++ b/src/utils/buildError.js @@ -1,4 +1,7 @@ import HttpStatus from 'http-status-codes'; +import * as store from '@leapfrogtechnology/async-store'; + +import TokenError from '../errors/token'; /** * Build error response for validation errors. @@ -7,9 +10,12 @@ import HttpStatus from 'http-status-codes'; * @returns {Object} */ function buildError(err) { + const requestID = store.getShortId(); + // Validation errors if (err.isJoi) { return { + id: requestID, code: HttpStatus.BAD_REQUEST, message: HttpStatus.getStatusText(HttpStatus.BAD_REQUEST), details: @@ -17,24 +23,43 @@ function buildError(err) { err.details.map(err => { return { message: err.message, - param: err.path.join('.') + param: err.path.join('.'), }; - }) + }), }; } // HTTP errors if (err.isBoom) { return { + id: requestID, code: err.output.statusCode, - message: err.output.payload.message || err.output.payload.error + message: err.output.payload.message || err.output.payload.error, + }; + } + + // Custom errors + if (err.isCustom) { + if (err instanceof TokenError) { + return { + id: requestID, + code: HttpStatus.UNAUTHORIZED, + message: HttpStatus.getStatusText(HttpStatus.UNAUTHORIZED), + }; + } + + return { + id: requestID, + code: HttpStatus.INTERNAL_SERVER_ERROR, + message: err.message || HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR), }; } // Return INTERNAL_SERVER_ERROR for all other cases return { + id: requestID, code: HttpStatus.INTERNAL_SERVER_ERROR, - message: HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR) + message: HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR), }; } diff --git a/src/utils/http.js b/src/utils/http.js new file mode 100644 index 0000000..1cd9956 --- /dev/null +++ b/src/utils/http.js @@ -0,0 +1,8 @@ +import axios from 'axios'; + +/** + * Axios Object + */ +const http = axios.create(); + +export { http }; diff --git a/src/utils/logger.js b/src/utils/logger.js index a64858e..c872fe3 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -1,5 +1,8 @@ import fs from 'fs'; +import path from 'path'; + import winston, { format } from 'winston'; +import * as store from '@leapfrogtechnology/async-store'; import 'winston-daily-rotate-file'; @@ -7,30 +10,78 @@ import 'winston-daily-rotate-file'; const LOG_DIR = process.env.LOG_DIR || 'logs'; const LOG_LEVEL = process.env.LOG_LEVEL || 'info'; +const isFileLogTransportEnabled = process.env.ENABLE_FILE_LOG_TRANSPORT === 'TRUE'; + // Create log directory if it does not exist if (!fs.existsSync(LOG_DIR)) { fs.mkdirSync(LOG_DIR); } +// logFormat used for console logging +const logFormat = format.printf(info => { + const formattedNamespace = info.metadata.namespace ? info.metadata.namespace : ''; + + // TODO: Will there be a situation when requestID would be empty string? + // May logs before middleware initialization? + const requestID = store.getShortId(); + const formattedReqID = requestID ? requestID : ''; + + return `${info.timestamp} [${info.level}] [${info.label}] [${formattedReqID}] [${formattedNamespace}]: ${info.message}`; +}); + /** * Create a new winston logger. */ const logger = winston.createLogger({ - transports: [ + format: format.combine( + format.label({ label: path.basename(process.mainModule.filename) }), + format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + // Format the metadata object + format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }), + logFormat + ), + transports: setupTransports(), +}); + +/** + * Creates a child logger with namespace for logging. + * + * @param {String} namespace + * @returns {Object} + */ +logger.withNamespace = function (namespace) { + return logger.child({ namespace }); +}; + +/** + * Setup transports for winston. + * + * @returns {Array} + */ +function setupTransports() { + const transports = []; + + transports.push( new winston.transports.Console({ - format: format.combine(format.colorize(), format.simple()), - level: 'info' - }), - new winston.transports.DailyRotateFile({ - format: format.combine(format.timestamp(), format.json()), - maxFiles: '14d', - level: LOG_LEVEL, - dirname: LOG_DIR, - datePattern: 'YYYY-MM-DD', - filename: '%DATE%-debug.log' + format: format.combine(format.colorize()), + level: 'info', }) - ] -}); + ); + if (isFileLogTransportEnabled) { + transports.push( + new winston.transports.DailyRotateFile({ + format: format.combine(format.timestamp(), format.json()), + maxFiles: process.env.LOG_RETENTION_PERIOD || '14d', + level: LOG_LEVEL, + dirname: LOG_DIR, + datePattern: 'YYYY-MM-DD', + filename: '%DATE%-debug.log', + }) + ); + } + + return transports; +} export const logStream = { /** @@ -40,7 +91,7 @@ export const logStream = { */ write(message) { logger.info(message.toString()); - } + }, }; export default logger; diff --git a/src/utils/math.js b/src/utils/math.js new file mode 100644 index 0000000..821ab4f --- /dev/null +++ b/src/utils/math.js @@ -0,0 +1,10 @@ +/** + * Clamps value between min and max. + * + * @param {Number} value + * @param {Number} min + * @param {Number} max + */ +export function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} diff --git a/src/utils/swagger.js b/src/utils/swagger.js index f44b025..9ca28d7 100644 --- a/src/utils/swagger.js +++ b/src/utils/swagger.js @@ -8,9 +8,9 @@ const swaggerDefinition = { info: { title: process.env.APP_NAME, version: process.env.APP_VERSION, - description: process.env.APP_DESCRIPTION + description: process.env.APP_DESCRIPTION, }, - basePath: '/api' + basePath: '/api', }; /** @@ -24,8 +24,8 @@ const swaggerOptions = { path.join(__dirname, '/../routes.js'), path.join(__dirname, '/../docs/*.js'), path.join(__dirname, '/../docs/*.yml'), - path.join(__dirname, '/../docs/*.yaml') - ] + path.join(__dirname, '/../docs/*.yaml'), + ], }; /** diff --git a/src/validators/userValidator.js b/src/validators/userValidator.js index 2295ffd..1457756 100644 --- a/src/validators/userValidator.js +++ b/src/validators/userValidator.js @@ -1,14 +1,10 @@ import Joi from '@hapi/joi'; import validate from '../utils/validate'; -import * as userService from '../services/userService'; // Validation schema const schema = Joi.object({ - name: Joi.string() - .label('Name') - .max(90) - .required() + name: Joi.string().label('Name').max(90).required(), }); /** @@ -22,22 +18,7 @@ const schema = Joi.object({ function userValidator(req, res, next) { return validate(req.body, schema) .then(() => next()) - .catch(err => next(err)); + .catch(next); } -/** - * Validate users existence. - * - * @param {Object} req - * @param {Object} res - * @param {Function} next - * @returns {Promise} - */ -function findUser(req, res, next) { - return userService - .getUser(req.params.id) - .then(() => next()) - .catch(err => next(err)); -} - -export { findUser, userValidator }; +export { userValidator }; diff --git a/test/api.test.js b/test/api.test.js index 3524d00..4893b2c 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -16,9 +16,7 @@ describe('Base API Test', () => { }); it('should return 405 method not allowed for random API hits', done => { - const randomString = Math.random() - .toString(36) - .substr(2, 5); + const randomString = Math.random().toString(36).substr(2, 5); request(app) .get(`/api/${randomString}`) diff --git a/test/controllers/users.test.js b/test/controllers/users.test.js index 30ad275..3fe8e3d 100644 --- a/test/controllers/users.test.js +++ b/test/controllers/users.test.js @@ -28,7 +28,7 @@ describe('Users Controller Test', () => { it('should not create a new user if name is not provided', done => { const user = { - noname: 'Jane Doe' + noname: 'Jane Doe', }; request(app) @@ -50,7 +50,7 @@ describe('Users Controller Test', () => { it('should create a new user with valid data', done => { const user = { - name: 'Jane Doe' + name: 'Jane Doe', }; request(app) @@ -104,7 +104,7 @@ describe('Users Controller Test', () => { it('should update a user if name is provided', done => { const user = { - name: 'John Doe' + name: 'John Doe', }; request(app) @@ -126,7 +126,7 @@ describe('Users Controller Test', () => { it('should not update a user if name is not provided', done => { const user = { - noname: 'John Doe' + noname: 'John Doe', }; request(app) diff --git a/yarn.lock b/yarn.lock index f59ed91..f3650f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -853,6 +853,15 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== +"@leapfrogtechnology/async-store@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@leapfrogtechnology/async-store/-/async-store-1.2.0.tgz#a9162b4075159ed1452e5d48fa21049c508c20ac" + integrity sha512-u3LNseCs21d8w21OHrY6CY4FRIkeupgJQk65sYaZCO0GtNXBCz/YoS0qqWKnLtUrDWkIYiYusskO06XMg1UbiQ== + dependencies: + debug "4.1.1" + ramda "0.26.1" + uuid "3.3.2" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -1197,6 +1206,13 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +axios@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" + integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== + dependencies: + follow-redirects "1.5.10" + babel-plugin-dynamic-import-node@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" @@ -1229,6 +1245,11 @@ basic-auth@~2.0.1: dependencies: safe-buffer "5.1.2" +bignumber.js@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.0.tgz#805880f84a329b5eac6e7cb6f8274b6d82bdf075" + integrity sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A== + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -1246,11 +1267,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1267,23 +1283,6 @@ body-parser@1.19.0, body-parser@^1.19.0: raw-body "2.4.0" type-is "~1.6.17" -bookshelf-virtuals-plugin@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/bookshelf-virtuals-plugin/-/bookshelf-virtuals-plugin-0.1.1.tgz#cdd8c1609a4558a581ee684b5eadcf295afcf8ac" - integrity sha512-MFFjtzLoyWaGD7eMT2UZ8H00CHNk7M5WuSTxi+82gLJqbq0TyQdSQsKS5/NHfQjqd3wZKEV+yEbzpck2oBGe2A== - dependencies: - lodash "^4.17.15" - -bookshelf@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/bookshelf/-/bookshelf-1.1.0.tgz#d456abd70ed4af4c9ae6f3d364424590ea31dcbf" - integrity sha512-a4rrDI5pnjnj7xFAT23FJQyVxlrrvRPGUNRNj1upuBC5al7ObvoelTZbmVl9bnj+BZE5x19Y07+p933fCsBHNQ== - dependencies: - bluebird "^3.7.2" - create-error "~0.3.1" - inflection "^1.12.0" - lodash "^4.17.15" - bowser@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" @@ -1353,11 +1352,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -1866,11 +1860,6 @@ create-error-class@^3.0.0: dependencies: capture-stack-trace "^1.0.0" -create-error@~0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23" - integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM= - cross-env@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" @@ -1943,6 +1932,13 @@ debug@4, debug@4.1.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2662,6 +2658,13 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3168,11 +3171,6 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -inflection@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" - integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY= - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4134,6 +4132,16 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mysql@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/mysql/-/mysql-2.18.1.tgz#2254143855c5a8c73825e4522baf2ea021766717" + integrity sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig== + dependencies: + bignumber.js "9.0.0" + readable-stream "2.3.7" + safe-buffer "5.1.2" + sqlstring "2.3.1" + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -4553,11 +4561,6 @@ package-json@^4.0.0: registry-url "^3.0.3" semver "^5.1.0" -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -4666,63 +4669,11 @@ pathval@^1.1.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= -pg-connection-string@0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-0.1.3.tgz#da1847b20940e42ee1492beaf65d49d91b245df7" - integrity sha1-2hhHsglA5C7hSSvq9l1J2RskXfc= - pg-connection-string@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.1.0.tgz#e07258f280476540b24818ebb5dca29e101ca502" integrity sha512-bhlV7Eq09JrRIvo1eKngpwuqKtJnNhZdpdOlvrPrA4dxqXPjxSrbNrfnIDmTpwMyRszrcV4kU5ZA4mMsQUrjdg== -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-packet-stream@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pg-packet-stream/-/pg-packet-stream-1.1.0.tgz#e45c3ae678b901a2873af1e17b92d787962ef914" - integrity sha512-kRBH0tDIW/8lfnnOyTwKD23ygJ/kexQVXZs7gEyBljw4FYqimZFxnMMx50ndZ8In77QgfGuItS5LLclC2TtjYg== - -pg-pool@^2.0.10: - version "2.0.10" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-2.0.10.tgz#842ee23b04e86824ce9d786430f8365082d81c4a" - integrity sha512-qdwzY92bHf3nwzIUcj+zJ0Qo5lpG/YxchahxIN8+ZVmXqkahKXsnl2aiJPHLYN9o5mB/leG+Xh6XKxtP7e0sjg== - -pg-types@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - pg-int8 "1.0.1" - postgres-array "~2.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.4" - postgres-interval "^1.1.0" - -pg@^7.18.2: - version "7.18.2" - resolved "https://registry.yarnpkg.com/pg/-/pg-7.18.2.tgz#4e219f05a00aff4db6aab1ba02f28ffa4513b0bb" - integrity sha512-Mvt0dGYMwvEADNKy5PMQGlzPudKcKKzJds/VbOeZJpb6f/pI3mmoXX0JksPgI3l3JPP/2Apq7F36O63J7mgveA== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "0.1.3" - pg-packet-stream "^1.1.0" - pg-pool "^2.0.10" - pg-types "^2.1.0" - pgpass "1.x" - semver "4.3.2" - -pgpass@1.x: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.2.tgz#2a7bb41b6065b67907e91da1b07c1847c877b306" - integrity sha1-Knu0G2BltnkH6R2hsHwYR8h3swY= - dependencies: - split "^1.0.0" - picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.0.7: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -4778,28 +4729,6 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" - integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= - -postgres-date@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.4.tgz#1c2728d62ef1bff49abdd35c1f86d4bdf118a728" - integrity sha512-bESRvKVuTrjoBluEcpv2346+6kgB7UlnqWZsnbnCccTNq/pqfj1j6oBaN5+b/NrDXepYUT/HKadqv3iS9lJuVA== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -4887,6 +4816,11 @@ qs@^6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== +ramda@0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" + integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -4917,7 +4851,7 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -readable-stream@^2.0.2, readable-stream@^2.3.5, readable-stream@^2.3.6: +readable-stream@2.3.7, readable-stream@^2.0.2, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -5214,11 +5148,6 @@ semver-regex@^2.0.0: resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== -semver@4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7" - integrity sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c= - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" @@ -5445,18 +5374,16 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" - integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sqlstring@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.1.tgz#475393ff9e91479aea62dcaf0ca3d14983a7fb40" + integrity sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A= + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -5772,7 +5699,7 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@2, through@^2.3.6: +through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -6020,6 +5947,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -6202,11 +6134,6 @@ xregexp@^4.3.0: dependencies: "@babel/runtime-corejs3" "^7.8.3" -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"