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
JavaScript
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;