UNPKG

landmark-serve

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

298 lines (246 loc) 8.6 kB
var _ = require('underscore'), async = require('async'), landmark = require('../'), utils = require('landmark-utils'); /** * UpdateHandler Class * * @param {Object} item to update * @api public */ function UpdateHandler(list, item, req, res, options) { if (!(this instanceof UpdateHandler)) return new UpdateHandler(list, item); this.list = list; this.item = item; this.req = req; this.res = res; this.user = req.user; this.options = options || {}; if (!this.options.errorMessage) { this.options.errorMessage = 'There was a problem saving your changes:'; } if (this.options.user) { this.user = this.options.user; } this.validationMethods = {}; this.validationErrors = {}; } /** * Adds a custom validation method for a given path * * @param {string} path to call method for * @param {function} method to call * @api public */ UpdateHandler.prototype.validate = function(path, fn) { this.validationMethods[path] = fn; return this; }; /** * Adds a validationError to the updateHandler; can be used before * `.process()` is called to handle errors generated by custom pre- * processing. * * @param {string} path that failed validation * @param {string} message to display * @param {string} error type (defaults to 'required') * @api public */ UpdateHandler.prototype.addValidationError = function(path, msg, type) { this.validationErrors[path] = { name: 'ValidatorError', path: path, message: msg, type: type || 'required' }; return this; }; /** * Processes data from req.body, req.query, or any data source. * * Options: * - fields (comma-delimited list or array of field paths) * - flashErrors (boolean, default false; whether to push validation errors to req.flash) * - ignoreNoedit (boolean, default false; whether to ignore noedit settings on fields) * - validationErrors (object; validation errors from previous form handling that should be included) * * @param {Object} data * @param {Object} options (can be comma-delimited list of fields) (optional) * @param {Function} callback (optional) * @api public */ UpdateHandler.prototype.process = function(data, options, callback) { var usingDefaultFields = false; if ('function' == typeof options) { callback = options; options = null; } // Initialise options if (!options) { options = {}; } else if ('string' == typeof options) { options = { fields: options }; } if (!options.fields) { options.fields = _.keys(this.list.fields); usingDefaultFields = true; } else if ('string' == typeof options.fields) { options.fields = options.fields.split(',').map(function(i) { return i.trim(); }); } options.required = options.required || {}; options.errorMessage = options.errorMessage || this.options.errorMessage; options.invalidMessages = options.invalidMessages || {}; options.requiredMessages = options.requiredMessages || {}; // Parse a string of required fields into field paths if ('string' == typeof options.required) { var requiredFields = options.required.split(',').map(function(i) { return i.trim(); }); options.required = {}; requiredFields.forEach(function(path) { options.required[path] = true; }); } // Make sure fields with the required option set are included in the required paths options.fields.forEach(function(path) { var field = (path instanceof landmark.Field) ? path : this.list.field(path); if (field && field.required) { options.required[path] = true; } }, this); // TODO: The whole progress queue management code could be a lot neater... var actionQueue = [], addValidationError = this.addValidationError.bind(this), validationErrors = this.validationErrors; var progress = (function(err) { if (err) { if (options.logErrors) { console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':'); console.log(err); } callback(err, this); } else if (_.size(validationErrors)) { if (options.flashErrors) { this.req.flash('error', { type: 'ValidationError', title: options.errorMessage, list: _.pluck(validationErrors, 'message') }); } callback({ message: 'Validation failed', name: 'ValidationError', errors: validationErrors }, this); } else if (actionQueue.length) { // TODO: parallel queue handling for cloudinary uploads? actionQueue.pop()(); } else { saveItem(); } }).bind(this); var saveItem = (function() { this.item.save((function(err) { if (err) { if (err.name == 'ValidationError') { // don't log simple validation errors if (options.flashErrors) { this.req.flash('error', { type: 'ValidationError', title: options.errorMessage, list: _.pluck(err.errors, 'message') }); } } else { if (options.logErrors) { console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':'); console.log(err); } if (options.flashErrors) { this.req.flash('error', 'There was an error saving your changes: ' + err.message + ' (' + err.name + (err.type ? ': ' + err.type : '') + ')'); } } } return callback(err, this); }).bind(this)); }).bind(this); options.fields.forEach(function(path) { // console.log('Processing field ' + path); var message; var field = (path instanceof landmark.Field) ? path : this.list.field(path), invalidated = false; if (!field) { throw new Error('UpdateHandler.process called with invalid field path: ' + path); } // skip uneditable fields if (usingDefaultFields && field.noedit && !options.ignoreNoedit) { // console.log('Skipping field ' + path + ' (noedit: true)'); return; } // Some field types have custom behaviours for queueing or validation switch (field.type) { case 'localfile': case 'localfiles': case 'cloudinaryimage': case 'cloudinaryimages': case 'azurefile': case 's3file': actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, (function(err) { if (err && options.flashErrors) { this.req.flash('error', field.label + ' upload failed - ' + err.message); } progress(err); }).bind(this))); break; case 'location': actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, (function(err) { if (err && options.flashErrors) { this.req.flash('error', field.label + ' improve failed - ' + (err.status_text || err.status)); } progress(err); }).bind(this))); break; case 'password': // passwords should only be set if a value is provided. // if no value is provided, as long as the field isn't required or empty, bail. if (!data[field.path] && (!options.required[field.path] || this.item.get(field.path))) { return; } // validate the password fields match, with a custom error message. if (data[field.path] != data[field.paths.confirm]) { message = options.invalidMessages[field.path + '_match'] || 'Passwords must match'; addValidationError(field.path, message); invalidated = true; } break; } // validate field input, unless it's already been invalidated by field-specific behaviour if (!invalidated && !field.validateInput(data)) { // console.log('Field ' + field.path + ' is invalid'); message = options.invalidMessages[field.path] || field.options.invalidMessage || 'Please enter a valid ' + field.typeDescription + ' in the ' + field.label + ' field'; addValidationError(field.path, message); invalidated = true; } // validate required fields, unless they've already been invalidated by field-specific behaviour // console.log('Field ' + path + ' required: ' + options.required[field.path]); if (!invalidated && options.required[field.path] && !field.validateInput(data, true, this.item)) { // console.log('Field ' + field.path + ' is required, but not provided.'); message = options.requiredMessages[field.path] || field.options.requiredMessage || field.label + ' is required'; addValidationError(field.path, message); invalidated = true; } // check for a custom validation rule at the path, and run it (unless the field is already invalid) if (!invalidated && this.validationMethods[field.path]) { message = this.validationMethods[field.path](data); if (message) { addValidationError(field.path, message); } } // console.log('Setting field value: ' + path); field.updateItem(this.item, data); }, this); progress(); }; /*! * Export class */ exports = module.exports = UpdateHandler;