diff --git a/README.md b/README.md index f27e7e4..8c2c87c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ [![Coverage Status](https://coveralls.io/repos/github/SpartanLabs/mindless/badge.svg?branch=master)](https://coveralls.io/github/SpartanLabs/mindless?branch=master) # mindless + +This documentation is out of date. + ### A Library for creating APIs with TypeScript. Mindless allows developers to write typical controller-styled apis with models using TypeScript. A great use of the mindless framework is with applications built using the [serverless framework](https://serverless.com/). In a typical serverless application, each route goes to its own function. Using mindless allows the developer to flip the script on this paradigm. Using the lightweight routing mechanism, developers can use routes to point to controllers based on path. Mindless also enables parameter injection and general dependency injection in controllers. Mindless will also have extensions such as permissions and data access that will further empower developers. diff --git a/index.ts b/index.ts index 6b312bf..736ffa5 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,6 @@ -export {App, IContainer, IApp} from "./src/app"; -export { Router, Route, MindlessRoute, RouteUrl, IRouteUrl, IRouter } from './src/routing'; -export { Context } from './src/interfaces'; -export { Request, Event, HttpMethods } from './src/request'; +export * from "./src/app"; +export * from './src/routing'; +export * from './src/request'; export { Response } from './src/response'; export { Middleware } from './src/middleware/middleware'; export { Controller } from './src/controller/controller'; diff --git a/package-lock.json b/package-lock.json index 68527dd..7d3018a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -342,41 +342,44 @@ "dev": true }, "@types/events": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, "@types/fs-extra": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.1.tgz", - "integrity": "sha512-h3wnflb+jMTipvbbZnClgA2BexrT4w0GcfoCz5qyxd0IRsbqhLSyesM6mqZTAnhbVmhyTm5tuxfRu9R+8l+lGw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.1.0.tgz", + "integrity": "sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ==", "dev": true, "requires": { "@types/node": "9.6.24" } }, "@types/glob": { - "version": "5.0.35", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-5.0.35.tgz", - "integrity": "sha512-wc+VveszMLyMWFvXLkloixT4n0harUIVZjnpzztaZ0nKLuul7Z32iMt2fUFGAaZ4y1XWjFRMtCI5ewvyh4aIeg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", + "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", "dev": true, "requires": { - "@types/events": "1.2.0", + "@types/events": "3.0.0", "@types/minimatch": "3.0.3", "@types/node": "9.6.24" } }, "@types/handlebars": { - "version": "4.0.36", - "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.0.36.tgz", - "integrity": "sha512-LjNiTX7TY7wtuC6y3QwC93hKMuqYhgV9A1uXBKNvZtVC8ZvyWAjZkJ5BvT0K7RKqORRYRLMrqCxpw5RgS+MdrQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.1.0.tgz", + "integrity": "sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==", + "dev": true, + "requires": { + "handlebars": "4.0.11" + } }, "@types/highlight.js": { - "version": "9.12.2", - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.2.tgz", - "integrity": "sha512-y5x0XD/WXDaGSyiTaTcKS4FurULJtSiYbGTeQd0m2LYZGBcZZ/7fM6t5H/DzeUF+kv8y6UfmF6yJABQsHcp9VQ==", + "version": "9.12.3", + "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz", + "integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==", "dev": true }, "@types/jest": { @@ -392,9 +395,9 @@ "dev": true }, "@types/marked": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", - "integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.4.2.tgz", + "integrity": "sha512-cDB930/7MbzaGF6U3IwSQp6XBru8xWajF5PV2YZZeV8DyiliTuld11afVztGI9+yJZ29il5E+NpGA6ooV/Cjkg==", "dev": true }, "@types/minimatch": { @@ -416,25 +419,15 @@ "dev": true }, "@types/shelljs": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.7.8.tgz", - "integrity": "sha512-M2giRw93PxKS7YjU6GZjtdV9HASdB7TWqizBXe4Ju7AqbKlWvTr0gNO92XH56D/gMxqD/jNHLNfC5hA34yGqrQ==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-bZgjwIWu9gHCjirKJoOlLzGi5N0QgZ5t7EXEuoqyWCHTuSddURXo3FOBYDyRPNOWzZ6NbkLvZnVkn483Y/tvcQ==", "dev": true, "requires": { - "@types/glob": "5.0.35", + "@types/glob": "7.1.1", "@types/node": "9.6.24" } }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", - "dev": true, - "requires": { - "jsonparse": "1.3.1", - "through": "2.3.8" - } - }, "abab": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", @@ -2568,8 +2561,8 @@ "integrity": "sha512-GWh71U26BLWgMykCp+VghZ4s64wVbtseECcKQ/PvcPZR2cUnz+FUc2J9KjxNl7/ZbCxST8R03c9fc+Vi0umS9Q==", "dev": true, "requires": { - "JSONStream": "1.3.3", "is-text-path": "1.0.1", + "JSONStream": "1.3.3", "lodash": "4.17.5", "meow": "4.0.1", "split2": "2.2.0", @@ -4399,23 +4392,23 @@ "dev": true, "optional": true }, - "string-width": { - "version": "1.0.2", + "string_decoder": { + "version": "1.1.1", "bundled": true, "dev": true, + "optional": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "safe-buffer": "5.1.1" } }, - "string_decoder": { - "version": "1.1.1", + "string-width": { + "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { - "safe-buffer": "5.1.1" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, "strip-ansi": { @@ -4875,9 +4868,9 @@ } }, "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=", + "version": "9.15.8", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.8.tgz", + "integrity": "sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA==", "dev": true }, "home-or-tmp": { @@ -6520,6 +6513,16 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "JSONStream": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", + "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", + "dev": true, + "requires": { + "jsonparse": "1.3.1", + "through": "2.3.8" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -8618,9 +8621,9 @@ "dev": true }, "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "prompt": { @@ -10503,6 +10506,15 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, "string-argv": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz", @@ -10547,15 +10559,6 @@ "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, "stringify-object": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz", @@ -11570,40 +11573,34 @@ "dev": true }, "typedoc": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.11.1.tgz", - "integrity": "sha512-jdNIoHm5wkZqxQTe/g9AQ3LKnZyrzHXqu6A/c9GUOeJyBWLxNr7/Dm3rwFvLksuxRNwTvY/0HRDU9sJTa9WQSg==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.14.2.tgz", + "integrity": "sha512-aEbgJXV8/KqaVhcedT7xG6d2r+mOvB5ep3eIz1KuB5sc4fDYXcepEEMdU7XSqLFO5hVPu0nllHi1QxX2h/QlpQ==", "dev": true, "requires": { - "@types/fs-extra": "5.0.1", - "@types/handlebars": "4.0.36", - "@types/highlight.js": "9.12.2", - "@types/lodash": "4.14.104", - "@types/marked": "0.3.0", + "@types/fs-extra": "5.1.0", + "@types/handlebars": "4.1.0", + "@types/highlight.js": "9.12.3", + "@types/lodash": "4.14.115", + "@types/marked": "0.4.2", "@types/minimatch": "3.0.3", - "@types/shelljs": "0.7.8", - "fs-extra": "5.0.0", + "@types/shelljs": "0.8.5", + "fs-extra": "7.0.1", "handlebars": "4.0.11", - "highlight.js": "9.12.0", - "lodash": "4.17.5", - "marked": "0.3.19", + "highlight.js": "9.15.8", + "lodash": "4.17.15", + "marked": "0.4.0", "minimatch": "3.0.4", - "progress": "2.0.0", - "shelljs": "0.8.2", + "progress": "2.0.3", + "shelljs": "0.8.3", "typedoc-default-themes": "0.5.0", - "typescript": "2.7.2" + "typescript": "3.2.4" }, "dependencies": { - "@types/lodash": { - "version": "4.14.104", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.104.tgz", - "integrity": "sha512-ufQcVg4daO8xQ5kopxRHanqFdL4AI7ondQkV+2f+7mz3gvp0LkBx2zBRC6hfs3T87mzQFmf5Fck7Fi145Ul6NQ==", - "dev": true - }, "fs-extra": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", - "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "requires": { "graceful-fs": "4.1.11", @@ -11620,16 +11617,16 @@ "graceful-fs": "4.1.11" } }, - "marked": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", - "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "shelljs": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.2.tgz", - "integrity": "sha512-pRXeNrCA2Wd9itwhvLp5LZQvPJ0wU6bcjaTMywHHGX5XWhVN2nzSu7WV0q+oUY7mGK3mgSkDDzP3MgjqdyIgbQ==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", + "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", "dev": true, "requires": { "glob": "7.1.1", @@ -11638,9 +11635,9 @@ } }, "typescript": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", - "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", + "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", "dev": true } } @@ -11663,9 +11660,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index f235794..83cc3dd 100644 --- a/package.json +++ b/package.json @@ -99,16 +99,16 @@ "rollup-plugin-sourcemaps": "^0.4.2", "rollup-plugin-typescript2": "^0.11.1", "semantic-release": "^15.4.1", + "travis-deploy-once": "^5.0.0", "ts-jest": "^22.0.0", "ts-node": "^6.0.0", "tslint": "^5.8.0", "tslint-config-prettier": "^1.1.0", "tslint-config-standard": "^7.0.0", - "typedoc": "^0.11.0", + "typedoc": "^0.14.2", "typemoq": "^2.1.0", - "typescript": "^2.6.2", - "validate-commit-msg": "^2.12.2", - "travis-deploy-once": "^5.0.0" + "typescript": "^3.5.3", + "validate-commit-msg": "^2.12.2" }, "dependencies": { "route-parser": "0.0.5" diff --git a/src/app/IApp.ts b/src/app/IApp.ts index 8facc95..61736f8 100644 --- a/src/app/IApp.ts +++ b/src/app/IApp.ts @@ -1,8 +1,8 @@ -import { Request } from '../request' import { GenericConstructor } from '../interfaces' import { Response } from '../response' +import { RequestEvent } from '../request' export interface IApp { resolve(constructor: GenericConstructor): T - handleRequest(request: Request): Promise + handleRequest(request: RequestEvent): Promise } diff --git a/src/app/app.ts b/src/app/app.ts index feb948f..c23fc40 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,9 +1,12 @@ import { CustomErrorHandler } from '../error/custom-error-handler' import { DefaultErrorHandler } from '../error/default-error-handler' import { GenericConstructor } from '../interfaces' -import { Request } from '../request' +import { Request, RequestEvent } from '../request' +import { DefaultBodyDeserializer } from '../request/default-body-deserializer' +import { RequestBodyDeserializer } from '../request/request-body-deserializer' import { Response } from '../response' import { IRouter } from '../routing' +import { RouteData } from '../routing/IRouter' import { Dispatcher } from './dispatcher' import { IApp } from './IApp' import { IContainer } from './IContainer' @@ -12,18 +15,32 @@ export class App implements IApp { constructor( protected container: IContainer, protected router: IRouter, - protected errorHandler: CustomErrorHandler = DefaultErrorHandler + protected errorHandler: CustomErrorHandler = DefaultErrorHandler, + protected requestBodyDeserializer: RequestBodyDeserializer = new DefaultBodyDeserializer() ) {} resolve(constructor: GenericConstructor): T { return this.container.resolve(constructor) } - async handleRequest(request: Request): Promise { + async handleRequest(event: RequestEvent): Promise { + const [data, error] = this.getRouteData(event) + + if (error !== undefined) { + return this.errorHandler(error, event) + } + // If error is undefined then data must be defined + + const request = this.createRequest(event, data!) + try { - const data = this.router.getRouteData(request) - await Dispatcher.dispatchMiddleware(this.container, request, data.route.middleware || []) - return await Dispatcher.dispatchController(this.container, request, data.route, data.params) + await Dispatcher.dispatchMiddleware(this.container, request, data!.route.middleware || []) + return await Dispatcher.dispatchController( + this.container, + request, + data!.route, + data!.methodParameters + ) } catch (e) { // Allow middleware and controller to reject with response object if (e instanceof Response) { @@ -32,4 +49,25 @@ export class App implements IApp { return this.errorHandler(e, request) } } + + protected getRouteData(event: RequestEvent): [RouteData, undefined] | [undefined, Error] { + try { + return [this.router.getRouteData(event), undefined] + } catch (e) { + return [undefined, e] + } + } + + protected createRequest(event: RequestEvent, data: RouteData): Request { + const bodyObj = this.requestBodyDeserializer.deserialize(event, data.metadata) + + return new Request( + event.path, + bodyObj, + data!.metadata, + data!.pathParameters, + event.queryStringParameters, + event.headers + ) + } } diff --git a/src/app/dispatcher.ts b/src/app/dispatcher.ts index a56d14d..88de5ff 100644 --- a/src/app/dispatcher.ts +++ b/src/app/dispatcher.ts @@ -38,13 +38,15 @@ export class Dispatcher { return request } - try { - return request.getOrFail(param) - } catch (e) { - throw new MindlessError( - `Unable to inject ${param} into ${route.controller.name} ${route.function}` - ) + const value = request.getPathParameter(param) || request.getQueryStringParameter(param) + + if (value !== undefined) { + return value } + + throw new MindlessError( + `Unable to inject ${param} into ${route.controller.name} ${route.function}` + ) } let subjectController: Controller diff --git a/src/app/index.ts b/src/app/index.ts index 99048db..6cb0134 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -1,4 +1,3 @@ -export { Dispatcher } from './dispatcher' export { IContainer } from './IContainer' export { IApp } from './IApp' export { App } from './app' diff --git a/src/error/custom-error-handler.ts b/src/error/custom-error-handler.ts index 98ef88d..3bbcb7b 100644 --- a/src/error/custom-error-handler.ts +++ b/src/error/custom-error-handler.ts @@ -1,4 +1,4 @@ -import { Request } from '../request' +import { Request, RequestEvent } from '../request' import { Response } from '../response' -export type CustomErrorHandler = (e: Error, r: Request) => Response +export type CustomErrorHandler = (e: Error, r: Request | RequestEvent) => Response diff --git a/src/error/default-error-handler.ts b/src/error/default-error-handler.ts index d29d031..c98168a 100644 --- a/src/error/default-error-handler.ts +++ b/src/error/default-error-handler.ts @@ -1,9 +1,12 @@ -import { Request } from '../request' +import { Request, RequestEvent } from '../request' import { Response } from '../response' import { CustomErrorHandler } from './custom-error-handler' -export const DefaultErrorHandler: CustomErrorHandler = (e: Error, request: Request): Response => { - console.error(e) +export const DefaultErrorHandler: CustomErrorHandler = ( + error: Error, + request: Request | RequestEvent +): Response => { + console.error({ request, error }) return new Response(500, { message: 'An error occurred, using default error handler (see docs to supply your own). Please check your logs' diff --git a/src/interfaces/context.ts b/src/interfaces/context.ts deleted file mode 100644 index c154e3a..0000000 --- a/src/interfaces/context.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface Context { - functionName: string - functionVersion: string - invokedFunctionArn: string - awsRequestId: string - logGroupName: string - logStreamName: string - identity: any // idk? - clientContext: any // idk? -} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index bda1d23..bc3a03b 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -1,2 +1 @@ -export { Context } from './context' export { GenericConstructor } from './generic' diff --git a/src/mindless.ts b/src/mindless.ts index 3d8a85d..b16d8cd 100644 --- a/src/mindless.ts +++ b/src/mindless.ts @@ -1,12 +1,8 @@ -// Import here Polyfills if needed. Recommended core-js (npm i -D core-js) -// import "core-js/fn/array.find" - export { MindlessError } from './error/mindless.error' export { CustomErrorHandler } from './error/custom-error-handler' export { App, IContainer, IApp } from './app' export { Router, Route, MindlessRoute, RouteUrl, IRouteUrl, IRouter } from './routing' -export { Context } from './interfaces' -export { Request, Event, HttpMethods } from './request' +export { Request, HttpMethods, RequestEvent } from './request' export { Response } from './response' export { Middleware } from './middleware/middleware' export { Controller } from './controller/controller' diff --git a/src/request/default-body-deserializer.ts b/src/request/default-body-deserializer.ts new file mode 100644 index 0000000..a41703b --- /dev/null +++ b/src/request/default-body-deserializer.ts @@ -0,0 +1,9 @@ +import { RouteMetadata } from '../routing/IRouter' +import { RequestBodyDeserializer } from './request-body-deserializer' +import { RequestEvent } from './request-event' + +export class DefaultBodyDeserializer implements RequestBodyDeserializer { + deserialize(event: RequestEvent, metadata: RouteMetadata): object { + return event.body + } +} diff --git a/src/request/event.ts b/src/request/event.ts deleted file mode 100644 index e0f4c09..0000000 --- a/src/request/event.ts +++ /dev/null @@ -1,37 +0,0 @@ -export enum HttpMethods { - GET, - POST, - PUT, - DELETE, - PATCH, - OPTIONS, - HEAD -} - -export interface Event { - headers: { [key: string]: string } - path: string - pathParameters: { [key: string]: string } - requestContext: { [key: string]: any } - resource: string - httpMethod: string - queryStringParameters: { [key: string]: any } - stageVariables: { [key: string]: any } - body: string - isOffline?: boolean -} - -/* -export class Event { - constructor( - protected headers: { [key: string]: string }, - protected pathParameters: { [key: string]: string }, - protected requestContext: { [key: string]: any }, - protected queryStringParameters: { [key: string]: any }, - protected stageVariables: { [key: string]: any }, - protected body: string, - path: string, - resource: string, - httpMethod: HttpMethods, - isOffline?: boolean - ) {} */ diff --git a/src/request/http-methods.ts b/src/request/http-methods.ts new file mode 100644 index 0000000..2806b63 --- /dev/null +++ b/src/request/http-methods.ts @@ -0,0 +1,9 @@ +export enum HttpMethods { + GET, + POST, + PUT, + DELETE, + PATCH, + OPTIONS, + HEAD +} diff --git a/src/request/index.ts b/src/request/index.ts index 407d4e0..e8a819c 100644 --- a/src/request/index.ts +++ b/src/request/index.ts @@ -1,2 +1,3 @@ +export { RequestEvent } from './request-event' export { Request } from './request' -export { Event, HttpMethods } from './event' +export { HttpMethods } from './http-methods' diff --git a/src/request/request-body-deserializer.ts b/src/request/request-body-deserializer.ts new file mode 100644 index 0000000..090f07e --- /dev/null +++ b/src/request/request-body-deserializer.ts @@ -0,0 +1,6 @@ +import { RouteMetadata } from '../routing/IRouter' +import { RequestEvent } from './request-event' + +export interface RequestBodyDeserializer { + deserialize(event: RequestEvent, metadata: RouteMetadata): object +} diff --git a/src/request/request-event.ts b/src/request/request-event.ts new file mode 100644 index 0000000..7227a80 --- /dev/null +++ b/src/request/request-event.ts @@ -0,0 +1,10 @@ +import { HttpMethods } from './http-methods' + +export interface RequestEvent { + body: TBody + queryStringParameters: ReadonlyMap + headers: ReadonlyMap + + path: string + method: HttpMethods +} diff --git a/src/request/request-interface.ts b/src/request/request-interface.ts deleted file mode 100644 index 84abe3d..0000000 --- a/src/request/request-interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IRequest { - get(key: string): any // retrieve request input - header(key: string): string | undefined // retrieve request header - add(key: string, value: any): void // add request data -} diff --git a/src/request/request.ts b/src/request/request.ts index 0d5c9ef..2a57052 100644 --- a/src/request/request.ts +++ b/src/request/request.ts @@ -1,87 +1,85 @@ import { MindlessError } from '../error/mindless.error' -import { Event, HttpMethods } from './event' -import { IRequest } from './request-interface' +import { RouteMetadata } from '../routing/IRouter' +import { HttpMethods } from './http-methods' -export class Request implements IRequest { - public RouteMetaData: { [key: string]: any } = {} - protected _body: { [key: string]: any } - protected data: { [key: string]: any } = {} +export class Request { + protected _pathParam: ReadonlyMap + protected _queryParam: ReadonlyMap + protected _header: ReadonlyMap + protected _context = new Map() + protected _routeMetadata: Readonly + protected _method: Readonly - constructor(protected event: Event) { - if (event.body === '' || event.body == null) { - this._body = {} - } else { - this._body = JSON.parse(event.body) - } - if (this.event.pathParameters == null) { - this.event.pathParameters = {} - } - if (this.event.queryStringParameters == null) { - this.event.queryStringParameters = {} - } - if (this.event.headers == null) { - this.event.headers = {} - } + constructor( + protected _path: Readonly, + protected _body: Readonly, + routeMetadata: RouteMetadata, + pathParameters: ReadonlyMap = new Map(), // { [keys: string]: string } = {}, + queryStringParameters: ReadonlyMap = new Map(), // { [key: string]: string | string[] } = {}, + headers: ReadonlyMap = new Map() // { [key: string]: string | string[] } = {} + ) { + this._method = routeMetadata.method + this._pathParam = pathParameters + this._queryParam = queryStringParameters + this._header = headers + this._routeMetadata = Object.freeze(routeMetadata) + } + + get path() { + return this._path + } + get method() { + return this._method } - get path(): string { - return this.event.path + get body() { + return this._body + } + get routeMetadata() { + return this._routeMetadata } - get method(): HttpMethods { - return (HttpMethods as any)[this.event.httpMethod.toUpperCase()] + public getPathParameter(key: string): string | undefined { + return this._pathParam.get(key) } - public getOrFail(key: string): any { - const value = this.get(key) - if (value) { - return value + public getPathParameterOrFail(key: string): string { + if (this._pathParam.has(key)) { + return this._pathParam.get(key)! } - throw new MindlessError( - `Invalid key: '${key}', key not found in pathParameters, queryStringParameters, or Body parameters.` - ) + throw new MindlessError(`Invalid key: '${key}', key not found in path parameters`) } - public get(key: string): any { - if (typeof this.data[key] !== 'undefined') { - return this.data[key] - } - if (typeof this._body[key] !== 'undefined') { - return this._body[key] - } - if (typeof this.event.queryStringParameters[key] !== 'undefined') { - return this.event.queryStringParameters[key] + public getQueryStringParameter(key: string): string | string[] | undefined { + return this._queryParam.get(key) + } + + public getQueryStringParameterOrFail(key: string): string | string[] { + if (this._queryParam.has(key)) { + return this._queryParam.get(key)! } - return undefined + throw new MindlessError(`Invalid key: '${key}', key not found in query string parameters`) } - public header(key: string): string | undefined { - return this.event.headers[key] + public getHeader(key: string): string | string[] | undefined { + return this._header.get(key) } - public headerOrFail(key: string): string { - const value = this.header(key) - if (value !== undefined) { - return value + public getHeaderOrFail(key: string): string | string[] { + if (this._header.has(key)) { + return this._header.get(key)! } throw new MindlessError(`Invalid key: '${key}', key not found in headers`) } - public add(key: string, val: any, overwrite: boolean = false): void { - if (overwrite || typeof this.data[key] === 'undefined') { - this.data[key] = val - return - } - - throw new MindlessError( - `The key '${key}' already exists, pass 'overwrite=true' or use a different key.` - ) + public addContext(key: string, value: TValue) { + this._context.set(key, value) } - public addMultiple(data: { [key: string]: any }) { - Object.keys(data).forEach(key => this.add(key, data[key])) + public getContext(key: string): TValue { + return this._context.get(key) } } diff --git a/src/response.ts b/src/response.ts index 7ca7e66..c1eab87 100644 --- a/src/response.ts +++ b/src/response.ts @@ -1,9 +1,3 @@ -export interface AWSLambdaIntegrationResponse { - statusCode: number - body: string - headers: { [key: string]: string } -} - export class Response { /** * @@ -16,13 +10,4 @@ export class Response { public body: { [key: string]: any } = {}, public headers: { [key: string]: string } = {} ) {} - - // TODO: remove this from the response class - public toAWSLambdaIntegrationResponse(): AWSLambdaIntegrationResponse { - return { - statusCode: this.statusCode, - body: JSON.stringify(this.body), - headers: this.headers - } - } } diff --git a/src/routing/IRouter.ts b/src/routing/IRouter.ts index 8544f99..687abb9 100644 --- a/src/routing/IRouter.ts +++ b/src/routing/IRouter.ts @@ -1,15 +1,27 @@ import { Controller } from '../controller/controller' import { Middleware } from '../middleware/middleware' -import { Request } from '../request' +import { RequestEvent } from '../request/request-event' import { Route } from './routes' -export interface RouteData { - route: Route - params: string[] +type Omit = Pick> + +export type RouteMetadata< + TRoute extends Route = Route +> = Omit + +export interface RouteData< + TRoute extends Route = Route +> { + route: TRoute + metadata: RouteMetadata + pathParameters: ReadonlyMap + methodParameters: string[] } -export interface IRouter { - readonly routes: Route[] +export interface IRouter< + TRoute extends Route = Route +> { + readonly routes: TRoute[] - getRouteData(request: Request): RouteData + getRouteData(request: RequestEvent): RouteData } diff --git a/src/routing/router.ts b/src/routing/router.ts index 4e4246c..1395f10 100644 --- a/src/routing/router.ts +++ b/src/routing/router.ts @@ -1,12 +1,11 @@ +import { Controller } from '../controller/controller' import { MindlessError } from '../error/mindless.error' -import { Route } from './routes' import { Middleware } from '../middleware/middleware' -import { Controller } from '../controller/controller' -import { Request } from '../request' -import { IRouter } from './IRouter' +import { Request, RequestEvent } from '../request' +import { IRouter, RouteData, RouteMetadata } from './IRouter' +import { Route } from './routes' -export class Router> - implements IRouter { +export class Router> implements IRouter { /** * Map that keeps a cache of the names of parameters each controller function requires * `key` is of the form - @@ -14,23 +13,7 @@ export class Router): { [key: string]: any } { - /** - * controller and middleware are constructors - * there should be no need for them - */ - const isUsefulKey = (key: string) => - typeof (route as any)[key] !== 'undefined' && key !== 'controller' && key !== 'middleware' - - return Object.keys(route) - .filter(isUsefulKey) - .reduce((metaData: any, key) => { - metaData[key] = (route as any)[key] - return metaData - }, {}) - } + constructor(protected _routes: TRoute[]) {} private static getParameters(func: Function) { const funcPieces = func.toString().match(/\(([^)]*)\)/) @@ -47,7 +30,7 @@ export class Router arg) // don't add undefined } - public get routes(): R[] { + public get routes(): TRoute[] { return this._routes } @@ -58,39 +41,46 @@ export class Router { + const [route, pathParameters] = this.getRequestedRoute(request) - request.RouteMetaData = Router.getRouteMetaData(route) + const metadata = this.getRouteMetaData(route) - const params = this.getMethodParameters(route) + const methodParameters = this.getMethodParameters(route) - return { route, params } + return { route, metadata, pathParameters, methodParameters } } - protected getRequestedRoute(request: Request): R { - const isRequestedRoute = (route: Route) => { - if (route.method !== request.method) { - return false - } - let params = route.url.match(request.path) - if (params) { - request.addMultiple(params) - return true + protected getRequestedRoute(request: RequestEvent): [TRoute, ReadonlyMap] { + for (const route of this._routes) { + if (route.method === request.method) { + const params = route.url.match(request.path) + if (params) { + return [route, new Map(Object.entries(params))] + } } - return false } - let route = this._routes.find(isRequestedRoute) + throw new MindlessError('Could not find requested route.') + } - if (route) { - return route - } + protected getRouteMetaData(route: Route): RouteMetadata { + /** + * controller and middleware are constructors + * there should be no need for them + */ + const isUsefulKey = (key: string) => + typeof (route as any)[key] !== 'undefined' && key !== 'controller' && key !== 'middleware' - throw new MindlessError('Could not find requested route.') + return Object.keys(route) + .filter(isUsefulKey) + .reduce((metaData: any, key) => { + metaData[key] = (route as any)[key] + return metaData + }, {}) } - protected getMethodParameters(route: R) { + protected getMethodParameters(route: TRoute) { const key = `${route.controller.name}-${route.function}` if (this.methodParameterCache[key] === undefined) { diff --git a/test/app/app.test.ts b/test/app/app.test.ts index a092e92..74d68d8 100644 --- a/test/app/app.test.ts +++ b/test/app/app.test.ts @@ -1,7 +1,7 @@ import * as TypeMoq from 'typemoq' +import { Dispatcher } from '../../src/app/dispatcher' import { CustomErrorHandler } from '../../src/error/custom-error-handler' import { GenericConstructor } from '../../src/interfaces' -import { Dispatcher } from '../../src/app' import { App, Controller, @@ -13,6 +13,9 @@ import { Response, RouteUrl } from '../../src/mindless' +import { RequestEvent } from '../../src/request' +import { DefaultBodyDeserializer } from '../../src/request/default-body-deserializer' +import { RouteData } from '../../src/routing/IRouter' describe('App', () => { const containerMock = TypeMoq.Mock.ofType() @@ -46,25 +49,46 @@ describe('App', () => { }) describe('App handle request', () => { - const requestMock = TypeMoq.Mock.ofType() + const eventMock = TypeMoq.Mock.ofType() const containerMock = TypeMoq.Mock.ofType() const routerMock = TypeMoq.Mock.ofType() const controllerConstructorMock = TypeMoq.Mock.ofType>() const middlewareConstructorMock = TypeMoq.Mock.ofType>() const dispatchMiddlewareMock = TypeMoq.Mock.ofInstance(Dispatcher.dispatchMiddleware) const dispatchControllerMock = TypeMoq.Mock.ofInstance(Dispatcher.dispatchController) + const errorHandlerMock = TypeMoq.Mock.ofType() + const bodyDeserializerMock = TypeMoq.Mock.ofType() beforeEach(() => { - requestMock.reset() + eventMock.reset() containerMock.reset() routerMock.reset() controllerConstructorMock.reset() middlewareConstructorMock.reset() dispatchMiddlewareMock.reset() dispatchControllerMock.reset() + errorHandlerMock.reset() + bodyDeserializerMock.reset() + }) + + afterEach(() => { + eventMock.verifyAll() + containerMock.verifyAll() + routerMock.verifyAll() + controllerConstructorMock.verifyAll() + middlewareConstructorMock.verifyAll() + dispatchMiddlewareMock.verifyAll() + dispatchControllerMock.verifyAll() + errorHandlerMock.verifyAll() + bodyDeserializerMock.verifyAll() }) test('successfully handle request', async () => { - const app = new App(containerMock.object, routerMock.object) + const app = new App( + containerMock.object, + routerMock.object, + errorHandlerMock.object, + bodyDeserializerMock.object + ) Dispatcher.dispatchMiddleware = dispatchMiddlewareMock.object Dispatcher.dispatchController = dispatchControllerMock.object @@ -80,32 +104,53 @@ describe('App handle request', () => { params: [] } + const routeData: RouteData = { + route: data.route, + metadata: { url: data.route.url, method: data.route.method, function: data.route.function }, + pathParameters: {}, + methodParameters: [] + } + routerMock - .setup(r => r.getRouteData(requestMock.object)) - .returns(() => data) + .setup(r => r.getRouteData(eventMock.object)) + .returns(() => routeData) + .verifiable(TypeMoq.Times.once()) + + bodyDeserializerMock + .setup(bd => bd.deserialize(eventMock.object, routeData.metadata)) .verifiable(TypeMoq.Times.once()) dispatchMiddlewareMock - .setup(m => m(containerMock.object, requestMock.object, data.route.middleware)) + .setup(m => + m(containerMock.object, TypeMoq.It.is(obj => obj instanceof Request), data.route.middleware) + ) .verifiable(TypeMoq.Times.once()) dispatchControllerMock - .setup(c => c(containerMock.object, requestMock.object, data.route, data.params)) + .setup(c => + c( + containerMock.object, + TypeMoq.It.is(obj => obj instanceof Request), + data.route, + data.params + ) + ) .returns(() => Promise.resolve(new Response())) .verifiable(TypeMoq.Times.once()) - const response = await app.handleRequest(requestMock.object) + const response = await app.handleRequest(eventMock.object) expect(response).toBeInstanceOf(Response) expect(response.statusCode).toBe(200) - - routerMock.verifyAll() - dispatchMiddlewareMock.verifyAll() - dispatchControllerMock.verifyAll() }) test('successfully handle request without middleware', async () => { - const app = new App(containerMock.object, routerMock.object) + const app = new App( + containerMock.object, + routerMock.object, + errorHandlerMock.object, + bodyDeserializerMock.object + ) Dispatcher.dispatchController = dispatchControllerMock.object @@ -119,27 +164,47 @@ describe('App handle request', () => { params: [] } + const routeData: RouteData = { + route: data.route, + metadata: { url: data.route.url, method: data.route.method, function: data.route.function }, + pathParameters: {}, + methodParameters: [] + } + routerMock - .setup(r => r.getRouteData(requestMock.object)) - .returns(() => data) + .setup(r => r.getRouteData(eventMock.object)) + .returns(() => routeData) + .verifiable(TypeMoq.Times.once()) + + bodyDeserializerMock + .setup(bd => bd.deserialize(eventMock.object, routeData.metadata)) .verifiable(TypeMoq.Times.once()) dispatchControllerMock - .setup(c => c(containerMock.object, requestMock.object, data.route, data.params)) + .setup(c => + c( + containerMock.object, + TypeMoq.It.is(obj => obj instanceof Request), + data.route, + data.params + ) + ) .returns(() => Promise.resolve(new Response())) .verifiable(TypeMoq.Times.once()) - const response = await app.handleRequest(requestMock.object) + const response = await app.handleRequest(eventMock.object) expect(response).toBeInstanceOf(Response) expect(response.statusCode).toBe(200) - - routerMock.verifyAll() - dispatchControllerMock.verifyAll() }) test('middleware rejects with response', async () => { - const app = new App(containerMock.object, routerMock.object) + const app = new App( + containerMock.object, + routerMock.object, + errorHandlerMock.object, + bodyDeserializerMock.object + ) Dispatcher.dispatchMiddleware = dispatchMiddlewareMock.object Dispatcher.dispatchController = dispatchControllerMock.object @@ -155,135 +220,140 @@ describe('App handle request', () => { params: [] } + const routeData: RouteData = { + route: data.route, + metadata: { url: data.route.url, method: data.route.method, function: data.route.function }, + pathParameters: {}, + methodParameters: [] + } + const errorMsg = 'error message' routerMock - .setup(r => r.getRouteData(requestMock.object)) - .returns(() => data) + .setup(r => r.getRouteData(eventMock.object)) + .returns(() => routeData) + .verifiable(TypeMoq.Times.once()) + + bodyDeserializerMock + .setup(bd => bd.deserialize(eventMock.object, routeData.metadata)) .verifiable(TypeMoq.Times.once()) dispatchMiddlewareMock - .setup(m => m(containerMock.object, requestMock.object, data.route.middleware)) + .setup(m => + m(containerMock.object, TypeMoq.It.is(obj => obj instanceof Request), data.route.middleware) + ) .returns(() => Promise.reject(new Response(399, { rejected: errorMsg }))) .verifiable(TypeMoq.Times.once()) dispatchControllerMock - .setup(c => c(containerMock.object, requestMock.object, data.route, data.params)) - .throws(new Error(errorMsg)) + .setup(c => c(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .verifiable(TypeMoq.Times.never()) - const response = await app.handleRequest(requestMock.object) + const response = await app.handleRequest(eventMock.object) expect(response).toBeInstanceOf(Response) expect(response.statusCode).toBe(399) expect(response.body.rejected).toEqual(errorMsg) - - routerMock.verifyAll() - dispatchMiddlewareMock.verifyAll() - dispatchControllerMock.verifyAll() }) - test('error with default error handler', async () => { - const app = new App(containerMock.object, routerMock.object) + test('handle error from getRouteData', async () => { + const app = new App( + containerMock.object, + routerMock.object, + errorHandlerMock.object, + bodyDeserializerMock.object + ) Dispatcher.dispatchMiddleware = dispatchMiddlewareMock.object Dispatcher.dispatchController = dispatchControllerMock.object - const data = { - route: { - url: new RouteUrl(''), - method: HttpMethods.GET, - function: 'test', - controller: controllerConstructorMock.object, - middleware: [middlewareConstructorMock.object] - }, - params: [] - } - - const errorMsg = 'error message' + const error = new Error('error message') + const expectedRes = new Response(399, { err: 'blah' }) routerMock - .setup(r => r.getRouteData(requestMock.object)) - .returns(() => data) + .setup(r => r.getRouteData(eventMock.object)) + .throws(error) .verifiable(TypeMoq.Times.once()) - dispatchMiddlewareMock - .setup(m => m(containerMock.object, requestMock.object, data.route.middleware)) + errorHandlerMock + .setup(handler => handler(error, eventMock.object)) + .returns(() => expectedRes) .verifiable(TypeMoq.Times.once()) dispatchControllerMock - .setup(c => c(containerMock.object, requestMock.object, data.route, data.params)) - .throws(new Error(errorMsg)) - .verifiable(TypeMoq.Times.once()) + .setup(c => c(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()) - const response = await app.handleRequest(requestMock.object) + dispatchMiddlewareMock + .setup(m => m(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .verifiable(TypeMoq.Times.never()) - expect(response).toBeInstanceOf(Response) - expect(response.statusCode).toBe(500) - expect(response.body.message).toMatch(/An error occurred/) - expect(response.body.message).toMatch(/using default error handler/) + const actualResponse = await app.handleRequest(eventMock.object) - routerMock.verifyAll() - dispatchMiddlewareMock.verifyAll() - dispatchControllerMock.verifyAll() + expect(actualResponse).toBeInstanceOf(Response) + expect(actualResponse).toBe(expectedRes) }) - test('error with custom error handler', async () => { - const key = 'key' - const value = 'special request value' + test('handle error from dispatching controller', async () => { + const app = new App( + containerMock.object, + routerMock.object, + errorHandlerMock.object, + bodyDeserializerMock.object + ) - const customErrorHandler: CustomErrorHandler = (e: Error, request: Request) => { - return new Response(200, { message: `request value: ${request.get(key)}` }) - } - - const app = new App(containerMock.object, routerMock.object, customErrorHandler) - - Dispatcher.dispatchMiddleware = dispatchMiddlewareMock.object Dispatcher.dispatchController = dispatchControllerMock.object + const error = new Error('error message') + const expectedRes = new Response(399, { err: 'blah' }) + const data = { route: { url: new RouteUrl(''), method: HttpMethods.GET, function: 'test', controller: controllerConstructorMock.object, - middleware: [middlewareConstructorMock.object] + middleware: [] }, params: [] } - const errorMsg = 'error message' + const routeData: RouteData = { + route: data.route, + metadata: { url: data.route.url, method: data.route.method, function: data.route.function }, + pathParameters: {}, + methodParameters: [] + } - requestMock - .setup(r => r.get(key)) - .returns(() => value) + routerMock + .setup(r => r.getRouteData(eventMock.object)) + .returns(() => routeData) .verifiable(TypeMoq.Times.once()) - routerMock - .setup(r => r.getRouteData(requestMock.object)) - .returns(() => data) + bodyDeserializerMock + .setup(bd => bd.deserialize(eventMock.object, routeData.metadata)) .verifiable(TypeMoq.Times.once()) - dispatchMiddlewareMock - .setup(m => m(containerMock.object, requestMock.object, data.route.middleware)) + errorHandlerMock + .setup(handler => handler(error, TypeMoq.It.is(obj => obj instanceof Request))) + .returns(() => expectedRes) .verifiable(TypeMoq.Times.once()) dispatchControllerMock - .setup(c => c(containerMock.object, requestMock.object, data.route, data.params)) - .throws(new Error(errorMsg)) + .setup(c => + c( + containerMock.object, + TypeMoq.It.is(obj => obj instanceof Request), + data.route, + data.params + ) + ) + .returns(() => Promise.reject(error)) .verifiable(TypeMoq.Times.once()) - process.env.NODE_ENV = 'prod' - - const response = await app.handleRequest(requestMock.object) - expect(response).toBeInstanceOf(Response) - expect(response.statusCode).toBe(200) - expect(response.body.message).toMatch(/request value/) - expect(response.body.message).toMatch(new RegExp(value)) + const actualResponse = await app.handleRequest(eventMock.object) - requestMock.verifyAll() - routerMock.verifyAll() - dispatchMiddlewareMock.verifyAll() - dispatchControllerMock.verifyAll() + expect(actualResponse).toBeInstanceOf(Response) + expect(actualResponse).toBe(expectedRes) }) }) diff --git a/test/app/dispatcher.test.ts b/test/app/dispatcher.test.ts index b81cedb..0ea2c14 100644 --- a/test/app/dispatcher.test.ts +++ b/test/app/dispatcher.test.ts @@ -1,39 +1,38 @@ import * as TypeMoq from 'typemoq' -import { Dispatcher } from '../../src/app' +import { Dispatcher } from '../../src/app/dispatcher' import { MindlessError } from '../../src/error/mindless.error' import { GenericConstructor } from '../../src/interfaces' -import { MiddlewareHandle } from '../../src/middleware/middleware-handle' import { Controller, + Event, HttpMethods, IContainer, Middleware, Request, Response, - RouteUrl, - Event + RouteUrl } from '../../src/mindless' import { ActionMiddlewareMock } from '../mocks/action-middleware.mock' import { MiddlewareMock } from '../mocks/middleware.mock' describe('Dispatch middleware', () => { const containerMock = TypeMoq.Mock.ofType() + const requestMock = TypeMoq.Mock.ofType() beforeEach(() => { containerMock.reset() + requestMock.reset() }) test('no middleware', async () => { - const request = new Request({} as Event) - const middlewareList: GenericConstructor[] = [] containerMock.setup(c => c.resolve(TypeMoq.It.isAny())).verifiable(TypeMoq.Times.never()) const response = await Dispatcher.dispatchMiddleware( containerMock.object, - request, + requestMock.object, middlewareList ) @@ -43,7 +42,6 @@ describe('Dispatch middleware', () => { }) test('single middleware', async () => { - const request = new Request({} as Event) const middlewareList: GenericConstructor[] = [MiddlewareMock] const middlewareMock = new MiddlewareMock() @@ -54,7 +52,7 @@ describe('Dispatch middleware', () => { const response = await Dispatcher.dispatchMiddleware( containerMock.object, - request, + requestMock.object, middlewareList ) @@ -65,30 +63,24 @@ describe('Dispatch middleware', () => { }) test('multiple middleware are called in correct order', async () => { - const request = new Request({} as Event) - class AnotherMiddlewareMock extends MiddlewareMock {} - let order = 0 const getTag = () => (order++).toString() - const middlewareList: GenericConstructor[] = [MiddlewareMock, AnotherMiddlewareMock] + const middlewareList: GenericConstructor[] = [MiddlewareMock, MiddlewareMock] const middlewareMock = new MiddlewareMock(getTag) - const anotherMiddlewareMock = new AnotherMiddlewareMock(getTag) + const anotherMiddlewareMock = new MiddlewareMock(getTag) - containerMock - .setup(c => c.resolve(MiddlewareMock)) - .returns(() => middlewareMock) - .verifiable(TypeMoq.Times.once()) + containerMock.setup(c => c.resolve(MiddlewareMock)).returns(() => middlewareMock) containerMock - .setup(c => c.resolve(AnotherMiddlewareMock)) + .setup(c => c.resolve(MiddlewareMock)) .returns(() => anotherMiddlewareMock) - .verifiable(TypeMoq.Times.once()) + .verifiable(TypeMoq.Times.exactly(2)) const response = await Dispatcher.dispatchMiddleware( containerMock.object, - request, + requestMock.object, middlewareList ) @@ -102,33 +94,37 @@ describe('Dispatch middleware', () => { }) test('multiple middleware share same request', async () => { - const request = new Request({} as Event) - class AnotherMiddlewareMock extends ActionMiddlewareMock {} - - const middlewareList: GenericConstructor[] = [MiddlewareMock, AnotherMiddlewareMock] + const middlewareList: GenericConstructor[] = [ + ActionMiddlewareMock, + ActionMiddlewareMock + ] const key = 'key' const value = 'special value from middleware 1' - const middlewareMock = new ActionMiddlewareMock(req => req.add(key, value)) + const middlewareMock = new ActionMiddlewareMock(req => req.addContext(key, value)) let valueFromAnotherMiddlewareMock: string | undefined = undefined - const anotherMiddlewareMock = new AnotherMiddlewareMock( - req => (valueFromAnotherMiddlewareMock = req.get(key)) + const anotherMiddlewareMock = new ActionMiddlewareMock( + req => (valueFromAnotherMiddlewareMock = req.getContext(key)) ) - containerMock - .setup(c => c.resolve(MiddlewareMock)) - .returns(() => middlewareMock) - .verifiable(TypeMoq.Times.once()) + containerMock.setup(c => c.resolve(ActionMiddlewareMock)).returns(() => middlewareMock) containerMock - .setup(c => c.resolve(AnotherMiddlewareMock)) + .setup(c => c.resolve(ActionMiddlewareMock)) .returns(() => anotherMiddlewareMock) + .verifiable(TypeMoq.Times.exactly(2)) + + requestMock.setup(r => r.addContext(key, value)).verifiable(TypeMoq.Times.once()) + + requestMock + .setup(r => r.getContext(key)) + .returns(() => value) .verifiable(TypeMoq.Times.once()) const response = await Dispatcher.dispatchMiddleware( containerMock.object, - request, + requestMock.object, middlewareList ) @@ -136,9 +132,9 @@ describe('Dispatch middleware', () => { expect(middlewareMock.numberOfTimesHandleHasBeenCalled).toEqual(1) expect(anotherMiddlewareMock.numberOfTimesHandleHasBeenCalled).toEqual(1) expect(valueFromAnotherMiddlewareMock).toEqual(value) - expect(request.get(key)).toEqual(value) containerMock.verifyAll() + requestMock.verifyAll() }) }) @@ -197,7 +193,7 @@ describe('Dispatch controller', () => { function: 'test' } - const params = ['test', 'test2'] + const params = ['test1', 'test2'] containerMock .setup(c => c.resolve(controllerConstructorMock.object)) @@ -205,16 +201,16 @@ describe('Dispatch controller', () => { .verifiable(TypeMoq.Times.once()) requestMock - .setup(r => r.getOrFail('test')) - .returns(() => 1) + .setup(r => r.getPathParameter('test1')) + .returns(() => '1') .verifiable(TypeMoq.Times.once()) requestMock - .setup(r => r.getOrFail('test2')) - .returns(() => 2) + .setup(r => r.getQueryStringParameter('test2')) + .returns(() => '2') .verifiable(TypeMoq.Times.once()) controllerMock - .setup(m => (m as any).test(1, 2)) + .setup(c => (c as any).test('1', '2')) .returns(() => Promise.resolve(new Response())) .verifiable(TypeMoq.Times.once()) @@ -333,8 +329,13 @@ describe('Dispatch controller fails', () => { .verifiable(TypeMoq.Times.once()) requestMock - .setup(r => r.getOrFail('test')) - .throws(new Error()) + .setup(r => r.getPathParameter('test')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()) + + requestMock + .setup(r => r.getQueryStringParameter('test')) + .returns(() => undefined) .verifiable(TypeMoq.Times.once()) try { diff --git a/test/request/request.test.ts b/test/request/request.test.ts index 00efc82..c07768c 100644 --- a/test/request/request.test.ts +++ b/test/request/request.test.ts @@ -1,215 +1,137 @@ -import { Event, HttpMethods, Request } from '../../src/request' +import { MindlessError } from '../../src/mindless' +import { HttpMethods, Request } from '../../src/request' +import { RouteUrl } from '../../src/routing' +import { RouteMetadata } from '../../src/routing/IRouter' +import { escapeRegExp } from 'tslint/lib/utils' -import * as TypeMoq from 'typemoq' - -function getEvent(): Event { +function getRouteMetadata(): RouteMetadata { return { - headers: {}, - path: '', - pathParameters: {}, - requestContext: {}, - resource: '', - httpMethod: 'GET', - queryStringParameters: {}, - stageVariables: {}, - body: '' + function: '', + url: new RouteUrl(''), + method: HttpMethods.GET } } describe('Test request constructor', () => { - // construct an event object - // no need to mock just a DTO essentially - // const eventMock: TypeMoq.IMock = TypeMoq.Mock.ofType(Event); - const localEvent: Event = getEvent() // default event with no data. - - test('empty event', () => { - let request = new Request(localEvent) - expect(request.path).toBe('') - expect(request.method).toBe(HttpMethods.GET) + const path = 'some-path' + + test('method and path are properly exposed', () => { + const request = new Request(path, {}, getRouteMetadata()) + expect(request.path).toEqual(path) + expect(request.method).toEqual(HttpMethods.GET) }) - test('successfully parses json event body', () => { - let eventWithBody = localEvent - let body = { + test('request exposes body object', () => { + const body = { name: 'zach', number: 12345, things: ['a', 'b', 'c'] } - eventWithBody.body = JSON.stringify(body) - - let request = new Request(eventWithBody) - - expect(request.get('name')).toBe('zach') - expect(request.get('number')).toBe(12345) - expect(request.get('things')).toEqual(['a', 'b', 'c']) - }) - - // needed to not break Request.get() - test('defaults pathParameters, queryStringParameters and headers if null', () => { - let defaultEvent = localEvent - defaultEvent.pathParameters = null - defaultEvent.queryStringParameters = null - defaultEvent.headers = null - - let request = new Request(defaultEvent) - expect(() => { - request.getOrFail('abc') - }).toThrow(/key not found/) - expect(request.get('abc')).toBeUndefined() - }) -}) - -describe('Test request get method ', () => { - const localEvent = getEvent() - test('get added params', () => { - let defaultEvent = Object.assign({}, localEvent) - let request = new Request(defaultEvent) - request.add('param', 'abc') - let actualRetrievedValue = request.get('param') + const request = new Request(path, body, getRouteMetadata()) - expect(actualRetrievedValue).toBe('abc') + expect(request.body).toEqual(body) }) - test('get query string parameters', () => { - let defaultEvent = getEvent() - defaultEvent.queryStringParameters['param'] = 'abc' - - let request = new Request(defaultEvent) + test('request exposes body object of class instance', () => { + class TestBody { + constructor(public data: string) {} + getGreeting(name: string): string { + return `Hello ${name}` + } + } + const body = new TestBody('some data') - let actualRetrievedValue = request.get('param') + const request = new Request(path, body, getRouteMetadata()) - expect(actualRetrievedValue).toBe('abc') + expect(request.body).toBeInstanceOf(TestBody) + expect(request.body.data).toEqual('some data') + expect(request.body.getGreeting('bob')).toEqual('Hello bob') }) - test('get body parameters', () => { - let defaultEvent = getEvent() - - defaultEvent.body = JSON.stringify({ param: 'abc' }) - - let request = new Request(defaultEvent) + test('request exposes routeMetedata', () => { + const routeMetadata = getRouteMetadata() + routeMetadata.function = 'blah' - let actualRetrievedValue = request.get('param') + const request = new Request(path, {}, routeMetadata) - expect(actualRetrievedValue).toBe('abc') + expect(request.routeMetadata).toEqual(routeMetadata) }) +}) - test('getOrFail retrieve body parameters', () => { - let defaultEvent = getEvent() - - defaultEvent.body = JSON.stringify({ param: 'abc' }) - - let request = new Request(defaultEvent) +describe('Test request get data ', () => { + const key = 'param' + const val = 'abc' + const obj = new Map([['param', 'abc']]) - let actualRetrievedValue = request.getOrFail('param') + test('set and get context', () => { + const request = new Request('', {}, getRouteMetadata()) + request.addContext(key, val) + const actual = request.getContext(key) - expect(actualRetrievedValue).toBe('abc') + expect(actual).toBe(val) }) - test('invalid key getOrFail', () => { - let defaultEvent = getEvent() - defaultEvent.pathParameters['test'] = 'abc' - defaultEvent.queryStringParameters['testb'] = 'abc' - defaultEvent.body = JSON.stringify({ testc: 'abc' }) - - let request = new Request(defaultEvent) + test('get path parameters', () => { + const request = new Request('', {}, getRouteMetadata(), obj) + const actual = request.getPathParameter(key) + const actualFromOrFail = request.getPathParameterOrFail(key) - expect(() => { - request.getOrFail('abc') - }).toThrow(/key not found/) + expect(actual).toBe(val) + expect(actualFromOrFail).toBe(val) }) - test('key not found returns undefined', () => { - let defaultEvent = getEvent() - defaultEvent.pathParameters['test'] = 'abc' - defaultEvent.queryStringParameters['testb'] = 'abc' - defaultEvent.body = JSON.stringify({ testc: 'abc' }) + test('get path param or fail', () => { + const request = new Request('', {}, getRouteMetadata()) - let request = new Request(defaultEvent) - - let retrievedValue = request.get('abc') + try { + const actual = request.getPathParameterOrFail(key) + } catch (e) { + expect(e).toBeInstanceOf(MindlessError) + } - expect(retrievedValue).toBeUndefined() + expect.assertions(1) }) -}) -describe('Test request header', () => { - test('invalid key', () => { - let event = getEvent() - let request = new Request(event) - - expect(() => { - request.headerOrFail('abc') - }).toThrow(/key not found/) - }) - - test('headerOrFail retrieve header', () => { - let event = getEvent() - event.headers['test'] = 'val' - let request = new Request(event) - expect(request.headerOrFail('test')).toBe('val') - }) + test('get query string parameters', () => { + const request = new Request('', {}, getRouteMetadata(), new Map(), obj) + const actual = request.getQueryStringParameter(key) + const actualFromOrFail = request.getQueryStringParameterOrFail(key) - - test('undefined header value', () => { - let event = getEvent() - let request = new Request(event) - - expect(() => { - request.header('abc').toBeUndefined() - } + expect(actual).toBe(val) + expect(actualFromOrFail).toBe(val) }) - - test('retrieve header value', () => { - let event = getEvent() - event.headers['test'] = 'val' - let request = new Request(event) + test('get query string param or fail', () => { + const request = new Request('', new Map(), getRouteMetadata()) - expect(request.header('test')).toBe('val') - }) - -}) - -describe('Test request add method', () => { - let event = getEvent() - let request: Request + try { + const actual = request.getQueryStringParameterOrFail(key) + } catch (e) { + expect(e).toBeInstanceOf(MindlessError) + } - beforeEach(() => { - // reset request object - request = new Request(event) + expect.assertions(1) }) - test('Add new key,val pair', () => { - request.add('abc', 'val') - let retrievedVal = request.get('abc') + test('get header', () => { + const request = new Request('', {}, getRouteMetadata(), new Map(), new Map(), obj) + const actual = request.getHeader(key) + const actualFromFail = request.getHeaderOrFail(key) - expect(retrievedVal).toBe('val') + expect(actual).toBe(val) + expect(actualFromFail).toBe(val) }) - test('Add new key,val pair with already existing key', () => { - request.add('abc', 'val') + test('get header or fail', () => { + const request = new Request('', {}, getRouteMetadata()) - let addKeyAlreadyExists = () => { - request.add('abc', 'val2') + try { + const actual = request.getHeaderOrFail(key) + } catch (e) { + expect(e).toBeInstanceOf(MindlessError) } - expect(addKeyAlreadyExists).toThrow(/key 'abc' already exists/) - expect(request.get('abc')).toBe('val') - }) - - test('Add new key,val pair with already existing key and overwrite set to true', () => { - request.add('abc', 'val') - request.add('abc', 'val2', true) - - expect(request.get('abc')).toBe('val2') - }) - - test('Add multiple key,val pair', () => { - const map = { abc: 'val', def: 'lav' } - request.addMultiple(map) - - expect(request.get('abc')).toBe('val') - expect(request.get('def')).toBe('lav') + expect.assertions(1) }) }) diff --git a/test/response.test.ts b/test/response.test.ts index 5ae2953..e57bf1f 100644 --- a/test/response.test.ts +++ b/test/response.test.ts @@ -9,15 +9,14 @@ describe('Response class test', () => { expect(response.headers).toEqual({}) }) - test('aws integration respone', () => { - const body = { msg: 'hello world' } - const headers = { aHeader: 'i am a header' } - const response = new Response(200, body, headers) + test('provide values', () => { + const code = 837 + const body = { key: 'val' } + const header = { headerKey: 'headerval' } + const response = new Response(code, body, header) - const awsResponse = response.toAWSLambdaIntegrationResponse() - - expect(awsResponse.statusCode).toBe(200) - expect(awsResponse.body).toBe(JSON.stringify(body)) - expect(awsResponse.headers).toBe(headers) + expect(response.statusCode).toBe(code) + expect(response.body).toEqual(body) + expect(response.headers).toEqual(header) }) }) diff --git a/test/routing/router.test.ts b/test/routing/router.test.ts index d3fa670..81399a7 100644 --- a/test/routing/router.test.ts +++ b/test/routing/router.test.ts @@ -10,7 +10,7 @@ import { } from '../../src/mindless' import * as TypeMoq from 'typemoq' -import { GenericConstructor } from '../../src/interfaces' +import { RequestEvent } from '../../src/request' class TestController extends Controller { test(): Response { @@ -34,15 +34,17 @@ class TestController extends Controller { return new Response(200, res) } } +const eventMock = TypeMoq.Mock.ofType() -describe('Router getRequestRoute returns the correct route and parameters', () => { - let requestMock = TypeMoq.Mock.ofType() - - beforeEach(() => { - requestMock.reset() - }) +beforeEach(() => { + eventMock.reset() +}) +afterEach(() => { + eventMock.verifyAll() +}) - let routes: MindlessRoute[] = [ +describe('Router getRequestRoute returns the correct route and parameters', () => { + const routes: MindlessRoute[] = [ { url: new RouteUrl('/test'), method: HttpMethods.POST, @@ -59,23 +61,23 @@ describe('Router getRequestRoute returns the correct route and parameters', () = } ] - const router = new Router(routes) + const router = new Router(routes) test('Throws error when route group undefined (method mismatch)', () => { - requestMock.setup(c => c.path).returns(() => '/test') - requestMock.setup(c => c.method).returns(() => HttpMethods.GET) + eventMock.setup(ev => ev.path).returns(() => '/test') + eventMock.setup(ev => ev.method).returns(() => HttpMethods.GET) expect(() => { - router.getRouteData(requestMock.object) + router.getRouteData(eventMock.object) }).toThrow('Could not find requested route.') }) test('Throws error when route group undefined (path mismatch)', () => { - requestMock.setup(c => c.path).returns(() => '/blah') - requestMock.setup(c => c.method).returns(() => HttpMethods.POST) + eventMock.setup(ev => ev.path).returns(() => '/blah') + eventMock.setup(ev => ev.method).returns(() => HttpMethods.POST) expect(() => { - router.getRouteData(requestMock.object) + router.getRouteData(eventMock.object) }).toThrow('Could not find requested route.') }) @@ -86,35 +88,44 @@ describe('Router getRequestRoute returns the correct route and parameters', () = function: routes[0].function } - requestMock - .setup(c => c.path) + eventMock + .setup(ev => ev.path) .returns(() => '/test') .verifiable(TypeMoq.Times.once()) - requestMock - .setup(c => c.method) + eventMock + .setup(ev => ev.method) .returns(() => HttpMethods.POST) .verifiable(TypeMoq.Times.once()) - requestMock - .setup(r => (r.RouteMetaData = TypeMoq.It.isValue(expectedRouteMetadata))) - .verifiable(TypeMoq.Times.once()) - const data = router.getRouteData(requestMock.object) + const data = router.getRouteData(eventMock.object) expect(data.route).toEqual(routes[0]) - expect(data.params.length).toEqual(0) - - requestMock.verifyAll() + expect(data.methodParameters).toHaveLength(0) + expect(data.pathParameters).toEqual(new Map()) + expect(data.metadata.url).toEqual(expectedRouteMetadata.url) + expect(data.metadata.method).toEqual(expectedRouteMetadata.method) + expect(data.metadata.function).toEqual(expectedRouteMetadata.function) }) test('Finds the correct route and returns the route object with parameters', () => { - requestMock.setup(c => c.path).returns(() => '/test') - requestMock.setup(c => c.method).returns(() => HttpMethods.PUT) + const expectedRouteMetadata = { + url: routes[1].url, + method: routes[1].method, + function: routes[1].function + } - const data = router.getRouteData(requestMock.object) + eventMock.setup(ev => ev.path).returns(() => '/test') + eventMock.setup(ev => ev.method).returns(() => HttpMethods.PUT) + + const data = router.getRouteData(eventMock.object) expect(data.route).toEqual(routes[1]) - expect(data.params.length).toEqual(1) - expect(data.params[0]).toEqual('val') + expect(data.methodParameters).toHaveLength(1) + expect(data.methodParameters[0]).toEqual('val') + expect(data.pathParameters).toEqual(new Map()) + expect(data.metadata.url).toEqual(expectedRouteMetadata.url) + expect(data.metadata.method).toEqual(expectedRouteMetadata.method) + expect(data.metadata.function).toEqual(expectedRouteMetadata.function) }) test('get routes', () => { @@ -124,13 +135,7 @@ describe('Router getRequestRoute returns the correct route and parameters', () = }) describe('Router add all path parameters to the request', () => { - let requestMock = TypeMoq.Mock.ofType() - - beforeEach(() => { - requestMock.reset() - }) - - let routes: MindlessRoute[] = [ + const routes: MindlessRoute[] = [ { url: new RouteUrl('/test/:id/:name'), method: HttpMethods.POST, @@ -140,34 +145,21 @@ describe('Router add all path parameters to the request', () => { } ] - const router = new Router(routes) + const router = new Router(routes) test('Finds the correct route and returns the route object with parameters', () => { - requestMock.setup(c => c.path).returns(() => '/test/123/abc') - requestMock.setup(c => c.method).returns(() => HttpMethods.POST) - - const data = router.getRouteData(requestMock.object) - const expectedParams = { - id: '123', - name: 'abc' - } + eventMock.setup(ev => ev.path).returns(() => '/test/123/abc') + eventMock.setup(ev => ev.method).returns(() => HttpMethods.POST) - requestMock.verify( - r => r.addMultiple(TypeMoq.It.isObjectWith(expectedParams)), - TypeMoq.Times.once() - ) + const data = router.getRouteData(eventMock.object) + const expectedParams = new Map([['id', '123'], ['name', 'abc']]) expect(data.route).toEqual(routes[0]) + expect(data.pathParameters).toEqual(expectedParams) }) }) describe('Route function value is not a method name of the controller', () => { - let requestMock = TypeMoq.Mock.ofType() - - beforeEach(() => { - requestMock.reset() - }) - let routes: MindlessRoute[] = [ { url: new RouteUrl('/test'), @@ -178,25 +170,19 @@ describe('Route function value is not a method name of the controller', () => { } ] - const router = new Router(routes) + const router = new Router(routes) test('throws correct error', () => { - requestMock.setup(c => c.path).returns(() => '/test') - requestMock.setup(c => c.method).returns(() => HttpMethods.GET) + eventMock.setup(ev => ev.path).returns(() => '/test') + eventMock.setup(ev => ev.method).returns(() => HttpMethods.GET) - expect(() => router.getRouteData(requestMock.object)).toThrow( + expect(() => router.getRouteData(eventMock.object)).toThrow( "'blah' is not a method on the controller 'TestController'" ) }) }) describe('Cannot parse function', () => { - let requestMock = TypeMoq.Mock.ofType() - - beforeEach(() => { - requestMock.reset() - }) - let routes: MindlessRoute[] = [ { url: new RouteUrl('/test'), @@ -207,29 +193,24 @@ describe('Cannot parse function', () => { } ] - const router = new Router(routes) + const router = new Router(routes) test('throws correct error', () => { - requestMock - .setup(r => r.path) + eventMock + .setup(ev => ev.path) .returns(() => '/test') .verifiable(TypeMoq.Times.once()) - requestMock - .setup(r => r.method) + eventMock + .setup(ev => ev.method) .returns(() => HttpMethods.GET) .verifiable(TypeMoq.Times.once()) - expect(() => router.getRouteData(requestMock.object)).toThrow( - 'Route has invalid function' - ) - - requestMock.verifyAll() + expect(() => router.getRouteData(eventMock.object)).toThrow('Route has invalid function') }) }) describe('methodParameterCache works', () => { - const requestMock = TypeMoq.Mock.ofType() const functionMock = TypeMoq.Mock.ofType<() => number>() class MyController extends Controller { @@ -241,11 +222,10 @@ describe('methodParameterCache works', () => { MyController.prototype.test = functionMock.object beforeEach(() => { - requestMock.reset() functionMock.reset() }) - let routes: MindlessRoute[] = [ + const routes: MindlessRoute[] = [ { url: new RouteUrl('/test'), method: HttpMethods.GET, @@ -254,16 +234,16 @@ describe('methodParameterCache works', () => { function: 'test' } ] - const router = new Router(routes) + const router = new Router(routes) test('will not parse function if already parsed', () => { - requestMock - .setup(r => r.path) + eventMock + .setup(ev => ev.path) .returns(() => '/test') .verifiable(TypeMoq.Times.exactly(2)) - requestMock - .setup(r => r.method) + eventMock + .setup(ev => ev.method) .returns(() => HttpMethods.GET) .verifiable(TypeMoq.Times.exactly(2)) @@ -272,13 +252,12 @@ describe('methodParameterCache works', () => { .returns(() => '()') .verifiable(TypeMoq.Times.once()) - const data1 = router.getRouteData(requestMock.object) - const data2 = router.getRouteData(requestMock.object) + const data1 = router.getRouteData(eventMock.object) + const data2 = router.getRouteData(eventMock.object) expect(data1.route).toEqual(routes[0]) expect(data1).toEqual(data2) - requestMock.verifyAll() functionMock.verifyAll() }) }) diff --git a/tsconfig.json b/tsconfig.json index 215a7f1..578acb8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,8 @@ "experimentalDecorators": true, "emitDecoratorMetadata": true, "declarationDir": "dist/types", + "strictPropertyInitialization": true, + "strictNullChecks": true, "outDir": "dist/lib", "typeRoots": [ "node_modules/@types"