diff options
Diffstat (limited to 'common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib')
3 files changed, 707 insertions, 0 deletions
diff --git a/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/handlers.js b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/handlers.js new file mode 100644 index 0000000..8423310 --- /dev/null +++ b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/handlers.js @@ -0,0 +1,221 @@ +var _ = require('underscore'); + +/* + * The last handler to be called in the chain of middleware + * This figures out what response format it should be in and sends it + */ +exports.last = function(req, res, next) { + if (res.locals.bundle) { + if (req.body.format === 'js') { + return res.send(res.locals.bundle); + } else if (req.body.format === 'html' || req.query.format === 'html') { + return res.render(this.templateRoot + '/' + req.templatePath, res.locals.bundle); + } else { + return res.status(res.locals.status_code).json(res.locals.bundle); + } + } + res.send(); +}; + +exports.schema = function(req, res, next) { + // We can mount a model to multiple apps, so we need to get the base url from the request url + var baseuri = req.url.split('/'); + baseuri = baseuri.slice(0, baseuri.length - 1).join('/'); + var detailuri = baseuri + '/:id'; + exports.respond(res, 200, { + resource: this.modelName, + allowed_methods: Object.keys(this.allowed_methods), + list_uri: baseuri, + detail_uri: detailuri, + fields: keep(this.schema.paths, ['regExp', 'path', 'instance', 'isRequired']) + }); + next(); +}; + +exports.get = function(req, res, next) { + req.quer.exec(function(err, list) { + if (err) { + exports.respond(res, 500, err); + } else if (req.params.id) { + exports.respondOrErr(res, 404, !list && exports.objectNotFound(), 200, (list && _.isArray(list)) ? list[0] : list); + } else { + exports.respondOrErr(res, 500, err, 200, list); + } + next(); + }); +}; + +exports.getDetail = function(req, res, next) { + req.quer.exec(function(err, one) { + exports.respondOrErr(res, 500, err, 200, one); + next(); + }); +}; + +/** + * Generates a handler that returns the object at @param pathName + * where pathName is the path to an objectId field + */ +exports.getPath = function(pathName) { + return function(req, res, next) { + req.quer = req.quer.populate(pathName); + req.quer.exec(function(err, one) { + var errStatus = ((err && err.status) ? err.status : 500); + exports.respondOrErr(res, errStatus, err, 200, (one && one.get(pathName)) || {}); + next(); + }); + }; +}; + +exports.post = function(req, res, next) { + var obj = new this(req.body); + obj.save(function(err) { + exports.respondOrErr(res, 400, err, 201, obj); + next(); + }); +}; + +exports.put = function(req, res, next) { + // Remove immutable ObjectId from update attributes to prevent request failure + if (req.body._id && req.body._id === req.params.id) { + delete req.body._id; + } + + // Update in 1 atomic operation on the database if not specified otherwise + if (this.shouldUseAtomicUpdate) { + req.quer.findOneAndUpdate({}, req.body, this.update_options, function(err, newObj) { + if (err) { + exports.respond(res, 500, err); + } else if (!newObj) { + exports.respond(res, 404, exports.objectNotFound()); + } else { + exports.respond(res, 200, newObj); + } + next(); + }); + } else { + // Preform the update in two operations allowing mongoose to fire its schema update hook + req.quer.findOne({"_id": req.params.id}, function(err, docToUpdate) { + if (err) { + exports.respond(res, 500, err); + } + var objNotFound = !docToUpdate && exports.objectNotFound(); + if (objNotFound) { + exports.respond(res, 404, objNotFound); + return next(); + } + + docToUpdate.set(req.body); + docToUpdate.save(function (err, obj) { + exports.respondOrErr(res, 400, err, 200, obj); + next(); + }); + }); + } +}; + +exports.delete = function(req, res, next) { + // Delete in 1 atomic operation on the database if not specified otherwise + if (this.shouldUseAtomicUpdate) { + req.quer.findOneAndRemove({}, this.delete_options, function(err, obj) { + if (err) { + exports.respond(res, 500, err); + } + exports.respondOrErr(res, 404, !obj && exports.objectNotFound(), 204, {}); + next(); + }); + } else { + // Preform the remove in two steps allowing mongoose to fire its schema update hook + req.quer.findOne({"_id": req.params.id}, function(err, docToRemove) { + if (err) { + exports.respond(res, 500, err); + } + var objNotFound = !docToRemove && exports.objectNotFound(); + if (objNotFound) { + exports.respond(res, 404, objNotFound); + return next(); + } + + docToRemove.remove(function (err, obj) { + exports.respondOrErr(res, 400, err, 204, {}); + next(); + }); + }); + } +}; + +// I'm going to leave these here because it might be nice to have standardized +// error messages for common failures + +exports.objectNotFound = function() { + return { + status: 404, + message: 'Object not found', + name: 'ObjectNotFound', + errors: { + _id: { + message: "Could not find object with specified attributes" + } + } + }; +}; +exports.respond404 = function() { + return { + status: 404, + message: 'Page Not Found', + name: "PageNotFound", + errors: 'Endpoint not found for model ' + this.modelName + }; +}; +exports.authFailure = function() { + return { + status: 401, + message: 'Unauthorized', + name: "Unauthorized", + errors: 'Operation not authorzed on ' + this.modelName + }; +}; +exports.badRequest = function(errobj) { + return { + status: 400, + message: 'Bad Request', + name: "BadRequest", + errors: errobj || "Your request was invalid" + }; +}; + +/** + * Takes a response, error, success statusCode and success payload + * + * If there is an error, it returns a 400 with the error as the payload + * If there is no error, it returns statusCode with the specified payload + * + */ +exports.respondOrErr = function(res, errStatusCode, err, statusCode, content) { + if (err) { + exports.respond(res, errStatusCode, err); + } else { + exports.respond(res, statusCode, content); + } +}; + +exports.respond = function(res, statusCode, content) { + res.locals.status_code = statusCode; + res.locals.bundle = content; +}; + +function keep(obj, keepers) { + var result = {}; + for (var key in obj) { + result[key] = {}; + for (var key2 in obj[key]) { + if (keepers.indexOf(key2) > -1) { + result[key][key2] = obj[key][key2]; + } + if ('schema' === key2) { + result[key][key2] = keep(obj[key][key2].paths, keepers); + } + } + } + return result; +} diff --git a/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/model.js b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/model.js new file mode 100644 index 0000000..1531498 --- /dev/null +++ b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/model.js @@ -0,0 +1,479 @@ +var mongoose = require('mongoose'), + _ = require('underscore'), + Model = mongoose.Model, + handlers = require('./handlers'); + +exports = module.exports = model; + +var methods = ['get', 'post', 'put', 'delete'], // All HTTP methods, PATCH not currently supported + endpoints = ['get', 'post', 'put', 'delete', 'getDetail'], + defaultroutes = ['schema'], + lookup = { + 'get': 'index', + 'getDetail': 'show', + 'put': 'updated', + 'post': 'created', + 'delete': 'deleted' + }, + valid_alterables = filterable({ + 'populate': query('populate'), + }, {}); + valid_filters = filterable({ + 'limit': query('limit'), + 'skip': query('skip'), + 'offset': query('offset'), + 'select': query('select'), + 'sort': query('sort'), + }, { + 'equals': query('equals'), + 'gte': query('gte'), + 'gt': query('gt'), + 'lt': query('lt'), + 'lte': query('lte'), + 'ne': query('ne'), + 'regex': function(val, query) { + var regParts = val.match(/^\/(.*?)\/([gim]*)$/); + if (regParts) { + // the parsed pattern had delimiters and modifiers. handle them. + val = new RegExp(regParts[1], regParts[2]); + } else { + // we got pattern string without delimiters + val = new RegExp(val); + } + + return query.regex(val); + }, + 'in': query('in'), + 'nin': query('nin'), + }); + defaults = function() { + return { + routes: {}, + allowed_methods: { + get: { detail: false } + }, + update_options: {}, + remove_options: {}, + templateRoot: '', + shouldIncludeSchema: true, + shouldUseAtomicUpdate: true + }; + }; + +/** + * Returns the model associated with the given name or + * registers the model with mongoose + */ +function model() { + var result = mongoose.model.apply(mongoose, arguments), + default_properties = defaults(); + if (1 === arguments.length) return result; + + for (var key in default_properties) { + result[key] = default_properties[key]; + } + + return result; +} + +Model.includeSchema = function(shouldIncludeSchema) { + this.shouldIncludeSchema = shouldIncludeSchema; + return this; +}; + +Model.methods = function(newmethods) { + var self = this, + get = contains(newmethods, 'get'); + + methods.forEach(function(method) { + delete self.routes[method]; + }); + + this.allowed_methods = []; + if (!Array.isArray(newmethods)) { + newmethods = [newmethods]; + } + if (get && !contains(newmethods, 'getDetail')) { + newmethods.push({ + method: 'getDetail', + before: (typeof get !== 'string') ? get.before : null, + after: (typeof get !== 'string') ? get.after : null + }); + } + newmethods.forEach(function(meth) { + var method = meth.method; + if ('string' === typeof meth) { + method = meth; + meth = {}; + } + if (!method) throw new Error("Method object must have a method property"); + self.allowed_methods.push(method); + + meth.handler = handlers[method]; + meth.detail = (method !== 'get' && method !== 'post'); + self.route(method, meth); + }); + return this; +}; + +Model.updateOptions = function(options) { + this['update_options'] = options; + return this; +}; + +Model.removeOptions = function(options) { + this['remove_options'] = options; + return this; +}; + +Model.template = function(templatePath) { + if (templatePath.substr(-1) == '/') { + templatePath = templatePath.substr(0, templatePath.length - 1); + } + this.templateRoot = templatePath; + return this; +}; + +/** + * Adds the default routes for the HTTP methods and one to get the schema + */ +Model.addDefaultRoutes = function() { + if (this.shouldIncludeSchema) { + this.route('schema', handlers.schema); + } + this.addSchemaRoutes(); +}; + +Model.addSchemaRoutes = function() { + var self = this; + this.schema.eachPath(function(pathName, schemaType) { + if (pathName.indexOf('_id') === -1 && schemaType.instance === 'ObjectID') { + // Right now, getting nested models is the only operation supported + ['get'].forEach(function(method) { + self.route(pathName, method , { + handler: handlers[method + 'Path'].call(self, pathName), + detail: true + }); + }); + } + }); +}; + +/** + * Adds an internal route for a path and method or methods to a function + * + * @param {String|Object} path: absolute path (including method) or object of routes + * @param {String|Function} method: the method to route to or the handler function + * @param {Function} fn: The handler function + * @return {Model} for chaining + * @api public + */ +Model.route = function(path, method, fn) { + var route = getRoute(this.routes, path), + meths = methods, // Default to all methods + lastPath = path.substr(path.lastIndexOf('.') + 1); + + if (2 === arguments.length) { + fn = method; + if (!fn.methods && endpoints.indexOf(lastPath) > -1) { + meths = [lastPath]; + } else if (fn.methods) { + meths = fn.methods; + } + } else { + meths = [method]; + } + + if (fn) { + fn = normalizeHandler(fn); + + meths.forEach(function(meth) { + route[meth] = merge(route[meth], fn); + }); + } + return this; +}; + +Model.before = function(path, method, fn) { + if (2 == arguments.length) { + arguments[1] = { before: arguments[1] }; + } + return this.route.apply(this, arguments); +}; + +Model.after = function(path, method, fn) { + if (2 == arguments.length) { + arguments[1] = { after: arguments[1] }; + } + return this.route.apply(this, arguments); +}; + +/** + * Registers all of the routes in routeObj to the given app + * + * TODO(baugarten): refactor to make less ugly + * + * if (isEndpoint(routeObj, path)) { handleRegistration(app, prefix, path, routeObj); } + * else { + * for (var key in routeObj) { recurse } + * } + */ +Model.registerRoutes = function(app, prefix, path, routeObj) { + var self = this; + for (var key in routeObj) { + if (isEndpoint(routeObj, key)) { + var route = routeObj[key]; + var routehandlers = _.isArray(route.handler) ? route.handler : [route.handler]; + routehandlers = _.map(routehandlers, function(handler) { return handler.bind(self); }); + var detailGet = !route.detail && !path && key === 'get', + handlerlist = route.before.concat( + [preprocess.bind(self)], + routehandlers, + route.after, + [handlers.last] + ); + /** + * TODO(baugarten): Add an enum type-thing to specify detail route, detail optional or list + * aka prettify this + */ + if (route.detail) { + app[key](prefix + '/:id([0-9a-fA-F]{0,24})' + path , handlerlist); + } else if (detailGet) { + app[key](prefix + '/:id([0-9a-fA-F]{0,24}$)?', handlerlist); + } else { + app[key](prefix + path, handlerlist); + } + } else { + this.registerRoutes(app, prefix, path + '/' + key, routeObj[key]); + } + } +}; + +/** + * Registers this model to the given app + * + * This includes registering endpoints for all the methods desired + * in the model definition + * + */ +Model.register = function(app, url) { + this.addDefaultRoutes(); + app.getDetail = app.get; + this.registerRoutes(app, url, '', this.routes); +}; + +// Will I still support handle()? I think maybe for default routes it might be nice, but +// exposed via model.get, model.post, etc. +/*Model.prototype.handle = function(route, filters, data, callback) { + if (arguments.length === 3) { + callback = data; + data = {}; + } else if (arguments.length === 2) { + callback = filters; + filters = []; + data = {}; + } + route = route.replace(/\//g, /\./); + data.format = 'js'; + var req = { + url: route, + filters: filters, + body: data, + format: 'js', + } + var res = { + writeHeader: function() { }, + write: function(ret) { callback(ret); }, + send: function() {}, + }; + this.send(route.split(/\./), req, res); +} + +Model.prototype.send = function(routes, req, res, next) { + var handler = this.routes; + req.quer = this.filter(req.filters, req.body, req.query, this.Model.find({})); + req.templatePath = this.template(routes, req.filters); + routes.forEach(function(route) { + if (route in handler) handler = handler[route]; + else if (!('all' in handler)) { + handlers.respond(res, 404, handlers.respond404()); + handlers.last(req, res); + } + }); + if ('all' in handler) handler = handler.all; + + if ('function' === typeof handler) { + return handler.call(this, req, res, next); + } + + handlers.respond(res, 404, handlers.respond404()); + handlers.last(req, res); +}*/ + +/** + * Returns a query filtered by the data in the request + * Looks in req.body and req.query to get the filterable data + * Filters the query based on functions in valid_filters + */ +Model.filter = function(req, quer) { + var detail = false; // detail route + // filter by id + if (req.params.id) { + quer = this.findById(req.params.id); + detail = true + } + + [req.body, req.query, req.headers].forEach(function(alterableResponse) { + Object.keys(alterableResponse).filter(function(potential) { + return valid_alterables.contains(potential, quer); + }).forEach(function(valid_key) { + query = valid_alterables.filter(valid_key, alterableResponse[valid_key], quer); + }); + }); + + if (!detail) { + [req.body, req.query, req.headers].forEach(function(filterableData) { + Object.keys(filterableData).filter(function(potential_filter) { + return valid_filters.contains(potential_filter, quer); + }).forEach(function(valid_key) { + quer = valid_filters.filter(valid_key, filterableData[valid_key], quer); + }); + }); + } + return quer; +} + +function preprocess(req, res, next) { + req.body = req.body || {}; + req.query = req.query || {}; + req.quer = this.filter(req, this.find({})); + if (!('locals' in res)) { + res.locals = {}; + } + res.locals.bundle = {}; + + req.templatePath = resolveTemplate(req); + next(); +} + +function query(key) { + return function(val, query) { + return query[key](val); + }; +} + +function haveOneModel(req) { + return !!req.params.id; +} + +function resolveTemplate(req) { + var method = req.method.toLowerCase(), + tmplName; + if (methods.indexOf(method) > -1) { + if (haveOneModel(req) && method === 'get') { + method += 'Detail'; + } + tmplName = lookup[method]; + } + return tmplName; +} + +/** + * Merges a route with another function object + * fn.before is called after the old before + * fn.after is called before the old after + * If fn.handler is specified, then route.handler is overwritten + */ +function merge(route, fn) { + if (!route) return fn; + + route.before = route.before.concat(fn.before); + route.handler = fn.handler || route.handler; + route.after = fn.after.concat(route.after); + return route; +} + +function getRoute(routes, path) { + path = path.replace(/\//g, /\./).split(/\./); + if (1 === path.length && '' === path[0]) { // we got the empty string + path = []; + } + if (endpoints.indexOf(path[path.length - 1]) > -1) { + path.splice(path.length - 1, 1); + } + path.forEach(function(sub, i) { + if (!routes[sub]) routes[sub] = {}; + routes = routes[sub]; + }); + return routes; +} + +function normalizeHandler(fn) { + var result = {}; + result.handler = fn.handler; + result.detail = fn.detail; + if ({}.toString.call(fn) == '[object Function]') { + result = { + handler: fn + }; + } + ['before', 'after'].forEach(function(hook) { + result[hook] = fn[hook] || []; + if (!Array.isArray(result[hook])) { + result[hook] = [ result[hook] ]; + } + }); + return result; +} + +function isEndpoint(route, method) { + return endpoints.indexOf(method) > -1 && route[method].handler; +} + +function contains(arr, key) { + if (arr.indexOf(key) > -1) return true; + for (var obj in arr) { + if (obj.method === key) { + return true; + } + } + return false; +}; + +function coerceData(filter_func, data) { + // Assume data is a string + if (data && data.toLowerCase && data.toLowerCase() === 'true') { + return true; + } else if (data && data.toLowerCase && data.toLowerCase() === 'false') { + return false; + } else if (filter_func === 'limit' || filter_func === 'skip') { + return parseInt(data); + } + return data; +}; + +function filterable(props, subfilters) { + return { + filter: function(key, val, quer) { + if (props[key]) { + return props[key](val, quer); + } + var field = key.split('__'), + filter_func = field[1] || 'equals', + data = coerceData(filter_func, val); + + // Turn data into array for $in and $nin clause + if (filter_func === 'in' || filter_func === 'nin') { + data = data.split(','); + } + + return subfilters[filter_func](data, quer.where(field[0])); + }, + contains: function(key, quer) { + if (key in props) return true; + var field = key.split('__'); + var filter_func = field[1] || 'equals'; + return field[0] in quer.model.schema.paths && filter_func in subfilters; + } + } +} diff --git a/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/restful.js b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/restful.js new file mode 100644 index 0000000..0194a5b --- /dev/null +++ b/common/src/main/webapp/usageguide/appserver/node_modules/node-restful/lib/restful.js @@ -0,0 +1,7 @@ +var model = require('./model'), + handlers = require('./handlers'), + mongoose = require('mongoose'); + +exports = module.exports = handlers; +exports.model = model; +exports.mongoose = mongoose; |