Skip to content
This repository has been archived by the owner on Mar 8, 2018. It is now read-only.

Admin CRUD

Reza Akhavan edited this page Feb 17, 2014 · 1 revision

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

/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

/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);
};

Listing Documents

/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)) } });
    }
  });
};
...

Create

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');
};

Read

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)) } });
    }
  });
};

Update

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');
};

Delete

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');
};

Use the Force

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.