Comparing version 0.10.0-rc4 to 0.10.0-rc5
@@ -29,3 +29,6 @@ const util = require('util'); | ||
function parseErr(err){ | ||
function parseErr({ err }){ | ||
if(!err) | ||
return {}; | ||
if(!(err instanceof Error)) | ||
@@ -46,3 +49,5 @@ err = new Error(err); | ||
function parseReq(req){ | ||
function parseReq({ req }){ | ||
if(!req) | ||
return {}; | ||
return { | ||
@@ -59,5 +64,6 @@ req: null, | ||
function parseRes(res){ | ||
function parseRes({ res }){ | ||
if(!res) | ||
return {}; | ||
let req = res.req; | ||
return { | ||
@@ -71,20 +77,16 @@ res: null, | ||
msg: 'Sent ' + res.statusCode + ' response to ' + req.method + ' ' + req.url | ||
} | ||
}; | ||
} | ||
function parseWs(ws){ | ||
function parseWs({ ws }){ | ||
if(!ws) | ||
return {}; | ||
let client = ws._socket ? ws._socket.address().address : ws.addr; | ||
return { | ||
ws: null, | ||
type: 'websocket', | ||
client | ||
} | ||
return { ws: null, type: 'websocket', client }; | ||
} | ||
function getEntry(level, ...args){ | ||
let time = new Date(); | ||
let data = typeof args[0] == 'object' ? args.shift() : {}; | ||
let msg = util.format(...args); | ||
let type = data.type || 'event'; | ||
// TODO HOST NAME? | ||
@@ -94,19 +96,10 @@ /* istanbul ignore next */ | ||
if(data.err) | ||
Object.assign(data, parseErr(data.err)); | ||
if(data.res) | ||
Object.assign(data, parseRes(data.res)); | ||
if(data.ws) | ||
Object.assign(data, parseWs(data.ws)); | ||
if(data.req) | ||
Object.assign(data, parseReq(data.req)); | ||
Object.assign(data, parseErr(data), parseRes(data), parseWs(data), | ||
parseReq(data)); | ||
msg = msg || data.msg; | ||
return { level, type, ...data, msg, pid, time }; | ||
return { level, type, ...data, msg, pid, time: new Date() }; | ||
} | ||
function log(level, ...args){ | ||
// TODO parse app | ||
let entry = getEntry(level, ...args); | ||
@@ -113,0 +106,0 @@ entry.app = this._app._name; |
113
lib/main.js
@@ -5,8 +5,8 @@ const | ||
http = require('http'), | ||
https = require('https'), | ||
assert = require('assert'), | ||
https = require('https'), | ||
Confort = require('confort'), | ||
{ METHODS } = require('http'), | ||
compression = require('compression'), | ||
cookieParser = require('cookie-parser'), | ||
Confort = require('confort'), | ||
{ METHODS } = require('http'); | ||
cookieParser = require('cookie-parser'); | ||
@@ -17,2 +17,8 @@ const Logger = require('./logger'); | ||
const SHORT_TYPES = { | ||
form: 'multipart/form-data', | ||
urlencoded: 'application/x-www-form-urlencoded', | ||
json: 'application/json' | ||
}; | ||
const noop = function(){}; | ||
@@ -31,22 +37,40 @@ noop.noop = true; | ||
module.exports = class Nodecaf { | ||
function validateOpts(opts){ | ||
assert(typeof opts == 'object', | ||
new TypeError('Options argument must be an object')); | ||
constructor(opts = {}){ | ||
this._api = opts.api || noop; | ||
this._startup = opts.startup || noop; | ||
this._shutdown = opts.shutdown || noop; | ||
assert(typeof opts == 'object', | ||
new TypeError('Options argument must be an object')); | ||
assert(typeof this._api == 'function', | ||
new TypeError('API builder must be a function')); | ||
this._api = opts.api || noop; | ||
this._startup = opts.startup || noop; | ||
this._shutdown = opts.shutdown || noop; | ||
assert(typeof this._startup == 'function', | ||
new TypeError('Startup handler must be a function')); | ||
assert(typeof this._api == 'function', | ||
new TypeError('API builder must be a function')); | ||
assert(typeof this._shutdown == 'function', | ||
new TypeError('Shutdown handler must be a function')); | ||
} | ||
assert(typeof this._startup == 'function', | ||
new TypeError('Startup handler must be a function')); | ||
function getRouterProxy(router){ | ||
assert(typeof this._shutdown == 'function', | ||
new TypeError('Shutdown handler must be a function')); | ||
// Generate HTTP verb shortcut route methods | ||
let proxy = METHODS.reduce( (o, m) => | ||
({ ...o, [m.toLowerCase()]: router.addRoute.bind(router, m.toLowerCase()) }), {}); | ||
// Needed because it's not possible to call a function called 'delete' | ||
proxy.del = router.addRoute.bind(router, 'delete'); | ||
proxy.all = (...chain) => | ||
METHODS.forEach(m => router.addRoute(m.toLowerCase(), '/:path*', ...chain)); | ||
return proxy; | ||
} | ||
module.exports = class Nodecaf { | ||
constructor(opts = {}){ | ||
validateOpts.apply(this, [ opts ]); | ||
// TODO sha1 of host+time+name to identify app | ||
@@ -62,10 +86,4 @@ | ||
this._alwaysRebuildAPI = opts.alwaysRebuildAPI || false; | ||
// Generate HTTP verb shortcut route methods | ||
this._routeProxy = METHODS.reduce( (o, m) => | ||
({ ...o, [m.toLowerCase()]: this._router.addRoute.bind(this._router, m.toLowerCase()) }), {}); | ||
// Needed because it's not possible to call a function called 'delete' | ||
this._routeProxy.del = this._router.addRoute.bind(this._router, 'delete'); | ||
this._serverOpts = opts.server || {}; | ||
this._routeProxy = getRouterProxy(this._router); | ||
this._routeProxy.ws = this._wsRouter.set.bind(this._wsRouter); | ||
@@ -102,2 +120,11 @@ | ||
accept(types){ | ||
types = [].concat(types).map(t => SHORT_TYPES[t] || t); | ||
return ({ body, req, res, next }) => { | ||
if(typeof body !== 'undefined' && !types.includes(req.contentType)) | ||
return res.status(415).end(); | ||
next(); | ||
} | ||
} | ||
async start(){ | ||
@@ -127,4 +154,4 @@ if(this.running) | ||
this._server = this._ssl | ||
? https.createServer(this._ssl, handler) | ||
: http.createServer(handler); | ||
? https.createServer({ ...this._serverOpts, ...this._ssl }, handler) | ||
: http.createServer(this._serverOpts, handler); | ||
@@ -174,33 +201,1 @@ this._wsRouter.start(); | ||
} | ||
/* CHANGES SO FAR | ||
- log class -> type | ||
- Log name -> app | ||
- Removed Express | ||
- Nice Exceptions | ||
- Logger Auto-parse err, app, res, req (TODO) | ||
- UpTime entrypoint (TODO) | ||
- Health entrypoint (TODO) | ||
- Removed 'port' attr | ||
- Change main class to 'Nodecaf' | ||
- 4xx errors won't spit default body anymore | ||
- expose() to app.global | ||
- PID 1 omitted from log entries | ||
- Added res.type() | ||
- Added res.text() | ||
- Entire assertions usage | ||
- error() -> res.error() | ||
- res log entry now has method | ||
- form-data file interface | ||
- removed accept() and filters (TMP) | ||
- removed user error handler (TMP) | ||
- app.api() to new App(api, ...) | ||
- startup and shutdown | ||
- moved cookieSecret to conf | ||
- constructor argument to OPTS spec | ||
- remove programmatic name and version | ||
- Added WS cookies | ||
- option to disable log completely | ||
- log entry for error on parsing body | ||
*/ |
@@ -9,15 +9,22 @@ const querystring = require('querystring'); | ||
function parseContentType(req){ | ||
try{ | ||
var ct = contentType.parse(req.headers['content-type']); | ||
} | ||
catch(err){ | ||
ct = FALLBACK_CONTENT_TYPE; | ||
} | ||
ct.textCharset = ct.parameters.charset || 'utf-8'; | ||
ct.originalCharset = ct.parameters.charset; | ||
req.contentType = ct.type; | ||
return ct; | ||
} | ||
module.exports = { | ||
async parseBody(req){ | ||
try{ | ||
var ct = contentType.parse(req.headers['content-type']); | ||
} | ||
catch(err){ | ||
ct = FALLBACK_CONTENT_TYPE; | ||
} | ||
let ct = parseContentType(req); | ||
let textCharset = ct.parameters.charset || 'utf-8'; | ||
let originalCharset = ct.parameters.charset; | ||
try{ | ||
@@ -33,9 +40,9 @@ | ||
if(ct.type.slice(-4) == 'json') | ||
return JSON.parse(req.rawBody.toString(textCharset)); | ||
return JSON.parse(req.rawBody.toString(ct.textCharset)); | ||
if(ct.type == 'application/x-www-form-urlencoded') | ||
return querystring.parse(req.rawBody.toString(textCharset)); | ||
return querystring.parse(req.rawBody.toString(ct.textCharset)); | ||
if(originalCharset || ct.type.slice(4) == 'text') | ||
return req.rawBody.toString(textCharset); | ||
if(ct.originalCharset || ct.type.slice(4) == 'text') | ||
return req.rawBody.toString(ct.textCharset); | ||
@@ -42,0 +49,0 @@ return req.rawBody; |
@@ -0,1 +1,2 @@ | ||
const assert = require('assert'); | ||
const querystring = require('querystring'); | ||
@@ -11,2 +12,39 @@ const { pathToRegexp } = require('path-to-regexp'); | ||
function findDynamicRouteMatch(req, pool = []){ | ||
for(let route of pool){ | ||
let match = route.regexp.exec(req.path); | ||
if(match){ | ||
route.params.forEach( (p, i) => req.params[p.name] = match[i + 1]); | ||
return route.handler; | ||
} | ||
} | ||
} | ||
const normalizePath = p => (p.slice(-1) == '/' ? p.slice(0, -1) : p) || '/'; | ||
function buildStack(chain){ | ||
let nextHandler = null; | ||
return chain.reverse().map(h => { | ||
assert(typeof h == 'function', | ||
new TypeError('Invalid route option \'' + typeof h + '\'')); | ||
h = normalizeHandler(h.bind(this.app)); | ||
h.next = nextHandler; | ||
return nextHandler = h; | ||
}).reverse(); | ||
} | ||
function routeRequest(router, req){ | ||
let url = new URL(req.url, 'http://0.0.0.0'); | ||
req.path = normalizePath(url.pathname); | ||
router.app.log.debug({ req }); | ||
req.params = {}; | ||
req.flash = {}; | ||
req.query = querystring.parse(url.search.slice(1)); | ||
let route = req.method + ' ' + req.path; | ||
return router.static[route] || | ||
findDynamicRouteMatch(req, router.dynamic[req.method]); | ||
} | ||
module.exports = class Router { | ||
@@ -25,39 +63,27 @@ | ||
addRoute(method, path, ...opts){ | ||
addRoute(method, path, ...chain){ | ||
let stack = []; | ||
let m = method.toUpperCase(); | ||
let route = m + ' ' + path; | ||
if(!this.app._alwaysRebuildAPI && route in this.routes) | ||
throw new Error('Route for \'' + route + '\' is already defined'); | ||
let dup = !this.app._alwaysRebuildAPI && route in this.routes; | ||
assert(!dup, new Error('Route for \'' + route + '\' is already defined')); | ||
for(let o of opts) | ||
if(typeof o == 'function'){ | ||
let h = normalizeHandler(o.bind(this.app)); | ||
stack.length > 0 && (stack[stack.length - 1].next = h); | ||
stack.push(h); | ||
} | ||
else | ||
throw new TypeError('Invalid route option \'' + typeof o + '\''); | ||
assert(chain.length > 0, new Error('Route is empty at \'' + path + '\'')); | ||
let stack = buildStack.apply(this, [ chain ]); | ||
if(stack.length == 0) | ||
throw new Error('Route is empty at \'' + path + '\''); | ||
stack.slice(-1)[0].tail = true; | ||
if(path.indexOf(':') >= 0){ | ||
let params = []; | ||
this.routes[route] = true; | ||
this.dynamic[m] = this.dynamic[m] || []; | ||
this.dynamic[m].push({ | ||
regexp: pathToRegexp(path, params, PRG_OPTS), | ||
handler: stack[0], | ||
params | ||
}); | ||
} | ||
else | ||
this.static[route] = stack[0]; | ||
if(!/[:?+*]/.test(path)) | ||
return this.static[route] = stack[0]; | ||
this.routes[route] = true; | ||
let params = []; | ||
this.dynamic[m] = this.dynamic[m] || []; | ||
this.dynamic[m].push({ | ||
regexp: pathToRegexp(path, params, PRG_OPTS), | ||
handler: stack[0], | ||
params | ||
}); | ||
} | ||
@@ -67,37 +93,12 @@ | ||
try{ | ||
let url = new URL(req.url, 'http://0.0.0.0'); | ||
let reqPath = (url.pathname.slice(-1) == '/' | ||
? url.pathname.slice(0, -1) : url.pathname) || '/'; | ||
res.req = req; | ||
//this.app.log.debug({ req }); | ||
res.on('finish', () => this.app.log.debug({ res })); | ||
let route = req.method + ' ' + reqPath; | ||
Object.assign(res, resMethods); | ||
req.params = {}; | ||
req.flash = {}; | ||
req.query = querystring.parse(url.search.slice(1)); | ||
let handler = routeRequest(this, req); | ||
let handler = false; | ||
if(req.method == 'OPTIONS') | ||
return handleCORS(this.app, req, res); | ||
if(route in this.static) | ||
handler = this.static[route]; | ||
else if(req.method in this.dynamic) | ||
for(let route of this.dynamic[req.method]){ | ||
let match = route.regexp.exec(reqPath); | ||
if(match){ | ||
route.params.forEach( (p, i) => req.params[p.name] = match[i + 1]); | ||
handler = route.handler; | ||
break; | ||
} | ||
} | ||
if(!handler) | ||
@@ -104,0 +105,0 @@ return res.status(404).end(); |
{ | ||
"name": "nodecaf", | ||
"version": "0.10.0-rc4", | ||
"version": "0.10.0-rc5", | ||
"description": "Nodecaf is a framework on top of Express for building RESTful services in a quick and convenient manner.", | ||
@@ -41,3 +41,3 @@ "main": "lib/main.js", | ||
"cookie": "^0.4.1", | ||
"cookie-parser": "^1.4.4", | ||
"cookie-parser": "^1.4.5", | ||
"cookie-signature": "^1.1.0", | ||
@@ -48,3 +48,3 @@ "cors": "^2.8.5", | ||
"stdout-stream": "^1.4.1", | ||
"ws": "^7.2.1" | ||
"ws": "^7.3.1" | ||
}, | ||
@@ -51,0 +51,0 @@ "devDependencies": { |
184
README.md
@@ -5,5 +5,5 @@ # [Nodecaf](https://gitlab.com/GCSBOSS/nodecaf) | ||
Nodecaf is an Express framework for developing REST APIs in a quick and | ||
convenient manner. | ||
Nodecaf is a light framework for developing RESTful Apps in a quick and convenient manner. | ||
Using Nodecaf you'll get: | ||
- Familiar middleware style routes declaration | ||
- Useful [handler arguments](#handlers-args). | ||
@@ -15,3 +15,3 @@ - Built-in [settings file support](#settings-file) with layering. | ||
responses. | ||
- Built-in [assertions for most common REST scenarios](#rest-assertions). | ||
- Built-in [assertions for readable RESTful error handling](#rest-assertions). | ||
- Function to [expose global objects](#expose-globals) to all routes (eg.: | ||
@@ -27,6 +27,2 @@ database connections). | ||
> If you are unfamiliar with Express, checkout | ||
> [their routing docs](https://expressjs.com/en/starter/basic-routing.html) | ||
> so that you can better grasp Nodecaf features and whatnot. | ||
## Get Started | ||
@@ -41,33 +37,21 @@ | ||
```js | ||
const { AppServer } = require('nodecaf'); | ||
const Nodecaf = require('nodecaf'); | ||
const api = require('./api'); | ||
module.exports = function init(){ | ||
let app = new AppServer(); | ||
module.exports = () => new Nodecaf({ | ||
// Expose things to all routes putting them in the 'shared' object. | ||
let shared = {}; | ||
app.expose(shared); | ||
// Load your routes and API definitions. | ||
api, | ||
// You can intercept all error that escape the route handlers. | ||
app.onRuoteError = function(input, err, send){ | ||
// Any error that is not handled here will just become a harmless 500. | ||
}; | ||
// Perform your server initialization logic. | ||
app.beforeStart = async function(){ | ||
async startup({ conf, log, global }){ | ||
}; | ||
}, | ||
// Perform your server finalization logic. | ||
app.afterStop = async function(){ | ||
async shutdown({ conf, log, global }){ | ||
}; | ||
} | ||
// Load your routes and API definitions. | ||
app.api(api); | ||
// Don't forget to return your app. | ||
return app; | ||
} | ||
}); | ||
``` | ||
@@ -78,8 +62,11 @@ | ||
```js | ||
module.exports = function({ post, get, del, head, patch, put }){ | ||
module.exports = function({ post, get, del, head, patch, put, all }){ | ||
// Use express routes and a list of functions (async or regular no matter). | ||
// Define routes and a list of middleware functions (async or regular no matter). | ||
get('/foo/:f/bar/:b', Foo.read, Bar.read); | ||
post('/foo/:f/bar', Foo.read, Bar.write); | ||
// ... | ||
// ALL middleware will run on any method and any path | ||
all(Foo.read, Bar.write); | ||
}; | ||
@@ -117,4 +104,4 @@ ``` | ||
## Manual | ||
On top of all the cool features Express offers, check out how to use all | ||
the awesome goodies Nodecaf introduces. | ||
Formerly based on Express, Nodecaf preserves the same interface for defining routes | ||
through middleware chains. Check out how to use all the awesome goodies Nodecaf introduces. | ||
@@ -128,3 +115,3 @@ ### Handler Args | ||
```js | ||
function({ req, res, next, query, params, body, flash, conf, log, error, headers }){ | ||
function({ req, res, next, query, params, body, flash, conf, log, headers }){ | ||
// Do your stuff. | ||
@@ -136,9 +123,9 @@ } | ||
- `req`, `res`, `next`: The good old parameters used regularly in Express. | ||
- `req`, `res`, `next`: The good old parameters used regularly in middleware-like frameworks. | ||
- `query`, `parameters`, `body`, `headers`: Shortcuts to the homonymous properties of `req`. | ||
They contain respectively the query string, the URL parameters, and the request | ||
body data. | ||
- `flash`: Is a shortcut to Express `req.locals`. Keys inserted in this a object | ||
are preserved for the lifetime of a request and can be accessed in all handlers | ||
of a route chain. | ||
- `flash`: Is an object where you can store arbitrary values. Keys inserted in this | ||
object are preserved for the lifetime of a request and can be accessed in all | ||
handlers of a route chain. | ||
- `conf`: This object contains the entire | ||
@@ -150,4 +137,2 @@ [application configuration data](#settings-file). | ||
as handler args for all routes. | ||
- `error`: A function to [output REST errors](#error-handling) and abort the | ||
handler chain execution. | ||
@@ -164,3 +149,3 @@ ### Settings File | ||
Suported config formats: **TOML**, **YAML**, **JSON** | ||
Suported config formats: **TOML**, **YAML**, **JSON**, **CSON** | ||
@@ -184,6 +169,3 @@ > Check out how to [generate a project with configuration file already plugged in](#init-project) | ||
```js | ||
module.exports = function init(){ | ||
let conf = { key: 'value' }; | ||
let app = new AppServer(conf); | ||
} | ||
module.exports = () => new Nodecaf({ conf: { key: 'value' } }); | ||
``` | ||
@@ -194,5 +176,3 @@ | ||
```js | ||
module.exports = function init(){ | ||
let app = new AppServer(__dirname + '/default.toml'); | ||
} | ||
module.exports = () => new Nodecaf({ conf: __dirname + '/default.toml' }); | ||
``` | ||
@@ -227,3 +207,3 @@ | ||
log.info('hi'); | ||
log.warn({lang: 'fr'}, 'au revoir'); | ||
log.warn({ lang: 'fr' }, 'au revoir'); | ||
log.fatal({ err: new Error() }, 'The error code is %d', 1234); | ||
@@ -243,3 +223,3 @@ } | ||
| Class | Level | Event | | ||
| Type | Level | Event | | ||
|-------|-------|-------| | ||
@@ -260,3 +240,3 @@ | error after headers sent | warn | An error happened inside a route after the headers were already sent | | ||
Additionally, you can filter log entries by level and class with the following | ||
Additionally, you can filter log entries by level and type with the following | ||
settings: | ||
@@ -267,5 +247,10 @@ | ||
level = 'warn' # Only produce log entries with level 'warn' or higher ('error' & 'fatal') | ||
class = 'my-class' # Only produce log entries with class matching exactly 'my-class' | ||
type = 'my-type' # Only produce log entries with type matching exactly 'my-type' | ||
``` | ||
You can disable logging entirely for a given app by setting it to `false` in the config | ||
```toml | ||
log = false | ||
``` | ||
### Async Handlers | ||
@@ -302,3 +287,4 @@ | ||
To support the callback error pattern, use the `error` handler arg. | ||
To support the callback error pattern, use the `res.error()` function arg. This | ||
function will stop the middleware chain from being executed any further. | ||
@@ -308,6 +294,6 @@ ```js | ||
post('/my/thing', function({ error, res }){ | ||
post('/my/thing', function({ res }){ | ||
fs.readFile('./my/file', 'utf8', function(err, contents){ | ||
if(err) | ||
return error(err, 'Optional message to replace the original'); | ||
return res.error(err); | ||
res.end(contents); | ||
@@ -318,14 +304,5 @@ }); | ||
To use other HTTP status codes you can send a string in the first parameter of | ||
`error`. The supported error names are the following: | ||
To use other HTTP status codes you can send an integer in the first parameter of | ||
`res.error()`. | ||
| Error name | Status Code | | ||
|------------|-------------| | ||
| `NotFound` | **404** | | ||
| `Unauthorized` | **401** | | ||
| `ServerFault` | **500** | | ||
| `InvalidActionForState` | **405** | | ||
| `InvalidCredentials` | **400** | | ||
| `InvalidContent` | **400** | | ||
```js | ||
@@ -337,3 +314,3 @@ post('/my/thing', function({ error }){ | ||
catch(e){ | ||
error('NotFound', 'Optional message for the JSON response'); | ||
error(404, 'Optional message for the response'); | ||
} | ||
@@ -343,21 +320,2 @@ }); | ||
You can always deal with uncaught exceptions in all routes through a default | ||
global error handler. In your `lib/main.js` add an `onRuoteError` function | ||
property to the `app`. | ||
```js | ||
app.onRuoteError = function(input, err, send){ | ||
if(err instanceof MyDBError) | ||
send('ServerFalut', 'Sorry! Database is sleeping.'); | ||
else if(err instanceof ValidationError) | ||
send('InvalidContent', err.data); | ||
} | ||
``` | ||
- The `send` function will instruct Nodecaf to output the given error. | ||
- The `input` arg contain all handler args for the request. | ||
- If you do nothing for a specific type of `Error` the normal 500 behavior will | ||
take place. | ||
### REST Assertions | ||
@@ -370,7 +328,5 @@ | ||
```js | ||
let { exist } = require('nodecaf').assertions; | ||
get('/my/thing/:id', function({ params, db }){ | ||
get('/my/thing/:id', function({ params, db, res }){ | ||
let thing = await db.getById(params.id); | ||
exist(thing, 'thing not found'); | ||
res.notFound(!thing, 'thing not found'); | ||
@@ -381,29 +337,16 @@ doStuff(); | ||
If the record is not found, the `exist` call will stop the route execution right | ||
If the record is not found, the `res.notfound()` call will stop the route execution right | ||
away and generate a [RESTful `NotFound` error](#error-handling). | ||
Along with `exist`, the following assertions with similar behavior are provided: | ||
Along with `notFound`, the following assertions with similar behavior are provided: | ||
| Method | Error to be output | | ||
|--------|--------------------| | ||
| `exist` | `NotFound` | | ||
| `valid` | `InvalidContent` | | ||
| `authorized` | `Unauthorized` | | ||
| `authn` | `InvalidCredentials` | | ||
| `able` | `InvalidActionForState` | | ||
| Method | Status Code | | ||
|--------|-------------| | ||
| `badRequest` | 400 | | ||
| `unauthorized` | 401 | | ||
| `forbidden` | 403 | | ||
| `notFound` | 404 | | ||
| `conflict` | 409 | | ||
| `gone` | 410 | | ||
To use it with callback style functions, pass the `error` handler arg as the | ||
third parameter. | ||
```js | ||
let { exist } = require('nodecaf').assertions; | ||
post('/my/file/:id', function({ error, res, params }){ | ||
fs.readFile('./my/file/' + params.id, 'utf8', function(err, contents){ | ||
exist(!err, 'File not found', error); | ||
res.end(contents); | ||
}); | ||
}); | ||
``` | ||
### Expose Globals | ||
@@ -416,5 +359,7 @@ | ||
```js | ||
app.expose({ | ||
db: myDbConnection, | ||
libX: new LibXInstance() | ||
module.exports = () => new Nodecaf({ | ||
startup({ global }){ | ||
global.db = myDbConnection; | ||
global.libX = new LibXInstance(); | ||
} | ||
}); | ||
@@ -580,9 +525,8 @@ ``` | ||
|----------|------|-------------|---------| | ||
| `app.name` | String | Name to be displayed in logs and documentation | `'Untitled'` | | ||
| `app.version` | String | Verison to be displayed in logs and documentation | Version in Package JSON | | ||
| `app.conf.delay` | Integer | Milliseconds to wait before actually starting the app | `0` | | ||
| `app.conf.port` | Integer | Port for the web server to listen (also exposed as user conf) | `80` or `443` | | ||
| `app.shouldParseBody` | Boolean | Wether supported request body types should be parsed | `true` | | ||
| `app.conf.formFileDir` | Path | Where to store files uploaded as form-data | OS default temp dir | | ||
| `app.alwaysRebuildAPI` | Boolean | Wether the API should be rebuilt dynamically for every start or setup operation | `false` | | ||
| `app.cookieSecret` | String | A secure random string to be used for signing cookies | none | | ||
| `app.conf.cookie.secret` | String | A secure random string to be used for signing cookies | none | | ||
| `opts.shouldParseBody` | Boolean | Wether supported request body types should be parsed | `true` | | ||
| `opts.server` | Object | An options object to be passed directly to the http(s) [`createServer()`](https://nodejs.org/api/http.html#http_http_createserver_options_requestlistener) | `{}` | | ||
| `opts.alwaysRebuildAPI` | Boolean | Wether the API should be rebuilt dynamically for every start or setup operation | `false` | |
142
test/spec.js
@@ -240,2 +240,17 @@ const assert = require('assert'); | ||
it('Should execute \'all\' handler on any path/method', async () => { | ||
let app = new Nodecaf({ | ||
api({ all }){ | ||
all(({ res, params }) => { | ||
res.badRequest(!params.path); | ||
res.end(); | ||
}); | ||
} | ||
}); | ||
await app.start(); | ||
(await base.get('foo/bar')).assert.status.is(200); | ||
(await base.post('bah/baz')).assert.status.is(200); | ||
await app.stop(); | ||
}); | ||
it('Should pass all present parameters to handler', async () => { | ||
@@ -623,57 +638,2 @@ let app = new Nodecaf({ | ||
it.skip('Should execute intermediary error handler', async () => { | ||
let app = new Nodecaf(); | ||
let count = 0; | ||
app.onRouteError = function(input, err){ | ||
assert.strictEqual(err.message, 'resterr'); | ||
count++; | ||
}; | ||
app.api(function({ post }){ | ||
post('/known', () => { | ||
throw new Error('resterr'); | ||
}); | ||
post('/unknown', ({ error }) => { | ||
error('NotFound', 'resterr'); | ||
}); | ||
}); | ||
await app.start(); | ||
let { status } = await base.post('known'); | ||
assert.strictEqual(status, 500); | ||
let { status: s2 } = await base.post('unknown'); | ||
assert.strictEqual(s2, 404); | ||
assert.strictEqual(count, 2); | ||
await app.stop(); | ||
}); | ||
it.skip('Should allow tapping into the thrown error', async () => { | ||
let app = new Nodecaf(); | ||
app.onRouteError = function(input, err, error){ | ||
error('Unauthorized', 'resterr'); | ||
}; | ||
app.api(function({ post }){ | ||
post('/unknown', () => { | ||
throw new Error('resterr'); | ||
}); | ||
}); | ||
await app.start(); | ||
let { status } = await base.post('unknown'); | ||
assert.strictEqual(status, 401); | ||
await app.stop(); | ||
}); | ||
it.skip('Should expose handler args object to user error handler', async () => { | ||
let app = new Nodecaf(); | ||
app.onRouteError = function(input){ | ||
assert.strictEqual(typeof input.req, 'object'); | ||
}; | ||
app.api(function({ post }){ | ||
post('/unknown', () => { | ||
throw new Error('resterr'); | ||
}); | ||
}); | ||
await app.start(); | ||
await base.post('unknown'); | ||
await app.stop(); | ||
}); | ||
}); | ||
@@ -735,21 +695,2 @@ | ||
it.skip('Should execute user error handler even if headers were already sent', async () => { | ||
let app = new Nodecaf(); | ||
app.api(function({ post }){ | ||
post('/bar', ({ res }) => { | ||
res.end(); | ||
throw new Error(); | ||
}); | ||
}); | ||
let gotHere = false; | ||
app.onRouteError = function(){ | ||
gotHere = true; | ||
}; | ||
await app.start(); | ||
app.express.set('env', 'test'); | ||
await base.post('bar'); | ||
await app.stop(); | ||
assert(gotHere); | ||
}); | ||
it('Should not hang up connections when they have a query string', function(done){ | ||
@@ -945,4 +886,55 @@ let count = 0; | ||
await app.stop(); | ||
}) | ||
}); | ||
it('Should reject unwanted content-types for the given route', async () => { | ||
let app = new Nodecaf({ | ||
api({ post }){ | ||
let acc = this.accept([ 'urlencoded', 'text/html' ]); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
} | ||
}); | ||
await app.start(); | ||
let { assert } = await base.post( | ||
'foo', | ||
{ 'Content-Type': 'application/json' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.status.is(415); | ||
await app.stop(); | ||
}); | ||
it('Should accept wanted content-types for the given route', async () => { | ||
let app = new Nodecaf({ | ||
api({ post }){ | ||
let acc = this.accept('text/html'); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
} | ||
}); | ||
await app.start(); | ||
let { status } = await base.post( | ||
'foo', | ||
{ 'Content-Type': 'text/html' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
it('Should accept requests without a body payload', async () => { | ||
let app = new Nodecaf({ | ||
api({ post }){ | ||
let acc = this.accept('text/html'); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
} | ||
}); | ||
await app.start(); | ||
let { status } = await base.post( | ||
'foo', | ||
{ 'no-auto': true }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
92412
1489
508
Updatedcookie-parser@^1.4.5
Updatedws@^7.3.1