UNPKG

@momsfriendlydevco/moody

Version:

MOngoose On DYnamo - Wrapper for AWS, bringing the API closer to Mongoose

180 lines (165 loc) 7.74 kB
var _ = require('lodash'); var debug = require('debug')('moody'); /** * ReST server middleware for Express * This middleware is designed to be used with `app.use` rather than picking specific endpoints, although this is also possible if needed * * NOTES: * * Middleware functions are standard Express pattern middlewares, they can either be a single function or an array * * This function is available as either `moody.serve(modelName, options)` or `dyanmoosey.models.MODEL.serve(options)` * * @param {MoodyModel} model Moody model to link against * @param {Object} [options] Options object * @param {string} [options.param="id"] Where to look in req.params for the document ID to get/update/delete * @param {string} [options.countToken="count"] Special case to of req.params[options.param] when to count documents rather than query them * @param {boolean|array <function>|function} [options.get=true] Enable getting of records or specify middleware(s) to execute beforehand * @param {boolean|array <function>|function} [options.query=true] Enable querying of records or specify middleware(s) to execute beforehand * @param {boolean|array <function>|function} [options.count=true] Enable counting of records or specify middleware(s) to execute beforehand * @param {boolean|array <function>|function} [options.create=false] Enable creating of records or specify middleware(s) to execute beforehand * @param {boolean|array <function>|function} [options.save=false] Enable updating of records or specify middleware(s) to execute beforehand * @param {boolean|array <function>|function} [options.delete=false] Enable deleting of records or specify middleware(s) to execute beforehand * @param {object|function <Promise>|function} [options.queryForce] Override the incomming req.query object with either a static object or an evaluated promise returns. Called as `(req)` * @param {boolean} [options.softQuery=false] Enable Sift powered soft querying (slow but more accurate than Dynamo) * @param {function <Promise>|function} [options.queryValidate] Validate an incomming query, similar to `queryForce`. Throw an error to reject. Called as `(req)`. * @param {function} [options.errorHandler] How to handle errors, default is to use Expresses `res.status(code).send(text)` method. Called as (res, code, text) * * @example Create a simple ReST server of 'users' with default options * app.use('/api/users', dynamoose.serve('users')) * * @example Create a ReST server where widgets can be created, updated and deleted as well as the default queries * app.use('/api/widgets', dynamoose.serve('widgets', { * create: true, * save: true, * delete: (req, res, next) => res.send('Are you sure you should be deleting that?'), * )) */ var MoodyRestServer = function(model, options) { var settings = { param: 'id', countToken: 'count', get: true, query: true, count: true, create: false, save: false, delete: false, searchId: model.settings.idField, errorHandler: (res, code, text) => res.status(code).send(text), softQuery: false, ...model.my.settings.serve, ...options, }; var removeMetaParams = query => _.omit(query, ['limit', 'select', 'skip', 'sort']); return (req, res) => { var serverMethod; Promise.resolve() // Determine serverMethod {{{ .then(()=> { // Work out method to use (GET /api/:id -> 'get', POST /api/:id -> 'save' etc.) if (req.method == 'GET' && settings.countToken && req.params[settings.param] && req.params[settings.param] == settings.countToken) { // Count matches serverMethod = 'count'; } else if (req.method == 'GET' && req.params[settings.param] != undefined) { // Get one document serverMethod = 'get'; } else if (req.method == 'GET') { // List all documents (filtered via req.query) serverMethod = 'query'; } else if (req.method == 'POST' && req.params[settings.param] != undefined) { // Update an existing document serverMethod = 'save'; } else if (req.method == 'POST') { // Create a new document (from req.body) serverMethod = 'create'; } else if (req.method == 'DELETE' && req.params[settings.param] != undefined) { // Delete one document serverMethod = 'delete'; } else { throw new Error('Unknown endpoint'); } if (settings[serverMethod] === false) throw new Error('Not found'); // Endpoint is disabled }) // }}} // Force query injection via queryForce {{{ .then(()=> { if (!settings.queryForce || serverMethod == 'get' || serverMethod == 'save' || serverMethod == 'create' || serverMethod == 'delete') return; if (_.isFunction(settings.queryForce)) { return Promise.resolve(settings.queryForce(req, res)) .then(newQuery => req.query = newQuery) } else if (_.isObject(settings.queryForce)) { req.query = settings.queryForce; } }) // }}} // Query validation {{{ .then(()=> { if (!settings.queryValidate || serverMethod == 'get' || serverMethod == 'save' || serverMethod == 'create' || serverMethod == 'delete') return; return Promise.resolve(settings.queryValidate(req, res)) }) // }}} // Run middleware {{{ .then(()=> { var middleware = settings[serverMethod]; if (middleware === true || (Array.isArray(middleware) && !middleware.length)) { // Endpoint enabled or no middleware to call return; // Pass through } else if (Array.isArray(middleware)) { // Array of middleware - run in series until exhausted return new Promise((resolve, reject) => { var middlewareOffset = 0; var runNextMiddleware = err => { if (err) return reject(err); var thisMiddleware = middleware[middlewareOffset++]; if (!thisMiddleware) return resolve(); // Exhausted all middleware thisMiddleware(req, res, runNextMiddleware); } runNextMiddleware(); }); } else if (typeof middleware == 'function') { // Single function return new Promise((resolve, reject) => { middleware(req, res, err => { if (err) return reject(err); resolve(); }); }); } else { throw new Error('Unknown middleware structure'); } }) // }}} // Execute function and return (main query handler - GET, POST etc.) {{{ .then(()=> { switch (serverMethod) { case 'count': return model.count(removeMetaParams(req.query)) .then(count => ({count})) .catch(()=> res.sendStatus(404)); case 'get': return model.findOne({ [model.settings.idField]: req.params[settings.param], }) .select(req.query.select) .then(doc => { if (doc) return doc; res.sendStatus(404); return; }) .catch(()=> res.sendStatus(404)); case 'query': return model.find(removeMetaParams(req.query)) .sift(settings.softQuery) .select(req.query.select) .sort(req.query.sort) .limit(req.query.limit) .skip(req.query.skip) .catch(e => settings.errorHandler(res, 400, e)); case 'save': return model.updateOne({ [model.settings.idField]: req.params[settings.param], }, req.body) .then(doc => doc.toObject()) .catch(e => settings.errorHandler(res, 400, e)) case 'create': return model.create(req.body) .catch(e => settings.errorHandler(res, 400, e)) case 'delete': return model.deleteOne({ [model.settings.idField]: req.params[settings.param], }) .then(()=> undefined) .catch(e => settings.errorHandler(res, 400, e)) } }) // }}} // End {{{ .then(output => output == res ? res.end() : res.send(output)) // Send output if Express has not already terminated .catch(e => settings.errorHandler(res, 400, e)) // }}} }; }; module.exports = MoodyRestServer;