New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

take-five

Package Overview
Dependencies
Maintainers
5
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

take-five - npm Package Compare versions

Comparing version 1.4.1 to 2.0.0

.nyc_output/2201fc75c7fdee2d8849e8e2a543f36b.json

13

package.json
{
"name": "take-five",
"version": "1.4.1",
"version": "2.0.0",
"description": "Very minimal JSON-REST server",

@@ -30,10 +30,13 @@ "main": "take-five.js",

"dependencies": {
"fast-safe-stringify": "^1.0.9",
"concat-stream": "^1.6.2",
"fast-safe-stringify": "^2.0.3",
"wayfarer": "^6.1.5"
},
"devDependencies": {
"dependency-check": "^2.6.0",
"standard": "^10.0.2",
"tap": "^10.5.1"
"dependency-check": "^3.1.0",
"nanorequest": "^1.1.0",
"selfsigned": "^1.10.2",
"standard": "^11.0.1",
"tap": "^11.1.3"
}
}
# take-five
[![Greenkeeper badge](https://badges.greenkeeper.io/scriptoLLC/take-five.svg)](https://greenkeeper.io/)
[![Build Status](https://travis-ci.org/scriptoLLC/take-five.svg?branch=master)](https://travis-ci.org/scriptoLLC/take-five) [![NSP Status](https://nodesecurity.io/orgs/scriptollc/projects/24857fc4-2472-446e-ac2d-5a0f5913503d/badge)](https://nodesecurity.io/orgs/scriptollc/projects/24857fc4-2472-446e-ac2d-5a0f5913503d) [![Coverage Status](https://coveralls.io/repos/github/scriptoLLC/take-five/badge.svg?branch=master)](https://coveralls.io/github/scriptoLLC/take-five?branch=master)

@@ -18,7 +17,7 @@

```js
const five = require('take-five')
const server = five()
server.get('/', (req, res) => res.send('Hello, world'))
server.post('/', (req, res) => res.send(req.body))
server.listen(3000)
const Five = require('take-five')
const five = new Five()
five.get('/', async (req, res, ctx) => ctx.send('Hello, world'))
five.post('/', async (req, res, ctx) => ctx.send(req.body))
five.listen(3000)
```

@@ -33,30 +32,131 @@

## Routing and route-handlers
In lieu of pre-set middleware, routes handlers can be arrays of functions that will
be iterated over asynchronously. To simplify handling of these handlers,
it is expected that the handlers will return [thenables](https://promisesaplus.com/), or terminate the response
stream. This means any promises library should work (including the built in one),
as well as using the `async` function keyword.
**If you do not return a [thenable](https://promisesaplus.com/), handler processing will stop in that function**
e.g.:
```js
function badSetContentHeader (req, res, ctx) {
res.setHeader('x-no-way', 'this is gonna do nothing')
}
function goodSetContentHeader (req, res, ctx) {
return new Promise((resolve) => {
res.setHeader('x-yes-way', 'this is gonna do everything!')
resolve()
})
}
function sendReply (req, res, ctx) {
ctx.send('beep!')
}
five.get('/nope', [badSetContentHeader, sendReply])
five.get('/yup', [goodSetContentHeader, sendReply)
```
since `badSetContentHeader` doesn't return a [`thenable`](https://promisesaplus.com/), take-five will not
know that it needs to call the `sendReply` function in the handler list for the `/nope`
route.
If you have either closed the response stream, or `reject`ed the [thenable](https://promisesaplus.com/) returned
from your route handler, the next route will not be called. In the case that you have
`reject`ed the [thenable](https://promisesaplus.com/), the error handler will be invoked as well. If you have
closed the response stream, the server assumes you were done processing the request
and will just ignore the remaning functions in the queue.
By default, `get`, `post`, `put`, `delete`, `options` and `patch` will be available
for routing, but this can be changed by providing an array of methods on the options
hash when instatiating a new TakeFive prototype.
### Examples
#### Using async/await
```js
five.handleError = (err, req, res, ctx) => {
ctx.err(err.statusCode, err.message)
}
five.get('/:secretdata', [
async (req, res, ctx) => {
try {
const session = await isAuthorized(req.headers.Authorization)
ctx.session = session
} catch (err) {
err.statusCode = 401
throw err
}
},
async (res, res, ctx) => {
try {
const data = await getResource(ctx.params.secretdata, ctx.session)
ctx.send(data)
} catch (err) {
err.statusCode = 500
reject(err)
}
}
])
```
#### Using a "then"-able
```js
five.get('/:secretdata', [
(req, res, ctx) => {
return new Promise((resolve, reject) => {
isAuthorized(req.headers.Authorization, (err, session) => {
if (err) {
ctx.err(401, 'Not Authorized')
return reject(new Error('Not authorized'))
}
ctx.session = session
resolve()
})
})
},
(res, res, ctx) => {
return new Promise((resolve, reject) => {
getResource(ctx.params.secretdata, ctx.session, (err, data) => {
if (err) {
ctx.err(500, 'Server error')
return reject(new Error('server error'))
}
ctx.send(data)
resolve()
})
})
}
])
```
## API
### `five(opts?:object):object`
### `Five(opts?:object):object`
Create and return a new HTTP server object.
* `opts.maxPost?:number`: the max size for a payload. Default: 512kb
* `opts.cors?:object`
* `opts.cors.headers?:array(string)`: an array of headers to accept besides the default. Default: `Content-Type`, `Accept`, `X-Requested-With`
* `opts.cors.origin?:string`: What origin(s) are accepted. Deafult: `*`
* `opts.cors.credentials?:boolean`: Allow or deny credentials. Default: `true`
* `opts.cores.methods?array(string)`: an array of methods to accept besides the default. Default: `PUT`, `POST`, `DELETE`, `GET`, `OPTIONS`
* `opts.allowHeaders?:array(string)`: an array of headers to accept besides the default. Default: `Content-Type`, `Accept`, `X-Requested-With`
* `opts.allowOrigin?:string`: What origin(s) are accepted. Deafult: `*`
* `opts.allowCredentials?:boolean`: Allow or deny credentials. Default: `true`
* `opts.allowContentTypes?:string|string[]`: What content types are allowed to be used when sending data to the server. Default: `['application/json']`. Note: This is additive, so `application/json` will ALWAYS be allowed.
* `opts.allowMethods?array(string)`: an array of methods to accept besides the default. Default: `PUT`, `POST`, `DELETE`, `GET`, `OPTIONS`, `PATCH`
* `opts.methods?array(string)`: An array of methods to create route handlers for. Default: `PUT`, `POST`, `DELETE`, `GET`, `OPTIONS`, `PATCH`
* `opts.http?object`: options for `http(s).createServer`. If you supply `key`,
`cert` and `ca` as options, it will assume you are trying to create an https server`
### `Five#use(middleware:function)`
Add a new middleware to the stack. Middleware will be processed in the order in
which they are added, which means they will be run after the built-in middleware.
`Access-Control-Allow-Headers` and `Access-Control-Allow-Methods` can also be changed during runtime
by setting `allowHeaders` and `allowMethods` respectively.
* `middleware(request:object, response:object, next:function):function` -You must either call `next` or send data to the client when you are finshed.
Since the middleware signature is the same express/restify, you *might* be able to use existing middleware with take-five, but ymmv.
### `Five#router(namespace:string):object`
Namespace routes. All routes defined off this router will be prefixed with the supplied
namespace. The methods have the same signature as the router provided.
### `Five#get(route:string, handler:(function|array(function)))`
### `Five#post(route:string, handler:(function|array(function)))`
### `Five#put(route:string, handler:(function|array(function)))`
### `Five#delete(route:string, handler:(function|array(function)))`
#### `Five#get(route:string, handler:(function|array(function)), routeOpts?:object)`
#### `Five#post(route:string, handler:(function|array(function)), routeOpts?:object)`
#### `Five#put(route:string, handler:(function|array(function)), routeOpts?:object)`
#### `Five#patch(route:string, handler:(function|array(function)), routeOpts?:object)`
#### `Five#delete(route:string, handler:(function|array(function)), routeOpts?:object)`
#### `Five#options(route:string, handler:(function|array(function)), routeOpts?:object)`
Add a new route to the server. Routes may be added after the server has been

@@ -67,35 +167,68 @@ started. You can supply either a single function or an array of functions to call.

* `route:string` A [wayfarer](https://github.com/yoshuawuyts/wayfarer) route definition.
* `handler(request:object, response:object, next:function):function`: The handler for this route.
* `handler(request:object, response:object, ctx:object):function`: The handler for this route.
* `routeOpts?:object` - overrides for this specific chain of handlers
* `maxPost:number` - set the maximum size of a payload for this set of handlers
* `allowedContentTypes:string|string[]` - add new allowable content-types for this set of handlers
Since this is an augmented instance of [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
all methods and properties are available on this as well.
#### `ctx:object`
* `query?:object`: query parameters from the URL (if any)
* `params?:object`: Named parameters from the route definition as provided by wayfarer
* `body?:object`: The parsed JSON body available on POST requests
* `send(statusCode?:number, body?:(string|object)):function`: Send a response.
* `err(statusCode?:number, message?:string):function`: Send an error. If no status code is provided it will default to a 500 error. If no message is provided, it will use the default message for that status code. The message will be wrapped in a JSON object under the key `message`
* `next():function`: Go to the next handler
### `request:object`
The [`http.IncomingMessage`](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
object extended with:
The `ctx` object can also be extended to contain user defined objects, through
setting `this.ctx` to an object. The object will be copied over using `Object.assign`.
* `params?:object`: query parameters from the URL (if any)
* `urlParams?:object`: Named parameters from the route definition as provided by wayfarer
* `body?:object`: The parsed JSON body available on POST requests
The keys from above will overwrite any keys you provide named the same.
### `response:object`
The [`http.ServerResponse`](https://nodejs.org/api/http.html#http_class_http_serverresponse)
object augmented with two additional methods. The defaults for sending messages are
`content-type: application/json` and `statusCode: 200`. The header may be overridden by
calling `res.header`. The statusCode can be provided when calling the `send` method.
#### `Five#handleError(err:Error, req:Request, res:Response, ctx:Context)`
This function is invoked when either an error object is passed to the `ctx.next`
method, or the `then`-able function's `reject` handler is invoked.
* `send(statusCode?:number, body?:(string|object)):function`: Send a response.
* `err(statusCode?:number, message?:string):function`: Send an error. If no status code is provided it will default to a 500 error. If no message is provided, it will use the default message for that status code. The message will be wrapped in a JSON object under the key `message`
This is a no-op by default, allowing you to customize it's behavior.
### `next:function`
If you are done processing the request, but you want a later handler to be able to modify the response, call next. This will invoke the next handler in the stack. If there are no more handlers left, it will call `res.end()` and send the response as is. If you want to immediately send the response, you can call `res.end`, `res.send` or `res.err` directly.
#### `Five#listen(port:number, handle?:function)`
Start listening for requests and call the optional callback when you've started
listening
#### `Five.addParser(type:string, parser:functon):void`
Add a new content parser to the parsers list. By default there is only a single
parser (`application/json`: JSON.parser). This can be overridden with a custom
JSON parser if you'd like.
#### `Five#close()`
Shutdown the underlying server
### Getters/Setters
#### `Five.server`
The underlying http(s) server from node can be accessed directly. This is non-writeable
#### `Five.maxPost`
Globally control the maximum payload size after creation
#### `Five.allowContentTypes`
Add new allowable content types for clients to send data with. You can use either
an array of strings or a string
#### `Five.allowHeaders`
Set a new allowable header or headers for CORS requests. You can use either an
array of strings or a string.
#### `Five.allowMethods`
Set a new allowable method for CORS requests.
#### `Five.ctx`
Add new keys to the ctx objects
## Do we need another REST server?
Probably not, but [`restify`](http://restify.com), [`hapi`](http://hapijs.com) and [`express`](http://expressjs.com) are all over-kill on the types of services I'm building for the most part.
* Setting up CORS is difficult or laborious: most REST services need to support CORS, this should be enabled by default (and easily configurable)
* It has no need to accept anything other than `application/json` payloads, and doesn't need the cruft associated with other payload types.
* By default it woill respond with `application/json` as well, but allow it be override-able if needed
* It has no need to accept anything other than `application/json` payloads, but you can easily extend it to
* By default it will respond with `application/json` as well, but allow it be override-able if needed
* Should be trivial to reject large payloads to prevent DOS attacks
* Each route should have the ability to have multiple placeholders, regardless of the payload type
* It shouldn't mutate the built-in request and response objects
* It should be as fast as possible

@@ -106,11 +239,3 @@

There are some scripts used for the (extremely reductive) benchmarking in `/benchmark`. Using my Core-i7, I get the following data using `wrk -t12 -c400 -d30s http://localhost:3000/test`. You might see different results. As with all benchmarks, these are likely indicative of nothing!
```
take-five: Requests/sec: 20296.63
express: Requests/sec: 10974.18
restify: Requests/sec: 9201.09
```
## License
Copyright © 2016 Scripto LLC, Todd Kennedy. Reuse permitted under the Apache-2.0 license
Copyright © 2018 Scripto LLC, Todd Kennedy. Reuse permitted under the Apache-2.0 license

@@ -1,64 +0,301 @@

const path = require('path')
const http = require('http')
const https = require('https')
const querystring = require('querystring')
const Buffer = require('buffer').Buffer
const stringify = require('fast-safe-stringify')
const wayfarer = require('wayfarer')
const concat = require('concat-stream')
const handleRequest = require('./lib/handle-request')
const cors = require('./lib/cors')
const parseBody = require('./lib/parse-body')
const restrictPost = require('./lib/restrict-post')
const makeRes = require('./lib/make-res')
const methods = ['get', 'put', 'post', 'delete']
const methods = ['get', 'put', 'post', 'delete', 'patch']
const dataMethods = ['put', 'post', 'patch']
module.exports = function (opts) {
opts = opts || {}
opts.maxPost = opts.maxPost || 512 * 1024
const routers = new Map()
const middleware = []
const server = http.createServer()
methods.forEach(m => {
server[m] = (matcher, func) => addRoute(m, matcher, func)
})
const MAX_POST = 512 * 1024
const ORIGIN = '*'
const CREDENTIALS = true
const ALLOWED_TYPES = ['application/json']
const HEADERS = ['Content-Type', 'Accept', 'X-Requested-With']
server.use = (funcs) => {
if (!Array.isArray(funcs)) {
funcs = [funcs]
class TakeFive {
constructor (opts) {
this._allowMethods = ['options'].concat(methods)
this._allowHeaders = HEADERS.slice(0)
this._allowContentTypes = ALLOWED_TYPES.slice(0)
this._parsers = {
'application/json': JSON.parse
}
middleware.push.apply(middleware, funcs)
opts = opts || {}
this.maxPost = opts.maxPost || MAX_POST
this.allowedContentTypes = opts.allowContentTypes
this.allowOrigin = opts.allowOrigin || ORIGIN
this.allowCredentials = CREDENTIALS
if (typeof opts.allowCredentials === 'boolean') {
this.allowCredentials = opts.allowCredentials
}
if (opts.allowHeaders) {
this.allowHeaders = opts.allowHeaders
}
if (opts.allowMethods) {
this.allowMethods = opts.allowMethods
}
this._httpLib = http
this._httpOpts = opts.http || {}
this._ctx = {}
if (this._httpOpts.key && this._httpOpts.cert) {
this._httpLib = https
}
this.routers = new Map()
const args = []
if (Object.keys(this._httpOpts).length > 0) {
args.push(this._httpOpts)
}
args.push(this._onRequest.bind(this))
this.server = this._httpLib.createServer.apply(this._httpLib, args)
this.methods = methods.concat(opts.methods || [])
this._addRouters()
}
server.router = (ns) => {
const router = {}
methods.forEach(m => {
router[m] = (matcher, func) => {
const nsMatcher = path.posix.join(ns, matcher)
addRoute(m, nsMatcher, func)
set allowContentTypes (types) {
if (!Array.isArray(types)) {
types = [types]
}
this._allowContentTypes = this._allowContentTypes.concat(types)
}
get allowContentTypes () {
return this._allowContentTypes
}
addParser (type, func) {
if (typeof type === 'string' && typeof func === 'function') {
this._parsers[type] = func
}
}
set allowHeaders (headers) {
headers = Array.isArray(headers) ? headers : [headers]
this._allowHeaders = this._allowHeaders.concat(headers)
}
get allowHeaders () {
return this._allowHeaders
}
set allowMethods (methods) {
methods = Array.isArray(methods) ? methods : [methods]
this._allowMethods = this._allowMethods.concat(methods)
}
get allowMethods () {
return this._allowMethods
}
set ctx (ctx) {
const ctxType = Object.prototype.toString.call(ctx)
if (ctxType !== '[object Object]') {
throw new Error(`ctx must be an object, was ${ctxType}`)
}
this._ctx = Object.assign({}, ctx)
}
get ctx () {
return this._ctx
}
parseBody (data, type) {
const parser = this._parsers[type]
if (typeof parser === 'function') {
return parser(data)
}
return data
}
makeCtx (res) {
function send (code, content) {
if (typeof content === 'undefined') {
content = code
code = 200
}
if (typeof content !== 'string') {
content = stringify(content)
}
res.statusCode = code
res.setHeader('content-type', 'application/json')
res.end(content, 'utf8')
}
function err (code, content) {
if (typeof content === 'undefined') {
if (parseInt(code, 10)) {
content = http.STATUS_CODES[code]
} else {
content = code
code = 500
}
}
res.statusCode = code
res.statusMessage = content
res.setHeader('content-type', 'application/json')
res.end(stringify({message: content}))
}
return Object.assign({}, this.ctx, {send, err})
}
_handleError (err, req, res, ctx) {
if (typeof this.handleError === 'function') {
this.handleError(err, req, res, ctx)
}
if (!res.finished) {
ctx.err('Internal server error')
}
}
cors (res) {
res.setHeader('Access-Control-Allow-Origin', this.allowOrigin)
res.setHeader('Access-Control-Allow-Headers', this.allowHeaders.join(','))
res.setHeader('Access-Control-Allow-Credentials', this.allowCredentials)
res.setHeader('Access-Control-Allow-Methods', this.allowMethods.join(',').toUpperCase())
}
listen (port, func) {
this.server.listen(port, func)
}
close () {
this.server.close()
}
_verifyBody (req, res, ctx) {
return new Promise((resolve) => {
const type = req.headers['content-type']
const size = req.headers['content-length']
const _ctxMax = parseInt(ctx.maxPost, 10)
const maxPost = Number.isNaN(_ctxMax) ? this.maxPost : _ctxMax
let allowContentTypes = this.allowContentTypes.slice(0)
if (ctx.allowContentTypes) {
allowContentTypes = allowContentTypes.concat(ctx.allowContentTypes)
}
if (size > maxPost) {
return ctx.err(413, `Payload size exceeds maximum size for requests`)
}
if (!allowContentTypes.includes(type)) {
return ctx.err(415, `Expected data to be of ${allowContentTypes.join(', ')} not ${type}`)
} else {
const parser = concat((data) => {
try {
ctx.body = this.parseBody(data.toString('utf8'), type)
} catch (err) {
return ctx.err(400, `Payload is not valid ${type}`)
}
resolve()
})
req.pipe(parser)
const body = []
req.on('data', (chunk) => {
body.push(chunk.toString('utf8'))
if (chunk.length > this.maxPost || Buffer.byteLength(body.join(''), 'utf8') > this.maxPost) {
req.pause()
return ctx.err(413, 'Payload size exceeds maximum body length')
}
})
}
})
return router
}
server.use(cors(opts.cors || {}))
server.use(restrictPost(opts.maxPost))
server.use(parseBody(opts.maxPost))
server.on('request', (req, res) => handleRequest(req, makeRes(res), middleware, routers))
_onRequest (req, res) {
this.cors(res)
return server
function addRoute (method, matcher, funcs) {
if (!routers.has(method)) {
routers.set(method, wayfarer('/_'))
if (req.method === 'OPTIONS') {
res.statusCode = 204
return res.end()
}
if (!Array.isArray(funcs)) {
funcs = [funcs]
const ctx = this.makeCtx(res)
try {
const method = req.method.toLowerCase()
const url = req.url.split('?')[0]
const router = this.routers.get(method)
router(url, req, res, ctx)
} catch (err) {
if (res.finished) {
throw err
}
return ctx.err(404, 'Not found')
}
}
routers.get(method).on(matcher, (params, req, res) => {
req.params = querystring.parse(req.url.split('?')[1])
req.urlParams = params
handleRequest(req, res, funcs)
_addRouters () {
this.methods.forEach((method) => {
Object.defineProperty(this, method, {value: generateRouter(method)})
})
function generateRouter (method) {
return function (matcher, handler, ctxOpts) {
let router = this.routers.get(method)
if (!router) {
router = wayfarer('/_')
this.routers.set(method, router)
}
const handlers = Array.isArray(handler) ? handler : [handler]
if (handlers.some((f) => typeof f !== 'function')) {
throw new Error('handlers must be functions')
}
router.on(matcher, (params, req, res, ctx) => {
const routeHandlers = handlers.slice(0)
const conlen = parseInt(req.headers['content-length'], 10) || 0
if (conlen !== 0 && dataMethods.includes(req.method.toLowerCase())) {
if (ctxOpts) ctx = Object.assign({}, ctx, ctxOpts)
routeHandlers.unshift(this._verifyBody.bind(this))
}
ctx.query = querystring.parse(req.url.split('?')[1])
ctx.params = params
this._resolveHandlers(req, res, ctx, routeHandlers)
})
}
}
}
_resolveHandlers (req, res, ctx, handlers) {
const iterate = (handler) => {
const p = handler(req, res, ctx)
if (p && typeof p.then === 'function') {
p.then(() => {
if (!res.finished && handlers.length > 0) {
const next = handlers.shift()
iterate(next)
}
})
.catch((err) => {
this._handleError(err, req, res, ctx)
})
}
}
const next = handlers.shift()
iterate(next)
}
}
module.exports = TakeFive
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc