From 9d9c2f638d0a77cb6b5bde2411ec00f084488d61 Mon Sep 17 00:00:00 2001 From: Federico Pinna Date: Tue, 21 Mar 2017 15:29:45 +0100 Subject: [PATCH] feat(arrest): Added default and options to the API constructor, more debug options --- package.json | 4 +- src/api.ts | 86 +++++++++++++++++++++++++++--------------- src/debug.ts | 2 + test/api.test.js | 65 ++++++++++++++++++++++++------- test/mongo.test.js | 2 +- test/operation.test.js | 8 ++-- 6 files changed, 116 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index c3378be..0b53d61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "arrest", - "version": "2.3.0-beta.39", + "version": "2.3.0-beta.40", "description": "REST framework for Node.js, Express and MongoDB", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -96,7 +96,7 @@ "path": "node_modules/cz-conventional-changelog" }, "ghooks": { - "_pre-commit": "npm run build && npm run cover && npm run check-coverage" + "pre-commit": "npm run build && npm run cover && npm run check-coverage" } }, "engines": { diff --git a/src/api.ts b/src/api.ts index 0c6d6ea..48b0b6d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -12,15 +12,16 @@ import { SchemaRegistry } from './schema'; import { Swagger } from './swagger'; import { Scopes } from './scopes'; import { Resource } from './resource'; -import {escape} from "querystring"; -const logger = debug('arrest'); +const __logger = Symbol(); +const __options = Symbol(); const __schemas = Symbol(); const __registry = Symbol(); const __resources = Symbol(); const __router = Symbol(); const __default_swagger = { swagger: '2.0', + info: { version: '1.0.0' }, schemes: [ "https", "http" ], consumes: [ "application/json" ], produces: [ "application/json" ], @@ -144,6 +145,9 @@ const __default_schema_tag: Swagger.Tag = { "description": "JSON-Schema definitions used by this API", "x-name-plural": "Schemas" }; +const __default_options: APIOptions = { + swagger: true +} let reqId: number = 0; @@ -158,6 +162,11 @@ export interface APIRequestHandler extends RequestHandler { (req: APIRequest, res: APIResponse, next: NextFunction): any; } +export interface APIOptions { + swagger?: boolean; + registry?: SchemaRegistry; +} + export class API implements Swagger { swagger; info; @@ -175,15 +184,17 @@ export class API implements Swagger { tags?: Swagger.Tag[]; externalDocs?: Swagger.ExternalDocs; - constructor(info:Swagger, registry:SchemaRegistry = new SchemaRegistry()) { - delete info.paths; - delete info.tags; - Object.assign(this, (new Eredita(info, new Eredita(_.cloneDeep(__default_swagger)))).mergePath()); + constructor(info?:Swagger, options?:APIOptions) { + this[__logger] = debug(this.getDebugLabel()); + Object.assign(this, (new Eredita(info || {}, new Eredita(_.cloneDeep(__default_swagger)))).mergePath()); + delete this.paths; + delete this.tags; if (!semver.valid(this.info.version)) { throw new Error('invalid_version'); } + this[__options] = Object.assign({}, (new Eredita(options || {}, new Eredita(_.cloneDeep(__default_options)))).mergePath()); this[__schemas] = null; - this[__registry] = registry; + this[__registry] = this.options.registry || new SchemaRegistry(); this[__resources] = []; } @@ -194,6 +205,16 @@ export class API implements Swagger { return this[__resources]; } + get logger(): Logger { + return this[__logger]; + } + get options(): APIOptions { + return this[__options]; + } + + protected getDebugLabel(): string { + return 'arrest'; + } protected getDebugContext(): string { return `#${++reqId}`; } @@ -273,40 +294,43 @@ export class API implements Swagger { router(options?: RouterOptions): Promise { if (!this[__router]) { + this.logger.info('creating router'); let r = Router(options); r.use((_req: Request, res: Response, next: NextFunction) => { let req: APIRequest = _req as APIRequest; if (!req.logger) { - req.logger = debug('arrest', this.getDebugContext()); + req.logger = debug(this.getDebugLabel(), this.getDebugContext()); } next(); }); r.use(this.securityValidator.bind(this)); - let originalSwagger:Swagger = _.cloneDeep(this) as Swagger; - r.get('/swagger.json', (req: APIRequest, res: APIResponse, next: NextFunction) => { - let out:any = _.cloneDeep(originalSwagger); - if (!req.headers['host']) { - next(API.newError(400, 'Bad Request', 'Missing Host header in the request')); - } else { - out.host = req.headers['host']; - out.basePath = req.baseUrl; - let proto = this.schemes && this.schemes.length ? this.schemes[0] : 'http'; - out.id = proto + '://' + out.host + out.basePath + '/swagger.json#'; - if (originalSwagger.securityDefinitions) { - _.each(originalSwagger.securityDefinitions, (i:any, k) => { - if (k) { - if (i.authorizationUrl) { - out.securityDefinitions[k].authorizationUrl = jr.normalizeUri(i.authorizationUrl, out.id, true); - } - if (i.tokenUrl) { - out.securityDefinitions[k].tokenUrl = jr.normalizeUri(i.tokenUrl, out.id, true); + if (this.options.swagger) { + let originalSwagger:Swagger = _.cloneDeep(this) as Swagger; + r.get('/swagger.json', (req: APIRequest, res: APIResponse, next: NextFunction) => { + let out:any = _.cloneDeep(originalSwagger); + if (!req.headers['host']) { + next(API.newError(400, 'Bad Request', 'Missing Host header in the request')); + } else { + out.host = req.headers['host']; + out.basePath = req.baseUrl; + let proto = this.schemes && this.schemes.length ? this.schemes[0] : 'http'; + out.id = proto + '://' + out.host + out.basePath + '/swagger.json#'; + if (originalSwagger.securityDefinitions) { + _.each(originalSwagger.securityDefinitions, (i:any, k) => { + if (k) { + if (i.authorizationUrl) { + out.securityDefinitions[k].authorizationUrl = jr.normalizeUri(i.authorizationUrl, out.id, true); + } + if (i.tokenUrl) { + out.securityDefinitions[k].tokenUrl = jr.normalizeUri(i.tokenUrl, out.id, true); + } } - } - }); + }); + } + res.json(out); } - res.json(out); - } - }); + }); + } if (this[__schemas]) { r.get('/schemas/:id', (req: APIRequest, res: APIResponse, next: NextFunction) => { let s = this[__schemas][req.params.id]; diff --git a/src/debug.ts b/src/debug.ts index 9c2ee58..ce0351d 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -2,6 +2,7 @@ import * as debug from 'debug'; export interface Logger { log: debug.IDebugger; + info: debug.IDebugger; warn: debug.IDebugger; error: debug.IDebugger; debug: debug.IDebugger; @@ -20,6 +21,7 @@ export default function(label: string, context?: string): Logger { } : debug; return { log: d(label + ':log'), + info: d(label + ':info'), warn: d(label + ':warn'), error: d(label + ':error'), debug: d(label + ':debug') diff --git a/test/api.test.js b/test/api.test.js index eb2edf9..14388ba 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -16,20 +16,17 @@ describe('API', function() { describe('constructor', function() { - it('should fail to create an API instance if no version or an invalid version is specified', function() { - (function() { new API() }).should.throw(Error, /Cannot convert/); - (function() { new API({}) }).should.throw(Error, /Cannot read/); - (function() { new API({ info: { }}) }).should.throw(Error, 'invalid_version'); + it('should fail to create an API instance if an invalid version is specified', function() { (function() { new API({ info: { version: true }}) }).should.throw(Error, 'invalid_version'); (function() { new API({ info: { version: 'a' }}) }).should.throw(Error, 'invalid_version'); (function() { new API({ info: { version: 1 }}) }).should.throw(Error, 'invalid_version'); (function() { new API({ info: { version: '1' }}) }).should.throw(Error, 'invalid_version'); (function() { new API({ info: { version: '1.0' }}) }).should.throw(Error, 'invalid_version'); - (function() { new API({ info: { version: '1.0.0' }}) }).should.not.throw(); + (function() { new API() }).should.not.throw(); }); it('should create an API instance', function() { - let api = new API({ info: { version: '1.0.0' }}); + let api = new API(); api.swagger.should.equal('2.0') api.info.version.should.equal('1.0.0') }); @@ -38,14 +35,14 @@ describe('API', function() { describe('router', function() { it('should return an Expressjs router', function() { - let api = new API({ info: { version: '1.0.0' }}); + let api = new API(); return api.router().then(router => { router.should.be.a('function'); }); }); it('should return the same object when called twice', function() { - let api = new API({ info: { version: '1.0.0' }}); + let api = new API(); return api.router().then(router1 => { return api.router().then(router2 => { router1.should.equal(router2); @@ -57,13 +54,55 @@ describe('API', function() { describe('plain api (no resources)', function() { + describe('swagger disabled', function() { + + const port = 9876; + const host = 'localhost:' + port; + const basePath = 'http://' + host; + const request = supertest(basePath); + const api = new API(null, { swagger: false }); + const app = express(); + let server; + + before(function() { + api.securityDefinitions = { + "access_code": { + "type": "oauth2", + "flow": "accessCode", + "tokenUrl": "token" + }, + "implicit": { + "type": "oauth2", + "flow": "implicit", + "authorizationUrl": "/a/b/authorize" + }, + "": {} + }; + return api.router().then(router => { + app.use(router); + server = app.listen(port); + }); + }); + + after(function() { + server.close(); + }); + + it('should return a the default swagger file', function() { + return request + .get('/swagger.json') + .expect(404); + }); + + }); + describe('/swagger.json', function() { const port = 9876; const host = 'localhost:' + port; const basePath = 'http://' + host; const request = supertest(basePath); - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const app = express(); let server; @@ -184,7 +223,7 @@ describe('API', function() { const host = 'localhost:' + port; const basePath = 'http://' + host; const request = supertest(basePath); - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const app = express(); const schema1 = { a: true, b: 2 }; const schema2 = { c: 'd', e: [] }; @@ -443,7 +482,7 @@ describe('API', function() { const app = express(); let server, spy; - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); class Op1 extends Operation { constructor(resource, path, method) { super('op1', resource, path, method); @@ -569,7 +608,7 @@ describe('API', function() { it('should be able to resolve an internal schema', function() { const spy = chai.spy((req, res) => { res.json({})}); - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); class Op1 extends Operation { constructor(resource, path, method) { super('op1', resource, path, method); @@ -672,7 +711,7 @@ describe('API', function() { it('should be able to resolve an external schema', function() { const spy = chai.spy((req, res) => { res.json({})}); - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); class Op1 extends Operation { constructor(resource, path, method) { super('op1', resource, path, method); diff --git a/test/mongo.test.js b/test/mongo.test.js index 3d311da..7ea2b29 100644 --- a/test/mongo.test.js +++ b/test/mongo.test.js @@ -79,7 +79,7 @@ describe('mongo', function() { const request = supertest(basePath); const db = (new mongo.MongoClient()).connect('mongodb://localhost:27017'); const app = express(); - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const collectionName = 'arrest_test'; class FakeOp1 extends QueryMongoOperation { diff --git a/test/operation.test.js b/test/operation.test.js index 7550e13..a3d7b33 100644 --- a/test/operation.test.js +++ b/test/operation.test.js @@ -134,7 +134,7 @@ describe('Operation', function() { const app = express(); let server; - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const spy = chai.spy(function(req, res) { res.send({ headers: req.headers }); }); @@ -265,7 +265,7 @@ describe('Operation', function() { const app = express(); let server; - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const spy = chai.spy(function(req, res) { res.send({ params: req.params }); }); @@ -366,7 +366,7 @@ describe('Operation', function() { const app = express(); let server; - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const spy = chai.spy(function(req, res) { res.send({ query: req.query }); }); @@ -488,7 +488,7 @@ describe('Operation', function() { const app = express(); let server; - const api = new API({ info: { version: '1.0.0' }}); + const api = new API(); const spy = chai.spy(function(req, res) { res.send({ body: req.body }); });