Comparing version 0.7.0 to 0.7.1
@@ -8,2 +8,16 @@ # Nodecaf Changelog | ||
## [v0.7.1] - 2019-07-08 | ||
### Added | ||
- a function to apply config files on top of each other | ||
### Fixed | ||
- type filtering to not require content-type from requests without body | ||
### Changed | ||
- parser to ignore request body when no content-length is present | ||
### Removed | ||
- the settings argument that used to be passed to user init function | ||
## [v0.7.0] - 2019-07-07 | ||
@@ -144,1 +158,2 @@ | ||
[v0.7.0]: https://gitlab.com/GCSBOSS/nodecaf/-/tags/v0.7.0 | ||
[v0.7.1]: https://gitlab.com/GCSBOSS/nodecaf/-/tags/v0.7.1s |
@@ -9,3 +9,4 @@ const fs = require('fs'); | ||
const { parseTypes } = require('./parse-types'); | ||
const setupLogger = require('./logger'); | ||
const { setupLogger, logRequest } = require('./logger'); | ||
const loadConf = require('./conf-loader'); | ||
const errors = require('./errors'); | ||
@@ -20,2 +21,8 @@ const HTTP_VERBS = ['get', 'post', 'patch', 'put', 'head']; | ||
function noAPI(req, res){ | ||
res.statusCode = 404; | ||
res.write('This Nodecaf server has no API setup.'); | ||
res.end(); | ||
} | ||
/* o\ | ||
@@ -28,18 +35,9 @@ Application Server to be instanced by users. Contain the basic REST | ||
constructor(settings){ | ||
this.settings = settings || {}; | ||
this.afterStop = this.beforeStart = this.onRouteError = noop; | ||
this.exposed = {}; | ||
this.name = this.settings.name || 'express'; | ||
this.version = this.settings.version || '0.0.0'; | ||
this.express = express(); | ||
this.express.app = this; | ||
this.server = null; | ||
this.accepts = false; | ||
this.express = noAPI; | ||
this.setup(settings); | ||
// Setup logger. | ||
setupLogger.bind(this)(this.settings.log); | ||
// Prepare Express middleware. | ||
this.express.use(compression()); | ||
// Create adapted versions of all Express routing methods. | ||
@@ -54,2 +52,17 @@ this.routerFuncs = HTTP_VERBS.reduce( (o, method) => | ||
setup(objectOrPath, type){ | ||
this.settings = this.settings || {}; | ||
if(typeof objectOrPath == 'string') | ||
objectOrPath = loadConf(objectOrPath, type); | ||
this.settings = { ...this.settings, ...objectOrPath }; | ||
this.name = this.settings.name || 'express'; | ||
this.version = this.settings.version || '0.0.0'; | ||
// Setup logger. | ||
setupLogger.bind(this)(this.settings.log); | ||
} | ||
/* o\ | ||
@@ -70,2 +83,8 @@ Define a whitelist of accepted request body mime-types for all routes | ||
api(callback){ | ||
// Prepare Express and middleware. | ||
this.express = express(); | ||
this.express.app = this; | ||
this.express.use(compression()); | ||
this.express.use(logRequest.bind(this)); | ||
callback.bind(this)(this.routerFuncs); | ||
@@ -72,0 +91,0 @@ this.express.use(routeNotFoundHandler); |
@@ -37,3 +37,4 @@ const path = require('path'); | ||
let settings = loadConf(input.confType, input.confPath || false); | ||
if(input.confPath) | ||
var settings = loadConf(input.confPath, input.confType); | ||
@@ -40,0 +41,0 @@ let doc = new APIDoc(settings); |
@@ -5,3 +5,2 @@ | ||
const YAML = require('yaml'); | ||
const path = require('path'); | ||
@@ -13,8 +12,7 @@ const loaders = { | ||
module.exports = function(type, conf){ | ||
conf = conf || process.env.CONF_FILE || false; | ||
module.exports = function loadConf(conf, type){ | ||
type = type || 'toml'; | ||
// Bale if not conf file is informed. | ||
if(!conf) | ||
return {}; | ||
if(typeof loaders[type] !== 'function') | ||
throw new Error('Conf type not supported: ' + type); | ||
@@ -25,8 +23,5 @@ // Attempt loading said conf file. | ||
} | ||
// DOES NOT throw exception if conf file is not found. | ||
catch(e){ | ||
console.log('Could not read conf file at: ' + path.resolve(conf)); | ||
return {}; | ||
throw new Error('Couldn\'t open conf file: ' + conf); | ||
} | ||
} |
const bunyan = require('bunyan'); | ||
module.exports = function setupLogger(conf){ | ||
conf = conf || {}; | ||
let level = conf.level || bunyan.INFO; | ||
module.exports = { | ||
// Prepare logger instance. | ||
this.log = bunyan.createLogger({ | ||
name: this.name, | ||
serializers: bunyan.stdSerializers | ||
}); | ||
setupLogger(conf){ | ||
conf = conf || {}; | ||
let level = conf.level || bunyan.INFO; | ||
// Desable logging if no output method is defined. | ||
if(!conf.file && !conf.stream){ | ||
this.log.level(bunyan.FATAL + 1); | ||
return; | ||
} | ||
// Prepare logger instance. | ||
this.log = bunyan.createLogger({ | ||
name: this.name, | ||
serializers: bunyan.stdSerializers | ||
}); | ||
// Remove default stdout stream. | ||
this.log.streams.shift(); | ||
// Desable logging if no output method is defined. | ||
if(!conf.file && !conf.stream){ | ||
this.log.level(bunyan.FATAL + 1); | ||
return; | ||
} | ||
// if 'stream' is set. | ||
if(conf.stream) | ||
this.log.addStream({ stream: conf.stream, level: level }); | ||
// Remove default stdout stream. | ||
this.log.streams.shift(); | ||
// If 'file' is set. | ||
else | ||
this.log.addStream({ path: conf.file, level: level }); | ||
// if 'stream' is set. | ||
if(conf.stream) | ||
this.log.addStream({ stream: conf.stream, level: level }); | ||
// Setup logging for every request. | ||
this.express.use((req, res, next) => { | ||
// If 'file' is set. | ||
else | ||
this.log.addStream({ path: conf.file, level: level }); | ||
}, | ||
logRequest(req, res, next){ | ||
this.log.debug({ req: req }, 'REQUEST'); | ||
next(); | ||
}); | ||
} | ||
} |
@@ -18,3 +18,6 @@ const os = require('os'); | ||
// this => app | ||
if(!custom && !this.accepts) | ||
let filter = custom || this.accepts || false; | ||
if(!filter || !req.headers['content-length']) | ||
return next(); | ||
@@ -26,4 +29,3 @@ | ||
let arr = custom || this.accepts; | ||
if(!arr.includes(ct)) | ||
if(!filter.includes(ct)) | ||
return next(errors.BadRequest('Unsupported content type \'' + ct + '\'')); | ||
@@ -35,2 +37,5 @@ | ||
async function parse(req, res, next){ | ||
req.body = ''; | ||
if(!req.headers['content-length']) | ||
return next(); | ||
@@ -41,3 +46,3 @@ try{ | ||
catch(e){ | ||
ct = { type: 'plain/text', parameters: { charset: 'utf8' } }; | ||
ct = { type: 'text/plain', parameters: { charset: 'utf8' } }; | ||
} | ||
@@ -50,3 +55,3 @@ | ||
length: req.headers['content-length'], | ||
encoding: ct.parameters.charset | ||
encoding: ct.parameters.charset || 'utf8' | ||
}); | ||
@@ -53,0 +58,0 @@ next(); |
const assert = require('assert'); | ||
const AppServer = require('./app-server'); | ||
const loadConf = require('./conf-loader'); | ||
@@ -29,8 +28,9 @@ /* istanbul ignore next */ | ||
// Load conf and inputs it on the app class. | ||
let settings = loadConf(confType || 'toml', confPath || false); | ||
let app = init(settings); | ||
let app = init(); | ||
assert(app instanceof AppServer); | ||
if(confPath) | ||
app.setup(confPath, confType); | ||
// Handle signals. | ||
let debug = settings.debug || false; | ||
let debug = app.settings.debug || false; | ||
process.on('SIGINT', term.bind(null, app, debug)); | ||
@@ -37,0 +37,0 @@ process.on('SIGTERM', term.bind(null, app, debug)); |
{ | ||
"name": "nodecaf", | ||
"version": "0.7.0", | ||
"version": "0.7.1", | ||
"description": "Nodecaf is an Express framework for developing REST APIs in a quick and convenient manner.", | ||
@@ -53,4 +53,5 @@ "main": "lib/main.js", | ||
"swagger-parser": "^7.0.1", | ||
"tempper": "^0.1.0", | ||
"wtfnode": "^0.8.0" | ||
} | ||
} |
@@ -44,4 +44,4 @@ # [Nodecaf](https://gitlab.com/GCSBOSS/nodecaf) | ||
module.exports = function init(conf){ | ||
let app = new AppServer(conf); | ||
module.exports = function init(){ | ||
let app = new AppServer(); | ||
@@ -160,12 +160,12 @@ // Expose things to all routes putting them in the 'shared' object. | ||
The data in the config file can be accessed in `lib/main.js` through the first | ||
parameter of the exported `init` function: | ||
The config data can be passed as an object to the app constructor in `lib/main.js`: | ||
```js | ||
module.exports = function init(conf){ | ||
console.log(conf); | ||
module.exports = function init(){ | ||
let conf = { key: 'value' }; | ||
let app = new AppServer(conf); | ||
} | ||
``` | ||
You can also use the config data through [it's handler arg](#handler-args) in | ||
You can use the config data through [it's handler arg](#handler-args) in | ||
all route handlers as follows: | ||
@@ -175,6 +175,24 @@ | ||
post('/foo', function({ conf }){ | ||
console.log(conf.myField); | ||
console.log(conf.key); //=> 'value' | ||
}); | ||
``` | ||
#### Layered Configs | ||
You can also use the `app.setup` to add a given configuration | ||
file or object on top of the current one as follows: | ||
```js | ||
app.setup('/path/to/settings.toml'); | ||
app.setup('/path/to/settings.yaml', 'yaml'); | ||
app.setup({ key: 'value' }); | ||
app.setup({ key: 'new-value', foo: 'bar' }); | ||
``` | ||
Layering is useful, for example, to keep a **default** settings file in your server | ||
source code to be overwritten by your user's. | ||
### Logging | ||
@@ -181,0 +199,0 @@ |
//const wtf = require('wtfnode'); | ||
const assert = require('assert'); | ||
const Tempper = require('tempper'); | ||
const fs = require('fs'); | ||
const os = require('os'); | ||
const assertPathExists = p => fs.existsSync(p); | ||
const assertPathExists = p => assert(fs.existsSync(p)); | ||
describe('CLI: nodecaf', () => { | ||
var resDir, projDir; | ||
var tmp; | ||
before(function(){ | ||
projDir = process.cwd(); | ||
process.chdir('./test'); | ||
resDir = process.cwd() + '/res/'; | ||
tmp = new Tempper(); | ||
}); | ||
after(function(){ | ||
process.chdir(projDir); | ||
tmp.clear(); | ||
process.chdir('..'); | ||
}); | ||
@@ -24,13 +23,9 @@ | ||
const init = require('../lib/cli/init'); | ||
let tdir; | ||
beforeEach(function(){ | ||
let suffix = Math.random() * 1e3; | ||
tdir = os.tmpdir + '/' + String(new Date()).replace(/\D/g, '') + suffix + '/'; | ||
fs.mkdirSync(tdir); | ||
process.chdir(tdir); | ||
afterEach(function(){ | ||
tmp.refresh(); | ||
}); | ||
it('Should fail when unsupported conf type is sent', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
assert.throws( () => | ||
@@ -45,7 +40,7 @@ init({ confPath: 'foo', confType: 'baz' }), /type not supported/g ); | ||
it('Should fail when \'lib\' or \'bin\' directories already exist', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
fs.mkdirSync('./bin'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
tmp.mkdir('bin'); | ||
assert.throws( () => init({}), /already exists/g); | ||
fs.rmdirSync('./bin'); | ||
fs.mkdirSync('./lib'); | ||
tmp.mkdir('lib'); | ||
assert.throws( () => init({}), /already exists/g); | ||
@@ -55,3 +50,3 @@ }); | ||
it('Should generate basic structure files', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
init({}); | ||
@@ -61,3 +56,3 @@ assertPathExists('./bin/my-proj.js'); | ||
assertPathExists('./lib/api.js'); | ||
let pkgInfo = require(tdir + 'package.json'); | ||
let pkgInfo = require(tmp.dir + '/package.json'); | ||
assert.equal(pkgInfo.bin['my-proj'], 'bin/my-proj.js'); | ||
@@ -67,8 +62,8 @@ }); | ||
it('Should target specified directory', () => { | ||
fs.mkdirSync('./foo'); | ||
fs.copyFileSync(resDir + 'nmless-package.json', './foo/package.json'); | ||
tmp.mkdir('foo'); | ||
tmp.addFile('res/nmless-package.json', './foo/package.json'); | ||
const cli = require('cli'); | ||
cli.setArgv(['thing', '-p', './foo']); | ||
init(); | ||
let pkgInfo = require(tdir + 'foo/package.json'); | ||
let pkgInfo = require(tmp.dir + '/foo/package.json'); | ||
assert.equal(pkgInfo.bin['my-app'], 'bin/my-app.js'); | ||
@@ -78,5 +73,5 @@ }); | ||
it('Should use specified project name', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
init({ name: 'proj-foo' }); | ||
let pkgInfo = require(tdir + 'package.json'); | ||
let pkgInfo = require(tmp.dir + '/package.json'); | ||
assert.equal(pkgInfo.bin['proj-foo'], 'bin/proj-foo.js'); | ||
@@ -86,3 +81,3 @@ }); | ||
it('Should generate conf file if specified', () => { | ||
fs.copyFileSync(resDir + 'nmless-package.json', './package.json'); | ||
tmp.addFile('res/nmless-package.json', './package.json'); | ||
init({ confPath: './conf.toml' }); | ||
@@ -93,3 +88,3 @@ assertPathExists('./conf.toml'); | ||
it('Should generate create conf file dir if it doesn\'t exist', () => { | ||
fs.copyFileSync(resDir + 'nmless-package.json', './package.json'); | ||
tmp.addFile('res/nmless-package.json', './package.json'); | ||
init({ confPath: './my/conf.toml' }); | ||
@@ -103,9 +98,5 @@ assertPathExists('./my/conf.toml'); | ||
const SwaggerParser = require('swagger-parser'); | ||
let tdir; | ||
beforeEach(function(){ | ||
let suffix = Math.random() * 1e3; | ||
tdir = os.tmpdir + '/' + String(new Date()).replace(/\D/g, '') + suffix + '/'; | ||
fs.mkdirSync(tdir); | ||
process.chdir(tdir); | ||
afterEach(function(){ | ||
tmp.refresh(); | ||
}); | ||
@@ -118,3 +109,3 @@ | ||
it('Should fail when no API file is found', () => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
const cli = require('cli'); | ||
@@ -127,6 +118,7 @@ cli.setArgv(['thing']); | ||
it('Should output a well formed JSON API doc to default file', done => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
fs.copyFileSync(resDir + 'api.js', './api.js'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
tmp.addFile('res/api.js', './api.js'); | ||
tmp.addFile('res/conf.toml', './conf.toml'); | ||
openapi({ apiPath: './api.js' }); | ||
openapi({ apiPath: './api.js', confPath: './conf.toml' }); | ||
@@ -137,4 +129,4 @@ SwaggerParser.validate('./output.json', done); | ||
it('Should output a well formed YAML API doc to given file', done => { | ||
fs.copyFileSync(resDir + 'test-package.json', './package.json'); | ||
fs.copyFileSync(resDir + 'api.js', './api.js'); | ||
tmp.addFile('res/test-package.json', './package.json'); | ||
tmp.addFile('res/api.js', './api.js'); | ||
@@ -141,0 +133,0 @@ openapi({ apiPath: './api.js', outFile: './outfile.yml' }); |
@@ -10,14 +10,16 @@ //const wtf = require('wtfnode'); | ||
it('Should NOT fail if no conf file is specified', () => { | ||
let obj = loadConf('toml'); | ||
assert.deepEqual(obj, {}); | ||
it('Should fail if no conf file is specified', () => { | ||
assert.throws( () => loadConf() ); | ||
}); | ||
it('Should NOT fail if conf file is not found', () => { | ||
let obj = loadConf('toml', './bla'); | ||
assert.deepEqual(obj, {}); | ||
it('Should fail if conf file is not found', () => { | ||
assert.throws( () => loadConf('./bla') ); | ||
}); | ||
it('Should fail if given conf type is not supported', () => { | ||
assert.throws( () => loadConf('./test/res/conf.xml', 'xml'), /type not supported/ ); | ||
}); | ||
it('Should properly load a TOML file and generate an object', () => { | ||
let obj = loadConf('toml', './test/res/conf.toml'); | ||
let obj = loadConf('./test/res/conf.toml'); | ||
assert.strictEqual(obj.key, 'value'); | ||
@@ -27,3 +29,3 @@ }); | ||
it('Should properly load an YAML file and generate an object', () => { | ||
let obj = loadConf('yaml', './test/res/conf.yaml'); | ||
let obj = loadConf('./test/res/conf.yaml', 'yaml'); | ||
assert.strictEqual(obj.key, 'value'); | ||
@@ -67,7 +69,2 @@ }); | ||
it('Should create the Express server', () => { | ||
let app = new AppServer(); | ||
assert.strictEqual(typeof app.express.use, 'function'); | ||
}); | ||
}); | ||
@@ -243,3 +240,3 @@ | ||
LOCAL_HOST + 'foo', | ||
{ '--no-auto': true }, | ||
{ '--no-auto': true, 'Content-Length': 13 }, | ||
'{"foo":"bar"}' | ||
@@ -268,4 +265,36 @@ ); | ||
it('Should accept requests without body payload', 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', | ||
{ '--no-auto': true } | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
}); | ||
describe('::setup', () => { | ||
it('Should apply settings on top of existing one', () => { | ||
let app = new AppServer({ key: 'value' }); | ||
app.setup({ key: 'value2', key2: 'value' }); | ||
assert.strictEqual(app.settings.key, 'value2'); | ||
assert.strictEqual(app.settings.key2, 'value'); | ||
}); | ||
it('Should load form file when path is sent', () => { | ||
let app = new AppServer({ key: 'valueOld' }); | ||
app.setup('test/res/conf.toml'); | ||
assert.strictEqual(app.settings.key, 'value'); | ||
}); | ||
}); | ||
}); | ||
@@ -364,3 +393,3 @@ | ||
LOCAL_HOST + 'foobar', | ||
{ '--no-auto': true }, | ||
{ '--no-auto': true, 'Content-Length': 13 }, | ||
JSON.stringify({foo: 'bar'}) | ||
@@ -465,2 +494,18 @@ ); | ||
it('Should accept requests without a body payload', async () => { | ||
let app = new AppServer(); | ||
app.api(function({ post }){ | ||
let acc = accept('text/html'); | ||
post('/foo', acc, ({ res }) => res.end()); | ||
}); | ||
await app.start(); | ||
let { status } = await post( | ||
LOCAL_HOST + 'foo', | ||
{ '--no-auto': true }, | ||
'{"foo":"bar"}' | ||
); | ||
assert.strictEqual(status, 200); | ||
await app.stop(); | ||
}); | ||
}); | ||
@@ -486,4 +531,3 @@ | ||
let app | ||
await run({ init(settings){ | ||
assert(typeof settings == 'object'); | ||
await run({ init(){ | ||
app = new AppServer(); | ||
@@ -500,2 +544,16 @@ app.api(function({ get }){ | ||
it('Should inject the given conf file', async () => { | ||
let app | ||
await run({ init(){ | ||
app = new AppServer(); | ||
app.api(function({ get }){ | ||
get('/bar', ({ res, conf }) => res.end(conf.key)); | ||
}); | ||
return app; | ||
}, confPath: 'test/res/conf.toml' }); | ||
let { body } = await get(LOCAL_HOST + 'bar'); | ||
assert.strictEqual(body, 'value'); | ||
await app.stop(); | ||
}); | ||
}); | ||
@@ -502,0 +560,0 @@ |
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
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
96697
1706
513
20
5