Comparing version 0.6.0 to 0.7.0
@@ -5,2 +5,7 @@ #!node | ||
if(process.argv[2] == '-h') | ||
process.argv[2] = 'help'; | ||
else if(process.argv[2] == '-v') | ||
process.argv[2] = 'version'; | ||
let cf = path.resolve(__dirname, '../lib/cli', process.argv[2] + '.js'); | ||
@@ -7,0 +12,0 @@ |
@@ -8,2 +8,21 @@ # Nodecaf Changelog | ||
## [v0.7.0] - 2019-07-07 | ||
### Added | ||
- support for YAML config files | ||
- app method to filter requests by body content-type app-wide | ||
- top-level function to define per-route content-type filtering rules | ||
- default generic request body description to open api doc operations | ||
- accepted mime-types to operation request body api doc | ||
- global CLI help command to list available commands and usage | ||
- CLI command to output version of the globally installed Nodecaf | ||
- `--no-optional` flag to install command output by `nodecaf init` | ||
### Fixed | ||
- CLI error: unknown type "as-is" on cli options | ||
- CLI init error when lib or bin directory already exists | ||
### Changed | ||
- error messages to not be JSON by default | ||
## [v0.6.0] - 2019-06-24 | ||
@@ -124,1 +143,2 @@ | ||
[v0.6.0]: https://gitlab.com/GCSBOSS/nodecaf/-/tags/v0.6.0 | ||
[v0.7.0]: https://gitlab.com/GCSBOSS/nodecaf/-/tags/v0.7.0 |
@@ -1,2 +0,1 @@ | ||
const os = require('os'); | ||
const fs = require('fs'); | ||
@@ -7,21 +6,13 @@ const http = require('http'); | ||
const compression = require('compression'); | ||
const fileUpload = require('express-fileupload'); | ||
const { defaultErrorHandler, addRoute } = require('./route-adapter'); | ||
const { parseTypes } = require('./parse-types'); | ||
const setupLogger = require('./logger'); | ||
const errors = require('./errors'); | ||
const HTTP_VERBS = ['get', 'post', 'patch', 'put', 'head']; | ||
const noop = Function.prototype; | ||
function parsePlainTextBody(req, res, next){ | ||
if(Object.keys(req.body).length > 0) | ||
return next(); | ||
req.setEncoding('utf8'); | ||
req.body = ''; | ||
req.on('data', chunk => req.body += chunk); | ||
req.on('end', next); | ||
} | ||
function routeNotFoundHandler(req, res, next){ | ||
next(errors.NotFound('NotFound')); | ||
next(errors.NotFound()); | ||
} | ||
@@ -44,2 +35,3 @@ | ||
this.server = null; | ||
this.accepts = false; | ||
@@ -51,9 +43,2 @@ // Setup logger. | ||
this.express.use(compression()); | ||
this.express.use(express.json({ strict: false })); | ||
this.express.use(express.urlencoded({ extended: true })); | ||
this.express.use(fileUpload({ | ||
useTempFiles: true, | ||
tempFileDir: os.tmpdir() | ||
})); | ||
this.express.use(parsePlainTextBody); | ||
@@ -70,2 +55,11 @@ // Create adapted versions of all Express routing methods. | ||
/* o\ | ||
Define a whitelist of accepted request body mime-types for all routes | ||
in the app. Effectively blocks all requests whose mime-type is not one | ||
of @types. May be overriden by route specific accepts. | ||
\o */ | ||
accept(types){ | ||
this.accepts = parseTypes(types); | ||
} | ||
/* o\ | ||
Execute the @callback to define user routes, exposing all REST methods | ||
@@ -76,3 +70,3 @@ as arguments. Meant to shorten the way you define routes and plug in the | ||
api(callback){ | ||
callback(this.routerFuncs); | ||
callback.bind(this)(this.routerFuncs); | ||
this.express.use(routeNotFoundHandler); | ||
@@ -79,0 +73,0 @@ this.express.use(defaultErrorHandler.bind(this)); |
@@ -69,4 +69,4 @@ const path = require('path'); | ||
confPath: [ 'c', 'Conf file path', 'file', undefined ], | ||
confType: [ false, 'Conf file extension', 'as-is', 'toml' ], | ||
name: [ 'n', 'A name/title for the app', 'as-is', undefined ] | ||
confType: [ false, 'Conf file extension', 'string', 'toml' ], | ||
name: [ 'n', 'A name/title for the app', 'string', undefined ] | ||
}); | ||
@@ -84,4 +84,8 @@ | ||
console.log('Generating basic file structure...'); | ||
if(fs.existsSync(projDir + '/lib')) | ||
throw new Error('The \'lib\' directory already exists'); | ||
if(fs.existsSync(projDir + '/bin')) | ||
throw new Error('The \'bin\' directory already exists'); | ||
input.confType = generateConfFile(input); | ||
@@ -97,4 +101,7 @@ generateRunFile(input, projDir, projName); | ||
if(!('nodecaf' in (pkgInfo.dependencies || []))) | ||
console.log('Install nodecaf localy with:\n npm i nodecaf'); | ||
console.log('Install nodecaf localy with:\n npm i --no-optional nodecaf'); | ||
console.log('Install your app run binary with:\n npm link'); | ||
}; | ||
module.exports.description = 'Generates a skelleton Nodecaf project file ' + | ||
'structure in the current directory'; |
@@ -19,5 +19,5 @@ const path = require('path'); | ||
apiPath: [ false, 'The path to your API file (defaults to ./lib/api.js)', 'file', './lib/api.js' ], | ||
type: [ 't', 'A type of output file [yaml || json] (defaults to json)', 'as-is', 'yaml' ], | ||
type: [ 't', 'A type of output file [yaml || json] (defaults to json)', 'string', 'yaml' ], | ||
confPath: [ 'c', 'Conf file path', 'file', undefined ], | ||
confType: [ false, 'Conf file extension', 'as-is', 'toml' ], | ||
confType: [ false, 'Conf file extension', 'string', 'toml' ], | ||
outFile: [ 'o', 'Output file (required)', 'file', undefined ] | ||
@@ -49,1 +49,4 @@ }); | ||
}; | ||
module.exports.description = 'Generates an Open API compliant document of a ' + | ||
'given Nodecaf API'; |
const fs = require('fs'); | ||
const toml = require('toml'); | ||
const TOML = require('toml'); | ||
const YAML = require('yaml'); | ||
const path = require('path'); | ||
const loaders = { | ||
toml: conf => toml.parse(fs.readFileSync(conf)) | ||
toml: conf => TOML.parse(fs.readFileSync(conf)), | ||
yaml: conf => YAML.parse(fs.readFileSync(conf, 'utf8')) | ||
} | ||
@@ -9,0 +11,0 @@ |
@@ -0,9 +1,7 @@ | ||
const createError = require('http-errors'); | ||
function composeError(type, status, msg){ | ||
let e = new Error(msg); | ||
e.type = type; | ||
e.status = status; | ||
let data = { message: msg }; | ||
e.body = JSON.stringify(data); | ||
return e; | ||
msg = msg || ''; | ||
msg = typeof msg == 'object' ? JSON.stringify(msg) : String(msg); | ||
return createError(status, msg, { type: type }); | ||
} | ||
@@ -18,2 +16,3 @@ | ||
InvalidContent: msg => composeError('InvalidContent', 400, msg), | ||
BadRequest: msg => composeError('BadRequest', 400, msg), | ||
@@ -20,0 +19,0 @@ parse(err, msg){ |
@@ -1,49 +0,7 @@ | ||
const assert = require('assert'); | ||
const AppServer = require('./app-server'); | ||
const loadConf = require('./conf-loader'); | ||
/* istanbul ignore next */ | ||
function term(app, debug){ | ||
app.stop(); | ||
if(debug) | ||
setTimeout(() => process.exit(0), 1000); | ||
else | ||
console.log('%s is shutting down', app.name); | ||
} | ||
/* istanbul ignore next */ | ||
function die(app, debug, err, origin){ | ||
if(app && app.log) | ||
app.log.fatal({ err: err }, 'FATAL ERROR'); | ||
if(debug) | ||
console.log('Unhandled Exception', err, origin); | ||
else | ||
console.log('Unhandled Exception', err.message, origin); | ||
process.exit(1); | ||
} | ||
module.exports = { | ||
async run({ init, confType, confPath }){ | ||
assert.equal(typeof init, 'function'); | ||
// Load conf and inputs it on the app class. | ||
let settings = loadConf(confType || 'toml', confPath || false); | ||
let app = init(settings); | ||
assert(app instanceof AppServer); | ||
// Handle signals. | ||
let debug = settings.debug || false; | ||
process.on('SIGINT', term.bind(null, app, debug)); | ||
process.on('SIGTERM', term.bind(null, app, debug)); | ||
process.on('uncaughtException', die.bind(null, app, debug)); | ||
process.on('unhandledRejection', die.bind(null, app, debug)); | ||
// Starts the app. | ||
await app.start(); | ||
console.log('%s listening at %s', app.name, app.server.address().port); | ||
}, | ||
AppServer: AppServer, | ||
assertions: require('./assertions') | ||
run: require('./run'), | ||
AppServer: require('./app-server'), | ||
assertions: require('./assertions'), | ||
accept: require('./parse-types').accept | ||
} |
const express = require('express'); | ||
const { parseTypes } = require('./parse-types'); | ||
const HTTP_VERBS = ['get', 'post', 'patch', 'put', 'head']; | ||
@@ -11,2 +13,5 @@ | ||
description: 'The server has faced an error state caused by unknown reasons.' | ||
}, | ||
Success: { | ||
description: 'The request has been processed without any issues' | ||
} | ||
@@ -16,2 +21,68 @@ } | ||
function buildDefaultSchemas(){ | ||
return { | ||
MissingType: { type: 'string', description: 'Missing \'Content-Type\' header' }, | ||
BadType: { type: 'string', description: 'Unsupported content type' } | ||
} | ||
} | ||
function buildResponses(opts){ | ||
let r = { | ||
500: { $ref: '#/components/responses/ServerFault' }, | ||
200: { $ref: '#/components/responses/Success' } | ||
}; | ||
if(opts.accept) | ||
r[400] = { | ||
description: 'Bad Request', | ||
content: { | ||
'text/plain': { | ||
schema: { | ||
oneOf: [ | ||
{ '$ref': '#/components/schemas/MissingType' }, | ||
{ '$ref': '#/components/schemas/BadType' } | ||
] | ||
} | ||
} | ||
} | ||
}; | ||
return r; | ||
} | ||
function buildDefaultRequestBody(){ | ||
return { | ||
description: 'Any request body type/format.', | ||
content: { '*/*': {} } | ||
} | ||
} | ||
function buildCustomRequestBodies(accepts){ | ||
return { | ||
description: 'Accepts the following types: ' + accepts.join(', '), | ||
content: accepts.reduce((a, c) => ({ ...a, [c]: {} }), {}) | ||
} | ||
} | ||
function parseRouteHandlers(handlers){ | ||
let opts = {}; | ||
for(let h of handlers) | ||
if(typeof h == 'object') | ||
opts = { ...opts, ...h }; | ||
return opts; | ||
} | ||
function parsePathParams(params){ | ||
return params.map(k => ({ | ||
name: k.name, | ||
in: 'path', | ||
description: k.description || '', | ||
required: true, | ||
deprecated: k.deprecated || false, | ||
schema: { type: 'string' } | ||
})); | ||
} | ||
/* o\ | ||
@@ -31,14 +102,22 @@ Add summary and description to a route. | ||
\o */ | ||
function addOp(method, path){ | ||
function addOp(method, path, ...handlers){ | ||
// this => app | ||
let paths = this.paths; | ||
paths[path] = paths[path] || {}; | ||
let p = this.paths[path] || {}; | ||
this.paths[path] = p; | ||
// Reference the global responses used. | ||
paths[path][method] = { | ||
responses: { | ||
500: { $ref: '#/components/responses/ServerFault' } | ||
} | ||
// Asseble reqests and responses data. | ||
let opts = parseRouteHandlers(handlers); | ||
let responses = buildResponses(opts); | ||
let accs = opts.accept || this.accepts; | ||
let reqBody = !accs ? buildDefaultRequestBody() : buildCustomRequestBodies(accs); | ||
// Asseble basic operation object. | ||
p[method] = { | ||
responses: responses, | ||
requestBody: reqBody | ||
}; | ||
if(method in { get: true, head: true, delete: true }) | ||
delete p[method].requestBody; | ||
// Add express route. | ||
@@ -49,13 +128,5 @@ this.router[method](path, Function.prototype); | ||
this.router.stack.forEach(l => { | ||
if(l.route.path !== path || paths[path].parameters) | ||
if(l.route.path !== path || p.parameters) | ||
return; | ||
paths[path].parameters = l.keys.map(k => ({ | ||
name: k.name, | ||
in: 'path', | ||
description: k.description || '', | ||
required: true, | ||
deprecated: k.deprecated || false, | ||
schema: { type: 'string' } | ||
})); | ||
p.parameters = parsePathParams(l.keys); | ||
}); | ||
@@ -100,2 +171,10 @@ | ||
/* o\ | ||
Define allowed mime-types for request accross the entire app. Can be | ||
overriden by route specific settings. | ||
\o */ | ||
accept(types){ | ||
this.accepts = parseTypes(types); | ||
} | ||
/* o\ | ||
@@ -106,3 +185,3 @@ Execute the @callback to define user routes, exposing all REST methods | ||
api(callback){ | ||
callback(this.routerFuncs); | ||
callback.bind(this)(this.routerFuncs); | ||
} | ||
@@ -119,3 +198,4 @@ | ||
components: { | ||
responses: buildDefaultRESTResponses() | ||
responses: buildDefaultRESTResponses(), | ||
schemas: buildDefaultSchemas() | ||
} | ||
@@ -122,0 +202,0 @@ }; |
@@ -0,4 +1,51 @@ | ||
const os = require('os'); | ||
const express = require('express'); | ||
const getRawBody = require('raw-body'); | ||
const contentType = require('content-type'); | ||
const fileUpload = require('express-fileupload'); | ||
const adaptErrors = require('./a-sync-error-adapter'); | ||
const errors = require('./errors'); | ||
const parsers = { | ||
'application/json': express.json({ strict: false }), | ||
'application/x-www-form-urlencoded': express.urlencoded({ extended: true }), | ||
'multipart/form-data': fileUpload({ useTempFiles: true, tempFileDir: os.tmpdir() }) | ||
}; | ||
function filter(custom, req, res, next){ | ||
// this => app | ||
if(!custom && !this.accepts) | ||
return next(); | ||
let ct = req.headers['content-type']; | ||
if(!ct) | ||
return next(errors.BadRequest('Missing \'Content-Type\' header')); | ||
let arr = custom || this.accepts; | ||
if(!arr.includes(ct)) | ||
return next(errors.BadRequest('Unsupported content type \'' + ct + '\'')); | ||
next(); | ||
} | ||
async function parse(req, res, next){ | ||
try{ | ||
var ct = contentType.parse(req); | ||
} | ||
catch(e){ | ||
ct = { type: 'plain/text', parameters: { charset: 'utf8' } }; | ||
} | ||
if(ct.type in parsers) | ||
return parsers[ct.type](req, res, next); | ||
req.body = await getRawBody(req, { | ||
length: req.headers['content-length'], | ||
encoding: ct.parameters.charset | ||
}); | ||
next(); | ||
} | ||
function triggerError(input, err, msg){ | ||
@@ -22,3 +69,3 @@ // this => app | ||
query: req.query, params: req.params, body: req.body, | ||
flash: res.locals, conf: app.settings, log: app.log || null | ||
flash: res.locals, conf: app.settings, log: app.log | ||
}; | ||
@@ -43,14 +90,25 @@ | ||
addRoute(method, path, ...route){ | ||
// this => app | ||
let customAccept = false; | ||
let aRoutes = []; | ||
// Loop through th route handlers adapting them. | ||
route = route.map(handler => { | ||
for(let handler of route){ | ||
if(handler.accept){ | ||
customAccept = handler.accept; | ||
continue; | ||
} | ||
if(typeof handler !== 'function') | ||
throw Error('Trying to add non-function route handler'); | ||
return adaptHandler(this, handler); | ||
}); | ||
aRoutes.push(adaptHandler(this, handler)); | ||
} | ||
let accept = filter.bind(this, customAccept); | ||
// Physically add the adapted route to Express. | ||
this.express[method](path, ...route); | ||
this.express[method](path, accept, parse, ...aRoutes); | ||
return { desc: Function.prototype }; | ||
@@ -80,3 +138,3 @@ }, | ||
// Handle known REST errors. | ||
res.status(err.status).end(err.body); | ||
res.status(err.status).end(err.message); | ||
@@ -83,0 +141,0 @@ // Log unexpected errors sent to the user. |
{ | ||
"name": "nodecaf", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "Nodecaf is an Express framework for developing REST APIs in a quick and convenient manner.", | ||
@@ -43,3 +43,5 @@ "main": "lib/main.js", | ||
"express": "^4.17.1", | ||
"express-fileupload": "^1.1.4", | ||
"express-fileupload": "^1.1.5", | ||
"http-errors": "^1.7.3", | ||
"mime": "^2.4.4", | ||
"toml": "^3.0.0", | ||
@@ -49,7 +51,7 @@ "yaml": "^1.6.0" | ||
"devDependencies": { | ||
"form-data": "^2.3.3", | ||
"muhb": "0.0.2", | ||
"swagger-parser": "^7.0.0", | ||
"form-data": "^2.4.0", | ||
"muhb": "^0.1.1", | ||
"swagger-parser": "^7.0.1", | ||
"wtfnode": "^0.8.0" | ||
} | ||
} |
122
README.md
# [Nodecaf](https://gitlab.com/GCSBOSS/nodecaf) | ||
> Docs for version v0.6.x. | ||
> Docs for version v0.7.x. | ||
@@ -9,3 +9,3 @@ Nodecaf is an Express framework for developing REST APIs in a quick and | ||
- Useful [handler arguments](#handlers-args). | ||
- Built-in TOML [settings file support](#settings-file). | ||
- Built-in [settings file support](#settings-file). | ||
- [Out-of-the-box logging](#logging) through Bunyan. | ||
@@ -21,2 +21,3 @@ - Seamless support for [async functions as route handlers](#async-handlers). | ||
source of truth. | ||
- Functions to [filter request bodies](#filter-requests-by-mime-type) by mime-type. | ||
- CLI command to [generate a basic Nodecaf project structure](#init-project). | ||
@@ -48,3 +49,3 @@ - CLI command to [generate an OpenAPI document](#open-api-support) or your APIs. | ||
// Expose things to all routes putting them on the 'shared' object. | ||
// Expose things to all routes putting them in the 'shared' object. | ||
let shared = {}; | ||
@@ -110,4 +111,4 @@ app.expose(shared); | ||
## Manual | ||
Beyond all the cool features of Express has to offer, check out how to use all | ||
the awesome goodies Nodecaf can give you. | ||
On top of all the cool features Express offers, check out how to use all | ||
the awesome goodies Nodecaf introduces. | ||
@@ -128,3 +129,3 @@ ### Handler Args | ||
- `req`, `res`, `next`: Basically the good old parameters used regularly in Express. | ||
- `req`, `res`, `next`: The good old parameters used regularly in Express. | ||
- `query`, `parameters`, `body`: Shortcuts to the homonymous properties of `req`. | ||
@@ -134,5 +135,5 @@ They contain respectively the query string, the URL parameters, and the request | ||
- `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 on all handlers | ||
are preserved for the lifetime of a request and can be accessed in all handlers | ||
of a route chain. | ||
- `conf`: This object contain the entire | ||
- `conf`: This object contains the entire | ||
[application configuration data](#settings-file). | ||
@@ -148,4 +149,4 @@ - `log`: A bunyan logger instance. Use it to [log custom events](#logging) of | ||
Nodecaf allow you to read a configuration file in the TOML format (we plan to | ||
add more in the future) and use it's data on all routes and server configuration. | ||
Nodecaf allow you to read a configuration file and use it's data in all routes | ||
and server configuration. | ||
@@ -157,9 +158,11 @@ Use this feature to manage: | ||
> [generate a project with configuration file already plugged in](#init-project) | ||
Suported config formats: **TOML**, **YAML** | ||
> Check out how to [generate a project with configuration file already plugged in](#init-project) | ||
To setup a config file for an existing project, open the binary for your server | ||
on `bin/proj-name.js`. Then add a `confPath` key to the run parameter object | ||
in `bin/proj-name.js`. Then add a `confPath` key to the run parameter object | ||
whose value must be a string path pointing to your conf file. | ||
The data in the config file can be accessed on `lib/main.js` through the first | ||
The data in the config file can be accessed in `lib/main.js` through the first | ||
parameter of the exported `init` function: | ||
@@ -173,3 +176,3 @@ | ||
You can also use the config data through [it's handler arg](#handler-args) on | ||
You can also use the config data through [it's handler arg](#handler-args) in | ||
all route handlers as follows: | ||
@@ -194,3 +197,3 @@ | ||
You can also setup a file log on your settings file to be automatically | ||
You can also setup a log file in your settings file to be automatically | ||
transferred to the `conf` argument of `init`. | ||
@@ -203,3 +206,3 @@ | ||
On your route handlers, use the `log` handler arg as a | ||
In your route handlers, use the `log` handler arg as a | ||
[bunyan](https://github.com/trentm/node-bunyan) instance: | ||
@@ -226,7 +229,6 @@ | ||
Nodecaf brings a useful feature of accepting async functions as route handlers | ||
with zero configuration. The real deal is that all rejections/error within your | ||
async handler will be gracefully handled by the same routine the deals with | ||
regular functions. Allowing you to avoid callback hell without creating bogus | ||
adapters for your promises. | ||
Nodecaf brings the useful feature of accepting async functions as route handlers | ||
with zero configuration. All rejections/error within your async handler will be | ||
gracefully handled by the same routine the deals with regular functions. You will | ||
be able to avoid callback hell without creating bogus adapters for your promises. | ||
@@ -248,4 +250,4 @@ ```js | ||
In Nodecaf, all uncaught synchronous errors happening inside route handler code | ||
is automatically converted into a harmless RESTful 500. | ||
In Nodecaf, any uncaught synchronous error happening inside route handler will be | ||
automatically converted into a harmless RESTful 500. | ||
@@ -275,8 +277,10 @@ ```js | ||
- `NotFound`: 404 | ||
- `Unauthorized`: 401 | ||
- `ServerFault`: 500 | ||
- `InvalidActionForState`: 405 | ||
- `InvalidCredentials`: 400 | ||
- `InvalidContent`: 400 | ||
| Error name | Status Code | | ||
|------------|-------------| | ||
| `NotFound` | **404** | | ||
| `Unauthorized` | **401** | | ||
| `ServerFault` | **500** | | ||
| `InvalidActionForState` | **405** | | ||
| `InvalidCredentials` | **400** | | ||
| `InvalidContent` | **400** | | ||
@@ -294,4 +298,4 @@ ```js | ||
You can always deal with uncaught exceptions on all routes through a default | ||
global error handler. In your `lib/main.js` add a `onRuoteError` function | ||
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`. | ||
@@ -317,3 +321,3 @@ | ||
Nodecaf provides you with an assertion module containing functions to generate | ||
the most common REST outputs based on some condition. Checn an example to | ||
the most common REST outputs based on some condition. Check an example to | ||
trigger a 404 in case a database record doesn't exist. | ||
@@ -337,6 +341,8 @@ | ||
- `valid`: `InvalidContent` | ||
- `authorized`: `Unauthorized` | ||
- `authn`: `InvalidCredentials` | ||
- `able`: `InvalidActionForState` | ||
| Method | Error to be output | | ||
|--------|--------------------| | ||
| `valid` | `InvalidContent` | | ||
| `authorized` | `Unauthorized` | | ||
| `authn` | `InvalidCredentials` | | ||
| `able` | `InvalidActionForState` | | ||
@@ -392,2 +398,38 @@ To use it with callback style functions, pass the `error` handler arg as the | ||
### Filter Requests by Mime-type | ||
Nodecaf allow you to reject request bodies whose mime-type is not in a defined | ||
white-list. Denied requests will receive a 400 response with the apporpriate | ||
message. | ||
Define a filter for the entire app on your `api.js`: | ||
```js | ||
module.exports = function({ }){ | ||
this.accept(['json', 'text/html']); | ||
} | ||
``` | ||
Override the global accept per route on your `api.js`: | ||
```js | ||
const { accept } = require('nodecaf'); | ||
module.exports = function({ post, put }){ | ||
// Define global accept rules | ||
this.accept(['json', 'text/html']); | ||
// Obtain accepts settings | ||
let json = accept('json'); | ||
let img = accept([ 'png', 'jpg', 'svg', 'image/*' ]); | ||
// Prepend accept definition in each route chain | ||
post('/my/json/thing', json, myJSONHandler); | ||
post('/my/img/thing', img, myImageHandler); | ||
} | ||
``` | ||
### API Description | ||
@@ -446,6 +488,6 @@ | ||
`nodecaf init` Generates a skelleton Nodecaf project file structure on the current | ||
`nodecaf init` Generates a skelleton Nodecaf project file structure in the current | ||
directory. | ||
> You must already have a well-formed package.json on the target directory. | ||
> You must already have a well-formed package.json in the target directory. | ||
@@ -457,6 +499,7 @@ **Options** | ||
- `-n --name [string]`: A name/title for the generated app structure | ||
- `--confType (yaml | toml)`: The type for the generated config file | ||
#### Open API Support | ||
`nodecaf openapi` Generates a [Open API](https://www.openapis.org/) compliant | ||
`nodecaf openapi` Generates an [Open API](https://www.openapis.org/) compliant | ||
document of a given Nodecaf API. | ||
@@ -471,1 +514,2 @@ | ||
- `-c --confPath [file]`: The config file to be considered | ||
- `--confType (yaml | toml)`: The type of the given config file |
@@ -43,2 +43,11 @@ //const wtf = require('wtfnode'); | ||
it('Should fail when \'lib\' or \'bin\' directories already exist', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
fs.mkdirSync('./bin'); | ||
assert.throws( () => init({}), /already exists/g); | ||
fs.rmdirSync('./bin'); | ||
fs.mkdirSync('./lib'); | ||
assert.throws( () => init({}), /already exists/g); | ||
}); | ||
it('Should generate basic structure files', () => { | ||
@@ -128,2 +137,18 @@ fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
describe('nodecaf -h', () => { | ||
const help = require('../lib/cli/help'); | ||
it('Should output the top-level CLI help', () => { | ||
let text = help(); | ||
assert(/Commands\:/.test(text)); | ||
assert(text.length > 100); | ||
}); | ||
}); | ||
describe('nodecaf -v', () => { | ||
const version = require('../lib/cli/version'); | ||
it('Should output a proper version number', () => { | ||
assert(/^v\d+\.\d+\.\d+$/.test(version())); | ||
}); | ||
}); | ||
}); | ||
@@ -130,0 +155,0 @@ |
301
test/spec.js
//const wtf = require('wtfnode'); | ||
const assert = require('assert'); | ||
// Address for the tests' local servers to listen. | ||
const LOCAL_HOST = 'http://localhost:80/' | ||
describe('Conf Loader', () => { | ||
@@ -21,2 +24,7 @@ const loadConf = require('../lib/conf-loader'); | ||
}); | ||
it('Should properly load an YAML file and generate an object', () => { | ||
let obj = loadConf('yaml', './test/res/conf.yaml'); | ||
assert.strictEqual(obj.key, 'value'); | ||
}); | ||
}); | ||
@@ -47,33 +55,2 @@ | ||
describe('Route Adapter', () => { | ||
const { EventEmitter } = require('events'); | ||
const { addRoute } = require('../lib/route-adapter'); | ||
it('Should fail when anything other than a function is passed', () => { | ||
let ee = new EventEmitter(); | ||
assert.throws( () => addRoute.bind(ee)('get', '/foo', null) ); | ||
}); | ||
it('Should add adapted handler to chosen route', () => { | ||
let ee = new EventEmitter(); | ||
ee.express = { | ||
foo(path){ assert.strictEqual(path, 'foo') } | ||
}; | ||
addRoute.bind(ee)('foo', 'foo', function bar(){ }); | ||
}); | ||
it('Should pass all the required args to adapted function', () => { | ||
let ee = new EventEmitter(); | ||
let fn; | ||
ee.express = { | ||
foo(path, handler){ fn = handler } | ||
}; | ||
addRoute.bind(ee)('foo', 'foo', function bar(args){ | ||
assert.strictEqual(typeof args, 'object'); | ||
}); | ||
fn('foo', 'bar', 'foobar'); | ||
}); | ||
}); | ||
describe('AppServer', () => { | ||
@@ -102,3 +79,3 @@ const AppServer = require('../lib/app-server'); | ||
await app.start(); | ||
let { status } = await get('http://127.0.0.1:80/'); | ||
let { status } = await get(LOCAL_HOST); | ||
assert.strictEqual(status, 404); | ||
@@ -151,3 +128,3 @@ await app.stop(); | ||
await app.start(); | ||
let { status } = await post('http://127.0.0.1:80/foo'); | ||
let { status } = await post(LOCAL_HOST + 'foo'); | ||
assert.strictEqual(status, 500); | ||
@@ -166,3 +143,3 @@ await app.stop(); | ||
await app.start(); | ||
let { body } = await get('http://127.0.0.1:80/bar'); | ||
let { body } = await get(LOCAL_HOST + 'bar'); | ||
assert.strictEqual(body, 'bar'); | ||
@@ -183,3 +160,3 @@ await app.stop(); | ||
await app.start(); | ||
let { body } = await post('http://127.0.0.1:80/bar'); | ||
let { body } = await post(LOCAL_HOST + 'bar'); | ||
assert.strictEqual(body, 'foobar'); | ||
@@ -198,3 +175,3 @@ await app.stop(); | ||
try{ | ||
await get('http://127.0.0.1:80/'); | ||
await get(LOCAL_HOST); | ||
} | ||
@@ -230,6 +207,6 @@ catch(e){ | ||
await app.start(); | ||
let { status } = await get('http://127.0.0.1:80/'); | ||
let { status } = await get(LOCAL_HOST); | ||
assert.strictEqual(status, 404); | ||
await app.restart(); | ||
let { status: s } = await get('http://127.0.0.1:80/'); | ||
let { status: s } = await get(LOCAL_HOST); | ||
assert.strictEqual(s, 404); | ||
@@ -241,2 +218,58 @@ await app.stop(); | ||
describe('#accept', () => { | ||
it('Should reject unwanted content-types API-wide', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
this.accept([ 'urlencoded', 'text/html' ]); | ||
assert(this.accepts.includes('application/x-www-form-urlencoded')); | ||
assert.strictEqual(this.accepts.length, 2); | ||
post('/foo', ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { body, status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ 'Content-Type': 'application/json' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 400); | ||
assert(/Unsupported/.test(body)); | ||
await app.stop(); | ||
}); | ||
it('Should reject requests without content-type', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
this.accept('text/html'); | ||
post('/foo', ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { body, status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ '--no-auto': true }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 400); | ||
assert(/Missing/.test(body)); | ||
await app.stop(); | ||
}); | ||
it('Should accept wanted content-types API-wide', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
this.accept([ 'urlencoded', 'text/html' ]); | ||
post('/foo', ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ 'Content-Type': 'text/html' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
}); | ||
}); | ||
@@ -247,4 +280,37 @@ | ||
const AppServer = require('../lib/app-server'); | ||
const { post, get } = require('muhb'); | ||
it('Should expose file content sent as multipart-form', async () => { | ||
const { EventEmitter } = require('events'); | ||
const { addRoute } = require('../lib/route-adapter'); | ||
it('Should fail when anything other than a function is passed', () => { | ||
let ee = new EventEmitter(); | ||
assert.throws( () => addRoute.bind(ee)('get', '/foo', 4) ); | ||
}); | ||
it('Should add adapted handler to chosen route', () => { | ||
let ee = new EventEmitter(); | ||
ee.express = { | ||
foo(path){ assert.strictEqual(path, 'foo') } | ||
}; | ||
addRoute.bind(ee)('foo', 'foo', function bar(){ }); | ||
}); | ||
it('Should pass all the required args to adapted function', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ get }){ | ||
get('/foo', (obj) => { | ||
assert(obj.res && obj.req && obj.next && obj.body === '' | ||
&& obj.params && obj.query && obj.flash && obj.error | ||
&& obj.conf && obj.log); | ||
obj.res.end(); | ||
}); | ||
}); | ||
await app.start(); | ||
let { status } = await get(LOCAL_HOST + 'foo'); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
it('Should expose file content sent as multipart/form-data', async () => { | ||
const FormData = require('form-data'); | ||
@@ -265,3 +331,3 @@ let app = new AppServer(); | ||
await new Promise(resolve => | ||
form.submit('http://localhost/bar/', (err, res) => { | ||
form.submit(LOCAL_HOST + 'bar/', (err, res) => { | ||
assert(res.headers['x-test'] == 'file.txt'); | ||
@@ -275,4 +341,2 @@ resolve(); | ||
const { post } = require('muhb'); | ||
it('Should parse JSON request body payloads', async () => { | ||
@@ -288,3 +352,3 @@ let app = new AppServer(); | ||
let { status } = await post( | ||
'http://localhost:80/foobar', | ||
LOCAL_HOST + 'foobar', | ||
{ 'Content-Type': 'application/json' }, | ||
@@ -301,3 +365,3 @@ JSON.stringify({foo: 'bar'}) | ||
post('/foobar', ({ body, res }) => { | ||
assert.strictEqual(typeof body, 'string'); | ||
assert.strictEqual(body, '{"foo":"bar"}'); | ||
res.end(); | ||
@@ -308,3 +372,4 @@ }); | ||
let { status } = await post( | ||
'http://localhost:80/foobar', | ||
LOCAL_HOST + 'foobar', | ||
{ '--no-auto': true }, | ||
JSON.stringify({foo: 'bar'}) | ||
@@ -326,3 +391,3 @@ ); | ||
let { status } = await post( | ||
'http://localhost:80/foobar', | ||
LOCAL_HOST + 'foobar', | ||
{ 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
@@ -344,3 +409,3 @@ 'foo=bar' | ||
await app.start(); | ||
let { status } = await post('http://localhost:80/foobar?foo=bar'); | ||
let { status } = await post(LOCAL_HOST + 'foobar?foo=bar'); | ||
assert.strictEqual(status, 200); | ||
@@ -350,8 +415,21 @@ await app.stop(); | ||
it('Should output a JSON 404 when no route is found for a given path', async () => { | ||
it('Should output a 404 when no route is found for a given path', async () => { | ||
let app = new AppServer(); | ||
app.api(function(){ }); | ||
await app.start(); | ||
let { status, body } = await post('http://localhost/foobar'); | ||
let { status, body } = await post(LOCAL_HOST + 'foobar'); | ||
assert.strictEqual(status, 404); | ||
assert.strictEqual(body, ''); | ||
await app.stop(); | ||
}); | ||
it('Should output a JSON when the error message is an object', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
post('/foobar', ({ error }) => { | ||
error('NotFound', { foo: 'bar' }); | ||
}); | ||
}); | ||
await app.start(); | ||
let { body } = await post(LOCAL_HOST + 'foobar'); | ||
assert.doesNotThrow( () => JSON.parse(body) ); | ||
@@ -361,2 +439,42 @@ await app.stop(); | ||
describe('Accept setter', () => { | ||
const { accept } = require('../lib/parse-types'); | ||
it('Should reject unwanted content-types for the given route', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
let acc = accept([ 'urlencoded', 'text/html' ]); | ||
assert(acc.accept.includes('application/x-www-form-urlencoded')); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { body, status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ 'Content-Type': 'application/json' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 400); | ||
assert(/Unsupported/.test(body)); | ||
await app.stop(); | ||
}); | ||
it('Should accept wanted content-types for the given route', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
let acc = accept('text/html'); | ||
assert(acc.accept.includes('text/html')); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ 'Content-Type': 'text/html' }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
}); | ||
}); | ||
@@ -388,3 +506,3 @@ | ||
} }); | ||
let { body } = await get('http://127.0.0.1:80/bar'); | ||
let { body } = await get(LOCAL_HOST + 'bar'); | ||
assert.strictEqual(body, 'foo'); | ||
@@ -442,3 +560,3 @@ await app.stop(); | ||
await app.start(); | ||
let { status: status } = await post('http://localhost:80/unknown'); | ||
let { status: status } = await post(LOCAL_HOST + 'unknown'); | ||
assert.strictEqual(status, 500); | ||
@@ -459,5 +577,5 @@ await app.stop(); | ||
await app.start(); | ||
let { status } = await post('http://localhost:80/known'); | ||
let { status } = await post(LOCAL_HOST + 'known'); | ||
assert.strictEqual(status, 404); | ||
let { status: s2 } = await post('http://localhost:80/unknown'); | ||
let { status: s2 } = await post(LOCAL_HOST + 'unknown'); | ||
assert.strictEqual(s2, 500); | ||
@@ -486,7 +604,7 @@ await app.stop(); | ||
await app.start(); | ||
let { status } = await post('http://localhost:80/known'); | ||
let { status } = await post(LOCAL_HOST + 'known'); | ||
assert.strictEqual(status, 404); | ||
let { status: s2 } = await post('http://localhost:80/unknown'); | ||
let { status: s2 } = await post(LOCAL_HOST + 'unknown'); | ||
assert.strictEqual(s2, 500); | ||
let { status: s3 } = await post('http://localhost:80/unknown/object'); | ||
let { status: s3 } = await post(LOCAL_HOST + 'unknown/object'); | ||
assert.strictEqual(s3, 500); | ||
@@ -512,5 +630,5 @@ await app.stop(); | ||
await app.start(); | ||
let { status } = await post('http://localhost:80/known'); | ||
let { status } = await post(LOCAL_HOST + 'known'); | ||
assert.strictEqual(status, 500); | ||
let { status: s2 } = await post('http://localhost:80/unknown'); | ||
let { status: s2 } = await post(LOCAL_HOST + 'unknown'); | ||
assert.strictEqual(s2, 404); | ||
@@ -532,3 +650,3 @@ assert.strictEqual(count, 2); | ||
await app.start(); | ||
let { status } = await post('http://localhost/unknown'); | ||
let { status } = await post(LOCAL_HOST + 'unknown'); | ||
assert.strictEqual(status, 401); | ||
@@ -563,3 +681,3 @@ await app.stop(); | ||
await app.start(); | ||
await post('http://localhost:80/foo'); | ||
await post(LOCAL_HOST + 'foo'); | ||
let data = await fs.promises.readFile(file, 'utf-8'); | ||
@@ -581,3 +699,3 @@ assert(data.indexOf('logfile') > 0); | ||
await app.start(); | ||
await post('http://localhost:80/foo'); | ||
await post(LOCAL_HOST + 'foo'); | ||
let data = await fs.promises.readFile(file, 'utf-8'); | ||
@@ -596,3 +714,3 @@ assert(data.indexOf('logstream') > 0); | ||
await app.start(); | ||
await post('http://localhost:80/foo'); | ||
await post(LOCAL_HOST + 'foo'); | ||
let data = await fs.promises.readFile(file, 'utf-8'); | ||
@@ -611,3 +729,3 @@ assert(data.indexOf('POST') > 0); | ||
await app.start(); | ||
await post('http://localhost/foo'); | ||
await post(LOCAL_HOST + 'foo'); | ||
let data = await fs.promises.readFile(file, 'utf-8'); | ||
@@ -618,8 +736,7 @@ assert(data.indexOf('Oh yeah') > 0); | ||
it.skip('Should log errors that crach the server process', async () => { | ||
it.skip('Should log errors that crash the server process', async () => { | ||
let file = path.resolve(dir, 'logstream.txt'); | ||
let stream = fs.createWriteStream(file); | ||
let app | ||
await run({ init(){ | ||
app = new AppServer({ log: { stream: stream, level: 'fatal' } }); | ||
new AppServer({ log: { stream: stream, level: 'fatal' } }); | ||
throw new Error('fatality'); | ||
@@ -636,2 +753,3 @@ } }); | ||
const AppServer = require('../lib/app-server'); | ||
const { accept } = require('../lib/parse-types'); | ||
const { post } = require('muhb'); | ||
@@ -651,3 +769,3 @@ | ||
await app.start(); | ||
let { body } = await post('http://localhost:80/foo/baz'); | ||
let { body } = await post(LOCAL_HOST + 'foo/baz'); | ||
assert.strictEqual(body, 'OK'); | ||
@@ -657,3 +775,3 @@ await app.stop(); | ||
it('Should have app name and verison by default', function(){ | ||
it('Should have app name and version by default', function(){ | ||
let doc = new APIDoc(); | ||
@@ -693,2 +811,37 @@ let spec = doc.spec(); | ||
}); | ||
it('Should auto-populate operation with permissive requests body', function(){ | ||
let doc = new APIDoc(); | ||
doc.api( ({ post }) => { | ||
post('/foo', function(){}); | ||
post('/baz', function(){}); | ||
}); | ||
let spec = doc.spec(); | ||
assert.strictEqual(typeof spec.paths['/foo'].post.requestBody, 'object'); | ||
assert('*/*' in spec.paths['/foo'].post.requestBody.content); | ||
}); | ||
it('Should add request body types based on app accepts', function(){ | ||
let doc = new APIDoc(); | ||
doc.api( function({ post }){ | ||
this.accept(['json', 'text/html']); | ||
post('/foo', function(){}); | ||
}); | ||
let spec = doc.spec(); | ||
assert(/following types/.test(spec.paths['/foo'].post.requestBody.description)); | ||
assert('application/json' in spec.paths['/foo'].post.requestBody.content); | ||
assert('text/html' in spec.paths['/foo'].post.requestBody.content); | ||
}); | ||
it('Should add request body types based on route accepts', function(){ | ||
let doc = new APIDoc(); | ||
doc.api( function({ post }){ | ||
let acc = accept('json'); | ||
post('/foo', acc, function(){}); | ||
}); | ||
let spec = doc.spec(); | ||
assert(/following types/.test(spec.paths['/foo'].post.requestBody.description)); | ||
assert('application/json' in spec.paths['/foo'].post.requestBody.content); | ||
}); | ||
}); | ||
@@ -739,3 +892,3 @@ | ||
await app.start(); | ||
let { status } = await post('http://localhost:80/bar'); | ||
let { status } = await post(LOCAL_HOST + 'bar'); | ||
assert.strictEqual(status, 500); | ||
@@ -755,5 +908,5 @@ await app.stop(); | ||
let r1 = JSON.parse((await post('http://localhost:80/bar')).body).message; | ||
let r2 = JSON.parse((await post('http://localhost:80/bar')).body).message; | ||
let r3 = JSON.parse((await post('http://localhost:80/bar')).body).message; | ||
let r1 = (await post(LOCAL_HOST + 'bar')).body; | ||
let r2 = (await post(LOCAL_HOST + 'bar')).body; | ||
let r3 = (await post(LOCAL_HOST + 'bar')).body; | ||
assert(r1 == r2 && r2 == r3 && r3 == 'errfoobar'); | ||
@@ -774,3 +927,3 @@ | ||
let m = JSON.parse((await post('http://localhost:80/bar')).body).message; | ||
let m = (await post(LOCAL_HOST + 'bar')).body; | ||
assert.strictEqual(m, 'NotFound'); | ||
@@ -794,3 +947,3 @@ | ||
await app.start(); | ||
await post('http://localhost:80/bar'); | ||
await post(LOCAL_HOST + 'bar'); | ||
await app.stop(); | ||
@@ -797,0 +950,0 @@ assert(gotHere); |
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
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
93639
35
1647
495
9
21
4
+ Addedhttp-errors@^1.7.3
+ Addedmime@^2.4.4
+ Addeddepd@1.1.2(transitive)
+ Addedhttp-errors@1.8.1(transitive)
+ Addedmime@2.6.0(transitive)
+ Addedstatuses@1.5.0(transitive)
Updatedexpress-fileupload@^1.1.5