From f645c6db0dadbf941a149e421ec11e4876bb82e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 7 Oct 2014 09:19:44 +0200 Subject: [PATCH 01/53] swagger: allow cross-origin requests Add CORS middleware to the swagger app. Add a configuration option allowing developers to disable CORS. --- lib/swagger.js | 14 ++++++++++++-- package.json | 5 +++-- test/swagger.test.js | 26 ++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index bcf3e07..7b5644c 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -12,6 +12,7 @@ var urlJoin = require('./url-join'); var _defaults = require('lodash.defaults'); var classHelper = require('./class-helper'); var routeHelper = require('./route-helper'); +var cors = require('cors'); /** * Create a remotable Swagger module for plugging into `RemoteObjects`. @@ -38,6 +39,8 @@ function Swagger(loopbackApplication, swaggerApp, opts) { var routes = adapter.allRoutes(); var classes = remotes.classes(); + setupCors(swaggerApp, remotes); + // These are the docs we will be sending from the /swagger endpoints. var resourceDoc = generateResourceDoc(opts); var apiDocs = {}; @@ -70,13 +73,20 @@ function Swagger(loopbackApplication, swaggerApp, opts) { }); /** - * The topmost Swagger resource is a description of all (non-Swagger) - * resources available on the system, and where to find more + * The topmost Swagger resource is a description of all (non-Swagger) + * resources available on the system, and where to find more * information about them. */ addRoute(swaggerApp, opts.resourcePath, resourceDoc, opts); } +function setupCors(swaggerApp, remotes) { + var corsOptions = remotes.options && remotes.options.cors || + { origin: true, credentials: true }; + + swaggerApp.use(cors(corsOptions)); +} + /** * Add a route to this remoting extension. * @param {Application} app Express application. diff --git a/package.json b/package.json index e1cde4a..a8e43fc 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,11 @@ "url": "https://github.com/strongloop/loopback-explorer/blob/master/LICENSE" }, "dependencies": { - "swagger-ui": "~2.0.18", + "cors": "^2.4.2", "debug": "~1.0.3", + "express": "3.x", "lodash.clonedeep": "^2.4.1", "lodash.defaults": "^2.4.1", - "express": "3.x" + "swagger-ui": "~2.0.18" } } diff --git a/test/swagger.test.js b/test/swagger.test.js index 1cd0a5b..b07f0bb 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -106,6 +106,31 @@ describe('swagger definition', function() { }); }); + describe('Cross-origin resource sharing', function() { + it('allows cross-origin requests by default', function(done) { + var app = mountSwagger(); + request(app) + .options('/explorer/resources') + .set('Origin', 'http://example.com/') + .expect('Access-Control-Allow-Origin', /^http:\/\/example.com\/|\*/) + .expect('Access-Control-Allow-Methods', /\bGET\b/) + .end(done); + }); + + it('can be disabled by configuration', function(done) { + var app = mountSwagger({}, { remoting: { cors: { origin: false } } }); + request(app) + .options('/explorer/resources') + .end(function(err, res) { + if (err) return done(err); + var allowOrigin = res.get('Access-Control-Allow-Origin'); + expect(allowOrigin, 'Access-Control-Allow-Origin') + .to.equal(undefined); + done(); + }); + }); + }); + function getSwaggerResources(app, restPath, classPath) { return request(app) .get(urlJoin(restPath || '/explorer', '/resources', classPath || '')) @@ -122,6 +147,7 @@ describe('swagger definition', function() { addlOptions = addlOptions || {}; var app = createLoopbackAppWithModel(addlOptions.apiRoot); var swaggerApp = express(); + if (addlOptions.remoting) app.set('remoting', addlOptions.remoting); swagger(app, swaggerApp, options); app.use(addlOptions.explorerRoot || '/explorer', swaggerApp); return app; From 546a122eaa7a8cd5fd39f03e5924f785ab44c1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 8 Oct 2014 19:22:45 +0200 Subject: [PATCH 02/53] 1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a8e43fc..fea26bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.2.11", + "version": "1.3.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 37179f3e61352e56d4114bf0f1c7f1a410971304 Mon Sep 17 00:00:00 2001 From: Shelby Sanders Date: Wed, 6 Aug 2014 23:52:59 -0700 Subject: [PATCH 03/53] Pull model description from ctor.settings first --- lib/class-helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/class-helper.js b/lib/class-helper.js index 333b5ea..860e008 100644 --- a/lib/class-helper.js +++ b/lib/class-helper.js @@ -43,7 +43,8 @@ var classHelper = module.exports = { generateResourceDocAPIEntry: function(aClass) { return { path: aClass.http.path, - description: aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description + description: aClass.ctor.settings.description || + aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description }; } }; From 4d0e711087a9b9ad628a13ec8d003ac7aca37bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 13 Oct 2014 16:32:12 +0200 Subject: [PATCH 04/53] route-helper: include `notes` and `deprecated` --- lib/route-helper.js | 5 +++-- test/route-helper.test.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index 263bbd2..b47b850 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -137,8 +137,9 @@ var routeHelper = module.exports = { parameters: accepts, // TODO(schoon) - We don't have descriptions for this yet. responseMessages: [], - summary: route.description, // TODO(schoon) - Excerpt? - notes: '' // TODO(schoon) - `description` metadata? + summary: route.description, + notes: route.notes, + deprecated: route.deprecated })] }; diff --git a/test/route-helper.test.js b/test/route-helper.test.js index 396007c..a25d993 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -75,6 +75,19 @@ describe('route-helper', function() { expect(opDoc.format).to.equal('byte'); }); + it('includes `notes` metadata', function() { + var doc = createAPIDoc({ + notes: 'some notes' + }); + expect(doc.operations[0].notes).to.equal('some notes'); + }); + + it('includes `deprecated` metadata', function() { + var doc = createAPIDoc({ + deprecated: 'true' + }); + expect(doc.operations[0].deprecated).to.equal('true'); + }); }); // Easy wrapper around createRoute From 622f6176f39203f987b5072e0fb902ae983be7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 13 Oct 2014 17:24:58 +0200 Subject: [PATCH 05/53] Extend `consumes` and `produces` metadata - Include XML content-types for both input and output - Include JSONP (javascript) content-types for output --- lib/swagger.js | 13 +++++++++++-- test/swagger.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 7b5644c..f570500 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -28,8 +28,17 @@ function Swagger(loopbackApplication, swaggerApp, opts) { basePath: loopbackApplication.get('restApiRoot') || '/api', resourcePath: 'resources', // Default consumes/produces - consumes: ['application/json', 'application/x-www-form-urlencoded'], - produces: ['application/json'], + consumes: [ + 'application/json', + 'application/x-www-form-urlencoded', + 'application/xml', 'text/xml' + ], + produces: [ + 'application/json', + 'application/xml', 'text/xml', + // JSONP content types + 'application/javascript', 'text/javascript' + ], version: getVersion() }); diff --git a/test/swagger.test.js b/test/swagger.test.js index b07f0bb..c6f0987 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -104,6 +104,33 @@ describe('swagger definition', function() { done(); }); }); + + it('includes `consumes`', function(done) { + var app = mountSwagger(); + getAPIDeclaration(app, 'products').end(function(err, res) { + if (err) return done(err); + expect(res.body.consumes).to.have.members([ + 'application/json', + 'application/x-www-form-urlencoded', + 'application/xml', 'text/xml' + ]); + done(); + }); + }); + + it('includes `produces`', function(done) { + var app = mountSwagger(); + getAPIDeclaration(app, 'products').end(function(err, res) { + if (err) return done(err); + expect(res.body.produces).to.have.members([ + 'application/json', + 'application/xml', 'text/xml', + // JSONP content types + 'application/javascript', 'text/javascript' + ]); + done(); + }); + }); }); describe('Cross-origin resource sharing', function() { From be36f116298fdc7bdf4aef5c3e5462c4a53db083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 13 Oct 2014 17:27:52 +0200 Subject: [PATCH 06/53] Use `1.0.0` as the default app version. Change the default version number returned when the version number cannot be read from `package.json` in CWD. --- lib/swagger.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index f570500..0edd55f 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -110,17 +110,17 @@ function addRoute(app, uri, doc, opts) { app.get(urlJoin('/', uri), function(req, res) { // There's a few forces at play that require this "hack". The Swagger spec - // requires a `basePath` to be set in the API descriptions. However, we - // can't guarantee this path is either reachable or desirable if it's set + // requires a `basePath` to be set in the API descriptions. However, we + // can't guarantee this path is either reachable or desirable if it's set // as a part of the options. - // + // // The simplest way around this is to reflect the value of the `Host` HTTP // header as the `basePath`. Because we pre-build the Swagger data, we don't // know that header at the time the data is built. if (hasBasePath) { var headers = req.headers; var host = headers.Host || headers.host; - doc.basePath = (opts.protocol || req.protocol) + '://' + + doc.basePath = (opts.protocol || req.protocol) + '://' + host + initialPath; } res.status(200).send(doc); @@ -156,7 +156,7 @@ function getVersion() { try { version = require(path.join(process.cwd(), 'package.json')).version; } catch(e) { - version = ''; + version = '1.0.0'; } return version; } From 705776517bc3e1b5412183e22e5fd0e5b1782c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 13 Oct 2014 19:32:09 +0200 Subject: [PATCH 07/53] Support multi-line array `description` and `notes` When a string value is expected and the user supplied an array, convert the value to a single string by joining all array items. --- lib/class-helper.js | 7 +++++-- lib/route-helper.js | 18 +++++++++++++++--- test/class-helper.test.js | 23 +++++++++++++++++++++++ test/route-helper.test.js | 21 +++++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 test/class-helper.test.js diff --git a/lib/class-helper.js b/lib/class-helper.js index 860e008..1e9c128 100644 --- a/lib/class-helper.js +++ b/lib/class-helper.js @@ -4,6 +4,7 @@ * Module dependencies. */ var modelHelper = require('./model-helper'); +var routeHelper = require('./route-helper'); var urlJoin = require('./url-join'); /** @@ -41,10 +42,12 @@ var classHelper = module.exports = { * @return {Object} API declaration reference. */ generateResourceDocAPIEntry: function(aClass) { + var description = aClass.ctor.settings.description || + aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description; + return { path: aClass.http.path, - description: aClass.ctor.settings.description || - aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description + description: routeHelper.convertText(description) }; } }; diff --git a/lib/route-helper.js b/lib/route-helper.js index b47b850..6cf5b4a 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -137,8 +137,8 @@ var routeHelper = module.exports = { parameters: accepts, // TODO(schoon) - We don't have descriptions for this yet. responseMessages: [], - summary: route.description, - notes: route.notes, + summary: routeHelper.convertText(route.description), + notes: routeHelper.convertText(route.notes), deprecated: route.deprecated })] }; @@ -195,7 +195,7 @@ var routeHelper = module.exports = { var out = { paramType: paramType || type, name: name, - description: accepts.description, + description: routeHelper.convertText(accepts.description), type: accepts.type, required: !!accepts.required, defaultValue: accepts.defaultValue, @@ -234,6 +234,18 @@ var routeHelper = module.exports = { obj[key] = typeDesc[key]; }); return obj; + }, + + /** + * Convert a text value that can be expressed either as a string or + * as an array of strings. + * @param {string|Array} value + * @returns {string} + */ + convertText: function(value) { + if (Array.isArray(value)) + return value.join('\n'); + return value; } }; diff --git a/test/class-helper.test.js b/test/class-helper.test.js new file mode 100644 index 0000000..beb6d8f --- /dev/null +++ b/test/class-helper.test.js @@ -0,0 +1,23 @@ +'use strict'; + +var classHelper = require('../lib/class-helper'); +var expect = require('chai').expect; +var _defaults = require('lodash.defaults'); + +describe('class-helper', function() { + it('joins array descriptions', function() { + var doc = generateResourceDocAPIEntry({ + ctor: { settings: { description: [ 'line1', 'line2' ] } } + }); + + expect(doc.description).to.equal('line1\nline2'); + }); +}); + +// Easy wrapper around createRoute +function generateResourceDocAPIEntry(def) { + return classHelper.generateResourceDocAPIEntry(_defaults(def, { + http: { path: '/test' }, + ctor: { settings: { } }, + })); +} diff --git a/test/route-helper.test.js b/test/route-helper.test.js index a25d993..52e21b8 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -88,6 +88,27 @@ describe('route-helper', function() { }); expect(doc.operations[0].deprecated).to.equal('true'); }); + + it('joins array description/summary', function() { + var doc = createAPIDoc({ + description: [ 'line1', 'line2' ] + }); + expect(doc.operations[0].summary).to.equal('line1\nline2'); + }); + + it('joins array notes', function() { + var doc = createAPIDoc({ + notes: [ 'line1', 'line2' ] + }); + expect(doc.operations[0].notes).to.equal('line1\nline2'); + }); + + it('joins array description/summary of an input arg', function() { + var doc = createAPIDoc({ + accepts: [{ name: 'arg', description: [ 'line1', 'line2' ] }] + }); + expect(doc.operations[0].parameters[0].description).to.equal('line1\nline2'); + }); }); // Easy wrapper around createRoute From 347e1f045e28408389ba600a73a5ae6f83cc6f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 08:19:12 +0200 Subject: [PATCH 08/53] gitignore: add .idea, *.tgz, *.iml --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a72b52e..a9578a3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ lib-cov *.out *.pid *.gz +.idea +*.iml +*.tgz pids logs From c31c89a29ad8f2fd580a7eebd795a5e4d23a15cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 14:15:17 +0200 Subject: [PATCH 09/53] package: update devDependencies - loopback 1.x to ^2.4.1 - mocha ~1.20.1 to ^1.21.5 - supertest ~0.13.0 to ~0.14.0 --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index fea26bc..0543493 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "url": "https://github.com/strongloop/loopback-explorer/issues" }, "devDependencies": { - "loopback": "1.x", - "mocha": "~1.20.1", - "supertest": "~0.13.0", - "chai": "~1.9.1" + "loopback": "^2.4.1", + "mocha": "^1.21.5", + "supertest": "~0.14.0", + "chai": "^1.9.1" }, "license": { "name": "Dual MIT/StrongLoop", From 856d34b9d433fd20dd23bc5c9d37bf2c258eac20 Mon Sep 17 00:00:00 2001 From: Krishna Raman Date: Mon, 13 Oct 2014 18:57:39 -0700 Subject: [PATCH 10/53] Add support for `context` and `res` param types --- lib/route-helper.js | 7 ++++++- test/route-helper.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index 6cf5b4a..f52ed4c 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -61,7 +61,12 @@ var routeHelper = module.exports = { if (typeof arg.http === 'function') return false; // Don't show arguments set to the incoming http request. // Please note that body needs to be shown, such as User.create(). - if (arg.http.source === 'req') return false; + if (arg.http.source === 'req' || + arg.http.source === 'res' || + arg.http.source === 'context') { + + return false; + } return true; }); diff --git a/test/route-helper.test.js b/test/route-helper.test.js index 52e21b8..6714480 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -109,6 +109,40 @@ describe('route-helper', function() { }); expect(doc.operations[0].parameters[0].description).to.equal('line1\nline2'); }); + + it('correctly does not include context params', function() { + var doc = createAPIDoc({ + accepts: [ + {arg: 'ctx', http: {source: 'context'}} + ], + path: '/test' + }); + var params = doc.operations[0].parameters; + expect(params.length).to.equal(0); + }); + + it('correctly does not include request params', function() { + var doc = createAPIDoc({ + accepts: [ + {arg: 'req', http: {source: 'req'}} + ], + path: '/test' + }); + var params = doc.operations[0].parameters; + expect(params.length).to.equal(0); + }); + + it('correctly does not include response params', function() { + var doc = createAPIDoc({ + accepts: [ + {arg: 'res', http: {source: 'res'}} + ], + path: '/test' + }); + var params = doc.operations[0].parameters; + expect(params.length).to.equal(0); + }); + }); // Easy wrapper around createRoute From 42dedfcaccf99cdcd95c26608c23cc2a543f3bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 15 Oct 2014 13:54:21 +0200 Subject: [PATCH 11/53] Move `convertText` to `typeConverter` Create a new module `lib/type-converter.js`. Move `routeHelper.convertText` to this new module. --- lib/class-helper.js | 4 ++-- lib/route-helper.js | 19 ++++--------------- lib/type-converter.js | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 lib/type-converter.js diff --git a/lib/class-helper.js b/lib/class-helper.js index 1e9c128..c03cb1b 100644 --- a/lib/class-helper.js +++ b/lib/class-helper.js @@ -4,7 +4,7 @@ * Module dependencies. */ var modelHelper = require('./model-helper'); -var routeHelper = require('./route-helper'); +var typeConverter = require('./type-converter'); var urlJoin = require('./url-join'); /** @@ -47,7 +47,7 @@ var classHelper = module.exports = { return { path: aClass.http.path, - description: routeHelper.convertText(description) + description: typeConverter.convertText(description) }; } }; diff --git a/lib/route-helper.js b/lib/route-helper.js index f52ed4c..add9775 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -8,6 +8,7 @@ var debug = require('debug')('loopback:explorer:routeHelpers'); var _cloneDeep = require('lodash.clonedeep'); var translateDataTypeKeys = require('./translate-data-type-keys'); var modelHelper = require('./model-helper'); +var typeConverter = require('./type-converter'); /** * Export the routeHelper singleton. @@ -142,8 +143,8 @@ var routeHelper = module.exports = { parameters: accepts, // TODO(schoon) - We don't have descriptions for this yet. responseMessages: [], - summary: routeHelper.convertText(route.description), - notes: routeHelper.convertText(route.notes), + summary: typeConverter.convertText(route.description), + notes: typeConverter.convertText(route.notes), deprecated: route.deprecated })] }; @@ -200,7 +201,7 @@ var routeHelper = module.exports = { var out = { paramType: paramType || type, name: name, - description: routeHelper.convertText(accepts.description), + description: typeConverter.convertText(accepts.description), type: accepts.type, required: !!accepts.required, defaultValue: accepts.defaultValue, @@ -239,18 +240,6 @@ var routeHelper = module.exports = { obj[key] = typeDesc[key]; }); return obj; - }, - - /** - * Convert a text value that can be expressed either as a string or - * as an array of strings. - * @param {string|Array} value - * @returns {string} - */ - convertText: function(value) { - if (Array.isArray(value)) - return value.join('\n'); - return value; } }; diff --git a/lib/type-converter.js b/lib/type-converter.js new file mode 100644 index 0000000..c7eb686 --- /dev/null +++ b/lib/type-converter.js @@ -0,0 +1,14 @@ +var typeConverter = module.exports = { + + /** + * Convert a text value that can be expressed either as a string or + * as an array of strings. + * @param {string|Array} value + * @returns {string} + */ + convertText: function(value) { + if (Array.isArray(value)) + return value.join('\n'); + return value; + } +}; From dc815a84211fb7d8ab3946dc27416eaa9908d704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 15 Oct 2014 13:58:13 +0200 Subject: [PATCH 12/53] Refactor conversion of data types Refactor the way how loopback types are converted to swagger data types. - `modelHelper.LDLPropToSwaggerDataType` is responsible for producing a valid Swagger Data Type object from LDL object (be it a property, accepts item or returns item). - LDLPropToSwaggerDataType picks only fields that are part of the swagger spec, everything else is excluded from the result. It's up to the caller to add extra fields like `description`. - refactor `routeHelper.extendWithType` to accept an additional arg: the original LDL object. This way it's possible to copy all type-specific fields to the output object and don't add anything else. --- lib/model-helper.js | 68 +++++++++++++++++++++------------ lib/route-helper.js | 53 ++++++++++--------------- lib/translate-data-type-keys.js | 1 - package.json | 2 + test/model-helper.test.js | 27 +++++++++++-- test/route-helper.test.js | 15 ++++++++ 6 files changed, 104 insertions(+), 62 deletions(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 1cff049..508df88 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -4,7 +4,9 @@ * Module dependencies. */ var _cloneDeep = require('lodash.clonedeep'); +var _pick = require('lodash.pick'); var translateDataTypeKeys = require('./translate-data-type-keys'); +var typeConverter = require('./type-converter'); /** * Export the modelHelper singleton. @@ -57,20 +59,20 @@ var modelHelper = module.exports = { } // Eke a type out of the constructors we were passed. - prop = modelHelper.LDLPropToSwaggerDataType(prop); + var swaggerType = modelHelper.LDLPropToSwaggerDataType(prop); + + var desc = typeConverter.convertText(prop.description || prop.doc); + if (desc) swaggerType.description = desc; // Required props sit in a per-model array. if (prop.required || (prop.id && !prop.generated)) { required.push(key); } - // Change mismatched keys. - prop = translateDataTypeKeys(prop); - // Assign this back to the properties object. - properties[key] = prop; + properties[key] = swaggerType; - var propType = def.properties[key].type; + var propType = prop.type; if (typeof propType === 'function' && propType.modelName) { if (referencedModels.indexOf(propType) === -1) { referencedModels.push(propType); @@ -134,34 +136,52 @@ var modelHelper = module.exports = { // Converts a prop defined with the LDL spec to one conforming to the // Swagger spec. // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives - LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(prop) { - var out = _cloneDeep(prop); - out.type = modelHelper.getPropType(out.type); + LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(ldlType) { + var SWAGGER_DATA_TYPE_FIELDS = [ + 'format', + 'defaultValue', + 'enum', + 'minimum', + 'maximum', + 'uniqueItems', + // loopback-explorer extensions + 'length', + // https://www.npmjs.org/package/swagger-validation + 'pattern' + ]; - if (out.type === 'array') { - var hasItemType = Array.isArray(prop.type) && prop.type.length; - var arrayItem = hasItemType && prop.type[0]; + // Rename LoopBack keys to Swagger keys + ldlType = translateDataTypeKeys(ldlType); + + // Pick only keys supported by Swagger + var swaggerType = _pick(ldlType, SWAGGER_DATA_TYPE_FIELDS); + + swaggerType.type = modelHelper.getPropType(ldlType.type); + + if (swaggerType.type === 'array') { + var hasItemType = Array.isArray(ldlType.type) && ldlType.type.length; + var arrayItem = hasItemType && ldlType.type[0]; if (arrayItem) { if(typeof arrayItem === 'object') { - out.items = modelHelper.LDLPropToSwaggerDataType(arrayItem); + swaggerType.items = modelHelper.LDLPropToSwaggerDataType(arrayItem); } else { - out.items = { type: modelHelper.getPropType(arrayItem) }; + swaggerType.items = { type: modelHelper.getPropType(arrayItem) }; } } else { // NOTE: `any` is not a supported type in swagger 1.2 - out.items = { type: 'any' }; + swaggerType.items = { type: 'any' }; } - } else if (out.type === 'date') { - out.type = 'string'; - out.format = 'date'; - } else if (out.type === 'buffer') { - out.type = 'string'; - out.format = 'byte'; - } else if (out.type === 'number') { - out.format = 'double'; // Since all JS numbers are doubles + } else if (swaggerType.type === 'date') { + swaggerType.type = 'string'; + swaggerType.format = 'date'; + } else if (swaggerType.type === 'buffer') { + swaggerType.type = 'string'; + swaggerType.format = 'byte'; + } else if (swaggerType.type === 'number') { + swaggerType.format = 'double'; // Since all JS numbers are doubles } - return out; + return swaggerType; } }; diff --git a/lib/route-helper.js b/lib/route-helper.js index add9775..d148cea 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -6,7 +6,7 @@ var debug = require('debug')('loopback:explorer:routeHelpers'); var _cloneDeep = require('lodash.clonedeep'); -var translateDataTypeKeys = require('./translate-data-type-keys'); +var _assign = require('lodash.assign'); var modelHelper = require('./model-helper'); var typeConverter = require('./type-converter'); @@ -71,9 +71,6 @@ var routeHelper = module.exports = { return true; }); - // Translate LDL keys to Swagger keys. - accepts = accepts.map(translateDataTypeKeys); - // Turn accept definitions in to parameter docs. accepts = accepts.map(routeHelper.acceptToParameter(route)); @@ -98,19 +95,19 @@ var routeHelper = module.exports = { } } - // Translate LDL keys to Swagger keys. - var returns = routeReturns.map(translateDataTypeKeys); - - // Convert `returns` into a single object for later conversion into an + // Convert `returns` into a single object for later conversion into an // operation object. - if (returns && returns.length > 1) { + if (routeReturns && routeReturns.length > 1) { // TODO ad-hoc model definition in the case of multiple return values. - returns = {model: 'object'}; + routeReturns = { type: 'object' }; } else { - returns = returns[0] || {}; + // Per the spec: + // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object + // This is the only object that may have a type of 'void'. + routeReturns = routeReturns[0] || { type: 'void' }; } - return returns; + return routeReturns; }, /** @@ -119,9 +116,7 @@ var routeHelper = module.exports = { * See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object */ routeToAPIDoc: function routeToAPIDoc(route, classDef) { - var returnDesc; - - // Some parameters need to be altered; eventually most of this should + // Some parameters need to be altered; eventually most of this should // be removed. var accepts = routeHelper.convertAcceptsToSwagger(route, classDef); var returns = routeHelper.convertReturnsToSwagger(route, classDef); @@ -136,17 +131,13 @@ var routeHelper = module.exports = { method: routeHelper.convertVerb(route.verb), // [rfeng] Swagger UI doesn't escape '.' for jQuery selector nickname: route.method.replace(/\./g, '_'), - // Per the spec: - // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object - // This is the only object that may have a type of 'void'. - type: returns.model || returns.type || 'void', parameters: accepts, // TODO(schoon) - We don't have descriptions for this yet. responseMessages: [], summary: typeConverter.convertText(route.description), notes: typeConverter.convertText(route.notes), deprecated: route.deprecated - })] + }, returns)] }; return apiDoc; @@ -202,15 +193,11 @@ var routeHelper = module.exports = { paramType: paramType || type, name: name, description: typeConverter.convertText(accepts.description), - type: accepts.type, required: !!accepts.required, - defaultValue: accepts.defaultValue, - minimum: accepts.minimum, - maximum: accepts.maximum, allowMultiple: false }; - out = routeHelper.extendWithType(out); + out = routeHelper.extendWithType(out, accepts); // HACK: Derive the type from model if(out.name === 'data' && out.type === 'object') { @@ -222,23 +209,23 @@ var routeHelper = module.exports = { }, /** - * Extends an Operation Object or Parameter object with + * Extends an Operation Object or Parameter object with * a proper Swagger type and optional `format` and `items` fields. * Does not modify original object. * @param {Object} obj Object to extend. - * @return {Object} Extended object. + * @param {Object} ldlType LDL type definition + * @return {Object} Extended object. */ - extendWithType: function extendWithType(obj) { + extendWithType: function extendWithType(obj, ldlType) { obj = _cloneDeep(obj); // Format the `type` property using our LDL converter. - var typeDesc = modelHelper - .LDLPropToSwaggerDataType({type: obj.model || obj.type}); + var typeDesc = modelHelper.LDLPropToSwaggerDataType(ldlType); + // The `typeDesc` may have additional attributes, such as // `format` for non-primitive types. - Object.keys(typeDesc).forEach(function(key){ - obj[key] = typeDesc[key]; - }); + _assign(obj, typeDesc); + return obj; } }; diff --git a/lib/translate-data-type-keys.js b/lib/translate-data-type-keys.js index a47c85d..d1e84a7 100644 --- a/lib/translate-data-type-keys.js +++ b/lib/translate-data-type-keys.js @@ -9,7 +9,6 @@ var _cloneDeep = require('lodash.clonedeep'); // Keys that are different between LDL and Swagger var KEY_TRANSLATIONS = { // LDL : Swagger - 'doc': 'description', 'default': 'defaultValue', 'min': 'minimum', 'max': 'maximum' diff --git a/package.json b/package.json index 0543493..86e6dc7 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,10 @@ "cors": "^2.4.2", "debug": "~1.0.3", "express": "3.x", + "lodash.assign": "^2.4.1", "lodash.clonedeep": "^2.4.1", "lodash.defaults": "^2.4.1", + "lodash.pick": "^2.4.1", "swagger-ui": "~2.0.18" } } diff --git a/test/model-helper.test.js b/test/model-helper.test.js index 98fd4b1..299f5ae 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -1,6 +1,7 @@ 'use strict'; var modelHelper = require('../lib/model-helper'); +var _assign = require('lodash.assign'); var loopback = require('loopback'); var expect = require('chai').expect; @@ -122,6 +123,22 @@ describe('model-helper', function() { }); }); + + it('converts model property field `doc`', function() { + var def = buildSwaggerModels({ + name: { type: String, doc: 'a-description' } + }); + var nameProp = def.properties.name; + expect(nameProp).to.have.property('description', 'a-description'); + }); + + it('converts model property field `description`', function() { + var def = buildSwaggerModels({ + name: { type: String, description: 'a-description' } + }); + var nameProp = def.properties.name; + expect(nameProp).to.have.property('description', 'a-description'); + }); }); describe('related models', function() { @@ -200,15 +217,17 @@ function buildSwaggerModels(model) { return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel; } -function createModelCtor(model) { - Object.keys(model).forEach(function(name) { - model[name] = {type: model[name]}; +function createModelCtor(properties) { + Object.keys(properties).forEach(function(name) { + var type = properties[name]; + if (typeof type !== 'object' || Array.isArray(type)) + properties[name] = { type: type }; }); var aClass = { ctor: { definition: { name: 'testModel', - properties: model + properties: properties } } }; diff --git a/test/route-helper.test.js b/test/route-helper.test.js index 6714480..e5a85e8 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -143,6 +143,21 @@ describe('route-helper', function() { expect(params.length).to.equal(0); }); + it('preserves `enum` accepts arg metadata', function() { + var doc = createAPIDoc({ + accepts: [{ name: 'arg', type: 'number', enum: [1,2,3] }] + }); + expect(doc.operations[0].parameters[0]) + .to.have.property('enum').eql([1,2,3]); + }); + + it('preserves `enum` returns arg metadata', function() { + var doc = createAPIDoc({ + returns: [{ name: 'arg', root: true, type: 'number', enum: [1,2,3] }] + }); + expect(doc.operations[0]) + .to.have.property('enum').eql([1,2,3]); + }); }); // Easy wrapper around createRoute From 060354cff8e80b81ac7f22c936523a774332b974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 15 Oct 2014 14:07:23 +0200 Subject: [PATCH 13/53] models: include model's `description` --- lib/model-helper.js | 1 + test/model-helper.test.js | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 508df88..51fbf53 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -90,6 +90,7 @@ var modelHelper = module.exports = { out[name] = { id: name, + description: typeConverter.convertText(def.description), properties: properties, required: required }; diff --git a/test/model-helper.test.js b/test/model-helper.test.js index 299f5ae..b183400 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -1,7 +1,7 @@ 'use strict'; var modelHelper = require('../lib/model-helper'); -var _assign = require('lodash.assign'); +var _defaults = require('lodash.defaults'); var loopback = require('loopback'); var expect = require('chai').expect; @@ -139,6 +139,11 @@ describe('model-helper', function() { var nameProp = def.properties.name; expect(nameProp).to.have.property('description', 'a-description'); }); + + it('converts model field `description`', function() { + var def = buildSwaggerModels({}, { description: 'a-description' }); + expect(def).to.have.property('description', 'a-description'); + }); }); describe('related models', function() { @@ -212,23 +217,27 @@ describe('model-helper', function() { }); // Simulates the format of a remoting class. -function buildSwaggerModels(model) { - var aClass = createModelCtor(model); +function buildSwaggerModels(modelProperties, modelOptions) { + var aClass = createModelCtor(modelProperties, modelOptions); return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel; } -function createModelCtor(properties) { +function createModelCtor(properties, modelOptions) { Object.keys(properties).forEach(function(name) { var type = properties[name]; if (typeof type !== 'object' || Array.isArray(type)) properties[name] = { type: type }; }); + + var definition = { + name: 'testModel', + properties: properties + }; + _defaults(definition, modelOptions); + var aClass = { ctor: { - definition: { - name: 'testModel', - properties: properties - } + definition: definition } }; return aClass; From 604248ec17d472b3831cd41cbbaba669a1eecb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 16 Oct 2014 10:17:47 +0200 Subject: [PATCH 14/53] example: use PersistedModel instead of Model Use `PersistedModel` as a base for the Product model to ensure it has some methods to inspect in the explorer. --- example/simple.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/simple.js b/example/simple.js index 3705ca5..70937cd 100644 --- a/example/simple.js +++ b/example/simple.js @@ -3,7 +3,7 @@ var app = loopback(); var explorer = require('../'); var port = 3000; -var Product = loopback.Model.extend('product', { +var Product = loopback.PersistedModel.extend('product', { foo: {type: 'string', required: true}, bar: 'string', aNum: {type: 'number', min: 1, max: 10, required: true, default: 5} @@ -14,6 +14,6 @@ app.model(Product); var apiPath = '/api'; app.use('/explorer', explorer(app, {basePath: apiPath})); app.use(apiPath, loopback.rest()); -console.log('Explorer mounted at localhost:' + port + '/explorer'); +console.log('Explorer mounted at http://localhost:' + port + '/explorer'); app.listen(port); From 6838087a5c79e29a63f141ce96df42cbe6b00071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:07:10 +0200 Subject: [PATCH 15/53] swagger: use X-Forwarded-Host for basePath --- lib/swagger.js | 10 ++++++---- test/swagger.test.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 0edd55f..b879a18 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -114,12 +114,14 @@ function addRoute(app, uri, doc, opts) { // can't guarantee this path is either reachable or desirable if it's set // as a part of the options. // - // The simplest way around this is to reflect the value of the `Host` HTTP - // header as the `basePath`. Because we pre-build the Swagger data, we don't - // know that header at the time the data is built. + // The simplest way around this is to reflect the value of the `Host` and/or + // `X-Forwarded-Host` HTTP headers as the `basePath`. + // Because we pre-build the Swagger data, we don't know that header at + // the time the data is built. if (hasBasePath) { var headers = req.headers; - var host = headers.Host || headers.host; + // NOTE header names (keys) are always all-lowercase + var host = headers['x-forwarded-host'] || headers.host; doc.basePath = (opts.protocol || req.protocol) + '://' + host + initialPath; } diff --git a/test/swagger.test.js b/test/swagger.test.js index c6f0987..c967ad6 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -81,6 +81,18 @@ describe('swagger definition', function() { done(); }); }); + + it('respects X-Forwarded-Host header (behind a proxy)', function(done) { + var app = mountSwagger(); + getAPIDeclaration(app, 'products') + .set('X-Forwarded-Host', 'example.com') + .end(function(err, res) { + if (err) return done(err); + var baseUrl = url.parse(res.body.basePath); + expect(baseUrl.hostname).to.equal('example.com'); + done(); + }); + }); }); describe('Model definition attributes', function() { From 2decdcc234a0ff5aa9cf2620eaaf78ced2fa620d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:12:21 +0200 Subject: [PATCH 16/53] swagger: Deprecate `opts.swaggerVersion` Users of loopback-explorer should not override the swagger version, as it's the explorer who decides what version of the Swagger Spec it implements. --- lib/swagger.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/swagger.js b/lib/swagger.js index b879a18..23fbff0 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -23,6 +23,9 @@ var cors = require('cors'); * @param {Object} opts Options. */ function Swagger(loopbackApplication, swaggerApp, opts) { + if (opts && opts.swaggerVersion) + console.warn('loopback-explorer\'s options.swaggerVersion is deprecated.'); + opts = _defaults(opts || {}, { swaggerVersion: '1.2', basePath: loopbackApplication.get('restApiRoot') || '/api', From d212741638588a81adb112a53f09ec1b717617cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 10:25:57 +0200 Subject: [PATCH 17/53] loopbackStyles: improve spacing in small window Improve spacing of page elements when the browser window is small. --- public/css/loopbackStyles.css | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/public/css/loopbackStyles.css b/public/css/loopbackStyles.css index 04d6894..5a56782 100644 --- a/public/css/loopbackStyles.css +++ b/public/css/loopbackStyles.css @@ -34,7 +34,12 @@ color: #080; } -/* -FIXME: Separate the overrides from the rest of the styles, rather than override screen.css entirely. -*/ +/* Improve spacing when the browser window is small */ +#message-bar, #swagger-ui-container { + padding-left: 30px; + padding-right: 30px; +} +#api_selector { + padding: 0px 20px; +} From aa7cb0b118baedd469aba7eab59b325b6c43c0c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 13:06:01 +0200 Subject: [PATCH 18/53] swagger: include models from accepts/returns args Models not attached to the app are included too. --- lib/swagger.js | 36 ++++++++++++++++ test/swagger.test.js | 99 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 23fbff0..c994bf8 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -12,6 +12,7 @@ var urlJoin = require('./url-join'); var _defaults = require('lodash.defaults'); var classHelper = require('./class-helper'); var routeHelper = require('./route-helper'); +var modelHelper = require('./model-helper'); var cors = require('cors'); /** @@ -84,6 +85,41 @@ function Swagger(loopbackApplication, swaggerApp, opts) { routeHelper.addRouteToAPIDeclaration(route, classDef, doc); }); + // Add models referenced from routes (e.g. accepts/returns) + Object.keys(apiDocs).forEach(function(className) { + var classDoc = apiDocs[className]; + classDoc.apis.forEach(function(api) { + api.operations.forEach(function(routeDoc) { + routeDoc.parameters.forEach(function(param) { + addTypeToModels(param.type); + }); + + addTypeToModels(routeDoc.type); + + // TODO(bajtos) handle types used by responseMessages + + function addTypeToModels(name) { + if (!name || name === 'void') return; + + var model = loopbackApplication.models[name]; + if (!model) { + var loopback = loopbackApplication.loopback; + if (!loopback) return; + + if (loopback.findModel) { + model = loopback.findModel(name); // LoopBack 2.x + } else { + model = loopback.getModel(name); // LoopBack 1.x + } + } + if (!model) return; + + modelHelper.generateModelDefinition(model, classDoc.models); + } + }); + }); + }); + /** * The topmost Swagger resource is a description of all (non-Swagger) * resources available on the system, and where to find more diff --git a/test/swagger.test.js b/test/swagger.test.js index c967ad6..2cab310 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -13,7 +13,7 @@ describe('swagger definition', function() { describe('basePath', function() { // No basepath on resource doc in 1.2 it('no longer exists on resource doc', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getSwaggerResources(app); getReq.end(function(err, res) { @@ -24,7 +24,7 @@ describe('swagger definition', function() { }); it('is "http://{host}/api" by default', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -35,7 +35,7 @@ describe('swagger definition', function() { }); it('is "http://{host}/{basePath}" when basePath is a path', function(done){ - var app = mountSwagger({ basePath: '/api-root'}); + var app = givenAppWithSwagger({ basePath: '/api-root'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -47,7 +47,7 @@ describe('swagger definition', function() { }); it('infers API basePath from app', function(done){ - var app = mountSwagger({}, {apiRoot: '/custom-api-root'}); + var app = givenAppWithSwagger({}, {apiRoot: '/custom-api-root'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -60,7 +60,7 @@ describe('swagger definition', function() { it('is reachable when explorer mounting location is changed', function(done){ var explorerRoot = '/erforscher'; - var app = mountSwagger({}, {explorerRoot: explorerRoot}); + var app = givenAppWithSwagger({}, {explorerRoot: explorerRoot}); var getReq = getSwaggerResources(app, explorerRoot, 'products'); getReq.end(function(err, res) { @@ -71,7 +71,7 @@ describe('swagger definition', function() { }); it('respects a hardcoded protocol (behind SSL terminator)', function(done){ - var app = mountSwagger({protocol: 'https'}); + var app = givenAppWithSwagger({protocol: 'https'}); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -83,7 +83,7 @@ describe('swagger definition', function() { }); it('respects X-Forwarded-Host header (behind a proxy)', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products') .set('X-Forwarded-Host', 'example.com') .end(function(err, res) { @@ -97,7 +97,7 @@ describe('swagger definition', function() { describe('Model definition attributes', function() { it('Properly defines basic attributes', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); var getReq = getAPIDeclaration(app, 'products'); getReq.end(function(err, res) { @@ -118,7 +118,7 @@ describe('swagger definition', function() { }); it('includes `consumes`', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products').end(function(err, res) { if (err) return done(err); expect(res.body.consumes).to.have.members([ @@ -131,7 +131,7 @@ describe('swagger definition', function() { }); it('includes `produces`', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); getAPIDeclaration(app, 'products').end(function(err, res) { if (err) return done(err); expect(res.body.produces).to.have.members([ @@ -143,11 +143,53 @@ describe('swagger definition', function() { done(); }); }); + + it('includes models from `accepts` args', function(done) { + var app = createLoopbackAppWithModel(); + givenPrivateAppModel(app, 'Image'); + givenSharedMethod(app.models.Product, 'setImage', { + accepts: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); + + it('includes models from `returns` args', function(done) { + var app = createLoopbackAppWithModel(); + givenPrivateAppModel(app, 'Image'); + givenSharedMethod(app.models.Product, 'getImage', { + returns: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); + + it('includes `accepts` models not attached to the app', function(done) { + var app = createLoopbackAppWithModel(); + loopback.createModel('Image'); + givenSharedMethod(app.models.Product, 'setImage', { + accepts: { name: 'image', type: 'Image' } + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('Image'); + done(); + }); + }); }); describe('Cross-origin resource sharing', function() { it('allows cross-origin requests by default', function(done) { - var app = mountSwagger(); + var app = givenAppWithSwagger(); request(app) .options('/explorer/resources') .set('Origin', 'http://example.com/') @@ -157,7 +199,7 @@ describe('swagger definition', function() { }); it('can be disabled by configuration', function(done) { - var app = mountSwagger({}, { remoting: { cors: { origin: false } } }); + var app = givenAppWithSwagger({}, { remoting: { cors: { origin: false } } }); request(app) .options('/explorer/resources') .end(function(err, res) { @@ -182,26 +224,35 @@ describe('swagger definition', function() { return getSwaggerResources(app, '', urlJoin('/', className)); } - function mountSwagger(options, addlOptions) { - addlOptions = addlOptions || {}; - var app = createLoopbackAppWithModel(addlOptions.apiRoot); + function givenAppWithSwagger(swaggerOptions, appConfig) { + appConfig = appConfig || {}; + var app = createLoopbackAppWithModel(appConfig.apiRoot); + + if (appConfig.remoting) app.set('remoting', appConfig.remoting); + if (appConfig.explorerRoot) app.set('explorerRoot', appConfig.explorerRoot); + + mountExplorer(app, swaggerOptions); + return app; + } + + function mountExplorer(app, options) { var swaggerApp = express(); - if (addlOptions.remoting) app.set('remoting', addlOptions.remoting); swagger(app, swaggerApp, options); - app.use(addlOptions.explorerRoot || '/explorer', swaggerApp); + app.use(app.get('explorerRoot') || '/explorer', swaggerApp); return app; } function createLoopbackAppWithModel(apiRoot) { var app = loopback(); + app.dataSource('db', { connector: 'memory' }); + var Product = loopback.Model.extend('product', { foo: {type: 'string', required: true}, bar: 'string', aNum: {type: 'number', min: 1, max: 10, required: true, default: 5} }); - Product.attachTo(loopback.memory()); - app.model(Product); + app.model(Product, { dataSource: 'db'}); // Simulate a restApiRoot set in config app.set('restApiRoot', apiRoot || '/api'); @@ -209,4 +260,14 @@ describe('swagger definition', function() { return app; } + + function givenSharedMethod(model, name, metadata) { + model[name] = function(){}; + loopback.remoteMethod(model[name], metadata); + } + + function givenPrivateAppModel(app, name) { + var model = loopback.createModel(name); + app.model(model, { dataSource: 'db', public: false} ); + } }); From 9a6bd35df704a6dc4de1c17f8c2e8231987261ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 16 Oct 2014 13:49:58 +0200 Subject: [PATCH 19/53] model-helper: support anonymous object types Accepts/returns arguments allow anonymous object types, e.g. { 'arg': 'kvp', type: { 'name': 'string', 'value': 'string' } } As of this commit, these types are converted to Swagger type 'object'. --- lib/model-helper.js | 9 ++++++--- test/model-helper.test.js | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 51fbf53..598f0f1 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -121,9 +121,12 @@ var modelHelper = module.exports = { if (typeof propType === 'function') { // See https://github.com/strongloop/loopback-explorer/issues/32 // The type can be a model class - propType = propType.modelName || propType.name.toLowerCase(); - } else if(Array.isArray(propType)) { - propType = 'array'; + return propType.modelName || propType.name.toLowerCase(); + } else if (Array.isArray(propType)) { + return 'array'; + } else if (typeof propType === 'object') { + // Anonymous objects, they are allowed e.g. in accepts/returns definitions + return 'object'; } return propType; }, diff --git a/test/model-helper.test.js b/test/model-helper.test.js index b183400..1fb6414 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -214,6 +214,13 @@ describe('model-helper', function() { expect(def.properties).to.have.property('visibleProperty'); }); }); + + describe('getPropType', function() { + it('converts anonymous object types', function() { + var type = modelHelper.getPropType({ name: 'string', value: 'string' }); + expect(type).to.eql('object'); + }); + }); }); // Simulates the format of a remoting class. From d05dcb71df55998851c7e822fcf0b6c7520f161c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 14 Oct 2014 13:42:09 +0200 Subject: [PATCH 20/53] route-helper: add `responseMessages` Add a default "success" response message, the status code is 200 or 204 depending on whether the method returns any data. Append any error messages as specified in the `errors` property of method's remoting metadata. Move the description of operation's return type to the "success" response message. Include error message models in the API models. --- lib/route-helper.js | 33 +++++++++++++++----- lib/swagger.js | 4 ++- test/route-helper.test.js | 65 +++++++++++++++++++++++++++++---------- test/swagger.test.js | 18 +++++++++++ 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index d148cea..98ea610 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -123,21 +123,40 @@ var routeHelper = module.exports = { debug('route %j', route); + var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns); + + // Note: Swagger Spec does not provide a way how to specify + // that the responseModel is "array of X". However, + // Swagger UI converts Arrays to the item types anyways, + // therefore it should be ok to do the same here. + var responseModel = responseDoc.type === 'array' ? + responseDoc.items.type : responseDoc.type; + + var responseMessages = [{ + code: route.returns && route.returns.length ? 200 : 204, + message: 'Request was successful', + responseModel: responseModel + }]; + + if (route.errors) { + responseMessages.push.apply(responseMessages, route.errors); + } + var apiDoc = { path: routeHelper.convertPathFragments(route.path), - // Create the operation doc. Use `extendWithType` to add the necessary - // `items` and `format` fields. - operations: [routeHelper.extendWithType({ + // Create the operation doc. + // Note that we are not calling `extendWithType`, as the response type + // is specified in the first response message. + operations: [{ method: routeHelper.convertVerb(route.verb), // [rfeng] Swagger UI doesn't escape '.' for jQuery selector - nickname: route.method.replace(/\./g, '_'), + nickname: route.method.replace(/\./g, '_'), parameters: accepts, - // TODO(schoon) - We don't have descriptions for this yet. - responseMessages: [], + responseMessages: responseMessages, summary: typeConverter.convertText(route.description), notes: typeConverter.convertText(route.notes), deprecated: route.deprecated - }, returns)] + }] }; return apiDoc; diff --git a/lib/swagger.js b/lib/swagger.js index c994bf8..3889166 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -96,7 +96,9 @@ function Swagger(loopbackApplication, swaggerApp, opts) { addTypeToModels(routeDoc.type); - // TODO(bajtos) handle types used by responseMessages + routeDoc.responseMessages.forEach(function(msg) { + addTypeToModels(msg.responseModel); + }); function addTypeToModels(name) { if (!name || name === 'void') return; diff --git a/test/route-helper.test.js b/test/route-helper.test.js index e5a85e8..cfe2010 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -13,7 +13,8 @@ describe('route-helper', function() { { arg: 'avg', type: 'number' } ] }); - expect(doc.operations[0].type).to.equal('object'); + expect(doc.operations[0].type).to.equal(undefined); + expect(getResponseType(doc.operations[0])).to.equal('object'); }); it('converts path params when they exist in the route name', function() { @@ -60,19 +61,12 @@ describe('route-helper', function() { ] }); var opDoc = doc.operations[0]; - expect(opDoc.type).to.equal('array'); - expect(opDoc.items).to.eql({type: 'customType'}); - }); + // Note: swagger-ui treat arrays of X the same way as object X + expect(getResponseType(opDoc)).to.equal('customType'); - it('correctly converts return types (format)', function() { - var doc = createAPIDoc({ - returns: [ - {arg: 'data', type: 'buffer'} - ] - }); - var opDoc = doc.operations[0]; - expect(opDoc.type).to.equal('string'); - expect(opDoc.format).to.equal('byte'); + // NOTE(bajtos) this would be the case if there was a single response type + // expect(opDoc.type).to.equal('array'); + // expect(opDoc.items).to.eql({type: 'customType'}); }); it('includes `notes` metadata', function() { @@ -151,12 +145,45 @@ describe('route-helper', function() { .to.have.property('enum').eql([1,2,3]); }); - it('preserves `enum` returns arg metadata', function() { + it('includes the default response message with code 200', function() { var doc = createAPIDoc({ - returns: [{ name: 'arg', root: true, type: 'number', enum: [1,2,3] }] + returns: [{ name: 'result', type: 'object', root: true }] + }); + expect(doc.operations[0].responseMessages).to.eql([ + { + code: 200, + message: 'Request was successful', + responseModel: 'object' + } + ]); + }); + + it('uses the response code 204 when `returns` is empty', function() { + var doc = createAPIDoc({ + returns: [] + }); + expect(doc.operations[0].responseMessages).to.eql([ + { + code: 204, + message: 'Request was successful', + responseModel: 'void' + } + ]); + }); + + it('includes custom error response in `responseMessages`', function() { + var doc = createAPIDoc({ + errors: [{ + code: 422, + message: 'Validation failed', + responseModel: 'ValidationError' + }] + }); + expect(doc.operations[0].responseMessages[1]).to.eql({ + code: 422, + message: 'Validation failed', + responseModel: 'ValidationError' }); - expect(doc.operations[0]) - .to.have.property('enum').eql([1,2,3]); }); }); @@ -168,3 +195,7 @@ function createAPIDoc(def) { method: 'test.get' })); } + +function getResponseType(operationDoc) { + return operationDoc.responseMessages[0].responseModel; +} diff --git a/test/swagger.test.js b/test/swagger.test.js index 2cab310..77a4542 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -185,6 +185,24 @@ describe('swagger definition', function() { done(); }); }); + + it('includes `responseMessages` models', function(done) { + var app = createLoopbackAppWithModel(); + loopback.createModel('ValidationError'); + givenSharedMethod(app.models.Product, 'setImage', { + errors: [{ + code: '422', + message: 'Validation failed', + responseModel: 'ValidationError' + }] + }); + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + expect(Object.keys(res.body.models)).to.include('ValidationError'); + done(); + }); + }); }); describe('Cross-origin resource sharing', function() { From 6fb81c279b498f119bd70ef819296b9e4a7e8085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 16 Oct 2014 14:35:55 +0200 Subject: [PATCH 21/53] Add integration tests for included models Add tests verifying that Swagger docs include model description for recursively nested references to Models and Arrays of Models in properties, modelTo and modelThrough relations, accepts, returns and errors. Fix bugs discovered along the way. --- lib/swagger.js | 6 +- test/swagger.test.js | 131 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 3889166..32843b7 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -91,7 +91,11 @@ function Swagger(loopbackApplication, swaggerApp, opts) { classDoc.apis.forEach(function(api) { api.operations.forEach(function(routeDoc) { routeDoc.parameters.forEach(function(param) { - addTypeToModels(param.type); + var type = param.type; + if (type === 'array' && param.items) + type = param.items.type; + + addTypeToModels(type); }); addTypeToModels(routeDoc.type); diff --git a/test/swagger.test.js b/test/swagger.test.js index 77a4542..9a79082 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -196,12 +196,108 @@ describe('swagger definition', function() { responseModel: 'ValidationError' }] }); - mountExplorer(app); - getAPIDeclaration(app, 'products').end(function(err, res) { - expect(Object.keys(res.body.models)).to.include('ValidationError'); - done(); + expectProductDocIncludesModels(app, 'ValidationError', done); + }); + + it('includes nested model references in properties', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.defineProperty('location', { type: 'Warehouse' }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in properties', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.defineProperty('location', { type: ['Warehouse'] }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in modelTo relation', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + app.models.Product.belongsTo(app.models.Warehouse); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in modelTo relation', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + givenPrivateAppModel(app, 'ProductLocations'); + + app.models.Product.hasMany(app.models.Warehouse, + { through: app.models.ProductLocations }); + + expectProductDocIncludesModels( + app, + ['Address', 'Warehouse', 'ProductLocations'], + done); + }); + + it('includes nested model references in accept args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + accepts: { arg: 'w', type: 'Warehouse' } }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in accept args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + accepts: { arg: 'w', type: [ 'Warehouse' ] } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in return args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + returns: { arg: 'w', type: 'Warehouse', root: true } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested array model references in return args', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + returns: { arg: 'w', type: ['Warehouse'], root: true } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); + }); + + it('includes nested model references in error responses', function(done) { + var app = createLoopbackAppWithModel(); + givenWarehouseWithAddressModels(app); + + givenSharedMethod(app.models.Product, 'aMethod', { + errors: { + code: '222', + message: 'Warehouse', + responseModel: 'Warehouse' + } + }); + + expectProductDocIncludesModels(app, ['Address', 'Warehouse'], done); }); }); @@ -234,8 +330,8 @@ describe('swagger definition', function() { return request(app) .get(urlJoin(restPath || '/explorer', '/resources', classPath || '')) .set('Accept', 'application/json') - .expect('Content-Type', /json/) - .expect(200); + .expect(200) + .expect('Content-Type', /json/); } function getAPIDeclaration(app, className) { @@ -284,8 +380,27 @@ describe('swagger definition', function() { loopback.remoteMethod(model[name], metadata); } - function givenPrivateAppModel(app, name) { - var model = loopback.createModel(name); + function givenPrivateAppModel(app, name, properties) { + var model = loopback.createModel(name, properties); app.model(model, { dataSource: 'db', public: false} ); } + + function givenWarehouseWithAddressModels(app) { + givenPrivateAppModel(app, 'Address'); + givenPrivateAppModel(app, 'Warehouse', { + shippingAddress: { type: 'Address' } + }); + } + + function expectProductDocIncludesModels(app, modelNames, done) { + if (!Array.isArray(modelNames)) modelNames = [modelNames]; + + mountExplorer(app); + + getAPIDeclaration(app, 'products').end(function(err, res) { + if (err) return done(err); + expect(Object.keys(res.body.models)).to.include.members(modelNames); + done(); + }); + } }); From 1b288406d141c5fbdf6c30c08b44a9083bca7b3d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 21 Oct 2014 15:39:26 -0700 Subject: [PATCH 22/53] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 86e6dc7..d550307 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.3.0", + "version": "1.4.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From a4179e454aaeed182de394284afca7cd86f3d175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 22 Oct 2014 11:10:15 +0200 Subject: [PATCH 23/53] swagger: honour X-Forwarded-Proto header Improve the algorithm building `baseUrl` to honour `X-Forwarded-Proto` header when it is present. --- lib/swagger.js | 4 ++-- test/swagger.test.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/swagger.js b/lib/swagger.js index 32843b7..5da84ae 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -166,9 +166,9 @@ function addRoute(app, uri, doc, opts) { if (hasBasePath) { var headers = req.headers; // NOTE header names (keys) are always all-lowercase + var proto = headers['x-forwarded-proto'] || opts.protocol || req.protocol; var host = headers['x-forwarded-host'] || headers.host; - doc.basePath = (opts.protocol || req.protocol) + '://' + - host + initialPath; + doc.basePath = proto + '://' + host + initialPath; } res.status(200).send(doc); }); diff --git a/test/swagger.test.js b/test/swagger.test.js index 9a79082..f70c9b0 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -93,6 +93,18 @@ describe('swagger definition', function() { done(); }); }); + + it('respects X-Forwarded-Proto header (behind a proxy)', function(done) { + var app = givenAppWithSwagger(); + getAPIDeclaration(app, 'products') + .set('X-Forwarded-Proto', 'https') + .end(function(err, res) { + if (err) return done(err); + var baseUrl = url.parse(res.body.basePath); + expect(baseUrl.protocol).to.equal('https:'); + done(); + }); + }); }); describe('Model definition attributes', function() { From 2ec096a278448d01b682dfd2f3963fdc0c46e482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 22 Oct 2014 10:55:25 +0200 Subject: [PATCH 24/53] Add an option `uiDirs` The `uiDirs` option allows users to provide their own set of directories with UI files, e.g. to provide a custom swagger-ui fork and a custom set of style/font overrides: explorer(app, { uiDirs: [ path.resolve(__dirname, 'public'), path.resolve(__dirname, 'node_modules', 'swagger-ui') ] }); The existing option `swaggerDistRoot` is deprecated now. --- README.md | 21 ++++++++------ index.js | 16 +++++++++-- test/explorer.test.js | 29 +++++++++++++++++++- test/fixtures/dummy-swagger-ui/index.html | 1 + test/fixtures/dummy-swagger-ui/swagger-ui.js | 1 + 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 test/fixtures/dummy-swagger-ui/index.html create mode 100644 test/fixtures/dummy-swagger-ui/swagger-ui.js diff --git a/README.md b/README.md index 882e104..9ef49eb 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ app.listen(port); ## Advanced Usage -Many aspects of the explorer are configurable. +Many aspects of the explorer are configurable. See [options](#options) for a description of these options: @@ -34,7 +34,10 @@ See [options](#options) for a description of these options: app.use('/explorer', loopback.basicAuth('user', 'password')); app.use('/explorer', explorer(app, { basePath: '/custom-api-root', - swaggerDistRoot: '/swagger', + uiDirs: [ + path.resolve(__dirname, 'public'), + path.resolve(__dirname, 'node_modules', 'swagger-ui') + ] apiInfo: { 'title': 'My API', 'description': 'Explorer example app.' @@ -67,27 +70,27 @@ Options are passed to `explorer(app, options)`. > and thus needs to report its endpoints as `https`, even though incoming traffic is auto-detected > as `http`. -`swaggerDistRoot`: **String** +`uiDirs`: **Array of Strings** -> Sets a path within your application for overriding Swagger UI files. +> Sets a list of paths within your application for overriding Swagger UI files. -> If present, will search `swaggerDistRoot` first when attempting to load Swagger UI, allowing -> you to pick and choose overrides to the interface. Use this to style your explorer or -> add additional functionality. +> If present, will search `uiDirs` first when attempting to load Swagger UI, +> allowing you to pick and choose overrides to the interface. Use this to +> style your explorer or add additional functionality. > See [index.html](public/index.html), where you may want to begin your overrides. > The rest of the UI is provided by [Swagger UI](https://github.com/wordnik/swagger-ui). `apiInfo`: **Object** -> Additional information about your API. See the +> Additional information about your API. See the > [spec](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#513-info-object). `resourcePath`: **String** > Default: `'resources'` -> Sets a different path for the +> Sets a different path for the > [resource listing](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#51-resource-listing). > You generally shouldn't have to change this. diff --git a/index.js b/index.js index 6c5e003..e34a778 100644 --- a/index.js +++ b/index.js @@ -47,15 +47,25 @@ function explorer(loopbackApplication, options) { }); }); - // Allow specifying a static file root for swagger files. Any files in - // that folder will override those in the swagger-ui distribution. - // In this way one could e.g. make changes to index.html without having + // Allow specifying a static file roots for swagger files. Any files in + // these folders will override those in the swagger-ui distribution. + // In this way one could e.g. make changes to index.html without having // to worry about constantly pulling in JS updates. + if (options.uiDirs) { + options.uiDirs.forEach(function(dir) { + app.use(express.static(dir)); + }); + } + if (options.swaggerDistRoot) { + console.warn('loopback-explorer: `swaggerDistRoot` is deprecated,' + + ' use `uiDirs` instead'); app.use(express.static(options.swaggerDistRoot)); } + // File in node_modules are overridden by a few customizations app.use(express.static(STATIC_ROOT)); + // Swagger UI distribution app.use(express.static(SWAGGER_UI_ROOT)); diff --git a/test/explorer.test.js b/test/explorer.test.js index e53c4d9..ce900aa 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -2,6 +2,7 @@ var loopback = require('loopback'); var explorer = require('../'); var request = require('supertest'); var assert = require('assert'); +var path = require('path'); var expect = require('chai').expect; describe('explorer', function() { @@ -24,7 +25,7 @@ describe('explorer', function() { .end(function(err, res) { if (err) throw err; - assert(!!~res.text.indexOf('StrongLoop API Explorer'), + assert(!!~res.text.indexOf('StrongLoop API Explorer'), 'text does not contain expected string'); done(); }); @@ -79,6 +80,32 @@ describe('explorer', function() { }); }); + describe('with custom front-end files', function() { + var app; + beforeEach(function setupExplorerWithUiDirs() { + app = loopback(); + app.use('/explorer', explorer(app, { + uiDirs: [ path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui') ] + })); + }); + + it('overrides swagger-ui files', function(done) { + request(app).get('/explorer/swagger-ui.js') + .expect(200) + // expect the content of `dummy-swagger-ui/swagger-ui.js` + .expect('/* custom swagger-ui file */\n') + .end(done); + }); + + it('overrides strongloop overrides', function(done) { + request(app).get('/explorer/') + .expect(200) + // expect the content of `dummy-swagger-ui/index.html` + .expect('custom index.html\n') + .end(done); + }); + }); + function givenLoopBackAppWithExplorer(explorerBase) { return function(done) { var app = this.app = loopback(); diff --git a/test/fixtures/dummy-swagger-ui/index.html b/test/fixtures/dummy-swagger-ui/index.html new file mode 100644 index 0000000..f6c3a1f --- /dev/null +++ b/test/fixtures/dummy-swagger-ui/index.html @@ -0,0 +1 @@ +custom index.html diff --git a/test/fixtures/dummy-swagger-ui/swagger-ui.js b/test/fixtures/dummy-swagger-ui/swagger-ui.js new file mode 100644 index 0000000..55f593f --- /dev/null +++ b/test/fixtures/dummy-swagger-ui/swagger-ui.js @@ -0,0 +1 @@ +/* custom swagger-ui file */ From ee2d0d4ddb627ff7af25e950ba76ef9304024428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 24 Oct 2014 19:31:30 +0200 Subject: [PATCH 25/53] 1.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d550307..6b7771e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.4.0", + "version": "1.5.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From b9a74a0b204ca0071fbf58b2b478090ba94b4fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 24 Oct 2014 19:34:32 +0200 Subject: [PATCH 26/53] 1.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b7771e..8e8065a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.5.0", + "version": "1.5.1", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From ce76f17ee14cfae3c74d4fcfcde35e3c04d260df Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Mon, 10 Nov 2014 10:11:25 +0000 Subject: [PATCH 27/53] Save accessToken in localStorage. Fixes #47 --- public/lib/loadSwaggerUI.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/public/lib/loadSwaggerUI.js b/public/lib/loadSwaggerUI.js index c89917b..327a126 100644 --- a/public/lib/loadSwaggerUI.js +++ b/public/lib/loadSwaggerUI.js @@ -3,9 +3,10 @@ // Refactoring of inline script from index.html. /*global SwaggerUi, log, ApiKeyAuthorization, hljs, window, $ */ $(function() { + var lsKey = 'swagger_accessToken'; $.getJSON('config.json', function(config) { - log(config); - loadSwaggerUi(config); + log(config); + loadSwaggerUi(config); }); var accessToken; @@ -35,6 +36,14 @@ $(function() { $('#api_selector').submit(setAccessToken); $('#input_accessToken').keyup(onInputChange); + // Recover accessToken from localStorage if present. + if (window.localStorage) { + var key = window.localStorage.getItem(lsKey); + if (key) { + $('#input_accessToken').val(key).submit(); + } + } + window.swaggerUi.load(); } @@ -49,6 +58,11 @@ $(function() { accessToken = key; $('.accessTokenDisplay').text('Token Set.').addClass('set'); $('.accessTokenDisplay').attr('data-tooltip', 'Current Token: ' + key); + + // Save this token to localStorage if we can to make it persist on refresh. + if (window.localStorage) { + window.localStorage.setItem(lsKey, key); + } } } From 4e5cbe43f98ea8159e02cbe7f75e643aa1d135b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Sat, 29 Nov 2014 12:05:12 +0100 Subject: [PATCH 28/53] model-helper: ignore unknown property types --- lib/model-helper.js | 9 ++++++++- test/model-helper.test.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 598f0f1..faee5b4 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -21,8 +21,15 @@ var modelHelper = module.exports = { */ generateModelDefinition: function generateModelDefinition(modelClass, definitions) { var def = modelClass.definition; - var name = def.name; var out = definitions || {}; + + if (!def) { + // The model does not have any definition, it was most likely + // created as a placeholder for an unknown property type + return out; + } + + var name = def.name; if (out[name]) { // The model is already included return out; diff --git a/test/model-helper.test.js b/test/model-helper.test.js index 1fb6414..abe8586 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -198,6 +198,18 @@ describe('model-helper', function() { expect(Object.keys(defs)).has.property('length', 1); }); + // https://github.com/strongloop/loopback-explorer/issues/71 + it('should skip unknown types', function() { + var Model8 = loopback.createModel('Model8', { + patient: { + model: 'physician', + type: 'hasMany', + through: 'appointment' + } + }); + var defs = modelHelper.generateModelDefinition(Model8, {}); + expect(Object.keys(defs)).to.not.contain('hasMany'); + }); }); describe('hidden properties', function() { From 8147ba582133646acd4135a9dc53f88db9db401f Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Mon, 10 Nov 2014 11:10:50 +0000 Subject: [PATCH 29/53] Remove model name from nickname, swagger spec understands op context. This removes the redundancy from paths in swagger-ui such as `/api/user/user_login`. It will now be displayed simply as `/api/user/login`. This is consistent with how `nickname` is used in Swagger examples. Added tests to route nickname processing. --- lib/route-helper.js | 6 ++++-- test/route-helper.test.js | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index 98ea610..a5ba8d5 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -149,8 +149,10 @@ var routeHelper = module.exports = { // is specified in the first response message. operations: [{ method: routeHelper.convertVerb(route.verb), - // [rfeng] Swagger UI doesn't escape '.' for jQuery selector - nickname: route.method.replace(/\./g, '_'), + // [strml] remove leading model name from op, swagger uses leading + // path as class name so it remains unique between models. + // route.method is always #{className}.#{methodName} + nickname: route.method.replace(/.*?\./, ''), parameters: accepts, responseMessages: responseMessages, summary: typeConverter.convertText(route.description), diff --git a/test/route-helper.test.js b/test/route-helper.test.js index cfe2010..faca3a5 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -185,11 +185,24 @@ describe('route-helper', function() { responseModel: 'ValidationError' }); }); + + it('route nickname does not include model name.', function() { + var doc = createAPIDoc(); + expect(doc.operations[0].nickname).to.equal('get'); + }); + + it('route nickname with a period is shorted correctly', function() { + // Method is built by remoting to always be #{className}.#{methodName} + var doc = createAPIDoc({ + method: 'test.get.me' + }); + expect(doc.operations[0].nickname).to.eql('get.me'); + }); }); // Easy wrapper around createRoute function createAPIDoc(def) { - return routeHelper.routeToAPIDoc(_defaults(def, { + return routeHelper.routeToAPIDoc(_defaults(def || {}, { path: '/test', verb: 'GET', method: 'test.get' From 22cf27dac8008d0fe930c99b592d5c53567d8a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Sat, 29 Nov 2014 12:11:31 +0100 Subject: [PATCH 30/53] v1.5.2 --- CHANGES.md | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..e2d2e22 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,265 @@ +2014-11-29, Version 1.5.2 +========================= + + * model-helper: ignore unknown property types (Miroslav Bajtoš) + + +2014-10-24, Version 1.5.1 +========================= + + + +2014-10-24, Version 1.5.0 +========================= + + * Add an option `uiDirs` (Miroslav Bajtoš) + + * swagger: honour X-Forwarded-Proto header (Miroslav Bajtoš) + + +2014-10-21, Version 1.4.0 +========================= + + * Bump version (Raymond Feng) + + * Add integration tests for included models (Miroslav Bajtoš) + + * route-helper: add `responseMessages` (Miroslav Bajtoš) + + * model-helper: support anonymous object types (Miroslav Bajtoš) + + * swagger: include models from accepts/returns args (Miroslav Bajtoš) + + * loopbackStyles: improve spacing in small window (Miroslav Bajtoš) + + * swagger: Deprecate `opts.swaggerVersion` (Miroslav Bajtoš) + + * swagger: use X-Forwarded-Host for basePath (Miroslav Bajtoš) + + * example: use PersistedModel instead of Model (Miroslav Bajtoš) + + * models: include model's `description` (Miroslav Bajtoš) + + * Refactor conversion of data types (Miroslav Bajtoš) + + * Move `convertText` to `typeConverter` (Miroslav Bajtoš) + + * Add support for `context` and `res` param types (Krishna Raman) + + * package: update devDependencies (Miroslav Bajtoš) + + * gitignore: add .idea, *.tgz, *.iml (Miroslav Bajtoš) + + * Support multi-line array `description` and `notes` (Miroslav Bajtoš) + + * Use `1.0.0` as the default app version. (Miroslav Bajtoš) + + * Extend `consumes` and `produces` metadata (Miroslav Bajtoš) + + * route-helper: include `notes` and `deprecated` (Miroslav Bajtoš) + + * Pull model description from ctor.settings first (Shelby Sanders) + + +2014-10-08, Version 1.3.0 +========================= + + * swagger: allow cross-origin requests (Miroslav Bajtoš) + + * Sort endpoints by letter. (Samuel Reed) + + * Add syntax highlighting styles & highlight threshold. (Samuel Reed) + + * Add contribution guidelines (Ryan Graham) + + +2014-09-22, Version 1.2.11 +========================== + + * Bump version (Raymond Feng) + + * Fix how the array of models is iterated (Raymond Feng) + + +2014-09-05, Version 1.2.10 +========================== + + * Bump version (Raymond Feng) + + * Make sure nested/referenced models in array are mapped to swagger (Clark Wang) + + * Make sure nested/referenced models are mapped to swagger (Raymond Feng) + + +2014-08-15, Version 1.2.9 +========================= + + * Bump version (Raymond Feng) + + * Newest Swagger UI requires application/x-www-form-urlencoded. (Samuel Reed) + + * Use `dist` property from swagger-ui package. (Samuel Reed) + + * Fixed undefined modelClass when using polymorphic relations (Navid Nikpour) + + +2014-08-08, Version 1.2.8 +========================= + + * Bump version (Raymond Feng) + + * Fix the type name for a property if model class is used (Raymond Feng) + + +2014-08-04, Version 1.2.7 +========================= + + * Bump version (Raymond Feng) + + * Set up default consumes/produces media types (Raymond Feng) + + * Fix the default opts (Raymond Feng) + + * Add required swagger 1.2 items property for property type array (Ritchie Martori) + + * Allow passing a custom protocol. (Samuel Reed) + + +2014-07-29, Version 1.2.6 +========================= + + * Bump version (Raymond Feng) + + * res.send deprecated - updated to res.status (Geoffroy) + + * Remove hidden properties from definition. (Samuel Reed) + + +2014-07-25, Version 1.2.5 +========================= + + * Bump version (Raymond Feng) + + * Ensure models from relations are included (Raymond Feng) + + +2014-07-22, Version 1.2.4 +========================= + + * model-helper: handle arrays with undefined items (Miroslav Bajtoš) + + +2014-07-22, Version 1.2.3 +========================= + + * model-helper: handle array types with no item type (Miroslav Bajtoš) + + +2014-07-20, Version 1.2.2 +========================= + + * Bump version (Raymond Feng) + + * Properly convert complex return types. (Samuel Reed) + + +2014-07-18, Version 1.2.1 +========================= + + * Bump version (Raymond Feng) + + * Fix up loopback.rest() model definition hack. (Samuel Reed) + + +2014-07-14, Version 1.2.0 +========================= + + * Bump version and update deps (Raymond Feng) + + * s/accessToken/access_token in authorization key name (Samuel Reed) + + * Fix resources if the explorer is at a deep path. (Samuel Reed) + + * Fix debug namespace, express version. (Samuel Reed) + + * Remove forgotten TODO. (Samuel Reed) + + * Simplify `accepts` and `returns` hacks. (Samuel Reed) + + * More consise type tests (Samuel Reed) + + * Remove preMiddleware. (Samuel Reed) + + * Remove swagger.test.js license (Samuel Reed) + + * Remove peerDependencies, use express directly. (Samuel Reed) + + * Add url-join so path.join() doesn't break windows (Samuel Reed) + + * Rename translateKeys to translateDataTypeKeys. (Samuel Reed) + + * Refactor route-helper & add tests. (Samuel Reed) + + * LDL to Swagger fixes & extensions. (Samuel Reed) + + * Use express routes instead of modifying remoting. (Samuel Reed) + + * Fix missing strong-remoting devDependency. (Samuel Reed) + + * Restore existing styles. (Samuel Reed) + + * Allow easy setting of accessToken in explorer UI. (Samuel Reed) + + * Refactor key translations between LDL & Swagger. (Samuel Reed) + + * Refactoring swagger 1.2 rework. (Samuel Reed) + + * Make sure body parameter is shown. (Raymond Feng) + + * Some swagger 1.2 migration cleanup. (Samuel Reed) + + * Fix api resource path and type ref to models. (Raymond Feng) + + * Swagger 1.2 compatability. Moved strong-remoting/ext/swagger to this module. (Samuel Reed) + + * Load swagger ui from `swagger-ui` package instead. (Samuel Reed) + + +2014-05-28, Version 1.1.1 +========================= + + * package.json: add support for loopback 2.x (Miroslav Bajtoš) + + * Make sure X-Powered-By header is disabled (Alex Pica) + + * Fix license url (Raymond Feng) + + * Update to dual MIT/StrongLoop license (Raymond Feng) + + +2014-01-14, Version 1.1.0 +========================= + + * Bump up loopback min version to 1.5 (Miroslav Bajtoš) + + * Use `app.get('restApiRoot')` as default basePath (Miroslav Bajtoš) + + * Replace strong-remoting ext/swagger with app.docs (Miroslav Bajtoš) + + +2014-01-13, Version 1.0.2 +========================= + + * Bump version (Raymond Feng) + + * README: mount REST at /api in the sample code (Miroslav Bajtos) + + * Reorder middleware to fix unit-test failures. (Miroslav Bajtos) + + * Fix loading of loopback dependencies. (Miroslav Bajtos) + + +2013-12-04, Version 1.0.1 +========================= + + * First release! diff --git a/package.json b/package.json index 8e8065a..1a8d628 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.5.1", + "version": "1.5.2", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 33d0ac953186781532476fb0097f4e791c627cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 2 Dec 2014 09:08:43 +0100 Subject: [PATCH 31/53] v1.6.0 --- CHANGES.md | 10 ++++++++-- package.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e2d2e22..61c2a8d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,15 +1,21 @@ +2014-12-02, Version 1.6.0 +========================= + + * Remove model name from nickname, swagger spec understands op context. (Samuel Reed) + + 2014-11-29, Version 1.5.2 ========================= * model-helper: ignore unknown property types (Miroslav Bajtoš) -2014-10-24, Version 1.5.1 +2014-10-24, Version 1.5.0 ========================= -2014-10-24, Version 1.5.0 +2014-10-24, Version 1.5.1 ========================= * Add an option `uiDirs` (Miroslav Bajtoš) diff --git a/package.json b/package.json index 1a8d628..1fc09ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.5.2", + "version": "1.6.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 6b5a016c5935e92c676a3c944b4e1454246d4671 Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Tue, 2 Dec 2014 19:09:48 -0800 Subject: [PATCH 32/53] Use full lodash instead of lodash components This change trims 134 modules from the dependency tree, many of which were duplicate depdencies between the 4 lodash.function modules used and their sub-dependencies. Before: $ npm ls | wc -l 494 $ du -sch node_modules/lodash* 432K node_modules/lodash.assign 660K node_modules/lodash.clonedeep 96K node_modules/lodash.defaults 980K node_modules/lodash.pick 2.1M total After: $ npm ls | wc -l 360 $ du -sch node_modules/lodash* 964K node_modules/lodash 964K total --- index.js | 2 +- lib/model-helper.js | 4 ++-- lib/route-helper.js | 4 ++-- lib/swagger.js | 2 +- lib/translate-data-type-keys.js | 2 +- package.json | 5 +---- test/class-helper.test.js | 2 +- test/model-helper.test.js | 2 +- test/route-helper.test.js | 2 +- 9 files changed, 11 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index e34a778..08962c3 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ var url = require('url'); var path = require('path'); var urlJoin = require('./lib/url-join'); -var _defaults = require('lodash.defaults'); +var _defaults = require('lodash').defaults; var express = require('express'); var swagger = require('./lib/swagger'); var SWAGGER_UI_ROOT = require('swagger-ui').dist; diff --git a/lib/model-helper.js b/lib/model-helper.js index faee5b4..07699c9 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -3,8 +3,8 @@ /** * Module dependencies. */ -var _cloneDeep = require('lodash.clonedeep'); -var _pick = require('lodash.pick'); +var _cloneDeep = require('lodash').cloneDeep; +var _pick = require('lodash').pick; var translateDataTypeKeys = require('./translate-data-type-keys'); var typeConverter = require('./type-converter'); diff --git a/lib/route-helper.js b/lib/route-helper.js index a5ba8d5..4d5545f 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -5,8 +5,8 @@ */ var debug = require('debug')('loopback:explorer:routeHelpers'); -var _cloneDeep = require('lodash.clonedeep'); -var _assign = require('lodash.assign'); +var _cloneDeep = require('lodash').cloneDeep; +var _assign = require('lodash').assign; var modelHelper = require('./model-helper'); var typeConverter = require('./type-converter'); diff --git a/lib/swagger.js b/lib/swagger.js index 5da84ae..f6ee454 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -9,7 +9,7 @@ module.exports = Swagger; */ var path = require('path'); var urlJoin = require('./url-join'); -var _defaults = require('lodash.defaults'); +var _defaults = require('lodash').defaults; var classHelper = require('./class-helper'); var routeHelper = require('./route-helper'); var modelHelper = require('./model-helper'); diff --git a/lib/translate-data-type-keys.js b/lib/translate-data-type-keys.js index d1e84a7..d2ca63a 100644 --- a/lib/translate-data-type-keys.js +++ b/lib/translate-data-type-keys.js @@ -4,7 +4,7 @@ * Module dependencies. */ -var _cloneDeep = require('lodash.clonedeep'); +var _cloneDeep = require('lodash').cloneDeep; // Keys that are different between LDL and Swagger var KEY_TRANSLATIONS = { diff --git a/package.json b/package.json index 1fc09ed..112f387 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,7 @@ "cors": "^2.4.2", "debug": "~1.0.3", "express": "3.x", - "lodash.assign": "^2.4.1", - "lodash.clonedeep": "^2.4.1", - "lodash.defaults": "^2.4.1", - "lodash.pick": "^2.4.1", + "lodash": "^2.4.1", "swagger-ui": "~2.0.18" } } diff --git a/test/class-helper.test.js b/test/class-helper.test.js index beb6d8f..de3bc15 100644 --- a/test/class-helper.test.js +++ b/test/class-helper.test.js @@ -2,7 +2,7 @@ var classHelper = require('../lib/class-helper'); var expect = require('chai').expect; -var _defaults = require('lodash.defaults'); +var _defaults = require('lodash').defaults; describe('class-helper', function() { it('joins array descriptions', function() { diff --git a/test/model-helper.test.js b/test/model-helper.test.js index abe8586..06bd259 100644 --- a/test/model-helper.test.js +++ b/test/model-helper.test.js @@ -1,7 +1,7 @@ 'use strict'; var modelHelper = require('../lib/model-helper'); -var _defaults = require('lodash.defaults'); +var _defaults = require('lodash').defaults; var loopback = require('loopback'); var expect = require('chai').expect; diff --git a/test/route-helper.test.js b/test/route-helper.test.js index faca3a5..6654f78 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -2,7 +2,7 @@ var routeHelper = require('../lib/route-helper'); var expect = require('chai').expect; -var _defaults = require('lodash.defaults'); +var _defaults = require('lodash').defaults; describe('route-helper', function() { it('returns "object" when a route has multiple return values', function() { From f3480f857cee2f3909d71ebbd8ff34f11b032391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 8 Dec 2014 08:10:06 +0100 Subject: [PATCH 33/53] v1.6.1 --- CHANGES.md | 10 ++++++++-- package.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 61c2a8d..4e8b4cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2014-12-08, Version 1.6.1 +========================= + + * Use full lodash instead of lodash components (Ryan Graham) + + 2014-12-02, Version 1.6.0 ========================= @@ -10,12 +16,12 @@ * model-helper: ignore unknown property types (Miroslav Bajtoš) -2014-10-24, Version 1.5.0 +2014-10-24, Version 1.5.1 ========================= -2014-10-24, Version 1.5.1 +2014-10-24, Version 1.5.0 ========================= * Add an option `uiDirs` (Miroslav Bajtoš) diff --git a/package.json b/package.json index 112f387..fce503e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.6.0", + "version": "1.6.1", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 28a5a2619afe369273333cf8d93764170486d538 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Fri, 12 Dec 2014 13:10:45 +0100 Subject: [PATCH 34/53] Move 200 response to `type` on the operation object. See #75. --- lib/route-helper.js | 19 ++++++------------- lib/swagger.js | 6 +++++- test/route-helper.test.js | 30 ++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/route-helper.js b/lib/route-helper.js index 4d5545f..4625e8b 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -125,17 +125,9 @@ var routeHelper = module.exports = { var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns); - // Note: Swagger Spec does not provide a way how to specify - // that the responseModel is "array of X". However, - // Swagger UI converts Arrays to the item types anyways, - // therefore it should be ok to do the same here. - var responseModel = responseDoc.type === 'array' ? - responseDoc.items.type : responseDoc.type; - var responseMessages = [{ code: route.returns && route.returns.length ? 200 : 204, - message: 'Request was successful', - responseModel: responseModel + message: 'Request was successful' }]; if (route.errors) { @@ -145,9 +137,10 @@ var routeHelper = module.exports = { var apiDoc = { path: routeHelper.convertPathFragments(route.path), // Create the operation doc. - // Note that we are not calling `extendWithType`, as the response type - // is specified in the first response message. - operations: [{ + // We are using extendWithType to use `type` for the top-level (200) + // response type. We use responseModels for error responses. + // see https://github.com/strongloop/loopback-explorer/issues/75 + operations: [routeHelper.extendWithType({ method: routeHelper.convertVerb(route.verb), // [strml] remove leading model name from op, swagger uses leading // path as class name so it remains unique between models. @@ -158,7 +151,7 @@ var routeHelper = module.exports = { summary: typeConverter.convertText(route.description), notes: typeConverter.convertText(route.notes), deprecated: route.deprecated - }] + }, returns)] }; return apiDoc; diff --git a/lib/swagger.js b/lib/swagger.js index f6ee454..3572c81 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -98,7 +98,11 @@ function Swagger(loopbackApplication, swaggerApp, opts) { addTypeToModels(type); }); - addTypeToModels(routeDoc.type); + if (routeDoc.type === 'array') { + addTypeToModels(routeDoc.items.type); + } else { + addTypeToModels(routeDoc.type); + } routeDoc.responseMessages.forEach(function(msg) { addTypeToModels(msg.responseModel); diff --git a/test/route-helper.test.js b/test/route-helper.test.js index 6654f78..8e35f5c 100644 --- a/test/route-helper.test.js +++ b/test/route-helper.test.js @@ -13,8 +13,8 @@ describe('route-helper', function() { { arg: 'avg', type: 'number' } ] }); - expect(doc.operations[0].type).to.equal(undefined); - expect(getResponseType(doc.operations[0])).to.equal('object'); + expect(doc.operations[0].type).to.equal('object'); + expect(getResponseType(doc.operations[0])).to.equal(undefined); }); it('converts path params when they exist in the route name', function() { @@ -61,12 +61,22 @@ describe('route-helper', function() { ] }); var opDoc = doc.operations[0]; - // Note: swagger-ui treat arrays of X the same way as object X - expect(getResponseType(opDoc)).to.equal('customType'); + expect(getResponseType(opDoc)).to.equal(undefined); // NOTE(bajtos) this would be the case if there was a single response type - // expect(opDoc.type).to.equal('array'); - // expect(opDoc.items).to.eql({type: 'customType'}); + expect(opDoc.type).to.equal('array'); + expect(opDoc.items).to.eql({type: 'customType'}); + }); + + it('correctly converts return types (format)', function() { + var doc = createAPIDoc({ + returns: [ + {arg: 'data', type: 'buffer'} + ] + }); + var opDoc = doc.operations[0]; + expect(opDoc.type).to.equal('string'); + expect(opDoc.format).to.equal('byte'); }); it('includes `notes` metadata', function() { @@ -149,11 +159,11 @@ describe('route-helper', function() { var doc = createAPIDoc({ returns: [{ name: 'result', type: 'object', root: true }] }); + expect(doc.operations[0].type).to.eql('object'); expect(doc.operations[0].responseMessages).to.eql([ { code: 200, - message: 'Request was successful', - responseModel: 'object' + message: 'Request was successful' } ]); }); @@ -162,11 +172,11 @@ describe('route-helper', function() { var doc = createAPIDoc({ returns: [] }); + expect(doc.operations[0].type).to.eql('void'); expect(doc.operations[0].responseMessages).to.eql([ { code: 204, - message: 'Request was successful', - responseModel: 'void' + message: 'Request was successful' } ]); }); From b540651658f3d1b3451f33c16f6e10dae8496ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 12 Dec 2014 13:56:52 +0100 Subject: [PATCH 35/53] v1.6.2 --- CHANGES.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4e8b4cb..2e4644d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2014-12-12, Version 1.6.2 +========================= + + * Move 200 response to `type` on the operation object. See #75. (Samuel Reed) + + 2014-12-08, Version 1.6.1 ========================= diff --git a/package.json b/package.json index fce503e..c46b6d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.6.1", + "version": "1.6.2", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 4aa5fd372fc58138c36008c7ea5b8a3b2dd7e8b8 Mon Sep 17 00:00:00 2001 From: Nick Van Dyck Date: Fri, 26 Dec 2014 20:56:15 +0100 Subject: [PATCH 36/53] Add X-UA-Compatible tag Allows people to open the explorer in IE without constantly changing the document mode. --- public/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/index.html b/public/index.html index e5b6ff6..dc45d91 100644 --- a/public/index.html +++ b/public/index.html @@ -1,6 +1,7 @@ + StrongLoop API Explorer From 15ffe02e1bbf69a96d06ad3b20b62e243f67d75d Mon Sep 17 00:00:00 2001 From: Ryan Graham Date: Mon, 29 Dec 2014 08:27:46 -0800 Subject: [PATCH 37/53] Fix bad CLA URL in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed649b0..f170f19 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Contributing to `loopback-explorer` is easy. In a few simple steps: * Adhere to code style outlined in the [Google C++ Style Guide][] and [Google Javascript Style Guide][]. - * Sign the [Contributor License Agreement](https://cla.strongloop.com/strongloop/loopback-explorer) + * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-explorer) * Submit a pull request through Github. From e825c4b49f65e9b95af8ee0f342033476664ead3 Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Wed, 31 Dec 2014 13:49:06 -0800 Subject: [PATCH 38/53] Allow `uiDirs` to be defined as a String --- index.js | 10 +++++++--- test/explorer.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 08962c3..6048c3a 100644 --- a/index.js +++ b/index.js @@ -52,9 +52,13 @@ function explorer(loopbackApplication, options) { // In this way one could e.g. make changes to index.html without having // to worry about constantly pulling in JS updates. if (options.uiDirs) { - options.uiDirs.forEach(function(dir) { - app.use(express.static(dir)); - }); + if (typeof options.uiDirs === 'string') { + app.use(express.static(options.uiDirs)); + } else if (Array.isArray(options.uiDirs)) { + options.uiDirs.forEach(function(dir) { + app.use(express.static(dir)); + }); + } } if (options.swaggerDistRoot) { diff --git a/test/explorer.test.js b/test/explorer.test.js index ce900aa..2a17a15 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -106,6 +106,37 @@ describe('explorer', function() { }); }); + describe('when specifying custom static file root directories', function() { + var app; + beforeEach(function() { + app = loopback(); + }); + + it('should allow `uiDirs` to be defined as an Array', function(done) { + app.use('/explorer', explorer(app, { + uiDirs: [ path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui') ] + })); + + request(app).get('/explorer/') + .expect(200) + // expect the content of `dummy-swagger-ui/index.html` + .expect('custom index.html\n') + .end(done); + }); + + it('should allow `uiDirs` to be defined as an String', function(done) { + app.use('/explorer', explorer(app, { + uiDirs: path.resolve(__dirname, 'fixtures', 'dummy-swagger-ui') + })); + + request(app).get('/explorer/') + .expect(200) + // expect the content of `dummy-swagger-ui/index.html` + .expect('custom index.html\n') + .end(done); + }); + }); + function givenLoopBackAppWithExplorer(explorerBase) { return function(done) { var app = this.app = loopback(); From cbf3b36aef902064bf6c3367ee04e6520532fb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 6 Jan 2015 09:23:09 +0100 Subject: [PATCH 39/53] v1.6.3 --- CHANGES.md | 19 +++++++++++++++---- package.json | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2e4644d..05e9cd1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +2015-01-06, Version 1.6.3 +========================= + + * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) + + * Add X-UA-Compatible tag (Nick Van Dyck) + + 2014-12-12, Version 1.6.2 ========================= @@ -160,16 +168,19 @@ * Ensure models from relations are included (Raymond Feng) - -2014-07-22, Version 1.2.4 -========================= - * model-helper: handle arrays with undefined items (Miroslav Bajtoš) 2014-07-22, Version 1.2.3 ========================= + + +2014-07-22, Version 1.2.4 +========================= + + * model-helper: handle arrays with undefined items (Miroslav Bajtoš) + * model-helper: handle array types with no item type (Miroslav Bajtoš) diff --git a/package.json b/package.json index c46b6d6..c6b6776 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.6.2", + "version": "1.6.3", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From c6693f8725e98071c77313b27a5dcd0710bc1978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 9 Jan 2015 14:00:22 +0100 Subject: [PATCH 40/53] Prevent double slash in the resource URLs SwaggerUI builds resource URL by concatenating basePath + resourcePath. Since the resource paths are always startign with a slash, if the basePath ends with a slash too, an incorrect URL is produced. This was typically happenning when `restApiRoot` was '/'. This commit modifies the code producing the base path to remove the trailing slash. --- lib/swagger.js | 5 +++++ test/explorer.test.js | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/swagger.js b/lib/swagger.js index 3572c81..b25d517 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -156,6 +156,11 @@ function addRoute(app, uri, doc, opts) { var hasBasePath = Object.keys(doc).indexOf('basePath') !== -1; var initialPath = doc.basePath || ''; + // Remove the trailing slash, see + // https://github.com/strongloop/loopback-explorer/issues/48 + if (initialPath[initialPath.length-1] === '/') + initialPath = initialPath.slice(0, -1); + app.get(urlJoin('/', uri), function(req, res) { // There's a few forces at play that require this "hack". The Swagger spec diff --git a/test/explorer.test.js b/test/explorer.test.js index 2a17a15..c3b4aeb 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -4,6 +4,7 @@ var request = require('supertest'); var assert = require('assert'); var path = require('path'); var expect = require('chai').expect; +var urlJoin = require('../lib/url-join'); describe('explorer', function() { @@ -78,6 +79,26 @@ describe('explorer', function() { done(); }); }); + + it('removes trailing slash from baseUrl', function(done) { + // SwaggerUI builds resource URL by concatenating basePath + resourcePath + // Since the resource paths are always startign with a slash, + // if the basePath ends with a slash too, an incorrect URL is produced + var app = loopback(); + app.set('restApiRoot', '/'); + configureRestApiAndExplorer(app); + + request(app) + .get('/explorer/resources/products') + .expect(200) + .end(function(err, res) { + if (err) return done(err); + var baseUrl = res.body.basePath; + var apiPath = res.body.apis[0].path; + expect(baseUrl + apiPath).to.match(/http:\/\/[^\/]+\/products/); + done(); + }); + }); }); describe('with custom front-end files', function() { @@ -146,7 +167,7 @@ describe('explorer', function() { } function configureRestApiAndExplorer(app, explorerBase) { - var Product = loopback.Model.extend('product'); + var Product = loopback.PersistedModel.extend('product'); Product.attachTo(loopback.memory()); app.model(Product); From 79e21fde10db2af6aecf85e2a6f1524f66bd96d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 9 Jan 2015 14:48:07 +0100 Subject: [PATCH 41/53] v1.6.4 --- CHANGES.md | 14 ++++++++++++-- package.json | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05e9cd1..fcc7f98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2015-01-09, Version 1.6.4 +========================= + + * Prevent double slash in the resource URLs (Miroslav Bajtoš) + + * Allow `uiDirs` to be defined as a String (Simon Ho) + + * Save accessToken in localStorage. Fixes #47 (Samuel Reed) + + 2015-01-06, Version 1.6.3 ========================= @@ -30,12 +40,12 @@ * model-helper: ignore unknown property types (Miroslav Bajtoš) -2014-10-24, Version 1.5.1 +2014-10-24, Version 1.5.0 ========================= -2014-10-24, Version 1.5.0 +2014-10-24, Version 1.5.1 ========================= * Add an option `uiDirs` (Miroslav Bajtoš) diff --git a/package.json b/package.json index c6b6776..9e71f3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.6.3", + "version": "1.6.4", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 16c54c038e60e7ef05d6f9252ad1883d475350b5 Mon Sep 17 00:00:00 2001 From: gandrianakis Date: Tue, 17 Feb 2015 17:23:32 +0200 Subject: [PATCH 42/53] Made API doc of class use the http.path of the class if available, or the name of the class as a fallback --- lib/class-helper.js | 7 ++++++- test/class-helper.test.js | 28 ++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/class-helper.js b/lib/class-helper.js index c03cb1b..377b19e 100644 --- a/lib/class-helper.js +++ b/lib/class-helper.js @@ -23,11 +23,16 @@ var classHelper = module.exports = { * @return {Object} API Declaration. */ generateAPIDoc: function(aClass, opts) { + var resourcePath = urlJoin('/', aClass.name); + if(aClass.http && aClass.http.path) { + resourcePath = aClass.http.path; + } + return { apiVersion: opts.version, swaggerVersion: opts.swaggerVersion, basePath: opts.basePath, - resourcePath: urlJoin('/', opts.resourcePath), + resourcePath: urlJoin('/', resourcePath), apis: [], consumes: aClass.http.consumes || opts.consumes, produces: aClass.http.produces || opts.produces, diff --git a/test/class-helper.test.js b/test/class-helper.test.js index de3bc15..cd578a1 100644 --- a/test/class-helper.test.js +++ b/test/class-helper.test.js @@ -12,12 +12,32 @@ describe('class-helper', function() { expect(doc.description).to.equal('line1\nline2'); }); + + it('sets resourcePath from aClass.http.path', function() { + var doc = generateAPIDoc({}, 'otherPath'); + + expect(doc.resourcePath).to.equal('/otherPath'); + }); + + it('sets resourcePath from aClass.name', function() { + var doc = generateAPIDoc({}); + + expect(doc.resourcePath).to.equal('/test'); + }); }); // Easy wrapper around createRoute function generateResourceDocAPIEntry(def) { - return classHelper.generateResourceDocAPIEntry(_defaults(def, { - http: { path: '/test' }, - ctor: { settings: { } }, - })); + return classHelper.generateResourceDocAPIEntry(_defaults(def, { + http: { path: '/test' }, + ctor: { settings: { } } + })); +} + +function generateAPIDoc(def, httpPath) { + return classHelper.generateAPIDoc(_defaults(def, { + http: { path: httpPath || null }, + name: 'test', + ctor: { settings: { } } + }), {resourcePath: 'resources'}); } From 8e82a5cabdcb56c92d70bc1460076abd84e18e1e Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Feb 2015 08:50:11 -0800 Subject: [PATCH 43/53] 1.7.0 * Made API doc of class use the http.path of the class if available, or the name of the class as a fallback (gandrianakis) --- CHANGES.md | 21 ++++++++++++--------- package.json | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index fcc7f98..5fab166 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2015-02-17, Version 1.7.0 +========================= + + * Made API doc of class use the http.path of the class if available, or the name of the class as a fallback (gandrianakis) + + 2015-01-09, Version 1.6.4 ========================= @@ -40,12 +46,12 @@ * model-helper: ignore unknown property types (Miroslav Bajtoš) -2014-10-24, Version 1.5.0 +2014-10-24, Version 1.5.1 ========================= -2014-10-24, Version 1.5.1 +2014-10-24, Version 1.5.0 ========================= * Add an option `uiDirs` (Miroslav Bajtoš) @@ -178,19 +184,16 @@ * Ensure models from relations are included (Raymond Feng) - * model-helper: handle arrays with undefined items (Miroslav Bajtoš) - - -2014-07-22, Version 1.2.3 -========================= - - 2014-07-22, Version 1.2.4 ========================= * model-helper: handle arrays with undefined items (Miroslav Bajtoš) + +2014-07-22, Version 1.2.3 +========================= + * model-helper: handle array types with no item type (Miroslav Bajtoš) diff --git a/package.json b/package.json index 9e71f3e..7120b21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.6.4", + "version": "1.7.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 6028f70fda7280e64c59a6e059b1bc1b3103b21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Wed, 18 Feb 2015 15:14:41 +0100 Subject: [PATCH 44/53] Remove unused external font "Droid Sans". We are shipping a custom style that uses "Ubuntu" instead of "Droid Sans", thus there is no need to download the "Droid Sans" font from Google's CDN. This change should improve explorer's loading time in the situations where the external URL takes too long to load or cannot be loaded at all, for example when running behind a restricting proxy. --- public/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/index.html b/public/index.html index dc45d91..e96db3f 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,6 @@ StrongLoop API Explorer - From 780929ab7357618f5c9f4da0519d27aa0554414a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 23 Feb 2015 18:29:44 +0100 Subject: [PATCH 45/53] 1.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove unused external font "Droid Sans". (Miroslav Bajtoš) --- CHANGES.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 5fab166..62327b5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +2015-02-23, Version 1.7.1 +========================= + + * Remove unused external font "Droid Sans". (Miroslav Bajtoš) + + 2015-02-17, Version 1.7.0 ========================= diff --git a/package.json b/package.json index 7120b21..0d337e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.7.0", + "version": "1.7.1", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From 297086fb551829f99dd9208b65ffbcbc4491f57d Mon Sep 17 00:00:00 2001 From: Pradnya Baviskar Date: Fri, 27 Feb 2015 15:20:10 +0530 Subject: [PATCH 46/53] Fix explorer tests for different line endings on Windows --- test/explorer.test.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/explorer.test.js b/test/explorer.test.js index c3b4aeb..431fd3f 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -5,6 +5,7 @@ var assert = require('assert'); var path = require('path'); var expect = require('chai').expect; var urlJoin = require('../lib/url-join'); +var os = require('os'); describe('explorer', function() { @@ -114,7 +115,7 @@ describe('explorer', function() { request(app).get('/explorer/swagger-ui.js') .expect(200) // expect the content of `dummy-swagger-ui/swagger-ui.js` - .expect('/* custom swagger-ui file */\n') + .expect('/* custom swagger-ui file */' + os.EOL) .end(done); }); @@ -122,7 +123,7 @@ describe('explorer', function() { request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` - .expect('custom index.html\n') + .expect('custom index.html' + os.EOL) .end(done); }); }); @@ -141,7 +142,7 @@ describe('explorer', function() { request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` - .expect('custom index.html\n') + .expect('custom index.html' + os.EOL) .end(done); }); @@ -153,7 +154,7 @@ describe('explorer', function() { request(app).get('/explorer/') .expect(200) // expect the content of `dummy-swagger-ui/index.html` - .expect('custom index.html\n') + .expect('custom index.html' + os.EOL) .end(done); }); }); From 5d4d25e853f188578ba9da01a1b9dbdae139ec81 Mon Sep 17 00:00:00 2001 From: Pradnya Baviskar Date: Fri, 27 Feb 2015 15:44:04 +0530 Subject: [PATCH 47/53] Fix duplicate stylesheet issue --- public/index.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index e96db3f..1f633d4 100644 --- a/public/index.html +++ b/public/index.html @@ -3,10 +3,8 @@ StrongLoop API Explorer - - - - + + From b7c14b7a09630a9448129116a0909a0feae5eff9 Mon Sep 17 00:00:00 2001 From: Samuel Reed Date: Fri, 6 Mar 2015 12:32:45 +0700 Subject: [PATCH 48/53] Allow submitting token input with empty value to remove token. --- public/lib/loadSwaggerUI.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/public/lib/loadSwaggerUI.js b/public/lib/loadSwaggerUI.js index 327a126..8cea670 100644 --- a/public/lib/loadSwaggerUI.js +++ b/public/lib/loadSwaggerUI.js @@ -64,6 +64,17 @@ $(function() { window.localStorage.setItem(lsKey, key); } } + // If submitted with an empty token, remove the current token. Can be + // useful to intentionally remove authorization. + else { + log('removed accessToken.'); + $('.accessTokenDisplay').text('Token Not Set.').removeClass('set'); + $('.accessTokenDisplay').removeAttr('data-tooltip'); + window.authorizations.remove('key'); + if (window.localStorage) { + window.localStorage.removeItem(lsKey); + } + } } function onInputChange(e) { From c9b6d451ccfd80bba7e3b11322ecff0c55684288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Mar 2015 15:19:38 +0200 Subject: [PATCH 49/53] 1.7.2 * Allow submitting token input with empty value to remove token. (Samuel Reed) * Fix duplicate stylesheet issue (Pradnya Baviskar) * Fix explorer tests for different line endings on Windows (Pradnya Baviskar) --- CHANGES.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 62327b5..656d839 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2015-03-30, Version 1.7.2 +========================= + + * Allow submitting token input with empty value to remove token. (Samuel Reed) + + * Fix duplicate stylesheet issue (Pradnya Baviskar) + + * Fix explorer tests for different line endings on Windows (Pradnya Baviskar) + + 2015-02-23, Version 1.7.1 ========================= diff --git a/package.json b/package.json index 0d337e5..255dc71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.7.1", + "version": "1.7.2", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { From fa3035c4bb0ed1b0d255c8418afe5a68dc336572 Mon Sep 17 00:00:00 2001 From: bkniffler Date: Mon, 20 Apr 2015 16:04:44 +0200 Subject: [PATCH 50/53] Fix model description getting lost --- lib/model-helper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 07699c9..3da950e 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -97,7 +97,7 @@ var modelHelper = module.exports = { out[name] = { id: name, - description: typeConverter.convertText(def.description), + description: typeConverter.convertText(def.settings.description), properties: properties, required: required }; From ccfd9c42b08e30e8ea6564735827940c519db308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 27 Apr 2015 08:28:04 +0200 Subject: [PATCH 51/53] Fix tests broken by fa3035c (#96) --- lib/model-helper.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/model-helper.js b/lib/model-helper.js index 3da950e..ddb4d79 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -97,7 +97,8 @@ var modelHelper = module.exports = { out[name] = { id: name, - description: typeConverter.convertText(def.settings.description), + description: typeConverter.convertText( + def.description || (def.settings && def.settings.description)), properties: properties, required: required }; From 5462ce9aac973ec6d85ae964e0d2806ec3f4196e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Tue, 23 Jun 2015 16:36:19 +0200 Subject: [PATCH 52/53] Add opts.omitProtocolInBaseUrl --- lib/swagger.js | 3 ++- test/swagger.test.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/swagger.js b/lib/swagger.js index b25d517..3e9aacf 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -176,8 +176,9 @@ function addRoute(app, uri, doc, opts) { var headers = req.headers; // NOTE header names (keys) are always all-lowercase var proto = headers['x-forwarded-proto'] || opts.protocol || req.protocol; + var prefix = opts.omitProtocolInBaseUrl ? '//' : proto + '://'; var host = headers['x-forwarded-host'] || headers.host; - doc.basePath = proto + '://' + host + initialPath; + doc.basePath = prefix + host + initialPath; } res.status(200).send(doc); }); diff --git a/test/swagger.test.js b/test/swagger.test.js index f70c9b0..120f08e 100644 --- a/test/swagger.test.js +++ b/test/swagger.test.js @@ -105,6 +105,20 @@ describe('swagger definition', function() { done(); }); }); + + it('supports options.omitProtocolInBaseUrl', function(done) { + var app = givenAppWithSwagger({ omitProtocolInBaseUrl: true }); + + var getReq = getAPIDeclaration(app, 'products'); + getReq.end(function(err, res) { + if (err) return done(err); + var basePath = res.body.basePath; + expect(basePath).to.match(/^\/\//); + var parsed = url.parse(res.body.basePath); + expect(parsed.protocol).to.equal(null); + done(); + }); + }); }); describe('Model definition attributes', function() { From 265d1cdbe1ad26b83379e8ba43814646186e0e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 25 Jun 2015 18:53:53 +0200 Subject: [PATCH 53/53] 1.8.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add opts.omitProtocolInBaseUrl (Miroslav Bajtoš) * Fix tests broken by fa3035c (#96) (Miroslav Bajtoš) * Fix model description getting lost (bkniffler) --- CHANGES.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 656d839..54a0fd2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +2015-06-25, Version 1.8.0 +========================= + + * Add opts.omitProtocolInBaseUrl (Miroslav Bajtoš) + + * Fix tests broken by fa3035c (#96) (Miroslav Bajtoš) + + * Fix model description getting lost (bkniffler) + + 2015-03-30, Version 1.7.2 ========================= diff --git a/package.json b/package.json index 255dc71..2483653 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.7.2", + "version": "1.8.0", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": {