UNPKG

merest

Version:

Express based REST-full API for Mongoose models

247 lines (215 loc) 9.57 kB
'use strict'; const path = require('path').posix; const express = require('express'); const Router = express.Router; const mongoose = require('mongoose'); const helpers = require('./helpers'); const ModelAPIError = require('./model-api-error'); const controllers = require('./controllers'); const middlewares = require('./middlewares'); const ensureMethodObj = helpers.ensureMethodObj; const normalizeReadonly = helpers.normalizeReadonly; const camel2giffen = helpers.camel2giffen; const flat = helpers.flat; const addController = helpers.addController; const exposeMethods = helpers.exposeMethods; /** * @class ModelAPIRouter - express router dispatches REST-calls to api-controllers * * @param {mongoose.Model|String} model model to be exposed * @param {Object} [options] router configuration object * @param {Object|Boolean} [options.options] options for OPTIONS http-method * @param {Object|Boolean} [options.create] options for CREATE REST-method * @param {Object|Boolean} [options.search] options for SEARCH(LIST) REST-method * @param {Object|Boolean} [options.details] options for DETAILS REST-method * @param {Object|Boolean} [options.update] options for UPDATE REST-method * @param {Object|Boolean} [options.delete] options for DELETE REST-method * @param {Object|Boolean} [options.expose] options for exposing methods of model * @param {Object|Boolean} [options.exposeStatic] options for exposing static model methods * @param {String} [options.path] the path to mount router (levels: router|METHOD) * @param {Object|Function} [options.filter] the mongodb-filter object or function that returns the same type object (levels: router|METHOD) * @param {Object} [options.fields] the mongodb-fields descriptor (levels: router|METHOD) * @param {String|Object|Array} [options.readonly] the list of fields (paths) that couldn't be modified directly via create and update * @param {Object} [options.queryFields] the object with keys that defines allow to use field in query or not * @param {String|Object|Array} [options.popuplate] the mongoose population descriptor (levels: router|METHOD) * @param {Boolean} [options.skip] the flag to allow or deny skipping documents (levels: router|METHOD) * @param {Boolean} [options.limit] the flag to allow or deny limit documents number (levels: router|METHOD) * @param {Object|Boolean} [options.sort] the flag or an object that defines fields to be used for documents corting (levels: router|METHOD) * @param {Function|Array} [options.middlewares] the Function or Array of Function that should be called before the router controller will be called (levels: router|METHOD) * @param {String|RegExp} [options.matchId] the String or RegExp that allows to identify values of _id to build correct routing paths * @param {String} [options.title] the short description of the path * @return {ModelAPIRouter} created router */ function ModelAPIRouter(model, options) { options = options || {}; const router = express(); router.__proto__ = ModelAPIRouter.prototype; // router.application = null; router.model = (typeof model === 'string') ? mongoose.model(model) : model; router.apiOptions = router.__applyOptions(options) router.middlewares = router.apiOptions.middlewares; // router.nameSingle = options.apiName || router.model.modelName; router.nameSingleURI = camel2giffen(router.nameSingle); router.plural = options.plural || router.model.collection.name; router.pluralURI = camel2giffen(router.plural); router.path = router.apiOptions.path || ('/' + router.pluralURI); router.__configureProperties(); return router; } ModelAPIRouter.prototype = Object.create(Router); ModelAPIRouter.prototype.constructor = ModelAPIRouter; /** * ModelAPIRouter.prototype.__configureProperties - configures properties of object * * @private */ ModelAPIRouter.prototype.__configureProperties = function() { Object.defineProperties(this, { application: {writeable: true, enumerable: false}, apiOptions: {writeable: false, enumerable: false}, model: {writeable: false, enumerable: false}, middleware: {wrtitable: false, enumerable: false}, }); } /** * ModelAPIRouter.prototype.__applyOptions - overrides default options by custom options * * @param {Object} options Custom options for model exposition * @return {Object} The overrided options * * @private */ ModelAPIRouter.prototype.__applyOptions = function(options) { return { options: ensureMethodObj(options.options, 'options'), create: ensureMethodObj(options.create, 'post'), search: ensureMethodObj(options.search, 'get'), details: ensureMethodObj(options.details, 'get'), update: ensureMethodObj(options.update, 'post'), delete: ensureMethodObj(options.delete, 'delete'), expose: options.expose || { '*': false }, exposeStatic: options.exposeStatic || { '*': false }, // default options for all methods path: options.path || null, filter: options.filter || null, fields: options.fields || {}, readonly: normalizeReadonly(options.readonly), queryFields: options.queryFields || null, sortFields: options.sortFields || null, populate: options.populate || null, skip: options.skip == null ? true : options.skip, limit: options.limit == null ? true : options.limit, sort: options.sort == null ? true : options.sort, middlewares: options.middlewares || null, matchId: options.matchId || options.id || '[a-f\\d]{24}', title: options.title || '' }; }; /** * ModelAPIRouter.prototype.option - defines options for certain method * * @param {String} method the method which option should be defined * @param {String} name the name of option * @return {*} defined option value * * @private */ ModelAPIRouter.prototype.option = function(method, name) { const methodOptions = this.apiOptions[method]; if (methodOptions && typeof(methodOptions[name]) !== 'undefined') { return methodOptions[name]; } return this.apiOptions[name]; } /** * ModelAPIRouter.prototype.attachTo - attaches the router to the ModelAPIExpress * * @param {ModelAPIExpress} app the API Application to which the router should be mounted * @return {ModelAPIRouter} self * */ ModelAPIRouter.prototype.attachTo = function(app) { if (this.application) return this; this.application = app; this.application.__addModelAPIrouter(this); this.__init(); if (this.middlewares && this.middlewares.length) { let args = [this.path]; args = args.concat(this.middlewares); this.application.use.apply(this.application, args); } this.application.use(this.path, this); return this; } /** * ModelAPIRouter.prototype.urls - returns avaliable urls * * @param {String} rootPath the path to the router (before it) * @return {Array} the list of avaliable urls */ ModelAPIRouter.prototype.urls = function(rootPath) { if (this.rootPath && this._urls) return this._urls; this._urls = this._urls || []; if (rootPath) { this.rootPath = rootPath; for (let i=0; i<this._urls.length; i++) { this._urls[i][1] = path.join(rootPath, this._urls[i][1]); } } return this._urls; } /** * ModelAPIRouter.prototype.__init - initializes routes * * @private */ ModelAPIRouter.prototype.__init = function() { const self = this; this._urls = []; this._controllers = []; let path = "/"; this.use(middlewares.registerApi(false, this)); if (this.apiOptions.options) { addController(this, 'options', path, this.apiOptions.options, controllers.options, "List API-options for "+this.plural) } else { addController(this, null, path, {method: 'options'}, controllers.notSupported, false) } if (this.apiOptions.search) { addController(this, 'search', path, this.apiOptions.search, controllers.search, "List/Search all "+this.plural) } if (this.apiOptions.create) { addController(this, 'create', path, this.apiOptions.create, controllers.create, "Create a new "+this.nameSingle) } exposeMethods(this, path, this.apiOptions.exposeStatic, this.model.schema.statics, controllers.callStaticMethod); path = "/:id"; if (this.apiOptions.details) { addController(this, 'findById', path, this.apiOptions.details, controllers.find.bind(this, 'details'), "Find a "+this.nameSingle+" by Id") } if (this.apiOptions.update) { addController(this, 'update', path, this.apiOptions.update, controllers.update, "Find a "+this.nameSingle+" by Id and update it (particulary)") } if (this.apiOptions.delete) { addController(this, 'delById', path, this.apiOptions.delete, controllers.delete, "Find a "+this.nameSingle+" by Id and delete it.") } exposeMethods(this, path, this.apiOptions.expose, this.model.schema.methods, controllers.callInstanceMethod); } module.exports = exports = ModelAPIRouter;