diff --git a/index.js b/index.js index 74fa045..5b1177d 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,8 @@ var url = require('url'); var path = require('path'); var urlJoin = require('./lib/url-join'); var _defaults = require('lodash').defaults; -var swagger = require('./lib/swagger'); +var cors = require('cors'); +var createSwaggerObject = require('loopback-swagger').generateSwaggerSpec; var SWAGGER_UI_ROOT = require('strong-swagger-ui/index').dist; var STATIC_ROOT = path.join(__dirname, 'public'); @@ -42,7 +43,7 @@ function routes(loopbackApplication, options) { var router = new loopback.Router(); - swagger.mountSwagger(loopbackApplication, router, options); + mountSwagger(loopbackApplication, router, options); // config.json is loaded by swagger-ui. The server should respond // with the relative URI of the resource doc. @@ -81,3 +82,34 @@ function routes(loopbackApplication, options) { return router; } + +/** + * Setup Swagger documentation on the given express app. + * + * @param {Application} loopbackApplication The loopback application to + * document. + * @param {Application} swaggerApp Swagger application used for hosting + * swagger documentation. + * @param {Object} opts Options. + */ +function mountSwagger(loopbackApplication, swaggerApp, opts) { + var swaggerObject = createSwaggerObject(loopbackApplication, opts); + + var resourcePath = opts && opts.resourcePath || 'swagger.json'; + if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath; + + var remotes = loopbackApplication.remotes(); + setupCors(swaggerApp, remotes); + + swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) { + res.status(200).send(swaggerObject); + }); +} + +function setupCors(swaggerApp, remotes) { + var corsOptions = remotes.options && remotes.options.cors || + { origin: true, credentials: true }; + + // TODO(bajtos) Skip CORS when remotes.options.cors === false + swaggerApp.use(cors(corsOptions)); +} diff --git a/lib/model-helper.js b/lib/model-helper.js deleted file mode 100644 index c0a5863..0000000 --- a/lib/model-helper.js +++ /dev/null @@ -1,122 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var schemaBuilder = require('./schema-builder'); -var typeConverter = require('./type-converter'); -var TypeRegistry = require('./type-registry'); - -/** - * Export the modelHelper singleton. - */ -var modelHelper = module.exports = { - /** - * Given a class (from remotes.classes()), generate a model definition. - * This is used to generate the schema at the top of many endpoints. - * @param {Class} modelClass Model class. - * @param {TypeRegistry} typeRegistry Registry of types and models. - * @return {Object} Associated model definition. - */ - registerModelDefinition: function(modelCtor, typeRegistry) { - var lbdef = modelCtor.definition; - - if (!lbdef) { - // The model does not have any definition, it was most likely - // created as a placeholder for an unknown property type - return; - } - - var name = lbdef.name; - if (typeRegistry.isDefined(name)) { - // The model is already included - return; - } - - var swaggerDef = { - description: typeConverter.convertText( - lbdef.description || (lbdef.settings && lbdef.settings.description)), - properties: {}, - required: [] - }; - - var properties = lbdef.rawProperties || lbdef.properties; - - // Iterate through each property in the model definition. - // Types may be defined as constructors (e.g. String, Date, etc.), - // or as strings; swaggerSchema.builFromLoopBackType() will take - // care of the conversion. - Object.keys(properties).forEach(function(key) { - var prop = properties[key]; - - // Hide hidden properties. - if (modelHelper.isHiddenProperty(lbdef, key)) - return; - - // Eke a type out of the constructors we were passed. - var schema = schemaBuilder.buildFromLoopBackType(prop, typeRegistry); - - var desc = typeConverter.convertText(prop.description || prop.doc); - if (desc) schema.description = desc; - - // Required props sit in a per-model array. - if (prop.required || (prop.id && !prop.generated)) { - swaggerDef.required.push(key); - } - - // Assign the schema to the properties object. - swaggerDef.properties[key] = schema; - }); - - if (lbdef.settings) { - var strict = lbdef.settings.strict; - var additionalProperties = lbdef.settings.additionalProperties; - var notAllowAdditionalProperties = strict || (additionalProperties !== true); - if (notAllowAdditionalProperties){ - swaggerDef.additionalProperties = false; - } - } - - if (!swaggerDef.required.length) { - // "required" must have at least one item when present - delete swaggerDef.required; - } - - typeRegistry.register(name, swaggerDef); - - // Add models from settings - if (lbdef.settings && lbdef.settings.models) { - for (var m in lbdef.settings.models) { - var model = modelCtor[m]; - if (typeof model !== 'function' || !model.modelName) continue; - modelHelper.registerModelDefinition(model, typeRegistry); - // TODO it shouldn't be necessary to reference the model here, - // let accepts/returns/property reference it instead - typeRegistry.reference(model.modelName); - } - } - - // Generate model definitions for related models - for (var r in modelCtor.relations) { - var rel = modelCtor.relations[r]; - if (rel.modelTo) { - modelHelper.registerModelDefinition(rel.modelTo, typeRegistry); - // TODO it shouldn't be necessary to reference the model here, - // let accepts/returns/property reference it instead - typeRegistry.reference(rel.modelTo.modelName); - } - if (rel.modelThrough) { - modelHelper.registerModelDefinition(rel.modelThrough, typeRegistry); - // TODO it shouldn't be necessary to reference the model here, - // let accepts/returns/property reference it instead - typeRegistry.reference(rel.modelThrough.modelName); - } - } - }, - - isHiddenProperty: function(definition, propName) { - return definition.settings && - Array.isArray(definition.settings.hidden) && - definition.settings.hidden.indexOf(propName) !== -1; - }, -}; diff --git a/lib/route-helper.js b/lib/route-helper.js deleted file mode 100644 index 8dea0e8..0000000 --- a/lib/route-helper.js +++ /dev/null @@ -1,252 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ - -var debug = require('debug')('loopback:explorer:routeHelpers'); -var _assign = require('lodash').assign; -var typeConverter = require('./type-converter'); -var schemaBuilder = require('./schema-builder'); - -/** - * Export the routeHelper singleton. - */ -var routeHelper = module.exports = { - /** - * Given a route, generate an API description and add it to the doc. - * If a route shares a path with another route (same path, different verb), - * add it as a new operation under that path entry. - * - * Routes can be translated to API declaration 'operations', - * but they need a little massaging first. The `accepts` and - * `returns` declarations need some basic conversions to be compatible. - * - * This method will convert the route and add it to the doc. - * @param {Route} route Strong Remoting Route object. - * @param {Class} classDef Strong Remoting class. - * @param {TypeRegistry} typeRegistry Registry of types and models. - * @param {Object} paths Swagger Path Object, - * see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#pathsObject - */ - addRouteToSwaggerPaths: function(route, classDef, typeRegistry, paths) { - var entryToAdd = routeHelper.routeToPathEntry(route, classDef, - typeRegistry); - if (!(entryToAdd.path in paths)) { - paths[entryToAdd.path] = {}; - } - paths[entryToAdd.path][entryToAdd.method] = entryToAdd.operation; - }, - - /** - * Massage route.accepts. - * @param {Object} route Strong Remoting Route object. - * @param {Class} classDef Strong Remoting class. - * @param {TypeRegistry} typeRegistry Registry of types and models. - * @return {Array} Array of param docs. - */ - convertAcceptsToSwagger: function(route, classDef, typeRegistry) { - var accepts = route.accepts || []; - var split = route.method.split('.'); - if (classDef && classDef.sharedCtor && - classDef.sharedCtor.accepts && split.length > 2 /* HACK */) { - accepts = accepts.concat(classDef.sharedCtor.accepts); - } - - // Filter out parameters that are generated from the incoming request, - // or generated by functions that use those resources. - accepts = accepts.filter(function(arg) { - if (!arg.http) return true; - // Don't show derived arguments. - if (typeof arg.http === 'function') return false; - // Don't show arguments set to the incoming http request. - // Please note that body needs to be shown, such as User.create(). - if (arg.http.source === 'req' || - arg.http.source === 'res' || - arg.http.source === 'context') { - - return false; - } - return true; - }); - - // Turn accept definitions in to parameter docs. - accepts = accepts.map( - routeHelper.acceptToParameter(route, classDef, typeRegistry)); - - return accepts; - }, - - /** - * Massage route.returns. - * @param {Object} route Strong Remoting Route object. - * @return {Object} A single returns param doc. - */ - convertReturnsToSwagger: function(route, typeRegistry) { - var routeReturns = route.returns; - if (!routeReturns || !routeReturns.length) { - // An operation that returns nothing will have - // no schema declaration for its response. - return undefined; - } - - if (routeReturns.length === 1 && routeReturns[0].root) { - if (routeReturns[0].model) - return { $ref: typeRegistry.reference(routeReturns[0].model) }; - return schemaBuilder.buildFromLoopBackType(routeReturns[0], typeRegistry); - } - - // Convert `returns` into a single object for later conversion into an - // operation object. - // TODO ad-hoc model definition in the case of multiple return values. - // It is enough to replace 'object' with an anonymous type definition - // based on all routeReturn items. The schema converter should take - // care of the remaning conversions. - var def = { type: 'object' }; - return schemaBuilder.buildFromLoopBackType(def, typeRegistry); - }, - - /** - * Converts from an sl-remoting-formatted "Route" description to a - * Swagger-formatted "Path Item Object" - * See swagger-spec/2.0.md#pathItemObject - */ - routeToPathEntry: function(route, classDef, typeRegistry) { - // Some parameters need to be altered; eventually most of this should - // be removed. - var accepts = routeHelper.convertAcceptsToSwagger(route, classDef, - typeRegistry); - var returns = routeHelper.convertReturnsToSwagger(route, typeRegistry); - var defaultCode = route.returns && route.returns.length ? 200 : 204; - // TODO - support strong-remoting's option for a custom response code - - var responseMessages = {}; - responseMessages[defaultCode] = { - description: 'Request was successful', - schema: returns, - // TODO - headers, examples - }; - - if (route.errors) { - // TODO define new LDL syntax that is status-code-indexed - // and which allow users to specify headers & examples - route.errors.forEach(function(msg) { - responseMessages[msg.code] = { - description: msg.message, - schema: schemaBuilder.buildFromLoopBackType(msg.responseModel, - typeRegistry), - // TODO - headers, examples - }; - }); - } - - debug('route %j', route); - - var tags = []; - if (classDef && classDef.name) { - tags.push(classDef.name); - } - - var entry = { - path: routeHelper.convertPathFragments(route.path), - method: routeHelper.convertVerb(route.verb), - operation: { - tags: tags, - summary: typeConverter.convertText(route.description), - description: typeConverter.convertText(route.notes), - // [bajtos] We used to remove leading model name from the operation - // name for Swagger Spec 1.2. Swagger Spec 2.0 requires - // operation ids to be unique, thus we have to include the model name. - operationId: route.method, - // [bajtos] we are omitting consumes and produces, as they are same - // for all methods and they are already specified in top-level fields - parameters: accepts, - responses: responseMessages, - deprecated: !!route.deprecated, - // TODO: security - } - }; - - return entry; - }, - - convertPathFragments: function convertPathFragments(path) { - return path.split('/').map(function (fragment) { - if (fragment.charAt(0) === ':') { - return '{' + fragment.slice(1) + '}'; - } - return fragment; - }).join('/'); - }, - - convertVerb: function convertVerb(verb) { - if (verb.toLowerCase() === 'all') { - return 'post'; - } - - if (verb.toLowerCase() === 'del') { - return 'delete'; - } - - return verb.toLowerCase(); - }, - - /** - * A generator to convert from an sl-remoting-formatted "Accepts" description - * to a Swagger-formatted "Parameter" description. - */ - acceptToParameter: function acceptToParameter(route, classDef, typeRegistry) { - var DEFAULT_TYPE = - route.verb.toLowerCase() === 'get' ? 'query' : 'formData'; - - return function (accepts) { - var name = accepts.name || accepts.arg; - var paramType = DEFAULT_TYPE; - - // TODO: Regex. This is leaky. - if (route.path.indexOf(':' + name) !== -1) { - paramType = 'path'; - } - - // Check the http settings for the argument - if (accepts.http && accepts.http.source) { - paramType = accepts.http.source; - } - - // TODO: ensure that paramType has a valid value - // path, query, header, body, formData - // See swagger-spec/2.0.md#parameterObject - - var paramObject = { - name: name, - in: paramType, - description: typeConverter.convertText(accepts.description), - required: !!accepts.required - }; - - var schema = schemaBuilder.buildFromLoopBackType(accepts, typeRegistry); - if (paramType === 'body') { - // HACK: Derive the type from model - if (paramObject.name === 'data' && schema.type === 'object') { - paramObject.schema = { $ref: typeRegistry.reference(classDef.name) }; - } else { - paramObject.schema = schema; - } - } else { - var isComplexType = schema.type === 'object' || - schema.type === 'array' || - schema.$ref; - if (isComplexType) { - paramObject.type = 'string'; - paramObject.format = 'JSON'; - // TODO support array of primitive types - // and map them to Swagger array of primitive types - } else { - _assign(paramObject, schema); - } - } - - return paramObject; - }; - }, -}; diff --git a/lib/schema-builder.js b/lib/schema-builder.js deleted file mode 100644 index 512046c..0000000 --- a/lib/schema-builder.js +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var typeConverter = require('./type-converter'); - -var TYPES_PRIMITIVE = [ - 'boolean', - 'integer', - 'number', - 'null', - 'string', - 'object', - 'array' -]; - -var KEY_TRANSLATIONS = { - // LDL : Swagger - min: 'minimum', - max: 'maximum', - length: 'maxLength', -}; - -var SWAGGER_DATA_TYPE_FIELDS = [ - 'format', - 'default', - 'enum', - 'minimum', - 'minItems', - 'minLength', - 'maximum', - 'maxItems', - 'maxLength', - 'uniqueItems', - 'pattern' -]; - -/** - * Build a Swagger Schema Object and/or Parameter Object from LoopBack - * type descriptor. - * - * @param {String|Function|Array|Object} ldlDef The loopback type to convert, - * the value should be one of the following: - * - a string value (type name), e.g. `'string'` or `'MyModel'` - * - a constructor function, e.g. `String` or `MyModel` - * - an array of a single item in `lbType` format - * - an object containing a `type` property with string/function/array value - * and validation fields like `length` or `max` - * @param {TypeRegistry} typeRegistry The registry of known types and models. - * @returns {Object} Swagger Schema Object that can be used as `schema` field - * or as a base for Parameter Object. - */ -exports.buildFromLoopBackType = function(ldlDef, typeRegistry) { - assert(!!typeRegistry, 'typeRegistry is a required parameter'); - - // Normalize non-object values to object format `{ type: XYZ }` - if (typeof ldlDef === 'string' || typeof ldlDef === 'function') { - ldlDef = { type: ldlDef }; - } else if (Array.isArray(ldlDef)) { - ldlDef = { type: ldlDef }; - } - - var schema = exports.buildMetadata(ldlDef); - var ldlType = exports.getLdlTypeName(ldlDef.type); - - if (Array.isArray(ldlType)) { - var itemLdl = ldlType[0] || 'any'; - var itemSchema = exports.buildFromLoopBackType(itemLdl, typeRegistry); - schema.type = 'array'; - schema.items = itemSchema; - return schema; - } - - var ldlTypeLowerCase = ldlType.toLowerCase(); - switch (ldlTypeLowerCase) { - case 'date': - schema.type = 'string'; - schema.format = 'date'; - break; - case 'buffer': - schema.type = 'string'; - schema.format = 'byte'; - break; - case 'number': - schema.type = 'number'; - schema.format = schema.format || 'double'; // All JS numbers are doubles - break; - case 'any': - schema.$ref = typeRegistry.reference('x-any'); - break; - default: - if (exports.isPrimitiveType(ldlTypeLowerCase)) { - schema.type = ldlTypeLowerCase; - } else { - // TODO - register anonymous types - schema.$ref = typeRegistry.reference(ldlType); - } - } - return schema; -}; - -/** - * @param {String|Function|Array|Object} ldlType LDL type - * @returns {String|Array} Type name - */ -exports.getLdlTypeName = function(ldlType) { - // Value "array" is a shortcut for `['any']` - if (ldlType === 'array') { - return ['any']; - } - - if (typeof ldlType === 'string') { - var arrayMatch = ldlType.match(/^\[(.*)\]$/); - return arrayMatch ? [arrayMatch[1]] : ldlType; - } - - if (typeof ldlType === 'function') { - return ldlType.modelName || ldlType.name; - } - - if (Array.isArray(ldlType)) { - return ldlType; - } - - if (typeof ldlType === 'object') { - // Anonymous objects, they are allowed e.g. in accepts/returns definitions - // TODO(bajtos) Build a named schema for this anonymous object - return 'object'; - } - - if (ldlType === undefined) { - return 'any'; - } - - console.error('Warning: unknown LDL type %j, using "any" instead', ldlType); - return 'any'; -}; - -/** - * Convert validations and other metadata from LDL format to Swagger format. - * @param {Object} ldlDef LDL property/argument definition, - * for example `{ type: 'string', maxLength: 64 }`. - * @return {Object} Metadata in Swagger format. - */ -exports.buildMetadata = function(ldlDef) { - var result = {}; - var key; - - for (key in KEY_TRANSLATIONS) { - if (key in ldlDef) - result[KEY_TRANSLATIONS[key]] = ldlDef[key]; - } - - for (var ix in SWAGGER_DATA_TYPE_FIELDS) { - key = SWAGGER_DATA_TYPE_FIELDS[ix]; - if (key in ldlDef) - result[key] = ldlDef[key]; - } - - if (ldlDef.description) { - result.description = typeConverter.convertText(ldlDef.description); - } else if (ldlDef.doc) { - result.description = typeConverter.convertText(ldlDef.doc); - } - - return result; -}; - -exports.isPrimitiveType = function(typeName) { - return TYPES_PRIMITIVE.indexOf(typeName.toLowerCase()) !== -1; -}; diff --git a/lib/swagger.js b/lib/swagger.js deleted file mode 100644 index 8223c81..0000000 --- a/lib/swagger.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict'; - -/** - * Module dependencies. - */ -var path = require('path'); -var _ = require('lodash'); -var routeHelper = require('./route-helper'); -var modelHelper = require('./model-helper'); -var cors = require('cors'); -var typeConverter = require('./type-converter'); -var tagBuilder = require('./tag-builder'); -var TypeRegistry = require('./type-registry'); - -/** - * Create Swagger Object describing the API provided by loopbacApplication. - * - * @param {Application} loopbackApplication The application to document. - * @param {Object} opts Options. - * @returns {Object} - */ -exports.createSwaggerObject = function(loopbackApplication, opts) { - opts = _.defaults(opts || {}, { - basePath: loopbackApplication.get('restApiRoot') || '/api', - // Default consumes/produces - consumes: [ - 'application/json', - 'application/x-www-form-urlencoded', - 'application/xml', 'text/xml' - ], - produces: [ - 'application/json', - 'application/xml', 'text/xml', - // JSONP content types - 'application/javascript', 'text/javascript' - ], - version: getPackagePropertyOrDefault('version', '1.0.0'), - }); - - // We need a temporary REST adapter to discover our available routes. - var remotes = loopbackApplication.remotes(); - var adapter = remotes.handler('rest').adapter; - var routes = adapter.allRoutes(); - var classes = remotes.classes(); - - // Generate fixed fields like info and basePath - var swaggerObject = generateSwaggerObjectBase(opts); - - var typeRegistry = new TypeRegistry(); - var loopbackRegistry = loopbackApplication.registry || - loopbackApplication.loopback.registry || - loopbackApplication.loopback; - var models = loopbackRegistry.modelBuilder.models; - for (var modelName in models) { - modelHelper.registerModelDefinition(models[modelName], typeRegistry); - } - - // A class is an endpoint root; e.g. /users, /products, and so on. - // In Swagger 2.0, there is no endpoint roots, but one can group endpoints - // using tags. - classes.forEach(function(aClass) { - if (!aClass.name) return; - - var hasDocumentedMethods = aClass.methods().some(function(m) { - return m.documented; - }); - if (!hasDocumentedMethods) return; - - swaggerObject.tags.push(tagBuilder.buildTagFromClass(aClass)); - }); - - // A route is an endpoint, such as /users/findOne. - routes.forEach(function(route) { - if (!route.documented) return; - - // Get the class definition matching this route. - var className = route.method.split('.')[0]; - var classDef = classes.filter(function(item) { - return item.name === className; - })[0]; - - if (!classDef) { - console.error('Route exists with no class: %j', route); - return; - } - - routeHelper.addRouteToSwaggerPaths(route, classDef, typeRegistry, - swaggerObject.paths); - }); - - _.assign(swaggerObject.definitions, typeRegistry.getDefinitions()); - - loopbackApplication.emit('swaggerResources', swaggerObject); - return swaggerObject; -}; - -/** - * Setup Swagger documentation on the given express app. - * - * @param {Application} loopbackApplication The loopback application to - * document. - * @param {Application} swaggerApp Swagger application used for hosting - * swagger documentation. - * @param {Object} opts Options. - */ -exports.mountSwagger = function(loopbackApplication, swaggerApp, opts) { - var swaggerObject = exports.createSwaggerObject(loopbackApplication, opts); - - var resourcePath = opts && opts.resourcePath || 'swagger.json'; - if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath; - - var remotes = loopbackApplication.remotes(); - setupCors(swaggerApp, remotes); - - swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) { - res.status(200).send(swaggerObject); - }); -}; - -/** - * Generate a top-level resource doc. This is the entry point for swagger UI - * and lists all of the available APIs. - * @param {Object} opts Swagger options. - * @return {Object} Resource doc. - */ -function generateSwaggerObjectBase(opts) { - var apiInfo = _.cloneDeep(opts.apiInfo) || {}; - for (var propertyName in apiInfo) { - var property = apiInfo[propertyName]; - apiInfo[propertyName] = typeConverter.convertText(property); - } - apiInfo.version = String(apiInfo.version || opts.version); - if (!apiInfo.title) { - apiInfo.title = getPackagePropertyOrDefault('name', 'LoopBack Application'); - } - - var basePath = opts.basePath; - if (basePath && /\/$/.test(basePath)) - basePath = basePath.slice(0, -1); - - return { - swagger: '2.0', - // See swagger-spec/2.0.md#infoObject - info: apiInfo, - host: opts.host, - basePath: basePath, - schemes: opts.protocol ? [opts.protocol] : undefined, - consumes: opts.consumes, - produces: opts.produces, - paths: {}, - definitions: opts.models || {}, - // TODO Authorizations (security, securityDefinitions) - // TODO: responses, externalDocs - tags: [] - }; -} - -function setupCors(swaggerApp, remotes) { - var corsOptions = remotes.options && remotes.options.cors || - { origin: true, credentials: true }; - - // TODO(bajtos) Skip CORS when remotes.options.cors === false - swaggerApp.use(cors(corsOptions)); -} - -function getPackagePropertyOrDefault(name, defautValue) { - try { - var pkg = require(path.join(process.cwd(), 'package.json')); - return pkg[name] || defautValue; - } catch(e) { - return defautValue; - } -} diff --git a/lib/tag-builder.js b/lib/tag-builder.js deleted file mode 100644 index 515ad25..0000000 --- a/lib/tag-builder.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var typeConverter = require('./type-converter'); - -exports.buildTagFromClass = function(sharedClass) { - var name = sharedClass.name; - var modelSettings = sharedClass.ctor && sharedClass.ctor.settings; - var sharedCtor = sharedClass.ctor && sharedClass.ctor.sharedCtor; - - var description = modelSettings && modelSettings.description || - sharedCtor && sharedCtor.description; - - return { - name: name, - description: typeConverter.convertText(description), - // TODO: externalDocs: { description, url } - }; -}; diff --git a/lib/type-converter.js b/lib/type-converter.js deleted file mode 100644 index 66e1439..0000000 --- a/lib/type-converter.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; - -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; - } -}; diff --git a/lib/type-registry.js b/lib/type-registry.js deleted file mode 100644 index cb3456d..0000000 --- a/lib/type-registry.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -var _ = require('lodash'); - -module.exports = TypeRegistry; - -function TypeRegistry() { - this._definitions = Object.create(null); - this._referenced = Object.create(null); - - this.register('x-any', { properties: {} }); - // TODO - register GeoPoint and other built-in LoopBack types -} - -TypeRegistry.prototype.register = function(typeName, definition) { - this._definitions[typeName] = definition; -}; - -TypeRegistry.prototype.reference = function(typeName) { - this._referenced[typeName] = true; - return '#/definitions/' + typeName; -}; - -TypeRegistry.prototype.getDefinitions = function() { - var defs = Object.create(null); - for (var name in this._referenced) { - if (this._definitions[name]) { - defs[name] = _.cloneDeep(this._definitions[name]); - } else { - // https://github.com/strongloop/loopback-explorer/issues/71 - console.warn('Swagger: skipping unknown type %j.', name); - } - } - return defs; -}; - -TypeRegistry.prototype.getAllDefinitions = function() { - return _.cloneDeep(this._definitions); -}; - -TypeRegistry.prototype.isDefined = function(typeName) { - return typeName in this._definitions; -}; diff --git a/package.json b/package.json index 89b74af..882c104 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "cors": "^2.7.1", "debug": "^2.2.0", "lodash": "^3.10.0", + "loopback-swagger": "^2.1.0", "strong-swagger-ui": "^21.0.0" } } diff --git a/test/explorer.test.js b/test/explorer.test.js index 5aeae77..91ef0a4 100644 --- a/test/explorer.test.js +++ b/test/explorer.test.js @@ -190,6 +190,36 @@ describe('explorer', function() { }); }); + describe('Cross-origin resource sharing', function() { + it('allows cross-origin requests by default', function(done) { + var app = loopback(); + configureRestApiAndExplorer(app, '/explorer'); + + request(app) + .options('/explorer/swagger.json') + .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 = loopback(); + app.set('remoting', { cors: { origin: false } }); + configureRestApiAndExplorer(app, '/explorer'); + + request(app) + .options('/explorer/swagger.json') + .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 givenLoopBackAppWithExplorer(explorerBase) { return function(done) { var app = this.app = loopback(); diff --git a/test/model-helper.test.js b/test/model-helper.test.js deleted file mode 100644 index 2401889..0000000 --- a/test/model-helper.test.js +++ /dev/null @@ -1,193 +0,0 @@ -'use strict'; - -var modelHelper = require('../lib/model-helper'); -var TypeRegistry = require('../lib/type-registry'); -var _defaults = require('lodash').defaults; -var loopback = require('loopback'); -var expect = require('chai').expect; - -describe('model-helper', function() { - describe('related models', function() { - it('should include related models', function() { - var defs = buildSwaggerModelsWithRelations({ - str: String // 'string' - }); - expect(defs).has.property('testModel'); - expect(defs).has.property('relatedModel'); - }); - - it('should include nesting models', function() { - var Model2 = loopback.createModel('Model2', {street: String}); - var Model1 = loopback.createModel('Model1', { - str: String, // 'string' - address: Model2 - }, { models: { Model2: Model2 } }); - var defs = getDefinitionsForModel(Model1); - expect(defs).has.property('Model1'); - expect(defs).has.property('Model2'); - }); - - it('should include used models', function() { - var Model4 = loopback.createModel('Model4', {street: String}); - var Model3 = loopback.createModel('Model3', { - str: String // 'string' - }, {models: {model4: 'Model4'}}); - var defs = getDefinitionsForModel(Model3); - expect(defs).has.property('Model3'); - expect(defs).has.property('Model4'); - }); - - it('should include nesting models in array', function() { - var Model6 = loopback.createModel('Model6', {street: String}); - var Model5 = loopback.createModel('Model5', { - str: String, // 'string' - addresses: [Model6] - }, { models: { Model6: Model6 } }); - var defs = getDefinitionsForModel(Model5); - expect(defs).has.property('Model5'); - expect(defs).has.property('Model6'); - }); - - // https://github.com/strongloop/loopback-explorer/issues/49 - it('should work if Array class is extended and no related models are found', - function() { - var Model7 = loopback.createModel('Model7', {street: String}); - Array.prototype.customFunc = function() { - }; - var defs = getDefinitionsForModel(Model7); - expect(defs).has.property('Model7'); - 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 = getDefinitionsForModel(Model8); - // Hack: prevent warnings in other tests caused by global model registry - Model8.definition.rawProperties.patient.type = 'string'; - Model8.definition.properties.patient.type = 'string'; - - expect(Object.keys(defs)).to.not.contain('hasMany'); - }); - }); - - describe('hidden properties', function() { - it('should hide properties marked as "hidden"', function() { - var aClass = createModelCtor({ - visibleProperty: 'string', - hiddenProperty: 'string' - }); - aClass.ctor.definition.settings = { - hidden: ['hiddenProperty'] - }; - var def = getDefinitionsForModel(aClass.ctor).testModel; - expect(def.properties).to.not.have.property('hiddenProperty'); - expect(def.properties).to.have.property('visibleProperty'); - }); - }); - - it('should convert top level array description to string', function() { - var model = {}; - model.definition = { - name: 'test', - description: ['1', '2', '3'], - properties: {} - }; - var defs = getDefinitionsForModel(model); - expect(defs.test.description).to.equal('1\n2\n3'); - }); - - it('should convert property level array description to string', function() { - var model = {}; - model.definition = { - name: 'test', - properties: { - prop1: { - type: 'string', - description: ['1', '2', '3'] - } - } - }; - var defs = getDefinitionsForModel(model); - expect(defs.test.properties.prop1.description).to.equal('1\n2\n3'); - }); - - it('omits empty "required" array', function() { - var aClass = createModelCtor({}); - var def = getDefinitionsForModel(aClass.ctor).testModel; - expect(def).to.not.have.property('required'); - }); -}); - -// Simulates the format of a remoting class. -function buildSwaggerModels(modelProperties, modelOptions) { - var aClass = createModelCtor(modelProperties, modelOptions); - return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel; -} - -function createModelCtor(properties, modelOptions) { - Object.keys(properties).forEach(function(name) { - var type = properties[name]; - if (typeof type !== 'object' || Array.isArray(type)) - properties[name] = { type: type }; - }); - - var definition = { - name: 'testModel', - properties: properties - }; - _defaults(definition, modelOptions); - - var aClass = { - ctor: { - definition: definition - } - }; - return aClass; -} - -function buildSwaggerModelsWithRelations(model) { - Object.keys(model).forEach(function(name) { - model[name] = {type: model[name]}; - }); - // Mock up the related model - var relatedModel = { - definition: { - name: 'relatedModel', - properties: { - fk: String - } - } - }; - var aClass = { - ctor: { - definition: { - name: 'testModel', - properties: model - }, - // Mock up relations - relations: { - other: { - modelTo: relatedModel - } - } - } - }; - - var registry = new TypeRegistry(); - modelHelper.registerModelDefinition(aClass.ctor, registry); - return registry.getAllDefinitions(); -} - -function getDefinitionsForModel(modelCtor) { - var registry = new TypeRegistry(); - modelHelper.registerModelDefinition(modelCtor, registry); - registry.reference(modelCtor.modelName || modelCtor.definition.name); - return registry.getDefinitions(); -} diff --git a/test/route-helper.test.js b/test/route-helper.test.js deleted file mode 100644 index 920b99b..0000000 --- a/test/route-helper.test.js +++ /dev/null @@ -1,280 +0,0 @@ -'use strict'; - -var routeHelper = require('../lib/route-helper'); -var TypeRegistry = require('../lib/type-registry'); -var expect = require('chai').expect; -var _defaults = require('lodash').defaults; - -describe('route-helper', function() { - it('returns "object" when a route has multiple return values', function() { - var entry = createAPIDoc({ - returns: [ - { arg: 'max', type: 'number' }, - { arg: 'min', type: 'number' }, - { arg: 'avg', type: 'number' } - ] - }); - // TODO use a custom (dynamicaly-created) model schema instead of "object" - expect(getResponseMessage(entry.operation)) - .to.have.property('schema').eql({ type: 'object' }); - }); - - it('converts path params when they exist in the route name', function() { - var entry = createAPIDoc({ - accepts: [ - {arg: 'id', type: 'string'} - ], - path: '/test/:id' - }); - var paramDoc = entry.operation.parameters[0]; - expect(paramDoc).to.have.property('in', 'path'); - expect(paramDoc).to.have.property('name', 'id'); - expect(paramDoc).to.have.property('required', false); - }); - - // FIXME need regex in routeHelper.acceptToParameter - xit('won\'t convert path params when they don\'t exist in the route name', function() { - var doc = createAPIDoc({ - accepts: [ - {arg: 'id', type: 'string'} - ], - path: '/test/:identifier' - }); - var paramDoc = doc.operation.parameters[0]; - expect(paramDoc.in).to.equal('query'); - }); - - it('correctly coerces param types', function() { - var doc = createAPIDoc({ - accepts: [ - {arg: 'binaryData', type: 'buffer'} - ] - }); - var paramDoc = doc.operation.parameters[0]; - expect(paramDoc).to.have.property('in', 'query'); - expect(paramDoc).to.have.property('type', 'string'); - expect(paramDoc).to.have.property('format', 'byte'); - }); - - it('correctly converts return types (arrays)', function() { - var doc = createAPIDoc({ - returns: [ - { arg: 'data', type: ['customType'], root: true } - ] - }); - var opDoc = doc.operation; - - var responseSchema = getResponseMessage(opDoc).schema; - expect(responseSchema).to.have.property('type', 'array'); - expect(responseSchema).to.have.property('items') - .eql({ $ref: '#/definitions/customType' }); - }); - - it('correctly converts return types (format)', function() { - var doc = createAPIDoc({ - returns: [ - { arg: 'data', type: 'buffer', root: true } - ] - }); - - var responseSchema = getResponseMessage(doc.operation).schema; - expect(responseSchema.type).to.equal('string'); - expect(responseSchema.format).to.equal('byte'); - }); - - it('includes `notes` metadata as `description`', function() { - var doc = createAPIDoc({ - notes: 'some notes' - }); - expect(doc.operation).to.have.property('description', 'some notes'); - }); - - describe('#acceptToParameter', function() { - var A_CLASS_DEF = { name: 'TestModelName' }; - - it('returns fn converting description from array to string', function() { - var f = routeHelper.acceptToParameter( - {verb: 'get', path: 'path'}, - A_CLASS_DEF, - new TypeRegistry()); - var result = f({description: ['1', '2', '3']}); - expect(result.description).to.eql('1\n2\n3'); - }); - }); - - describe('#routeToPathEntry', function() { - it('converts route.description from array to string', function() { - var result = routeHelper.routeToPathEntry({ - method: 'someMethod', - verb: 'get', - path: 'path', - description: ['1', '2', '3'] - }); - expect(result.operation.summary).to.eql('1\n2\n3'); - }); - - it('converts route.notes from array of string to string', function() { - var result = routeHelper.routeToPathEntry({ - method: 'someMethod', - verb: 'get', - path: 'path', - notes: ['1', '2', '3'] - }); - expect(result.operation.description).to.eql("1\n2\n3"); - }); - }); - - it('includes `deprecated` metadata', function() { - var doc = createAPIDoc({ - deprecated: 'true' - }); - expect(doc.operation).to.have.property('deprecated', true); - }); - - it('joins array description/summary', function() { - var doc = createAPIDoc({ - description: [ 'line1', 'line2' ] - }); - expect(doc.operation.summary).to.equal('line1\nline2'); - }); - - it('joins array notes', function() { - var doc = createAPIDoc({ - notes: [ 'line1', 'line2' ] - }); - expect(doc.operation.description).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.operation.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.operation.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.operation.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.operation.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.operation.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.operation.responses).to.eql({ - 200: { - description: 'Request was successful', - schema: { type: 'object' } - } - }); - }); - - it('uses the response code 204 when `returns` is empty', function() { - var doc = createAPIDoc({ - returns: [] - }); - expect(doc.operation.responses).to.eql({ - 204: { - description: 'Request was successful', - schema: undefined - } - }); - }); - - it('includes custom error response in `responseMessages`', function() { - var doc = createAPIDoc({ - errors: [{ - code: 422, - message: 'Validation failed', - responseModel: 'ValidationError' - }] - }); - expect(doc.operation.responses).to.have.property(422).eql({ - description: 'Validation failed', - schema: { $ref: '#/definitions/ValidationError' } - }); - }); - - it('route operationId DOES include model name.', function() { - var doc = createAPIDoc({ method: 'User.login' }); - expect(doc.operation.operationId).to.equal('User.login'); - }); - - it('adds class name to `tags`', function() { - var doc = createAPIDoc( - { method: 'User.login' }, - { name: 'User' }); - expect(doc.operation.tags).to.contain('User'); - }); - - it('converts non-primitive param types to JSON strings', function() { - var doc = createAPIDoc({ - accepts: [{arg: 'filter', type: 'object', http: { source: 'query' }}] - }); - var param = doc.operation.parameters[0]; - expect(param).to.have.property('type', 'string'); - expect(param).to.have.property('format', 'JSON'); - }); - - it('converts single "data" body arg to Model type', function() { - var doc = createAPIDoc( - { - accepts: [{arg: 'data', type: 'object', http: { source: 'body' }}], - }, - { name: 'User' }); - var param = doc.operation.parameters[0]; - expect(param) - .to.have.property('schema') - .eql({ $ref: '#/definitions/User' }); - }); -}); - -// Easy wrapper around createRoute -function createAPIDoc(def, classDef) { - return routeHelper.routeToPathEntry(_defaults(def || {}, { - path: '/test', - verb: 'GET', - method: 'test.get' - }), classDef, new TypeRegistry()); -} - -function getResponseMessage(operationDoc) { - return operationDoc.responses[200] || operationDoc.responses[204] - || operationDoc.responses.default; -} diff --git a/test/schema-builder.test.js b/test/schema-builder.test.js deleted file mode 100644 index 7084ae0..0000000 --- a/test/schema-builder.test.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict'; - -var schemaBuilder = require('../lib/schema-builder'); -var TypeRegistry = require('../lib/type-registry'); -var format = require('util').format; -var _defaults = require('lodash').defaults; -var loopback = require('loopback'); -var expect = require('chai').expect; - -var ANY_TYPE = { $ref: '#/definitions/x-any' }; - -describe('schema-builder', function() { - describeTestCases('for constructor types', [ - { in: String, out: { type: 'string' } }, - { in: Number, out: { type: 'number', format: 'double' } }, - { in: Date, out: { type: 'string', format: 'date' } }, - { in: Boolean, out: { type: 'boolean' } }, - { in: Buffer, out: { type: 'string', format: 'byte' } } - ]); - - describeTestCases('for string types', [ - { in: 'string', out: { type: 'string' } }, - { in: 'number', out: { type: 'number', format: 'double' } }, - { in: 'date', out: { type: 'string', format: 'date' } }, - { in: 'boolean', out: { type: 'boolean' } }, - { in: 'buffer', out: { type: 'string', format: 'byte' } }, - ]); - - describeTestCases('for array definitions', [ - { in: [String], - out: { type: 'array', items: { type: 'string' } } }, - { in: ['string'], - out: { type: 'array', items: { type: 'string' } } }, - { in: [{ type: 'string', maxLength: 64 }], - out: { type: 'array', items: { type: 'string', maxLength: 64 } } }, - { in: [{ type: 'date' }], - out: { type: 'array', items: { type: 'string', format: 'date' } } }, - { in: [], - out: { type: 'array', items: ANY_TYPE } }, - // This value is somehow provided by loopback-boot called from - // loopback-workspace. - { in: [undefined], - out: { type: 'array', items: ANY_TYPE } }, - { in: 'array', - out: { type: 'array', items: ANY_TYPE } }, - ]); - - describeTestCases('for complex types', [ - // Note: User is a built-in loopback model - { in: loopback.User, - out: { $ref: '#/definitions/User' } }, - { in: { type: 'User' }, - out: { $ref: '#/definitions/User' } }, - // Anonymous type - { in: { type: { foo: 'string', bar: 'number' } }, - out: { type: 'object' } }, - ]); - - describeTestCases('for extra metadata', [ - { in: { type: String, doc: 'a-description' }, - out: { type: 'string', description: 'a-description' } }, - { in: { type: String, doc: ['line1', 'line2'] }, - out: { type: 'string', description: 'line1\nline2' } }, - { in: { type: String, description: 'a-description' }, - out: { type: 'string', description: 'a-description' } }, - { in: { type: String, description: ['line1', 'line2'] }, - out: { type: 'string', description: 'line1\nline2' } }, - { in: { type: String, required: true }, - out: { type: 'string' } }, // the flag required is handled specially - { in: { type: String, length: 10 }, - out: { type: 'string', maxLength: 10 } }, - ]); - - function describeTestCases(name, testCases) { - describe(name, function() { - testCases.forEach(function(tc) { - var inStr = formatType(tc.in); - var outStr = formatType(tc.out); - it(format('converts %s to %s', inStr, outStr), function() { - var registry = new TypeRegistry(); - var schema = schemaBuilder.buildFromLoopBackType(tc.in, registry); - expect(schema).to.eql(tc.out); - }); - }); - }); - } - - function formatType(type) { - if (Array.isArray(type)) - return '[' + type.map(formatType) + ']'; - - if (typeof type === 'function') - return type.modelName ? - 'model ' + type.modelName : - 'ctor ' + type.name; - - return format(type); - } - -}); diff --git a/test/swagger.test.js b/test/swagger.test.js deleted file mode 100644 index 3354d04..0000000 --- a/test/swagger.test.js +++ /dev/null @@ -1,410 +0,0 @@ -'use strict'; - -var url = require('url'); -var urlJoin = require('../lib/url-join'); -var loopback = require('loopback'); -var swagger = require('../lib/swagger'); - -var request = require('supertest'); -var expect = require('chai').expect; - -describe('swagger definition', function() { - describe('defaults', function() { - var swaggerResource; - before(function() { - var app = createLoopbackAppWithModel(); - swaggerResource = swagger.createSwaggerObject(app); - }); - - it('advertises Swagger Spec version 2.0', function() { - expect(swaggerResource).to.have.property('swagger', '2.0'); - }); - - it('has "basePath" set to "/api"', function() { - expect(swaggerResource).to.have.property('basePath', '/api'); - }); - - it('uses the "host" serving the documentation', function() { - // see swagger-spec/2.0.md#fixed-fields - // If the host is not included, the host serving the documentation is to - // be used (including the port). - expect(swaggerResource).to.have.property('host', undefined); - }); - - it('uses the "schemes" serving the documentation', function() { - // see swagger-spec/2.0.md#fixed-fields - // If the schemes is not included, the default scheme to be used is the - // one used to access the Swagger definition itself. - expect(swaggerResource).to.have.property('schemes', undefined); - }); - - it('provides info.title', function() { - expect(swaggerResource.info) - .to.have.property('title', 'loopback-explorer'); - }); - }); - - describe('basePath', function() { - it('is "{basePath}" when basePath is a path', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app, { - basePath: '/api-root' - }); - - expect(swaggerResource.basePath).to.equal('/api-root'); - }); - - it('is inferred from app.get("apiRoot")', function() { - var app = createLoopbackAppWithModel(); - app.set('restApiRoot', '/custom-api-root'); - var swaggerResource = swagger.createSwaggerObject(app); - expect(swaggerResource.basePath).to.equal('/custom-api-root'); - }); - - it('is reachable when explorer mounting location is changed', - function(done) { - var explorerRoot = '/erforscher'; - var app = givenAppWithSwagger({}, {explorerRoot: explorerRoot}); - - getSwaggerResource(app, explorerRoot).end(function(err, res) { - if (err) return done(err); - expect(res.body.basePath).to.be.a('string'); - done(); - }); - }); - - it('respects a hardcoded protocol (behind SSL terminator)', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app, { - protocol: 'https' - }); - expect(swaggerResource.schemes).to.eql(['https']); - }); - - it('supports opts.host', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app, { - host: 'example.com:8080' - }); - expect(swaggerResource.host).to.equal('example.com:8080'); - }); - }); - - it('has global "consumes"', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app); - expect(swaggerResource.consumes).to.have.members([ - 'application/json', - 'application/x-www-form-urlencoded', - 'application/xml', 'text/xml' - ]); - }); - - it('has global "produces"', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app); - expect(swaggerResource.produces).to.have.members([ - 'application/json', - 'application/xml', 'text/xml', - // JSONP content types - 'application/javascript', 'text/javascript' - ]); - }); - - describe('tags', function() { - it('has one tag for each model', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app); - expect(swaggerResource.tags).to.eql([ - { name: 'Product', description: 'a-description\nline2' } - ]); - }); - }); - - describe('paths node', function() { - it('contains model routes for static methods', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app); - expect(swaggerResource.paths).to.have.property('/Products'); - var products = swaggerResource.paths['/Products']; - var verbs = Object.keys(products); - verbs.sort(); - expect(verbs).to.eql(['get', 'post', 'put']); - }); - }); - - describe('definitions node', function() { - it('properly defines basic attributes', function() { - var app = createLoopbackAppWithModel(); - var swaggerResource = swagger.createSwaggerObject(app); - var data = swaggerResource.definitions.Product; - expect(data.required.sort()).to.eql(['aNum', 'foo'].sort()); - expect(data.properties.foo.type).to.equal('string'); - expect(data.properties.bar.type).to.equal('string'); - expect(data.properties.aNum.type).to.equal('number'); - // These will be Numbers for Swagger 2.0 - expect(data.properties.aNum.minimum).to.equal(1); - expect(data.properties.aNum.maximum).to.equal(10); - // Should be Number even in 1.2 - expect(data.properties.aNum.default).to.equal(5); - }); - - it('includes models from "accepts" args', function() { - var app = createLoopbackAppWithModel(); - givenPrivateAppModel(app, 'Image'); - givenSharedMethod(app.models.Product, 'setImage', { - accepts: { name: 'image', type: 'Image' } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)).to.include('Image'); - }); - - it('includes models from "returns" args', function() { - var app = createLoopbackAppWithModel(); - givenPrivateAppModel(app, 'Image'); - givenSharedMethod(app.models.Product, 'getImage', { - returns: { name: 'image', type: 'Image', root: true } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)).to.include('Image'); - }); - - it('includes "accepts" models not attached to the app', function() { - var app = createLoopbackAppWithModel(); - loopback.createModel('Image'); - givenSharedMethod(app.models.Product, 'setImage', { - accepts: { name: 'image', type: 'Image' } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)).to.include('Image'); - }); - - it('includes "responseMessages" models', function() { - var app = createLoopbackAppWithModel(); - loopback.createModel('ValidationError'); - givenSharedMethod(app.models.Product, 'setImage', { - errors: [{ - code: '422', - message: 'Validation failed', - responseModel: 'ValidationError' - }] - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include('ValidationError'); - }); - - it('includes nested model references in properties', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - app.models.Product.defineProperty('location', { type: 'Warehouse' }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested array model references in properties', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - app.models.Product.defineProperty('location', { type: ['Warehouse'] }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested model references in modelTo relation', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - app.models.Product.belongsTo(app.models.Warehouse); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested model references in modelThrough relation', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - givenPrivateAppModel(app, 'ProductLocations'); - - app.models.Product.hasMany(app.models.Warehouse, - { through: app.models.ProductLocations }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse', 'ProductLocations']); - }); - - it('includes nested model references in accept args', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - givenSharedMethod(app.models.Product, 'aMethod', { - accepts: { arg: 'w', type: 'Warehouse' } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested array model references in accept args', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - givenSharedMethod(app.models.Product, 'aMethod', { - accepts: { arg: 'w', type: ['Warehouse'] } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested model references in return args', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - givenSharedMethod(app.models.Product, 'aMethod', { - returns: { arg: 'w', type: 'Warehouse', root: true } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested array model references in return args', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - givenSharedMethod(app.models.Product, 'aMethod', { - returns: { arg: 'w', type: ['Warehouse'], root: true } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - - it('includes nested model references in error responses', function() { - var app = createLoopbackAppWithModel(); - givenWarehouseWithAddressModels(app); - - givenSharedMethod(app.models.Product, 'aMethod', { - errors: { - code: '222', - message: 'Warehouse', - responseModel: 'Warehouse' - } - }); - - var swaggerResource = swagger.createSwaggerObject(app); - expect(Object.keys(swaggerResource.definitions)) - .to.include.members(['Address', 'Warehouse']); - }); - }); - - describe('Cross-origin resource sharing', function() { - it('allows cross-origin requests by default', function(done) { - var app = givenAppWithSwagger(); - request(app) - .options('/explorer/swagger.json') - .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/swagger.json') - .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 getSwaggerResource(app, restPath, classPath) { - if (classPath) throw new Error('classPath is no longer supported'); - return request(app) - .get(urlJoin(restPath || '/explorer', '/swagger.json')) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/); - } - - function getAPIDeclaration(app, className) { - return getSwaggerResource(app, '', urlJoin('/', className)); - } - - function givenAppWithSwagger(swaggerOptions, appConfig) { - appConfig = appConfig || {}; - var app = createLoopbackAppWithModel(appConfig.apiRoot); - - if (appConfig.remoting) app.set('remoting', appConfig.remoting); - if (appConfig.explorerRoot) app.set('explorerRoot', appConfig.explorerRoot); - - mountExplorer(app, swaggerOptions); - return app; - } - - function mountExplorer(app, options) { - var swaggerApp = loopback(); - swagger.mountSwagger(app, swaggerApp, options); - app.use(app.get('explorerRoot') || '/explorer', swaggerApp); - return app; - } - - function createLoopbackAppWithModel(apiRoot) { - var app = loopback(); - - app.dataSource('db', { connector: 'memory' }); - - var Product = loopback.createModel('Product', { - foo: {type: 'string', required: true}, - bar: 'string', - aNum: {type: 'number', min: 1, max: 10, required: true, default: 5} - }, { description: ['a-description', 'line2'] }); - app.model(Product, { dataSource: 'db' }); - - // Simulate a restApiRoot set in config - app.set('restApiRoot', apiRoot || '/api'); - app.use(app.get('restApiRoot'), loopback.rest()); - - 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' } - }); - } -}); diff --git a/test/tag-builder.test.js b/test/tag-builder.test.js deleted file mode 100644 index c0b69c0..0000000 --- a/test/tag-builder.test.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -var tagBuilder = require('../lib/tag-builder'); -var expect = require('chai').expect; -var _defaults = require('lodash').defaults; - -describe('tag-builder', function() { - it('joins array descriptions from ctor.settings', function() { - var tag = tagBuilder.buildTagFromClass({ - ctor: { settings: { description: ['line1', 'line2'] } } - }); - - expect(tag.description).to.equal('line1\nline2'); - }); - - it('joins array descriptions from ctor.sharedCtor', function() { - var tag = tagBuilder.buildTagFromClass({ - ctor: { sharedCtor: { description: ['1', '2', '3'] } } - }); - - expect(tag.description).to.eql('1\n2\n3'); - }); -});