diff --git a/.eslintrc.json b/.eslintrc.json index f55dc0a..939de86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,36 +1,17 @@ { "env": { - "es6": true, - "node": true, - "jest": true + "es6": true, + "node": true, + "jest": true }, - "extends": "eslint:recommended", + "extends": ["eslint:recommended", "prettier"], "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - }, - "rules": { - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single", - { "allowTemplateLiterals": true } - ], - "semi": [ - "error", - "never" - ], - "indent": [ - "error", - 2, - { "SwitchCase": 1, "flatTernaryExpressions": true } - ] + "ecmaVersion": 2018, + "sourceType": "module" }, + "rules": {}, "globals": { - "expect": true, - "it": true + "expect": true, + "it": true } } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b351a13..07a2c62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,8 @@ name: build on: [pull_request, push] - jobs: build: - runs-on: ubuntu-latest strategy: @@ -37,5 +35,3 @@ jobs: COVERALLS_SERVICE_NAME: GithubActions COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} COVERALLS_GIT_BRANCH: ${{ env.BRANCH_NAME }} - - \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..628e08b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +coverage +node_modules +__tests__ +*.test.js +dist \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..544138b --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/README.md b/README.md index 2dbc81f..2277ebe 100644 --- a/README.md +++ b/README.md @@ -13,37 +13,40 @@ Lambda API is a lightweight web framework for AWS Lambda using AWS API Gateway L ```javascript // Require the framework and instantiate it -const api = require('lambda-api')() +const api = require('lambda-api')(); // Define a route -api.get('/status', async (req,res) => { - return { status: 'ok' } -}) +api.get('/status', async (req, res) => { + return { status: 'ok' }; +}); // Declare your Lambda handler exports.handler = async (event, context) => { // Run the request - return await api.run(event, context) -} + return await api.run(event, context); +}; ``` For a full tutorial see [How To: Build a Serverless API with Serverless, AWS Lambda and Lambda API](https://www.jeremydaly.com/build-serverless-api-serverless-aws-lambda-lambda-api/). ## Why Another Web Framework? + Express.js, Fastify, Koa, Restify, and Hapi are just a few of the many amazing web frameworks out there for Node.js. So why build yet another one when there are so many great options already? One word: **DEPENDENCIES**. These other frameworks are extremely powerful, but that benefit comes with the steep price of requiring several additional Node.js modules. Not only is this a bit of a security issue (see Beware of Third-Party Packages in [Securing Serverless](https://www.jeremydaly.com/securing-serverless-a-newbies-guide/)), but it also adds bloat to your codebase, filling your `node_modules` directory with a ton of extra files. For serverless applications that need to load quickly, all of these extra dependencies slow down execution and use more memory than necessary. Express.js has **30 dependencies**, Fastify has **12**, and Hapi has **17**! These numbers don't even include their dependencies' dependencies. -Lambda API has **ZERO** dependencies. *None*. *Zip*. *Zilch*. +Lambda API has **ZERO** dependencies. _None_. _Zip_. _Zilch_. -Lambda API was written to be *extremely lightweight* and built specifically for **SERVERLESS** applications using AWS Lambda and API Gateway. It provides support for API routing, serving up HTML pages, issuing redirects, serving binary files and much more. Worried about observability? Lambda API has a built-in logging engine that can even periodically sample requests for things like tracing and benchmarking. It has a powerful middleware and error handling system, allowing you to implement just about anything you can dream of. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses **REQUESTS** and formats **RESPONSES**, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs. +Lambda API was written to be _extremely lightweight_ and built specifically for **SERVERLESS** applications using AWS Lambda and API Gateway. It provides support for API routing, serving up HTML pages, issuing redirects, serving binary files and much more. Worried about observability? Lambda API has a built-in logging engine that can even periodically sample requests for things like tracing and benchmarking. It has a powerful middleware and error handling system, allowing you to implement just about anything you can dream of. Best of all, it was designed to work with Lambda's Proxy Integration, automatically handling all the interaction with API Gateway for you. It parses **REQUESTS** and formats **RESPONSES**, allowing you to focus on your application's core functionality, instead of fiddling with inputs and outputs. ### Single Purpose Functions + You may have heard that a serverless "best practice" is to keep your functions small and limit them to a single purpose. I generally agree since building monolith applications is not what serverless was designed for. However, what exactly is a "single purpose" when it comes to building serverless APIs and web services? Should we create a separate function for our "create user" `POST` endpoint and then another one for our "update user" `PUT` endpoint? Should we create yet another function for our "delete user" `DELETE` endpoint? You certainly could, but that seems like a lot of repeated boilerplate code. On the other hand, you could create just one function that handled all your user management features. It may even make sense (in certain circumstances) to create one big serverless function handling several related components that can share your VPC database connections. Whatever you decide is best for your use case, **Lambda API** is there to support you. Whether your function has over a hundred routes, or just one, Lambda API's small size and lightning fast load time has virtually no impact on your function's performance. You can even define global wildcard routes that will process any incoming request, allowing you to use API Gateway or ALB to determine the routing. Yet despite its small footprint, it gives you the power of a full-featured web framework. ## Table of Contents + - [Simple Example](#simple-example) - [Why Another Web Framework?](#why-another-web-framework) - [Single Purpose Functions](#single-purpose-functions) @@ -120,27 +123,29 @@ Whatever you decide is best for your use case, **Lambda API** is there to suppor - [Are you using Lambda API?](#are-you-using-lambda-api) ## Installation + ``` npm i lambda-api --save ``` ## Requirements + - AWS Lambda running **Node 8.10+** - AWS API Gateway using [Proxy Integration](#lambda-proxy-integration) ## Configuration -Require the `lambda-api` module into your Lambda handler script and instantiate it. You can initialize the API with the following options: -| Property | Type | Description | -| -------- | ---- | ----------- | -| base | `String` | Base path for all routes, e.g. `base: 'v1'` would prefix all routes with `/v1` | -| callbackName | `String` | Override the default callback query parameter name for JSONP calls | -| logger | `boolean` or `object` | Enables default [logging](#logging) or allows for configuration through a [Logging Configuration](#logging-configuration) object. | -| mimeTypes | `Object` | Name/value pairs of additional MIME types to be supported by the `type()`. The key should be the file extension (without the `.`) and the value should be the expected MIME type, e.g. `application/json` | -| serializer | `Function` | Optional object serializer function. This function receives the `body` of a response and must return a string. Defaults to `JSON.stringify` | -| version | `String` | Version number accessible via the `REQUEST` object | -| errorHeaderWhitelist | `Array` | Array of headers to maintain on errors | +Require the `lambda-api` module into your Lambda handler script and instantiate it. You can initialize the API with the following options: +| Property | Type | Description | +| -------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| base | `String` | Base path for all routes, e.g. `base: 'v1'` would prefix all routes with `/v1` | +| callbackName | `String` | Override the default callback query parameter name for JSONP calls | +| logger | `boolean` or `object` | Enables default [logging](#logging) or allows for configuration through a [Logging Configuration](#logging-configuration) object. | +| mimeTypes | `Object` | Name/value pairs of additional MIME types to be supported by the `type()`. The key should be the file extension (without the `.`) and the value should be the expected MIME type, e.g. `application/json` | +| serializer | `Function` | Optional object serializer function. This function receives the `body` of a response and must return a string. Defaults to `JSON.stringify` | +| version | `String` | Version number accessible via the `REQUEST` object | +| errorHeaderWhitelist | `Array` | Array of headers to maintain on errors | ```javascript // Require the framework and instantiate it with optional version and base parameters @@ -148,14 +153,16 @@ const api = require('lambda-api')({ version: 'v1.0', base: 'v1' }); ``` ## Recent Updates + For detailed release notes see [Releases](https://github.com/jeremydaly/lambda-api/releases). ### v0.10: ALB support, method-based middleware, and multi-value headers and query string parameters + Lambda API now allows you to seamlessly switch between API Gateway and Application Load Balancers. New [execution stacks](execution-stacks) enables method-based middleware and more wildcard functionality. Plus full support for multi-value headers and query string parameters. ## Routes and HTTP Methods -Routes are defined by using convenience methods or the `METHOD` method. There are currently eight convenience route methods: `get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()` and `any()`. Convenience route methods require an optional *route* and one or more handler functions. A *route* is simply a path such as `/users`. If a *route* is not provided, then it will default to `/*` and will execute on every path. Handler functions accept a `REQUEST`, `RESPONSE`, and optional `next()` argument. These arguments can be named whatever you like, but convention dictates `req`, `res`, and `next`. +Routes are defined by using convenience methods or the `METHOD` method. There are currently eight convenience route methods: `get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()` and `any()`. Convenience route methods require an optional _route_ and one or more handler functions. A _route_ is simply a path such as `/users`. If a _route_ is not provided, then it will default to `/*` and will execute on every path. Handler functions accept a `REQUEST`, `RESPONSE`, and optional `next()` argument. These arguments can be named whatever you like, but convention dictates `req`, `res`, and `next`. Multiple handler functions can be assigned to a path, which can be used to execute middleware for specific paths and methods. For more information, see [Middleware](#middleware) and [Execution Stacks](#execution-stacks). @@ -189,7 +196,8 @@ api.post((req,res) => { }) ``` -Additional methods are support by calling `METHOD`. Arguments must include an HTTP method (or array of methods), an optional *route*, and one or more handler functions. Like the convenience methods above, handler functions accept a `REQUEST`, `RESPONSE`, and optional `next` argument. + +Additional methods are support by calling `METHOD`. Arguments must include an HTTP method (or array of methods), an optional _route_, and one or more handler functions. Like the convenience methods above, handler functions accept a `REQUEST`, `RESPONSE`, and optional `next` argument. ```javascript api.METHOD('trace','/users', (req,res) => { @@ -216,8 +224,12 @@ All `GET` methods have a `HEAD` alias that executes the `GET` request but return Routes that use the `any()` method or pass `ANY` to `api.METHOD` will respond to all HTTP methods. Routes that specify a specific method (such as `GET` or `POST`), will override the route for that method. For example: ```javascript -api.any('/users', (req,res) => { res.send('any') }) -api.get('/users', (req,res) => { res.send('get') }) +api.any('/users', (req, res) => { + res.send('any'); +}); +api.get('/users', (req, res) => { + res.send('get'); +}); ``` A `POST` to `/users` will return "any", but a `GET` request would return "get". Please note that routes defined with an `ANY` method will override default `HEAD` aliasing for `GET` routes. @@ -227,17 +239,17 @@ A `POST` to `/users` will return "any", but a `GET` request would return "get". Lambda API supports both `callback-style` and `async-await` for returning responses to users. The [RESPONSE](#response) object has several callbacks that will trigger a response (`send()`, `json()`, `html()`, etc.) You can use any of these callbacks from within route functions and middleware to send the response: ```javascript -api.get('/users', (req,res) => { - res.send({ foo: 'bar' }) -}) +api.get('/users', (req, res) => { + res.send({ foo: 'bar' }); +}); ``` You can also `return` data from route functions and middleware. The contents will be sent as the body: ```javascript -api.get('/users', (req,res) => { - return { foo: 'bar' } -}) +api.get('/users', (req, res) => { + return { foo: 'bar' }; +}); ``` ### Async/Await @@ -245,19 +257,21 @@ api.get('/users', (req,res) => { If you prefer to use `async/await`, you can easily apply this to your route functions. Using `return`: + ```javascript -api.get('/users', async (req,res) => { - let users = await getUsers() - return users -}) +api.get('/users', async (req, res) => { + let users = await getUsers(); + return users; +}); ``` Or using callbacks: + ```javascript -api.get('/users', async (req,res) => { - let users = await getUsers() - res.send(users) -}) +api.get('/users', async (req, res) => { + let users = await getUsers(); + res.send(users); +}); ``` ### Promises @@ -265,21 +279,21 @@ api.get('/users', async (req,res) => { If you like promises, you can either use a callback like `res.send()` at the end of your promise chain, or you can simply `return` the resolved promise: ```javascript -api.get('/users', (req,res) => { - getUsers().then(users => { - res.send(users) - }) -}) +api.get('/users', (req, res) => { + getUsers().then((users) => { + res.send(users); + }); +}); ``` OR ```javascript -api.get('/users', (req,res) => { - return getUsers().then(users => { - return users - }) -}) +api.get('/users', (req, res) => { + return getUsers().then((users) => { + return users; + }); +}); ``` **IMPORTANT:** You must either use a callback like `res.send()` **OR** `return` a value. Otherwise the execution will hang and no data will be sent to the user. Also, be sure not to return `undefined`, otherwise it will assume no response. @@ -289,27 +303,25 @@ api.get('/users', (req,res) => { While callbacks like `res.send()` and `res.error()` will trigger a response, they will not necessarily terminate execution of the current route function. Take a look at the following example: ```javascript -api.get('/users', (req,res) => { - +api.get('/users', (req, res) => { if (req.headers.test === 'test') { - res.error('Throw an error') + res.error('Throw an error'); } - return { foo: 'bar' } -}) + return { foo: 'bar' }; +}); ``` The example above would not have the intended result of displaying an error. `res.error()` would signal Lambda API to execute the error handling, but the function would continue to run. This would cause the function to `return` a response that would override the intended error. In this situation, you could either wrap the return in an `else` clause, or a cleaner approach would be to `return` the call to the `error()` method, like so: ```javascript -api.get('/users', (req,res) => { - +api.get('/users', (req, res) => { if (req.headers.test === 'test') { - return res.error('Throw an error') + return res.error('Throw an error'); } - return { foo: 'bar' } -}) + return { foo: 'bar' }; +}); ``` `res.error()` does not have a return value (meaning it is `undefined`). However, the `return` tells the function to stop executing, and the call to `res.error()` handles and formats the appropriate response. This will allow Lambda API to properly return the expected results. @@ -320,31 +332,32 @@ Lambda API makes it easy to create multiple versions of the same api without cha ```javascript // handler.js -const api = require('lambda-api')() +const api = require('lambda-api')(); -api.register(require('./routes/v1/products'), { prefix: '/v1' }) -api.register(require('./routes/v2/products'), { prefix: '/v2' }) +api.register(require('./routes/v1/products'), { prefix: '/v1' }); +api.register(require('./routes/v2/products'), { prefix: '/v2' }); module.exports.handler = (event, context, callback) => { - api.run(event, context, callback) -} + api.run(event, context, callback); +}; ``` ```javascript // routes/v1/products.js module.exports = (api, opts) => { - api.get('/product', handler_v1) -} + api.get('/product', handler_v1); +}; ``` ```javascript // routes/v2/products.js module.exports = (api, opts) => { - api.get('/product', handler_v2) -} + api.get('/product', handler_v2); +}; ``` Even though both modules create a `/product` route, Lambda API will add the `prefix` to them, creating two unique routes. Your users can now access: + - `/v1/product` - `/v2/product` @@ -352,9 +365,9 @@ You can use `register()` as many times as you want AND it is recursive, so if yo ```javascript module.exports = (api, opts) => { - api.get('/product', handler_v1) - api.register(require('./v2/products.js'), { prefix: '/v2'} ) -} + api.get('/product', handler_v1); + api.register(require('./v2/products.js'), { prefix: '/v2' }); +}; ``` This would create a `/v1/product` and `/v1/v2/product` route. You can also use `register()` to load routes from an external file without the `prefix`. This will just add routes to your `base` path. **NOTE:** Prefixed routes are built off of your `base` path if one is set. If your `base` was set to `/api`, then the first example above would produce the routes: `/api/v1/product` and `/api/v2/product`. @@ -364,12 +377,12 @@ This would create a `/v1/product` and `/v1/v2/product` route. You can also use ` Lambda API has a `routes()` method that can be called on the main instance that will return an array containing the `METHOD` and full `PATH` of every configured route. This will include base paths and prefixed routes. This is helpful for debugging your routes. ```javascript - const api = require('lambda-api')() +const api = require('lambda-api')(); - api.get('/', (req,res) => {}) - api.post('/test', (req,res) => {}) +api.get('/', (req, res) => {}); +api.post('/test', (req, res) => {}); - api.routes() // => [ [ 'GET', '/' ], [ 'POST', '/test' ] ] +api.routes(); // => [ [ 'GET', '/' ], [ 'POST', '/test' ] ] ``` You can also log the paths in table form to the console by passing in `true` as the only parameter. @@ -392,7 +405,6 @@ You can also log the paths in table form to the console by passing in `true` as ╚═══════════╧═════════════════╝ ``` - ## REQUEST The `REQUEST` object contains a parsed and normalized request from API Gateway. It contains the following values by default: @@ -438,26 +450,29 @@ The request object can be used to pass additional information through the proces The `RESPONSE` object is used to send a response back to the API Gateway. The `RESPONSE` object contains several methods to manipulate responses. All methods are chainable unless they trigger a response. ### status(code) + The `status` method allows you to set the status code that is returned to API Gateway. By default this will be set to `200` for normal requests or `500` on a thrown error. Additional built-in errors such as `404 Not Found` and `405 Method Not Allowed` may also be returned. The `status()` method accepts a single integer argument. ```javascript -api.get('/users', (req,res) => { - res.status(304).send('Not Modified') -}) +api.get('/users', (req, res) => { + res.status(304).send('Not Modified'); +}); ``` ### sendStatus(code) + The `sendStatus` method sets the status code and returns its string representation as the response body. The `sendStatus()` method accepts a single integer argument. ```javascript -res.sendStatus(200) // equivalent to res.status(200).send('OK') -res.sendStatus(304) // equivalent to res.status(304).send('Not Modified') -res.sendStatus(403) // equivalent to res.status(403).send('Forbidden') +res.sendStatus(200); // equivalent to res.status(200).send('OK') +res.sendStatus(304); // equivalent to res.status(304).send('Not Modified') +res.sendStatus(403); // equivalent to res.status(403).send('Forbidden') ``` **NOTE:** If an unsupported status code is provided, it will return 'Unknown' as the body. ### header(key, value [,append]) + The `header` method allows for you to set additional headers to return to the client. By default, just the `content-type` header is sent with `application/json` as the value. Headers can be added or overwritten by calling the `header()` method with two string arguments. The first is the name of the header and then second is the value. You can utilize multi-value headers by specifying an array with multiple values as the `value`, or you can use an optional third boolean parameter and append multiple headers. ```javascript @@ -481,59 +496,67 @@ api.get('/users', (req,res) => { **NOTE:** Header keys are converted and stored as lowercase in compliance with [rfc7540 8.1.2. HTTP Header Fields](https://tools.ietf.org/html/rfc7540) for HTTP/2. Header convenience methods (`getHeader`, `hasHeader`, and `removeHeader`) automatically ignore case. ### getHeader(key [,asArray]) + Retrieve a specific header value. `key` is case insensitive. By default (and for backwards compatibility), header values are returned as a `string`. Multi-value headers will be concatenated using a comma (see [rfc2616 4.2. Message Headers](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2)). An optional second boolean parameter can be passed to return header values as an `array`. **NOTE:** The ability to retrieve the current header object by calling `getHeader()` is still possible, but the preferred method is to use the `getHeaders()` method. By default, `getHeader()` will return the object with `string` values. ### getHeaders() + Retrieve the current header object. Values are returned as `array`s. ### hasHeader(key) + Returns a boolean indicating the existence of `key` in the response headers. `key` is case insensitive. ### removeHeader(key) + Removes header matching `key` from the response headers. `key` is case insensitive. This method is chainable. ### getLink(s3Path [, expires] [, callback]) + This returns a signed URL to the referenced file in S3 (using the `s3://{my-bucket}/{path-to-file}` format). You can optionally pass in an integer as the second parameter that will changed the default expiration time of the link. The expiration time is in seconds and defaults to `900`. In order to ensure proper URL signing, the `getLink()` must be asynchronous, and therefore returns a promise. You must either `await` the result or use a `.then` to retrieve the value. There is an optional third parameter that takes an error handler callback. If the underlying `getSignedUrl()` call fails, the error will be returned using the standard `res.error()` method. You can override this by providing your own callback. ```javascript // async/await -api.get('/getLink', async (req,res) => { - let url = await res.getLink('s3://my-bucket/my-file.pdf') - return { link: url } -}) +api.get('/getLink', async (req, res) => { + let url = await res.getLink('s3://my-bucket/my-file.pdf'); + return { link: url }; +}); // promises -api.get('/getLink', (req,res) => { - res.getLink('s3://my-bucket/my-file.pdf').then(url => { - res.json({ link: url }) - }) -}) +api.get('/getLink', (req, res) => { + res.getLink('s3://my-bucket/my-file.pdf').then((url) => { + res.json({ link: url }); + }); +}); ``` ### send(body) + The `send` methods triggers the API to return data to the API Gateway. The `send` method accepts one parameter and sends the contents through as is, e.g. as an object, string, integer, etc. AWS Gateway expects a string, so the data should be converted accordingly. ### json(body) + There is a `json` convenience method for the `send` method that will set the headers to `application/json` as well as perform `JSON.stringify()` on the contents passed to it. ```javascript -api.get('/users', (req,res) => { - res.json({ message: 'This will be converted automatically' }) -}) +api.get('/users', (req, res) => { + res.json({ message: 'This will be converted automatically' }); +}); ``` ### jsonp(body) + There is a `jsonp` convenience method for the `send` method that will set the headers to `application/json`, perform `JSON.stringify()` on the contents passed to it, and wrap the results in a callback function. By default, the callback function is named `callback`. ```javascript -res.jsonp({ foo: 'bar' }) +res.jsonp({ foo: 'bar' }); // => callback({ "foo": "bar" }) -res.status(500).jsonp({ error: 'some error'}) +res.status(500).jsonp({ error: 'some error' }); // => callback({ "error": "some error" }) ``` @@ -541,7 +564,7 @@ The default can be changed by passing in `callback` as a URL parameter, e.g. `?c ```javascript // ?callback=foo -res.jsonp({ foo: 'bar' }) +res.jsonp({ foo: 'bar' }); // => foo({ "foo": "bar" }) ``` @@ -551,69 +574,78 @@ You can change the default URL parameter using the optional `callback` option wh const api = require('lambda-api')({ callback: 'cb' }); // ?cb=bar -res.jsonp({ foo: 'bar' }) +res.jsonp({ foo: 'bar' }); // => bar({ "foo": "bar" }) ``` ### html(body) + There is also an `html` convenience method for the `send` method that will set the headers to `text/html` and pass through the contents. ```javascript -api.get('/users', (req,res) => { - res.html('
This is HTML
') -}) +api.get('/users', (req, res) => { + res.html('
This is HTML
'); +}); ``` ### type(type) + Sets the `content-type` header for you based on a single `String` input. There are thousands of MIME types, many of which are likely never to be used by your application. Lambda API stores a list of the most popular file types and will automatically set the correct `content-type` based on the input. If the `type` contains the "/" character, then it sets the `content-type` to the value of `type`. ```javascript -res.type('.html'); // => 'text/html' -res.type('html'); // => 'text/html' -res.type('json'); // => 'application/json' -res.type('application/json'); // => 'application/json' -res.type('png'); // => 'image/png' -res.type('.doc'); // => 'application/msword' -res.type('text/css'); // => 'text/css' +res.type('.html'); // => 'text/html' +res.type('html'); // => 'text/html' +res.type('json'); // => 'application/json' +res.type('application/json'); // => 'application/json' +res.type('png'); // => 'image/png' +res.type('.doc'); // => 'application/msword' +res.type('text/css'); // => 'text/css' ``` For a complete list of auto supported types, see [mimemap.js](lib/mindmap.js). Custom MIME types can be added by using the `mimeTypes` option when instantiating Lambda API ### location(path) + The `location` convenience method sets the `Location:` header with the value of a single string argument. The value passed in is not validated but will be encoded before being added to the header. Values that are already encoded can be safely passed in. Note that a valid `3xx` status code must be set to trigger browser redirection. The value can be a relative/absolute path OR a FQDN. ```javascript -api.get('/redirectToHome', (req,res) => { - res.location('/home').status(302).html('
Redirect to Home
') -}) +api.get('/redirectToHome', (req, res) => { + res.location('/home').status(302).html('
Redirect to Home
'); +}); -api.get('/redirectToGithub', (req,res) => { - res.location('https://github.com').status(302).html('
Redirect to GitHub
') -}) +api.get('/redirectToGithub', (req, res) => { + res + .location('https://github.com') + .status(302) + .html('
Redirect to GitHub
'); +}); ``` ### redirect([status,] path) + The `redirect` convenience method triggers a redirection and ends the current API execution. This method is similar to the `location()` method, but it automatically sets the status code and calls `send()`. The redirection URL (relative/absolute path, a FQDN, or an S3 path reference) can be specified as the only parameter or as a second parameter when a valid `3xx` status code is supplied as the first parameter. The status code is set to `302` by default, but can be changed to `300`, `301`, `302`, `303`, `307`, or `308` by adding it as the first parameter. ```javascript -api.get('/redirectToHome', (req,res) => { - res.redirect('/home') -}) +api.get('/redirectToHome', (req, res) => { + res.redirect('/home'); +}); -api.get('/redirectToGithub', (req,res) => { - res.redirect(301,'https://github.com') -}) +api.get('/redirectToGithub', (req, res) => { + res.redirect(301, 'https://github.com'); +}); // This will redirect a signed URL using the getLink method -api.get('/redirectToS3File', (req,res) => { - res.redirect('s3://my-bucket/someFile.pdf') -}) +api.get('/redirectToS3File', (req, res) => { + res.redirect('s3://my-bucket/someFile.pdf'); +}); ``` ### cors([options]) + Convenience method for adding [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) headers to responses. An optional `options` object can be passed in to customize the defaults. The six defined **CORS** headers are as follows: + - Access-Control-Allow-Origin (defaults to `*`) - Access-Control-Allow-Methods (defaults to `GET, PUT, POST, DELETE, OPTIONS`) - Access-Control-Allow-Headers (defaults to `Content-Type, Authorization, Content-Length, X-Requested-With`) @@ -622,12 +654,13 @@ The six defined **CORS** headers are as follows: - Access-Control-Allow-Credentials The `options` object can contain the following properties that correspond to the above headers: -- origin *(string)* -- methods *(string)* -- headers *(string)* -- exposeHeaders *(string)* -- maxAge *(number in milliseconds)* -- credentials *(boolean)* + +- origin _(string)_ +- methods _(string)_ +- headers _(string)_ +- exposeHeaders _(string)_ +- maxAge _(number in milliseconds)_ +- credentials _(boolean)_ Defaults can be set by calling `res.cors()` with no properties, or with any combination of the above options. @@ -636,101 +669,123 @@ res.cors({ origin: 'example.com', methods: 'GET, POST, OPTIONS', headers: 'content-type, authorization', - maxAge: 84000000 -}) + maxAge: 84000000, +}); ``` You can override existing values by calling `res.cors()` with just the updated values: ```javascript res.cors({ - origin: 'api.example.com' -}) + origin: 'api.example.com', +}); ``` ### error([code], message [,detail]) + An error can be triggered by calling the `error` method. This will cause the API to stop execution and return the message to the client. The status code can be set by optionally passing in an integer as the first parameter. Additional detail can be added as an optional third parameter (or second parameter if no status code is passed). This will add an additional `detail` property to error logs. Details accepts any value that can be serialized by `JSON.stringify` including objects, strings and arrays. Custom error handling can be accomplished using the [Error Handling](#error-handling) feature. ```javascript -api.get('/users', (req,res) => { - res.error('This is an error') -}) +api.get('/users', (req, res) => { + res.error('This is an error'); +}); -api.get('/users', (req,res) => { - res.error(403,'Not authorized') -}) +api.get('/users', (req, res) => { + res.error(403, 'Not authorized'); +}); -api.get('/users', (req,res) => { - res.error('Error', { foo: 'bar' }) -}) +api.get('/users', (req, res) => { + res.error('Error', { foo: 'bar' }); +}); -api.get('/users', (req,res) => { - res.error(404, 'Page not found', 'foo bar') -}) +api.get('/users', (req, res) => { + res.error(404, 'Page not found', 'foo bar'); +}); ``` ### cookie(name, value [,options]) Convenience method for setting cookies. This method accepts a `name`, `value` and an optional `options` object with the following parameters: -| Property | Type | Description | -| -------- | ---- | ----------- | -| domain | `String` | Domain name to use for the cookie. This defaults to the current domain. | -| expires | `Date` | The expiration date of the cookie. Local dates will be converted to GMT. Creates session cookie if this value is not specified. | -| httpOnly | `Boolean` | Sets the cookie to be accessible only via a web server, not JavaScript. | -| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `expires` property if not explicitly provided. | -| path | `String` | Path for the cookie. Defaults to "/" for the root directory. | -| secure | `Boolean` | Sets the cookie to be used with HTTPS only. | -|sameSite | `Boolean` or `String` | Sets the SameSite value for cookie. `true` or `false` sets `Strict` or `Lax` respectively. Also allows a string value. See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 | +| Property | Type | Description | +| -------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| domain | `String` | Domain name to use for the cookie. This defaults to the current domain. | +| expires | `Date` | The expiration date of the cookie. Local dates will be converted to GMT. Creates session cookie if this value is not specified. | +| httpOnly | `Boolean` | Sets the cookie to be accessible only via a web server, not JavaScript. | +| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `expires` property if not explicitly provided. | +| path | `String` | Path for the cookie. Defaults to "/" for the root directory. | +| secure | `Boolean` | Sets the cookie to be used with HTTPS only. | +| sameSite | `Boolean` or `String` | Sets the SameSite value for cookie. `true` or `false` sets `Strict` or `Lax` respectively. Also allows a string value. See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1 | The `name` attribute should be a string (auto-converted if not), but the `value` attribute can be any type of value. The `value` will be serialized (if an object, array, etc.) and then encoded using `encodeURIComponent` for safely assigning the cookie value. Cookies are automatically parsed, decoded, and available via the `REQUEST` object (see [REQUEST](#request)). **NOTE:** The `cookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response. ```javascript -res.cookie('foo', 'bar', { maxAge: 3600*1000, secure: true }).send() -res.cookie('fooObject', { foo: 'bar' }, { domain: '.test.com', path: '/admin', httpOnly: true }).send() -res.cookie('fooArray', [ 'one', 'two', 'three' ], { path: '/', httpOnly: true }).send() +res.cookie('foo', 'bar', { maxAge: 3600 * 1000, secure: true }).send(); +res + .cookie( + 'fooObject', + { foo: 'bar' }, + { domain: '.test.com', path: '/admin', httpOnly: true } + ) + .send(); +res + .cookie('fooArray', ['one', 'two', 'three'], { path: '/', httpOnly: true }) + .send(); ``` ### clearCookie(name [,options]) + Convenience method for expiring cookies. Requires the `name` and optional `options` object as specified in the [cookie](#cookiename-value-options) method. This method will automatically set the expiration time. However, most browsers require the same options to clear a cookie as was used to set it. E.g. if you set the `path` to "/admin" when you set the cookie, you must use this same value to clear it. ```javascript -res.clearCookie('foo', { secure: true }).send() -res.clearCookie('fooObject', { domain: '.test.com', path: '/admin', httpOnly: true }).send() -res.clearCookie('fooArray', { path: '/', httpOnly: true }).send() +res.clearCookie('foo', { secure: true }).send(); +res + .clearCookie('fooObject', { + domain: '.test.com', + path: '/admin', + httpOnly: true, + }) + .send(); +res.clearCookie('fooArray', { path: '/', httpOnly: true }).send(); ``` + **NOTE:** The `clearCookie()` method only sets the header. A execution ending method like `send()`, `json()`, etc. must be called to send the response. ### etag([boolean]) + Enables Etag generation for the response if at value of `true` is passed in. Lambda API will generate an Etag based on the body of the response and return the appropriate header. If the request contains an `If-No-Match` header that matches the generated Etag, a `304 Not Modified` response will be returned with a blank body. ### cache([age] [, private]) + Adds `cache-control` header to responses. If the first parameter is an `integer`, it will add a `max-age` to the header. The number should be in milliseconds. If the first parameter is `true`, it will add the cache headers with `max-age` set to `0` and use the current time for the `expires` header. If set to false, it will add a cache header with `no-cache, no-store, must-revalidate` as the value. You can also provide a custom string that will manually set the value of the `cache-control` header. And optional second argument takes a `boolean` and will set the `cache-control` to `private` This method is chainable. ```javascript -res.cache(false).send() // 'cache-control': 'no-cache, no-store, must-revalidate' -res.cache(1000).send() // 'cache-control': 'max-age=1' -res.cache(30000,true).send() // 'cache-control': 'private, max-age=30' +res.cache(false).send(); // 'cache-control': 'no-cache, no-store, must-revalidate' +res.cache(1000).send(); // 'cache-control': 'max-age=1' +res.cache(30000, true).send(); // 'cache-control': 'private, max-age=30' ``` ### modified(date) + Adds a `last-modified` header to responses. A value of `true` will set the value to the current date and time. A JavaScript `Date` object can also be passed in. Note that it will be converted to UTC if not already. A `string` can also be passed in and will be converted to a date if JavaScript's `Date()` function is able to parse it. A value of `false` will prevent the header from being generated, but will not remove any existing `last-modified` headers. ### attachment([filename]) + Sets the HTTP response `content-disposition` header field to "attachment". If a `filename` is provided, then the `content-type` is set based on the file extension using the `type()` method and the "filename=" parameter is added to the `content-disposition` header. ```javascript -res.attachment() +res.attachment(); // content-disposition: attachment -res.attachment('path/to/logo.png') +res.attachment('path/to/logo.png'); // content-disposition: attachment; filename="logo.png" // content-type: image/png ``` ### download(file [, filename] [, options] [, callback]) + This transfers the `file` (either a local path, S3 file reference, or Javascript `Buffer`) as an "attachment". This is a convenience method that combines `attachment()` and `sendFile()` to prompt the user to download the file. This method optionally takes a `filename` as a second parameter that will overwrite the "filename=" parameter of the `content-disposition` header, otherwise it will use the filename from the `file`. An optional `options` object passes through to the [sendFile()](#sendfilefile--options--callback) method and takes the same parameters. Finally, a optional `callback` method can be defined which is passed through to [sendFile()](#sendfilefile--options--callback) as well. ```javascript @@ -748,16 +803,17 @@ res.download(, 'my-file.docx', { maxAge: 3600000 }, (err) => { ``` ### sendFile(file [, options] [, callback]) + The `sendFile()` method takes up to three arguments. The first is the `file`. This is either a local filename (stored within your uploaded lambda code), a reference to a file in S3 (using the `s3://{my-bucket}/{path-to-file}` format), or a JavaScript `Buffer`. You can optionally pass an `options` object using the properties below as well as a callback function `callback(err)` that can handle custom errors or manipulate the response before sending to the client. -| Property | Type | Description | Default | -| -------- | ---- | ----------- | ------- | -| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `Expires` header | 0 | -| root | `String` | Root directory for relative filenames. | | -| lastModified | `Boolean` or `String` | Sets the `last-modified` header to the last modified date of the file. This can be disabled by setting it to `false`, or overridden by setting it to a valid `Date` object | | -| headers | `Object` | Key value pairs of additional headers to be sent with the file | | -| cacheControl | `Boolean` or `String` | Enable or disable setting `cache-control` response header. Override value with custom string. | true | -| private | `Boolean` | Sets the `cache-control` to `private`. | false | +| Property | Type | Description | Default | +| ------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| maxAge | `Number` | Set the expiration time relative to the current time in milliseconds. Automatically sets the `Expires` header | 0 | +| root | `String` | Root directory for relative filenames. | | +| lastModified | `Boolean` or `String` | Sets the `last-modified` header to the last modified date of the file. This can be disabled by setting it to `false`, or overridden by setting it to a valid `Date` object | | +| headers | `Object` | Key value pairs of additional headers to be sent with the file | | +| cacheControl | `Boolean` or `String` | Enable or disable setting `cache-control` response header. Override value with custom string. | true | +| private | `Boolean` | Sets the `cache-control` to `private`. | false | ```javascript res.sendFile('./img/logo.png') @@ -773,25 +829,27 @@ res.sendFile(, 'my-file.docx', { maxAge: 3600000 }, (err) => { }) ``` -The `callback` function supports returning a promise, allowing you to perform additional tasks *after* the file is successfully loaded from the source. This can be used to perform additional synchronous tasks before returning control to the API execution. +The `callback` function supports returning a promise, allowing you to perform additional tasks _after_ the file is successfully loaded from the source. This can be used to perform additional synchronous tasks before returning control to the API execution. **NOTE:** In order to access S3 files, your Lambda function must have `GetObject` access to the files you're attempting to access. See [Enabling Binary Support](#enabling-binary-support) for more information. ## Enabling Binary Support + To enable binary support, you need to add `*/*` under "Binary Media Types" in **API Gateway** -> **APIs** -> **[ your api ]** -> **Settings**. This will also `base64` encode all body content, but Lambda API will automatically decode it for you. ![Binary Media Types](http://jeremydaly.com//lambda-api/binary-media-types.png) -*Add* `*/*` *to Binary Media Types* +_Add_ `*/*` _to Binary Media Types_ ## Path Parameters + Path parameters are extracted from the path sent in by API Gateway. Although API Gateway supports path parameters, the API doesn't use these values but insteads extracts them from the actual path. This gives you more flexibility with the API Gateway configuration. Path parameters are defined in routes using a colon `:` as a prefix. ```javascript -api.get('/users/:userId', (req,res) => { - res.send('User ID: ' + req.params.userId) -}) +api.get('/users/:userId', (req, res) => { + res.send('User ID: ' + req.params.userId); +}); ``` Path parameters act as wildcards that capture the value into the `params` object. The example above would match `/users/123` and `/users/test`. The system always looks for static paths first, so if you defined paths for `/users/test` and `/users/:userId`, exact path matches would take precedence. Path parameters only match the part of the path they are defined on. E.g. `/users/456/test` would not match `/users/:userId`. You would either need to define `/users/:userId/test` as its own path, or create another path with an additional path parameter, e.g. `/users/:userId/:anotherParam`. @@ -799,56 +857,60 @@ Path parameters act as wildcards that capture the value into the `params` object A path can contain as many parameters as you want. E.g. `/users/:param1/:param2/:param3`. ## Wildcard Routes -Wildcard routes are supported for matching arbitrary paths. Wildcards only work at the *end of a route definition* such as `/*` or `/users/*`. Wildcards within a path, e.g. `/users/*/posts` are not supported. Wildcard routes do support parameters, however, so `/users/:id/*` would capture the `:id` parameter in your wildcard handler. + +Wildcard routes are supported for matching arbitrary paths. Wildcards only work at the _end of a route definition_ such as `/*` or `/users/*`. Wildcards within a path, e.g. `/users/*/posts` are not supported. Wildcard routes do support parameters, however, so `/users/:id/*` would capture the `:id` parameter in your wildcard handler. Wildcard routes will match any deep paths after the wildcard. For example, a `GET` method for path `/users/*` would match `/users/1/posts/latest`. The only exception is for the `OPTIONS` method. A path **must** exist for a wildcard on an `OPTIONS` route in order to execute the handler. If a wildcard route is defined for another method higher up the path, then the `OPTIONS` handler will fire. For example, if there was a `POST` method defined on `/users/*`, then an `OPTIONS` method for `/users/2/posts/*` would fire as it assumes that the `POST` path would exist. In most cases, [Path Parameters](#path-parameters) should be used in favor of wildcard routes. However, if you need to support unpredictable path lengths, or your are building single purpose functions and will be mapping routes from API Gateway, the wildcards are a powerful pattern. Another good use case is to use the `OPTIONS` method to provide CORS headers. ```javascript -api.options('/*', (req,res) => { +api.options('/*', (req, res) => { // Return CORS headers - res.cors().send({}) -}) + res.cors().send({}); +}); ``` ## Logging + Lambda API includes a robust logging engine specifically designed to utilize native JSON support for CloudWatch Logs. Not only is it ridiculously fast, but it's also highly configurable. Logging is disabled by default, but can be enabled by passing `{ logger: true }` when you create the Lambda API instance (or by passing a [Logging Configuration](#logging-configuration) definition). The logger is attached to the `REQUEST` object and can be used anywhere the object is available (e.g. routes, middleware, and error handlers). ```javascript -const api = require('lambda-api')({ logger: true }) +const api = require('lambda-api')({ logger: true }); -api.get('/status', (req,res) => { - req.log.info('Some info about this route') - res.send({ status: 'ok' }) -}) +api.get('/status', (req, res) => { + req.log.info('Some info about this route'); + res.send({ status: 'ok' }); +}); ``` In addition to manual logging, Lambda API can also generate "access" logs for your API requests. API Gateway can also provide access logs, but they are limited to contextual information about your request (see [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)). Lambda API allows you to capture the same data **PLUS** additional information directly from within your application. ### Logging Configuration + Logging can be enabled by setting the `logger` option to `true` when creating the Lambda API instance. Logging can be configured by setting `logger` to an object that contains configuration information. The following table contains available logging configuration properties. -| Property | Type | Description | Default | -| -------- | ---- | ----------- | ------- | -| access | `boolean` or `string` | Enables/disables automatic access log generation for each request. See [Access Logs](#access-logs). | `false` | -| errorLogging | `boolean` | Enables/disables automatic error logging. | `true` | -| customKey | `string` | Sets the JSON property name for custom data passed to logs. | `custom` | -| detail | `boolean` | Enables/disables adding `REQUEST` and `RESPONSE` data to all log entries. | `false` | -| level | `string` | Minimum logging level to send logs for. See [Logging Levels](#logging-levels). | `info` | -| levels | `object` | Key/value pairs of custom log levels and their priority. See [Custom Logging Levels](#custom-logging-levels). | | -| log | `function` | Custom function for overriding standard `console.log`. | `console.log` | -| messageKey | `string` | Sets the JSON property name of the log "message". | `msg` | -| multiValue | `boolean` | Enables multi-value support for querystrings. If enabled, the `qs` parameter will return all values as `array`s and will include multiple values if they exist. | `false` | -| nested | `boolean` | Enables/disables nesting of JSON logs for serializer data. See [Serializers](#serializers). | `false` | -| timestamp | `boolean` or `function` | By default, timestamps will return the epoch time in milliseconds. A value of `false` disables log timestamps. A function that returns a value can be used to override the default format. | `true` | -| sampling | `object` | Enables log sampling for periodic request tracing. See [Sampling](#sampling). | | -| serializers | `object` | Adds serializers that manipulate the log format. See [Serializers](#serializers). | | -| stack | `boolean` | Enables/disables the inclusion of stack traces in caught errors. | `false` | +| Property | Type | Description | Default | +| ------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | +| access | `boolean` or `string` | Enables/disables automatic access log generation for each request. See [Access Logs](#access-logs). | `false` | +| errorLogging | `boolean` | Enables/disables automatic error logging. | `true` | +| customKey | `string` | Sets the JSON property name for custom data passed to logs. | `custom` | +| detail | `boolean` | Enables/disables adding `REQUEST` and `RESPONSE` data to all log entries. | `false` | +| level | `string` | Minimum logging level to send logs for. See [Logging Levels](#logging-levels). | `info` | +| levels | `object` | Key/value pairs of custom log levels and their priority. See [Custom Logging Levels](#custom-logging-levels). | | +| log | `function` | Custom function for overriding standard `console.log`. | `console.log` | +| messageKey | `string` | Sets the JSON property name of the log "message". | `msg` | +| multiValue | `boolean` | Enables multi-value support for querystrings. If enabled, the `qs` parameter will return all values as `array`s and will include multiple values if they exist. | `false` | +| nested | `boolean` | Enables/disables nesting of JSON logs for serializer data. See [Serializers](#serializers). | `false` | +| timestamp | `boolean` or `function` | By default, timestamps will return the epoch time in milliseconds. A value of `false` disables log timestamps. A function that returns a value can be used to override the default format. | `true` | +| sampling | `object` | Enables log sampling for periodic request tracing. See [Sampling](#sampling). | | +| serializers | `object` | Adds serializers that manipulate the log format. See [Serializers](#serializers). | | +| stack | `boolean` | Enables/disables the inclusion of stack traces in caught errors. | `false` | Example: + ```javascript const api = require('lambda-api')({ logger: { @@ -857,15 +919,17 @@ const api = require('lambda-api')({ customKey: 'detail', messageKey: 'message', timestamp: () => new Date().toUTCString(), // custom timestamp - stack: true - } -}) + stack: true, + }, +}); ``` ### Log Format + Logs are generated using Lambda API's standard JSON format. The log format can be customized using [Serializers](#serializers). **Standard log format (manual logging):** + ```javascript { "level": "info", // log level @@ -885,11 +949,13 @@ Logs are generated using Lambda API's standard JSON format. The log format can b ``` ### Access Logs -Access logs generate detailed information about the API request. Access logs are disabled by default, but can be enabled by setting the `access` property to `true` in the logging configuration object. If set to `false`, access logs will *only* be generated when other log entries (`info`, `error`, etc.) are created. If set to the string `'never'`, access logs will never be generated. + +Access logs generate detailed information about the API request. Access logs are disabled by default, but can be enabled by setting the `access` property to `true` in the logging configuration object. If set to `false`, access logs will _only_ be generated when other log entries (`info`, `error`, etc.) are created. If set to the string `'never'`, access logs will never be generated. Access logs use the same format as the standard logs above, but include additional information about the request. The access log format can be customized using [Serializers](#serializers). **Access log format (automatic logging):** + ```javascript { ... Standard Log Data ..., @@ -906,65 +972,72 @@ Access logs use the same format as the standard logs above, but include addition ``` ### Logging Levels + Logging "levels" allow you to add detailed logging to your functions based on severity. There are six standard log levels as specified in the table below along with their default priority. -| Level | Priority | -| -------- | ------- | -| `trace` | 10 | -| `debug` | 20 | -| `info` | 30 | -| `warn` | 40 | -| `error` | 50 | -| `fatal` | 60 | +| Level | Priority | +| ------- | -------- | +| `trace` | 10 | +| `debug` | 20 | +| `info` | 30 | +| `warn` | 40 | +| `error` | 50 | +| `fatal` | 60 | -Logs are written to CloudWatch Logs *ONLY* if they are the same or higher severity than specified in the `level` log configuration. +Logs are written to CloudWatch Logs _ONLY_ if they are the same or higher severity than specified in the `level` log configuration. ```javascript // Logging level set to "warn" -const api = require('lambda-api')({ logger: { level: 'warn' } }) - -api.get('/', (req,res) => { - req.log.trace('trace log message') // ignored - req.log.debug('debug log message') // ignored - req.log.info('info log message') // ignored - req.log.warn('warn log message') // write to CloudWatch - req.log.error('error log message') // write to CloudWatch - req.log.fatal('fatal log message') // write to CloudWatch - res.send({ hello: 'world' }) -}) +const api = require('lambda-api')({ logger: { level: 'warn' } }); + +api.get('/', (req, res) => { + req.log.trace('trace log message'); // ignored + req.log.debug('debug log message'); // ignored + req.log.info('info log message'); // ignored + req.log.warn('warn log message'); // write to CloudWatch + req.log.error('error log message'); // write to CloudWatch + req.log.fatal('fatal log message'); // write to CloudWatch + res.send({ hello: 'world' }); +}); ``` ### Custom Logging Levels + Custom logging "levels" can be added by specifying an object containing "level names" as keys and their priorities as values. You can also adjust the priority of standard levels by adding it to the object. ```javascript const api = require('lambda-api')({ logger: { levels: { - 'test': 5, // low priority 'test' level - 'customLevel': 35, // between info and warn - 'trace': 70 // set trace to the highest priority - } - } -}) + test: 5, // low priority 'test' level + customLevel: 35, // between info and warn + trace: 70, // set trace to the highest priority + }, + }, +}); ``` In the example above, the `test` level would only generate logs if the priority was set to `test`. `customLevel` would generate logs if `level` was set to anything with the same or lower priority (e.g. `info`). `trace` now has the highest priority and would generate a log entry no matter what the level was set to. ### Adding Additional Detail -Manual logging also allows you to specify additional detail with each log entry. Details can be added by suppling *any variable type* as a second parameter to the logger function. + +Manual logging also allows you to specify additional detail with each log entry. Details can be added by suppling _any variable type_ as a second parameter to the logger function. ```javascript -req.log.info('This is the main log message','some other detail') // string -req.log.info('This is the main log message',{ foo: 'bar', isAuthorized: someVar }) // object -req.log.info('This is the main log message',25) // number -req.log.info('This is the main log message',['val1','val2','val3']) // array -req.log.info('This is the main log message',true) // boolean +req.log.info('This is the main log message', 'some other detail'); // string +req.log.info('This is the main log message', { + foo: 'bar', + isAuthorized: someVar, +}); // object +req.log.info('This is the main log message', 25); // number +req.log.info('This is the main log message', ['val1', 'val2', 'val3']); // array +req.log.info('This is the main log message', true); // boolean ``` If an `object` is provided, the keys will be merged into the main log entry's JSON. If any other `type` is provided, the value will be assigned to a key using the the `customKey` setting as its property name. If `nested` is set to `true`, objects will be nested under the value of `customKey` as well. ### Serializers + Serializers allow you to customize log formats as well as add additional data from your application. Serializers can be defined by adding a `serializers` property to the `logger` configuration object. A property named for an available serializer (`main`, `req`, `res`, `context` or `custom`) needs to return an anonymous function that takes one argument and returns an object. The returned object will be merged into the main JSON log entry. Existing properties can be removed by returning `undefined` as their values. ```javascript @@ -975,28 +1048,29 @@ const api = require('lambda-api')({ return { apiId: req.requestContext.apiId, // add the apiId stage: req.requestContext.stage, // add the stage - qs: undefined // remove the query string - } - } - } - } -}) + qs: undefined, // remove the query string + }; + }, + }, + }, +}); ``` -Serializers are passed one argument that contains their corresponding object. `req` *and* `main` receive the `REQUEST` object, `res` receives the `RESPONSE` object, `context` receives the `context` object passed into the main `run` function, and `custom` receives custom data passed in to the logging methods. Note that only custom `objects` will trigger the `custom` serializer. +Serializers are passed one argument that contains their corresponding object. `req` _and_ `main` receive the `REQUEST` object, `res` receives the `RESPONSE` object, `context` receives the `context` object passed into the main `run` function, and `custom` receives custom data passed in to the logging methods. Note that only custom `objects` will trigger the `custom` serializer. If the `nested` option is set to true in the `logger` configuration, then JSON log entries will be generated with properties for `req`, `res`, `context` and `custom` with their serialized data as nested objects. ### Sampling + Sampling allows you to periodically generate log entries for all possible severities within a single request execution. All of the log entries will be written to CloudWatch Logs and can be used to trace an entire request. This can be used for debugging, metric samples, resource response time sampling, etc. -Sampling can be enabled by adding a `sampling` property to the `logger` configuration object. A value of `true` will enable the default sampling rule. The default can be changed by passing in a configuration object with the following available *optional* properties: +Sampling can be enabled by adding a `sampling` property to the `logger` configuration object. A value of `true` will enable the default sampling rule. The default can be changed by passing in a configuration object with the following available _optional_ properties: -| Property | Type | Description | Default | -| -------- | ---- | ----------- | ------- | -| target | `number` | The minimum number of samples per `period`. | 1 | -| rate | `number` | The percentage of samples to be taken during the `period`. | 0.1 | -| period | `number` | Number of **seconds** representing the duration of each sampling period. | 60 | +| Property | Type | Description | Default | +| -------- | -------- | ------------------------------------------------------------------------ | ------- | +| target | `number` | The minimum number of samples per `period`. | 1 | +| rate | `number` | The percentage of samples to be taken during the `period`. | 0.1 | +| period | `number` | Number of **seconds** representing the duration of each sampling period. | 60 | The example below would sample at least `2` requests every `30` seconds as well as an additional `0.1` (10%) of all other requests during that period. Lambda API tracks the velocity of requests and attempts to distribute the samples as evenly as possible across the specified `period`. @@ -1006,23 +1080,23 @@ const api = require('lambda-api')({ sampling: { target: 2, rate: 0.1, - period: 30 - } - } -}) + period: 30, + }, + }, +}); ``` Additional rules can be added by specifying a `rules` parameter in the `sampling` configuration object. The `rules` should contain an `array` of "rule" objects with the following properties: -| Property | Type | Description | Default | Required | -| -------- | ---- | ----------- | ------- | -------- | -| route | `string` | The route (as defined in a route handler) to apply this rule to. | | **Yes** | -| target | `number` | The minimum number of samples per `period`. | 1 | No | -| rate | `number` | The percentage of samples to be taken during the `period`. | 0.1 | No | -| period | `number` | Number of **seconds** representing the duration of each sampling period. | 60 | No | -| method | `string` or `array` | A comma separated list or `array` of HTTP methods to apply this rule to. | | No | +| Property | Type | Description | Default | Required | +| -------- | ------------------- | ------------------------------------------------------------------------ | ------- | -------- | +| route | `string` | The route (as defined in a route handler) to apply this rule to. | | **Yes** | +| target | `number` | The minimum number of samples per `period`. | 1 | No | +| rate | `number` | The percentage of samples to be taken during the `period`. | 0.1 | No | +| period | `number` | Number of **seconds** representing the duration of each sampling period. | 60 | No | +| method | `string` or `array` | A comma separated list or `array` of HTTP methods to apply this rule to. | | No | -The `route` property is the only value required and must match a route's path definition (e.g. `/user/:userId`, not `/user/123`) to be activated. Routes can also use wildcards at the end of the route to match multiple routes (e.g. `/user/*` would match `/user/:userId` *AND* `/user/:userId/tags`). A list of `method`s can also be supplied that would limit the rule to just those HTTP methods. A comma separated `string` or an `array` will be properly parsed. +The `route` property is the only value required and must match a route's path definition (e.g. `/user/:userId`, not `/user/123`) to be activated. Routes can also use wildcards at the end of the route to match multiple routes (e.g. `/user/*` would match `/user/:userId` _AND_ `/user/:userId/tags`). A list of `method`s can also be supplied that would limit the rule to just those HTTP methods. A comma separated `string` or an `array` will be properly parsed. Sampling rules can be used to disable sampling on certain routes by setting the `target` and `rate` to `0`. For example, if you had a `/status` route that you didn't want to be sampled, you would use the following configuration: @@ -1030,12 +1104,10 @@ Sampling rules can be used to disable sampling on certain routes by setting the const api = require('lambda-api')({ logger: { sampling: { - rules: [ - { route: '/status', target: 0, rate: 0 } - ] - } - } -}) + rules: [{ route: '/status', target: 0, rate: 0 }], + }, + }, +}); ``` You could also use sampling rules to enable sampling on certain routes: @@ -1046,53 +1118,55 @@ const api = require('lambda-api')({ sampling: { rules: [ { route: '/user', target: 1, rate: 0.1 }, // enable for /user route - { route: '/posts/*', target: 1, rate: 0.1 } // enable for all routes that start with /posts + { route: '/posts/*', target: 1, rate: 0.1 }, // enable for all routes that start with /posts ], target: 0, // disable sampling default target - rate: 0 // disable sampling default rate - } - } -}) + rate: 0, // disable sampling default rate + }, + }, +}); ``` If you'd like to disable sampling for `GET` and `POST` requests to user: + ```javascript const api = require('lambda-api')({ logger: { sampling: { rules: [ // disable GET and POST on /user route - { route: '/user', target: 0, rate: 0, method: ['GET','POST'] } - ] - } - } -}) + { route: '/user', target: 0, rate: 0, method: ['GET', 'POST'] }, + ], + }, + }, +}); ``` Any combination of rules can be provided to customize sampling behavior. Note that each rule tracks requests and velocity separately, which could limit the number of samples for infrequently accessed routes. ## Middleware + The API supports middleware to preprocess requests before they execute their matching routes. Global middleware is defined using the `use` method one or more functions with three parameters for the `REQUEST`, `RESPONSE`, and `next` callback. For example: ```javascript -api.use((req,res,next) => { +api.use((req, res, next) => { // do something - next() -}) + next(); +}); ``` Middleware can be used to authenticate requests, create database connections, etc. The `REQUEST` and `RESPONSE` objects behave as they do within routes, allowing you to manipulate either object. In the case of authentication, for example, you could verify a request and update the `REQUEST` with an `authorized` flag and continue execution. Or if the request couldn't be authorized, you could respond with an error directly from the middleware. For example: ```javascript // Auth User -api.use((req,res,next) => { +api.use((req, res, next) => { if (req.headers.authorization === 'some value') { - req.authorized = true - next() // continue execution + req.authorized = true; + next(); // continue execution } else { - res.error(401,'Not Authorized') + res.error(401, 'Not Authorized'); } -}) +}); ``` The `next()` callback tells the system to continue executing. If this is not called then the system will hang (and eventually timeout) unless another request ending call such as `error` is called. You can define as many middleware functions as you want. They will execute serially and synchronously in the order in which they are defined. @@ -1100,91 +1174,106 @@ The `next()` callback tells the system to continue executing. If this is not cal **NOTE:** Middleware can use either callbacks like `res.send()` or `return` to trigger a response to the user. Please note that calling either one of these from within a middleware function will return the response immediately and terminate API execution. ### Restricting middleware execution to certain path(s) + By default, middleware will execute on every path. If you only need it to execute for specific paths, pass the path (or array of paths) as the first parameter to the `use` method. ```javascript // Single path -api.use('/users', (req,res,next) => { next() }) +api.use('/users', (req, res, next) => { + next(); +}); // Wildcard path -api.use('/users/*', (req,res,next) => { next() }) +api.use('/users/*', (req, res, next) => { + next(); +}); // Multiple path -api.use(['/users','/posts'], (req,res,next) => { next() }) +api.use(['/users', '/posts'], (req, res, next) => { + next(); +}); // Parameterized paths -api.use('/users/:userId',(req,res,next) => { next() }) +api.use('/users/:userId', (req, res, next) => { + next(); +}); // Multiple paths with parameters and wildcards -api.use(['/comments','/users/:userId','/posts/*'],(req,res,next) => { next() }) +api.use(['/comments', '/users/:userId', '/posts/*'], (req, res, next) => { + next(); +}); ``` **NOTE:** Path matching checks the defined `route`. This means that parameterized paths must be matched by the parameter (e.g. `/users/:param1`). ### Specifying multiple middleware + In addition to restricting middleware to certain paths, you can also add multiple middleware using a single `use` method. This is a convenient way to assign several pieces of middleware to the same path or minimize your code. ```javascript -const middleware1 = (req,res,next) => { +const middleware1 = (req, res, next) => { // middleware code -} +}; -const middleware2 = (req,res,next) => { +const middleware2 = (req, res, next) => { // some other middleware code -} +}; // Restrict middleware1 and middleware2 to /users route -api.use('/users', middleware1, middleware2) +api.use('/users', middleware1, middleware2); // Add middleware1 and middleware2 to all routes -api.use(middleware1, middleware2) +api.use(middleware1, middleware2); ``` ### Method-based middleware + Middleware can be restricted to a specific method (or array of methods) by using the route convenience methods or `METHOD`. Method-based middleware behaves exactly like global middleware, requiring a `REQUEST`, `RESPONSE`, and `next` parameter. You can specify multiple middlewares for each method/path using a single method call, or by using multiple method calls. Lambda API will merge the [execution stacks](#execution-stacks) for you. ```javascript -const middleware1 = (req,res,next) => { +const middleware1 = (req, res, next) => { // middleware code -} +}; -const middleware2 = (req,res,next) => { +const middleware2 = (req, res, next) => { // middleware code -} +}; // Execute middleware1 and middleware2 on /users route -api.get('/users', middleware1, middleware2, (req,res) => { +api.get('/users', middleware1, middleware2, (req, res) => { // handler function -}) +}); // Execute middleware1 on /users route -api.get('/users', middleware1) +api.get('/users', middleware1); // Add middleware2 and handler -api.get('/users', middleware2, (req,res) => { +api.get('/users', middleware2, (req, res) => { // handler function -}) +}); ``` ## Clean Up + The API has a built-in clean up method called 'finally()' that will execute after all middleware and routes have been completed, but before execution is complete. This can be used to close database connections or to perform other clean up functions. A clean up function can be defined using the `finally` method and requires a function with two parameters for the REQUEST and the RESPONSE as its only argument. For example: ```javascript -api.finally((req,res) => { +api.finally((req, res) => { // close unneeded database connections and perform clean up -}) +}); ``` The `RESPONSE` **CANNOT** be manipulated since it has already been generated. Only one `finally()` method can be defined and will execute after properly handled errors as well. ## Error Handling + Lambda API has sophisticated error handling that will automatically catch and log errors using the [Logging](#logging) system. By default, errors will trigger a JSON response with the error message. If you would like to define additional error handling, you can define them using the `use` method similar to middleware. Error handling middleware must be defined as a function with **four** arguments instead of three like normal middleware. An additional `error` parameter must be added as the first parameter. This will contain the error object generated. ```javascript -api.use((err,req,res,next) => { +api.use((err, req, res, next) => { // do something with the error - next() -}) + next(); +}); ``` The `next()` callback will cause the script to continue executing and eventually call the standard error handling function. You can short-circuit the default handler by calling a request ending method such as `send`, `html`, or `json` OR by `return`ing data from your handler. @@ -1205,9 +1294,10 @@ const errorHandler2 = (err,req,res,next) => { api.use(errorHandler1,errorHandler2) ``` -**NOTE:** Error handling middleware runs on *ALL* paths. If paths are passed in as the first parameter, they will be ignored by the error handling middleware. +**NOTE:** Error handling middleware runs on _ALL_ paths. If paths are passed in as the first parameter, they will be ignored by the error handling middleware. ### Error Types + Lambda API provides several different types of errors that can be used by your application. `RouteError`, `MethodError`, `ResponseError`, and `FileError` will all be passed to your error middleware. `ConfigurationError`s will throw an exception when you attempt to `.run()` your route and can be caught in a `try/catch` block. Most error types contain additional properties that further detail the issue. ```javascript @@ -1224,76 +1314,82 @@ const errorHandler = (err,req,res,next) => { ``` ### Error Logging + Error logs are generated using either the `error` or `fatal` logging level. Errors can be triggered from within routes and middleware by calling the `error()` method on the `RESPONSE` object. If provided a `string` as an error message, this will generate an `error` level log entry. If you supply a JavaScript `Error` object, or you `throw` an error, a `fatal` log entry will be generated. ```javascript -api.get('/somePath', (res,req) => { - res.error('This is an error message') // creates 'error' log -}) +api.get('/somePath', (res, req) => { + res.error('This is an error message'); // creates 'error' log +}); -api.get('/someOtherPath', (res,req) => { - res.error(new Error('This is a fatal error')) // creates 'fatal' log -}) +api.get('/someOtherPath', (res, req) => { + res.error(new Error('This is a fatal error')); // creates 'fatal' log +}); -api.get('/anotherPath', (res,req) => { - throw new Error('Another fatal error') // creates 'fatal' log -}) +api.get('/anotherPath', (res, req) => { + throw new Error('Another fatal error'); // creates 'fatal' log +}); -api.get('/finalPath', (res,req) => { +api.get('/finalPath', (res, req) => { try { // do something - } catch(e) { - res.error(e) // creates 'fatal' log + } catch (e) { + res.error(e); // creates 'fatal' log } -}) +}); ``` ## Namespaces + Lambda API allows you to map specific modules to namespaces that can be accessed from the `REQUEST` object. This is helpful when using the pattern in which you create a module that exports middleware, error, or route functions. In the example below, the `data` namespace is added to the API and then accessed by reference within an included module. The main handler file might look like this: ```javascript // Use app() function to add 'data' namespace -api.app('data',require('./lib/data.js')) +api.app('data', require('./lib/data.js')); // Create a get route to load user details -api.get('/users/:userId', require('./lib/users.js')) +api.get('/users/:userId', require('./lib/users.js')); ``` The users.js module might look like this: ```javascript module.exports = (req, res) => { - let userInfo = req.namespace.data.getUser(req.params.userId) - res.json({ 'userInfo': userInfo }) -} + let userInfo = req.namespace.data.getUser(req.params.userId); + res.json({ userInfo: userInfo }); +}; ``` -By saving references in namespaces, you can access them without needing to require them in every module. Namespaces can be added using the `app()` method of the API. `app()` accepts either two parameters: a string representing the name of the namespace and a function reference *OR* an object with string names as keys and function references as the values. For example: +By saving references in namespaces, you can access them without needing to require them in every module. Namespaces can be added using the `app()` method of the API. `app()` accepts either two parameters: a string representing the name of the namespace and a function reference _OR_ an object with string names as keys and function references as the values. For example: ```javascript -api.app('namespace',require('./lib/ns-functions.js')) +api.app('namespace', require('./lib/ns-functions.js')); // OR api.app({ - 'namespace1': require('./lib/ns1-functions.js'), - 'namespace2': require('./lib/ns2-functions.js') -}) + namespace1: require('./lib/ns1-functions.js'), + namespace2: require('./lib/ns2-functions.js'), +}); ``` ## CORS Support + CORS can be implemented using the [wildcard routes](#wildcard-routes) feature. A typical implementation would be as follows: ```javascript -api.options('/*', (req,res) => { +api.options('/*', (req, res) => { // Add CORS headers res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With'); + res.header( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization, Content-Length, X-Requested-With' + ); res.status(200).send({}); -}) +}); ``` You can also use the `cors()` ([see here](#corsoptions)) convenience method to add CORS headers. @@ -1301,12 +1397,13 @@ You can also use the `cors()` ([see here](#corsoptions)) convenience method to a Conditional route support could be added via middleware or with conditional logic within the `OPTIONS` route. ## Compression + Currently, API Gateway HTTP APIs do not support automatic compression, but that doesn't mean the Lambda can't return a compressed response. Lambda API supports compression out of the box: ```javascript const api = require('lambda-api')({ - compression: true -}) + compression: true, +}); ``` The response will automatically be compressed based on the `Accept-Encoding` header in the request. Supported compressions are Brotli, Gzip and Deflate - in that priority order. @@ -1314,35 +1411,38 @@ The response will automatically be compressed based on the `Accept-Encoding` hea For full control over the response compression, instantiate the API with `isBase64` set to true, and a custom serializer that returns a compressed response as a base64 encoded string. Also, don't forget to set the correct `content-encoding` header: ```javascript -const zlib = require('zlib') +const zlib = require('zlib'); const api = require('lambda-api')({ isBase64: true, headers: { - 'content-encoding': ['gzip'] + 'content-encoding': ['gzip'], }, - serializer: body => { - const json = JSON.stringify(body) - return zlib.gzipSync(json).toString('base64') - } -}) + serializer: (body) => { + const json = JSON.stringify(body); + return zlib.gzipSync(json).toString('base64'); + }, +}); ``` ## Execution Stacks + Lambda API v0.10 introduced execution stacks as a way to more efficiently process middleware. Execution stacks are automatically created for you when adding routes and middleware using the standard route convenience methods, as well as `METHOD()` and `use()`. This is a technical implementation that has made method-based middleware and additional wildcard functionality possible. Execution stacks are backwards compatible, so no code changes need to be made when upgrading from a lower version. The only caveat is with matching middleware to specific parameterized paths. Path-based middleware creates mount points that require methods to execute. This means that a `/users/:userId` middleware path would not execute if you defined a `/users/test` path. -Execution stacks allow you to execute multiple middlewares based on a number of factors including path and method. For example, you can specify a global middleware to run on every `/user/*` route, with additional middleware running on just `/user/settings/*` routes, with more middleware running on just `GET` requests to `/users/settings/name`. Execution stacks inherit middleware from matching routes and methods higher up the stack, building a final stack that is unique to each route. Definition order also matters, meaning that routes defined *before* global middleware **will not** have it as part of its execution stack. The same is true of any wildcard-based route, giving you flexibility and control over when middleware is applied. +Execution stacks allow you to execute multiple middlewares based on a number of factors including path and method. For example, you can specify a global middleware to run on every `/user/*` route, with additional middleware running on just `/user/settings/*` routes, with more middleware running on just `GET` requests to `/users/settings/name`. Execution stacks inherit middleware from matching routes and methods higher up the stack, building a final stack that is unique to each route. Definition order also matters, meaning that routes defined _before_ global middleware **will not** have it as part of its execution stack. The same is true of any wildcard-based route, giving you flexibility and control over when middleware is applied. For debugging purposes, a new `REQUEST` property called `stack` has been added. If you name your middleware functions (either by assigning them to variables or using standard named functions), the `stack` property will return an array that lists the function names of the execution stack in processing order. ## Lambda Proxy Integration + Lambda Proxy Integration is an option in API Gateway that allows the details of an API request to be passed as the `event` parameter of a Lambda function. A typical API Gateway request event with Lambda Proxy Integration enabled can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request). Lambda API automatically parses this information to create a normalized `REQUEST` object. The request can then be routed using the APIs methods. ## ALB Integration + AWS recently added support for Lambda functions as targets for Application Load Balancers. While the events from ALBs are similar to API Gateway, there are a number of differences that would require code changes based on implementation. Lambda API detects the event `interface` and automatically normalizes the `REQUEST` object. It also correctly formats the `RESPONSE` (supporting both multi-header and non-multi-header mode) for you. This allows you to call your Lambda function from API Gateway, ALB, or both, without requiring any code changes. Please note that ALB events do not contain all of the same headers as API Gateway (such as `clientType`), but Lambda API provides defaults for seamless integration between the interfaces. ALB also automatically enables binary support, giving you the ability to serve images and other binary file types. Lambda API reads the `path` parameter supplied by the ALB event and uses that to route your requests. If you specify a wildcard in your listener rule, then all matching paths will be forwarded to your Lambda function. Lambda API's routing system can be used to process these routes just like with API Gateway. This includes static paths, parameterized paths, wildcards, middleware, etc. @@ -1350,40 +1450,46 @@ Please note that ALB events do not contain all of the same headers as API Gatewa Sample ALB request and response events can be found [here](https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html). ## Configuring Routes in API Gateway + Routes must be configured in API Gateway in order to support routing to the Lambda function. The easiest way to support all of your routes without recreating them is to use [API Gateway's Proxy Integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-proxy-resource?icmpid=docs_apigateway_console). Simply create a `{proxy+}` route that uses the `ANY` method and all requests will be routed to your Lambda function and processed by the `lambda-api` module. In order for a "root" path mapping to work, you also need to create an `ANY` route for `/`. ## Reusing Persistent Connections + If you are using persistent connections in your function routes (such as AWS RDS or Elasticache), be sure to set `context.callbackWaitsForEmptyEventLoop = false;` in your main handler. This will allow the freezing of connections and will prevent Lambda from hanging on open connections. See [here](https://www.jeremydaly.com/reuse-database-connections-aws-lambda/) for more information. ## TypeScript Support + An `index.d.ts` declaration file has been included for use with your TypeScript projects (thanks @hassankhan). Please feel free to make suggestions and contributions to keep this up-to-date with future releases. **TypeScript Example** + ```typescript // import AWS Lambda types import { APIGatewayEvent, Context } from 'aws-lambda'; // import Lambda API default function -import createAPI from 'lambda-api' +import createAPI from 'lambda-api'; // instantiate framework const api = createAPI(); // Define a route -api.get('/status', async (req,res) => { - return { status: 'ok' } -}) +api.get('/status', async (req, res) => { + return { status: 'ok' }; +}); // Declare your Lambda handler exports.run = async (event: APIGatewayEvent, context: Context) => { // Run the request - return await api.run(event, context) -} + return await api.run(event, context); +}; ``` ## Contributions -Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports or create a pull request. + +Contributions, ideas and bug reports are welcome and greatly appreciated. Please add [issues](https://github.com/jeremydaly/lambda-api/issues) for suggestions and bug reports or create a pull request. ## Are you using Lambda API? + If you're using Lambda API and finding it useful, hit me up on [Twitter](https://twitter.com/jeremy_daly) or email me at contact[at]jeremydaly.com. I'd love to hear your stories, ideas, and even your complaints! diff --git a/__tests__/executionStacks.unit.js b/__tests__/executionStacks.unit.js index 27c1d8b..6b8dd17 100644 --- a/__tests__/executionStacks.unit.js +++ b/__tests__/executionStacks.unit.js @@ -1,50 +1,312 @@ -'use strict'; +"use strict"; + +const { expectation } = require("sinon"); // Init API instance -const api = require('../index')({ version: 'v1.0', logger: false }) +// const api = require("../index")({ version: "v1.0", logger: false }); let event = { - httpMethod: 'get', - path: '/', + httpMethod: "get", + path: "/", body: {}, multiValueHeaders: { - 'content-type': ['application/json'] - } -} + "content-type": ["application/json"], + }, +}; /******************************************************************************/ /*** DEFINE TEST ROUTES ***/ /******************************************************************************/ -const middleware1 = (res,req,next) => { next() } -const middleware2 = (res,req,next) => { next() } -const middleware3 = (res,req,next) => { next() } -const middleware4 = (res,req,next) => { next() } -const getRoute = (res,req) => {} -const getRoute2 = (res,req) => {} -const getRoute3 = (res,req) => {} - -// api.use((err,req,res,next) => {}) -api.use(middleware1) -api.use(middleware2) -api.use('/test',middleware3) -api.get('/*',middleware4,getRoute2) -api.get('/test',getRoute3) -// api.get('/test/*',getRoute) -// api.get('/test/testx',getRoute3) - -// api.use('/:test',middleware3) -// api.get('/', function baseGetRoute(req,res) {}) -// api.get('/p1/p2/:paramName', function getRoute(req,res) {}) -// api.get('/p1/p2', function getRoute(req,res,next) {}) -// api.get('/p1/p2', function getRoute2(req,res) {}) -// api.get('/p1/*', (req,res) => {}) +// const middleware1 = (res, req, next) => { +// next(); +// }; +// const middleware2 = (res, req, next) => { +// next(); +// }; +// const middleware3 = (res, req, next) => { +// next(); +// }; +// const middleware4 = (res, req, next) => { +// next(); +// }; +// const getRoute = (res, req) => {}; +// const getRoute2 = (res, req) => {}; +// const getRoute3 = (res, req) => {}; +// const postRoute = (res, req) => {}; + +// // api.use((err,req,res,next) => {}) + +// // api.post("/foo/bar", postRoute); + +// // api.use(middleware1); +// // api.use(middleware2); + +// // api.use("/foo/*", middleware4); + +// // // api.get('/*',middleware4,getRoute2) +// // api.get("/foo", getRoute3); +// // api.get("/foo/bar", getRoute); +// // api.use(middleware3); +// // // api.get('/test',getRoute2) +// // // api.use('/foo/:baz',middleware3) + +// // api.get("/foo/bar", getRoute2); +// // // api.get("/foo/:bat", getRoute3); + +// // //console.log(api.routes()) +// // console.log(JSON.stringify(api._routes, null, 2)); +// // api.routes(true); + +// // api.get('/test/*',getRoute) +// // api.get('/test/testx',getRoute3) + +// api.use("/:test", middleware3); +// api.get("/", function baseGetRoute(req, res) {}); +// api.get("/p1/p2/:paramName", function getRoute(req, res) {}); +// api.get("/p1/p2", function getRoute(req, res, next) {}); +// api.get("/p1/p2", function getRoute2(req, res) {}); +// api.get("/p1/*", (req, res) => {}); /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Execution Stacks:', function() { - it('test',()=>{}) -}) // end ROUTE tests +describe("Execution Stacks:", function () { + it("generates basic routes (no middleware)", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/", ["getRoute"]], + ["GET", "/foo", ["getFooRoute"]], + ["GET", "/foo/bar", ["getBarRoute"]], + ]); + }); + + it("generates basic routes (single middleware)", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use(function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/", ["middleware", "getRoute"]], + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["GET", "/foo/bar", ["middleware", "getBarRoute"]], + ]); + }); + + it("adds single middleware after a route", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/", function getRoute() {}); + api.use(function middleware(req, res, next) {}); + api.get("/foo", function getFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/", ["getRoute"]], + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["GET", "/foo/bar", ["middleware", "getBarRoute"]], + ]); + }); + + it("adds single middleware after a deeper route", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/foo", function getFooRoute() {}); + api.use(function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["getFooRoute"]], + ["GET", "/foo/bar", ["middleware", "getBarRoute"]], + ["GET", "/", ["middleware", "getRoute"]], + ]); + }); + + it("adds route-based middleware", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use("/foo", function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["GET", "/foo/bar", ["getBarRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("adds route-based middleware with *", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use("/foo/*", function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["getFooRoute"]], + ["GET", "/foo/bar", ["middleware", "getBarRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("adds method-based middleware", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/foo", function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.post("/foo", function postFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["POST", "/foo", ["postFooRoute"]], + ["GET", "/foo/bar", ["getBarRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("adds method-based middleware to multiple routes", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/foo", function middleware(req, res, next) {}); + api.get("/foo/bar", function middleware2(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.post("/foo", function postFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + api.get("/foo/baz", function getBazRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["POST", "/foo", ["postFooRoute"]], + ["GET", "/foo/bar", ["middleware2", "getBarRoute"]], + ["GET", "/foo/baz", ["getBazRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("adds middleware multiple routes", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use(["/foo", "/foo/baz"], function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.post("/foo", function postFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + api.get("/foo/baz", function getBazRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["middleware", "getFooRoute"]], + ["POST", "/foo", ["middleware", "postFooRoute"]], + ["GET", "/foo/baz", ["middleware", "getBazRoute"]], + ["GET", "/foo/bar", ["getBarRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("adds method-based middleware using *", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.get("/foo/*", function middleware(req, res, next) {}); + api.get("/", function getRoute() {}); + api.get("/foo", function getFooRoute() {}); + api.post("/foo", function postFooRoute() {}); + api.get("/foo/bar", function getBarRoute() {}); + api.get("/foo/baz", function getBazRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo", ["getFooRoute"]], + ["POST", "/foo", ["postFooRoute"]], + ["GET", "/foo/*", ["middleware"]], + ["GET", "/foo/bar", ["middleware", "getBarRoute"]], + ["GET", "/foo/baz", ["middleware", "getBazRoute"]], + ["GET", "/", ["getRoute"]], + ]); + }); + + it("assign multiple middleware to parameterized path", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use( + function middleware(req, res, next) {}, + function middleware2(req, res, next) {} + ); + api.get("/foo/:bar", function getParamRoute() {}); + + expect(api.routes()).toEqual([ + ["GET", "/foo/:bar", ["middleware", "middleware2", "getParamRoute"]], + ]); + }); + + it("inherit middleware based on * routes and parameterized paths", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use("/*", function middleware(req, res, next) {}); + api.get("/", function baseGetRoute(req, res) {}); + api.get("/p1/p2/:paramName", function getRoute(req, res) {}); + api.get("/p1/p2", function getRoute(req, res, next) {}); + api.get("/p1/p2", function getRoute2(req, res) {}); + api.get("/p1/p3", function getRoute3(req, res) {}); + api.get("/p1/*", function starRoute(req, res) {}); + api.get("/p1/p2/:paramName", function getRoute4(req, res) {}); + + // console.log(api.routes()); + expect(api.routes()).toEqual([ + ["GET", "/", ["middleware", "baseGetRoute"]], + ["GET", "/p1/p2", ["middleware", "getRoute", "getRoute2"]], + ["GET", "/p1/p2/:paramName", ["middleware", "getRoute", "getRoute4"]], + ["GET", "/p1/p3", ["middleware", "getRoute3"]], + ["GET", "/p1/*", ["middleware", "starRoute"]], + ]); + }); + + it.skip("inherit additional middleware after a * middleware is applied", () => { + // Init API instance + const api = require("../index")({ version: "v1.0", logger: false }); + + api.use(function middleware(req, res, next) {}); + // api.use(["/data"], function middlewareX(req, res, next) {}); + api.use(["/data/*"], function middleware2(req, res, next) {}); + api.use(function middleware3(req, res, next) {}); + + api.get("/", function getRoute(req, res) {}); + api.get("/data", function getDataRoute(req, res) {}); + api.get("/data/test", function getNestedDataRoute(req, res) {}); + + console.log(api.routes()); + // expect(api.routes()).toEqual([ + // ["GET", "/", ["middleware", "baseGetRoute"]], + // ["GET", "/p1/p2", ["middleware", "getRoute", "getRoute2"]], + // ["GET", "/p1/p2/:paramName", ["middleware", "getRoute", "getRoute4"]], + // ["GET", "/p1/p3", ["middleware", "getRoute3"]], + // ["GET", "/p1/*", ["middleware", "starRoute"]], + // ]); + }); +}); // end ROUTE tests diff --git a/__tests__/middleware.unit.js b/__tests__/middleware.unit.js index 188a462..868b0b3 100644 --- a/__tests__/middleware.unit.js +++ b/__tests__/middleware.unit.js @@ -1,79 +1,78 @@ -'use strict'; +"use strict"; -const delay = ms => new Promise(res => setTimeout(res, ms)) +const delay = (ms) => new Promise((res) => setTimeout(res, ms)); // Init API instance -const api = require('../index')({ version: 'v1.0' }) -const api2 = require('../index')({ version: 'v1.0' }) -const api3 = require('../index')({ version: 'v1.0' }) -const api4 = require('../index')({ version: 'v1.0' }) -const api5 = require('../index')({ version: 'v1.0' }) -const api6 = require('../index')({ version: 'v1.0' }) -const api7 = require('../index')({ version: 'v1.0' }) -const api8 = require('../index')({ version: 'v1.0' }) -const api9 = require('../index')({ version: 'v1.0' }) +const api = require("../index")({ version: "v1.0" }); +const api2 = require("../index")({ version: "v1.0" }); +const api3 = require("../index")({ version: "v1.0" }); +const api4 = require("../index")({ version: "v1.0" }); +const api5 = require("../index")({ version: "v1.0" }); +const api6 = require("../index")({ version: "v1.0" }); +const api7 = require("../index")({ version: "v1.0" }); +const api8 = require("../index")({ version: "v1.0" }); +const api9 = require("../index")({ version: "v1.0" }); let event = { - httpMethod: 'get', - path: '/test', + httpMethod: "get", + path: "/test", body: {}, multiValueHeaders: { - 'content-type': ['application/json'] - } -} + "content-type": ["application/json"], + }, +}; /******************************************************************************/ /*** DEFINE TEST MIDDLEWARE ***/ /******************************************************************************/ -api.use(function(req,res,next) { - req.testMiddleware = '123' - next() +api.use(function (req, res, next) { + req.testMiddleware = "123"; + next(); }); // Middleware that accesses params, querystring, and body values -api.use(function(req,res,next) { - req.testMiddleware2 = '456' - req.testMiddleware3 = req.params.test - req.testMiddleware4 = req.query.test ? req.query.test : null - req.testMiddleware5 = req.body.test ? req.body.test : null - next() +api.use(function (req, res, next) { + req.testMiddleware2 = "456"; + req.testMiddleware3 = req.params.test; + req.testMiddleware4 = req.query.test ? req.query.test : null; + req.testMiddleware5 = req.body.test ? req.body.test : null; + next(); }); // Add middleware with promise/delay -api.use(function(req,res,next) { - if (req.route === '/testPromise') { - let start = Date.now() +api.use(function (req, res, next) { + if (req.route === "/testPromise") { + let start = Date.now(); delay(100).then((x) => { // console.log('Time:',Date.now()-start); - req.testMiddlewarePromise = 'test' - next() - }) + req.testMiddlewarePromise = "test"; + next(); + }); } else { - next() + next(); } }); +api2.use("/test", function testMiddlware(req, res, next) { + req.testMiddleware = true; + next(); +}); -api2.use('/test',function testMiddlware(req,res,next) { - req.testMiddleware = true - next() -}) - -api2.use('/test/*',function testMiddlewareWildcard(req,res,next) { - req.testMiddlewareWildcard = true - next() -}) +api2.use("/test/*", function testMiddlewareWildcard(req, res, next) { + req.testMiddlewareWildcard = true; + next(); +}); -api2.use('/test/test2/*',function testMiddlewareWildcard2(req,res,next) { - req.testMiddlewareWildcard2 = true - next() -}) +api2.use("/test/test2/*", function testMiddlewareWildcard2(req, res, next) { + req.testMiddlewareWildcard2 = true; + next(); +}); -api2.use('/test/:param1',function testMiddlewareParam(req,res,next) { - req.testMiddlewareParam = true - next() -}) +api2.use("/test/:param1", function testMiddlewareParam(req, res, next) { + req.testMiddlewareParam = true; + next(); +}); // This test is deprecated // api2.use('/test/testing',function testMiddlewarePath(req,res,next) { @@ -81,254 +80,347 @@ api2.use('/test/:param1',function testMiddlewareParam(req,res,next) { // next() // }) -api2.use('/test/error',function testMiddlwareError(req,res,next) { +api2.use("/test/error", function testMiddlwareError(req, res, next) { // console.log('API2 ERROR MIDDLEWARE'); - res.error(401,'Not Authorized') -}) - - -api3.use(['/test','/test/:param1','/test2/*'],function arrayBasedMiddleware(req,res,next) { - // console.log('API3 MIDDLEWARE:',req.path); - req.testMiddlewareAll = true - next() -}) - -const middleware1 = (req,res,next) => { - req.middleware1 = true - next() -} - -const middleware2 = (req,res,next) => { - req.middleware2 = true - next() -} - -const middleware3 = (req,res,next) => { - req.middleware3 = true - next() -} - -api4.use(middleware1,middleware2); -api5.use('/test/x',middleware1,middleware2); -api5.use('/test/y',middleware1); - - + res.error(401, "Not Authorized"); +}); -api6.use((req,res,next) => { - res.header('middleware1',true) - return 'return from middleware' -}) +api3.use( + ["/test", "/test/:param1", "/test2/*"], + function arrayBasedMiddleware(req, res, next) { + // console.log('API3 MIDDLEWARE:',req.path); + req.testMiddlewareAll = true; + next(); + } +); + +const middleware1 = (req, res, next) => { + req.middleware1 = true; + next(); +}; + +const middleware2 = (req, res, next) => { + req.middleware2 = true; + next(); +}; + +const middleware3 = (req, res, next) => { + req.middleware3 = true; + next(); +}; + +api4.use(middleware1, middleware2); +api5.use("/test/x", middleware1, middleware2); +api5.use("/test/y", middleware1); + +api6.use((req, res, next) => { + res.header("middleware1", true); + return "return from middleware"; +}); // This shouldn't run -api6.use((req,res,next) => { - res.header('middleware2',true) - next() -}) - - +api6.use((req, res, next) => { + res.header("middleware2", true); + next(); +}); -api7.use((req,res,next) => { - res.header('middleware1',true) - res.send('return from middleware') - next() -}) +api7.use((req, res, next) => { + res.header("middleware1", true); + res.send("return from middleware"); + next(); +}); // This shouldn't run -api7.use((req,res,next) => { - res.header('middleware2',true) - next() -}) - +api7.use((req, res, next) => { + res.header("middleware2", true); + next(); +}); -api9.use(middleware1) -api9.use(['/data/*'],middleware2) -api9.use(middleware3) +api9.use(middleware1); +api9.use(["/data/*"], middleware2); +api9.use(middleware3); /******************************************************************************/ /*** DEFINE TEST ROUTES ***/ /******************************************************************************/ -api.get('/test', function(req,res) { - res.status(200).json({ method: 'get', testMiddleware: req.testMiddleware, testMiddleware2: req.testMiddleware2 }) -}) - -api.post('/test/:test', function(req,res) { - res.status(200).json({ method: 'get', testMiddleware3: req.testMiddleware3, testMiddleware4: req.testMiddleware4, testMiddleware5: req.testMiddleware5 }) -}) - -api.get('/testPromise', function(req,res) { - res.status(200).json({ method: 'get', testMiddlewarePromise: req.testMiddlewarePromise }) -}) +api.get("/test", function (req, res) { + res.status(200).json({ + method: "get", + testMiddleware: req.testMiddleware, + testMiddleware2: req.testMiddleware2, + }); +}); +api.post("/test/:test", function (req, res) { + res.status(200).json({ + method: "get", + testMiddleware3: req.testMiddleware3, + testMiddleware4: req.testMiddleware4, + testMiddleware5: req.testMiddleware5, + }); +}); -api2.get('/test', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddleware ? true : false, middlewareWildcard: req.testMiddlewareWildcard ? true : false, middlewareParam: req.testMiddlewareParam ? true : false, middlewarePath: req.testMiddlewarePath ? true : false }) -}) +api.get("/testPromise", function (req, res) { + res + .status(200) + .json({ method: "get", testMiddlewarePromise: req.testMiddlewarePromise }); +}); -api2.get('/test2/:test', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddleware ? true : false, middlewareWildcard: req.testMiddlewareWildcard ? true : false, middlewareParam: req.testMiddlewareParam ? true : false, middlewarePath: req.testMiddlewarePath ? true : false }) -}) +api2.get("/test", function (req, res) { + res.status(200).json({ + method: "get", + middleware: req.testMiddleware ? true : false, + middlewareWildcard: req.testMiddlewareWildcard ? true : false, + middlewareParam: req.testMiddlewareParam ? true : false, + middlewarePath: req.testMiddlewarePath ? true : false, + }); +}); -api2.get('/test/xyz', function testXYZ(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddleware ? true : false, middlewareWildcard: req.testMiddlewareWildcard ? true : false, middlewareParam: req.testMiddlewareParam ? true : false, middlewarePath: req.testMiddlewarePath ? true : false }) -}) +api2.get("/test2/:test", function (req, res) { + res.status(200).json({ + method: "get", + middleware: req.testMiddleware ? true : false, + middlewareWildcard: req.testMiddlewareWildcard ? true : false, + middlewareParam: req.testMiddlewareParam ? true : false, + middlewarePath: req.testMiddlewarePath ? true : false, + }); +}); -api2.get('/test/:param1', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddleware ? true : false, middlewareWildcard: req.testMiddlewareWildcard ? true : false, middlewareParam: req.testMiddlewareParam ? true : false, middlewarePath: req.testMiddlewarePath ? true : false }) -}) +api2.get("/test/xyz", function testXYZ(req, res) { + res.status(200).json({ + method: "get", + middleware: req.testMiddleware ? true : false, + middlewareWildcard: req.testMiddlewareWildcard ? true : false, + middlewareParam: req.testMiddlewareParam ? true : false, + middlewarePath: req.testMiddlewarePath ? true : false, + }); +}); -api2.get('/test/error', function(req,res) { - res.status(200).json({ message: 'should not get here' }) -}) +api2.get("/test/:param1", function (req, res) { + res.status(200).json({ + method: "get", + middleware: req.testMiddleware ? true : false, + middlewareWildcard: req.testMiddlewareWildcard ? true : false, + middlewareParam: req.testMiddlewareParam ? true : false, + middlewarePath: req.testMiddlewarePath ? true : false, + }); +}); +api2.get("/test/error", function (req, res) { + res.status(200).json({ message: "should not get here" }); +}); -api3.get('/test', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddlewareAll ? true : false }) -}) +api3.get("/test", function (req, res) { + res + .status(200) + .json({ method: "get", middleware: req.testMiddlewareAll ? true : false }); +}); -api3.get('/test/:param1', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddlewareAll ? true : false }) -}) +api3.get("/test/:param1", function (req, res) { + res + .status(200) + .json({ method: "get", middleware: req.testMiddlewareAll ? true : false }); +}); -api3.get('/test2/test', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddlewareAll ? true : false }) -}) +api3.get("/test2/test", function (req, res) { + res + .status(200) + .json({ method: "get", middleware: req.testMiddlewareAll ? true : false }); +}); -api3.get('/test3', function(req,res) { - res.status(200).json({ method: 'get', middleware: req.testMiddlewareAll ? true : false }) -}) +api3.get("/test3", function (req, res) { + res + .status(200) + .json({ method: "get", middleware: req.testMiddlewareAll ? true : false }); +}); -api4.get('/test', (req,res) => { +api4.get("/test", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api5.get('/test', (req,res) => { +api5.get("/test", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api5.get('/test/x', (req,res) => { +api5.get("/test/x", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api5.get('/test/y', (req,res) => { +api5.get("/test/y", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api6.get('/test', (req,res) => { +api6.get("/test", (req, res) => { // This should not run because of the middleware return - res.status(200).send('route response') -}) + res.status(200).send("route response"); +}); -api7.get('/test', (req,res) => { +api7.get("/test", (req, res) => { // This should not run because of the middleware return - res.status(200).send('route response') -}) + res.status(200).send("route response"); +}); -api8.get('/test/one', middleware1, (req,res) => { +api8.get("/test/one", middleware1, (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api8.get('/test/two', middleware1, middleware2, (req,res) => { +api8.get("/test/two", middleware1, middleware2, (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, - middleware2: req.middleware2 ? true : false - }) -}) + middleware2: req.middleware2 ? true : false, + }); +}); -api9.get('/test',(req,res) => { +api9.get("/test", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, middleware2: req.middleware2 ? true : false, - middleware3: req.middleware3 ? true : false - }) -}) + middleware3: req.middleware3 ? true : false, + }); +}); -api9.get('/data',(req,res) => { +api9.get("/data", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, middleware2: req.middleware2 ? true : false, - middleware3: req.middleware3 ? true : false - }) -}) + middleware3: req.middleware3 ? true : false, + }); +}); -api9.get('/data/test',(req,res) => { +api9.get("/data/test", (req, res) => { res.status(200).json({ - method: 'get', + method: "get", middleware1: req.middleware1 ? true : false, middleware2: req.middleware2 ? true : false, - middleware3: req.middleware3 ? true : false - }) -}) - + middleware3: req.middleware3 ? true : false, + }); +}); /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Middleware Tests:', function() { - +describe("Middleware Tests:", function () { // this.slow(300); - it('Set Values in res object', async function() { - let _event = Object.assign({},event,{}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}', isBase64Encoded: false }) - }) // end it - - it('Access params, querystring, and body values', async function() { - let _event = Object.assign({},event,{ httpMethod: 'post', path: '/test/123', queryStringParameters: { test: "456" }, body: { test: "789" } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}', isBase64Encoded: false }) - }) // end it - - - it('Middleware with Promise/Delay', async function() { - let _event = Object.assign({},event,{ path: '/testPromise'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","testMiddlewarePromise":"test"}', isBase64Encoded: false }) - }) // end it - - - it('With matching string path', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":true,"middlewareWildcard":false,"middlewareParam":false,"middlewarePath":false}', isBase64Encoded: false }) - }) // end it - - it('With non-matching string path', async function() { - let _event = Object.assign({},event,{ path: '/test2/xyz' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false,"middlewareWildcard":false,"middlewareParam":false,"middlewarePath":false}', isBase64Encoded: false }) - }) // end it - - it('Wildcard match', async function() { - let _event = Object.assign({},event,{ path: '/test/xyz' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":false,"middlewarePath":false}', isBase64Encoded: false }) - }) // end it + it("Set Values in res object", async function () { + let _event = Object.assign({}, event, {}); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","testMiddleware":"123","testMiddleware2":"456"}', + isBase64Encoded: false, + }); + }); // end it + + it("Access params, querystring, and body values", async function () { + let _event = Object.assign({}, event, { + httpMethod: "post", + path: "/test/123", + queryStringParameters: { test: "456" }, + body: { test: "789" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","testMiddleware3":"123","testMiddleware4":"456","testMiddleware5":"789"}', + isBase64Encoded: false, + }); + }); // end it + + it("Middleware with Promise/Delay", async function () { + let _event = Object.assign({}, event, { path: "/testPromise" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","testMiddlewarePromise":"test"}', + isBase64Encoded: false, + }); + }); // end it + + it("With matching string path", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":true,"middlewareWildcard":false,"middlewareParam":false,"middlewarePath":false}', + isBase64Encoded: false, + }); + }); // end it + + it("With non-matching string path", async function () { + let _event = Object.assign({}, event, { path: "/test2/xyz" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":false,"middlewareWildcard":false,"middlewareParam":false,"middlewarePath":false}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard match", async function () { + let _event = Object.assign({}, event, { path: "/test/xyz" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":false,"middlewarePath":false}', + isBase64Encoded: false, + }); + }); // end it // it('Parameter match', async function() { // let _event = Object.assign({},event,{ path: '/test/testing' }) @@ -337,141 +429,272 @@ describe('Middleware Tests:', function() { // expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":true,"middlewarePath":true}', isBase64Encoded: false }) // }) // end it - it('Parameter match', async function() { - let _event = Object.assign({},event,{ path: '/test/testing' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":true,"middlewarePath":false}', isBase64Encoded: false }) - }) // end it - - - it('Matching path (array)', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":true}', isBase64Encoded: false }) - }) // end it - - it('Matching param (array)', async function() { - let _event = Object.assign({},event,{ path: '/test/xyz' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":true}', isBase64Encoded: false }) - }) // end it - - it('Matching wildcard (array)', async function() { - let _event = Object.assign({},event,{ path: '/test2/test' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":true}', isBase64Encoded: false }) - }) // end it - - it('Non-matching path (array)', async function() { - let _event = Object.assign({},event,{ path: '/test3' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false}', isBase64Encoded: false }) - }) // end it - - - it('Multiple middlewares (no path)', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":true}', isBase64Encoded: false }) - }) // end it - - - it('Multiple middlewares (w/o matching path)', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":false,"middleware2":false}', isBase64Encoded: false }) - }) // end it - - - it('Multiple middlewares (w/ matching path)', async function() { - let _event = Object.assign({},event,{ path: '/test/x' }) - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":true}', isBase64Encoded: false }) - }) // end it - - - it('Single middleware (w/ matching path)', async function() { - let _event = Object.assign({},event,{ path: '/test/y' }) - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":false}', isBase64Encoded: false }) - }) // end it - - - it('Short-circuit route with middleware (async return)', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api6.run(_event,{},(e,res) => { r(res) })) + it("Parameter match", async function () { + let _event = Object.assign({}, event, { path: "/test/testing" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], middleware1: [true] }, - statusCode: 200, body: 'return from middleware', isBase64Encoded: false }) - }) // end it - - - it('Short-circuit route with middleware (callback)', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api7.run(_event,{},(e,res) => { r(res) })) + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":true,"middlewarePath":false}', + isBase64Encoded: false, + }); + }); // end it + + it("Matching path (array)", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], middleware1: [true] }, - statusCode: 200, body: 'return from middleware', isBase64Encoded: false }) - }) // end it - - - it('Trigger error in middleware', async function() { - let _event = Object.assign({},event,{ path: '/test/error' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Matching param (array)", async function () { + let _event = Object.assign({}, event, { path: "/test/xyz" }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, - statusCode: 401, body: '{"error":"Not Authorized"}', isBase64Encoded: false }) - }) // end it - - - it('Route-based middleware (single)', async function() { - let _event = Object.assign({},event,{ path: '/test/one' }) - let result = await new Promise(r => api8.run(_event,{},(e,res) => { r(res) })) + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Matching wildcard (array)", async function () { + let _event = Object.assign({}, event, { path: "/test2/test" }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Non-matching path (array)", async function () { + let _event = Object.assign({}, event, { path: "/test3" }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware":false}', + isBase64Encoded: false, + }); + }); // end it + + it("Multiple middlewares (no path)", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Multiple middlewares (w/o matching path)", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api5.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":false,"middleware2":false}', + isBase64Encoded: false, + }); + }); // end it + + it("Multiple middlewares (w/ matching path)", async function () { + let _event = Object.assign({}, event, { path: "/test/x" }); + let result = await new Promise((r) => + api5.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Single middleware (w/ matching path)", async function () { + let _event = Object.assign({}, event, { path: "/test/y" }); + let result = await new Promise((r) => + api5.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":false}', - isBase64Encoded: false }) - }) // end it - - it('Route-based middleware (two middlewares)', async function() { - let _event = Object.assign({},event,{ path: '/test/two' }) - let result = await new Promise(r => api8.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Short-circuit route with middleware (async return)", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api6.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { + "content-type": ["application/json"], + middleware1: [true], + }, + statusCode: 200, + body: "return from middleware", + isBase64Encoded: false, + }); + }); // end it + + it("Short-circuit route with middleware (callback)", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api7.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { + "content-type": ["application/json"], + middleware1: [true], + }, + statusCode: 200, + body: "return from middleware", + isBase64Encoded: false, + }); + }); // end it + + it("Trigger error in middleware", async function () { + let _event = Object.assign({}, event, { path: "/test/error" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 401, + body: '{"error":"Not Authorized"}', + isBase64Encoded: false, + }); + }); // end it + + it("Route-based middleware (single)", async function () { + let _event = Object.assign({}, event, { path: "/test/one" }); + let result = await new Promise((r) => + api8.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":false}', + isBase64Encoded: false, + }); + }); // end it + + it("Route-based middleware (two middlewares)", async function () { + let _event = Object.assign({}, event, { path: "/test/two" }); + let result = await new Promise((r) => + api8.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":true}', - isBase64Encoded: false }) - }) // end it - - - - it('Wildcard match - issue #112', async function() { - let _event = Object.assign({},event,{ path: '/test/' }) - let result = await new Promise(r => api9.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":false,"middleware3":true}', isBase64Encoded: false }) - }) // end it - - - it('Wildcard match - issue #112', async function() { - let _event = Object.assign({},event,{ path: '/data' }) - let result = await new Promise(r => api9.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware1":true,"middleware2":false,"middleware3":true}', isBase64Encoded: false }) - }) // end it - - it.only('Wildcard match - issue #112', async function() { - let _event = Object.assign({},event,{ path: '/data/test' }) - let result = await new Promise(r => api9.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard match - issue #112", async function () { + let _event = Object.assign({}, event, { path: "/test/" }); + let result = await new Promise((r) => + api9.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":false,"middleware3":true}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard match - issue #112", async function () { + let _event = Object.assign({}, event, { path: "/data" }); + let result = await new Promise((r) => + api9.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":false,"middleware3":true}', + isBase64Encoded: false, + }); + }); // end it + + it.skip("Wildcard match - issue #112", async function () { + let _event = Object.assign({}, event, { path: "/data/test" }); + let result = await new Promise((r) => + api9.run(_event, {}, (e, res) => { + r(res); + }) + ); + console.log(api9.routes()); console.log(result); - //expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","middleware":false,"middlewareWildcard":true,"middlewareParam":false,"middlewarePath":false}', isBase64Encoded: false }) - }) // end it - + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","middleware1":true,"middleware2":true,"middleware3":true}', + isBase64Encoded: false, + }); + }); // end it // api8.get('/test/error', (req,res) => {}, (req,res) => { // res.status(200).json({ // method: 'get' // }) // }) - - -}) // end MIDDLEWARE tests +}); // end MIDDLEWARE tests diff --git a/__tests__/prettyPrint.unit.js b/__tests__/prettyPrint.unit.js index 6c2caf0..72f2733 100644 --- a/__tests__/prettyPrint.unit.js +++ b/__tests__/prettyPrint.unit.js @@ -1,18 +1,33 @@ -'use strict'; +"use strict"; -const prettyPrint = require('../lib/prettyPrint') +const prettyPrint = require("../lib/prettyPrint"); /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('PrettyPrint Tests:', function() { - it('Minimum header widths', function() { - expect(prettyPrint([['GET','/'],['POST','/'],['DELETE','/']])).toBe('╔══════════╤═════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE\u001b[0m ║\n╟──────────┼─────────╢\n║ GET │ / ║\n╟──────────┼─────────╢\n║ POST │ / ║\n╟──────────┼─────────╢\n║ DELETE │ / ║\n╚══════════╧═════════╝') - }) // end it +describe("PrettyPrint Tests:", function () { + it("Minimum header widths", function () { + expect( + prettyPrint([ + ["GET", "/", ["unnamed"]], + ["POST", "/", ["unnamed"]], + ["DELETE", "/", ["unnamed"]], + ]) + ).toBe( + "╔══════════╤═════════╤═══════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE\u001b[0m │ \u001b[1mSTACK \u001b[0m ║\n╟──────────┼─────────┼───────────╢\n║ GET │ / │ unnamed ║\n╟──────────┼─────────┼───────────╢\n║ POST │ / │ unnamed ║\n╟──────────┼─────────┼───────────╢\n║ DELETE │ / │ unnamed ║\n╚══════════╧═════════╧═══════════╝" + ); + }); // end it - it('Adjusted header widths', function() { - expect(prettyPrint([['GET','/'],['POST','/testing'],['DELETE','/long-url-path-name']])).toBe('╔══════════╤═══════════════════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE \u001b[0m ║\n╟──────────┼───────────────────────╢\n║ GET │ / ║\n╟──────────┼───────────────────────╢\n║ POST │ /testing ║\n╟──────────┼───────────────────────╢\n║ DELETE │ /long-url-path-name ║\n╚══════════╧═══════════════════════╝') - }) // end it - -}) // end UTILITY tests + it("Adjusted header widths", function () { + expect( + prettyPrint([ + ["GET", "/", ["unnamed"]], + ["POST", "/testing", ["unnamed"]], + ["DELETE", "/long-url-path-name", ["unnamed"]], + ]) + ).toBe( + "╔══════════╤═══════════════════════╤═══════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE \u001b[0m │ \u001b[1mSTACK \u001b[0m ║\n╟──────────┼───────────────────────┼───────────╢\n║ GET │ / │ unnamed ║\n╟──────────┼───────────────────────┼───────────╢\n║ POST │ /testing │ unnamed ║\n╟──────────┼───────────────────────┼───────────╢\n║ DELETE │ /long-url-path-name │ unnamed ║\n╚══════════╧═══════════════════════╧═══════════╝" + ); + }); // end it +}); // end UTILITY tests diff --git a/__tests__/register.unit.js b/__tests__/register.unit.js index 1debb48..69d1bbc 100644 --- a/__tests__/register.unit.js +++ b/__tests__/register.unit.js @@ -1,147 +1,327 @@ -'use strict'; +"use strict"; // Init API instance -const api = require('../index')({ version: 'v1.0' }) -const api2 = require('../index')({ version: 'v2.0' }) +const api = require("../index")({ version: "v1.0" }); +const api2 = require("../index")({ version: "v2.0" }); let event = { - httpMethod: 'get', - path: '/test', + httpMethod: "get", + path: "/test", body: {}, multiValueHeaders: { - 'content-type': ['application/json'] - } -} + "content-type": ["application/json"], + }, +}; /******************************************************************************/ /*** REGISTER ROUTES ***/ /******************************************************************************/ -api.register(require('./_testRoutes-v1'), { prefix: '/v1' }) -api.register(require('./_testRoutes-v1'), { prefix: '/vX/vY' }) -api.register(require('./_testRoutes-v1'), { prefix: '' }) -api.register(require('./_testRoutes-v2'), { prefix: '/v2' }) -api.register(require('./_testRoutes-v3')) // no options -api2.register(require('./_testRoutes-v3'), ['array']) // w/ array options -api2.register(require('./_testRoutes-v3'), 'string') // w/ string +api.register(require("./_testRoutes-v1"), { prefix: "/v1" }); +api.register(require("./_testRoutes-v1"), { prefix: "/vX/vY" }); +api.register(require("./_testRoutes-v1"), { prefix: "" }); +api.register(require("./_testRoutes-v2"), { prefix: "/v2" }); +api.register(require("./_testRoutes-v3")); // no options +api2.register(require("./_testRoutes-v3"), ["array"]); // w/ array options +api2.register(require("./_testRoutes-v3"), "string"); // w/ string /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Register Tests:', function() { - - it('No prefix', async function() { - let _event = Object.assign({},event,{ path: '/test-register' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('No prefix (nested)', async function() { - let _event = Object.assign({},event,{ path: '/test-register/sub1' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('No prefix (nested w/ param)', async function() { - let _event = Object.assign({},event,{ path: '/test-register/TEST/test' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) - }) // end it - - it('With prefix', async function() { - let _event = Object.assign({},event,{ path: '/v1/test-register' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v1/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With prefix (nested)', async function() { - let _event = Object.assign({},event,{ path: '/v1/test-register/sub1' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v1/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With prefix (nested w/ param)', async function() { - let _event = Object.assign({},event,{ path: '/v1/test-register/TEST/test' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v1/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) - }) // end it - - - it('With double prefix', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/test-register' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With double prefix (nested)', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/test-register/sub1' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With double prefix (nested w/ param)', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/test-register/TEST/test' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) - }) // end it - - it('With recursive prefix', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With recursive prefix (nested)', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register/sub1' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('With recursive prefix (nested w/ param)', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/vZ/test-register/TEST/test' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/vZ/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) - }) // end it - - it('After recursive interation', async function() { - let _event = Object.assign({},event,{ path: '/vX/vY/test-register/sub2' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/vX/vY/test-register/sub2","route":"/test-register/sub2","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('New prefix', async function() { - let _event = Object.assign({},event,{ path: '/v2/test-register' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v2/test-register","route":"/test-register","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('New prefix (nested)', async function() { - let _event = Object.assign({},event,{ path: '/v2/test-register/sub1' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v2/test-register/sub1","route":"/test-register/sub1","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('New prefix (nested w/ param)', async function() { - let _event = Object.assign({},event,{ path: '/v2/test-register/TEST/test' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/v2/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', isBase64Encoded: false }) - }) // end it - - it('No options/no prefix', async function() { - let _event = Object.assign({},event,{ path: '/test-register-no-options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"path":"/test-register-no-options","route":"/test-register-no-options","method":"GET"}', isBase64Encoded: false }) - }) // end it - - it('Base path w/ multiple unprefixed registers', async function() { - const api = require('../index')({ - base: 'base-path' - }) - api.register((api) => { api.get('/foo', async () => {})}, { prefix: 'fuz'}) - api.register((api) => { api.get('/bar', async () => {})}) - api.register((api) => { api.get('/baz', async () => {})}) - expect(api.routes()).toEqual([["GET", "/base-path/fuz/foo"], ["GET", "/base-path/bar"], ["GET", "/base-path/baz"]]) - }) // end it - -}) // end ROUTE tests +describe("Register Tests:", function () { + it("No prefix", async function () { + let _event = Object.assign({}, event, { path: "/test-register" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/test-register","route":"/test-register","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("No prefix (nested)", async function () { + let _event = Object.assign({}, event, { path: "/test-register/sub1" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/test-register/sub1","route":"/test-register/sub1","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("No prefix (nested w/ param)", async function () { + let _event = Object.assign({}, event, { path: "/test-register/TEST/test" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With prefix", async function () { + let _event = Object.assign({}, event, { path: "/v1/test-register" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v1/test-register","route":"/test-register","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With prefix (nested)", async function () { + let _event = Object.assign({}, event, { path: "/v1/test-register/sub1" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v1/test-register/sub1","route":"/test-register/sub1","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With prefix (nested w/ param)", async function () { + let _event = Object.assign({}, event, { + path: "/v1/test-register/TEST/test", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v1/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With double prefix", async function () { + let _event = Object.assign({}, event, { path: "/vX/vY/test-register" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/test-register","route":"/test-register","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With double prefix (nested)", async function () { + let _event = Object.assign({}, event, { + path: "/vX/vY/test-register/sub1", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/test-register/sub1","route":"/test-register/sub1","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With double prefix (nested w/ param)", async function () { + let _event = Object.assign({}, event, { + path: "/vX/vY/test-register/TEST/test", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With recursive prefix", async function () { + let _event = Object.assign({}, event, { path: "/vX/vY/vZ/test-register" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/vZ/test-register","route":"/test-register","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With recursive prefix (nested)", async function () { + let _event = Object.assign({}, event, { + path: "/vX/vY/vZ/test-register/sub1", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/vZ/test-register/sub1","route":"/test-register/sub1","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("With recursive prefix (nested w/ param)", async function () { + let _event = Object.assign({}, event, { + path: "/vX/vY/vZ/test-register/TEST/test", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/vZ/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', + isBase64Encoded: false, + }); + }); // end it + + it("After recursive interation", async function () { + let _event = Object.assign({}, event, { + path: "/vX/vY/test-register/sub2", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/vX/vY/test-register/sub2","route":"/test-register/sub2","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("New prefix", async function () { + let _event = Object.assign({}, event, { path: "/v2/test-register" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v2/test-register","route":"/test-register","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("New prefix (nested)", async function () { + let _event = Object.assign({}, event, { path: "/v2/test-register/sub1" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v2/test-register/sub1","route":"/test-register/sub1","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("New prefix (nested w/ param)", async function () { + let _event = Object.assign({}, event, { + path: "/v2/test-register/TEST/test", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/v2/test-register/TEST/test","route":"/test-register/:param1/test","method":"GET","params":{"param1":"TEST"}}', + isBase64Encoded: false, + }); + }); // end it + + it("No options/no prefix", async function () { + let _event = Object.assign({}, event, { + path: "/test-register-no-options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"path":"/test-register-no-options","route":"/test-register-no-options","method":"GET"}', + isBase64Encoded: false, + }); + }); // end it + + it("Base path w/ multiple unprefixed registers", async function () { + const api = require("../index")({ + base: "base-path", + }); + api.register( + (api) => { + api.get("/foo", async () => {}); + }, + { prefix: "fuz" } + ); + api.register((api) => { + api.get("/bar", async () => {}); + }); + api.register((api) => { + api.get("/baz", async () => {}); + }); + + expect(api.routes()).toEqual([ + ["GET", "/base-path/fuz/foo", ["unnamed"]], + ["GET", "/base-path/bar", ["unnamed"]], + ["GET", "/base-path/baz", ["unnamed"]], + ]); + }); // end it +}); // end ROUTE tests diff --git a/__tests__/routes.unit.js b/__tests__/routes.unit.js index 6f4e487..45a7674 100644 --- a/__tests__/routes.unit.js +++ b/__tests__/routes.unit.js @@ -1,1170 +1,2394 @@ -'use strict'; - - +"use strict"; // Init API instance -const api = require('../index')({ version: 'v1.0', logger: false }) -const api2 = require('../index')({ version: 'v1.0', logger: false }) -const api3 = require('../index')({ version: 'v1.0', logger: false }) -const api4 = require('../index')({ version: 'v1.0', logger: false }) -const api5 = require('../index')({ version: 'v1.0', logger: false }) -const api6 = require('../index')({ version: 'v1.0', logger: false }) -const api7 = require('../index')({ version: 'v1.0', logger: false }) +const api = require("../index")({ version: "v1.0", logger: false }); +const api2 = require("../index")({ version: "v1.0", logger: false }); +const api3 = require("../index")({ version: "v1.0", logger: false }); +const api4 = require("../index")({ version: "v1.0", logger: false }); +const api5 = require("../index")({ version: "v1.0", logger: false }); +const api6 = require("../index")({ version: "v1.0", logger: false }); +const api7 = require("../index")({ version: "v1.0", logger: false }); let event = { - httpMethod: 'get', - path: '/test', + httpMethod: "get", + path: "/test", body: {}, multiValueHeaders: { - 'content-type': ['application/json'] - } -} + "content-type": ["application/json"], + }, +}; /******************************************************************************/ /*** DEFINE TEST ROUTES ***/ /******************************************************************************/ -api.get('/', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - - -api.get('/return', async function(req,res) { - return { method: 'get', status: 'ok' } -}) - -api.get('/returnNoArgs', async () => { - return { method: 'get', status: 'ok' } -}) - -api2.get('/', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - -api.get('/test', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - -api.patch('/test', function(req,res) { - res.status(200).json({ method: 'patch', status: 'ok' }) -}) - -api.get('/test_options', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - -api.get('/test_options2/:test', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - -api.post('/test', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok' }) -}) - -api.post('/test/base64', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', body: req.body }) -}) - -api.put('/test', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok' }) -}) - -api.options('/test', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok' }) -}) - -api.get('/test/:testX', function(req,res,next) { - next() -}) - -api.get('/test/:test', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok', param: req.params.test, param2: req.params.testX }) -}) - -api.post('/test/:test', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', param: req.params.test }) -}) - -api.put('/test/:test', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok', param: req.params.test }) -}) - -api.patch('/test/:test', function(req,res) { - res.status(200).json({ method: 'patch', status: 'ok', param: req.params.test }) -}) - -api.delete('/test/:test', function(req,res) { - res.status(200).json({ method: 'delete', status: 'ok', param: req.params.test }) -}) - -api.options('/test/:test', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', param: req.params.test }) -}) - -api.patch('/test/:test/:test2', function(req,res) { - res.status(200).json({ method: 'patch', status: 'ok', params: req.params }) -}) - -api.delete('/test/:test/:test2', function(req,res) { - res.status(200).json({ method: 'delete', status: 'ok', params: req.params }) -}) - -api.get('/test/:test/query', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok', param: req.params.test, query: req.query, multiValueQuery: req.multiValueQuery }) -}) - -api.post('/test/:test/query', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', param: req.params.test, query: req.query, multiValueQuery: req.multiValueQuery }) -}) - -api.put('/test/:test/query', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok', param: req.params.test, query: req.query, multiValueQuery: req.multiValueQuery }) -}) - -api.options('/test/:test/query', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', param: req.params.test, query: req.query, multiValueQuery: req.multiValueQuery }) -}) - -api.get('/test/:test/query/:test2', function(req,res) { - res.status(200).json({ method: 'get', status: 'ok', params: req.params, query: req.query.test }) -}) - -api.post('/test/:test/query/:test2', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', params: req.params, query: req.query.test }) -}) - -api.put('/test/:test/query/:test2', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok', params: req.params, query: req.query.test }) -}) - -api.options('/test/:test/query/:test2', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', params: req.params, query: req.query.test }) -}) - -api.post('/test/json', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', body: req.body }) -}) - -api.post('/test/form', function(req,res) { - res.status(200).json({ method: 'post', status: 'ok', body: req.body }) -}) - -api.put('/test/json', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok', body: req.body }) -}) - -api.put('/test/form', function(req,res) { - res.status(200).json({ method: 'put', status: 'ok', body: req.body }) -}) - -api.METHOD('TEST','/test/:param1/queryx', function(req,res) { - res.status(200).json({ method: 'test', status: 'ok', body: req.body }) -}) - -api.METHOD('TEST','/test_options2/:param1/test', function(req,res) { - res.status(200).json({ method: 'test', status: 'ok', body: req.body }) -}) - -api.options('/test_options2/:param1/*', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', path: '/test_options2/:param1/*', params:req.params}) -}) - -api.options('/test_options2/*', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', path: '/test_options2/*'}) -}) - -api.options('/*', function(req,res) { - res.status(200).json({ method: 'options', status: 'ok', path: '/*'}) -}) - -api.get('/override/head/request', (req,res) => { - res.status(200).header('method','get').json({ method: 'get', path: '/override/head/request' }) -}) - -api.head('/override/head/request', (req,res) => { - res.status(200).header('method','head').json({ method: 'head', path: '/override/head/request' }) -}) - -api.any('/any', (req,res) => { - res.status(200).json({ method: req.method, path: '/any', anyRoute: true }) -}) - -api.any('/any2', function any2(req,res) { - res.status(200).json({ method: req.method, path: '/any2', anyRoute: true }) -}) - -api.post('/any2', function any2post(req,res) { - res.status(200).json({ method: req.method, path: '/any2', anyRoute: false }) -}) - -api.options('/anywildcard/test', (req,res) => { - res.status(200).json({ method: req.method, path: '/anywildcard', anyRoute: true }) -}) - -api.any('/anywildcard/*', (req,res) => { - res.status(200).json({ method: req.method, path: '/anywildcard', anyRoute: true }) -}) - -api.get('/head/override', (req,res) => { - res.status(200).header('wildcard',false).json({ }) -}) - -api.head('/head/*', (req,res) => { - res.status(200).header('wildcard',true).json({ }) -}) - -api.get('/methodNotAllowed', (req,res) => { - res.send({status: 'OK'}) -}) +api.get("/", function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api.get("/return", async function (req, res) { + return { method: "get", status: "ok" }; +}); + +api.get("/returnNoArgs", async () => { + return { method: "get", status: "ok" }; +}); + +api2.get("/", function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api.get("/test", function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api.patch("/test", function (req, res) { + res.status(200).json({ method: "patch", status: "ok" }); +}); + +api.get("/test_options", function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api.get("/test_options2/:test", function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api.post("/test", function (req, res) { + res.status(200).json({ method: "post", status: "ok" }); +}); + +api.post("/test/base64", function (req, res) { + res.status(200).json({ method: "post", status: "ok", body: req.body }); +}); + +api.put("/test", function (req, res) { + res.status(200).json({ method: "put", status: "ok" }); +}); + +api.options("/test", function (req, res) { + res.status(200).json({ method: "options", status: "ok" }); +}); + +api.get("/test/:testX", function (req, res, next) { + next(); +}); + +api.get("/test/:test", function (req, res) { + res.status(200).json({ + method: "get", + status: "ok", + param: req.params.test, + param2: req.params.testX, + }); +}); + +api.post("/test/:test", function (req, res) { + res + .status(200) + .json({ method: "post", status: "ok", param: req.params.test }); +}); + +api.put("/test/:test", function (req, res) { + res.status(200).json({ method: "put", status: "ok", param: req.params.test }); +}); + +api.patch("/test/:test", function (req, res) { + res + .status(200) + .json({ method: "patch", status: "ok", param: req.params.test }); +}); + +api.delete("/test/:test", function (req, res) { + res + .status(200) + .json({ method: "delete", status: "ok", param: req.params.test }); +}); + +api.options("/test/:test", function (req, res) { + res + .status(200) + .json({ method: "options", status: "ok", param: req.params.test }); +}); + +api.patch("/test/:test/:test2", function (req, res) { + res.status(200).json({ method: "patch", status: "ok", params: req.params }); +}); + +api.delete("/test/:test/:test2", function (req, res) { + res.status(200).json({ method: "delete", status: "ok", params: req.params }); +}); + +api.get("/test/:test/query", function (req, res) { + res.status(200).json({ + method: "get", + status: "ok", + param: req.params.test, + query: req.query, + multiValueQuery: req.multiValueQuery, + }); +}); + +api.post("/test/:test/query", function (req, res) { + res.status(200).json({ + method: "post", + status: "ok", + param: req.params.test, + query: req.query, + multiValueQuery: req.multiValueQuery, + }); +}); + +api.put("/test/:test/query", function (req, res) { + res.status(200).json({ + method: "put", + status: "ok", + param: req.params.test, + query: req.query, + multiValueQuery: req.multiValueQuery, + }); +}); + +api.options("/test/:test/query", function (req, res) { + res.status(200).json({ + method: "options", + status: "ok", + param: req.params.test, + query: req.query, + multiValueQuery: req.multiValueQuery, + }); +}); + +api.get("/test/:test/query/:test2", function (req, res) { + res.status(200).json({ + method: "get", + status: "ok", + params: req.params, + query: req.query.test, + }); +}); + +api.post("/test/:test/query/:test2", function (req, res) { + res.status(200).json({ + method: "post", + status: "ok", + params: req.params, + query: req.query.test, + }); +}); + +api.put("/test/:test/query/:test2", function (req, res) { + res.status(200).json({ + method: "put", + status: "ok", + params: req.params, + query: req.query.test, + }); +}); + +api.options("/test/:test/query/:test2", function (req, res) { + res.status(200).json({ + method: "options", + status: "ok", + params: req.params, + query: req.query.test, + }); +}); + +api.post("/test/json", function (req, res) { + res.status(200).json({ method: "post", status: "ok", body: req.body }); +}); + +api.post("/test/form", function (req, res) { + res.status(200).json({ method: "post", status: "ok", body: req.body }); +}); + +api.put("/test/json", function (req, res) { + res.status(200).json({ method: "put", status: "ok", body: req.body }); +}); + +api.put("/test/form", function (req, res) { + res.status(200).json({ method: "put", status: "ok", body: req.body }); +}); + +api.METHOD("TEST", "/test/:param1/queryx", function (req, res) { + res.status(200).json({ method: "test", status: "ok", body: req.body }); +}); + +api.METHOD("TEST", "/test_options2/:param1/test", function (req, res) { + res.status(200).json({ method: "test", status: "ok", body: req.body }); +}); + +api.options("/test_options2/:param1/*", function (req, res) { + res.status(200).json({ + method: "options", + status: "ok", + path: "/test_options2/:param1/*", + params: req.params, + }); +}); + +api.options("/test_options2/*", function (req, res) { + res + .status(200) + .json({ method: "options", status: "ok", path: "/test_options2/*" }); +}); + +api.options("/*", function (req, res) { + res.status(200).json({ method: "options", status: "ok", path: "/*" }); +}); + +api.get("/override/head/request", (req, res) => { + res + .status(200) + .header("method", "get") + .json({ method: "get", path: "/override/head/request" }); +}); + +api.head("/override/head/request", (req, res) => { + res + .status(200) + .header("method", "head") + .json({ method: "head", path: "/override/head/request" }); +}); + +api.any("/any", (req, res) => { + res.status(200).json({ method: req.method, path: "/any", anyRoute: true }); +}); + +api.any("/any2", function any2(req, res) { + res.status(200).json({ method: req.method, path: "/any2", anyRoute: true }); +}); + +api.post("/any2", function any2post(req, res) { + res.status(200).json({ method: req.method, path: "/any2", anyRoute: false }); +}); + +api.options("/anywildcard/test", (req, res) => { + res + .status(200) + .json({ method: req.method, path: "/anywildcard", anyRoute: true }); +}); + +api.any("/anywildcard/*", (req, res) => { + res + .status(200) + .json({ method: req.method, path: "/anywildcard", anyRoute: true }); +}); + +api.get("/head/override", (req, res) => { + res.status(200).header("wildcard", false).json({}); +}); + +api.head("/head/*", (req, res) => { + res.status(200).header("wildcard", true).json({}); +}); + +api.get("/methodNotAllowed", (req, res) => { + res.send({ status: "OK" }); +}); // Multi methods -api3.METHOD('get,post','/multimethod/test', (req,res) => { - res.status(200).json({ method: req.method, path: '/multimethod/test' }) -}) -api3.METHOD(['get','put','delete'],'/multimethod/:var', (req,res) => { - res.status(200).json({ method: req.method, path: '/multimethod/:var' }) -}) -api3.METHOD([1,'DELETE'],'/multimethod/badtype', (req,res) => { - res.status(200).json({ method: req.method, path: '/multimethod/badtype' }) -}) - -api4.get('/test/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path, nested: "true" }) -}) - -api4.get('/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path }) -}) - -api4.options('/test/test/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path, nested: "true" }) -}) - -api4.options('/test/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path, nested: "true" }) -}) - -api4.post('/test/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path, nested: "true" }) -}) - -api4.post('/*', (req,res) => { - res.status(200).header('wildcard',true).json({ method: req.method, path: req.path }) -}) +api3.METHOD("get,post", "/multimethod/test", (req, res) => { + res.status(200).json({ method: req.method, path: "/multimethod/test" }); +}); +api3.METHOD(["get", "put", "delete"], "/multimethod/:var", (req, res) => { + res.status(200).json({ method: req.method, path: "/multimethod/:var" }); +}); +api3.METHOD([1, "DELETE"], "/multimethod/badtype", (req, res) => { + res.status(200).json({ method: req.method, path: "/multimethod/badtype" }); +}); + +api4.get("/test/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path, nested: "true" }); +}); + +api4.get("/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path }); +}); + +api4.options("/test/test/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path, nested: "true" }); +}); + +api4.options("/test/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path, nested: "true" }); +}); + +api4.post("/test/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path, nested: "true" }); +}); + +api4.post("/*", (req, res) => { + res + .status(200) + .header("wildcard", true) + .json({ method: req.method, path: req.path }); +}); // Default route -api5.get(function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - -api5.METHOD('any',function(req,res) { - res.status(200).json({ method: 'any', status: 'ok' }) -}) - -api6.any('/*', function anyWildcard(req,res,next) { next() }) -api6.get('/*', function getWildcard(req,res,next) { next() }) -api6.get('/test', function testHandler(req,res) { - res.send({ status: 'ok' }) -}) - -api7.get(function(req,res) { - res.status(200).json({ method: 'get', status: 'ok' }) -}) - +api5.get(function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); + +api5.METHOD("any", function (req, res) { + res.status(200).json({ method: "any", status: "ok" }); +}); + +api6.any("/*", function anyWildcard(req, res, next) { + next(); +}); +api6.get("/*", function getWildcard(req, res, next) { + next(); +}); +api6.get("/test", function testHandler(req, res) { + res.send({ status: "ok" }); +}); + +api7.get(function (req, res) { + res.status(200).json({ method: "get", status: "ok" }); +}); /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Route Tests:', function() { - +describe("Route Tests:", function () { /*****************/ /*** GET Tests ***/ /*****************/ - describe('GET', function() { - - it('Base path: /', async function() { - let _event = Object.assign({},event,{ path: '/' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + describe("GET", function () { + it("Base path: /", async function () { + let _event = Object.assign({}, event, { path: "/" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"get","status":"ok"}', - isBase64Encoded: false - }) - }) // end it - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) - }) // end it - - - it('Simple path w/ async return', async function() { - let _event = Object.assign({},event,{ path: '/return' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, {}); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"get","status":"ok"}', - isBase64Encoded: false - }) - }) // end it - - - it('Simple path w/ async return (no args)', async function() { - let _event = Object.assign({},event,{ path: '/returnNoArgs' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ async return", async function () { + let _event = Object.assign({}, event, { path: "/return" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ async return (no args)", async function () { + let _event = Object.assign({}, event, { path: "/returnNoArgs" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Simple path, no `context`", async function () { + let _event = Object.assign({}, event, {}); + let result = await new Promise((r) => + api.run(_event, null, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"get","status":"ok"}', - isBase64Encoded: false - }) - }) // end it - - - it('Simple path, no `context`', async function() { - let _event = Object.assign({},event,{}) - let result = await new Promise(r => api.run(_event,null,(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Simple path w/ trailing slash: /test/', async function() { - let _event = Object.assign({},event,{ path: '/test/' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","param2":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and querystring: /test/123/query/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and multiple querystring: /test/123/query/?test=123&test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', multiValueQueryStringParameters: { test: ['123', '321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters and querystring: /test/123/query/456/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/456', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) - }) // end it - - - it('Event path + querystring w/ trailing slash (this shouldn\'t happen with API Gateway)', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/?test=321', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - it('Event path + querystring w/o trailing slash (this shouldn\'t happen with API Gateway)', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query?test=321', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found (new api instance)', async function() { - let _event = Object.assign({},event,{ path: '/not_found' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard: /*', async function() { - let _event = Object.assign({},event,{ path: '/foo/bar' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ trailing slash: /test/", async function () { + let _event = Object.assign({}, event, { path: "/test/" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { path: "/test/123" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","param":"123","param2":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and querystring: /test/123/query/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and multiple querystring: /test/123/query/?test=123&test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + multiValueQueryStringParameters: { test: ["123", "321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters and querystring: /test/123/query/456/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/456", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', + isBase64Encoded: false, + }); + }); // end it + + it("Event path + querystring w/ trailing slash (this shouldn't happen with API Gateway)", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/?test=321", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Event path + querystring w/o trailing slash (this shouldn't happen with API Gateway)", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query?test=321", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { path: "/not_found" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found (new api instance)", async function () { + let _event = Object.assign({}, event, { path: "/not_found" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /*", async function () { + let _event = Object.assign({}, event, { path: "/foo/bar" }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); // console.log(JSON.stringify(api4._routes,null,2)); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"GET","path":"/foo/bar"}', - isBase64Encoded: false - }) - }) // end it - - it('Wildcard: /test/*', async function() { - let _event = Object.assign({},event,{ path: '/test/foo/bar' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /test/*", async function () { + let _event = Object.assign({}, event, { path: "/test/foo/bar" }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"GET","path":"/test/foo/bar","nested":"true"}', - isBase64Encoded: false - }) - }) // end it - - it('Default path', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Method not allowed', async function() { - let _event = Object.assign({},event,{ path: '/methodNotAllowed', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 405, body: '{"error":"Method not allowed"}', isBase64Encoded: false }) - }) // end it - - - it('Method not allowed (/* path - valid method)', async function() { - let _event = Object.assign({},event,{ path: '/methodNotAllowedStar', httpMethod: 'get' }) - let result = await new Promise(r => api7.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"get","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Method not allowed (/* path)', async function() { - let _event = Object.assign({},event,{ path: '/methodNotAllowedStar', httpMethod: 'post' }) - let result = await new Promise(r => api7.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 405, body: '{"error":"Method not allowed"}', isBase64Encoded: false }) - }) // end it - - }) // end GET tests - + isBase64Encoded: false, + }); + }); // end it + + it("Default path", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api5.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Method not allowed", async function () { + let _event = Object.assign({}, event, { + path: "/methodNotAllowed", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 405, + body: '{"error":"Method not allowed"}', + isBase64Encoded: false, + }); + }); // end it + + it("Method not allowed (/* path - valid method)", async function () { + let _event = Object.assign({}, event, { + path: "/methodNotAllowedStar", + httpMethod: "get", + }); + let result = await new Promise((r) => + api7.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"get","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Method not allowed (/* path)", async function () { + let _event = Object.assign({}, event, { + path: "/methodNotAllowedStar", + httpMethod: "post", + }); + let result = await new Promise((r) => + api7.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 405, + body: '{"error":"Method not allowed"}', + isBase64Encoded: false, + }); + }); // end it + }); // end GET tests /******************/ /*** HEAD Tests ***/ /******************/ - describe('HEAD', function() { - - it('Base path: /', async function() { - let _event = Object.assign({},event,{ path: '/', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, - statusCode: 200, - body: '', - isBase64Encoded: false - }) - }) // end it - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{ httpMethod: 'head'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Simple path w/ trailing slash: /test/', async function() { - let _event = Object.assign({},event,{ path: '/test/', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and querystring: /test/123/query/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'head', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and multiple querystring: /test/123/query/?test=123&test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'head', multiValueQueryStringParameters: { test: ['123', '321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters and querystring: /test/123/query/456/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/456', httpMethod: 'head', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Event path + querystring w/ trailing slash (this shouldn\'t happen with API Gateway)', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/?test=321', httpMethod: 'head', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Event path + querystring w/o trailing slash (this shouldn\'t happen with API Gateway)', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query?test=321', httpMethod: 'head', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '', isBase64Encoded: false }) - }) // end it - - it('Override HEAD request', async function() { - let _event = Object.assign({},event,{ path: '/override/head/request', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'], 'method': ['head'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - it('Wildcard HEAD request', async function() { - let _event = Object.assign({},event,{ path: '/head/override', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - }) // end HEAD tests + describe("HEAD", function () { + it("Base path: /", async function () { + let _event = Object.assign({}, event, { path: "/", httpMethod: "head" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, { httpMethod: "head" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ trailing slash: /test/", async function () { + let _event = Object.assign({}, event, { + path: "/test/", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and querystring: /test/123/query/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "head", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and multiple querystring: /test/123/query/?test=123&test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "head", + multiValueQueryStringParameters: { test: ["123", "321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters and querystring: /test/123/query/456/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/456", + httpMethod: "head", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Event path + querystring w/ trailing slash (this shouldn't happen with API Gateway)", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/?test=321", + httpMethod: "head", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Event path + querystring w/o trailing slash (this shouldn't happen with API Gateway)", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query?test=321", + httpMethod: "head", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Override HEAD request", async function () { + let _event = Object.assign({}, event, { + path: "/override/head/request", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { + "content-type": ["application/json"], + method: ["head"], + }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard HEAD request", async function () { + let _event = Object.assign({}, event, { + path: "/head/override", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + }); // end HEAD tests /******************/ /*** POST Tests ***/ /******************/ - describe('POST', function() { - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{ httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Simple path w/ trailing slash: /test/', async function() { - let _event = Object.assign({},event,{ path: '/test/', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and querystring: /test/123/query/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'post', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and multiple querystring: /test/123/query/?test=123&test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'post', multiValueQueryStringParameters: { test: ['123', '321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters and querystring: /test/123/query/456/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/456', httpMethod: 'post', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) - }) // end it - - it('With JSON body: /test/json', async function() { - let _event = Object.assign({},event,{ path: '/test/json', httpMethod: 'post', body: { test: '123' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) - }) // end it - - it('With stringified JSON body: /test/json', async function() { - let _event = Object.assign({},event,{ path: '/test/json', httpMethod: 'post', body: JSON.stringify({ test: '123' }) }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'post', body: 'test=123&test2=456', multiValueHeaders: { 'Content-Type': ['application/x-www-form-urlencoded'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With "x-www-form-urlencoded; charset=UTF-8" body: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'post', body: 'test=123&test2=456', multiValueHeaders: { 'Content-Type': ['application/x-www-form-urlencoded; charset=UTF-8'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body and lowercase "Content-Type" header: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'post', body: 'test=123&test2=456', multiValueHeaders: { 'content-type': ['application/x-www-form-urlencoded; charset=UTF-8'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body and mixed case "Content-Type" header: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'post', body: 'test=123&test2=456', multiValueHeaders: { 'CoNtEnt-TYPe': ['application/x-www-form-urlencoded'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With base64 encoded body', async function() { - let _event = Object.assign({},event,{ path: '/test/base64', httpMethod: 'post', body: 'VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=', isBase64Encoded: true }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":"Test file for sendFile\\n"}', isBase64Encoded: false }) - }) // end it - - it('With base64 encoding flagged and no body', async function() { - let _event = Object.assign({},event,{ path: '/test/base64', httpMethod: 'post', body: undefined, isBase64Encoded: true }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"post","status":"ok","body":""}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard: /*', async function() { - let _event = Object.assign({},event,{ path: '/foo/bar', httpMethod: 'post' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + describe("POST", function () { + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, { httpMethod: "post" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ trailing slash: /test/", async function () { + let _event = Object.assign({}, event, { + path: "/test/", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","param":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and querystring: /test/123/query/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "post", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and multiple querystring: /test/123/query/?test=123&test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "post", + multiValueQueryStringParameters: { test: ["123", "321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters and querystring: /test/123/query/456/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/456", + httpMethod: "post", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', + isBase64Encoded: false, + }); + }); // end it + + it("With JSON body: /test/json", async function () { + let _event = Object.assign({}, event, { + path: "/test/json", + httpMethod: "post", + body: { test: "123" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With stringified JSON body: /test/json", async function () { + let _event = Object.assign({}, event, { + path: "/test/json", + httpMethod: "post", + body: JSON.stringify({ test: "123" }), + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With x-www-form-urlencoded body: /test/form", async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "post", + body: "test=123&test2=456", + multiValueHeaders: { + "Content-Type": ["application/x-www-form-urlencoded"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With "x-www-form-urlencoded; charset=UTF-8" body: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "post", + body: "test=123&test2=456", + multiValueHeaders: { + "Content-Type": ["application/x-www-form-urlencoded; charset=UTF-8"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With x-www-form-urlencoded body and lowercase "Content-Type" header: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "post", + body: "test=123&test2=456", + multiValueHeaders: { + "content-type": ["application/x-www-form-urlencoded; charset=UTF-8"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With x-www-form-urlencoded body and mixed case "Content-Type" header: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "post", + body: "test=123&test2=456", + multiValueHeaders: { + "CoNtEnt-TYPe": ["application/x-www-form-urlencoded"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With base64 encoded body", async function () { + let _event = Object.assign({}, event, { + path: "/test/base64", + httpMethod: "post", + body: "VGVzdCBmaWxlIGZvciBzZW5kRmlsZQo=", + isBase64Encoded: true, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":"Test file for sendFile\\n"}', + isBase64Encoded: false, + }); + }); // end it + + it("With base64 encoding flagged and no body", async function () { + let _event = Object.assign({}, event, { + path: "/test/base64", + httpMethod: "post", + body: undefined, + isBase64Encoded: true, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"post","status":"ok","body":""}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /*", async function () { + let _event = Object.assign({}, event, { + path: "/foo/bar", + httpMethod: "post", + }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"POST","path":"/foo/bar"}', - isBase64Encoded: false - }) - }) // end it - - it('Wildcard: /test/*', async function() { - let _event = Object.assign({},event,{ path: '/test/foo/bar', httpMethod: 'post' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /test/*", async function () { + let _event = Object.assign({}, event, { + path: "/test/foo/bar", + httpMethod: "post", + }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"POST","path":"/test/foo/bar","nested":"true"}', - isBase64Encoded: false - }) - }) // end it - - }) // end POST tests - + isBase64Encoded: false, + }); + }); // end it + }); // end POST tests /*****************/ /*** PUT Tests ***/ /*****************/ - describe('PUT', function() { - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{ httpMethod: 'put' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Simple path w/ trailing slash: /test/', async function() { - let _event = Object.assign({},event,{ path: '/test/', httpMethod: 'put' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'put' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and querystring: /test/123/query/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'put', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and multiple querystring: /test/123/query/?test=123&test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'put', multiValueQueryStringParameters: { test: ['123', '321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters and querystring: /test/123/query/456/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/456', httpMethod: 'put', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) - }) // end it - - - it('With JSON body: /test/json', async function() { - let _event = Object.assign({},event,{ path: '/test/json', httpMethod: 'put', body: { test: '123' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) - }) // end it - - it('With stringified JSON body: /test/json', async function() { - let _event = Object.assign({},event,{ path: '/test/json', httpMethod: 'put', body: JSON.stringify({ test: '123' }) }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'put', body: 'test=123&test2=456', multiValueHeaders: { 'content-type': ['application/x-www-form-urlencoded'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With "x-www-form-urlencoded; charset=UTF-8" body: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'put', body: 'test=123&test2=456', multiValueHeaders: { 'content-type': ['application/x-www-form-urlencoded; charset=UTF-8'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body and lowercase "Content-Type" header: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'put', body: 'test=123&test2=456', multiValueHeaders: { 'content-type': ['application/x-www-form-urlencoded; charset=UTF-8'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('With x-www-form-urlencoded body and mixed case "Content-Type" header: /test/form', async function() { - let _event = Object.assign({},event,{ path: '/test/form', httpMethod: 'put', body: 'test=123&test2=456', multiValueHeaders: { 'CoNtEnt-TYPe': ['application/x-www-form-urlencoded'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'put' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - }) // end PUT tests - + describe("PUT", function () { + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, { httpMethod: "put" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ trailing slash: /test/", async function () { + let _event = Object.assign({}, event, { + path: "/test/", + httpMethod: "put", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "put", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","param":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and querystring: /test/123/query/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "put", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and multiple querystring: /test/123/query/?test=123&test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "put", + multiValueQueryStringParameters: { test: ["123", "321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters and querystring: /test/123/query/456/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/456", + httpMethod: "put", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', + isBase64Encoded: false, + }); + }); // end it + + it("With JSON body: /test/json", async function () { + let _event = Object.assign({}, event, { + path: "/test/json", + httpMethod: "put", + body: { test: "123" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With stringified JSON body: /test/json", async function () { + let _event = Object.assign({}, event, { + path: "/test/json", + httpMethod: "put", + body: JSON.stringify({ test: "123" }), + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123"}}', + isBase64Encoded: false, + }); + }); // end it + + it("With x-www-form-urlencoded body: /test/form", async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "put", + body: "test=123&test2=456", + multiValueHeaders: { + "content-type": ["application/x-www-form-urlencoded"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With "x-www-form-urlencoded; charset=UTF-8" body: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "put", + body: "test=123&test2=456", + multiValueHeaders: { + "content-type": ["application/x-www-form-urlencoded; charset=UTF-8"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With x-www-form-urlencoded body and lowercase "Content-Type" header: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "put", + body: "test=123&test2=456", + multiValueHeaders: { + "content-type": ["application/x-www-form-urlencoded; charset=UTF-8"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it('With x-www-form-urlencoded body and mixed case "Content-Type" header: /test/form', async function () { + let _event = Object.assign({}, event, { + path: "/test/form", + httpMethod: "put", + body: "test=123&test2=456", + multiValueHeaders: { + "CoNtEnt-TYPe": ["application/x-www-form-urlencoded"], + }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"put","status":"ok","body":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "put", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + }); // end PUT tests /********************/ /*** PATCH Tests ***/ /********************/ - describe('PATCH', function() { - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{ path: '/test', httpMethod: 'patch'}) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"patch","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'patch' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"patch","status":"ok","param":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters: /test/123/456', async function() { - let _event = Object.assign({},event,{ path: '/test/123/456', httpMethod: 'patch' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"patch","status":"ok","params":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'patch' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - }) // end PATCH tests + describe("PATCH", function () { + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, { + path: "/test", + httpMethod: "patch", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"patch","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "patch", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"patch","status":"ok","param":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters: /test/123/456", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/456", + httpMethod: "patch", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"patch","status":"ok","params":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "patch", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + }); // end PATCH tests /********************/ /*** DELETE Tests ***/ /********************/ - describe('DELETE', function() { - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'delete' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"delete","status":"ok","param":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters: /test/123/456', async function() { - let _event = Object.assign({},event,{ path: '/test/123/456', httpMethod: 'delete' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"delete","status":"ok","params":{"test":"123","test2":"456"}}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'delete' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - }) // end DELETE tests - + describe("DELETE", function () { + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "delete", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"delete","status":"ok","param":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters: /test/123/456", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/456", + httpMethod: "delete", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"delete","status":"ok","params":{"test":"123","test2":"456"}}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "delete", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + }); // end DELETE tests /*********************/ /*** OPTIONS Tests ***/ /*********************/ - describe('OPTIONS', function() { - - it('Simple path: /test', async function() { - let _event = Object.assign({},event,{ httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Simple path w/ trailing slash: /test/', async function() { - let _event = Object.assign({},event,{ path: '/test/', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter: /test/123', async function() { - let _event = Object.assign({},event,{ path: '/test/123', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123"}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and querystring: /test/123/query/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'options', queryStringParameters: { test: '321' }, multiValueQueryStringParameters: { test: ['321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with parameter and multiple querystring: /test/123/query/?test=123&test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query', httpMethod: 'options', multiValueQueryStringParameters: { test: ['123', '321'] } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', isBase64Encoded: false }) - }) // end it - - it('Path with multiple parameters and querystring: /test/123/query/456/?test=321', async function() { - let _event = Object.assign({},event,{ path: '/test/123/query/456', httpMethod: 'options', queryStringParameters: { test: '321' } }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard: /test_options', async function() { - let _event = Object.assign({},event,{ path: '/test_options', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard with path: /test_options2/123', async function() { - let _event = Object.assign({},event,{ path: '/test_options2/123', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/test_options2/*"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard with deep path: /test/param1/queryx', async function() { - let _event = Object.assign({},event,{ path: '/test/param1/queryx', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/*"}', isBase64Encoded: false }) - }) // end it - - it('Nested Wildcard: /test_options2', async function() { - let _event = Object.assign({},event,{ path: '/test_options2/test', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/test_options2/*"}', isBase64Encoded: false }) - }) // end it - - it('Nested Wildcard with parameters: /test_options2/param1/test', async function() { - let _event = Object.assign({},event,{ path: '/test_options2/param1/test', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"options","status":"ok","path":"/test_options2/:param1/*","params":{"param1":"param1"}}', isBase64Encoded: false }) - }) // end it - - it('Missing path: /not_found', async function() { - let _event = Object.assign({},event,{ path: '/not_found', httpMethod: 'options' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 404, body: '{"error":"Route not found"}', isBase64Encoded: false }) - }) // end it - - it('Wildcard: /test/*', async function() { - let _event = Object.assign({},event,{ path: '/test/foo/bar', httpMethod: 'options' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + describe("OPTIONS", function () { + it("Simple path: /test", async function () { + let _event = Object.assign({}, event, { httpMethod: "options" }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Simple path w/ trailing slash: /test/", async function () { + let _event = Object.assign({}, event, { + path: "/test/", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter: /test/123", async function () { + let _event = Object.assign({}, event, { + path: "/test/123", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","param":"123"}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and querystring: /test/123/query/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "options", + queryStringParameters: { test: "321" }, + multiValueQueryStringParameters: { test: ["321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with parameter and multiple querystring: /test/123/query/?test=123&test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query", + httpMethod: "options", + multiValueQueryStringParameters: { test: ["123", "321"] }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","param":"123","query":{"test":"321"},"multiValueQuery":{"test":["123","321"]}}', + isBase64Encoded: false, + }); + }); // end it + + it("Path with multiple parameters and querystring: /test/123/query/456/?test=321", async function () { + let _event = Object.assign({}, event, { + path: "/test/123/query/456", + httpMethod: "options", + queryStringParameters: { test: "321" }, + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","params":{"test":"123","test2":"456"},"query":"321"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /test_options", async function () { + let _event = Object.assign({}, event, { + path: "/test_options", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","path":"/*"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard with path: /test_options2/123", async function () { + let _event = Object.assign({}, event, { + path: "/test_options2/123", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","path":"/test_options2/*"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard with deep path: /test/param1/queryx", async function () { + let _event = Object.assign({}, event, { + path: "/test/param1/queryx", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","path":"/*"}', + isBase64Encoded: false, + }); + }); // end it + + it("Nested Wildcard: /test_options2", async function () { + let _event = Object.assign({}, event, { + path: "/test_options2/test", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","path":"/test_options2/*"}', + isBase64Encoded: false, + }); + }); // end it + + it("Nested Wildcard with parameters: /test_options2/param1/test", async function () { + let _event = Object.assign({}, event, { + path: "/test_options2/param1/test", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"options","status":"ok","path":"/test_options2/:param1/*","params":{"param1":"param1"}}', + isBase64Encoded: false, + }); + }); // end it + + it("Missing path: /not_found", async function () { + let _event = Object.assign({}, event, { + path: "/not_found", + httpMethod: "options", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 404, + body: '{"error":"Route not found"}', + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /test/*", async function () { + let _event = Object.assign({}, event, { + path: "/test/foo/bar", + httpMethod: "options", + }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"OPTIONS","path":"/test/foo/bar","nested":"true"}', - isBase64Encoded: false - }) - }) // end it - - it('Wildcard: /test/test/* (higher level matching)', async function() { - let _event = Object.assign({},event,{ path: '/test/test/foo/bar', httpMethod: 'options' }) - let result = await new Promise(r => api4.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Wildcard: /test/test/* (higher level matching)", async function () { + let _event = Object.assign({}, event, { + path: "/test/test/foo/bar", + httpMethod: "options", + }); + let result = await new Promise((r) => + api4.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'], 'wildcard': [true] }, + multiValueHeaders: { + "content-type": ["application/json"], + wildcard: [true], + }, statusCode: 200, body: '{"method":"OPTIONS","path":"/test/test/foo/bar","nested":"true"}', - isBase64Encoded: false - }) - }) // end it - - }) // end OPTIONS tests - - + isBase64Encoded: false, + }); + }); // end it + }); // end OPTIONS tests /*********************/ /*** ANY Tests ***/ /*********************/ - describe('ANY', function() { - - it('GET request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'get' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"GET","path":"/any","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('POST request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"POST","path":"/any","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('PUT request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'put' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"PUT","path":"/any","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('DELETE request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'delete' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"DELETE","path":"/any","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('PATCH request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'patch' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"PATCH","path":"/any","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('HEAD request on ANY route', async function() { - let _event = Object.assign({},event,{ path: '/any', httpMethod: 'head' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '', isBase64Encoded: false }) - }) // end it - - - it('GET request on ANY route: /any2', async function() { - let _event = Object.assign({},event,{ path: '/any2', httpMethod: 'get' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"GET","path":"/any2","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - it('POST request that overrides ANY route: /any2', async function() { - let _event = Object.assign({},event,{ path: '/any2', httpMethod: 'post' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"POST","path":"/any2","anyRoute":false}', isBase64Encoded: false }) - }) // end it - - it('GET request on ANY wildcard route: /anywildcard', async function() { - let _event = Object.assign({},event,{ path: '/anywildcard/test', httpMethod: 'get' }) - let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"GET","path":"/anywildcard","anyRoute":true}', isBase64Encoded: false }) - }) // end it - - }) // end ANY tests - - - - describe('METHOD', function() { - - it('Invalid method (new api instance)', async function() { - let _event = Object.assign({},event,{ path: '/', httpMethod: 'test' }) - let result = await new Promise(r => api2.run(_event,{},(e,res) => { r(res) })) + describe("ANY", function () { + it("GET request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "get", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"GET","path":"/any","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("POST request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"POST","path":"/any","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("PUT request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "put", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"PUT","path":"/any","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("DELETE request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "delete", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"DELETE","path":"/any","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("PATCH request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "patch", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"PATCH","path":"/any","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("HEAD request on ANY route", async function () { + let _event = Object.assign({}, event, { + path: "/any", + httpMethod: "head", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: "", + isBase64Encoded: false, + }); + }); // end it + + it("GET request on ANY route: /any2", async function () { + let _event = Object.assign({}, event, { + path: "/any2", + httpMethod: "get", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"GET","path":"/any2","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + + it("POST request that overrides ANY route: /any2", async function () { + let _event = Object.assign({}, event, { + path: "/any2", + httpMethod: "post", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"POST","path":"/any2","anyRoute":false}', + isBase64Encoded: false, + }); + }); // end it + + it("GET request on ANY wildcard route: /anywildcard", async function () { + let _event = Object.assign({}, event, { + path: "/anywildcard/test", + httpMethod: "get", + }); + let result = await new Promise((r) => + api.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"GET","path":"/anywildcard","anyRoute":true}', + isBase64Encoded: false, + }); + }); // end it + }); // end ANY tests + + describe("METHOD", function () { + it("Invalid method (new api instance)", async function () { + let _event = Object.assign({}, event, { path: "/", httpMethod: "test" }); + let result = await new Promise((r) => + api2.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 405, body: '{"error":"Method not allowed"}', - isBase64Encoded: false - }) - }) // end it - - it('Multiple methods GET (string creation)', async function() { - let _event = Object.assign({},event,{ path: '/multimethod/test', httpMethod: 'get' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Multiple methods GET (string creation)", async function () { + let _event = Object.assign({}, event, { + path: "/multimethod/test", + httpMethod: "get", + }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"GET","path":"/multimethod/test"}', - isBase64Encoded: false - }) - }) // end it - - it('Multiple methods POST (string creation)', async function() { - let _event = Object.assign({},event,{ path: '/multimethod/test', httpMethod: 'post' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Multiple methods POST (string creation)", async function () { + let _event = Object.assign({}, event, { + path: "/multimethod/test", + httpMethod: "post", + }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"POST","path":"/multimethod/test"}', - isBase64Encoded: false - }) - }) // end it - - it('Multiple methods GET (array creation)', async function() { - let _event = Object.assign({},event,{ path: '/multimethod/x', httpMethod: 'get' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Multiple methods GET (array creation)", async function () { + let _event = Object.assign({}, event, { + path: "/multimethod/x", + httpMethod: "get", + }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"GET","path":"/multimethod/:var"}', - isBase64Encoded: false - }) - }) // end it - - it('Multiple methods PUT (array creation)', async function() { - let _event = Object.assign({},event,{ path: '/multimethod/x', httpMethod: 'put' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Multiple methods PUT (array creation)", async function () { + let _event = Object.assign({}, event, { + path: "/multimethod/x", + httpMethod: "put", + }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 200, body: '{"method":"PUT","path":"/multimethod/:var"}', - isBase64Encoded: false - }) - }) // end it - - it('Multiple methods POST (method not allowed)', async function() { - let _event = Object.assign({},event,{ path: '/multimethod/x', httpMethod: 'post' }) - let result = await new Promise(r => api3.run(_event,{},(e,res) => { r(res) })) + isBase64Encoded: false, + }); + }); // end it + + it("Multiple methods POST (method not allowed)", async function () { + let _event = Object.assign({}, event, { + path: "/multimethod/x", + httpMethod: "post", + }); + let result = await new Promise((r) => + api3.run(_event, {}, (e, res) => { + r(res); + }) + ); expect(result).toEqual({ - multiValueHeaders: { 'content-type': ['application/json'] }, + multiValueHeaders: { "content-type": ["application/json"] }, statusCode: 405, body: '{"error":"Method not allowed"}', - isBase64Encoded: false - }) - }) // end it - - it('Default path', async function() { - let _event = Object.assign({},event,{ path: '/test', httpMethod: 'post' }) - let result = await new Promise(r => api5.run(_event,{},(e,res) => { r(res) })) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"method":"any","status":"ok"}', isBase64Encoded: false }) - }) // end it + isBase64Encoded: false, + }); + }); // end it + + it("Default path", async function () { + let _event = Object.assign({}, event, { + path: "/test", + httpMethod: "post", + }); + let result = await new Promise((r) => + api5.run(_event, {}, (e, res) => { + r(res); + }) + ); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"method":"any","status":"ok"}', + isBase64Encoded: false, + }); + }); // end it - it('Expected routes', function() { + it("Expected routes", function () { expect(api3.routes()).toEqual([ - [ 'GET', '/multimethod/test' ], - [ 'POST', '/multimethod/test' ], - [ 'GET', '/multimethod/:var' ], - [ 'PUT', '/multimethod/:var' ], - [ 'DELETE', '/multimethod/:var' ], - [ 'DELETE', '/multimethod/badtype' ] - ]) - }) // end it - - }) // end method tests - - - describe('Configuration errors', function() { - - it('Missing handler (w/ route)', async function() { - let error + ["GET", "/multimethod/test", ["unnamed"]], + ["POST", "/multimethod/test", ["unnamed"]], + ["GET", "/multimethod/:var", ["unnamed"]], + ["PUT", "/multimethod/:var", ["unnamed"]], + ["DELETE", "/multimethod/:var", ["unnamed"]], + ["DELETE", "/multimethod/badtype", ["unnamed"]], + ]); + }); // end it + }); // end method tests + + describe("Configuration errors", function () { + it("Missing handler (w/ route)", async function () { + let error; try { - const api_error1 = require('../index')({ version: 'v1.0' }) - api_error1.get('/test-missing-handler') - } catch(e) { + const api_error1 = require("../index")({ version: "v1.0" }); + api_error1.get("/test-missing-handler"); + } catch (e) { // console.log(e); - error = e + error = e; } - expect(error.name).toBe('ConfigurationError') - expect(error.message).toBe('No handler or middleware specified for GET method on /test-missing-handler route.') - }) // end it - - it('Missing handler', async function() { - let error + expect(error.name).toBe("ConfigurationError"); + expect(error.message).toBe( + "No handler or middleware specified for GET method on /test-missing-handler route." + ); + }); // end it + + it("Missing handler", async function () { + let error; try { - const api_error1 = require('../index')({ version: 'v1.0' }) - api_error1.get() - } catch(e) { + const api_error1 = require("../index")({ version: "v1.0" }); + api_error1.get(); + } catch (e) { // console.log(e); - error = e + error = e; } - expect(error.name).toBe('ConfigurationError') - expect(error.message).toBe('No handler or middleware specified for GET method on /* route.') - }) // end it - - it('Invalid middleware', async function() { - let error + expect(error.name).toBe("ConfigurationError"); + expect(error.message).toBe( + "No handler or middleware specified for GET method on /* route." + ); + }); // end it + + it("Invalid middleware", async function () { + let error; try { - const api_error2 = require('../index')({ version: 'v1.0' }) - api_error2.use((err,req) => {}) - } catch(e) { + const api_error2 = require("../index")({ version: "v1.0" }); + api_error2.use((err, req) => {}); + } catch (e) { // console.log(e); - error = e + error = e; } - expect(error.name).toBe('ConfigurationError') - expect(error.message).toBe('Middleware must have 3 or 4 parameters') - }) // end it + expect(error.name).toBe("ConfigurationError"); + expect(error.message).toBe("Middleware must have 3 or 4 parameters"); + }); // end it - it('Invalid route-based middleware', async function() { - let error + it("Invalid route-based middleware", async function () { + let error; try { - const api_error2 = require('../index')({ version: 'v1.0' }) - api_error2.get('/',() => {},(res,req) => {},(res,req) => {}) - } catch(e) { + const api_error2 = require("../index")({ version: "v1.0" }); + api_error2.get( + "/", + () => {}, + (res, req) => {}, + (res, req) => {} + ); + } catch (e) { // console.log(e); - error = e + error = e; } - expect(error.name).toBe('ConfigurationError') - expect(error.message).toBe('Route-based middleware must have 3 parameters') - }) // end it - - it('Invalid wildcard (mid-route)', async function() { - let error + expect(error.name).toBe("ConfigurationError"); + expect(error.message).toBe( + "Route-based middleware must have 3 parameters" + ); + }); // end it + + it("Invalid wildcard (mid-route)", async function () { + let error; try { - const api_error2 = require('../index')({ version: 'v1.0' }) - api_error2.get('/test/*/test',(res,req) => {}) - } catch(e) { + const api_error2 = require("../index")({ version: "v1.0" }); + api_error2.get("/test/*/test", (res, req) => {}); + } catch (e) { // console.log(e); - error = e + error = e; } - expect(error.name).toBe('ConfigurationError') - expect(error.message).toBe('Wildcards can only be at the end of a route definition') - }) // end it - - }) // end Configuration errors - - describe('Route Method Inheritance', function() { - - it('Inherit multiple wildcard routes', async function() { - let _event = Object.assign({},event,{ path: '/test' }) - let result = await new Promise(r => api6.run(_event,{},(e,res) => { r(res) })) - expect(api6._response._request._stack.map(x => x.name)).toEqual([ 'anyWildcard', 'getWildcard', 'testHandler' ]) - expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"status":"ok"}', isBase64Encoded: false }) - }) // end it - - }) // end Route Method Inheritance - - describe('routes() (debug method)', function() { - - it('Sample routes', function() { + expect(error.name).toBe("ConfigurationError"); + expect(error.message).toBe( + "Wildcards can only be at the end of a route definition" + ); + }); // end it + }); // end Configuration errors + + describe("Route Method Inheritance", function () { + it.skip("Inherit multiple wildcard routes [any routes do not inherit]", async function () { + let _event = Object.assign({}, event, { path: "/test" }); + let result = await new Promise((r) => + api6.run(_event, {}, (e, res) => { + r(res); + }) + ); + + console.log(api6._response._request._stack); + console.log(JSON.stringify(api6._routes, null, 2)); + + expect(api6._response._request._stack.map((x) => x.name)).toEqual([ + "anyWildcard", + "getWildcard", + "testHandler", + ]); + expect(result).toEqual({ + multiValueHeaders: { "content-type": ["application/json"] }, + statusCode: 200, + body: '{"status":"ok"}', + isBase64Encoded: false, + }); + }); // end it + }); // end Route Method Inheritance + + describe("routes() (debug method)", function () { + it("Sample routes", function () { // Create an api instance - let api2 = require('../index')() - api2.get('/', (req,res) => {}) - api2.post('/test', (req,res) => {}) - api2.put('/test/put', (req,res) => {}) - api2.delete('/test/:var/delete', (req,res) => {}) + let api2 = require("../index")(); + api2.get("/", (req, res) => {}); + api2.post("/test", (req, res) => {}); + api2.put("/test/put", (req, res) => {}); + api2.delete("/test/:var/delete", (req, res) => {}); expect(api2.routes()).toEqual([ - [ 'GET', '/' ], - [ 'POST', '/test' ], - [ 'PUT', '/test/put' ], - [ 'DELETE', '/test/:var/delete' ] - ]) - }) // end it - - it('Sample routes (print)', function() { + ["GET", "/", ["unnamed"]], + ["POST", "/test", ["unnamed"]], + ["PUT", "/test/put", ["unnamed"]], + ["DELETE", "/test/:var/delete", ["unnamed"]], + ]); + }); // end it + + it("Sample routes (print)", function () { // Create an api instance - let api2 = require('../index')() - api2.get('/', (req,res) => {}) - api2.post('/test', (req,res) => {}) - api2.put('/test/put', (req,res) => {}) - api2.delete('/test/:var/delete', (req,res) => {}) - - - let _log - let logger = console.log - console.log = log => { try { _log = JSON.parse(log) } catch(e) { _log = log } } - api2.routes(true) - console.log = logger - - expect(_log).toBe('╔══════════╤═════════════════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE \u001b[0m ║\n╟──────────┼─────────────────────╢\n║ GET │ / ║\n╟──────────┼─────────────────────╢\n║ POST │ /test ║\n╟──────────┼─────────────────────╢\n║ PUT │ /test/put ║\n╟──────────┼─────────────────────╢\n║ DELETE │ /test/:var/delete ║\n╚══════════╧═════════════════════╝') - }) // end it - - }) // end routes() test - -}) // end ROUTE tests + let api2 = require("../index")(); + api2.get("/", (req, res) => {}); + api2.post("/test", (req, res) => {}); + api2.put("/test/put", (req, res) => {}); + api2.delete("/test/:var/delete", (req, res) => {}); + + let _log; + let logger = console.log; + console.log = (log) => { + try { + _log = JSON.parse(log); + } catch (e) { + _log = log; + } + }; + api2.routes(true); + console.log = logger; + + expect(_log).toBe( + "╔══════════╤═════════════════════╤═══════════╗\n║ \u001b[1mMETHOD\u001b[0m │ \u001b[1mROUTE \u001b[0m │ \u001b[1mSTACK \u001b[0m ║\n╟──────────┼─────────────────────┼───────────╢\n║ GET │ / │ unnamed ║\n╟──────────┼─────────────────────┼───────────╢\n║ POST │ /test │ unnamed ║\n╟──────────┼─────────────────────┼───────────╢\n║ PUT │ /test/put │ unnamed ║\n╟──────────┼─────────────────────┼───────────╢\n║ DELETE │ /test/:var/delete │ unnamed ║\n╚══════════╧═════════════════════╧═══════════╝" + ); + }); // end it + }); // end routes() test +}); // end ROUTE tests diff --git a/__tests__/utils.unit.js b/__tests__/utils.unit.js index c631f1f..dcb16e6 100644 --- a/__tests__/utils.unit.js +++ b/__tests__/utils.unit.js @@ -1,425 +1,453 @@ -'use strict'; +"use strict"; - - -const utils = require('../lib/utils') +const utils = require("../lib/utils"); /******************************************************************************/ /*** BEGIN TESTS ***/ /******************************************************************************/ -describe('Utility Function Tests:', function() { - - describe('escapeHtml:', function() { - - it('Escape &, <, >, ", and \'', function() { - expect(utils.escapeHtml('&<>"\'')).toBe('&<>"'') - }) // end it - - }) // end escapeHtml tests - - describe('encodeUrl:', function() { - - it('Unencoded with space in param', function() { - expect(utils.encodeUrl('http://www.github.com/?foo=bar with space')).toBe('http://www.github.com/?foo=bar%20with%20space') - }) // end it - - it('Encoded URL with additional invalid sequence', function() { - expect(utils.encodeUrl('http://www.github.com/?foo=bar%20with%20space%foo')).toBe('http://www.github.com/?foo=bar%20with%20space%25foo') - }) // end it - - it('Encode special characters, double encode, decode', function() { - let url = 'http://www.github.com/?foo=шеллы' - let encoded = utils.encodeUrl(url) - let doubleEncoded = utils.encodeUrl(encoded) - let decoded = decodeURI(encoded) - expect(encoded).toBe('http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B') - expect(doubleEncoded).toBe('http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B') - expect(decoded).toBe(url) - }) // end it - - }) // end encodeUrl tests - - - describe('encodeBody:', function() { - - it('String', function() { - expect(utils.encodeBody('test string')).toBe('test string') - }) // end it - - it('Number', function() { - expect(utils.encodeBody(123)).toBe('123') - }) // end it - - it('Array', function() { - expect(utils.encodeBody([1,2,3])).toBe('[1,2,3]') - }) // end it - - it('Object', function() { - expect(utils.encodeBody({ foo: 'bar' })).toBe('{"foo":"bar"}') - }) // end it - - }) // end encodeBody tests - - - describe('parseBody:', function() { - - it('String', function() { - expect(utils.parseBody('test string')).toBe('test string') - }) // end it - - it('Number', function() { - expect(utils.parseBody('123')).toBe(123) - }) // end it - - it('Array', function() { - expect(utils.parseBody('[1,2,3]')).toEqual([ 1, 2, 3 ]) - }) // end it - - it('Object', function() { - expect(utils.parseBody('{"foo":"bar"}')).toEqual({ foo: 'bar' }) - }) // end it - - }) // end encodeBody tests - - - describe('parseAuth:', function() { - - it('None: undefined', function() { - let result = utils.parseAuth(undefined) - expect(result).toEqual({ type: 'none', value: null }) - }) // end it - - it('None: empty', function() { - let result = utils.parseAuth('') - expect(result).toEqual({ type: 'none', value: null }) - }) // end it - - it('Invalid schema', function() { - let result = utils.parseAuth('Test 12345') - expect(result).toEqual({ type: 'none', value: null }) - }) // end it - - it('Missing value/token', function() { - let result = utils.parseAuth('Bearer') - expect(result).toEqual({ type: 'none', value: null }) - }) // end it - - it('Bearer Token (OAuth2/JWT)', function() { - let result = utils.parseAuth('Bearer XYZ') - expect(result).toEqual({ type: 'Bearer', value: 'XYZ' }) - }) // end it - - it('Digest', function() { - let result = utils.parseAuth('Digest XYZ') - expect(result).toEqual({ type: 'Digest', value: 'XYZ' }) - }) // end it - - it('OAuth 1.0', function() { - let result = utils.parseAuth('OAuth realm="Example", oauth_consumer_key="xyz", oauth_token="abc", oauth_version="1.0"') +describe("Utility Function Tests:", function () { + describe("escapeHtml:", function () { + it("Escape &, <, >, \", and '", function () { + expect(utils.escapeHtml("&<>\"'")).toBe("&<>"'"); + }); // end it + }); // end escapeHtml tests + + describe("encodeUrl:", function () { + it("Unencoded with space in param", function () { + expect(utils.encodeUrl("http://www.github.com/?foo=bar with space")).toBe( + "http://www.github.com/?foo=bar%20with%20space" + ); + }); // end it + + it("Encoded URL with additional invalid sequence", function () { + expect( + utils.encodeUrl("http://www.github.com/?foo=bar%20with%20space%foo") + ).toBe("http://www.github.com/?foo=bar%20with%20space%25foo"); + }); // end it + + it("Encode special characters, double encode, decode", function () { + let url = "http://www.github.com/?foo=шеллы"; + let encoded = utils.encodeUrl(url); + let doubleEncoded = utils.encodeUrl(encoded); + let decoded = decodeURI(encoded); + expect(encoded).toBe( + "http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + ); + expect(doubleEncoded).toBe( + "http://www.github.com/?foo=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" + ); + expect(decoded).toBe(url); + }); // end it + }); // end encodeUrl tests + + describe("encodeBody:", function () { + it("String", function () { + expect(utils.encodeBody("test string")).toBe("test string"); + }); // end it + + it("Number", function () { + expect(utils.encodeBody(123)).toBe("123"); + }); // end it + + it("Array", function () { + expect(utils.encodeBody([1, 2, 3])).toBe("[1,2,3]"); + }); // end it + + it("Object", function () { + expect(utils.encodeBody({ foo: "bar" })).toBe('{"foo":"bar"}'); + }); // end it + }); // end encodeBody tests + + describe("parseBody:", function () { + it("String", function () { + expect(utils.parseBody("test string")).toBe("test string"); + }); // end it + + it("Number", function () { + expect(utils.parseBody("123")).toBe(123); + }); // end it + + it("Array", function () { + expect(utils.parseBody("[1,2,3]")).toEqual([1, 2, 3]); + }); // end it + + it("Object", function () { + expect(utils.parseBody('{"foo":"bar"}')).toEqual({ foo: "bar" }); + }); // end it + }); // end encodeBody tests + + describe("parseAuth:", function () { + it("None: undefined", function () { + let result = utils.parseAuth(undefined); + expect(result).toEqual({ type: "none", value: null }); + }); // end it + + it("None: empty", function () { + let result = utils.parseAuth(""); + expect(result).toEqual({ type: "none", value: null }); + }); // end it + + it("Invalid schema", function () { + let result = utils.parseAuth("Test 12345"); + expect(result).toEqual({ type: "none", value: null }); + }); // end it + + it("Missing value/token", function () { + let result = utils.parseAuth("Bearer"); + expect(result).toEqual({ type: "none", value: null }); + }); // end it + + it("Bearer Token (OAuth2/JWT)", function () { + let result = utils.parseAuth("Bearer XYZ"); + expect(result).toEqual({ type: "Bearer", value: "XYZ" }); + }); // end it + + it("Digest", function () { + let result = utils.parseAuth("Digest XYZ"); + expect(result).toEqual({ type: "Digest", value: "XYZ" }); + }); // end it + + it("OAuth 1.0", function () { + let result = utils.parseAuth( + 'OAuth realm="Example", oauth_consumer_key="xyz", oauth_token="abc", oauth_version="1.0"' + ); expect(result).toEqual({ - type: 'OAuth', - value: 'realm="Example", oauth_consumer_key="xyz", oauth_token="abc", oauth_version="1.0"', - realm: 'Example', - oauth_consumer_key: 'xyz', - oauth_token: 'abc', - oauth_version: '1.0' - }) - }) // end it - - it('Basic', function() { - let creds = Buffer.from('test:testing').toString('base64') - let result = utils.parseAuth('Basic ' + creds) - expect(result).toEqual({ type: 'Basic', value: creds, username: 'test', password: 'testing' }) - }) // end it - - it('Basic (no password)', function() { - let creds = Buffer.from('test').toString('base64') - let result = utils.parseAuth('Basic ' + creds) - expect(result).toEqual({ type: 'Basic', value: creds, username: 'test', password: null }) - }) // end it - - it('Invalid type', function() { - let result = utils.parseAuth(123) - expect(result).toEqual({ type: 'none', value: null }) - }) // end it - - }) // end encodeBody tests - - - describe('mimeLookup:', function() { - - it('.pdf', function() { - expect(utils.mimeLookup('.pdf')).toBe('application/pdf') - }) // end it - - it('application/pdf', function() { - expect(utils.mimeLookup('application/pdf')).toBe('application/pdf') - }) // end it - - it('application-x/pdf (non-standard w/ slash)', function() { - expect(utils.mimeLookup('application-x/pdf')).toBe('application-x/pdf') - }) // end it - - it('xml', function() { - expect(utils.mimeLookup('xml')).toBe('application/xml') - }) // end it - - it('.html', function() { - expect(utils.mimeLookup('.html')).toBe('text/html') - }) // end it - - it('css', function() { - expect(utils.mimeLookup('css')).toBe('text/css') - }) // end it - - it('jpg', function() { - expect(utils.mimeLookup('jpg')).toBe('image/jpeg') - }) // end it - - it('.svg', function() { - expect(utils.mimeLookup('.svg')).toBe('image/svg+xml') - }) // end it - - it('docx', function() { - expect(utils.mimeLookup('docx')).toBe('application/vnd.openxmlformats-officedocument.wordprocessingml.document') - }) // end it - - it('Custom', function() { - expect(utils.mimeLookup('.mpeg', { mpeg: 'video/mpeg' })).toBe('video/mpeg') - }) // end it - - }) // end encodeBody tests - - - describe('statusLookup:', function() { - - it('200', function() { - expect(utils.statusLookup(200)).toBe('OK') - }) // end it - - it('201', function() { - expect(utils.statusLookup(201)).toBe('Created') - }) // end it - - it('304', function() { - expect(utils.statusLookup(304)).toBe('Not Modified') - }) // end it - - it('404', function() { - expect(utils.statusLookup(404)).toBe('Not Found') - }) // end it - - it('502', function() { - expect(utils.statusLookup(502)).toBe('Bad Gateway') - }) // end it - - it('999 Uknown', function() { - expect(utils.statusLookup(999)).toBe('Unknown') - }) // end it - - it('As string (parsable as int)', function() { - expect(utils.statusLookup('200')).toBe('OK') - }) // end it - - it('As string (not parsable as int)', function() { - expect(utils.statusLookup('foo')).toBe('Unknown') - }) // end it - - }) // end encodeBody tests - - - describe('extractRoutes:', function() { - - it('Sample routes', function() { + type: "OAuth", + value: + 'realm="Example", oauth_consumer_key="xyz", oauth_token="abc", oauth_version="1.0"', + realm: "Example", + oauth_consumer_key: "xyz", + oauth_token: "abc", + oauth_version: "1.0", + }); + }); // end it + + it("Basic", function () { + let creds = Buffer.from("test:testing").toString("base64"); + let result = utils.parseAuth("Basic " + creds); + expect(result).toEqual({ + type: "Basic", + value: creds, + username: "test", + password: "testing", + }); + }); // end it + + it("Basic (no password)", function () { + let creds = Buffer.from("test").toString("base64"); + let result = utils.parseAuth("Basic " + creds); + expect(result).toEqual({ + type: "Basic", + value: creds, + username: "test", + password: null, + }); + }); // end it + + it("Invalid type", function () { + let result = utils.parseAuth(123); + expect(result).toEqual({ type: "none", value: null }); + }); // end it + }); // end encodeBody tests + + describe("mimeLookup:", function () { + it(".pdf", function () { + expect(utils.mimeLookup(".pdf")).toBe("application/pdf"); + }); // end it + + it("application/pdf", function () { + expect(utils.mimeLookup("application/pdf")).toBe("application/pdf"); + }); // end it + + it("application-x/pdf (non-standard w/ slash)", function () { + expect(utils.mimeLookup("application-x/pdf")).toBe("application-x/pdf"); + }); // end it + + it("xml", function () { + expect(utils.mimeLookup("xml")).toBe("application/xml"); + }); // end it + + it(".html", function () { + expect(utils.mimeLookup(".html")).toBe("text/html"); + }); // end it + + it("css", function () { + expect(utils.mimeLookup("css")).toBe("text/css"); + }); // end it + + it("jpg", function () { + expect(utils.mimeLookup("jpg")).toBe("image/jpeg"); + }); // end it + + it(".svg", function () { + expect(utils.mimeLookup(".svg")).toBe("image/svg+xml"); + }); // end it + + it("docx", function () { + expect(utils.mimeLookup("docx")).toBe( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ); + }); // end it + + it("Custom", function () { + expect(utils.mimeLookup(".mpeg", { mpeg: "video/mpeg" })).toBe( + "video/mpeg" + ); + }); // end it + }); // end encodeBody tests + + describe("statusLookup:", function () { + it("200", function () { + expect(utils.statusLookup(200)).toBe("OK"); + }); // end it + + it("201", function () { + expect(utils.statusLookup(201)).toBe("Created"); + }); // end it + + it("304", function () { + expect(utils.statusLookup(304)).toBe("Not Modified"); + }); // end it + + it("404", function () { + expect(utils.statusLookup(404)).toBe("Not Found"); + }); // end it + + it("502", function () { + expect(utils.statusLookup(502)).toBe("Bad Gateway"); + }); // end it + + it("999 Uknown", function () { + expect(utils.statusLookup(999)).toBe("Unknown"); + }); // end it + + it("As string (parsable as int)", function () { + expect(utils.statusLookup("200")).toBe("OK"); + }); // end it + + it("As string (not parsable as int)", function () { + expect(utils.statusLookup("foo")).toBe("Unknown"); + }); // end it + }); // end encodeBody tests + + describe("extractRoutes:", function () { + it("Sample routes", function () { // Create an api instance - let api = require('../index')() - api.get('/', (req,res) => {}) - api.post('/test', (req,res) => {}) - api.put('/test/put', (req,res) => {}) - api.delete('/test/:var/delete', (req,res) => {}) + let api = require("../index")(); + api.get("/", (req, res) => {}); + api.post("/test", (req, res) => {}); + api.put("/test/put", (req, res) => {}); + api.delete("/test/:var/delete", (req, res) => {}); expect(utils.extractRoutes(api._routes)).toEqual([ - [ 'GET', '/' ], - [ 'POST', '/test' ], - [ 'PUT', '/test/put' ], - [ 'DELETE', '/test/:var/delete' ] - ]) - }) // end it - - it('No routes', function() { + ["GET", "/", ["unnamed"]], + ["POST", "/test", ["unnamed"]], + ["PUT", "/test/put", ["unnamed"]], + ["DELETE", "/test/:var/delete", ["unnamed"]], + ]); + }); // end it + + it("No routes", function () { // Create an api instance - let api = require('../index')() + let api = require("../index")(); - expect(utils.extractRoutes(api._routes)).toEqual([]) - }) // end it + expect(utils.extractRoutes(api._routes)).toEqual([]); + }); // end it - it('Prefixed routes', function() { + it("Prefixed routes", function () { // Create an api instance - let api = require('../index')() - - api.register((apix,opts) => { - apix.get('/', (req,res) => {}) - apix.post('/test', (req,res) => {}) - }, { prefix: '/v1' }) - api.get('/', (req,res) => {}) - api.post('/test', (req,res) => {}) - api.put('/test/put', (req,res) => {}) - api.delete('/test/:var/delete', (req,res) => {}) + let api = require("../index")(); + + api.register( + (apix, opts) => { + apix.get("/", (req, res) => {}); + apix.post("/test", (req, res) => {}); + }, + { prefix: "/v1" } + ); + api.get("/", (req, res) => {}); + api.post("/test", (req, res) => {}); + api.put("/test/put", (req, res) => {}); + api.delete("/test/:var/delete", (req, res) => {}); expect(utils.extractRoutes(api._routes)).toEqual([ - [ 'GET', '/v1' ], - [ 'POST', '/v1/test' ], - [ 'GET', '/' ], - [ 'POST', '/test' ], - [ 'PUT', '/test/put' ], - [ 'DELETE', '/test/:var/delete' ] - ]) - }) // end it - - it('Base routes', function() { + ["GET", "/v1", ["unnamed"]], + ["POST", "/v1/test", ["unnamed"]], + ["GET", "/", ["unnamed"]], + ["POST", "/test", ["unnamed"]], + ["PUT", "/test/put", ["unnamed"]], + ["DELETE", "/test/:var/delete", ["unnamed"]], + ]); + }); // end it + + it("Base routes", function () { // Create an api instance - let api = require('../index')({ base: 'v2' }) - api.get('/', (req,res) => {}) - api.post('/test', (req,res) => {}) - api.put('/test/put', (req,res) => {}) - api.delete('/test/:var/delete', (req,res) => {}) + let api = require("../index")({ base: "v2" }); + api.get("/", (req, res) => {}); + api.post("/test", (req, res) => {}); + api.put("/test/put", (req, res) => {}); + api.delete("/test/:var/delete", (req, res) => {}); expect(utils.extractRoutes(api._routes)).toEqual([ - [ 'GET', '/v2' ], - [ 'POST', '/v2/test' ], - [ 'PUT', '/v2/test/put' ], - [ 'DELETE', '/v2/test/:var/delete' ] - ]) - }) // end it - - }) // end extractRoutes - - - describe('generateEtag:', function() { - - it('Sample text', function() { - expect(utils.generateEtag('this is a test string')).toBe('f6774519d1c7a3389ef327e9c04766b9') - }) // end it - - it('Sample object', function() { - expect(utils.generateEtag({ test: true, foo: 'bar' })).toBe('def7648849c1e7f30c9a9c0ac79e4e52') - }) // end it - - it('Sample JSON string object', function() { - expect(utils.generateEtag(JSON.stringify({ test: true, foo: 'bar' }))).toBe('def7648849c1e7f30c9a9c0ac79e4e52') - }) // end it - - it('Sample buffer', function() { - expect(utils.generateEtag(Buffer.from('this is a test string as a buffer'))).toBe('6a2f7473a72cfebc96ae8cf93d643b70') - }) // end it - - it('Long string', function() { - let longString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' - expect(utils.generateEtag(longString)).toBe('2d8c2f6d978ca21712b5f6de36c9d31f') - }) // end it - - it('Long string (minor variant)', function() { - let longString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est Laborum.' - expect(utils.generateEtag(longString)).toBe('bc82a4065a8ab48ade900c6466b19ccd') - }) // end it - - }) // end generateEtag tests - - - describe('isS3:', function() { - it('Empty path', function() { - expect(utils.isS3('')).toBe(false) - }) - - it('Valid S3 path', function() { - expect(utils.isS3('s3://test-bucket/key')).toBe(true) - }) - - it('Valid S3 path (uppercase)', function() { - expect(utils.isS3('S3://test-bucket/key')).toBe(true) - }) - - it('Invalid S3 path', function() { - expect(utils.isS3('s3://test-bucket')).toBe(false) - }) - - it('Empty S3 path', function() { - expect(utils.isS3('s3:///')).toBe(false) - }) - - it('URL', function() { - expect(utils.isS3('https://somedomain.com/test')).toBe(false) - }) - - it('Relative path', function() { - expect(utils.isS3('../test/file.txt')).toBe(false) - }) - }) // end isS3 tests - - - describe('parseS3:', function() { - it('Valid S3 path', function() { - expect(utils.parseS3('s3://test-bucket/key')).toEqual({ Bucket: 'test-bucket', Key: 'key' }) - }) - - it('Valid S3 path (nested key)', function() { - expect(utils.parseS3('s3://test-bucket/key/path/file.txt')).toEqual({ Bucket: 'test-bucket', Key: 'key/path/file.txt' }) - }) - - it('Invalid S3 path (no key)', function() { - let func = () => utils.parseS3('s3://test-bucket') - expect(func).toThrow('Invalid S3 path') - }) - - it('Invalid S3 path (no bucket or key)', function() { - let func = () => utils.parseS3('s3://') - expect(func).toThrow('Invalid S3 path') - }) - - it('Invalid S3 path (empty)', function() { - let func = () => utils.parseS3('') - expect(func).toThrow('Invalid S3 path') - }) - - }) // end parseS3 tests - - describe('mergeObjects:', function() { - it('Duplicate Items', function() { - let obj1 = { 1: ['test'] } - let obj2 = { 1: ['test'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test'] }) - }) - - it('Single Items', function() { - let obj1 = { 1: ['test'] } - let obj2 = { 1: ['test2'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test','test2'] }) - }) - - it('Multiple Items', function() { - let obj1 = { 1: ['test'], 2: ['testA'] } - let obj2 = { 1: ['test2'], 2: ['testB'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test','test2'], 2: ['testA','testB'] }) - }) - - it('Missing Items (obj1)', function() { - let obj1 = { 1: ['test'] } - let obj2 = { 1: ['test2'], 2: ['testB'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test','test2'], 2: ['testB'] }) - }) - - it('Missing Items (obj2)', function() { - let obj1 = { 1: ['test'], 2: ['testA'] } - let obj2 = { 1: ['test2'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test','test2'], 2: ['testA'] }) - }) - - it('No similarities', function() { - let obj1 = { 1: ['test'] } - let obj2 = { 2: ['testA'] } - expect(utils.mergeObjects(obj1,obj2)).toEqual({ 1: ['test'], 2: ['testA'] }) - }) - }) // end parseS3 tests - -}) // end UTILITY tests + ["GET", "/v2", ["unnamed"]], + ["POST", "/v2/test", ["unnamed"]], + ["PUT", "/v2/test/put", ["unnamed"]], + ["DELETE", "/v2/test/:var/delete", ["unnamed"]], + ]); + }); // end it + }); // end extractRoutes + + describe("generateEtag:", function () { + it("Sample text", function () { + expect(utils.generateEtag("this is a test string")).toBe( + "f6774519d1c7a3389ef327e9c04766b9" + ); + }); // end it + + it("Sample object", function () { + expect(utils.generateEtag({ test: true, foo: "bar" })).toBe( + "def7648849c1e7f30c9a9c0ac79e4e52" + ); + }); // end it + + it("Sample JSON string object", function () { + expect( + utils.generateEtag(JSON.stringify({ test: true, foo: "bar" })) + ).toBe("def7648849c1e7f30c9a9c0ac79e4e52"); + }); // end it + + it("Sample buffer", function () { + expect( + utils.generateEtag(Buffer.from("this is a test string as a buffer")) + ).toBe("6a2f7473a72cfebc96ae8cf93d643b70"); + }); // end it + + it("Long string", function () { + let longString = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + expect(utils.generateEtag(longString)).toBe( + "2d8c2f6d978ca21712b5f6de36c9d31f" + ); + }); // end it + + it("Long string (minor variant)", function () { + let longString = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est Laborum."; + expect(utils.generateEtag(longString)).toBe( + "bc82a4065a8ab48ade900c6466b19ccd" + ); + }); // end it + }); // end generateEtag tests + + describe("isS3:", function () { + it("Empty path", function () { + expect(utils.isS3("")).toBe(false); + }); + + it("Valid S3 path", function () { + expect(utils.isS3("s3://test-bucket/key")).toBe(true); + }); + + it("Valid S3 path (uppercase)", function () { + expect(utils.isS3("S3://test-bucket/key")).toBe(true); + }); + + it("Invalid S3 path", function () { + expect(utils.isS3("s3://test-bucket")).toBe(false); + }); + + it("Empty S3 path", function () { + expect(utils.isS3("s3:///")).toBe(false); + }); + + it("URL", function () { + expect(utils.isS3("https://somedomain.com/test")).toBe(false); + }); + + it("Relative path", function () { + expect(utils.isS3("../test/file.txt")).toBe(false); + }); + }); // end isS3 tests + + describe("parseS3:", function () { + it("Valid S3 path", function () { + expect(utils.parseS3("s3://test-bucket/key")).toEqual({ + Bucket: "test-bucket", + Key: "key", + }); + }); + + it("Valid S3 path (nested key)", function () { + expect(utils.parseS3("s3://test-bucket/key/path/file.txt")).toEqual({ + Bucket: "test-bucket", + Key: "key/path/file.txt", + }); + }); + + it("Invalid S3 path (no key)", function () { + let func = () => utils.parseS3("s3://test-bucket"); + expect(func).toThrow("Invalid S3 path"); + }); + + it("Invalid S3 path (no bucket or key)", function () { + let func = () => utils.parseS3("s3://"); + expect(func).toThrow("Invalid S3 path"); + }); + + it("Invalid S3 path (empty)", function () { + let func = () => utils.parseS3(""); + expect(func).toThrow("Invalid S3 path"); + }); + }); // end parseS3 tests + + describe("mergeObjects:", function () { + it("Duplicate Items", function () { + let obj1 = { 1: ["test"] }; + let obj2 = { 1: ["test"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ 1: ["test"] }); + }); + + it("Single Items", function () { + let obj1 = { 1: ["test"] }; + let obj2 = { 1: ["test2"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ 1: ["test", "test2"] }); + }); + + it("Multiple Items", function () { + let obj1 = { 1: ["test"], 2: ["testA"] }; + let obj2 = { 1: ["test2"], 2: ["testB"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ + 1: ["test", "test2"], + 2: ["testA", "testB"], + }); + }); + + it("Missing Items (obj1)", function () { + let obj1 = { 1: ["test"] }; + let obj2 = { 1: ["test2"], 2: ["testB"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ + 1: ["test", "test2"], + 2: ["testB"], + }); + }); + + it("Missing Items (obj2)", function () { + let obj1 = { 1: ["test"], 2: ["testA"] }; + let obj2 = { 1: ["test2"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ + 1: ["test", "test2"], + 2: ["testA"], + }); + }); + + it("No similarities", function () { + let obj1 = { 1: ["test"] }; + let obj2 = { 2: ["testA"] }; + expect(utils.mergeObjects(obj1, obj2)).toEqual({ + 1: ["test"], + 2: ["testA"], + }); + }); + }); // end parseS3 tests +}); // end UTILITY tests diff --git a/index.d.ts b/index.d.ts index dd869dc..a5cce8b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,7 +1,7 @@ import { APIGatewayEvent, APIGatewayEventRequestContext, - Context + Context, } from 'aws-lambda'; export declare interface CookieOptions { @@ -42,28 +42,42 @@ export declare interface App { [namespace: string]: Package; } -export declare type Middleware = (req: Request, res: Response, next: () => void) => void; -export declare type ErrorHandlingMiddleware = (error: Error, req: Request, res: Response, next: () => void) => void; +export declare type Middleware = ( + req: Request, + res: Response, + next: () => void +) => void; +export declare type ErrorHandlingMiddleware = ( + error: Error, + req: Request, + res: Response, + next: () => void +) => void; export declare type ErrorCallback = (error?: Error) => void; -export declare type HandlerFunction = (req: Request, res: Response, next?: NextFunction) => void | any | Promise; +export declare type HandlerFunction = ( + req: Request, + res: Response, + next?: NextFunction +) => void | any | Promise; export declare type LoggerFunction = (message: string) => void; export declare type NextFunction = () => void; export declare type TimestampFunction = () => string; export declare type SerializerFunction = (body: object) => string; export declare type FinallyFunction = (req: Request, res: Response) => void; -export declare type METHODS = 'GET' - | 'POST' - | 'PUT' - | 'PATCH' - | 'DELETE' - | 'OPTIONS' - | 'HEAD' - | 'ANY'; +export declare type METHODS = + | 'GET' + | 'POST' + | 'PUT' + | 'PATCH' + | 'DELETE' + | 'OPTIONS' + | 'HEAD' + | 'ANY'; export declare interface SamplingOptions { route?: string; target?: number; - rate?: number + rate?: number; period?: number; method?: string | string[]; } @@ -120,7 +134,7 @@ export declare class Request { [key: string]: string | undefined; }; multiValueQuery: { - [key: string]: string[] | undefined + [key: string]: string[] | undefined; }; headers: { [key: string]: string | undefined; @@ -171,7 +185,11 @@ export declare class Response { getHeader(key: string): string; hasHeader(key: string): boolean; removeHeader(key: string): this; - getLink(s3Path: string, expires?: number, callback?: ErrorCallback): Promise; + getLink( + s3Path: string, + expires?: number, + callback?: ErrorCallback + ): Promise; send(body: any): void; json(body: any): void; jsonp(body: any): void; @@ -189,8 +207,17 @@ export declare class Response { cache(age?: boolean | number | string, private?: boolean): this; modified(date: boolean | string | Date): this; attachment(fileName?: string): this; - download(file: string | Buffer, fileName?: string, options?: FileOptions, callback?: ErrorCallback): void; - sendFile(file: string | Buffer, options?: FileOptions, callback?: ErrorCallback): Promise; + download( + file: string | Buffer, + fileName?: string, + options?: FileOptions, + callback?: ErrorCallback + ): void; + sendFile( + file: string | Buffer, + options?: FileOptions, + callback?: ErrorCallback + ): Promise; } export declare class API { @@ -215,13 +242,14 @@ export declare class API { any(...handler: HandlerFunction[]): void; METHOD(method: METHODS, path: string, ...handler: HandlerFunction[]): void; METHOD(method: METHODS, ...handler: HandlerFunction[]): void; - register(routes: (api: API, options?: RegisterOptions) => void, options?: RegisterOptions): void; + register( + routes: (api: API, options?: RegisterOptions) => void, + options?: RegisterOptions + ): void; routes(format: true): void; routes(format: false): string[][]; routes(): string[][]; - - use(path: string, ...middleware: Middleware[]): void; use(paths: string[], ...middleware: Middleware[]): void; use(...middleware: Middleware[]): void; @@ -229,7 +257,11 @@ export declare class API { finally(callback: FinallyFunction): void; - run(event: APIGatewayEvent, context: Context, cb: (err: Error, result: any) => void): void; + run( + event: APIGatewayEvent, + context: Context, + cb: (err: Error, result: any) => void + ): void; run(event: APIGatewayEvent, context: Context): Promise; } diff --git a/index.js b/index.js index c301373..784f1bc 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -7,38 +7,56 @@ * @license MIT */ -const REQUEST = require('./lib/request') // Resquest object -const RESPONSE = require('./lib/response') // Response object -const UTILS = require('./lib/utils') // Require utils library -const LOGGER = require('./lib/logger') // Require logger library -const prettyPrint = require('./lib/prettyPrint') // Pretty print for debugging -const { ConfigurationError } = require('./lib/errors') // Require custom errors +const REQUEST = require('./lib/request'); // Resquest object +const RESPONSE = require('./lib/response'); // Response object +const UTILS = require('./lib/utils'); // Require utils library +const LOGGER = require('./lib/logger'); // Require logger library +const prettyPrint = require('./lib/prettyPrint'); // Pretty print for debugging +const { ConfigurationError } = require('./lib/errors'); // Require custom errors // Create the API class class API { - // Create the constructor function. constructor(props) { - // Set the version and base paths - this._version = props && props.version ? props.version : 'v1' - this._base = props && props.base && typeof props.base === 'string' ? props.base.trim() : '' - this._callbackName = props && props.callback ? props.callback.trim() : 'callback' - this._mimeTypes = props && props.mimeTypes && typeof props.mimeTypes === 'object' ? props.mimeTypes : {} - this._serializer = props && props.serializer && typeof props.serializer === 'function' ? props.serializer : JSON.stringify - this._errorHeaderWhitelist = props && Array.isArray(props.errorHeaderWhitelist) ? props.errorHeaderWhitelist.map(header => header.toLowerCase()) : [] - this._isBase64 = props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false - this._headers = props && props.headers && typeof props.headers === 'object' ? props.headers : {} - this._compression = props && typeof props.compression === 'boolean' ? props.compression : false + this._version = props && props.version ? props.version : 'v1'; + this._base = + props && props.base && typeof props.base === 'string' + ? props.base.trim() + : ''; + this._callbackName = + props && props.callback ? props.callback.trim() : 'callback'; + this._mimeTypes = + props && props.mimeTypes && typeof props.mimeTypes === 'object' + ? props.mimeTypes + : {}; + this._serializer = + props && props.serializer && typeof props.serializer === 'function' + ? props.serializer + : JSON.stringify; + this._errorHeaderWhitelist = + props && Array.isArray(props.errorHeaderWhitelist) + ? props.errorHeaderWhitelist.map((header) => header.toLowerCase()) + : []; + this._isBase64 = + props && typeof props.isBase64 === 'boolean' ? props.isBase64 : false; + this._headers = + props && props.headers && typeof props.headers === 'object' + ? props.headers + : {}; + this._compression = + props && typeof props.compression === 'boolean' + ? props.compression + : false; // Set sampling info - this._sampleCounts = {} + this._sampleCounts = {}; // Init request counter - this._requestCount = 0 + this._requestCount = 0; // Track init date/time - this._initTime = Date.now() + this._initTime = Date.now(); // Logging levels this._logLevels = { @@ -47,430 +65,494 @@ class API { info: 30, warn: 40, error: 50, - fatal: 60 - } + fatal: 60, + }; // Configure logger - this._logger = LOGGER.config(props && props.logger,this._logLevels) + this._logger = LOGGER.config(props && props.logger, this._logLevels); // Prefix stack w/ base - this._prefix = this.parseRoute(this._base) + this._prefix = this.parseRoute(this._base); // Stores route mappings - this._routes = {} + this._routes = {}; // Init callback - this._cb + this._cb; // Error middleware stack - this._errors = [] + this._errors = []; // Store app packages and namespaces - this._app = {} + this._app = {}; // Executed after the callback - this._finally = () => {} + this._finally = () => {}; // Global error status (used for response parsing errors) - this._errorStatus = 500 + this._errorStatus = 500; // Methods - this._methods = ['get','post','put','patch','delete','options','head','any'] + this._methods = [ + 'get', + 'post', + 'put', + 'patch', + 'delete', + 'options', + 'head', + 'any', + ]; // Convenience methods for METHOD - this._methods.forEach(m => { - this[m] = (...a) => this.METHOD(m.toUpperCase(),...a) - }) - + this._methods.forEach((m) => { + this[m] = (...a) => this.METHOD(m.toUpperCase(), ...a); + }); } // end constructor // METHOD: Adds method, middleware, and handlers to routes - METHOD(method,...args) { - + METHOD(method, ...args) { // Extract path if provided, otherwise default to global wildcard - let path = typeof args[0] === 'string' ? args.shift() : '/*' + let path = typeof args[0] === 'string' ? args.shift() : '/*'; // Extract the execution stack - let stack = args.map((fn,i) => { - if (typeof fn === 'function' && (fn.length === 3 || (i === args.length-1))) - return fn - throw new ConfigurationError('Route-based middleware must have 3 parameters') - }) + let stack = args.map((fn, i) => { + if ( + typeof fn === 'function' && + (fn.length === 3 || i === args.length - 1) + ) + return fn; + throw new ConfigurationError( + 'Route-based middleware must have 3 parameters' + ); + }); if (stack.length === 0) - throw new ConfigurationError(`No handler or middleware specified for ${method} method on ${path} route.`) + throw new ConfigurationError( + `No handler or middleware specified for ${method} method on ${path} route.` + ); // Ensure methods is an array and upper case - let methods = (Array.isArray(method) ? method : method.split(',')) - .map(x => typeof x === 'string' ? x.trim().toUpperCase() : null) + let methods = (Array.isArray(method) ? method : method.split(',')).map( + (x) => (typeof x === 'string' ? x.trim().toUpperCase() : null) + ); // Parse the path - let parsedPath = this.parseRoute(path) + let parsedPath = this.parseRoute(path); // Split the route and clean it up - let route = this._prefix.concat(parsedPath) + let route = this._prefix.concat(parsedPath); // For root path support - if (route.length === 0) { route.push('') } + if (route.length === 0) { + route.push(''); + } // Keep track of path variables - let pathVars = {} + let pathVars = {}; // Make a local copy of routes - let routes = this._routes + let routes = this._routes; // Create a local stack for inheritance - let _stack = {} + let _stack = { '*': [], m: [] }; // Loop through the path levels - for (let i=0; i { - + methods.forEach((_method) => { // Method must be a string if (typeof _method === 'string') { - // Check for wild card at this level if (routes['ROUTES']['*']) { - if (routes['ROUTES']['*']['MIDDLEWARE'] && (route[i] !== '*' || _method !== '__MW__')) { - _stack[method] = (_stack[method] || []).concat(routes['ROUTES']['*']['MIDDLEWARE'].stack) + if ( + routes['ROUTES']['*']['MIDDLEWARE'] && + (route[i] !== '*' || _method !== '__MW__') + ) { + _stack['*'][method] = routes['ROUTES']['*']['MIDDLEWARE'].stack; } - if (routes['ROUTES']['*']['METHODS'] && routes['ROUTES']['*']['METHODS'][method]) { - _stack[method] = (_stack[method] || []).concat(routes['ROUTES']['*']['METHODS'][method].stack) + if ( + routes['ROUTES']['*']['METHODS'] && + routes['ROUTES']['*']['METHODS'][method] + ) { + _stack['m'][method] = + routes['ROUTES']['*']['METHODS'][method].stack; } } // end if wild card // If this is the end of the path if (end) { - // Check for matching middleware - if (route[i] !== '*' && routes['ROUTES'][route[i]] && routes['ROUTES'][route[i]]['MIDDLEWARE']) { - _stack[method] = (_stack[method] || []).concat(routes['ROUTES'][route[i]]['MIDDLEWARE'].stack) + if ( + route[i] !== '*' && + routes['ROUTES'][route[i]] && + routes['ROUTES'][route[i]]['MIDDLEWARE'] + ) { + _stack['m'][method] = + routes['ROUTES'][route[i]]['MIDDLEWARE'].stack; } // end if - // Generate the route/method meta data let meta = { vars: pathVars, - stack, - inherited: _stack[method] ? _stack[method] : [], - route: '/'+parsedPath.join('/'), - path: '/'+this._prefix.concat(parsedPath).join('/') - } + stack: _stack['m'][method] + ? _stack['m'][method].concat(stack) + : _stack['*'][method] + ? _stack['*'][method].concat(stack) + : stack, + // inherited: _stack[method] ? _stack[method] : [], + route: '/' + parsedPath.join('/'), + path: '/' + this._prefix.concat(parsedPath).join('/'), + }; // If mounting middleware if (method === '__MW__') { // Merge stacks if middleware exists if (routes['ROUTES'][route[i]]['MIDDLEWARE']) { - meta.stack = routes['ROUTES'][route[i]]['MIDDLEWARE'].stack.concat(stack) - meta.vars = UTILS.mergeObjects(routes['ROUTES'][route[i]]['MIDDLEWARE'].vars,pathVars) + meta.stack = + routes['ROUTES'][route[i]]['MIDDLEWARE'].stack.concat(stack); + meta.vars = UTILS.mergeObjects( + routes['ROUTES'][route[i]]['MIDDLEWARE'].vars, + pathVars + ); } // Add/update middleware - routes['ROUTES'][route[i]]['MIDDLEWARE'] = meta + routes['ROUTES'][route[i]]['MIDDLEWARE'] = meta; + + // Apply middleware to all child middlware routes + // if (route[i] === "*") { + // // console.log("APPLY NESTED MIDDLEWARE"); + // // console.log(JSON.stringify(routes["ROUTES"], null, 2)); + // Object.keys(routes["ROUTES"]).forEach((nestedRoute) => { + // if (nestedRoute != "*") { + // console.log(nestedRoute); + // } + // }); + // } } else { - // Create the methods section if it doesn't exist - if (!routes['ROUTES'][route[i]]['METHODS']) routes['ROUTES'][route[i]]['METHODS'] = {} + if (!routes['ROUTES'][route[i]]['METHODS']) + routes['ROUTES'][route[i]]['METHODS'] = {}; // Merge stacks if method already exists for this route if (routes['ROUTES'][route[i]]['METHODS'][_method]) { - meta.stack = routes['ROUTES'][route[i]]['METHODS'][_method].stack.concat(stack) - meta.vars = UTILS.mergeObjects(routes['ROUTES'][route[i]]['METHODS'][_method].vars,pathVars) + meta.stack = + routes['ROUTES'][route[i]]['METHODS'][_method].stack.concat( + stack + ); + meta.vars = UTILS.mergeObjects( + routes['ROUTES'][route[i]]['METHODS'][_method].vars, + pathVars + ); } // Add method and meta data - routes['ROUTES'][route[i]]['METHODS'][_method] = meta - + routes['ROUTES'][route[i]]['METHODS'][_method] = meta; } // end else // console.log('STACK:',meta); - // If there's a wild card that's not at the end - } else if (route[i] === '*') { - throw new ConfigurationError('Wildcards can only be at the end of a route definition') + // If there's a wild card that's not at the end + } else if (route[i] === '*') { + throw new ConfigurationError( + 'Wildcards can only be at the end of a route definition' + ); } // end if end of path } // end if method is string - - }) // end methods loop + }); // end methods loop // Update the current routes pointer - routes = routes['ROUTES'][route[i]] - + routes = routes['ROUTES'][route[i]]; } // end path traversal loop // console.log(JSON.stringify(this._routes,null,2)); - } // end main METHOD function // RUN: This runs the routes - async run(event,context,cb) { - + async run(event, context, cb) { // Set the event, context and callback - this._event = event || {} - this._context = this.context = typeof context === 'object' ? context : {} - this._cb = cb ? cb : undefined + this._event = event || {}; + this._context = this.context = typeof context === 'object' ? context : {}; + this._cb = cb ? cb : undefined; // Initalize request and response objects - let request = new REQUEST(this) - let response = new RESPONSE(this,request) + let request = new REQUEST(this); + let response = new RESPONSE(this, request); try { - // Parse the request - await request.parseRequest() + await request.parseRequest(); // Loop through the execution stack for (const fn of request._stack) { // Only run if in processing state - if (response._state !== 'processing') break + if (response._state !== 'processing') break; - await new Promise(async r => { // eslint-disable-line + // eslint-disable-next-line + await new Promise(async (r) => { try { - let rtn = await fn(request,response,() => { r() }) - if (rtn) response.send(rtn) - if (response._state === 'done') r() // if state is done, resolve promise - } catch(e) { - await this.catchErrors(e,response) - r() // resolve the promise + let rtn = await fn(request, response, () => { + r(); + }); + if (rtn) response.send(rtn); + if (response._state === 'done') r(); // if state is done, resolve promise + } catch (e) { + await this.catchErrors(e, response); + r(); // resolve the promise } - }) - + }); } // end for - - } catch(e) { - await this.catchErrors(e,response) + } catch (e) { + // console.log(e); + await this.catchErrors(e, response); } // Return the final response - return response._response - + return response._response; } // end run function - - // Catch all async/sync errors - async catchErrors(e,response,code,detail) { - + async catchErrors(e, response, code, detail) { // Error messages should respect the app's base64 configuration - response._isBase64 = this._isBase64 + response._isBase64 = this._isBase64; // Strip the headers, keep whitelist - const strippedHeaders = Object.entries(response._headers).reduce((acc, [headerName, value]) => { - if (!this._errorHeaderWhitelist.includes(headerName.toLowerCase())) { return acc } + const strippedHeaders = Object.entries(response._headers).reduce( + (acc, [headerName, value]) => { + if (!this._errorHeaderWhitelist.includes(headerName.toLowerCase())) { + return acc; + } - return Object.assign( - acc, - { [headerName]: value } - ) - }, {}) + return Object.assign(acc, { [headerName]: value }); + }, + {} + ); - response._headers = Object.assign(strippedHeaders, this._headers) + response._headers = Object.assign(strippedHeaders, this._headers); - let message + let message; // Set the status code - response.status(code ? code : this._errorStatus) + response.status(code ? code : this._errorStatus); let info = { detail, statusCode: response._statusCode, coldStart: response._request.coldStart, - stack: this._logger.stack && e.stack || undefined - } + stack: (this._logger.stack && e.stack) || undefined, + }; if (e instanceof Error) { - message = e.message + message = e.message; if (this._logger.errorLogging) { - this.log.fatal(message, info) + this.log.fatal(message, info); } } else { - message = e + message = e; if (this._logger.errorLogging) { - this.log.error(message, info) + this.log.error(message, info); } } // If first time through, process error middleware if (response._state === 'processing') { - // Flag error state (this will avoid infinite error loops) - response._state = 'error' + response._state = 'error'; // Execute error middleware for (const err of this._errors) { - if (response._state === 'done') break + if (response._state === 'done') break; // Promisify error middleware - await new Promise(r => { - let rtn = err(e,response._request,response,() => { r() }) - if (rtn) response.send(rtn) - }) + await new Promise((r) => { + let rtn = err(e, response._request, response, () => { + r(); + }); + if (rtn) response.send(rtn); + }); } // end for } // Throw standard error unless callback has already been executed - if (response._state !== 'done') response.json({'error':message}) - + if (response._state !== 'done') response.json({ error: message }); } // end catch - - // Custom callback - async _callback(err,res,response) { - + async _callback(err, res, response) { // Set done status - response._state = 'done' + response._state = 'done'; // Execute finally - await this._finally(response._request,response) + await this._finally(response._request, response); // Output logs - response._request._logs.forEach(log => { - this._logger.logger(JSON.stringify(this._logger.detail ? - this._logger.format(log,response._request,response) : log)) - }) + response._request._logs.forEach((log) => { + this._logger.logger( + JSON.stringify( + this._logger.detail + ? this._logger.format(log, response._request, response) + : log + ) + ); + }); // Generate access log - if ((this._logger.access || response._request._logs.length > 0) && this._logger.access !== 'never') { + if ( + (this._logger.access || response._request._logs.length > 0) && + this._logger.access !== 'never' + ) { let access = Object.assign( - this._logger.log('access',undefined,response._request,response._request.context), - { statusCode: res.statusCode, coldStart: response._request.coldStart, count: response._request.requestCount } - ) - this._logger.logger(JSON.stringify(this._logger.format(access,response._request,response))) + this._logger.log( + 'access', + undefined, + response._request, + response._request.context + ), + { + statusCode: res.statusCode, + coldStart: response._request.coldStart, + count: response._request.requestCount, + } + ); + this._logger.logger( + JSON.stringify(this._logger.format(access, response._request, response)) + ); } // Reset global error code - this._errorStatus = 500 + this._errorStatus = 500; // Execute the primary callback - typeof this._cb === 'function' && this._cb(err,res) - + typeof this._cb === 'function' && this._cb(err, res); } // end _callback - - // Middleware handler use(...args) { - // Extract routes - let routes = typeof args[0] === 'string' ? Array.of(args.shift()) : (Array.isArray(args[0]) ? args.shift() : ['/*']) + let routes = + typeof args[0] === 'string' + ? Array.of(args.shift()) + : Array.isArray(args[0]) + ? args.shift() + : ['/*']; // Init middleware stack - let middleware = [] + let middleware = []; // Add func args as middleware for (let arg in args) { if (typeof args[arg] === 'function') { if (args[arg].length === 3) { - middleware.push(args[arg]) + middleware.push(args[arg]); } else if (args[arg].length === 4) { - this._errors.push(args[arg]) + this._errors.push(args[arg]); } else { - throw new ConfigurationError('Middleware must have 3 or 4 parameters') + throw new ConfigurationError( + 'Middleware must have 3 or 4 parameters' + ); } } } // Add middleware for all methods if (middleware.length > 0) { - routes.forEach(route => { - this.METHOD('__MW__',route,...middleware) - }) + routes.forEach((route) => { + this.METHOD('__MW__', route, ...middleware); + }); } - } // end use - // Finally handler finally(fn) { - this._finally = fn + this._finally = fn; } - - //-------------------------------------------------------------------------// // UTILITY FUNCTIONS //-------------------------------------------------------------------------// parseRoute(path) { - return path.trim().replace(/^\/(.*?)(\/)*$/,'$1').split('/').filter(x => x.trim() !== '') + return path + .trim() + .replace(/^\/(.*?)(\/)*$/, '$1') + .split('/') + .filter((x) => x.trim() !== ''); } // Load app packages app(packages) { - // Check for supplied packages if (typeof packages === 'object') { // Loop through and set package namespaces for (let namespace in packages) { try { - this._app[namespace] = packages[namespace] - } catch(e) { - console.error(e.message) // eslint-disable-line no-console + this._app[namespace] = packages[namespace]; + } catch (e) { + console.error(e.message); // eslint-disable-line no-console } } } else if (arguments.length === 2 && typeof packages === 'string') { - this._app[packages] = arguments[1] - }// end if + this._app[packages] = arguments[1]; + } // end if // Return a reference - return this._app + return this._app; } - // Register routes with options - register(fn,opts) { - - let options = typeof opts === 'object' ? opts : {} + register(fn, opts) { + let options = typeof opts === 'object' ? opts : {}; // Extract Prefix - let prefix = options.prefix && options.prefix.toString().trim() !== '' ? - this.parseRoute(options.prefix) : [] + let prefix = + options.prefix && options.prefix.toString().trim() !== '' + ? this.parseRoute(options.prefix) + : []; // Concat to existing prefix - this._prefix = this._prefix.concat(prefix) + this._prefix = this._prefix.concat(prefix); // Execute the routing function - fn(this,options) + fn(this, options); // Remove the last prefix (if a prefix exists) if (prefix.length > 0) { - this._prefix = this._prefix.slice(0,-(prefix.length)) + this._prefix = this._prefix.slice(0, -prefix.length); } - } // end register - // prettyPrint debugger routes(format) { // Parse the routes - let routes = UTILS.extractRoutes(this._routes) + let routes = UTILS.extractRoutes(this._routes); if (format) { - console.log(prettyPrint(routes)) // eslint-disable-line no-console + console.log(prettyPrint(routes)); // eslint-disable-line no-console } else { - return routes + return routes; } } - } // end API class // Export the API class as a new instance -module.exports = opts => new API(opts) +module.exports = (opts) => new API(opts); // Add createAPI as default export (to match index.d.ts) -module.exports.default = module.exports +module.exports.default = module.exports; diff --git a/lib/compression.js b/lib/compression.js index d5508f2..2601466 100644 --- a/lib/compression.js +++ b/lib/compression.js @@ -1,43 +1,51 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications * @author Jeremy Daly * @license MIT -*/ + */ -const zlib = require('zlib') +const zlib = require('zlib'); + +exports.compress = (input, headers) => { + const acceptEncodingHeader = headers['accept-encoding'] || ''; + const acceptableEncodings = new Set( + acceptEncodingHeader + .toLowerCase() + .split(',') + .map((str) => str.trim()) + ); -exports.compress = (input,headers) => { - const acceptEncodingHeader = headers['accept-encoding'] || '' - const acceptableEncodings = new Set(acceptEncodingHeader.toLowerCase().split(',').map(str => str.trim())) - // Handle Brotli compression (Only supported in Node v10 and later) - if (acceptableEncodings.has('br') && typeof zlib.brotliCompressSync === 'function') { + if ( + acceptableEncodings.has('br') && + typeof zlib.brotliCompressSync === 'function' + ) { return { data: zlib.brotliCompressSync(input), - contentEncoding: 'br' - } + contentEncoding: 'br', + }; } // Handle Gzip compression if (acceptableEncodings.has('gzip')) { return { data: zlib.gzipSync(input), - contentEncoding: 'gzip' - } + contentEncoding: 'gzip', + }; } // Handle deflate compression if (acceptableEncodings.has('deflate')) { return { data: zlib.deflateSync(input), - contentEncoding: 'deflate' - } + contentEncoding: 'deflate', + }; } return { data: input, - contentEncoding: null - } -} \ No newline at end of file + contentEncoding: null, + }; +}; diff --git a/lib/errors.js b/lib/errors.js index c5404e6..255d0b5 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -9,42 +9,42 @@ // Custom error types class RouteError extends Error { - constructor(message,path) { - super(message) - this.name = this.constructor.name - this.path = path + constructor(message, path) { + super(message); + this.name = this.constructor.name; + this.path = path; } } class MethodError extends Error { - constructor(message,method,path) { - super(message) - this.name = this.constructor.name - this.method = method - this.path = path + constructor(message, method, path) { + super(message); + this.name = this.constructor.name; + this.method = method; + this.path = path; } } class ConfigurationError extends Error { constructor(message) { - super(message) - this.name = this.constructor.name + super(message); + this.name = this.constructor.name; } } class ResponseError extends Error { - constructor(message,code) { - super(message) - this.name = this.constructor.name - this.code = code + constructor(message, code) { + super(message); + this.name = this.constructor.name; + this.code = code; } } class FileError extends Error { - constructor(message,err) { - super(message) - this.name = this.constructor.name - for (let e in err) this[e] = err[e] + constructor(message, err) { + super(message); + this.name = this.constructor.name; + for (let e in err) this[e] = err[e]; } } @@ -54,5 +54,5 @@ module.exports = { MethodError, ConfigurationError, ResponseError, - FileError -} + FileError, +}; diff --git a/lib/logger.js b/lib/logger.js index 54cea09..ea1bf05 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,62 +1,79 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications * @author Jeremy Daly * @license MIT -*/ + */ // IDEA: add unique function identifier // IDEA: response length // https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference -const UTILS = require('./utils') // Require utils library -const { ConfigurationError } = require('./errors') // Require custom errors +const UTILS = require('./utils'); // Require utils library +const { ConfigurationError } = require('./errors'); // Require custom errors // Config logger -exports.config = (config,levels) => { - - let cfg = config ? config : {} +exports.config = (config, levels) => { + let cfg = config ? config : {}; // Add custom logging levels if (cfg.levels && typeof cfg.levels === 'object') { for (let lvl in cfg.levels) { if (!/^[A-Za-z_]\w*$/.test(lvl) || isNaN(cfg.levels[lvl])) { - throw new ConfigurationError('Invalid level configuration') + throw new ConfigurationError('Invalid level configuration'); } } - levels = Object.assign(levels,cfg.levels) + levels = Object.assign(levels, cfg.levels); } // Configure sampling rules - let sampling = cfg.sampling ? parseSamplerConfig(cfg.sampling,levels) : false + let sampling = cfg.sampling + ? parseSamplerConfig(cfg.sampling, levels) + : false; // Parse/default the logging level - let level = cfg === true ? 'info' : - cfg.level && levels[cfg.level.toLowerCase()] ? - cfg.level.toLowerCase() : cfg.level === 'none' ? - 'none' : Object.keys(cfg).length > 0 ? 'info' : 'none' - - let messageKey = cfg.messageKey && typeof cfg.messageKey === 'string' ? - cfg.messageKey.trim() : 'msg' - - let customKey = cfg.customKey && typeof cfg.customKey === 'string' ? - cfg.customKey.trim() : 'custom' - - let timestamp = cfg.timestamp === false ? () => undefined : - typeof cfg.timestamp === 'function' ? cfg.timestamp : () => Date.now() - - let timer = cfg.timer === false ? () => undefined : (start) => (Date.now()-start) - - let nested = cfg.nested === true ? true : false // nest serializers - let stack = cfg.stack === true ? true : false // show stack traces in errors - let access = cfg.access === true ? true : cfg.access === 'never' ? 'never' : false // create access logs - let detail = cfg.detail === true ? true : false // add req/res detail to all logs - - let multiValue = cfg.multiValue === true ? true : false // return qs as multiValue + let level = + cfg === true + ? 'info' + : cfg.level && levels[cfg.level.toLowerCase()] + ? cfg.level.toLowerCase() + : cfg.level === 'none' + ? 'none' + : Object.keys(cfg).length > 0 + ? 'info' + : 'none'; + + let messageKey = + cfg.messageKey && typeof cfg.messageKey === 'string' + ? cfg.messageKey.trim() + : 'msg'; + + let customKey = + cfg.customKey && typeof cfg.customKey === 'string' + ? cfg.customKey.trim() + : 'custom'; + + let timestamp = + cfg.timestamp === false + ? () => undefined + : typeof cfg.timestamp === 'function' + ? cfg.timestamp + : () => Date.now(); + + let timer = + cfg.timer === false ? () => undefined : (start) => Date.now() - start; + + let nested = cfg.nested === true ? true : false; // nest serializers + let stack = cfg.stack === true ? true : false; // show stack traces in errors + let access = + cfg.access === true ? true : cfg.access === 'never' ? 'never' : false; // create access logs + let detail = cfg.detail === true ? true : false; // add req/res detail to all logs + + let multiValue = cfg.multiValue === true ? true : false; // return qs as multiValue let defaults = { - req: req => { + req: (req) => { return { path: req.path, ip: req.ip, @@ -64,46 +81,76 @@ exports.config = (config,levels) => { device: req.clientType, country: req.clientCountry, version: req.version, - qs: multiValue ? (Object.keys(req.multiValueQuery).length > 0 ? req.multiValueQuery : undefined) - : (Object.keys(req.query).length > 0 ? req.query : undefined) - } + qs: multiValue + ? Object.keys(req.multiValueQuery).length > 0 + ? req.multiValueQuery + : undefined + : Object.keys(req.query).length > 0 + ? req.query + : undefined, + }; }, res: () => { - return { } + return {}; }, - context: context => { + context: (context) => { return { - remaining: context.getRemainingTimeInMillis && context.getRemainingTimeInMillis(), + remaining: + context.getRemainingTimeInMillis && + context.getRemainingTimeInMillis(), function: context.functionName && context.functionName, - memory: context.memoryLimitInMB && context.memoryLimitInMB - } + memory: context.memoryLimitInMB && context.memoryLimitInMB, + }; }, - custom: custom => typeof custom === 'object' && !Array.isArray(custom) - || nested ? custom : { [customKey]: custom } - } + custom: (custom) => + (typeof custom === 'object' && !Array.isArray(custom)) || nested + ? custom + : { [customKey]: custom }, + }; let serializers = { - main: cfg.serializers && typeof cfg.serializers.main === 'function' ? cfg.serializers.main : () => {}, - req: cfg.serializers && typeof cfg.serializers.req === 'function' ? cfg.serializers.req : () => {}, - res: cfg.serializers && typeof cfg.serializers.res === 'function' ? cfg.serializers.res : () => {}, - context: cfg.serializers && typeof cfg.serializers.context === 'function' ? cfg.serializers.context : () => {}, - custom: cfg.serializers && typeof cfg.serializers.custom === 'function' ? cfg.serializers.custom : () => {} - } + main: + cfg.serializers && typeof cfg.serializers.main === 'function' + ? cfg.serializers.main + : () => {}, + req: + cfg.serializers && typeof cfg.serializers.req === 'function' + ? cfg.serializers.req + : () => {}, + res: + cfg.serializers && typeof cfg.serializers.res === 'function' + ? cfg.serializers.res + : () => {}, + context: + cfg.serializers && typeof cfg.serializers.context === 'function' + ? cfg.serializers.context + : () => {}, + custom: + cfg.serializers && typeof cfg.serializers.custom === 'function' + ? cfg.serializers.custom + : () => {}, + }; // Overridable logging function - let logger = cfg.log && typeof cfg.log === 'function' ? - cfg.log : - (...a) => console.log(...a) // eslint-disable-line no-console + let logger = + cfg.log && typeof cfg.log === 'function' + ? cfg.log + : (...a) => console.log(...a); // eslint-disable-line no-console // Main logging function - let log = (level,msg,req,context,custom) => { - - let _context = Object.assign({},defaults.context(context),serializers.context(context)) - let _custom = typeof custom === 'object' && !Array.isArray(custom) ? - Object.assign({},defaults.custom(custom),serializers.custom(custom)) : - defaults.custom(custom) - - return Object.assign({}, + let log = (level, msg, req, context, custom) => { + let _context = Object.assign( + {}, + defaults.context(context), + serializers.context(context) + ); + let _custom = + typeof custom === 'object' && !Array.isArray(custom) + ? Object.assign({}, defaults.custom(custom), serializers.custom(custom)) + : defaults.custom(custom); + + return Object.assign( + {}, { level, time: timestamp(), @@ -113,27 +160,26 @@ exports.config = (config,levels) => { [messageKey]: msg, timer: timer(req._start), int: req.interface, - sample: req._sample ? true : undefined + sample: req._sample ? true : undefined, }, serializers.main(req), nested ? { [customKey]: _custom } : _custom, nested ? { context: _context } : _context - ) - - } // end log + ); + }; // end log // Formatting function for additional log data enrichment - let format = function(info,req,res) { + let format = function (info, req, res) { + let _req = Object.assign({}, defaults.req(req), serializers.req(req)); + let _res = Object.assign({}, defaults.res(res), serializers.res(res)); - let _req = Object.assign({},defaults.req(req),serializers.req(req)) - let _res = Object.assign({},defaults.res(res),serializers.res(res)) - - return Object.assign({}, + return Object.assign( + {}, info, nested ? { req: _req } : _req, nested ? { res: _res } : _res - ) - } // end format + ); + }; // end format // Return logger object return { @@ -145,146 +191,181 @@ exports.config = (config,levels) => { access, detail, sampling, - errorLogging: cfg.errorLogging !== false - } -} + errorLogging: cfg.errorLogging !== false, + }; +}; // Determine if we should sample this request -exports.sampler = (app,req) => { - +exports.sampler = (app, req) => { if (app._logger.sampling) { - // Default level to false - let level = false + let level = false; // Create local reference to the rulesMap - let map = app._logger.sampling.rulesMap + let map = app._logger.sampling.rulesMap; // Parse the current route - let route = UTILS.parsePath(req.route) + let route = UTILS.parsePath(req.route); // Default wildcard mapping - let wildcard = {} + let wildcard = {}; // Loop the map and see if this route matches - route.forEach(part => { + route.forEach((part) => { // Capture wildcard mappings - if (map['*']) wildcard = map['*'] + if (map['*']) wildcard = map['*']; // Traverse map - map = map[part] ? map[part] : {} - }) // end for loop + map = map[part] ? map[part] : {}; + }); // end for loop // Set rule reference based on route - let ref = map['__'+req.method] ? map['__'+req.method] : - map['__ANY'] ? map['__ANY'] : wildcard['__'+req.method] ? - wildcard['__'+req.method] : wildcard['__ANY'] ? - wildcard['__ANY'] : -1 - - let rule = ref >= 0 ? app._logger.sampling.rules[ref] : app._logger.sampling.defaults + let ref = map['__' + req.method] + ? map['__' + req.method] + : map['__ANY'] + ? map['__ANY'] + : wildcard['__' + req.method] + ? wildcard['__' + req.method] + : wildcard['__ANY'] + ? wildcard['__ANY'] + : -1; + + let rule = + ref >= 0 + ? app._logger.sampling.rules[ref] + : app._logger.sampling.defaults; // Assign rule reference to the REQUEST - req._sampleRule = rule + req._sampleRule = rule; // Get last sample time (default start, last, fixed count, period count and total count) - let counts = app._sampleCounts[rule.default ? 'default' : req.route] - || Object.assign(app._sampleCounts, { - [rule.default ? 'default' : req.route]: { start: 0, fCount: 0, pCount: 0, tCount: 0 } - })[rule.default ? 'default' : req.route] - - let now = Date.now() + let counts = + app._sampleCounts[rule.default ? 'default' : req.route] || + Object.assign(app._sampleCounts, { + [rule.default ? 'default' : req.route]: { + start: 0, + fCount: 0, + pCount: 0, + tCount: 0, + }, + })[rule.default ? 'default' : req.route]; + + let now = Date.now(); // Calculate the current velocity - let velocity = rule.rate > 0 ? rule.period*1000/(counts.tCount/(now-app._initTime)*rule.period*1000*rule.rate) : 0 + let velocity = + rule.rate > 0 + ? (rule.period * 1000) / + ((counts.tCount / (now - app._initTime)) * + rule.period * + 1000 * + rule.rate) + : 0; // If this is a new period, reset values - if ((now-counts.start) > rule.period*1000) { - counts.start = now - counts.pCount = 0 + if (now - counts.start > rule.period * 1000) { + counts.start = now; + counts.pCount = 0; // If a rule target is set, sample the start if (rule.target > 0) { - counts.fCount = 1 - level = rule.level // set the sample level + counts.fCount = 1; + level = rule.level; // set the sample level // console.log('\n*********** NEW PERIOD ***********'); } - // Enable sampling if last sample is passed target split - } else if (rule.target > 0 && - counts.start+Math.floor(rule.period*1000/rule.target*counts.fCount) < now) { - level = rule.level - counts.fCount++ + // Enable sampling if last sample is passed target split + } else if ( + rule.target > 0 && + counts.start + + Math.floor(((rule.period * 1000) / rule.target) * counts.fCount) < + now + ) { + level = rule.level; + counts.fCount++; // console.log('\n*********** FIXED ***********'); - } else if (rule.rate > 0 && - counts.start+Math.floor(velocity*counts.pCount+velocity/2) < now) { - level = rule.level - counts.pCount++ + } else if ( + rule.rate > 0 && + counts.start + Math.floor(velocity * counts.pCount + velocity / 2) < now + ) { + level = rule.level; + counts.pCount++; // console.log('\n*********** RATE ***********'); } // Increment total count - counts.tCount++ - - return level + counts.tCount++; + return level; } // end if sampling - return false -} + return false; +}; // Parse sampler configuration -const parseSamplerConfig = (config,levels) => { - +const parseSamplerConfig = (config, levels) => { // Default config - let cfg = typeof config === 'object' ? config : config === true ? {} : false + let cfg = typeof config === 'object' ? config : config === true ? {} : false; // Error on invalid config - if (cfg === false) throw new ConfigurationError('Invalid sampler configuration') + if (cfg === false) + throw new ConfigurationError('Invalid sampler configuration'); // Create rule default let defaults = (inputs) => { - return { // target, rate, period, method, level + return { + // target, rate, period, method, level target: Number.isInteger(inputs.target) ? inputs.target : 1, rate: !isNaN(inputs.rate) && inputs.rate <= 1 ? inputs.rate : 0.1, period: Number.isInteger(inputs.period) ? inputs.period : 60, // in seconds - level: Object.keys(levels).includes(inputs.level) ? inputs.level : 'trace' - } - } + level: Object.keys(levels).includes(inputs.level) + ? inputs.level + : 'trace', + }; + }; // Init ruleMap - let rulesMap = {} + let rulesMap = {}; // Parse and default rules - let rules = Array.isArray(cfg.rules) ? cfg.rules.map((rule,i) => { - // Error if missing route or not a string - if (!rule.route || typeof rule.route !== 'string') - throw new ConfigurationError('Invalid route specified in rule') - - // Parse methods into array (if not already) - let methods = (Array.isArray(rule.method) ? rule.method : - typeof rule.method === 'string' ? - rule.method.split(',') : ['ANY']).map(x => x.toString().trim().toUpperCase()) - - let map = {} - let recursive = map // create recursive reference - - UTILS.parsePath(rule.route).forEach(part => { - Object.assign(recursive,{ [part === '' ? '/' : part]: {} }) - recursive = recursive[part === '' ? '/' : part] - }) - - Object.assign(recursive, methods.reduce((acc,method) => { - return Object.assign(acc, { ['__'+method]: i }) - },{})) - - // Deep merge the maps - UTILS.deepMerge(rulesMap,map) - - return defaults(rule) - },{}) : {} + let rules = Array.isArray(cfg.rules) + ? cfg.rules.map((rule, i) => { + // Error if missing route or not a string + if (!rule.route || typeof rule.route !== 'string') + throw new ConfigurationError('Invalid route specified in rule'); + + // Parse methods into array (if not already) + let methods = ( + Array.isArray(rule.method) + ? rule.method + : typeof rule.method === 'string' + ? rule.method.split(',') + : ['ANY'] + ).map((x) => x.toString().trim().toUpperCase()); + + let map = {}; + let recursive = map; // create recursive reference + + UTILS.parsePath(rule.route).forEach((part) => { + Object.assign(recursive, { [part === '' ? '/' : part]: {} }); + recursive = recursive[part === '' ? '/' : part]; + }); + + Object.assign( + recursive, + methods.reduce((acc, method) => { + return Object.assign(acc, { ['__' + method]: i }); + }, {}) + ); + + // Deep merge the maps + UTILS.deepMerge(rulesMap, map); + + return defaults(rule); + }, {}) + : {}; return { - defaults: Object.assign(defaults(cfg),{ default:true }), + defaults: Object.assign(defaults(cfg), { default: true }), rules, - rulesMap - } - -} // end parseSamplerConfig + rulesMap, + }; +}; // end parseSamplerConfig diff --git a/lib/mimemap.js b/lib/mimemap.js index 1023d9e..491a149 100644 --- a/lib/mimemap.js +++ b/lib/mimemap.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -68,6 +68,5 @@ module.exports = { pptm: 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', potm: 'application/vnd.ms-powerpoint.template.macroEnabled.12', ppsm: 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', - mdb: 'application/vnd.ms-access' - -} + mdb: 'application/vnd.ms-access', +}; diff --git a/lib/prettyPrint.js b/lib/prettyPrint.js index 2f45721..cba1c82 100644 --- a/lib/prettyPrint.js +++ b/lib/prettyPrint.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -6,28 +6,79 @@ * @license MIT */ -module.exports = routes => { - - let out = '' +module.exports = (routes) => { + let out = ''; // Calculate column widths - let widths = routes.reduce((acc,row) => { - return [ - Math.max(acc[0],Math.max(6,row[0].length)), - Math.max(acc[1],Math.max(5,row[1].length)) - ] - },[0,0]) + let widths = routes.reduce( + (acc, row) => { + return [ + Math.max(acc[0], Math.max(6, row[0].length)), + Math.max(acc[1], Math.max(5, row[1].length)), + Math.max(acc[2], Math.max(6, row[2].join(', ').length)), + ]; + }, + [0, 0, 0] + ); - out += '╔══' + ''.padEnd(widths[0],'═') + '══╤══' + ''.padEnd(widths[1],'═') + '══╗\n' - out += '║ ' + '\u001b[1m' + 'METHOD'.padEnd(widths[0]) + '\u001b[0m' + ' │ ' + '\u001b[1m' + 'ROUTE'.padEnd(widths[1]) + '\u001b[0m' + ' ║\n' - out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n' - routes.forEach((route,i) => { - out += '║ ' + route[0].padEnd(widths[0]) + ' │ ' + route[1].padEnd(widths[1]) + ' ║\n' - if (i < routes.length-1) { - out += '╟──' + ''.padEnd(widths[0],'─') + '──┼──' + ''.padEnd(widths[1],'─') + '──╢\n' + out += + '╔══' + + ''.padEnd(widths[0], '═') + + '══╤══' + + ''.padEnd(widths[1], '═') + + '══╤══' + + ''.padEnd(widths[2], '═') + + '══╗\n'; + out += + '║ ' + + '\u001b[1m' + + 'METHOD'.padEnd(widths[0]) + + '\u001b[0m' + + ' │ ' + + '\u001b[1m' + + 'ROUTE'.padEnd(widths[1]) + + '\u001b[0m' + + ' │ ' + + '\u001b[1m' + + 'STACK'.padEnd(widths[2]) + + '\u001b[0m' + + ' ║\n'; + out += + '╟──' + + ''.padEnd(widths[0], '─') + + '──┼──' + + ''.padEnd(widths[1], '─') + + '──┼──' + + ''.padEnd(widths[2], '─') + + '──╢\n'; + routes.forEach((route, i) => { + out += + '║ ' + + route[0].padEnd(widths[0]) + + ' │ ' + + route[1].padEnd(widths[1]) + + ' │ ' + + route[2].join(', ').padEnd(widths[2]) + + ' ║\n'; + if (i < routes.length - 1) { + out += + '╟──' + + ''.padEnd(widths[0], '─') + + '──┼──' + + ''.padEnd(widths[1], '─') + + '──┼──' + + ''.padEnd(widths[2], '─') + + '──╢\n'; } // end if - }) - out += '╚══' + ''.padEnd(widths[0],'═') + '══╧══' + ''.padEnd(widths[1],'═') + '══╝' + }); + out += + '╚══' + + ''.padEnd(widths[0], '═') + + '══╧══' + + ''.padEnd(widths[1], '═') + + '══╧══' + + ''.padEnd(widths[2], '═') + + '══╝'; - return out -} + return out; +}; diff --git a/lib/request.js b/lib/request.js index 42e56e3..390bedb 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -6,275 +6,353 @@ * @license MIT */ -const QS = require('querystring') // Require the querystring library -const UTILS = require('./utils') // Require utils library -const LOGGER = require('./logger') // Require logger library -const { RouteError, MethodError } = require('./errors') // Require custom errors +const QS = require('querystring'); // Require the querystring library +const UTILS = require('./utils'); // Require utils library +const LOGGER = require('./logger'); // Require logger library +const { RouteError, MethodError } = require('./errors'); // Require custom errors class REQUEST { - // Create the constructor function. constructor(app) { - // Record start time - this._start = Date.now() + this._start = Date.now(); // Create a reference to the app - this.app = app + this.app = app; // Flag cold starts - this.coldStart = app._requestCount === 0 ? true : false + this.coldStart = app._requestCount === 0 ? true : false; // Increment the requests counter - this.requestCount = ++app._requestCount + this.requestCount = ++app._requestCount; // Init the handler - this._handler + this._handler; // Init the execution stack - this._stack + this._stack; // Expose Namespaces - this.namespace = this.ns = app._app + this.namespace = this.ns = app._app; // Set the version - this.version = app._version + this.version = app._version; // Init the params - this.params = {} + this.params = {}; // Init headers - this.headers = {} + this.headers = {}; // Init multi-value support flag - this._multiValueSupport = null + this._multiValueSupport = null; // Init log helpers (message,custom) and create app reference - app.log = this.log = Object.keys(app._logLevels).reduce((acc,lvl) => - Object.assign(acc,{ [lvl]: (m,c) => this.logger(lvl, m, this, this.context, c) }),{}) + app.log = this.log = Object.keys(app._logLevels).reduce( + (acc, lvl) => + Object.assign(acc, { + [lvl]: (m, c) => this.logger(lvl, m, this, this.context, c), + }), + {} + ); // Init _logs array for storage - this._logs = [] - + this._logs = []; } // end constructor // Parse the request async parseRequest() { - // Set the payload version - this.payloadVersion = this.app._event.version ? this.app._event.version : null - + this.payloadVersion = this.app._event.version + ? this.app._event.version + : null; + // Detect multi-value support - this._multiValueSupport = 'multiValueHeaders' in this.app._event + this._multiValueSupport = 'multiValueHeaders' in this.app._event; // Set the method - this.method = this.app._event.httpMethod ? this.app._event.httpMethod.toUpperCase() - : this.app._event.requestContext && this.app._event.requestContext.http ? this.app._event.requestContext.http.method.toUpperCase() - : 'GET' + this.method = this.app._event.httpMethod + ? this.app._event.httpMethod.toUpperCase() + : this.app._event.requestContext && this.app._event.requestContext.http + ? this.app._event.requestContext.http.method.toUpperCase() + : 'GET'; // Set the path - this.path = this.payloadVersion === '2.0' ? this.app._event.rawPath : this.app._event.path + this.path = + this.payloadVersion === '2.0' + ? this.app._event.rawPath + : this.app._event.path; // Set the query parameters (backfill for ALB) - this.query = Object.assign({}, this.app._event.queryStringParameters, - 'queryStringParameters' in this.app._event ? {} // do nothing - : Object.keys(Object.assign({},this.app._event.multiValueQueryStringParameters)) - .reduce((qs,key) => Object.assign(qs, // get the last value of the array - { [key]: decodeURIComponent(this.app._event.multiValueQueryStringParameters[key].slice(-1)[0]) } - ), {}) - ) + this.query = Object.assign( + {}, + this.app._event.queryStringParameters, + 'queryStringParameters' in this.app._event + ? {} // do nothing + : Object.keys( + Object.assign({}, this.app._event.multiValueQueryStringParameters) + ).reduce( + (qs, key) => + Object.assign( + qs, // get the last value of the array + { + [key]: decodeURIComponent( + this.app._event.multiValueQueryStringParameters[key].slice( + -1 + )[0] + ), + } + ), + {} + ) + ); // Set the multi-value query parameters (simulate if no multi-value support) - this.multiValueQuery = Object.assign({}, - this._multiValueSupport ? {} : Object.keys(this.query) - .reduce((qs,key) => Object.assign(qs, { [key]: this.query[key].split(',') }), {}), - this.app._event.multiValueQueryStringParameters) + this.multiValueQuery = Object.assign( + {}, + this._multiValueSupport + ? {} + : Object.keys(this.query).reduce( + (qs, key) => + Object.assign(qs, { [key]: this.query[key].split(',') }), + {} + ), + this.app._event.multiValueQueryStringParameters + ); // Set the raw headers (normalize multi-values) // per https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 - this.rawHeaders = this._multiValueSupport && this.app._event.multiValueHeaders !== null ? - Object.keys(this.app._event.multiValueHeaders).reduce((headers,key) => - Object.assign(headers,{ [key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]) }),{}) - : this.app._event.headers || {} + this.rawHeaders = + this._multiValueSupport && this.app._event.multiValueHeaders !== null + ? Object.keys(this.app._event.multiValueHeaders).reduce( + (headers, key) => + Object.assign(headers, { + [key]: UTILS.fromArray(this.app._event.multiValueHeaders[key]), + }), + {} + ) + : this.app._event.headers || {}; // Set the headers to lowercase - this.headers = Object.keys(this.rawHeaders).reduce((acc,header) => - Object.assign(acc,{[header.toLowerCase()]:this.rawHeaders[header]}), {}) - - this.multiValueHeaders = this._multiValueSupport ? this.app._event.multiValueHeaders - : Object.keys(this.headers).reduce((headers,key) => - Object.assign(headers,{ [key.toLowerCase()]: this.headers[key].split(',') }),{}) + this.headers = Object.keys(this.rawHeaders).reduce( + (acc, header) => + Object.assign(acc, { [header.toLowerCase()]: this.rawHeaders[header] }), + {} + ); + + this.multiValueHeaders = this._multiValueSupport + ? this.app._event.multiValueHeaders + : Object.keys(this.headers).reduce( + (headers, key) => + Object.assign(headers, { + [key.toLowerCase()]: this.headers[key].split(','), + }), + {} + ); // Extract user agent - this.userAgent = this.headers['user-agent'] + this.userAgent = this.headers['user-agent']; // Get cookies from event - let cookies = this.app._event.cookies ? this.app._event.cookies - : this.headers.cookie ? this.headers.cookie.split(';') - : [] - + let cookies = this.app._event.cookies + ? this.app._event.cookies + : this.headers.cookie + ? this.headers.cookie.split(';') + : []; + // Set and parse cookies - this.cookies = cookies.reduce((acc,cookie) => { - cookie = cookie.trim().split('=') - return Object.assign(acc,{ [cookie[0]] : UTILS.parseBody(decodeURIComponent(cookie[1])) }) - },{}) + this.cookies = cookies.reduce((acc, cookie) => { + cookie = cookie.trim().split('='); + return Object.assign(acc, { + [cookie[0]]: UTILS.parseBody(decodeURIComponent(cookie[1])), + }); + }, {}); // Attempt to parse the auth - this.auth = UTILS.parseAuth(this.headers.authorization) + this.auth = UTILS.parseAuth(this.headers.authorization); // Set the requestContext - this.requestContext = this.app._event.requestContext || {} + this.requestContext = this.app._event.requestContext || {}; // Extract IP (w/ sourceIp fallback) - this.ip = (this.headers['x-forwarded-for'] && this.headers['x-forwarded-for'].split(',')[0].trim()) - || (this.requestContext['identity'] && this.requestContext['identity']['sourceIp'] - && this.requestContext['identity']['sourceIp'].split(',')[0].trim()) + this.ip = + (this.headers['x-forwarded-for'] && + this.headers['x-forwarded-for'].split(',')[0].trim()) || + (this.requestContext['identity'] && + this.requestContext['identity']['sourceIp'] && + this.requestContext['identity']['sourceIp'].split(',')[0].trim()); // Assign the requesting interface - this.interface = this.requestContext.elb ? 'alb' : 'apigateway' + this.interface = this.requestContext.elb ? 'alb' : 'apigateway'; // Set the pathParameters - this.pathParameters = this.app._event.pathParameters || {} + this.pathParameters = this.app._event.pathParameters || {}; // Set the stageVariables - this.stageVariables = this.app._event.stageVariables || {} + this.stageVariables = this.app._event.stageVariables || {}; // Set the isBase64Encoded - this.isBase64Encoded = this.app._event.isBase64Encoded || false + this.isBase64Encoded = this.app._event.isBase64Encoded || false; // Add context - this.context = this.app.context && typeof this.app.context === 'object' ? this.app.context : {} + this.context = + this.app.context && typeof this.app.context === 'object' + ? this.app.context + : {}; // Parse id from context - this.id = this.context.awsRequestId ? this.context.awsRequestId : null + this.id = this.context.awsRequestId ? this.context.awsRequestId : null; // Determine client type this.clientType = - this.headers['cloudfront-is-desktop-viewer'] === 'true' ? 'desktop' : - this.headers['cloudfront-is-mobile-viewer'] === 'true' ? 'mobile' : - this.headers['cloudfront-is-smarttv-viewer'] === 'true' ? 'tv' : - this.headers['cloudfront-is-tablet-viewer'] === 'true' ? 'tablet' : - 'unknown' + this.headers['cloudfront-is-desktop-viewer'] === 'true' + ? 'desktop' + : this.headers['cloudfront-is-mobile-viewer'] === 'true' + ? 'mobile' + : this.headers['cloudfront-is-smarttv-viewer'] === 'true' + ? 'tv' + : this.headers['cloudfront-is-tablet-viewer'] === 'true' + ? 'tablet' + : 'unknown'; // Parse country - this.clientCountry = this.headers['cloudfront-viewer-country'] ? - this.headers['cloudfront-viewer-country'].toUpperCase() : 'unknown' + this.clientCountry = this.headers['cloudfront-viewer-country'] + ? this.headers['cloudfront-viewer-country'].toUpperCase() + : 'unknown'; // Capture the raw body - this.rawBody = this.app._event.body + this.rawBody = this.app._event.body; // Set the body (decode it if base64 encoded) - this.body = this.app._event.isBase64Encoded ? Buffer.from(this.app._event.body || '', 'base64').toString() : this.app._event.body + this.body = this.app._event.isBase64Encoded + ? Buffer.from(this.app._event.body || '', 'base64').toString() + : this.app._event.body; // Set the body - if (this.headers['content-type'] && this.headers['content-type'].includes('application/x-www-form-urlencoded')) { - this.body = QS.parse(this.body) + if ( + this.headers['content-type'] && + this.headers['content-type'].includes('application/x-www-form-urlencoded') + ) { + this.body = QS.parse(this.body); } else if (typeof this.body === 'object') { // Do nothing } else { - this.body = UTILS.parseBody(this.body) + this.body = UTILS.parseBody(this.body); } // Init the stack reporter - this.stack = null + this.stack = null; // Extract path from event (strip querystring just in case) - let path = UTILS.parsePath(this.path) + let path = UTILS.parsePath(this.path); // Init the route - this.route = null + this.route = null; // Create a local routes reference - let routes = this.app._routes + let routes = this.app._routes; // Init wildcard - let wc = [] + let wc = []; // Loop the routes and see if this matches - for (let i=0; i this.params[y] = path[x]) + route.vars[x].map((y) => (this.params[y] = path[x])); } // end for // Set the route used - this.route = route.route + this.route = route.route; // Set the execution stack - this._stack = route.inherited.concat(route.stack) + // this._stack = route.inherited.concat(route.stack); + this._stack = route.stack; // Set the stack reporter - this.stack = this._stack.map(x => x.name.trim() !== '' ? x.name : 'unnamed') - + this.stack = this._stack.map((x) => + x.name.trim() !== '' ? x.name : 'unnamed' + ); } else { - this.app._errorStatus = 405 - throw new MethodError('Method not allowed',this.method,'/'+path.join('/')) + this.app._errorStatus = 405; + throw new MethodError( + 'Method not allowed', + this.method, + '/' + path.join('/') + ); } // Reference to sample rule - this._sampleRule = {} + this._sampleRule = {}; // Enable sampling - this._sample = LOGGER.sampler(this.app,this) - + this._sample = LOGGER.sampler(this.app, this); } // end parseRequest // Main logger logger(...args) { this.app._logger.level !== 'none' && this.app._logLevels[args[0]] >= - this.app._logLevels[this._sample ? this._sample : this.app._logger.level] && - this._logs.push(this.app._logger.log(...args)) + this.app._logLevels[ + this._sample ? this._sample : this.app._logger.level + ] && + this._logs.push(this.app._logger.log(...args)); } // Recursive wildcard function validWildcard(wc) { - return Object.keys(wc[wc.length-1]['METHODS']).length > 1 - || (wc.length > 1 && this.validWildcard(wc.slice(0,-1))) + return ( + Object.keys(wc[wc.length - 1]['METHODS']).length > 1 || + (wc.length > 1 && this.validWildcard(wc.slice(0, -1))) + ); } - } // end REQUEST class // Export the response object -module.exports = REQUEST +module.exports = REQUEST; diff --git a/lib/response.js b/lib/response.js index e7478b7..68f60a2 100644 --- a/lib/response.js +++ b/lib/response.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -6,514 +6,595 @@ * @license MIT */ -const UTILS = require('./utils.js') +const UTILS = require('./utils.js'); -const fs = require('fs') // Require Node.js file system -const path = require('path') // Require Node.js path -const compression = require('./compression') // Require compression lib -const { ResponseError, FileError } = require('./errors') // Require custom errors +const fs = require('fs'); // Require Node.js file system +const path = require('path'); // Require Node.js path +const compression = require('./compression'); // Require compression lib +const { ResponseError, FileError } = require('./errors'); // Require custom errors // Require AWS S3 service -const S3 = require('./s3-service') +const S3 = require('./s3-service'); class RESPONSE { - // Create the constructor function. - constructor(app,request) { - + constructor(app, request) { // Add a reference to the main app - app._response = this + app._response = this; // Create a reference to the app - this.app = app + this.app = app; // Create a reference to the request - this._request = request + this._request = request; // Create a reference to the JSON serializer - this._serializer = app._serializer + this._serializer = app._serializer; // Set the default state to processing - this._state = 'processing' + this._state = 'processing'; // Default statusCode to 200 - this._statusCode = 200 + this._statusCode = 200; // Default the header - this._headers = Object.assign({ - // Set the Content-Type by default - 'content-type': ['application/json'], //charset=UTF-8 - }, app._headers) + this._headers = Object.assign( + { + // Set the Content-Type by default + 'content-type': ['application/json'], //charset=UTF-8 + }, + app._headers + ); // base64 encoding flag - this._isBase64 = app._isBase64 + this._isBase64 = app._isBase64; // compression flag - this._compression = app._compression + this._compression = app._compression; // Default callback function - this._callback = 'callback' + this._callback = 'callback'; // Default Etag support - this._etag = false + this._etag = false; // Default response object - this._response = {} + this._response = {}; } // Sets the statusCode status(code) { - this._statusCode = code - return this + this._statusCode = code; + return this; } // Adds a header field - header(key,value,append) { - let _key = key.toLowerCase() // store as lowercase - let _values = value ? (Array.isArray(value) ? value : [value]) : [''] - this._headers[_key] = append ? - this.hasHeader(_key) ? this._headers[_key].concat(_values) : _values - : _values - return this + header(key, value, append) { + let _key = key.toLowerCase(); // store as lowercase + let _values = value ? (Array.isArray(value) ? value : [value]) : ['']; + this._headers[_key] = append + ? this.hasHeader(_key) + ? this._headers[_key].concat(_values) + : _values + : _values; + return this; } // Gets a header field - getHeader(key,asArr) { - if (!key) return asArr ? this._headers : - Object.keys(this._headers).reduce((headers,key) => - Object.assign(headers, { [key]: this._headers[key].toString() }) - ,{}) // return all headers - return asArr ? this._headers[key.toLowerCase()] - : this._headers[key.toLowerCase()] ? - this._headers[key.toLowerCase()].toString() : undefined + getHeader(key, asArr) { + if (!key) + return asArr + ? this._headers + : Object.keys(this._headers).reduce( + (headers, key) => + Object.assign(headers, { [key]: this._headers[key].toString() }), + {} + ); // return all headers + return asArr + ? this._headers[key.toLowerCase()] + : this._headers[key.toLowerCase()] + ? this._headers[key.toLowerCase()].toString() + : undefined; } getHeaders() { - return this._headers + return this._headers; } // Removes a header field removeHeader(key) { - delete this._headers[key.toLowerCase()] - return this + delete this._headers[key.toLowerCase()]; + return this; } // Returns boolean if header exists hasHeader(key) { - return this.getHeader(key ? key : '') !== undefined + return this.getHeader(key ? key : '') !== undefined; } // Convenience method for JSON json(body) { - this.header('Content-Type','application/json').send(this._serializer(body)) + this.header('Content-Type', 'application/json').send( + this._serializer(body) + ); } // Convenience method for JSONP jsonp(body) { // Check the querystring for callback or cb - let query = this.app._event.queryStringParameters || {} - let cb = query[this.app._callbackName] - - this.header('Content-Type','application/json') - .send((cb ? cb.replace(' ','_') : 'callback') + '(' + this._serializer(body) + ')') + let query = this.app._event.queryStringParameters || {}; + let cb = query[this.app._callbackName]; + + this.header('Content-Type', 'application/json').send( + (cb ? cb.replace(' ', '_') : 'callback') + + '(' + + this._serializer(body) + + ')' + ); } // Convenience method for HTML html(body) { - this.header('Content-Type','text/html').send(body) + this.header('Content-Type', 'text/html').send(body); } // Convenience method for setting Location header location(path) { - this.header('Location',UTILS.encodeUrl(path)) - return this + this.header('Location', UTILS.encodeUrl(path)); + return this; } // Convenience method for Redirect async redirect(path) { - let statusCode = 302 // default + let statusCode = 302; // default try { // If status code is provided if (arguments.length === 2) { - if ([300,301,302,303,307,308].includes(arguments[0])) { - statusCode = arguments[0] - path = arguments[1] + if ([300, 301, 302, 303, 307, 308].includes(arguments[0])) { + statusCode = arguments[0]; + path = arguments[1]; } else { - throw new ResponseError(arguments[0] + ' is an invalid redirect status code',arguments[0]) + throw new ResponseError( + arguments[0] + ' is an invalid redirect status code', + arguments[0] + ); } } // Auto convert S3 paths to signed URLs - if (UTILS.isS3(path)) path = await this.getLink(path) + if (UTILS.isS3(path)) path = await this.getLink(path); - let url = UTILS.escapeHtml(path) + let url = UTILS.escapeHtml(path); this.location(path) .status(statusCode) - .html(`

${statusCode} Redirecting to ${url}

`) - - } catch(e) { - this.error(e) + .html( + `

${statusCode} Redirecting to ${url}

` + ); + } catch (e) { + this.error(e); } } // end redirect // Convenience method for retrieving a signed link to an S3 bucket object - async getLink(path,expires,callback) { - let params = UTILS.parseS3(path) + async getLink(path, expires, callback) { + let params = UTILS.parseS3(path); // Default Expires - params.Expires = !isNaN(expires) ? parseInt(expires) : 900 + params.Expires = !isNaN(expires) ? parseInt(expires) : 900; // Default callback - let fn = typeof expires === 'function' ? expires : - typeof callback === 'function' ? callback : e => { if (e) this.error(e) } + let fn = + typeof expires === 'function' + ? expires + : typeof callback === 'function' + ? callback + : (e) => { + if (e) this.error(e); + }; // getSignedUrl doesn't support .promise() - return await new Promise(r => S3.getSignedUrl('getObject',params, async (e,url) => { - if (e) { - // Execute callback with caught error - await fn(e) - this.error(e) // Throw error if not done in callback - } - r(url) // return the url - })) + return await new Promise((r) => + S3.getSignedUrl('getObject', params, async (e, url) => { + if (e) { + // Execute callback with caught error + await fn(e); + this.error(e); // Throw error if not done in callback + } + r(url); // return the url + }) + ); } // end getLink // Convenience method for setting cookies // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - cookie(name,value,opts={}) { - + cookie(name, value, opts = {}) { // Set the name and value of the cookie - let cookieString = (typeof name !== 'string' ? name.toString() : name) - + '=' + encodeURIComponent(UTILS.encodeBody(value)) + let cookieString = + (typeof name !== 'string' ? name.toString() : name) + + '=' + + encodeURIComponent(UTILS.encodeBody(value)); // domain (String): Domain name for the cookie - cookieString += opts.domain ? '; Domain=' + opts.domain : '' + cookieString += opts.domain ? '; Domain=' + opts.domain : ''; // expires (Date): Expiry date of the cookie, convert to GMT - cookieString += opts.expires && typeof opts.expires.toUTCString === 'function' ? - '; Expires=' + opts.expires.toUTCString() : '' + cookieString += + opts.expires && typeof opts.expires.toUTCString === 'function' + ? '; Expires=' + opts.expires.toUTCString() + : ''; // httpOnly (Boolean): Flags the cookie to be accessible only by the web server - cookieString += opts.httpOnly && opts.httpOnly === true ? '; HttpOnly' : '' + cookieString += opts.httpOnly && opts.httpOnly === true ? '; HttpOnly' : ''; // maxAge (Number) Set expiry time relative to the current time in milliseconds - cookieString += opts.maxAge && !isNaN(opts.maxAge) ? - '; MaxAge=' + (opts.maxAge/1000|0) - + (!opts.expires ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() : '') - : '' + cookieString += + opts.maxAge && !isNaN(opts.maxAge) + ? '; MaxAge=' + + ((opts.maxAge / 1000) | 0) + + (!opts.expires + ? '; Expires=' + new Date(Date.now() + opts.maxAge).toUTCString() + : '') + : ''; // path (String): Path for the cookie - cookieString += opts.path ? '; Path=' + opts.path : '; Path=/' + cookieString += opts.path ? '; Path=' + opts.path : '; Path=/'; // secure (Boolean): Marks the cookie to be used with HTTPS only - cookieString += opts.secure && opts.secure === true ? '; Secure' : '' + cookieString += opts.secure && opts.secure === true ? '; Secure' : ''; // sameSite (Boolean or String) Value of the “SameSite” Set-Cookie attribute // see https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1. - cookieString += opts.sameSite !== undefined ? '; SameSite=' - + (opts.sameSite === true ? 'Strict' : - (opts.sameSite === false ? 'Lax' : opts.sameSite )) - : '' - - this.header('Set-Cookie',cookieString,true) - return this + cookieString += + opts.sameSite !== undefined + ? '; SameSite=' + + (opts.sameSite === true + ? 'Strict' + : opts.sameSite === false + ? 'Lax' + : opts.sameSite) + : ''; + + this.header('Set-Cookie', cookieString, true); + return this; } // Convenience method for clearing cookies - clearCookie(name,opts={}) { - let options = Object.assign(opts, { expires: new Date(1), maxAge: -1000 }) - return this.cookie(name,'',options) + clearCookie(name, opts = {}) { + let options = Object.assign(opts, { expires: new Date(1), maxAge: -1000 }); + return this.cookie(name, '', options); } - // Set content-disposition header and content type attachment(filename) { // Check for supplied filename/path - let name = typeof filename === 'string' && filename.trim().length > 0 ? path.parse(filename) : undefined - this.header('Content-Disposition','attachment' + (name ? '; filename="' + name.base + '"' : '')) + let name = + typeof filename === 'string' && filename.trim().length > 0 + ? path.parse(filename) + : undefined; + this.header( + 'Content-Disposition', + 'attachment' + (name ? '; filename="' + name.base + '"' : '') + ); // If name exits, attempt to set the type - if (name) { this.type(name.ext) } - return this + if (name) { + this.type(name.ext); + } + return this; } - // Convenience method combining attachment() and sendFile() download(file, filename, options, callback) { - - let name = filename - let opts = typeof options === 'object' ? options : {} - let fn = typeof callback === 'function' ? callback : undefined + let name = filename; + let opts = typeof options === 'object' ? options : {}; + let fn = typeof callback === 'function' ? callback : undefined; // Add optional parameter support for callback if (typeof filename === 'function') { - name = undefined - fn = filename + name = undefined; + fn = filename; } else if (typeof options === 'function') { - fn = options + fn = options; } // Add optional parameter support for options if (typeof filename === 'object') { - name = undefined - opts = filename + name = undefined; + opts = filename; } // Add the Content-Disposition header - this.attachment(name ? name : (typeof file === 'string' ? path.basename(file) : null) ) + this.attachment( + name ? name : typeof file === 'string' ? path.basename(file) : null + ); // Send the file - this.sendFile(file, opts, fn) - + this.sendFile(file, opts, fn); } - // Convenience method for returning static files async sendFile(file, options, callback) { + let buffer, modified; - let buffer, modified - - let opts = typeof options === 'object' ? options : {} - let fn = typeof callback === 'function' ? callback : () => {} + let opts = typeof options === 'object' ? options : {}; + let fn = typeof callback === 'function' ? callback : () => {}; // Add optional parameter support if (typeof options === 'function') { - fn = options + fn = options; } // Begin a try-catch block for callback errors try { - // Create buffer based on input if (typeof file === 'string') { - - let filepath = file.trim() + let filepath = file.trim(); // If an S3 file identifier if (/^s3:\/\//i.test(filepath)) { - - let params = UTILS.parseS3(filepath) + let params = UTILS.parseS3(filepath); // Attempt to get the object from S3 - let data = await S3.getObject(params).promise() + let data = await S3.getObject(params).promise(); // Set results, type and header - buffer = data.Body - modified = data.LastModified - this.type(data.ContentType) - this.header('ETag',data.ETag) + buffer = data.Body; + modified = data.LastModified; + this.type(data.ContentType); + this.header('ETag', data.ETag); - // else try and load the file locally + // else try and load the file locally } else { - buffer = fs.readFileSync((opts.root ? opts.root : '') + filepath) - modified = opts.lastModified !== false ? fs.statSync((opts.root ? opts.root : '') + filepath).mtime : undefined - this.type(path.extname(filepath)) + buffer = fs.readFileSync((opts.root ? opts.root : '') + filepath); + modified = + opts.lastModified !== false + ? fs.statSync((opts.root ? opts.root : '') + filepath).mtime + : undefined; + this.type(path.extname(filepath)); } - // If the input is a buffer, pass through + // If the input is a buffer, pass through } else if (Buffer.isBuffer(file)) { - buffer = file + buffer = file; } else { - throw new FileError('Invalid file',{path:file}) + throw new FileError('Invalid file', { path: file }); } // Add headers from options if (typeof opts.headers === 'object') { - Object.keys(opts.headers).map(header => { - this.header(header,opts.headers[header]) - }) + Object.keys(opts.headers).map((header) => { + this.header(header, opts.headers[header]); + }); } // Add cache-control headers if (opts.cacheControl !== false) { if (opts.cacheControl !== true && opts.cacheControl !== undefined) { - this.cache(opts.cacheControl) + this.cache(opts.cacheControl); } else { - this.cache( - !isNaN(opts.maxAge) ? opts.maxAge : 0, - opts.private - ) + this.cache(!isNaN(opts.maxAge) ? opts.maxAge : 0, opts.private); } } // Add last-modified headers if (opts.lastModified !== false) { - this.modified(opts.lastModified ? opts.lastModified : modified) + this.modified(opts.lastModified ? opts.lastModified : modified); } // Execute callback - await fn() + await fn(); // Set base64 encoding flag - this._isBase64 = true + this._isBase64 = true; // Convert buffer to base64 string - this.send(buffer.toString('base64')) - - } catch(e) { // TODO: Add second catch? + this.send(buffer.toString('base64')); + } catch (e) { + // TODO: Add second catch? // Execute callback with caught error - await fn(e) + await fn(e); // If missing file if (e.code === 'ENOENT') { - this.error(new FileError('No such file',e)) + this.error(new FileError('No such file', e)); } else { - this.error(e) // Throw error if not done in callback + this.error(e); // Throw error if not done in callback } } - } // end sendFile - // Convenience method for setting type type(type) { - let mimeType = UTILS.mimeLookup(type,this.app._mimeTypes) + let mimeType = UTILS.mimeLookup(type, this.app._mimeTypes); if (mimeType) { - this.header('Content-Type',mimeType) + this.header('Content-Type', mimeType); } - return this + return this; } - - // Convenience method for sending status codes sendStatus(status) { - this.status(status).send(UTILS.statusLookup(status)) + this.status(status).send(UTILS.statusLookup(status)); } - // Convenience method for setting CORS headers cors(options) { - const opts = typeof options === 'object' ? options : {} + const opts = typeof options === 'object' ? options : {}; // Check for existing headers - let acao = this.getHeader('Access-Control-Allow-Origin') - let acam = this.getHeader('Access-Control-Allow-Methods') - let acah = this.getHeader('Access-Control-Allow-Headers') + let acao = this.getHeader('Access-Control-Allow-Origin'); + let acam = this.getHeader('Access-Control-Allow-Methods'); + let acah = this.getHeader('Access-Control-Allow-Headers'); // Default CORS headers - this.header('Access-Control-Allow-Origin',opts.origin ? opts.origin : (acao ? acao : '*')) - this.header('Access-Control-Allow-Methods',opts.methods ? opts.methods : (acam ? acam : 'GET, PUT, POST, DELETE, OPTIONS')) - this.header('Access-Control-Allow-Headers',opts.headers ? opts.headers : (acah ? acah : 'Content-Type, Authorization, Content-Length, X-Requested-With')) + this.header( + 'Access-Control-Allow-Origin', + opts.origin ? opts.origin : acao ? acao : '*' + ); + this.header( + 'Access-Control-Allow-Methods', + opts.methods + ? opts.methods + : acam + ? acam + : 'GET, PUT, POST, DELETE, OPTIONS' + ); + this.header( + 'Access-Control-Allow-Headers', + opts.headers + ? opts.headers + : acah + ? acah + : 'Content-Type, Authorization, Content-Length, X-Requested-With' + ); // Optional CORS headers - if(opts.maxAge && !isNaN(opts.maxAge)) this.header('Access-Control-Max-Age',(opts.maxAge/1000|0).toString()) - if(opts.credentials) this.header('Access-Control-Allow-Credentials',opts.credentials.toString()) - if(opts.exposeHeaders) this.header('Access-Control-Expose-Headers',opts.exposeHeaders) - - return this + if (opts.maxAge && !isNaN(opts.maxAge)) + this.header( + 'Access-Control-Max-Age', + ((opts.maxAge / 1000) | 0).toString() + ); + if (opts.credentials) + this.header( + 'Access-Control-Allow-Credentials', + opts.credentials.toString() + ); + if (opts.exposeHeaders) + this.header('Access-Control-Expose-Headers', opts.exposeHeaders); + + return this; } - // Enable/Disable Etag etag(enable) { - this._etag = enable === true ? true : false - return this + this._etag = enable === true ? true : false; + return this; } // Add cache-control headers - cache(maxAge,isPrivate=false) { + cache(maxAge, isPrivate = false) { // if custom string value if (maxAge !== true && maxAge !== undefined && typeof maxAge === 'string') { - this.header('Cache-Control', maxAge) + this.header('Cache-Control', maxAge); } else if (maxAge === false) { - this.header('Cache-Control', 'no-cache, no-store, must-revalidate') + this.header('Cache-Control', 'no-cache, no-store, must-revalidate'); } else { - maxAge = maxAge && !isNaN(maxAge) ? (maxAge/1000|0) : 0 - this.header('Cache-Control', (isPrivate === true ? 'private, ' : '') + 'max-age=' + maxAge) - this.header('Expires',new Date(Date.now() + maxAge).toUTCString()) + maxAge = maxAge && !isNaN(maxAge) ? (maxAge / 1000) | 0 : 0; + this.header( + 'Cache-Control', + (isPrivate === true ? 'private, ' : '') + 'max-age=' + maxAge + ); + this.header('Expires', new Date(Date.now() + maxAge).toUTCString()); } - return this + return this; } // Add last-modified headers modified(date) { if (date !== false) { - let lastModified = date && typeof date.toUTCString === 'function' ? date : - date && Date.parse(date) ? new Date(date) : new Date() - this.header('Last-Modified', lastModified.toUTCString()) + let lastModified = + date && typeof date.toUTCString === 'function' + ? date + : date && Date.parse(date) + ? new Date(date) + : new Date(); + this.header('Last-Modified', lastModified.toUTCString()); } - return this + return this; } // Sends the request to the main callback send(body) { - // Generate Etag - if ( this._etag // if etag support enabled - && ['GET','HEAD'].includes(this._request.method) - && !this.hasHeader('etag') - && this._statusCode === 200 + if ( + this._etag && // if etag support enabled + ['GET', 'HEAD'].includes(this._request.method) && + !this.hasHeader('etag') && + this._statusCode === 200 ) { - this.header('etag','"'+UTILS.generateEtag(body)+'"') + this.header('etag', '"' + UTILS.generateEtag(body) + '"'); } // Check for matching Etag if ( - this._request.headers['if-none-match'] - && this._request.headers['if-none-match'] === this.getHeader('etag') + this._request.headers['if-none-match'] && + this._request.headers['if-none-match'] === this.getHeader('etag') ) { - this.status(304) - body = '' + this.status(304); + body = ''; } - let headers = {} - let cookies = {} + let headers = {}; + let cookies = {}; if (this._request.payloadVersion === '2.0') { if (this._headers['set-cookie']) { - cookies = { cookies: this._headers['set-cookie'] } - delete this._headers['set-cookie'] + cookies = { cookies: this._headers['set-cookie'] }; + delete this._headers['set-cookie']; } } - + if (this._request._multiValueSupport) { - headers = { multiValueHeaders: this._headers } + headers = { multiValueHeaders: this._headers }; } else { - headers = { headers: UTILS.stringifyHeaders(this._headers) } + headers = { headers: UTILS.stringifyHeaders(this._headers) }; } // Create the response - this._response = Object.assign({}, + this._response = Object.assign( + {}, headers, cookies, { statusCode: this._statusCode, - body: this._request.method === 'HEAD' ? '' : UTILS.encodeBody(body,this._serializer), - isBase64Encoded: this._isBase64 + body: + this._request.method === 'HEAD' + ? '' + : UTILS.encodeBody(body, this._serializer), + isBase64Encoded: this._isBase64, }, - this._request.interface === 'alb' ? { statusDescription: `${this._statusCode} ${UTILS.statusLookup(this._statusCode)}` } : {} - ) + this._request.interface === 'alb' + ? { + statusDescription: `${this._statusCode} ${UTILS.statusLookup( + this._statusCode + )}`, + } + : {} + ); // Compress the body if (this._compression && this._response.body) { - const { data, contentEncoding } = compression.compress(this._response.body, this._request.headers) + const { data, contentEncoding } = compression.compress( + this._response.body, + this._request.headers + ); if (contentEncoding) { - Object.assign(this._response, { body: data.toString('base64'), isBase64Encoded: true }) + Object.assign(this._response, { + body: data.toString('base64'), + isBase64Encoded: true, + }); if (this._response.multiValueHeaders) { - this._response.multiValueHeaders['content-encoding'] = [contentEncoding] + this._response.multiValueHeaders['content-encoding'] = [ + contentEncoding, + ]; } else { - this._response.headers['content-encoding'] = contentEncoding + this._response.headers['content-encoding'] = contentEncoding; } } } // Trigger the callback function - this.app._callback(null, this._response, this) - + this.app._callback(null, this._response, this); } // end send - // Trigger API error - error(code,e,detail) { - detail = typeof code !== 'number' && e !== undefined ? e : detail - e = typeof code !== 'number' ? code : e - code = typeof code === 'number' ? code : undefined - this.app.catchErrors(e,this,code,detail) + error(code, e, detail) { + detail = typeof code !== 'number' && e !== undefined ? e : detail; + e = typeof code !== 'number' ? code : e; + code = typeof code === 'number' ? code : undefined; + this.app.catchErrors(e, this, code, detail); } // end error - } // end Response class - // Export the response object -module.exports = RESPONSE +module.exports = RESPONSE; diff --git a/lib/s3-service.js b/lib/s3-service.js index 824a833..1963453 100644 --- a/lib/s3-service.js +++ b/lib/s3-service.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -7,7 +7,7 @@ */ // Require AWS SDK -const AWS = require('aws-sdk') // AWS SDK +const AWS = require('aws-sdk'); // AWS SDK // Export -module.exports = new AWS.S3() +module.exports = new AWS.S3(); diff --git a/lib/statusCodes.js b/lib/statusCodes.js index 63a363d..8a7b6b4 100644 --- a/lib/statusCodes.js +++ b/lib/statusCodes.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -78,5 +78,5 @@ module.exports = { 507: 'Insufficient Storage', 508: 'Loop Detected', 510: 'Not Extended', - 511: 'Network Authentication Required' -} + 511: 'Network Authentication Required', +}; diff --git a/lib/utils.js b/lib/utils.js index 6bcbbfc..17b4ed3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; /** * Lightweight web framework for your serverless applications @@ -6,164 +6,197 @@ * @license MIT */ -const QS = require('querystring') // Require the querystring library -const crypto = require('crypto') // Require Node.js crypto library -const { FileError } = require('./errors') // Require custom errors +const QS = require('querystring'); // Require the querystring library +const crypto = require('crypto'); // Require Node.js crypto library +const { FileError } = require('./errors'); // Require custom errors const entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', - '\'': ''' -} - -exports.escapeHtml = html => html.replace(/[&<>"']/g, s => entityMap[s]) + "'": ''', +}; +exports.escapeHtml = (html) => html.replace(/[&<>"']/g, (s) => entityMap[s]); // From encodeurl by Douglas Christopher Wilson -let ENCODE_CHARS_REGEXP = /(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g -let UNMATCHED_SURROGATE_PAIR_REGEXP = /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g -let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2' - -exports.encodeUrl = url => String(url) - .replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) - .replace(ENCODE_CHARS_REGEXP, encodeURI) - - - -const encodeBody = (body,serializer) => { - const encode = typeof serializer === 'function' ? serializer : JSON.stringify - return typeof body === 'object' ? encode(body) : (body && typeof body !== 'string' ? body.toString() : (body ? body : '')) -} - -exports.encodeBody = encodeBody - -exports.parsePath = path => { - return path ? path.trim().split('?')[0].replace(/^\/(.*?)(\/)*$/,'$1').split('/') : [] -} - - -exports.parseBody = body => { +let ENCODE_CHARS_REGEXP = + /(?:[^\x21\x25\x26-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7A\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g; +let UNMATCHED_SURROGATE_PAIR_REGEXP = + /(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g; +let UNMATCHED_SURROGATE_PAIR_REPLACE = '$1\uFFFD$2'; + +exports.encodeUrl = (url) => + String(url) + .replace(UNMATCHED_SURROGATE_PAIR_REGEXP, UNMATCHED_SURROGATE_PAIR_REPLACE) + .replace(ENCODE_CHARS_REGEXP, encodeURI); + +const encodeBody = (body, serializer) => { + const encode = typeof serializer === 'function' ? serializer : JSON.stringify; + return typeof body === 'object' + ? encode(body) + : body && typeof body !== 'string' + ? body.toString() + : body + ? body + : ''; +}; + +exports.encodeBody = encodeBody; + +exports.parsePath = (path) => { + return path + ? path + .trim() + .split('?')[0] + .replace(/^\/(.*?)(\/)*$/, '$1') + .split('/') + : []; +}; + +exports.parseBody = (body) => { try { - return JSON.parse(body) - } catch(e) { - return body + return JSON.parse(body); + } catch (e) { + return body; } -} - - +}; // Parses auth values into known formats -const parseAuthValue = (type,value) => { +const parseAuthValue = (type, value) => { switch (type) { case 'Basic': { - let creds = Buffer.from(value, 'base64').toString().split(':') - return { type, value, username: creds[0], password: creds[1] ? creds[1] : null } + let creds = Buffer.from(value, 'base64').toString().split(':'); + return { + type, + value, + username: creds[0], + password: creds[1] ? creds[1] : null, + }; } case 'OAuth': { - let params = QS.parse(value.replace(/",\s*/g,'&').replace(/"/g,'').trim()) - return Object.assign({ type, value }, params) + let params = QS.parse( + value.replace(/",\s*/g, '&').replace(/"/g, '').trim() + ); + return Object.assign({ type, value }, params); } default: { - return { type, value } + return { type, value }; } } -} - -exports.parseAuth = authStr => { - let auth = authStr && typeof authStr === 'string' ? authStr.split(' ') : [] - return auth.length > 1 && ['Bearer','Basic','Digest','OAuth'].includes(auth[0]) ? - parseAuthValue(auth[0], auth.slice(1).join(' ').trim()) : - { type: 'none', value: null } -} +}; +exports.parseAuth = (authStr) => { + let auth = authStr && typeof authStr === 'string' ? authStr.split(' ') : []; + return auth.length > 1 && + ['Bearer', 'Basic', 'Digest', 'OAuth'].includes(auth[0]) + ? parseAuthValue(auth[0], auth.slice(1).join(' ').trim()) + : { type: 'none', value: null }; +}; +const mimeMap = require('./mimemap.js'); // MIME Map -const mimeMap = require('./mimemap.js') // MIME Map - -exports.mimeLookup = (input,custom={}) => { - let type = input.trim().replace(/^\./,'') +exports.mimeLookup = (input, custom = {}) => { + let type = input.trim().replace(/^\./, ''); // If it contains a slash, return unmodified if (/.*\/.*/.test(type)) { - return input.trim() + return input.trim(); } else { // Lookup mime type - let mime = Object.assign(mimeMap,custom)[type] - return mime ? mime : false + let mime = Object.assign(mimeMap, custom)[type]; + return mime ? mime : false; } -} - -const statusCodes = require('./statusCodes.js') // MIME Map +}; -exports.statusLookup = status => { - return status in statusCodes ? statusCodes[status] : 'Unknown' -} +const statusCodes = require('./statusCodes.js'); // MIME Map +exports.statusLookup = (status) => { + return status in statusCodes ? statusCodes[status] : 'Unknown'; +}; // Parses routes into readable array -const extractRoutes = (routes,table=[]) => { +const extractRoutes = (routes, table = []) => { // Loop through all routes for (let route in routes['ROUTES']) { // Add methods for (let method in routes['ROUTES'][route]['METHODS']) { - table.push([method,routes['ROUTES'][route]['METHODS'][method].path]) + table.push([ + method, + routes['ROUTES'][route]['METHODS'][method].path, + routes['ROUTES'][route]['METHODS'][method].stack.map((x) => + x.name.trim() !== '' ? x.name : 'unnamed' + ), + ]); } - extractRoutes(routes['ROUTES'][route],table) + extractRoutes(routes['ROUTES'][route], table); } - return table -} - -exports.extractRoutes = extractRoutes + return table; +}; +exports.extractRoutes = extractRoutes; // Generate an Etag for the supplied value -exports.generateEtag = data => - crypto.createHash('sha256').update(encodeBody(data)).digest('hex').substr(0,32) - +exports.generateEtag = (data) => + crypto + .createHash('sha256') + .update(encodeBody(data)) + .digest('hex') + .substr(0, 32); // Check if valid S3 path -exports.isS3 = path => /^s3:\/\/.+\/.+/i.test(path) - +exports.isS3 = (path) => /^s3:\/\/.+\/.+/i.test(path); // Parse S3 path -exports.parseS3 = path => { - if (!this.isS3(path)) throw new FileError('Invalid S3 path',{path}) - let s3object = path.replace(/^s3:\/\//i,'').split('/') - return { Bucket: s3object.shift(), Key: s3object.join('/') } -} - +exports.parseS3 = (path) => { + if (!this.isS3(path)) throw new FileError('Invalid S3 path', { path }); + let s3object = path.replace(/^s3:\/\//i, '').split('/'); + return { Bucket: s3object.shift(), Key: s3object.join('/') }; +}; // Deep Merge -exports.deepMerge = (a,b) => { - Object.keys(b).forEach(key => (key in a) ? - this.deepMerge(a[key],b[key]) : Object.assign(a,b) ) - return a -} +exports.deepMerge = (a, b) => { + Object.keys(b).forEach((key) => + key in a ? this.deepMerge(a[key], b[key]) : Object.assign(a, b) + ); + return a; +}; // Concatenate arrays when merging two objects -exports.mergeObjects = (obj1,obj2) => - Object.keys(Object.assign({},obj1,obj2)).reduce((acc,key) => { - if (obj1[key] && obj2[key] && obj1[key].every(e => obj2[key].includes(e))) { - return Object.assign(acc,{ [key]: obj1[key] }) +exports.mergeObjects = (obj1, obj2) => + Object.keys(Object.assign({}, obj1, obj2)).reduce((acc, key) => { + if ( + obj1[key] && + obj2[key] && + obj1[key].every((e) => obj2[key].includes(e)) + ) { + return Object.assign(acc, { [key]: obj1[key] }); } else { - return Object.assign(acc,{ - [key]: obj1[key] ? (obj2[key] ? obj1[key].concat(obj2[key]) : obj1[key]) : obj2[key] - }) + return Object.assign(acc, { + [key]: obj1[key] + ? obj2[key] + ? obj1[key].concat(obj2[key]) + : obj1[key] + : obj2[key], + }); } - },{}) + }, {}); // Concats values from an array to ',' separated string -exports.fromArray = val => - val && val instanceof Array ? val.toString() : undefined +exports.fromArray = (val) => + val && val instanceof Array ? val.toString() : undefined; // Stringify multi-value headers -exports.stringifyHeaders = headers => - Object.keys(headers) - .reduce((acc,key) => - Object.assign(acc,{ +exports.stringifyHeaders = (headers) => + Object.keys(headers).reduce( + (acc, key) => + Object.assign(acc, { // set-cookie cannot be concatenated with a comma - [key]: key === 'set-cookie' ? headers[key].slice(-1)[0] : headers[key].toString() - }) - ,{}) + [key]: + key === 'set-cookie' + ? headers[key].slice(-1)[0] + : headers[key].toString(), + }), + {} + ); diff --git a/package-lock.json b/package-lock.json index c21dac2..84c6cfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "lambda-api", - "version": "0.10.8", + "version": "0.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.10.8", + "version": "0.11.0", "license": "MIT", "devDependencies": { "@types/aws-lambda": "^8.10.51", @@ -14,7 +14,9 @@ "bluebird": "^3.7.2", "coveralls": "^3.1.0", "eslint": "^7.22.0", + "eslint-config-prettier": "^8.3.0", "jest": "^26.6.3", + "prettier": "^2.3.2", "sinon": "^4.5.0" } }, @@ -2792,6 +2794,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", + "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7065,6 +7079,18 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -11347,6 +11373,13 @@ } } }, + "eslint-config-prettier": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", + "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -14491,6 +14524,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true + }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", diff --git a/package.json b/package.json index d63459a..e2a9e33 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,9 @@ "types": "index.d.ts", "scripts": { "test": "jest unit", + "prettier": "prettier --check .", "test-cov": "jest unit --coverage", - "test-ci": "eslint . && jest unit --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", + "test-ci": "eslint . && prettier --check . && jest unit --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", "lint": "eslint .", "prepublishOnly": "npm test && npm run lint" }, @@ -39,7 +40,9 @@ "bluebird": "^3.7.2", "coveralls": "^3.1.0", "eslint": "^7.22.0", + "eslint-config-prettier": "^8.3.0", "jest": "^26.6.3", + "prettier": "^2.3.2", "sinon": "^4.5.0" }, "files": [