Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: async creation of items #2146

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions admin/server/api/item/update.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
module.exports = function(req, res) {
var data = req.body;
var files = req.files;

req.list.model.findById(req.params.id, function(err, item) {
if (err) return res.status(500).json({ err: 'database error', detail: err });
if (!item) return res.status(404).json({ err: 'not found', id: req.params.id });
req.list.updateItem(item, {
data: req.body,
files: req.files
}, function(err) {
if (err) return res.status(500).json({ err: 'database error', detail: err });
res.json(req.list.getData(item));

req.list.processFormData(item, {
data: data,
files: files,
}, function(errors, item) {
if (errors) {
res.status(500).json({
err: 'validation error',
detail: {
errors: errors,
},
});
} else {
res.json(req.list.getData(item));
}
});
});
};
24 changes: 18 additions & 6 deletions admin/server/api/list/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ module.exports = function(req, res) {
console.log(`Refusing to create item; List.nocreate is true`);
return res.apiError(400, 'nocreate');
}

var data = req.body;
var files = req.files;
var item = new req.list.model();
req.list.updateItem(item, {
data: req.body,
files: req.files,
}, function (err) {
if (err) return res.status(500).json({ err: 'database error', detail: err });
res.json(req.list.getData(item));

req.list.processFormData(item, {
data: data,
files: files,
}, function(errors, item) {
if (errors) {
res.status(500).json({
err: 'validation error',
detail: {
errors: errors,
},
});
} else {
res.json(req.list.getData(item));
}
});
};
44 changes: 43 additions & 1 deletion fields/types/Type.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,21 @@ Field.prototype.isModified = function(item) {
* Validates that a value for this field has been provided in a data object
* Overridden by some fieldType Classes
*
* Note: passing the callback into process.nextTick() directly causes an
* error. See: http://stackoverflow.com/questions/33855141/mocha-returns-callback-is-not-a-function-for-async-calls
*
* @api public
*/
Field.prototype.validateInput = function(data, required, item, callback) {
process.nextTick(callback(null, this.inputIsValid()));
process.nextTick(function() {
if (this.inputIsValid(data, required, item)) {
// Valid input, call callback with no params
callback();
} else {
// Invalid input, call callback with an error object
callback(this.getValidationError());
}
}.bind(this));
};

/**
Expand All @@ -304,6 +315,21 @@ Field.prototype.inputIsValid = function(data, required, item) {
}
};

/**
* Returns a validation error object
* @param {String} msg The error message to display on the client
* @param {String} type The type of error message
* @return {Object} A Keystone error object
*/
Field.prototype.getValidationError = function (msg, type) {
return {
name: 'ValidatorError',
path: this.path,
message: msg || 'Error: Invalid input for ' + this.path,
type: type || 'required'
};
};

/**
* Updates the value for this field in the item from a data object
* Overridden by some fieldType Classes
Expand All @@ -327,3 +353,19 @@ Field.prototype.updateItem = function(item, data, callback) {
Field.prototype.getValueFromData = function(data) {
return this.path in data ? data[this.path] : this._path.get(data);
};

/**
* Processes formData before validating and updating an item.
* This method is overridden on fieldType classes that need
* to do things with file uploads. Pass processed form data
* to update and validation methods by modifying the data
* object by reference.
*
* @params item A list item object
* @params formData { data, files }
*
* @api public
*/
Field.prototype.processFormData = function(item, formData, callback) {
process.nextTick(callback);
};
39 changes: 31 additions & 8 deletions fields/types/cloudinaryimage/CloudinaryImageType.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ function cloudinaryimage(list, path, options) {
// TODO: implement filtering, usage disabled for now
options.nofilter = true;

// TODO: implement initial form, usage disabled for now
if (options.initial) {
throw new Error(
'Invalid Configuration\n\n' +
'CloudinaryImage fields (' + list.key + '.' + path + ') do not currently support being used as initial fields.\n'
);
}

cloudinaryimage.super_.call(this, list, path, options);

// validate cloudinary config
Expand Down Expand Up @@ -313,6 +305,35 @@ cloudinaryimage.prototype.updateItem = function(item, data, callback) {
process.nextTick(callback);
};

/**
* Processes the form data for file uploads
*/
// TODO: Implement reset, delete, select, folders, tags,
// prefix, autoCleanup, image type validation
cloudinaryimage.prototype.processFormData = function(item, formData, callback) {
var files = formData.files;
var data = formData.data;

var fileData = files[this.paths.upload];

// No files to upload, continue to validation and updating
if (!fileData) return callback();

var filePath = fileData.path;
this.apply(item, 'upload', filePath)
.then(function(response) {
if (response.error) {
// TODO: Better error handling
var validationErrors = {};
validationErrors[this.path] = this.getValidationError();
callback(validationErrors);
} else {
data[this.path] = response;
callback();
}
}.bind(this));
};

/**
* Returns a callback that handles a standard form submission for the field
*
Expand Down Expand Up @@ -347,6 +368,7 @@ cloudinaryimage.prototype.getRequestHandler = function(item, req, paths, callbac

if (req.body && req.body[paths.select]) {

// Use existing image from selection
cloudinary.api.resource(req.body[paths.select], function(result) {
if (result.error) {
callback(result.error);
Expand All @@ -358,6 +380,7 @@ cloudinaryimage.prototype.getRequestHandler = function(item, req, paths, callbac

} else if (req.files && req.files[paths.upload] && req.files[paths.upload].size) {

// Upload new image
var tp = keystone.get('cloudinary prefix') || '';
var imageDelete;

Expand Down
2 changes: 2 additions & 0 deletions lib/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,13 @@ List.prototype.isReserved = require('./list/isReserved');
List.prototype.map = require('./list/map');
List.prototype.paginate = require('./list/paginate');
List.prototype.processFilters = require('./list/processFilters');
List.prototype.processFormData = require('./list/processFormData');
List.prototype.register = require('./list/register');
List.prototype.relationship = require('./list/relationship');
List.prototype.selectColumns = require('./list/selectColumns');
List.prototype.updateItem = require('./list/updateItem');
List.prototype.underscoreMethod = require('./list/underscoreMethod');
List.prototype.validateInput = require('./list/validateInput');

/*!
* Export class
Expand Down
40 changes: 40 additions & 0 deletions lib/list/processFormData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var async = require('async');

function processFormData (item, formData, callback) {
var data = formData.data || {};
var files = formData.files || {};

async.forEach(this.fieldsArray, function(field, callback) {
field.processFormData(item, {
data: data,
files: files,
}, callback);
}, function(error) {
// Error processing formData
if (error) return callback(error, null);

async.series([
// Validata data
function(callback) {
this.validateInput(item, data,
function(error) {
callback(error);
});
}.bind(this),
// Update data
function(callback) {
this.updateItem(item, data,
function(error) {
callback(error);
});
}.bind(this),
], function(error) {
// If item was updated successfully,
// error will be null and the item will have
// been modified by reference.
callback(error, item);
});
}.bind(this));
};

module.exports = processFormData;
5 changes: 2 additions & 3 deletions lib/list/updateItem.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
var async = require('async');

function updateItem (item, options, callback) {
var data = options.data || {};
function updateItem (item, data, callback) {
async.forEach(this.fieldsArray, function(field, callback) {
field.updateItem(item, options.data, callback);
field.updateItem(item, data, callback);
}, function() {
item.save(callback);
});
Expand Down
33 changes: 33 additions & 0 deletions lib/list/validateInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
var async = require('async');

/**
* Validates the input for all fields in an item
* @param {List Item} item an instance of a List object
* @param {Object} data the data object to validate
* @param {Function} callback invoked with no params when valid data
* or an object of validation errors
*/
function validateInput (item, data, callback) {
var validationErrors = {};
async.forEach(this.fields, function(field, callback) {
// Validate the input for each field
field.validateInput(data, field.required, item,
function(error) {
if (error) {
validationErrors[field.path] = error;
}
callback();
});
}, function () {
if (Object.keys(validationErrors).length > 0) {
// There were validation errors
callback(validationErrors);
} else {
// If the input is valid, validationErrors will be a
// blank object {}
callback();
}
});
}

module.exports = validateInput;