pavarotti v0.0.1
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
idwith 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 |-> Modelbfunction 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 |-> Modelbfunction 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
setmethod. An error stops the method.Defaults to
bf.identity.
get
afterGet:
: Model |-> Modelbfunction 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
getmethod. An error stops the method.Defaults to
bf.identity.
find
beforeFind:
: Params |-> Paramsbfunction called at the beginning of the
findmethod. Is given the params to thefindmethod. The result will be fed into the next stem of thefindmethod. An error stops the method.Defaults to
bf.identity.buildFindFilter
: Params |-> FindFilterbfunction that, given the params to the
findmethod, builds the object that is given to mongoose to filter the model collection (thefind()filter). If null, builds the find filter using properties that are in thefilterobject in the params.Defaults to
null.buildFindSort
: Params |-> FindSortbfunction that, given the params to the
findmethod, builds the object that is given to mongoose to sort the model collection (thesort()object). If null, builds the sort filter using properties thare are in thesortobject 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
findmethod (results, number of total items and number of filtered items) returns some object that will be the output of thefindmethod.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) ->
w = bf.errorWrapper done
model.findById params.id, w (item) ->
item ?= new model()
item.set params
done null, itemNow, we have a fully populated offer model. All that's left to do is save it!
save = (item, done) ->
item.save doneRead (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.findById, model
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 null, params.filterThe 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 p, o of params.sort
if o == 'asc'
sort[p] = 1
else if o == 'desc'
sort[p] = -1
done null, sortWe 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.w (count) ->
total = count
next()
seq.then (next) ->
model.find(filter).count seq.w (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.w (items) ->
done null,
items: items
total: total
filteredTotal: filteredTotalDelete (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.remove, model
]Finishing up
We must conclude the methodsFor function we began to define.
return crudAnd then export this function.
module.exports = {
methodsFor
}12 years ago