pavarotti

0.0.1 • Public • Published

pavarotti - easily create CRUD controllers

We consistently require controllers that have CRUD functionality (create, read, update and delete).

To avoid institutionalising ourselves, we define generic versions of these methods here which we can then use to e.g. build a controller that manages users. Such a controller would be created with usersController = require('pavarotti').methodsFor(User), where User is the user model.

Methods

The methods we would like to build abstract versions of, are:

  • set

    Create an item with the given params, or update the item with given id with the given params. The newly saved item is returned.

  • get

    Get a single item by its id.

  • find

    Get all items that match the criteria given, possibly paging and sorting the result.

  • remove

    Delete a single item by its id.

Config

We can customise these methods by passing in configuration options to the main methodsFor method, which constructs the CRUD controller. The options are:

set

  • beforeSet : Model |-> Model

    bfunction called before the model is saved to the database. Is given the model that will be saved. The result is the final object saved to the database. An error stops the method.

    Defaults to bf.identity.

  • afterSet : Model |-> Model

    bfunction called after the model has been saved to the database. Is given the model that was saved. The result will be the output of the set method. An error stops the method.

    Defaults to bf.identity.

get

  • afterGet: : Model |-> Model

    bfunction called after the model has been retrieved from the database. Is given the model that was retrieved. The result will be the output of the get method. An error stops the method.

    Defaults to bf.identity.

find

  • beforeFind: : Params |-> Params

    bfunction called at the beginning of the find method. Is given the params to the find method. The result will be fed into the next stem of the find method. An error stops the method.

    Defaults to bf.identity.

  • buildFindFilter : Params |-> FindFilter

    bfunction that, given the params to the find method, builds the object that is given to mongoose to filter the model collection (the find() filter). If null, builds the find filter using properties that are in the filter object in the params.

    Defaults to null.

  • buildFindSort : Params |-> FindSort

    bfunction that, given the params to the find method, builds the object that is given to mongoose to sort the model collection (the sort() object). If null, builds the sort filter using properties thare are in the sort object in the params such that e.g. { sort: { name: 'asc' } } becomes { name: 1 }.

    Defaults to null.

  • afterFind: `: { items: Model[], total: number, filteredTotal: number } |-> any

    bfunction that, given the final result of the find method (results, number of total items and number of filtered items) returns some object that will be the output of the find method.

    Defaults to bf.identity.

Dependencies

_           = require 'underscore'
bf          = require 'barefoot'

Abstraction

The CRUD controller methods are generated once from a single initialisation call to methodsFor. This method takes the model that the CRUD methods are for and a variety of additional configuration methods that lets the consumer of this CRUD controller customise each of the CRUD methods.

methodsFor = (model, config = {}) ->
 
  config = _.defaults config,
    beforeSet: bf.identity
    afterSet: bf.identity
 
    afterGet: bf.identity
 
    beforeFind: bf.identity
    buildFindFilter: null
    buildFindSort: null
    afterFind: bf.identity
 
  crud = {}

Create and update (set)

We create and update with the single set method. This performs an insert operation if no id is given, and an update operation is an id is given. In any case, the newly saved item is returned.

  crud.set =
 
    bf.chain -> [
      bf.validate
        _id: String
 
      getOrCreate
      config.beforeSet
      save
      config.afterSet
    ]

First, attempt to retrieve the item by its id. If there is none found then create a new item. We use mongoose's set method to apply the given params to the fetched model.

  getOrCreate = (params, done) ->
 
    = bf.errorWrapper done
 
    model.findById params.id(item) ->
 
      item ?= new model()
      item.set params
 
      done nullitem

Now, we have a fully populated offer model. All that's left to do is save it!

  save = (item, done) ->
    item.save done

Read (get)

We can retrieve a single item by its id very simply. This method takes one param, id, and returns the item (or null if none exists).

  crud.get =
 
    bf.chain -> [
      bf.validate
        id: String
 
      bf.select (p) -> p.id
      _.bind model.findByIdmodel
      config.afterGet
    ]

Read (find)

We can search for items using a single super method that takes many different filtering params (findFilterParams) and sorting params (findSortParams) and retrieves a paginated list of items that match. The consumer of the CRUD API helper is able to modify the mongo find() object and sort() object.

  crud.find =
 
    bf.chain -> [
      bf.validate
        _filter: Object
        _sort: Object
        _skip: Number
        _limit: Number
 
      config.beforeFind
      bf.parallel [
        config.buildFindFilter ? buildFilter
        config.buildFindSort ? buildSort
        bf.identity
      ]
      runQuery
      config.afterFind
    ]

The default buildFindFilter just filters on the parameters given in params.filter.

  buildFilter = (params, done) ->
 
    done nullparams.filter

The default buildFindSort convers the params.sort object given from e.g. {prop: 'asc'} to {prop: 1}.

  buildSort = (params, done) ->
 
    sort = null
 
    if params.sort?
      sort = {}
      for pof params.sort
        if o == 'asc'
          sort[p= 1
        else if o == 'desc'
          sort[p= -1
 
    done nullsort

We require very flexible pagination, so running the query is a little tedious, though simple enough.

  runQuery = ([filter, sort, params], done) ->
 
    seq = bf.sequence done
 
    filter ?= {}
    sort ?= {}
 
    total = 0
    filteredTotal = 0
 
    seq.then (next) ->
      model.find().count seq.(count) ->
        total = count
        next()
 
    seq.then (next) ->
      model.find(filter).count seq.(count) ->
        filteredTotal = count
        next()
 
    seq.then (next) ->
      query = model.find(filter)
 
      if params.skip?
        query = query.skip(params.skip)
      if params.limit?
        query = query.limit(params.limit)
 
      query.exec seq.(items) ->
        done null,
          items: items
          total: total
          filteredTotal: filteredTotal

Delete (remove)

Deleting or removing an item is a simple method that retrieves an object by its id and then removes it if it exists. If no item with the given id exists, nothing happens.

  crud.remove =
 
    bf.chain -> [
      bf.validate
        id: String
 
      _.bind model.removemodel
    ]

Finishing up

We must conclude the methodsFor function we began to define.

  return crud

And then export this function.

module.exports = {
  methodsFor
}

Readme

Keywords

none

Package Sidebar

Install

npm i pavarotti

Weekly Downloads

0

Version

0.0.1

License

MIT

Last publish

Collaborators

  • noisysocks