UNPKG

resourcejs

Version:

A simple Express library to reflect Mongoose models to a REST interface.

510 lines (473 loc) 14.4 kB
const fixNestedRoutes = function(resource) { const routeParts = resource.route.split('/'); for (let i=0;i<routeParts.length;i++) { if (routeParts[i].charAt(0) === ':') { routeParts[i] = `{${ routeParts[i].slice(1) }}`; } } resource.routeFixed = routeParts.join('/'); return resource; }; const addNestedIdParameter = function(resource, parameters) { if (resource && resource.route && resource.route.includes('/:')) { if (resource.route.match(/:(.+)\//).length >= 1 && resource.route.match(/^\/(.+)\/:/).length >= 1) { const idName = resource.route.match(/:(.+)\//)[1]; const primaryModel = resource.route.match(/^\/(.+)\/:/)[1]; parameters.push({ in: 'path', name: idName, description: `The parent model of ${ resource.modelName }: ${ primaryModel}`, required: true, type: 'string', }); } } }; /** * Converts a Mongoose property to a Swagger property. * * @param options * @returns {*} */ const getProperty = function(path, name) { let options = path.options; // Convert to the proper format if needed. if (!Object.prototype.hasOwnProperty.call(options, 'type')) options = { type: options }; // If no type, then return null. if (!options.type) { return null; } // If this is an array, then return the array with items. if (Array.isArray(options.type)) { if (Object.prototype.hasOwnProperty.call(options.type[0], 'paths')) { return { type: 'array', title: name, items: { $ref: `#/definitions/${ name}`, }, definitions: getModel(options.type[0], name), }; } return { type: 'array', items: { type: 'string', }, }; } // For embedded schemas: if (options.type.constructor.name === 'Schema') { if (Object.prototype.hasOwnProperty.call(options.type, 'paths')) { return { $ref: `#/definitions/${ name}`, definitions: getModel(options.type, name), }; } } if (typeof options.type === 'function') { let functionName = options.type.toString(); functionName = functionName.substr('function '.length); functionName = functionName.substr(0, functionName.indexOf('(')); switch (functionName) { case 'ObjectId': return { 'type': 'string', 'description': 'ObjectId', }; case 'SchemaObjectId': return { 'type': 'string', 'description': 'ObjectId', }; case 'Oid': return { 'type': 'string', 'description': 'Oid', }; case 'Array': return { type: 'array', items: { type: 'string', }, }; case 'Mixed': return { type: 'object', }; case 'Buffer': return { type: 'string', }; } } switch (options.type) { case 'ObjectId': return { 'type': 'string', 'description': 'ObjectId', }; case 'SchemaObjectId': return { 'type': 'string', 'description': 'ObjectId', }; case String: return { type: 'string', }; case Number: return { type: 'integer', format: 'int64', }; case Date: return { type: 'string', format: 'date', }; case Boolean: return { type: 'boolean', }; case Function: break; case Object: return null; } if (options.type instanceof Object) return null; throw new Error(`Unrecognized type: ${ options.type}`); }; const getModel = function(schema, modelName) { // Define the definition structure. let definitions = {}; definitions[modelName] = { // required: [], title: modelName, properties: {}, }; // Iterate through each model schema path. Object.entries(schema.paths).forEach(([name, path]) => { // Set the property for the swagger model. const property = getProperty(path, name); if (name.substr(0, 2) !== '__' && property) { // Add the description if they provided it. if (path.options.description) { property.description = path.options.description; } // Add the example if they provided it. if (path.options.example) { property.example = path.options.example; } // Add the required params if needed. if (path.options.required) { // definition.required.push(name); } // 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 && !property.$ref) { console.log('Warning: That field type is not yet supported in Swagger definitions, using "string"'); console.log('Path name: %s.%s', definition.id, name); console.log('Mongoose type: %s', path.options.type); property.type = 'string'; } // Allow properties to pass back additional definitions. if (property.definitions) { definitions = Object.assign(definitions, property.definitions); delete property.definitions; } // Add this property to the definition. definitions[modelName].properties[name] = property; } }); return definitions; }; module.exports = function(resource) { resource = fixNestedRoutes(resource); // Build and return a Swagger definition for this model. const listPath = resource.routeFixed; const itemPath = `${listPath }/{${ resource.modelName }Id}`; const bodyDefinitions = getModel(resource.model.schema, resource.modelName); const swagger = { definitions: {}, paths: {}, }; // Build Swagger definitions. swagger.definitions = Object.assign(swagger.definitions, bodyDefinitions); // Build Swagger paths const methods = resource.methods; // INDEX and POST listPath if (methods.indexOf('index') > -1 || methods.indexOf('post') > -1) swagger.paths[listPath] = {}; // INDEX of listPath if (methods.indexOf('index') > -1) { swagger.paths[listPath].get = { tags: [resource.name], summary: `List multiple ${ resource.modelName } resources.`, description: `This operation allows you to list and search for ${ resource.modelName } resources provided query arguments.`, operationId: `get${ resource.modelName }s`, responses: { 401: { description: 'Unauthorized.', }, 200: { description: 'Resource(s) found. Returned as array.', schema: { type: 'array', items: { $ref: `#/definitions/${ resource.modelName}`, }, }, }, }, parameters: [ { name: 'skip', in: 'query', description: 'How many records to skip when listing. Used for pagination.', required: false, type: 'integer', default: 0, }, { name: 'limit', in: 'query', description: 'How many records to limit the output.', required: false, type: 'integer', default: 10, }, //{ // name: 'count', // in: 'query', // description: 'Set to true to return the number of records instead of the documents.', // type: 'boolean', // required: false, // default: false //}, { name: 'sort', in: 'query', description: 'Which fields to sort the records on.', type: 'string', required: false, default: '', }, { name: 'select', in: 'query', description: 'Select which fields will be returned by the query.', type: 'string', required: false, default: '', }, { name: 'populate', in: 'query', description: 'Select which fields will be fully populated with the reference.', type: 'string', required: false, default: '', }, ], }; addNestedIdParameter(resource, swagger.paths[listPath].get.parameters); } // POST listPath. if (methods.indexOf('post') > -1) { swagger.paths[listPath].post = { tags: [resource.name], summary: `Create a new ${ resource.modelName}`, description: `Create a new ${ resource.modelName}`, operationId: `create${ resource.modelName}`, responses: { 401: { description: 'Unauthorized. Note that anonymous submissions are *enabled* by default.', }, 400: { description: 'An error has occured trying to create the resource.', }, 201: { description: 'The resource has been created.', }, }, parameters: [ { in: 'body', name: 'body', description: `Data used to create a new ${ resource.modelName}`, required: true, schema: { $ref: `#/definitions/${ resource.modelName}`, }, }, ], }; addNestedIdParameter(resource, swagger.paths[listPath].post.parameters); } // The resource path for this resource. if (methods.indexOf('get') > -1 || methods.indexOf('put') > -1 || methods.indexOf('delete') > -1) swagger.paths[itemPath] = {}; // GET itemPath. if (methods.indexOf('get') > -1) { swagger.paths[itemPath].get = { tags: [resource.name], summary: `Return a specific ${ resource.name } instance.`, description: `Return a specific ${ resource.name } instance.`, operationId: `get${ resource.modelName}`, responses: { 500: { description: 'An error has occurred.', }, 404: { description: 'Resource not found', }, 401: { description: 'Unauthorized.', }, 200: { description: 'Resource found', schema: { $ref: `#/definitions/${ resource.modelName}`, }, }, }, parameters: [ { name: `${resource.modelName }Id`, in: 'path', description: `The ID of the ${ resource.name } that will be retrieved.`, required: true, type: 'string', }, ], }; addNestedIdParameter(resource, swagger.paths[itemPath].get.parameters); } // PUT itemPath if (methods.indexOf('put') > -1) { swagger.paths[itemPath].put = { tags: [resource.name], summary: `Update a specific ${ resource.name } instance.`, description: `Update a specific ${ resource.name } instance.`, operationId: `update${ resource.modelName}`, responses: { 500: { description: 'An error has occurred.', }, 404: { description: 'Resource not found', }, 401: { description: 'Unauthorized.', }, 400: { description: 'Resource could not be updated.', }, 200: { description: 'Resource updated', schema: { $ref: `#/definitions/${ resource.modelName}`, }, }, }, parameters: [ { name: `${resource.modelName }Id`, in: 'path', description: `The ID of the ${ resource.name } that will be updated.`, required: true, type: 'string', }, { in: 'body', name: 'body', description: `Data used to update ${ resource.modelName}`, required: true, schema: { $ref: `#/definitions/${ resource.modelName}`, }, }, ], }; addNestedIdParameter(resource, swagger.paths[itemPath].put.parameters); } // DELETE itemPath if (methods.indexOf('delete') > -1) { swagger.paths[itemPath].delete = { tags: [resource.name], summary: `Delete a specific ${ resource.name}`, description: `Delete a specific ${ resource.name}`, operationId: `delete${ resource.modelName}`, responses: { 500: { description: 'An error has occurred.', }, 404: { description: 'Resource not found', }, 401: { description: 'Unauthorized.', }, 400: { description: 'Resource could not be deleted.', }, 204: { description: 'Resource was deleted', }, }, parameters: [ { name: `${resource.modelName }Id`, in: 'path', description: `The ID of the ${ resource.name } that will be deleted.`, required: true, type: 'string', }, ], }; addNestedIdParameter(resource, swagger.paths[itemPath].delete.parameters); } // VIRTUAL itemPath if (methods.some(e => /^virtual\//.test(e))) { methods.filter((method) => /^virtual\//.test(method)).forEach((method) =>{ const virtualpath= `${listPath}/${method}`; swagger.paths[virtualpath] = {}; swagger.paths[virtualpath].get = { tags: [resource.name, 'virtual'], summary: `Virtual resource for ${resource.name} named ${method.split('/')[1]}`, description: `get ${resource.modelName} ${method.split('/')[1]}`, operationId: `get ${resource.modelName} ${method.split('/')[1]}`, responses: { 500: { description: 'An error has occurred.', }, 404: { description: 'Resource not found', }, 401: { description: 'Unauthorized.', }, 200: { description: 'Resource found', }, }, }; }); } // Return the swagger definition for this resource. return swagger; };