Merge pull request #5 from shelbys/validate_param_enum
Added support for param enum enforcement, and refactored to match changes in upstream of Forks
This commit is contained in:
commit
fccb69303f
|
@ -6,6 +6,9 @@ lib-cov
|
||||||
*.out
|
*.out
|
||||||
*.pid
|
*.pid
|
||||||
*.gz
|
*.gz
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
*.tgz
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
pids
|
pids
|
||||||
|
|
|
@ -0,0 +1,333 @@
|
||||||
|
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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* 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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* Remove unused external font "Droid Sans". (Miroslav Bajtoš)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* 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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham)
|
||||||
|
|
||||||
|
* Add X-UA-Compatible tag (Nick Van Dyck)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* Use full lodash instead of lodash components (Ryan Graham)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
=========================
|
||||||
|
|
||||||
|
* 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!
|
|
@ -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
|
* Adhere to code style outlined in the [Google C++ Style Guide][] and
|
||||||
[Google Javascript Style Guide][].
|
[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.
|
* Submit a pull request through Github.
|
||||||
|
|
||||||
|
|
15
README.md
15
README.md
|
@ -34,7 +34,10 @@ See [options](#options) for a description of these options:
|
||||||
app.use('/explorer', loopback.basicAuth('user', 'password'));
|
app.use('/explorer', loopback.basicAuth('user', 'password'));
|
||||||
app.use('/explorer', explorer(app, {
|
app.use('/explorer', explorer(app, {
|
||||||
basePath: '/custom-api-root',
|
basePath: '/custom-api-root',
|
||||||
swaggerDistRoot: '/swagger',
|
uiDirs: [
|
||||||
|
path.resolve(__dirname, 'public'),
|
||||||
|
path.resolve(__dirname, 'node_modules', 'swagger-ui')
|
||||||
|
]
|
||||||
apiInfo: {
|
apiInfo: {
|
||||||
'title': 'My API',
|
'title': 'My API',
|
||||||
'description': 'Explorer example app.'
|
'description': 'Explorer example app.'
|
||||||
|
@ -67,13 +70,13 @@ Options are passed to `explorer(app, options)`.
|
||||||
> and thus needs to report its endpoints as `https`, even though incoming traffic is auto-detected
|
> and thus needs to report its endpoints as `https`, even though incoming traffic is auto-detected
|
||||||
> as `http`.
|
> 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
|
> If present, will search `uiDirs` first when attempting to load Swagger UI,
|
||||||
> you to pick and choose overrides to the interface. Use this to style your explorer or
|
> allowing you to pick and choose overrides to the interface. Use this to
|
||||||
> add additional functionality.
|
> style your explorer or add additional functionality.
|
||||||
|
|
||||||
> See [index.html](public/index.html), where you may want to begin your overrides.
|
> 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).
|
> The rest of the UI is provided by [Swagger UI](https://github.com/wordnik/swagger-ui).
|
||||||
|
|
|
@ -3,7 +3,7 @@ var app = loopback();
|
||||||
var explorer = require('../');
|
var explorer = require('../');
|
||||||
var port = 3000;
|
var port = 3000;
|
||||||
|
|
||||||
var Product = loopback.Model.extend('product', {
|
var Product = loopback.PersistedModel.extend('product', {
|
||||||
foo: {type: 'string', required: true},
|
foo: {type: 'string', required: true},
|
||||||
bar: 'string',
|
bar: 'string',
|
||||||
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
|
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
|
||||||
|
@ -14,6 +14,6 @@ app.model(Product);
|
||||||
var apiPath = '/api';
|
var apiPath = '/api';
|
||||||
app.use('/explorer', explorer(app, {basePath: apiPath}));
|
app.use('/explorer', explorer(app, {basePath: apiPath}));
|
||||||
app.use(apiPath, loopback.rest());
|
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);
|
app.listen(port);
|
||||||
|
|
20
index.js
20
index.js
|
@ -5,7 +5,7 @@
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var urlJoin = require('./lib/url-join');
|
var urlJoin = require('./lib/url-join');
|
||||||
var _defaults = require('lodash.defaults');
|
var _defaults = require('lodash').defaults;
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var swagger = require('./lib/swagger');
|
var swagger = require('./lib/swagger');
|
||||||
var SWAGGER_UI_ROOT = require('swagger-ui').dist;
|
var SWAGGER_UI_ROOT = require('swagger-ui').dist;
|
||||||
|
@ -47,15 +47,29 @@ function explorer(loopbackApplication, options) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Allow specifying a static file root for swagger files. Any files in
|
// Allow specifying a static file roots for swagger files. Any files in
|
||||||
// that folder will override those in the swagger-ui distribution.
|
// these folders will override those in the swagger-ui distribution.
|
||||||
// In this way one could e.g. make changes to index.html without having
|
// In this way one could e.g. make changes to index.html without having
|
||||||
// to worry about constantly pulling in JS updates.
|
// to worry about constantly pulling in JS updates.
|
||||||
|
if (options.uiDirs) {
|
||||||
|
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) {
|
if (options.swaggerDistRoot) {
|
||||||
|
console.warn('loopback-explorer: `swaggerDistRoot` is deprecated,' +
|
||||||
|
' use `uiDirs` instead');
|
||||||
app.use(express.static(options.swaggerDistRoot));
|
app.use(express.static(options.swaggerDistRoot));
|
||||||
}
|
}
|
||||||
|
|
||||||
// File in node_modules are overridden by a few customizations
|
// File in node_modules are overridden by a few customizations
|
||||||
app.use(express.static(STATIC_ROOT));
|
app.use(express.static(STATIC_ROOT));
|
||||||
|
|
||||||
// Swagger UI distribution
|
// Swagger UI distribution
|
||||||
app.use(express.static(SWAGGER_UI_ROOT));
|
app.use(express.static(SWAGGER_UI_ROOT));
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
var modelHelper = require('./model-helper');
|
var modelHelper = require('./model-helper');
|
||||||
|
var typeConverter = require('./type-converter');
|
||||||
var urlJoin = require('./url-join');
|
var urlJoin = require('./url-join');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,11 +23,16 @@ var classHelper = module.exports = {
|
||||||
* @return {Object} API Declaration.
|
* @return {Object} API Declaration.
|
||||||
*/
|
*/
|
||||||
generateAPIDoc: function(aClass, opts) {
|
generateAPIDoc: function(aClass, opts) {
|
||||||
|
var resourcePath = urlJoin('/', aClass.name);
|
||||||
|
if(aClass.http && aClass.http.path) {
|
||||||
|
resourcePath = aClass.http.path;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiVersion: opts.version || '1',
|
apiVersion: opts.version || '1',
|
||||||
swaggerVersion: opts.swaggerVersion,
|
swaggerVersion: opts.swaggerVersion,
|
||||||
basePath: opts.basePath,
|
basePath: opts.basePath,
|
||||||
resourcePath: urlJoin('/', opts.resourcePath),
|
resourcePath: urlJoin('/', resourcePath),
|
||||||
apis: [],
|
apis: [],
|
||||||
consumes: aClass.http.consumes || opts.consumes,
|
consumes: aClass.http.consumes || opts.consumes,
|
||||||
produces: aClass.http.produces || opts.produces,
|
produces: aClass.http.produces || opts.produces,
|
||||||
|
@ -41,10 +47,12 @@ var classHelper = module.exports = {
|
||||||
* @return {Object} API declaration reference.
|
* @return {Object} API declaration reference.
|
||||||
*/
|
*/
|
||||||
generateResourceDocAPIEntry: function(aClass) {
|
generateResourceDocAPIEntry: function(aClass) {
|
||||||
var description = aClass.ctor.settings.description || aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description;
|
var description = aClass.ctor.settings.description ||
|
||||||
|
aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: aClass.http.path,
|
path: aClass.http.path,
|
||||||
description: Array.isArray(description) ? description.join('') : description
|
description: typeConverter.convertText(description)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
/**
|
/**
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
var _cloneDeep = require('lodash.clonedeep');
|
var _cloneDeep = require('lodash').cloneDeep;
|
||||||
|
var _pick = require('lodash').pick;
|
||||||
var translateDataTypeKeys = require('./translate-data-type-keys');
|
var translateDataTypeKeys = require('./translate-data-type-keys');
|
||||||
var TYPES_PRIMITIVE = ['array', 'boolean', 'integer', 'number', 'null', 'object', 'string', 'any'];
|
var TYPES_PRIMITIVE = ['array', 'boolean', 'integer', 'number', 'null', 'object', 'string', 'any'];
|
||||||
|
var typeConverter = require('./type-converter');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the modelHelper singleton.
|
* Export the modelHelper singleton.
|
||||||
|
@ -39,8 +41,15 @@ var modelHelper = module.exports = {
|
||||||
};
|
};
|
||||||
|
|
||||||
var def = modelClass.definition;
|
var def = modelClass.definition;
|
||||||
var name = def.name;
|
|
||||||
var out = definitions || {};
|
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]) {
|
if (out[name]) {
|
||||||
// The model is already included
|
// The model is already included
|
||||||
return out;
|
return out;
|
||||||
|
@ -77,13 +86,17 @@ var modelHelper = module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eke a type out of the constructors we were passed.
|
// Eke a type out of the constructors we were passed.
|
||||||
prop = modelHelper.LDLPropToSwaggerDataType(prop);
|
var swaggerType = modelHelper.LDLPropToSwaggerDataType(prop);
|
||||||
processType(modelClass.app, prop.type, referencedModels);
|
processType(modelClass.app, swaggerType.type, referencedModels);
|
||||||
if (prop.items) {
|
convertTypeTo$Ref(swaggerType);
|
||||||
processType(modelClass.app, prop.items.type, referencedModels);
|
if (swaggerType.items) {
|
||||||
convertTypeTo$Ref(prop.items);
|
processType(modelClass.app, swaggerType.items.type, referencedModels);
|
||||||
|
convertTypeTo$Ref(swaggerType.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var desc = typeConverter.convertText(prop.description || prop.doc);
|
||||||
|
if (desc) swaggerType.description = desc;
|
||||||
|
|
||||||
// Required props sit in a per-model array.
|
// Required props sit in a per-model array.
|
||||||
if (prop.required || (prop.id && !prop.generated)) {
|
if (prop.required || (prop.id && !prop.generated)) {
|
||||||
required.push(key);
|
required.push(key);
|
||||||
|
@ -92,19 +105,17 @@ var modelHelper = module.exports = {
|
||||||
// Change mismatched keys.
|
// Change mismatched keys.
|
||||||
prop = translateDataTypeKeys(prop);
|
prop = translateDataTypeKeys(prop);
|
||||||
|
|
||||||
convertTypeTo$Ref(prop);
|
|
||||||
|
|
||||||
delete prop.required;
|
delete prop.required;
|
||||||
delete prop.id;
|
delete prop.id;
|
||||||
|
|
||||||
if (prop.description){
|
if (prop.description){
|
||||||
prop.description = Array.isArray(prop.description) ? prop.description.join('') : prop.description;
|
prop.description = typeConverter.convertText(prop.description);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign this back to the properties object.
|
// 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 (typeof propType === 'function' && propType.modelName) {
|
||||||
if (referencedModels.indexOf(propType) === -1) {
|
if (referencedModels.indexOf(propType) === -1) {
|
||||||
referencedModels.push(propType);
|
referencedModels.push(propType);
|
||||||
|
@ -133,12 +144,14 @@ var modelHelper = module.exports = {
|
||||||
out[name] = {
|
out[name] = {
|
||||||
id: name,
|
id: name,
|
||||||
additionalProperties: additionalProperties,
|
additionalProperties: additionalProperties,
|
||||||
|
description: typeConverter.convertText(
|
||||||
|
def.description || (def.settings && def.settings.description)),
|
||||||
properties: properties,
|
properties: properties,
|
||||||
required: required
|
required: required
|
||||||
};
|
};
|
||||||
|
|
||||||
if (def.description){
|
if (def.description){
|
||||||
out[name].description = Array.isArray(def.description) ? def.description.join('') : def.description;
|
out[name].description = typeConverter.convertText(def.description);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate model definitions for related models
|
// Generate model definitions for related models
|
||||||
|
@ -195,9 +208,12 @@ var modelHelper = module.exports = {
|
||||||
if (typeof propType === 'function') {
|
if (typeof propType === 'function') {
|
||||||
// See https://github.com/strongloop/loopback-explorer/issues/32
|
// See https://github.com/strongloop/loopback-explorer/issues/32
|
||||||
// The type can be a model class
|
// The type can be a model class
|
||||||
propType = propType.modelName || propType.name.toLowerCase();
|
return propType.modelName || propType.name.toLowerCase();
|
||||||
} else if(Array.isArray(propType)) {
|
} else if (Array.isArray(propType)) {
|
||||||
propType = 'array';
|
return 'array';
|
||||||
|
} else if (typeof propType === 'object') {
|
||||||
|
// Anonymous objects, they are allowed e.g. in accepts/returns definitions
|
||||||
|
return 'object';
|
||||||
}
|
}
|
||||||
return propType;
|
return propType;
|
||||||
},
|
},
|
||||||
|
@ -211,38 +227,63 @@ var modelHelper = module.exports = {
|
||||||
// Converts a prop defined with the LDL spec to one conforming to the
|
// Converts a prop defined with the LDL spec to one conforming to the
|
||||||
// Swagger spec.
|
// Swagger spec.
|
||||||
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives
|
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives
|
||||||
LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(prop) {
|
LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(ldlType) {
|
||||||
if (typeof prop === 'string') {
|
var SWAGGER_DATA_TYPE_FIELDS = [
|
||||||
prop = {
|
'format',
|
||||||
type: prop
|
'defaultValue',
|
||||||
}
|
'enum',
|
||||||
}
|
'items',
|
||||||
var out = _cloneDeep(prop);
|
'minimum',
|
||||||
out.type = modelHelper.getPropType(out.type);
|
'minItems',
|
||||||
|
'minLength',
|
||||||
|
'maximum',
|
||||||
|
'maxItems',
|
||||||
|
'maxLength',
|
||||||
|
'uniqueItems',
|
||||||
|
// loopback-explorer extensions
|
||||||
|
'length',
|
||||||
|
// https://www.npmjs.org/package/swagger-validation
|
||||||
|
'pattern'
|
||||||
|
];
|
||||||
|
|
||||||
if (out.type === 'array') {
|
// Rename LoopBack keys to Swagger keys
|
||||||
var hasItemType = Array.isArray(prop.type) && prop.type.length;
|
ldlType = translateDataTypeKeys(ldlType);
|
||||||
var arrayItem = hasItemType && prop.type[0];
|
|
||||||
|
|
||||||
|
// Pick only keys supported by Swagger
|
||||||
|
var swaggerType = _pick(ldlType, SWAGGER_DATA_TYPE_FIELDS);
|
||||||
|
|
||||||
|
swaggerType.type = modelHelper.getPropType(ldlType.type || ldlType);
|
||||||
|
|
||||||
|
if (swaggerType.type === 'array') {
|
||||||
|
var hasItemType = Array.isArray(ldlType.type) && ldlType.type.length;
|
||||||
|
var arrayItem = hasItemType && ldlType.type[0];
|
||||||
|
|
||||||
|
var newItems = null;
|
||||||
if (arrayItem) {
|
if (arrayItem) {
|
||||||
if(typeof arrayItem === 'object') {
|
if(typeof arrayItem === 'object') {
|
||||||
out.items = modelHelper.LDLPropToSwaggerDataType(arrayItem);
|
newItems = modelHelper.LDLPropToSwaggerDataType(arrayItem);
|
||||||
} else {
|
} else {
|
||||||
out.items = { type: modelHelper.getPropType(arrayItem) };
|
newItems = { type: modelHelper.getPropType(arrayItem) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// NOTE: `any` is not a supported type in swagger 1.2
|
// NOTE: `any` is not a supported type in swagger 1.2
|
||||||
out.items = { type: 'any' };
|
newItems = { type: 'any' };
|
||||||
}
|
}
|
||||||
} else if (out.type === 'date') {
|
if (typeof swaggerType.items !== 'object') {
|
||||||
out.type = 'string';
|
swaggerType.items = {};
|
||||||
out.format = 'date';
|
}
|
||||||
} else if (out.type === 'buffer') {
|
for (var key in newItems) {
|
||||||
out.type = 'string';
|
swaggerType.items[key] = newItems[key];
|
||||||
out.format = 'byte';
|
}
|
||||||
} else if (out.type === 'number') {
|
} else if (swaggerType.type === 'date') {
|
||||||
out.format = 'double'; // Since all JS numbers are doubles
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -5,9 +5,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var debug = require('debug')('loopback:explorer:routeHelpers');
|
var debug = require('debug')('loopback:explorer:routeHelpers');
|
||||||
var _cloneDeep = require('lodash.clonedeep');
|
var _cloneDeep = require('lodash').cloneDeep;
|
||||||
var translateDataTypeKeys = require('./translate-data-type-keys');
|
var _assign = require('lodash').assign;
|
||||||
var modelHelper = require('./model-helper');
|
var modelHelper = require('./model-helper');
|
||||||
|
var typeConverter = require('./type-converter');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export the routeHelper singleton.
|
* Export the routeHelper singleton.
|
||||||
|
@ -61,13 +62,15 @@ var routeHelper = module.exports = {
|
||||||
if (typeof arg.http === 'function') return false;
|
if (typeof arg.http === 'function') return false;
|
||||||
// Don't show arguments set to the incoming http request.
|
// Don't show arguments set to the incoming http request.
|
||||||
// Please note that body needs to be shown, such as User.create().
|
// 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;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Translate LDL keys to Swagger keys.
|
|
||||||
accepts = accepts.map(translateDataTypeKeys);
|
|
||||||
|
|
||||||
// Turn accept definitions in to parameter docs.
|
// Turn accept definitions in to parameter docs.
|
||||||
accepts = accepts.map(routeHelper.acceptToParameter(route));
|
accepts = accepts.map(routeHelper.acceptToParameter(route));
|
||||||
|
|
||||||
|
@ -92,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.
|
// operation object.
|
||||||
if (returns && returns.length > 1) {
|
if (routeReturns && routeReturns.length > 1) {
|
||||||
// TODO ad-hoc model definition in the case of multiple return values.
|
// TODO ad-hoc model definition in the case of multiple return values.
|
||||||
returns = {model: 'object'};
|
routeReturns = { type: 'object' };
|
||||||
} else {
|
} 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;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,7 +173,7 @@ var routeHelper = module.exports = {
|
||||||
var returns = routeHelper.convertReturnsToSwagger(route, classDef);
|
var returns = routeHelper.convertReturnsToSwagger(route, classDef);
|
||||||
var responseMessages = [
|
var responseMessages = [
|
||||||
{
|
{
|
||||||
code: 200,
|
code: route.returns && route.returns.length ? 200 : 204,
|
||||||
message: 'Request was successful',
|
message: 'Request was successful',
|
||||||
responseModel: returns.model || prepareDataType(returns.type) || 'void'
|
responseModel: returns.model || prepareDataType(returns.type) || 'void'
|
||||||
}
|
}
|
||||||
|
@ -181,23 +184,29 @@ var routeHelper = module.exports = {
|
||||||
|
|
||||||
debug('route %j', route);
|
debug('route %j', route);
|
||||||
|
|
||||||
|
var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns);
|
||||||
|
|
||||||
var apiDoc = {
|
var apiDoc = {
|
||||||
path: routeHelper.convertPathFragments(route.path),
|
path: routeHelper.convertPathFragments(route.path),
|
||||||
// Create the operation doc. Use `extendWithType` to add the necessary
|
// Create the operation doc.
|
||||||
// `items` and `format` fields.
|
// 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({
|
operations: [routeHelper.extendWithType({
|
||||||
method: routeHelper.convertVerb(route.verb),
|
method: routeHelper.convertVerb(route.verb),
|
||||||
// [rfeng] Swagger UI doesn't escape '.' for jQuery selector
|
// [strml] remove leading model name from op, swagger uses leading
|
||||||
nickname: route.method.replace(/\./g, '_'),
|
// path as class name so it remains unique between models.
|
||||||
|
// route.method is always #{className}.#{methodName}
|
||||||
|
nickname: route.method.replace(/.*?\./, ''),
|
||||||
deprecated: route.deprecated,
|
deprecated: route.deprecated,
|
||||||
type: returns.model || returns.type || 'void',
|
|
||||||
summary: Array.isArray(route.description) ? route.description.join('') : route.description, // TODO(schoon) - Excerpt?
|
|
||||||
notes: Array.isArray(route.notes) ? route.notes.join('') : route.notes, // TODO(schoon) - `description` metadata?
|
|
||||||
consumes: ['application/json', 'application/xml', 'text/xml'],
|
consumes: ['application/json', 'application/xml', 'text/xml'],
|
||||||
produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'],
|
produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'],
|
||||||
parameters: accepts,
|
parameters: accepts,
|
||||||
responseMessages: responseMessages
|
responseMessages: responseMessages,
|
||||||
})]
|
type: returns.model || returns.type || 'void',
|
||||||
|
summary: typeConverter.convertText(route.description),
|
||||||
|
notes: typeConverter.convertText(route.notes)
|
||||||
|
}, returns)]
|
||||||
};
|
};
|
||||||
|
|
||||||
return apiDoc;
|
return apiDoc;
|
||||||
|
@ -264,10 +273,10 @@ var routeHelper = module.exports = {
|
||||||
minimum: accepts.minimum,
|
minimum: accepts.minimum,
|
||||||
maximum: accepts.maximum,
|
maximum: accepts.maximum,
|
||||||
allowMultiple: accepts.allowMultiple,
|
allowMultiple: accepts.allowMultiple,
|
||||||
description: Array.isArray(accepts.description) ? accepts.description.join('') : accepts.description
|
description: typeConverter.convertText(accepts.description)
|
||||||
};
|
};
|
||||||
|
|
||||||
out = routeHelper.extendWithType(out);
|
out = routeHelper.extendWithType(out, accepts);
|
||||||
|
|
||||||
// HACK: Derive the type from model
|
// HACK: Derive the type from model
|
||||||
if(out.name === 'data' && out.type === 'object') {
|
if(out.name === 'data' && out.type === 'object') {
|
||||||
|
@ -283,34 +292,37 @@ var routeHelper = module.exports = {
|
||||||
* a proper Swagger type and optional `format` and `items` fields.
|
* a proper Swagger type and optional `format` and `items` fields.
|
||||||
* Does not modify original object.
|
* Does not modify original object.
|
||||||
* @param {Object} obj Object to extend.
|
* @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);
|
obj = _cloneDeep(obj);
|
||||||
|
|
||||||
// Format the `type` property using our LDL converter.
|
// Format the `type` property using our LDL converter.
|
||||||
var typeDesc = modelHelper
|
var typeDesc = modelHelper.LDLPropToSwaggerDataType(ldlType);
|
||||||
.LDLPropToSwaggerDataType({type: obj.model || obj.type});
|
|
||||||
// The `typeDesc` may have additional attributes, such as
|
// The `typeDesc` may have additional attributes, such as
|
||||||
// `format` for non-primitive types.
|
// `format` for non-primitive types.
|
||||||
Object.keys(typeDesc).forEach(function(key){
|
Object.keys(typeDesc).forEach(function(key){
|
||||||
obj[key] = typeDesc[key];
|
obj[key] = typeDesc[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
//Ensure brief properties are first
|
//Ensure brief properties are first
|
||||||
if (typeof obj === 'object') {
|
if (typeof obj === 'object') {
|
||||||
var keysToSink = ['authorizations', 'consumes', 'notes', 'produces',
|
var keysToSink = ['authorizations', 'consumes', 'notes', 'produces',
|
||||||
'parameters', 'responseMessages', 'summary'];
|
'parameters', 'responseMessages', 'summary'];
|
||||||
var outKeys = Object.keys(obj);
|
var outKeys = Object.keys(obj);
|
||||||
for (var outKeyIdx in outKeys) {
|
for (var outKeyIdx in outKeys) {
|
||||||
var outKey = outKeys[outKeyIdx];
|
var outKey = outKeys[outKeyIdx];
|
||||||
if (keysToSink.indexOf(outKey) != -1) {
|
if (keysToSink.indexOf(outKey) != -1) {
|
||||||
var outValue = obj[outKey];
|
var outValue = obj[outKey];
|
||||||
delete obj[outKey];
|
delete obj[outKey];
|
||||||
obj[outKey] = outValue;
|
obj[outKey] = outValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_assign(obj, typeDesc);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
111
lib/swagger.js
111
lib/swagger.js
|
@ -9,10 +9,13 @@ module.exports = Swagger;
|
||||||
*/
|
*/
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var urlJoin = require('./url-join');
|
var urlJoin = require('./url-join');
|
||||||
var _defaults = require('lodash.defaults');
|
var _defaults = require('lodash').defaults;
|
||||||
var classHelper = require('./class-helper');
|
var classHelper = require('./class-helper');
|
||||||
var routeHelper = require('./route-helper');
|
var routeHelper = require('./route-helper');
|
||||||
var _cloneDeep = require('lodash.clonedeep');
|
var _cloneDeep = require('lodash').cloneDeep;
|
||||||
|
var modelHelper = require('./model-helper');
|
||||||
|
var cors = require('cors');
|
||||||
|
var typeConverter = require('./type-converter');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a remotable Swagger module for plugging into `RemoteObjects`.
|
* Create a remotable Swagger module for plugging into `RemoteObjects`.
|
||||||
|
@ -23,13 +26,25 @@ var _cloneDeep = require('lodash.clonedeep');
|
||||||
* @param {Object} opts Options.
|
* @param {Object} opts Options.
|
||||||
*/
|
*/
|
||||||
function Swagger(loopbackApplication, swaggerApp, opts) {
|
function Swagger(loopbackApplication, swaggerApp, opts) {
|
||||||
|
if (opts && opts.swaggerVersion)
|
||||||
|
console.warn('loopback-explorer\'s options.swaggerVersion is deprecated.');
|
||||||
|
|
||||||
opts = _defaults(opts || {}, {
|
opts = _defaults(opts || {}, {
|
||||||
swaggerVersion: '1.2',
|
swaggerVersion: '1.2',
|
||||||
basePath: loopbackApplication.get('restApiRoot') || '/api',
|
basePath: loopbackApplication.get('restApiRoot') || '/api',
|
||||||
resourcePath: 'resources',
|
resourcePath: 'resources',
|
||||||
// Default consumes/produces
|
// Default consumes/produces
|
||||||
consumes: ['application/json', 'application/x-www-form-urlencoded', 'application/xml', 'text/xml'],
|
consumes: [
|
||||||
produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'],
|
'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()
|
version: getVersion()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -39,6 +54,8 @@ function Swagger(loopbackApplication, swaggerApp, opts) {
|
||||||
var routes = adapter.allRoutes();
|
var routes = adapter.allRoutes();
|
||||||
var classes = remotes.classes();
|
var classes = remotes.classes();
|
||||||
|
|
||||||
|
setupCors(swaggerApp, remotes);
|
||||||
|
|
||||||
// These are the docs we will be sending from the /swagger endpoints.
|
// These are the docs we will be sending from the /swagger endpoints.
|
||||||
var resourceDoc = generateResourceDoc(opts);
|
var resourceDoc = generateResourceDoc(opts);
|
||||||
var apiDocs = {};
|
var apiDocs = {};
|
||||||
|
@ -46,15 +63,15 @@ function Swagger(loopbackApplication, swaggerApp, opts) {
|
||||||
// A class is an endpoint root; e.g. /users, /products, and so on.
|
// A class is an endpoint root; e.g. /users, /products, and so on.
|
||||||
classes.forEach(function (aClass) {
|
classes.forEach(function (aClass) {
|
||||||
var doc = apiDocs[aClass.name] = classHelper.generateAPIDoc(aClass, opts);
|
var doc = apiDocs[aClass.name] = classHelper.generateAPIDoc(aClass, opts);
|
||||||
var hasPublic = false;
|
var hasDocumented = false;
|
||||||
var methods = aClass.methods()
|
var methods = aClass.methods()
|
||||||
for (var methodKey in methods) {
|
for (var methodKey in methods) {
|
||||||
hasPublic = methods[methodKey].public;
|
hasDocumented = methods[methodKey].documented;
|
||||||
if (hasPublic) {
|
if (hasDocumented) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasPublic) {
|
if (hasDocumented) {
|
||||||
resourceDoc.apis.push(classHelper.generateResourceDocAPIEntry(aClass));
|
resourceDoc.apis.push(classHelper.generateResourceDocAPIEntry(aClass));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,11 +94,56 @@ function Swagger(loopbackApplication, swaggerApp, opts) {
|
||||||
return item.name === className;
|
return item.name === className;
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
if (route.public) {
|
if (route.documented) {
|
||||||
routeHelper.addRouteToAPIDeclaration(route, classDef, doc);
|
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) {
|
||||||
|
var type = param.type;
|
||||||
|
if (type === 'array' && param.items)
|
||||||
|
type = param.items.type;
|
||||||
|
|
||||||
|
addTypeToModels(type);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (routeDoc.type === 'array') {
|
||||||
|
addTypeToModels(routeDoc.items.type);
|
||||||
|
} else {
|
||||||
|
addTypeToModels(routeDoc.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
routeDoc.responseMessages.forEach(function(msg) {
|
||||||
|
addTypeToModels(msg.responseModel);
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
* The topmost Swagger resource is a description of all (non-Swagger)
|
||||||
* resources available on the system, and where to find more
|
* resources available on the system, and where to find more
|
||||||
|
@ -92,6 +154,13 @@ function Swagger(loopbackApplication, swaggerApp, opts) {
|
||||||
loopbackApplication.emit('swaggerResources', resourceDoc);
|
loopbackApplication.emit('swaggerResources', resourceDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Add a route to this remoting extension.
|
||||||
* @param {Application} app Express application.
|
* @param {Application} app Express application.
|
||||||
|
@ -103,6 +172,11 @@ function addRoute(app, uri, doc, opts) {
|
||||||
var hasBasePath = Object.keys(doc).indexOf('basePath') !== -1;
|
var hasBasePath = Object.keys(doc).indexOf('basePath') !== -1;
|
||||||
var initialPath = doc.basePath || '';
|
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) {
|
app.get(urlJoin('/', uri), function(req, res) {
|
||||||
|
|
||||||
// There's a few forces at play that require this "hack". The Swagger spec
|
// There's a few forces at play that require this "hack". The Swagger spec
|
||||||
|
@ -110,14 +184,17 @@ function addRoute(app, uri, doc, opts) {
|
||||||
// can't guarantee this path is either reachable or desirable if it's set
|
// can't guarantee this path is either reachable or desirable if it's set
|
||||||
// as a part of the options.
|
// as a part of the options.
|
||||||
//
|
//
|
||||||
// The simplest way around this is to reflect the value of the `Host` HTTP
|
// The simplest way around this is to reflect the value of the `Host` and/or
|
||||||
// header as the `basePath`. Because we pre-build the Swagger data, we don't
|
// `X-Forwarded-Host` HTTP headers as the `basePath`.
|
||||||
// know that header at the time the data is built.
|
// Because we pre-build the Swagger data, we don't know that header at
|
||||||
|
// the time the data is built.
|
||||||
if (hasBasePath) {
|
if (hasBasePath) {
|
||||||
var headers = req.headers;
|
var headers = req.headers;
|
||||||
var host = headers['x-forwarded-host'] || headers['X-Forwarded-Host'] || headers.Host || headers.host;
|
// NOTE header names (keys) are always all-lowercase
|
||||||
var protocol = headers['x-forwarded-proto'] || headers['X-Forwarded-Proto'] || opts.protocol || req.protocol
|
var proto = headers['x-forwarded-proto'] || opts.protocol || req.protocol;
|
||||||
doc.basePath = protocol + '://' + host + initialPath;
|
var prefix = opts.omitProtocolInBaseUrl ? '//' : proto + '://';
|
||||||
|
var host = headers['x-forwarded-host'] || headers.host;
|
||||||
|
doc.basePath = prefix + host + initialPath;
|
||||||
}
|
}
|
||||||
res.status(200).send(doc);
|
res.status(200).send(doc);
|
||||||
});
|
});
|
||||||
|
@ -133,7 +210,7 @@ function generateResourceDoc(opts) {
|
||||||
var apiInfo = _cloneDeep(opts.apiInfo);
|
var apiInfo = _cloneDeep(opts.apiInfo);
|
||||||
for (var propertyName in apiInfo) {
|
for (var propertyName in apiInfo) {
|
||||||
var property = apiInfo[propertyName];
|
var property = apiInfo[propertyName];
|
||||||
apiInfo[propertyName] = Array.isArray(property) ? property.join('') : property;
|
apiInfo[propertyName] = typeConverter.convertText(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -159,7 +236,7 @@ function getVersion() {
|
||||||
try {
|
try {
|
||||||
version = require(path.join(process.cwd(), 'package.json')).version;
|
version = require(path.join(process.cwd(), 'package.json')).version;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
version = '1';
|
version = '1.0.0';
|
||||||
}
|
}
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var _cloneDeep = require('lodash.clonedeep');
|
var _cloneDeep = require('lodash').cloneDeep;
|
||||||
|
|
||||||
// Keys that are different between LDL and Swagger
|
// Keys that are different between LDL and Swagger
|
||||||
var KEY_TRANSLATIONS = {
|
var KEY_TRANSLATIONS = {
|
||||||
// LDL : Swagger
|
// LDL : Swagger
|
||||||
'doc': 'description',
|
|
||||||
'default': 'defaultValue',
|
'default': 'defaultValue',
|
||||||
'min': 'minimum',
|
'min': 'minimum',
|
||||||
'max': 'maximum'
|
'max': 'maximum'
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "loopback-explorer",
|
"name": "loopback-explorer",
|
||||||
"version": "1.2.13",
|
"version": "1.8.2",
|
||||||
"description": "Browse and test your LoopBack app's APIs",
|
"description": "Browse and test your LoopBack app's APIs",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -22,19 +22,19 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"loopback": "git+https://github.com/shelbys/loopback.git",
|
"loopback": "git+https://github.com/shelbys/loopback.git",
|
||||||
"mocha": "~1.20.1",
|
"mocha": "^1.21.5",
|
||||||
"supertest": "~0.13.0",
|
"supertest": "~0.14.0",
|
||||||
"chai": "~1.9.1"
|
"chai": "^1.9.1"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"name": "Dual MIT/StrongLoop",
|
"name": "Dual MIT/StrongLoop",
|
||||||
"url": "https://github.com/strongloop/loopback-explorer/blob/master/LICENSE"
|
"url": "https://github.com/strongloop/loopback-explorer/blob/master/LICENSE"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"swagger-ui": "git+https://github.com/shelbys/swagger-ui.git",
|
"cors": "^2.4.2",
|
||||||
"debug": "~1.0.3",
|
"debug": "~1.0.3",
|
||||||
"lodash.clonedeep": "^2.4.1",
|
"express": "3.x",
|
||||||
"lodash.defaults": "^2.4.1",
|
"lodash": "^2.4.1",
|
||||||
"express": "3.x"
|
"swagger-ui": "git+https://github.com/shelbys/swagger-ui.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,4 +42,12 @@
|
||||||
/*
|
/*
|
||||||
FIXME: Separate the overrides from the rest of the styles, rather than override screen.css entirely.
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,30 +1,28 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>StrongLoop API Explorer</title>
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link href='https://fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'/>
|
<title>StrongLoop API Explorer</title>
|
||||||
<link href='css/reset.css' media='screen' rel='stylesheet' type='text/css'/>
|
<link href='css/reset.css' media='screen,print' rel='stylesheet' type='text/css'/>
|
||||||
<link href='css/screen.css' media='screen' rel='stylesheet' type='text/css'/>
|
<link href='css/screen.css' media='screen,print' rel='stylesheet' type='text/css'/>
|
||||||
<link href='css/reset.css' media='print' rel='stylesheet' type='text/css'/>
|
<link href='css/loopbackStyles.css' rel='stylesheet' type='text/css'/>
|
||||||
<link href='css/screen.css' media='print' rel='stylesheet' type='text/css'/>
|
<script type="text/javascript" src="lib/shred.bundle.js"></script>
|
||||||
<link href='css/loopbackStyles.css' rel='stylesheet' type='text/css'/>
|
<script src='lib/jquery-1.8.0.min.js' type='text/javascript'></script>
|
||||||
<script type="text/javascript" src="lib/shred.bundle.js"></script>
|
<script src='lib/jquery.slideto.min.js' type='text/javascript'></script>
|
||||||
<script src='lib/jquery-1.8.0.min.js' type='text/javascript'></script>
|
<script src='lib/jquery.wiggle.min.js' type='text/javascript'></script>
|
||||||
<script src='lib/jquery.slideto.min.js' type='text/javascript'></script>
|
<script src='lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
|
||||||
<script src='lib/jquery.wiggle.min.js' type='text/javascript'></script>
|
<script src='lib/handlebars-1.0.0.js' type='text/javascript'></script>
|
||||||
<script src='lib/jquery.ba-bbq.min.js' type='text/javascript'></script>
|
<script src='lib/underscore-min.js' type='text/javascript'></script>
|
||||||
<script src='lib/handlebars-1.0.0.js' type='text/javascript'></script>
|
<script src='lib/backbone-min.js' type='text/javascript'></script>
|
||||||
<script src='lib/underscore-min.js' type='text/javascript'></script>
|
<script src='lib/swagger.js' type='text/javascript'></script>
|
||||||
<script src='lib/backbone-min.js' type='text/javascript'></script>
|
<script src='swagger-ui.js' type='text/javascript'></script>
|
||||||
<script src='lib/swagger.js' type='text/javascript'></script>
|
<script src='lib/highlight.7.3.pack.js' type='text/javascript'></script>
|
||||||
<script src='swagger-ui.js' type='text/javascript'></script>
|
|
||||||
<script src='lib/highlight.7.3.pack.js' type='text/javascript'></script>
|
|
||||||
|
|
||||||
<!-- enabling this will enable oauth2 implicit scope support -->
|
<!-- enabling this will enable oauth2 implicit scope support -->
|
||||||
<script src='lib/swagger-oauth.js' type='text/javascript'></script>
|
<script src='lib/swagger-oauth.js' type='text/javascript'></script>
|
||||||
|
|
||||||
<!-- Init swagger UI. -->
|
<!-- Init swagger UI. -->
|
||||||
<script src='lib/loadSwaggerUI.js' type="text/javascript"></script>
|
<script src='lib/loadSwaggerUI.js' type="text/javascript"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="swagger-section">
|
<body class="swagger-section">
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
// Refactoring of inline script from index.html.
|
// Refactoring of inline script from index.html.
|
||||||
/*global SwaggerUi, log, ApiKeyAuthorization, hljs, window, $ */
|
/*global SwaggerUi, log, ApiKeyAuthorization, hljs, window, $ */
|
||||||
$(function() {
|
$(function() {
|
||||||
|
var lsKey = 'swagger_accessToken';
|
||||||
$.getJSON('config.json', function(config) {
|
$.getJSON('config.json', function(config) {
|
||||||
log(config);
|
log(config);
|
||||||
loadSwaggerUi(config);
|
loadSwaggerUi(config);
|
||||||
});
|
});
|
||||||
|
|
||||||
var accessToken;
|
var accessToken;
|
||||||
|
@ -35,6 +36,14 @@ $(function() {
|
||||||
$('#api_selector').submit(setAccessToken);
|
$('#api_selector').submit(setAccessToken);
|
||||||
$('#input_accessToken').keyup(onInputChange);
|
$('#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();
|
window.swaggerUi.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +58,22 @@ $(function() {
|
||||||
accessToken = key;
|
accessToken = key;
|
||||||
$('.accessTokenDisplay').text('Token Set.').addClass('set');
|
$('.accessTokenDisplay').text('Token Set.').addClass('set');
|
||||||
$('.accessTokenDisplay').attr('data-tooltip', 'Current Token: ' + key);
|
$('.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,30 @@
|
||||||
/**
|
'use strict';
|
||||||
* Created by ytang on 4/7/15.
|
|
||||||
*/
|
|
||||||
var classHelper = require('../lib/class-helper');
|
var classHelper = require('../lib/class-helper');
|
||||||
var loopback = require('loopback');
|
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
|
var _defaults = require('lodash').defaults;
|
||||||
|
|
||||||
describe('class-helper', function() {
|
describe('class-helper', function() {
|
||||||
|
it('joins array descriptions', function() {
|
||||||
|
var doc = generateResourceDocAPIEntry({
|
||||||
|
ctor: { settings: { description: [ 'line1', 'line2' ] } }
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
describe('#generateResourceDocAPIEntry', function() {
|
describe('#generateResourceDocAPIEntry', function() {
|
||||||
describe('when ctor.settings.description is an array of string', function() {
|
describe('when ctor.settings.description is an array of string', function() {
|
||||||
it('should return description as a string', function() {
|
it('should return description as a string', function() {
|
||||||
|
@ -21,7 +40,7 @@ describe('class-helper', function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = classHelper.generateResourceDocAPIEntry(aClass);
|
var result = classHelper.generateResourceDocAPIEntry(aClass);
|
||||||
expect(result.description).to.eql('123');
|
expect(result.description).to.eql("1\n2\n3");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,8 +59,24 @@ describe('class-helper', function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = classHelper.generateResourceDocAPIEntry(aClass);
|
var result = classHelper.generateResourceDocAPIEntry(aClass);
|
||||||
expect(result.description).to.eql('123');
|
expect(result.description).to.eql("1\n2\n3");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Easy wrapper around createRoute
|
||||||
|
function generateResourceDocAPIEntry(def) {
|
||||||
|
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'});
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,10 @@ var loopback = require('loopback');
|
||||||
var explorer = require('../');
|
var explorer = require('../');
|
||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var path = require('path');
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
|
var urlJoin = require('../lib/url-join');
|
||||||
|
var os = require('os');
|
||||||
|
|
||||||
describe('explorer', function() {
|
describe('explorer', function() {
|
||||||
|
|
||||||
|
@ -77,6 +80,83 @@ describe('explorer', function() {
|
||||||
done();
|
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() {
|
||||||
|
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 */' + os.EOL)
|
||||||
|
.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' + os.EOL)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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' + os.EOL)
|
||||||
|
.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' + os.EOL)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function givenLoopBackAppWithExplorer(explorerBase) {
|
function givenLoopBackAppWithExplorer(explorerBase) {
|
||||||
|
@ -88,7 +168,7 @@ describe('explorer', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureRestApiAndExplorer(app, explorerBase) {
|
function configureRestApiAndExplorer(app, explorerBase) {
|
||||||
var Product = loopback.Model.extend('product');
|
var Product = loopback.PersistedModel.extend('product');
|
||||||
Product.attachTo(loopback.memory());
|
Product.attachTo(loopback.memory());
|
||||||
app.model(Product);
|
app.model(Product);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
custom index.html
|
|
@ -0,0 +1 @@
|
||||||
|
/* custom swagger-ui file */
|
|
@ -1,6 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var modelHelper = require('../lib/model-helper');
|
var modelHelper = require('../lib/model-helper');
|
||||||
|
var _defaults = require('lodash').defaults;
|
||||||
var loopback = require('loopback');
|
var loopback = require('loopback');
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
|
|
||||||
|
@ -122,6 +123,27 @@ 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts model field `description`', function() {
|
||||||
|
var def = buildSwaggerModels({}, { description: 'a-description' });
|
||||||
|
expect(def).to.have.property('description', 'a-description');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('related models', function() {
|
describe('related models', function() {
|
||||||
|
@ -138,7 +160,7 @@ describe('model-helper', function() {
|
||||||
var Model1 = loopback.createModel('Model1', {
|
var Model1 = loopback.createModel('Model1', {
|
||||||
str: String, // 'string'
|
str: String, // 'string'
|
||||||
address: Model2
|
address: Model2
|
||||||
});
|
}, { models: { Model2: Model2 } });
|
||||||
var defs = modelHelper.generateModelDefinition(Model1, {});
|
var defs = modelHelper.generateModelDefinition(Model1, {});
|
||||||
expect(defs).has.property('Model1');
|
expect(defs).has.property('Model1');
|
||||||
expect(defs).has.property('Model2');
|
expect(defs).has.property('Model2');
|
||||||
|
@ -147,7 +169,7 @@ describe('model-helper', function() {
|
||||||
it('should include used models', function() {
|
it('should include used models', function() {
|
||||||
var Model4 = loopback.createModel('Model4', {street: String});
|
var Model4 = loopback.createModel('Model4', {street: String});
|
||||||
var Model3 = loopback.createModel('Model3', {
|
var Model3 = loopback.createModel('Model3', {
|
||||||
str: String, // 'string'
|
str: String // 'string'
|
||||||
}, {models: {model4: 'Model4'}});
|
}, {models: {model4: 'Model4'}});
|
||||||
var defs = modelHelper.generateModelDefinition(Model3, {});
|
var defs = modelHelper.generateModelDefinition(Model3, {});
|
||||||
expect(defs).has.property('Model3');
|
expect(defs).has.property('Model3');
|
||||||
|
@ -159,7 +181,7 @@ describe('model-helper', function() {
|
||||||
var Model5 = loopback.createModel('Model5', {
|
var Model5 = loopback.createModel('Model5', {
|
||||||
str: String, // 'string'
|
str: String, // 'string'
|
||||||
addresses: [Model6]
|
addresses: [Model6]
|
||||||
});
|
}, { models: { Model6: Model6 } });
|
||||||
var defs = modelHelper.generateModelDefinition(Model5, {});
|
var defs = modelHelper.generateModelDefinition(Model5, {});
|
||||||
expect(defs).has.property('Model5');
|
expect(defs).has.property('Model5');
|
||||||
expect(defs).has.property('Model6');
|
expect(defs).has.property('Model6');
|
||||||
|
@ -176,6 +198,18 @@ describe('model-helper', function() {
|
||||||
expect(Object.keys(defs)).has.property('length', 1);
|
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() {
|
describe('hidden properties', function() {
|
||||||
|
@ -193,53 +227,66 @@ describe('model-helper', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#generateModelDefinition', function(){
|
describe('#generateModelDefinition', function() {
|
||||||
it('should convert top level array description to string', function(){
|
it('should convert top level array description to string', function () {
|
||||||
var model = {};
|
var model = {};
|
||||||
model.definition = {
|
model.definition = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
description: ['1','2','3'],
|
description: ['1', '2', '3'],
|
||||||
properties: {}
|
properties: {}
|
||||||
};
|
};
|
||||||
var models = {};
|
var models = {};
|
||||||
modelHelper.generateModelDefinition(model, models);
|
modelHelper.generateModelDefinition(model, models);
|
||||||
expect(models.test.description).to.equal('123');
|
expect(models.test.description).to.equal("1\n2\n3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert property level array description to string', function(){
|
it('should convert property level array description to string', function () {
|
||||||
var model = {};
|
var model = {};
|
||||||
model.definition = {
|
model.definition = {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
properties: {
|
properties: {
|
||||||
prop1: {
|
prop1: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: ['1','2','3']
|
description: ['1', '2', '3']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var models = {};
|
var models = {};
|
||||||
modelHelper.generateModelDefinition(model, models);
|
modelHelper.generateModelDefinition(model, models);
|
||||||
expect(models.test.properties.prop1.description).to.equal('123');
|
expect(models.test.properties.prop1.description).to.equal("1\n2\n3");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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.
|
// Simulates the format of a remoting class.
|
||||||
function buildSwaggerModels(model) {
|
function buildSwaggerModels(modelProperties, modelOptions) {
|
||||||
var aClass = createModelCtor(model);
|
var aClass = createModelCtor(modelProperties, modelOptions);
|
||||||
return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel;
|
return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModelCtor(model) {
|
function createModelCtor(properties, modelOptions) {
|
||||||
Object.keys(model).forEach(function(name) {
|
Object.keys(properties).forEach(function(name) {
|
||||||
model[name] = {type: model[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 = {
|
var aClass = {
|
||||||
ctor: {
|
ctor: {
|
||||||
definition: {
|
definition: definition
|
||||||
name: 'testModel',
|
|
||||||
properties: model
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return aClass;
|
return aClass;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
var routeHelper = require('../lib/route-helper');
|
var routeHelper = require('../lib/route-helper');
|
||||||
var expect = require('chai').expect;
|
var expect = require('chai').expect;
|
||||||
var _defaults = require('lodash.defaults');
|
var _defaults = require('lodash').defaults;
|
||||||
|
|
||||||
describe('route-helper', function() {
|
describe('route-helper', function() {
|
||||||
it('returns "object" when a route has multiple return values', function() {
|
it('returns "object" when a route has multiple return values', function() {
|
||||||
|
@ -13,7 +13,8 @@ describe('route-helper', function() {
|
||||||
{ arg: 'avg', type: 'number' }
|
{ arg: 'avg', type: 'number' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
expect(doc.operations[0].responseMessages[0].responseModel).to.equal('object');
|
expect(doc.operations[0].type).to.equal('object');
|
||||||
|
expect(getResponseType(doc.operations[0])).to.equal('object');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('converts path params when they exist in the route name', function() {
|
it('converts path params when they exist in the route name', function() {
|
||||||
|
@ -60,7 +61,11 @@ describe('route-helper', function() {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
var opDoc = doc.operations[0];
|
var opDoc = doc.operations[0];
|
||||||
expect(opDoc.responseMessages[0].responseModel).to.equal('[customType]');
|
expect(getResponseType(opDoc)).to.eql('[customType]');
|
||||||
|
|
||||||
|
// 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('correctly converts return types (format)', function() {
|
it('correctly converts return types (format)', function() {
|
||||||
|
@ -70,45 +75,182 @@ describe('route-helper', function() {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
var opDoc = doc.operations[0];
|
var opDoc = doc.operations[0];
|
||||||
expect(opDoc.responseMessages[0].responseModel).to.equal('string');
|
expect(opDoc.type).to.equal('string');
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#acceptToParameter', function(){
|
describe('#acceptToParameter', function(){
|
||||||
it('should return function that converts accepts.description from array of string to string', function(){
|
it('should return function that converts accepts.description from array of string to string', function(){
|
||||||
var f = routeHelper.acceptToParameter({verb: 'get', path: 'path'});
|
var f = routeHelper.acceptToParameter({verb: 'get', path: 'path'});
|
||||||
var result = f({description: ['1','2','3']});
|
var result = f({description: ['1','2','3']});
|
||||||
expect(result.description).to.eql('123');
|
expect(result.description).to.eql("1\n2\n3");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#routeToAPIDoc', function(){
|
describe('#routeToAPIDoc', function() {
|
||||||
it('should convert route.description from array fo string to string', function(){
|
it('should convert route.description from array fo string to string', function () {
|
||||||
var result = routeHelper.routeToAPIDoc({
|
var result = routeHelper.routeToAPIDoc({
|
||||||
method: 'someMethod',
|
method: 'someMethod',
|
||||||
verb: 'get',
|
verb: 'get',
|
||||||
path: 'path',
|
path: 'path',
|
||||||
description:['1','2','3']
|
description: ['1', '2', '3']
|
||||||
});
|
});
|
||||||
expect(result.operations[0].summary).to.eql('123');
|
expect(result.operations[0].summary).to.eql("1\n2\n3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should convert route.notes from array fo string to string', function(){
|
it('should convert route.notes from array fo string to string', function () {
|
||||||
var result = routeHelper.routeToAPIDoc({
|
var result = routeHelper.routeToAPIDoc({
|
||||||
method: 'someMethod',
|
method: 'someMethod',
|
||||||
verb: 'get',
|
verb: 'get',
|
||||||
path: 'path',
|
path: 'path',
|
||||||
notes:['1','2','3']
|
notes: ['1', '2', '3']
|
||||||
});
|
});
|
||||||
expect(result.operations[0].notes).to.eql('123');
|
expect(result.operations[0].notes).to.eql("1\n2\n3");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes `deprecated` metadata', function() {
|
||||||
|
var doc = createAPIDoc({
|
||||||
|
deprecated: 'true'
|
||||||
|
});
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('includes the default response message with code 200', 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'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the response code 204 when `returns` is empty', 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'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
// Easy wrapper around createRoute
|
||||||
function createAPIDoc(def) {
|
function createAPIDoc(def) {
|
||||||
return routeHelper.routeToAPIDoc(_defaults(def, {
|
return routeHelper.routeToAPIDoc(_defaults(def || {}, {
|
||||||
path: '/test',
|
path: '/test',
|
||||||
verb: 'GET',
|
verb: 'GET',
|
||||||
method: 'test.get'
|
method: 'test.get'
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getResponseType(operationDoc) {
|
||||||
|
return operationDoc.responseMessages[0].responseModel;
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ describe('swagger definition', function() {
|
||||||
describe('basePath', function() {
|
describe('basePath', function() {
|
||||||
// No basepath on resource doc in 1.2
|
// No basepath on resource doc in 1.2
|
||||||
it('no longer exists on resource doc', function(done) {
|
it('no longer exists on resource doc', function(done) {
|
||||||
var app = mountSwagger();
|
var app = givenAppWithSwagger();
|
||||||
|
|
||||||
var getReq = getSwaggerResources(app);
|
var getReq = getSwaggerResources(app);
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -24,7 +24,7 @@ describe('swagger definition', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is "http://{host}/api" by default', function(done) {
|
it('is "http://{host}/api" by default', function(done) {
|
||||||
var app = mountSwagger();
|
var app = givenAppWithSwagger();
|
||||||
|
|
||||||
var getReq = getAPIDeclaration(app, 'products');
|
var getReq = getAPIDeclaration(app, 'products');
|
||||||
getReq.end(function(err, res) {
|
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){
|
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');
|
var getReq = getAPIDeclaration(app, 'products');
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -47,7 +47,7 @@ describe('swagger definition', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('infers API basePath from app', function(done){
|
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');
|
var getReq = getAPIDeclaration(app, 'products');
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -60,7 +60,7 @@ describe('swagger definition', function() {
|
||||||
|
|
||||||
it('is reachable when explorer mounting location is changed', function(done){
|
it('is reachable when explorer mounting location is changed', function(done){
|
||||||
var explorerRoot = '/erforscher';
|
var explorerRoot = '/erforscher';
|
||||||
var app = mountSwagger({}, {explorerRoot: explorerRoot});
|
var app = givenAppWithSwagger({}, {explorerRoot: explorerRoot});
|
||||||
|
|
||||||
var getReq = getSwaggerResources(app, explorerRoot, 'products');
|
var getReq = getSwaggerResources(app, explorerRoot, 'products');
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -71,7 +71,7 @@ describe('swagger definition', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects a hardcoded protocol (behind SSL terminator)', function(done){
|
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');
|
var getReq = getAPIDeclaration(app, 'products');
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -81,11 +81,49 @@ describe('swagger definition', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('respects X-Forwarded-Host header (behind a proxy)', function(done) {
|
||||||
|
var app = givenAppWithSwagger();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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() {
|
describe('Model definition attributes', function() {
|
||||||
it('Properly defines basic attributes', function(done) {
|
it('Properly defines basic attributes', function(done) {
|
||||||
var app = mountSwagger();
|
var app = givenAppWithSwagger();
|
||||||
|
|
||||||
var getReq = getAPIDeclaration(app, 'products');
|
var getReq = getAPIDeclaration(app, 'products');
|
||||||
getReq.end(function(err, res) {
|
getReq.end(function(err, res) {
|
||||||
|
@ -104,39 +142,257 @@ describe('swagger definition', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('includes `consumes`', function(done) {
|
||||||
|
var app = givenAppWithSwagger();
|
||||||
|
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 = givenAppWithSwagger();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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'
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cross-origin resource sharing', function() {
|
||||||
|
it('allows cross-origin requests by default', function(done) {
|
||||||
|
var app = givenAppWithSwagger();
|
||||||
|
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 = givenAppWithSwagger({}, { 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) {
|
function getSwaggerResources(app, restPath, classPath) {
|
||||||
return request(app)
|
return request(app)
|
||||||
.get(urlJoin(restPath || '/explorer', '/resources', classPath || ''))
|
.get(urlJoin(restPath || '/explorer', '/resources', classPath || ''))
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.expect('Content-Type', /json/)
|
.expect(200)
|
||||||
.expect(200);
|
.expect('Content-Type', /json/);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAPIDeclaration(app, className) {
|
function getAPIDeclaration(app, className) {
|
||||||
return getSwaggerResources(app, '', urlJoin('/', className));
|
return getSwaggerResources(app, '', urlJoin('/', className));
|
||||||
}
|
}
|
||||||
|
|
||||||
function mountSwagger(options, addlOptions) {
|
function givenAppWithSwagger(swaggerOptions, appConfig) {
|
||||||
addlOptions = addlOptions || {};
|
appConfig = appConfig || {};
|
||||||
var app = createLoopbackAppWithModel(addlOptions.apiRoot);
|
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();
|
var swaggerApp = express();
|
||||||
swagger(app, swaggerApp, options);
|
swagger(app, swaggerApp, options);
|
||||||
app.use(addlOptions.explorerRoot || '/explorer', swaggerApp);
|
app.use(app.get('explorerRoot') || '/explorer', swaggerApp);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoopbackAppWithModel(apiRoot) {
|
function createLoopbackAppWithModel(apiRoot) {
|
||||||
var app = loopback();
|
var app = loopback();
|
||||||
|
|
||||||
|
app.dataSource('db', { connector: 'memory' });
|
||||||
|
|
||||||
var Product = loopback.Model.extend('product', {
|
var Product = loopback.Model.extend('product', {
|
||||||
foo: {type: 'string', required: true},
|
foo: {type: 'string', required: true},
|
||||||
bar: 'string',
|
bar: 'string',
|
||||||
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
|
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
|
||||||
});
|
});
|
||||||
Product.attachTo(loopback.memory());
|
app.model(Product, { dataSource: 'db'});
|
||||||
app.model(Product);
|
|
||||||
|
|
||||||
// Simulate a restApiRoot set in config
|
// Simulate a restApiRoot set in config
|
||||||
app.set('restApiRoot', apiRoot || '/api');
|
app.set('restApiRoot', apiRoot || '/api');
|
||||||
|
@ -144,4 +400,33 @@ describe('swagger definition', function() {
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function givenSharedMethod(model, name, metadata) {
|
||||||
|
model[name] = function(){};
|
||||||
|
loopback.remoteMethod(model[name], metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue