diff --git a/.gitignore b/.gitignore index a9578a3..5e487e2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,12 @@ lib-cov *.iml *.tgz +.idea pids logs results npm-debug.log node_modules + +LoopBackExplorer.iml diff --git a/lib/class-helper.js b/lib/class-helper.js index 377b19e..761b018 100644 --- a/lib/class-helper.js +++ b/lib/class-helper.js @@ -29,7 +29,7 @@ var classHelper = module.exports = { } return { - apiVersion: opts.version, + apiVersion: opts.version || '1', swaggerVersion: opts.swaggerVersion, basePath: opts.basePath, resourcePath: urlJoin('/', resourcePath), diff --git a/lib/model-helper.js b/lib/model-helper.js index ddb4d79..6a6135c 100644 --- a/lib/model-helper.js +++ b/lib/model-helper.js @@ -6,6 +6,7 @@ var _cloneDeep = require('lodash').cloneDeep; var _pick = require('lodash').pick; var translateDataTypeKeys = require('./translate-data-type-keys'); +var TYPES_PRIMITIVE = ['array', 'boolean', 'integer', 'number', 'null', 'object', 'string', 'any']; var typeConverter = require('./type-converter'); /** @@ -20,6 +21,25 @@ var modelHelper = module.exports = { * @return {Object} Associated model definition. */ generateModelDefinition: function generateModelDefinition(modelClass, definitions) { + var processType = function(app, modelName, referencedModels) { + if (app && modelName) { + if (modelName.indexOf('[') == 0) { + modelName = modelName.replace(/[\[\]]/g, ''); + } + var model = app.models[modelName]; + if (model && referencedModels.indexOf(model) === -1) { + referencedModels.push(model); + } + } + }; + + var convertTypeTo$Ref = function convertTypeTo$Ref(prop){ + if (prop.type && TYPES_PRIMITIVE.indexOf(prop.type) === -1 ){ + prop.$ref = prop.type; + delete prop.type; + } + }; + var def = modelClass.definition; var out = definitions || {}; @@ -36,7 +56,7 @@ var modelHelper = module.exports = { } var required = []; // Don't modify original properties. - var properties = _cloneDeep(def.properties); + var properties = _cloneDeep(def.rawProperties || def.properties); var referencedModels = []; // Add models from settings @@ -44,7 +64,7 @@ var modelHelper = module.exports = { for (var m in def.settings.models) { var model = modelClass[m]; if (typeof model === 'function' && model.modelName) { - if (referencedModels.indexOf(model) === -1) { + if (model && referencedModels.indexOf(model) === -1) { referencedModels.push(model); } } @@ -67,6 +87,12 @@ var modelHelper = module.exports = { // Eke a type out of the constructors we were passed. var swaggerType = modelHelper.LDLPropToSwaggerDataType(prop); + processType(modelClass.app, swaggerType.type, referencedModels); + convertTypeTo$Ref(swaggerType); + if (swaggerType.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; @@ -76,6 +102,16 @@ var modelHelper = module.exports = { required.push(key); } + // Change mismatched keys. + prop = translateDataTypeKeys(prop); + + delete prop.required; + delete prop.id; + + if (prop.description){ + prop.description = typeConverter.convertText(prop.description); + } + // Assign this back to the properties object. properties[key] = swaggerType; @@ -95,26 +131,69 @@ var modelHelper = module.exports = { } }); + var additionalProperties = undefined; + if (def.settings){ + var strict = def.settings.strict; + additionalProperties = def.settings.additionalProperties; + var notAllowAdditionalProperties = strict || (additionalProperties !== true); + if (notAllowAdditionalProperties){ + additionalProperties = false; + } + } + out[name] = { id: name, + additionalProperties: additionalProperties, description: typeConverter.convertText( def.description || (def.settings && def.settings.description)), properties: properties, required: required }; + if (def.description){ + out[name].description = typeConverter.convertText(def.description); + } + // Generate model definitions for related models for (var r in modelClass.relations) { var rel = modelClass.relations[r]; - if (rel.modelTo){ - generateModelDefinition(rel.modelTo, out); + if (rel.modelTo && referencedModels.indexOf(rel.modelTo) === -1) { + referencedModels.push(rel.modelTo); } - if (rel.modelThrough) { - generateModelDefinition(rel.modelThrough, out); + if (rel.modelThrough && referencedModels.indexOf(rel.modelThrough) === -1) { + referencedModels.push(rel.modelThrough); } } + + if (modelClass.sharedClass) { + var remotes = modelClass.sharedClass.methods(); + for (var remoteIdx in remotes) { + var remote = remotes[remoteIdx]; + var accepts = remote.accepts; + if (accepts) { + for (var acceptIdx in accepts) { + processType(modelClass.app, accepts[acceptIdx].type, referencedModels); + } + } + var returns = remote.returns; + if (returns) { + for (var returnIdx in returns) { + processType(modelClass.app, returns[returnIdx].type, referencedModels); + } + } + var errors = remote.errors; + if (errors) { + for (var errorIdx in errors) { + processType(modelClass.app, errors[errorIdx].responseModel, referencedModels); + } + } + } + } + for (var i = 0, n = referencedModels.length; i < n; i++) { - generateModelDefinition(referencedModels[i], out); + if (referencedModels[i].definition) { + generateModelDefinition(referencedModels[i], out); + } } return out; }, @@ -124,7 +203,7 @@ var modelHelper = module.exports = { * get a string type. * @param {*} propType Prop type description. * @return {String} Prop type string. - */ + */ getPropType: function getPropType(propType) { if (typeof propType === 'function') { // See https://github.com/strongloop/loopback-explorer/issues/32 @@ -140,7 +219,7 @@ var modelHelper = module.exports = { }, isHiddenProperty: function(definition, propName) { - return definition.settings && + return definition.settings && Array.isArray(definition.settings.hidden) && definition.settings.hidden.indexOf(propName) !== -1; }, @@ -153,8 +232,13 @@ var modelHelper = module.exports = { 'format', 'defaultValue', 'enum', + 'items', 'minimum', + 'minItems', + 'minLength', 'maximum', + 'maxItems', + 'maxLength', 'uniqueItems', // loopback-explorer extensions 'length', @@ -168,21 +252,28 @@ var modelHelper = module.exports = { // Pick only keys supported by Swagger var swaggerType = _pick(ldlType, SWAGGER_DATA_TYPE_FIELDS); - swaggerType.type = modelHelper.getPropType(ldlType.type); + 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(typeof arrayItem === 'object') { - swaggerType.items = modelHelper.LDLPropToSwaggerDataType(arrayItem); + newItems = modelHelper.LDLPropToSwaggerDataType(arrayItem); } else { - swaggerType.items = { type: modelHelper.getPropType(arrayItem) }; + newItems = { type: modelHelper.getPropType(arrayItem) }; } } else { // NOTE: `any` is not a supported type in swagger 1.2 - swaggerType.items = { type: 'any' }; + newItems = { type: 'any' }; + } + if (typeof swaggerType.items !== 'object') { + swaggerType.items = {}; + } + for (var key in newItems) { + swaggerType.items[key] = newItems[key]; } } else if (swaggerType.type === 'date') { swaggerType.type = 'string'; @@ -195,7 +286,4 @@ var modelHelper = module.exports = { } return swaggerType; } -}; - - - +}; \ No newline at end of file diff --git a/lib/route-helper.js b/lib/route-helper.js index 4625e8b..4c35f97 100644 --- a/lib/route-helper.js +++ b/lib/route-helper.js @@ -116,24 +116,76 @@ var routeHelper = module.exports = { * See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object */ routeToAPIDoc: function routeToAPIDoc(route, classDef) { - // Some parameters need to be altered; eventually most of this should + /** + * Converts from an sl-remoting data type to a Swagger dataType. + */ + function prepareDataType(type) { + if (!type) { + return 'void'; + } + + if(Array.isArray(type)) { + if (type.length > 0) { + if (typeof type[0] === 'string') { + return '[' + type[0] + ']'; + } else if (typeof type[0] === 'function') { + return '[' + type[0].name + ']'; + } else if (typeof type[0] === 'object') { + if (typeof type[0].type === 'function') { + return '[' + type[0].type.name + ']'; + } else { + return '[' + type[0].type + ']'; + } + } else { + return '[' + type + ']'; + } + } + return 'array'; + } + + // TODO(schoon) - Add support for complex dataTypes, "models", etc. + switch (type) { + case 'Array': + return 'array'; + case 'Boolean': + return 'boolean'; + case 'buffer': + return 'string'; + case 'Date': + return 'date'; + case 'number': + case 'Number': + return 'double'; + case 'Object': + return 'object'; + case 'String': + return 'string'; + } + + return type; + } + + var returnDesc; + + // Some parameters need to be altered; eventually most of this should // be removed. var accepts = routeHelper.convertAcceptsToSwagger(route, classDef); var returns = routeHelper.convertReturnsToSwagger(route, classDef); + var responseMessages = [ + { + code: route.returns && route.returns.length ? 200 : 204, + message: 'Request was successful', + responseModel: returns.model || prepareDataType(returns.type) || 'void' + } + ]; + if (route.errors) { + responseMessages.push.apply(responseMessages, route.errors); + } debug('route %j', route); var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns); - var responseMessages = [{ - code: route.returns && route.returns.length ? 200 : 204, - message: 'Request was successful' - }]; - - if (route.errors) { - responseMessages.push.apply(responseMessages, route.errors); - } - var apiDoc = { path: routeHelper.convertPathFragments(route.path), // Create the operation doc. @@ -142,15 +194,18 @@ var routeHelper = module.exports = { // see https://github.com/strongloop/loopback-explorer/issues/75 operations: [routeHelper.extendWithType({ method: routeHelper.convertVerb(route.verb), - // [strml] remove leading model name from op, swagger uses leading + // [strml] remove leading model name from op, swagger uses leading // path as class name so it remains unique between models. // route.method is always #{className}.#{methodName} nickname: route.method.replace(/.*?\./, ''), + deprecated: route.deprecated, + consumes: ['application/json', 'application/xml', 'text/xml'], + produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], parameters: accepts, responseMessages: responseMessages, + type: returns.model || returns.type || 'void', summary: typeConverter.convertText(route.description), - notes: typeConverter.convertText(route.notes), - deprecated: route.deprecated + notes: typeConverter.convertText(route.notes) }, returns)] }; @@ -204,11 +259,21 @@ var routeHelper = module.exports = { } var out = { - paramType: paramType || type, name: name, - description: typeConverter.convertText(accepts.description), required: !!accepts.required, - allowMultiple: false + paramType: paramType || type, + type: accepts.type, + $ref: accepts.model, + items: accepts.items, + uniqueItems: accepts.uniqueItems, + format: accepts.format, + pattern: accepts.pattern, + defaultValue: accepts.defaultValue, + enum: accepts.enum, + minimum: accepts.minimum, + maximum: accepts.maximum, + allowMultiple: accepts.allowMultiple, + description: typeConverter.convertText(accepts.description) }; out = routeHelper.extendWithType(out, accepts); @@ -238,6 +303,25 @@ var routeHelper = module.exports = { // The `typeDesc` may have additional attributes, such as // `format` for non-primitive types. + Object.keys(typeDesc).forEach(function(key){ + obj[key] = typeDesc[key]; + }); + + //Ensure brief properties are first + if (typeof obj === 'object') { + var keysToSink = ['authorizations', 'consumes', 'notes', 'produces', + 'parameters', 'responseMessages', 'summary']; + var outKeys = Object.keys(obj); + for (var outKeyIdx in outKeys) { + var outKey = outKeys[outKeyIdx]; + if (keysToSink.indexOf(outKey) != -1) { + var outValue = obj[outKey]; + delete obj[outKey]; + obj[outKey] = outValue; + } + } + } + _assign(obj, typeDesc); return obj; diff --git a/lib/swagger.js b/lib/swagger.js index 3e9aacf..92d0d90 100644 --- a/lib/swagger.js +++ b/lib/swagger.js @@ -12,8 +12,10 @@ var urlJoin = require('./url-join'); var _defaults = require('lodash').defaults; var classHelper = require('./class-helper'); var routeHelper = require('./route-helper'); +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`. @@ -61,7 +63,17 @@ function Swagger(loopbackApplication, swaggerApp, opts) { // A class is an endpoint root; e.g. /users, /products, and so on. classes.forEach(function (aClass) { var doc = apiDocs[aClass.name] = classHelper.generateAPIDoc(aClass, opts); - resourceDoc.apis.push(classHelper.generateResourceDocAPIEntry(aClass)); + var hasDocumented = false; + var methods = aClass.methods() + for (var methodKey in methods) { + hasDocumented = methods[methodKey].documented; + if (hasDocumented) { + break; + } + } + if (hasDocumented) { + resourceDoc.apis.push(classHelper.generateResourceDocAPIEntry(aClass)); + } // Add the getter for this doc. var docPath = urlJoin(opts.resourcePath, aClass.http.path); @@ -82,7 +94,9 @@ function Swagger(loopbackApplication, swaggerApp, opts) { return item.name === className; })[0]; - routeHelper.addRouteToAPIDeclaration(route, classDef, doc); + if (route.documented) { + routeHelper.addRouteToAPIDeclaration(route, classDef, doc); + } }); // Add models referenced from routes (e.g. accepts/returns) @@ -136,6 +150,8 @@ function Swagger(loopbackApplication, swaggerApp, opts) { * information about them. */ addRoute(swaggerApp, opts.resourcePath, resourceDoc, opts); + + loopbackApplication.emit('swaggerResources', resourceDoc); } function setupCors(swaggerApp, remotes) { @@ -191,16 +207,23 @@ function addRoute(app, uri, doc, opts) { * @return {Object} Resource doc. */ function generateResourceDoc(opts) { + var apiInfo = _cloneDeep(opts.apiInfo); + for (var propertyName in apiInfo) { + var property = apiInfo[propertyName]; + apiInfo[propertyName] = typeConverter.convertText(property); + } + return { swaggerVersion: opts.swaggerVersion, apiVersion: opts.version, - apis: [], // See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#513-info-object - info: opts.apiInfo + info: apiInfo, // TODO Authorizations // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#514-authorizations-object - // TODO Produces/Consumes - // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#52-api-declaration + consumes: ['application/json', 'application/xml', 'text/xml'], + produces: ['application/json', 'application/javascript', 'application/xml', 'text/javascript', 'text/xml'], + apis: [], + models: opts.models }; } diff --git a/package.json b/package.json index ff79d43..073f5d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-explorer", - "version": "1.8.0", + "version": "1.8.2", "description": "Browse and test your LoopBack app's APIs", "main": "index.js", "scripts": { @@ -21,20 +21,20 @@ "url": "https://github.com/strongloop/loopback-explorer/issues" }, "devDependencies": { - "loopback": "^2.4.1", - "mocha": "^1.21.5", - "supertest": "~0.14.0", - "chai": "^1.9.1" + "loopback": "^2.19.1", + "mocha": "^2.2.5", + "supertest": "^1.0.1", + "chai": "^3.2.0" }, "license": { "name": "Dual MIT/StrongLoop", "url": "https://github.com/strongloop/loopback-explorer/blob/master/LICENSE" }, "dependencies": { - "cors": "^2.4.2", - "debug": "~1.0.3", - "express": "3.x", - "lodash": "^2.4.1", - "strong-swagger-ui": "^20.0.0" + "cors": "^2.7.1", + "debug": "^2.2.0", + "express": "4.x", + "lodash": "^3.10.0", + "strong-swagger-ui": "^20.0.2" } } diff --git a/public/css/loopbackStyles.css b/public/css/loopbackStyles.css index ec12aa9..cf188ec 100644 --- a/public/css/loopbackStyles.css +++ b/public/css/loopbackStyles.css @@ -350,6 +350,14 @@ li.operation.delete .content > .content-type > div > label { color: #080; } +.contentWell { + padding-left: 30px; + padding-right: 30px; +} + +/* +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; diff --git a/public/index.html b/public/index.html index 6d96adb..68a14b7 100644 --- a/public/index.html +++ b/public/index.html @@ -27,19 +27,21 @@