-
Notifications
You must be signed in to change notification settings - Fork 569
Admin CRUD
The goal of this page is to demonstrate how we've gone about creating CRUD screens in Drywall. We'll specifically focus on the admin area since that's where you typically have all create, read, update and delete actions in one place.
When I create a new section in my projects I usually start with something simple, like the category section of the admin, copy it, and customize as needed. (a Yeoman generator would be great to speed this up)
With that said let's take apart the admin CRUD screens for categories. For the sake of brevity, we'll only embed code snippets for the server-side logic, while linking to the markup and client-side JavaScript files.
/routes.js
is where we define all the app routes. Here are the routes for /admin/categories/
:
...
//admin > categories
app.get('/admin/categories/', require('./views/admin/categories/index').find);
app.post('/admin/categories/', require('./views/admin/categories/index').create);
app.get('/admin/categories/:id/', require('./views/admin/categories/index').read);
app.put('/admin/categories/:id/', require('./views/admin/categories/index').update);
app.delete('/admin/categories/:id/', require('./views/admin/categories/index').delete);
...
/schema/Category.js
defines the structure for our Category documents. Learn more about schemas from the Mongoose docs.
exports = module.exports = function(app, mongoose) {
var categorySchema = new mongoose.Schema({
_id: { type: String },
pivot: { type: String, default: '' },
name: { type: String, default: '' }
});
categorySchema.plugin(require('./plugins/pagedFind'));
categorySchema.index({ pivot: 1 });
categorySchema.index({ name: 1 });
categorySchema.set('autoIndex', (app.get('env') === 'development'));
app.db.model('Category', categorySchema);
};
/views/admin/categories/index.js
contains all of our CRUD methods. The first one we'll be looking at is the find
method, which lists our documents.
...
exports.find = function(req, res, next){
//define query filter conditions
req.query.pivot = req.query.pivot ? req.query.pivot : '';
req.query.name = req.query.name ? req.query.name : '';
req.query.limit = req.query.limit ? parseInt(req.query.limit, null) : 20;
req.query.page = req.query.page ? parseInt(req.query.page, null) : 1;
req.query.sort = req.query.sort ? req.query.sort : '_id';
var filters = {};
if (req.query.pivot) {
filters.pivot = new RegExp('^.*?'+ req.query.pivot +'.*$', 'i');
}
if (req.query.name) {
filters.name = new RegExp('^.*?'+ req.query.name +'.*$', 'i');
}
//query the documents
req.app.db.models.Category.pagedFind({
filters: filters,
keys: 'pivot name',
limit: req.query.limit,
page: req.query.page,
sort: req.query.sort
}, function(err, results) {
if (err) {
return next(err);
}
//if this is an xhr request, just send JSON data
if (req.xhr) {
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
results.filters = req.query;
res.send(results);
}
//otherwise, render the page markup and embed bootstrap data
else {
results.filters = req.query;
res.render('admin/categories/index', { data: { results: escape(JSON.stringify(results)) } });
}
});
};
...
- View Markup:
/views/admin/categories/index.jade
- Client-side Javascript:
/public/views/admin/categories/index.js
The create
method is up next. If you're wondering about the workflow
stuff, see the The Workflow EventEmitter wiki page for details.
exports.create = function(req, res, next){
var workflow = req.app.utility.workflow(req, res);
//initial user data validation
workflow.on('validate', function() {
if (!req.user.roles.admin.isMemberOf('root')) {
workflow.outcome.errors.push('You may not create categories.');
return workflow.emit('response');
}
if (!req.body.pivot) {
workflow.outcome.errors.push('A name is required.');
return workflow.emit('response');
}
if (!req.body.name) {
workflow.outcome.errors.push('A name is required.');
return workflow.emit('response');
}
workflow.emit('duplicateCategoryCheck');
});
//duplicate record check
workflow.on('duplicateCategoryCheck', function() {
req.app.db.models.Category.findById(req.app.utility.slugify(req.body.pivot +' '+ req.body.name)).exec(function(err, category) {
if (err) {
return workflow.emit('exception', err);
}
if (category) {
workflow.outcome.errors.push('That category+pivot is already taken.');
return workflow.emit('response');
}
workflow.emit('createCategory');
});
});
//actually create the document
workflow.on('createCategory', function() {
var fieldsToSet = {
_id: req.app.utility.slugify(req.body.pivot +' '+ req.body.name),
pivot: req.body.pivot,
name: req.body.name
};
req.app.db.models.Category.create(fieldsToSet, function(err, category) {
if (err) {
return workflow.emit('exception', err);
}
workflow.outcome.record = category;
return workflow.emit('response');
});
});
//start the workflow
workflow.emit('validate');
};
- View Markup:
/views/admin/categories/index.jade
- Client-side Javascript:
/public/views/admin/categories/index.js
The read
method is up next and probably the shortest of all.
exports.read = function(req, res, next){
//lookup the document
req.app.db.models.Category.findById(req.params.id).exec(function(err, category) {
if (err) {
return next(err);
}
//if this is an xhr request, just send JSON data
if (req.xhr) {
res.send(category);
}
//otherwise, render the page markup and embed bootstrap data
else {
res.render('admin/categories/details', { data: { record: escape(JSON.stringify(category)) } });
}
});
};
- View Markup:
/views/admin/categories/details.jade
- Client-side Javascript:
/public/views/admin/categories/details.js
The update
method is similar to create
.
exports.update = function(req, res, next){
var workflow = req.app.utility.workflow(req, res);
//initial user data validation
workflow.on('validate', function() {
if (!req.user.roles.admin.isMemberOf('root')) {
workflow.outcome.errors.push('You may not update categories.');
return workflow.emit('response');
}
if (!req.body.pivot) {
workflow.outcome.errfor.pivot = 'pivot';
return workflow.emit('response');
}
if (!req.body.name) {
workflow.outcome.errfor.name = 'required';
return workflow.emit('response');
}
workflow.emit('patchCategory');
});
//actually update the document
workflow.on('patchCategory', function() {
var fieldsToSet = {
pivot: req.body.pivot,
name: req.body.name
};
req.app.db.models.Category.findByIdAndUpdate(req.params.id, fieldsToSet, function(err, category) {
if (err) {
return workflow.emit('exception', err);
}
workflow.outcome.category = category;
return workflow.emit('response');
});
});
//start the workflow
workflow.emit('validate');
};
- View Markup:
/views/admin/categories/details.jade
- Client-side Javascript:
/public/views/admin/categories/details.js
The delete
does exactly what you think it should. See Admin & Admin Group Permissions for details on how permissions work.
exports.delete = function(req, res, next){
var workflow = req.app.utility.workflow(req, res);
//validate user permissions
workflow.on('validate', function() {
if (!req.user.roles.admin.isMemberOf('root')) {
workflow.outcome.errors.push('You may not delete categories.');
return workflow.emit('response');
}
workflow.emit('deleteCategory');
});
//actually delete the document
workflow.on('deleteCategory', function(err) {
req.app.db.models.Category.findByIdAndRemove(req.params.id, function(err, category) {
if (err) {
return workflow.emit('exception', err);
}
workflow.emit('response');
});
});
//start the workflow
workflow.emit('validate');
};
- View Markup:
/views/admin/categories/details.jade
- Client-side Javascript:
/public/views/admin/categories/details.js
I hope this was helpful. If you have questions or think this page should be expanded please contribute by opening an issue or updating this page.