diff --git a/package-lock.json b/package-lock.json index 8cf4a443..c518c2db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -266,6 +266,15 @@ "@types/node": "9.4.7" } }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "9.4.7" + } + }, "JSONStream": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz", @@ -7640,6 +7649,11 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" + }, "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", diff --git a/package.json b/package.json index 6cb78e5a..9812f394 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ }, "dependencies": { "@js-entity-repos/core": "^6.0.2", - "http-status-codes": "^1.3.0" + "http-status-codes": "^1.3.0", + "uuid": "^3.2.1" }, "devDependencies": { "@ht2-labs/semantic-release": "1.0.31", @@ -37,6 +38,7 @@ "@types/express": "4.11.1", "@types/mocha": "2.2.48", "@types/source-map-support": "0.4.0", + "@types/uuid": "3.4.3", "assert-rejects": "0.1.1", "axios": "0.18.0", "dotenv": "5.0.1", diff --git a/readme.md b/readme.md index 434d2a95..2550043c 100644 --- a/readme.md +++ b/readme.md @@ -33,12 +33,17 @@ const todosFacade = factory({ }, // Optional property. defaultPaginationLimit: 10, - // Optional property that catches errors from handlers. - errorCatcher: (handler) => (req, res) => { - handler(req, res).catch((err) => { - res.status(500).send(); - }); - }, + // Optional property to handle transactions. + handleTransaction: async ({ req, res }, handler) => { + // The transactionId allow items found in logs to be matched with responses to users. + const transactionId = uuid(); + try { + await handler({ transactionId }); + } catch (err) { + console.error({ err, req, res, transactionId}) + res.status(500).send(transactionId); + } + }; service, }); ``` diff --git a/src/FacadeConfig.ts b/src/FacadeConfig.ts index e1f7d41e..892d90be 100644 --- a/src/FacadeConfig.ts +++ b/src/FacadeConfig.ts @@ -1,11 +1,11 @@ import Facade from '@js-entity-repos/core/dist/Facade'; import Entity from '@js-entity-repos/core/dist/types/Entity'; import Filter from '@js-entity-repos/core/dist/types/Filter'; -import ErrorCatcher from './utils/ErrorCatcher'; +import TransactionHandler from './utils/TransactionHandler'; export default interface FacadeConfig { readonly constructFilter: (filter: Filter) => any; readonly service: Facade; - readonly errorCatcher: ErrorCatcher; + readonly handleTransaction: TransactionHandler; readonly defaultPaginationLimit: number; } diff --git a/src/FactoryConfig.ts b/src/FactoryConfig.ts index f2dfb569..2aae9cc6 100644 --- a/src/FactoryConfig.ts +++ b/src/FactoryConfig.ts @@ -1,11 +1,11 @@ import Facade from '@js-entity-repos/core/dist/Facade'; import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Filter } from '@js-entity-repos/core/dist/types/Filter'; -import ErrorCatcher from './utils/ErrorCatcher'; +import TransactionHandler from './utils/TransactionHandler'; export default interface FactoryConfig { readonly constructFilter?: (filter: Filter) => any; readonly service: Facade; - readonly errorCatcher?: ErrorCatcher; + readonly handleTransaction?: TransactionHandler; readonly defaultPaginationLimit?: number; } diff --git a/src/factory.ts b/src/factory.ts index 7b397947..5f867a0b 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -11,13 +11,13 @@ import patchEntities from './functions/patchEntities'; import removeEntities from './functions/removeEntities'; import removeEntity from './functions/removeEntity'; import replaceEntity from './functions/replaceEntity'; -import catchErrors from './utils/catchErrors'; +import handleTransaction from './utils/handleTransaction'; export default (factoryConfig: FactoryConfig): Router => { const facadeConfig: FacadeConfig = { constructFilter: (filter) => filter, defaultPaginationLimit: 10, - errorCatcher: catchErrors, + handleTransaction, ...factoryConfig, }; const router = Router(); diff --git a/src/functions/countEntities.ts b/src/functions/countEntities.ts index f75e697e..5500fe6f 100644 --- a/src/functions/countEntities.ts +++ b/src/functions/countEntities.ts @@ -2,14 +2,15 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const { count } = await config.service.countEntities({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const { count } = await config.service.countEntities({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + }); + res.status(OK).json(count); }); - res.status(OK).json(count); - }); + }; }; diff --git a/src/functions/createEntity.ts b/src/functions/createEntity.ts index 61d0efa3..f57eece1 100644 --- a/src/functions/createEntity.ts +++ b/src/functions/createEntity.ts @@ -2,14 +2,15 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const { entity } = await config.service.createEntity({ - entity: req.body, - id: req.body.id, + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const { entity } = await config.service.createEntity({ + entity: req.body, + id: req.body.id, + }); + res.status(OK).json(entity); }); - res.status(OK).json(entity); - }); + }; }; diff --git a/src/functions/getEntities.ts b/src/functions/getEntities.ts index d40202e3..98718cd7 100644 --- a/src/functions/getEntities.ts +++ b/src/functions/getEntities.ts @@ -2,29 +2,30 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; import getNumberQueryParam from '../utils/getNumberQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const limit = getNumberQueryParam(req.query, 'limit', config.defaultPaginationLimit); - const result = await config.service.getEntities({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), - pagination: { - cursor: req.query.cursor, - forward: req.query.forward === 'true', - limit, - }, - sort: getJsonQueryParam(req.query, 'sort'), + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const limit = getNumberQueryParam(req.query, 'limit', config.defaultPaginationLimit); + const result = await config.service.getEntities({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + pagination: { + cursor: req.query.cursor, + forward: req.query.forward === 'true', + limit, + }, + sort: getJsonQueryParam(req.query, 'sort'), + }); + res.status(OK); + if (result.nextCursor !== undefined) { + res.setHeader('x-entities-next-cursor', result.nextCursor); + } + if (result.previousCursor !== undefined) { + res.setHeader('x-entities-previous-cursor', result.previousCursor); + } + res.json(result.entities); }); - res.status(OK); - if (result.nextCursor !== undefined) { - res.setHeader('x-entities-next-cursor', result.nextCursor); - } - if (result.previousCursor !== undefined) { - res.setHeader('x-entities-previous-cursor', result.previousCursor); - } - res.json(result.entities); - }); + }; }; diff --git a/src/functions/getEntity.ts b/src/functions/getEntity.ts index f1aed8a8..0cc13324 100644 --- a/src/functions/getEntity.ts +++ b/src/functions/getEntity.ts @@ -2,15 +2,16 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const { entity } = await config.service.getEntity({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), - id: req.params.id, + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const { entity } = await config.service.getEntity({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + id: req.params.id, + }); + res.status(OK).json(entity); }); - res.status(OK).json(entity); - }); + }; }; diff --git a/src/functions/patchEntities.ts b/src/functions/patchEntities.ts index e11e40dd..f69a53a2 100644 --- a/src/functions/patchEntities.ts +++ b/src/functions/patchEntities.ts @@ -2,16 +2,17 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const { entity } = await config.service.patchEntity({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), - id: req.params.id, - patch: req.body, + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const { entity } = await config.service.patchEntity({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + id: req.params.id, + patch: req.body, + }); + res.status(OK).json(entity); }); - res.status(OK).json(entity); - }); + }; }; diff --git a/src/functions/removeEntities.ts b/src/functions/removeEntities.ts index 7a0952ae..8847191c 100644 --- a/src/functions/removeEntities.ts +++ b/src/functions/removeEntities.ts @@ -2,14 +2,15 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { NO_CONTENT } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - await config.service.removeEntities({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + await config.service.removeEntities({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + }); + res.status(NO_CONTENT).send(); }); - res.status(NO_CONTENT).send(); - }); + }; }; diff --git a/src/functions/removeEntity.ts b/src/functions/removeEntity.ts index c9fb7b76..e41343d2 100644 --- a/src/functions/removeEntity.ts +++ b/src/functions/removeEntity.ts @@ -2,15 +2,16 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { NO_CONTENT } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - await config.service.removeEntity({ - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), - id: req.params.id, + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + await config.service.removeEntity({ + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + id: req.params.id, + }); + res.status(NO_CONTENT).send(); }); - res.status(NO_CONTENT).send(); - }); + }; }; diff --git a/src/functions/replaceEntity.ts b/src/functions/replaceEntity.ts index f7d5ee38..1c29ea50 100644 --- a/src/functions/replaceEntity.ts +++ b/src/functions/replaceEntity.ts @@ -2,16 +2,17 @@ import Entity from '@js-entity-repos/core/dist/types/Entity'; import { Request, Response } from 'express'; import { OK } from 'http-status-codes'; import FacadeConfig from '../FacadeConfig'; -import catchErrors from '../utils/catchErrors'; import getJsonQueryParam from '../utils/getJsonQueryParam'; export default (config: FacadeConfig) => { - return catchErrors(async (req: Request, res: Response) => { - const { entity } = await config.service.replaceEntity({ - entity: req.body, - filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), - id: req.params.id, + return async (req: Request, res: Response) => { + await config.handleTransaction({ req, res }, async () => { + const { entity } = await config.service.replaceEntity({ + entity: req.body, + filter: config.constructFilter(getJsonQueryParam(req.query, 'filter')), + id: req.params.id, + }); + res.status(OK).json(entity); }); - res.status(OK).json(entity); - }); + }; }; diff --git a/src/utils/ErrorCatcher.ts b/src/utils/ErrorCatcher.ts deleted file mode 100644 index f0fa55a5..00000000 --- a/src/utils/ErrorCatcher.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Request, Response } from 'express'; - -export type Handler = (req: Request, res: Response) => Promise; - -type ErrorCatcher = (handler: Handler) => (req: Request, res: Response) => void; - -export default ErrorCatcher; diff --git a/src/utils/ErrorHandler.ts b/src/utils/ErrorHandler.ts new file mode 100644 index 00000000..07819a55 --- /dev/null +++ b/src/utils/ErrorHandler.ts @@ -0,0 +1,12 @@ +import { Request, Response } from 'express'; + +export interface Opts { + readonly req: Request; + readonly res: Response; + readonly err: any; + readonly transactionId: string; +} + +type ErrorHandler = (opts: Opts) => void; + +export default ErrorHandler; diff --git a/src/utils/TransactionHandler.ts b/src/utils/TransactionHandler.ts new file mode 100644 index 00000000..d82800a8 --- /dev/null +++ b/src/utils/TransactionHandler.ts @@ -0,0 +1,17 @@ +import { Response } from 'express'; +import { Request } from 'express-serve-static-core'; + +export interface Opts { + readonly req: Request; + readonly res: Response; +} + +export interface HandlerOpts { + readonly transactionId: string; +} + +export type Handler = (opts: HandlerOpts) => Promise; + +type TransactionHandler = (opts: Opts, handler: Handler) => Promise; + +export default TransactionHandler; diff --git a/src/utils/catchErrors.ts b/src/utils/catchErrors.ts deleted file mode 100644 index c15dd6e9..00000000 --- a/src/utils/catchErrors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import ConflictingEntityError from '@js-entity-repos/core/dist/errors/ConflictingEntityError'; -import MissingEntityError from '@js-entity-repos/core/dist/errors/MissingEntityError'; -import { BAD_REQUEST, CONFLICT, INTERNAL_SERVER_ERROR, NOT_FOUND } from 'http-status-codes'; -import ErrorCatcher from './ErrorCatcher'; -import JsonError from './JsonError'; -import NumberError from './NumberError'; - -const errorCatcher: ErrorCatcher = (handler) => { - return (req, res) => { - handler(req, res).catch((err) => { - if (err instanceof ConflictingEntityError) { - return res.status(CONFLICT).send(); - } - if (err instanceof MissingEntityError) { - return res.status(NOT_FOUND).send(); - } - if (err instanceof JsonError) { - return res.status(BAD_REQUEST).send(); - } - if (err instanceof NumberError) { - return res.status(BAD_REQUEST).send(); - } - /* istanbul ignore next */ - return res.status(INTERNAL_SERVER_ERROR).send(); - }); - }; -}; - -export default errorCatcher; diff --git a/src/utils/handleError.ts b/src/utils/handleError.ts new file mode 100644 index 00000000..419043e2 --- /dev/null +++ b/src/utils/handleError.ts @@ -0,0 +1,51 @@ +import ConflictingEntityError from '@js-entity-repos/core/dist/errors/ConflictingEntityError'; +import MissingEntityError from '@js-entity-repos/core/dist/errors/MissingEntityError'; +import { BAD_REQUEST, CONFLICT, INTERNAL_SERVER_ERROR, NOT_FOUND } from 'http-status-codes'; +import ErrorHandler from './ErrorHandler'; +import JsonError from './JsonError'; +import NumberError from './NumberError'; + +const handleError: ErrorHandler = ({ res, err, transactionId }) => { + const sendErrorResponse = (statusCode: number, errorData: object) => { + const body = { + ...errorData, + transactionId, + }; + res.status(statusCode).json(body); + }; + if (err instanceof ConflictingEntityError) { + sendErrorResponse(CONFLICT, { + entityId: err.entityId, + entityName: err.entityName, + }); + return; + } + if (err instanceof MissingEntityError) { + sendErrorResponse(NOT_FOUND, { + entityId: err.entityId, + entityName: err.entityName, + }); + return; + } + if (err instanceof JsonError) { + sendErrorResponse(BAD_REQUEST, { + data: err.data, + path: err.path, + }); + return; + } + if (err instanceof NumberError) { + sendErrorResponse(BAD_REQUEST, { + data: err.data, + path: err.path, + }); + return; + } + /* istanbul ignore next */ + { + sendErrorResponse(INTERNAL_SERVER_ERROR, {}); + return; + } +}; + +export default handleError; diff --git a/src/utils/handleTransaction.ts b/src/utils/handleTransaction.ts new file mode 100644 index 00000000..43137ce5 --- /dev/null +++ b/src/utils/handleTransaction.ts @@ -0,0 +1,14 @@ +import { v4 as uuid } from 'uuid'; +import handleError from './handleError'; +import TransactionHandler from './TransactionHandler'; + +const handleTransaction: TransactionHandler = async ({ req, res }, handler) => { + const transactionId = uuid(); + try { + await handler({ transactionId }); + } catch (err) { + handleError({ req, res, err, transactionId }); + } +}; + +export default handleTransaction;