UNPKG

@coorpacademy/baucis-swagger

Version:

Generate customizable swagger definitions for your Baucis REST API.

323 lines (267 loc) 10.5 kB
const _ = require('lodash/fp'); // Convert a Mongoose type into a Swagger type function swaggerTypeFor(type) { if (!type) return null; if (type === String) return 'string'; if (type === Number) return 'double'; if (type === Date) return 'Date'; if (type === Boolean) return 'boolean'; if (Array.isArray(type) || type.name === 'Array') return 'Array'; // mongoose Types if (type === 'ObjectId' || type.name === 'ObjectID' || type.name === 'ObjectId') return 'string'; if (type.name === 'Oid') return 'string'; if (type.name === 'Buffer') return null; if (type.name === 'Mixed') return null; // Fallbacks if (type instanceof Object) return null; throw new Error(`Unrecognized type: ${type}`); } module.exports = (pluginOptions = {}) => function extendController(controller) { // A method used to generated a Swagger property for a model function generatePropertyDefinition(name, path) { const property = {}; const select = controller.select(); const type = path.options.type ? swaggerTypeFor(path.options.type) : 'string'; // virtuals don't have type const mode = select && select.match(/(?:^|\s)[-]/g) ? 'exclusive' : 'inclusive'; const exclusiveNamePattern = new RegExp(`\\B-${name}\\b`, 'gi'); const inclusiveNamePattern = new RegExp(`(?:\\B[+]|\\b)${name}\\b`, 'gi'); // Keep deselected paths private if (path.selected === false) return; // TODO is _id always included unless explicitly excluded? // If it's excluded, skip this one. if (select && mode === 'exclusive' && select.match(exclusiveNamePattern)) return; // If the mode is inclusive but the name is not present, skip this one. if (select && mode === 'inclusive' && name !== '_id' && !select.match(inclusiveNamePattern)) return; // Configure the property property.required = path.options.required || false; // TODO _id is required for PUT property.type = type; // Set enum values if applicable if (path.enumValues && path.enumValues.length > 0) { property.allowableValues = {valueType: 'LIST', values: path.enumValues}; } // Set allowable values range if min or max is present if (!isNaN(path.options.min) || !isNaN(path.options.max)) { property.allowableValues = {valueType: 'RANGE'}; } if (!isNaN(path.options.min)) { property.allowableValues.min = path.options.min; } if (!isNaN(path.options.max)) { property.allowableValues.max = path.options.max; } if (!property.type) { if (!pluginOptions.noWarning) { console.log( 'Warning: That field type is not yet supported in baucis Swagger definitions, using "string."' ); console.log('Path name: %s.%s', _.capitalize(controller.model().singular()), name); console.log('Mongoose type: %s', path.options.type); } property.type = 'string'; } return property; } // A method used to generate a Swagger model definition for a controller function generateModelDefinition() { const definition = {}; const schema = controller.model().schema; definition.id = _.capitalize(controller.model().singular()); definition.properties = {}; Object.keys(schema.paths).forEach(function(name) { const path = schema.paths[name]; const property = generatePropertyDefinition(name, path); definition.properties[name] = property; }); Object.keys(schema.virtuals).forEach(function(name) { const path = schema.virtuals[name]; const property = generatePropertyDefinition(name, path); definition.properties[name] = property; }); return definition; } // Generate parameter list for operations function generateParameters(verb, plural, resourceRef, resourceRefName) { const parameters = []; // Parameters available for singular routes if (!plural) { parameters.push({ paramType: 'path', name: resourceRef, description: `The ${resourceRefName} of a ${controller.model().singular()}`, dataType: 'string', required: true, allowMultiple: false }); parameters.push({ paramType: 'header', name: 'X-Baucis-Update-Operator', description: '**BYPASSES VALIDATION** May be used with PUT to update the document using $push, $pull, or $set.', dataType: 'string', required: false, allowMultiple: false }); } // Parameters available for plural routes if (plural) { parameters.push({ paramType: 'query', name: 'skip', description: 'How many documents to skip.', dataType: 'int', required: false, allowMultiple: false }); parameters.push({ paramType: 'query', name: 'limit', description: 'The maximum number of documents to send.', dataType: 'int', required: false, allowMultiple: false }); parameters.push({ paramType: 'query', name: 'count', description: 'Set to true to return count instead of documents.', dataType: 'boolean', required: false, allowMultiple: false }); parameters.push({ paramType: 'query', name: 'conditions', description: 'Set the conditions used to find or remove the document(s).', dataType: 'string', required: false, allowMultiple: false }); parameters.push({ paramType: 'query', name: 'sort', description: 'Set the fields by which to sort.', dataType: 'string', required: false, allowMultiple: false }); } // Parameters available for singular and plural routes parameters.push({ paramType: 'query', name: 'select', description: 'Select which paths will be returned by the query.', dataType: 'string', required: false, allowMultiple: false }); parameters.push({ paramType: 'query', name: 'populate', description: 'Specify which paths to populate.', dataType: 'string', required: false, allowMultiple: false }); if (verb === 'post') { // TODO post body can be single or array parameters.push({ paramType: 'body', name: 'document', description: 'Create a document by sending the paths to be updated in the request body.', dataType: _.capitalize(controller.model().singular()), required: true, allowMultiple: false }); } if (verb === 'put') { parameters.push({ paramType: 'body', name: 'document', description: 'Update a document by sending the paths to be updated in the request body.', dataType: _.capitalize(controller.model().singular()), required: true, allowMultiple: false }); } return parameters; } function generateErrorResponses(plural) { const errorResponses = []; // TODO other errors (400, 403, etc. ) // Error rosponses for singular operations if (!plural) { errorResponses.push({ code: 404, reason: `No ${controller.model().singular()} was found with that ID.` }); } // Error rosponses for plural operations if (plural) { errorResponses.push({ code: 404, reason: `No ${controller.model().plural()} matched that query.` }); } // Error rosponses for both singular and plural operations // None. return errorResponses; } /** * Generate a list of a controller's operations */ function generateOperations(plural, resourceRef, resourceRefName) { const operations = []; controller.methods().forEach(function(verb) { const operation = {}; const model = controller.model(); const titlePlural = _.capitalize(model.plural()); const titleSingular = _.capitalize(model.singular()); // Don't do head, post/put for single/plural if (verb === 'head') return; if (verb === 'post' && !plural) return; if (verb === 'put' && plural) return; // Use the full word if (verb === 'del') verb = 'delete'; operation.httpMethod = verb.toUpperCase(); if (plural) operation.nickname = verb + titlePlural; else operation.nickname = `${verb + titleSingular}ById`; operation.responseClass = titleSingular; // TODO sometimes an array! if (plural) operation.summary = `${_.capitalize(verb)} some ${model.plural()}`; else operation.summary = `${_.capitalize( verb )} a ${model.singular()} by its unique ${resourceRefName}`; operation.parameters = generateParameters(verb, plural, resourceRef, resourceRefName); operation.errorResponses = generateErrorResponses(plural); operations.push(operation); }); return operations; } controller.generateSwagger = function() { if (controller.swagger) return controller; const modelName = _.capitalize(controller.model().singular()); const _findBy = controller.findBy(); const resourceRef = _findBy === '_id' ? 'id' : _findBy; const resourceRefName = _findBy === '_id' ? 'ID' : _findBy; controller.swagger = {apis: [], models: {}}; // Model controller.swagger.models[modelName] = generateModelDefinition(); // Instance route controller.swagger.apis.push({ path: `/${controller.model().plural()}/{${resourceRef}}`, description: `Operations about a given ${controller.model().singular()}`, operations: generateOperations(false, resourceRef, resourceRefName) }); // Collection route controller.swagger.apis.push({ path: `/${controller.model().plural()}`, description: `Operations about ${controller.model().plural()}`, operations: generateOperations(true, resourceRef, resourceRefName) }); return controller; }; return controller; }; module.exports.swaggerTypeFor = swaggerTypeFor;