2.2.1 • Published 8 years ago

booster v2.2.1

Weekly downloads
260
License
mit
Repository
github
Last release
8 years ago

#booster

##Overview Booster is the fastest way to get a full-fledged REST service up and running in nodejs!

var booster = require('booster'), express = require('express'), app = express(), db = require('./myDbSetup');

booster.init({app:app,db:db});
booster.resource('post');

// or
booster.init({app:app,db:db}).resource('post');


app.listen(3000);

Done! You now have a REST service listening on the five standard REST paths for 'post' and connecting to your database.

Want customized controllers? Disable some paths? Nest resources? Model validations? Uniqueness? Read on!

Breaking Changes

Version 2.0.0 and higher works only with expressjs version 4.x. Booster versions <2.0.0 will work only with express version <4.0.0 and booster versions >=2.0.0 will work only with express version >=4.0.0.

Version 2.0.0 and higher also does not pass the status code to the post-processor. You can retrieve it yourself via res.statusCode. See the documentation on post-processors.

Features

Here are just some of the key features:

  • Standard REST verbs/actions and paths: GET (index), GET (show), POST (create), PUT (update), PATCH (patch), DELETE (destroy)
  • Restrict verbs by resource
  • Multiple paths to same resource
  • Root path to resource
  • Nested resources
  • Override controller behaviour by action
  • Custom names for a resource by path
  • GET (index) can send list or object
  • Custom base paths (like /api)
  • Paths to resource properties
  • Define relations between resources - 1:1, 1:N, N:1, N:N
  • Automatic validation of field values for relations
  • Automatic retrieval of related resources
  • Pre-processing filters across all actions or by action - at the controller level and at the model level
  • Post-processing filters across all actions or by action - at the controller level and at the model level
  • Automatic list/update of related resources
  • Built-in validations
  • Custom validation functions
  • Validation based on values in other resources (e.g. child cannot be set to "valid" if "parent" is not "valid")
  • Default field values
  • Default search filters
  • Check for unique items and conflicts, and return or suppress errors
  • Restrict mutability by field
  • Cascading field changes to children
  • Cascading delete: automatically cascade deletes to children, prevent if children exist, or prevent unless force
  • Differing visibility by field: public, private, secret and custom
  • Custom presave processors
  • Custom model functions

##Installation

npm install booster

Doesn't get easier than that!

##Usage

What does it provide?

booster provides the following basic features:

  • REST routing - similar to express-resource
  • Easy controllers - with defaults for all patterns
  • Standardized models - with validation, optional schemas, and database agnostic interfaces

What does it look like?

var express = require('express'), app = express(), booster = require('booster');

// set up my database
db = dbSetUp();
booster.init({app:app,db:db,controllers:'./controllers',models:'./models'});
booster.resource('post');
booster.resource('comment',{parent:'post'},{only:"index"});
booster.resource('note',{parent:'comment'});
booster.resource('test',{base:'/api'});

app.listen(3000);

You now have a RESTful app that listens on the following verb/path/action

GET       /post                           post#index()
GET       /post/:post                     post#show()
POST      /post                           post#create()
PUT       /post/:post                     post#update()
PATCH     /post/:post                     post#patch()
DELETE    /post/:post                     post#destroy()
	GET       /post/:post/comment             comment#index()
	GET       /post/:post/comment/:comment    comment#show()
	POST      /post/:post/comment             comment#create()
	PUT       /post/:post/comment/:comment    comment#update()
	PATCH     /post/:post/comment/:comment    comment#patch()
	DELETE    /post/:post/comment/:comment    comment#destroy()
	GET       /comment             						comment#index()
	GET       /post/:post/comment/:comment/note					note#index()
	GET       /post/:post/comment/:comment/note/:note		note#show()
	POST      /post/:post/comment/:comment/note/:note		note#create()
	PUT       /post/:post/comment/:comment/note/:note   note#update()
	PATCH     /post/:post/comment/:comment/note/:note   note#patch()
	DELETE    /post/:post/comment/:comment/note/:note   note#destroy()
GET       /api/test                       test#index()
GET       /api/test/:test                 test#show()
POST      /api/test                       test#create()
PUT       /api/test/:test                 test#update()
PATCH     /api/test/:test                 test#patch()
DELETE    /api/test/:test                 test#destroy()

and where exactly are those post#index() and comment#show() functions? You can create them or use the defaults:

If you create them, they are in <controllerPath>/post.js and <controllerPath>/comment.js. So post#index() is just the method index() on the module.exports of <controllerPath>/post.js.

And if you don't create them? Well, then booster has built-in defaults!

For example, calling GET /post with no <controllerPath>/post.js defined, or without the index() function defined, will simply load the list of posts from the database and send them all.

Getting started

var booster = require('booster');

That shouldn't surprise anyone!

So what do I do with booster once I have required it?

booster.init(config); // returns booster, so you can chain

And what exactly goes in that config, anyways?

  • controllers: path to controller files directory, relative to app root, e.g. ./ctrlr (trailing slash is optional). Default is './routes', but controllers are totally optional.
  • models: path to model files director, relative to app root, e.g. ./mymodels (trailing slash is optional). Default is './models', but models are totally optional.
  • param: parameters you want to make available to controller functions. Optional. Default is {}.
  • db: a database object. Required.
  • app: your express app object (you really should have one of these!). Required.
  • embed: whether or not to enable global embedding via HTTP API. See below.

REST Resources

The basic step is defining a REST resource, like so:

booster.resource(name,opts[,opts,opts...]); // returns booster, so you can chain

For example:

booster.resource('comment');
	

This sets up a resource named 'comment', expecting a controller (or using the default) for comment, and a model (or using the default) for comment. As shown above, it creates six paths by default:

GET       /comment             comment#index()
GET       /comment/:comment    comment#show()
POST      /comment             comment#create()
PUT       /comment/:comment    comment#update()
PATCH     /comment/:comment    comment#patch()
DELETE    /comment/:comment    comment#destroy()

If you prefer to stick with the commonly-accepted standard for plural route names, just indicate it:

booster.resource('comment',{pluralize:true});
	

This sets up a resource named 'comment', expecting a controller (or using the default) for comment, and a model (or using the default) for comment. As shown above, it creates six paths by default:

GET       /comments             comment#index()
GET       /comments/:comment    comment#show()
POST      /comments             comment#create()
PUT       /comments/:comment    comment#update()
PATCH     /comments/:comment    comment#patch()
DELETE    /comments/:comment    comment#destroy()

Note that the controller name, model name and parameter are unchanged as single, and you instantiate the resource with a single element; you just tell it that the base of the route should be plural.

Name of the rose. I mean resource.

The first (and only required) argument to booster.resource() is the name of the resource. It should be all string, valid alphanumeric, and will ignore all leading and trailing whitespace. It will also ignore anything before the last slash, so:

abc     -> abc
	/abc    -> abc
	a/b/c   -> c
	ca/     -> INVALID

The name of the resource is expected to be single, since the resource is a car or post or comment, and not cars, posts or comments. If you want the path to be plural, as /comments/:comment and not /comment/:comment, then use the pluralize option; see below.

Format extension

Many apps, rather than having the path /comment/:comment prefer the format /commen/:comment.:format?. This means that both /comment/1 and /comment/1.json would be acceptable.

booster supports the format extension out of the box. If you need access to the parameter in your controller, it is in req.params.format. Of course, it is optional!

Search Params

When you do a GET to a resource collection - e.g. GET /comment as opposed to GET /comment/1 - you can pass search parameters to the query.

All query parameters passed to the request will be passed to model.find(), and by extension db.find(), except for ones specific to the controller. Controller parameters always start with $b., e.g. '$b.csview'.

Responses

Each type of http verb gives the appropriate response, with some options to change globally or per-request.

VerbSuccess HTTP CodeSuccess BodyFailure CodeFailure Body
GET index200Array of objects400,404Error Message or blank
GET show200Object or array of objects400,404Error Message or blank
POST201ID of created object400,404Error message or blank
PUT200ID of updated object OR updated object400,404Error message or blank
PATCH200ID of updated object OR updated object)400,404Error message or blank
DELETE204400,404Error message or blank

More details are available in responses.md.

GET after PUT/PATCH

PUT and PATCH return the ID of the updated object in the body of the response. Sometimes, though, you prefer to have the request return the updated object in its entirety in the body.

There has been extensive debate among the REST community which is the correct response. booster is smart enough not to take sides in this debate and support both options. By default, successful PUT/PATCH return the ID of the updated object as the body of the response.

If you want a successful PUT/PATCH to return the body - as if you did the successful PUT/PATCH and then followed it up with a GET - you have 3 options:

  • global: by making it the default server-side in your booster initialization settings.
booster.init({sendObject:true});
  • param: as part of the request in the URL, add sendObject=true to the request.
PUT http://server.com/api/user/1?sendObject=true
  • header: as part of the request in the headers, add a header X-Booster-SendObject: true to the request.
X-Booster-SendObject: true

PUT http://server.com/api/user/1

As a rule of thumb:

  1. URL param takes precedence over...
  2. HTTP header, which takes precedence over...
  3. booster initialization setting, which takes precedence over...
  4. booster default

The following table lays out the results of a PUT/PATCH:

booster inithttp headerparamsend object?
NO
sendObject=trueYES
X-Booster-SendObject: trueYES
X-Booster-SendObject: truesendObject=trueYES
X-Booster-SendObject: falsesendObject=trueYES
X-Booster-SendObject: truesendObject=falseNO
X-Booster-SendObject: truesendObject=falseNO
{sendObject:true}YES
{sendObject:true}X-Booster-SendObject: falseNO
{sendObject:true}sendObject=falseNO
{sendObject:true}X-Booster-SendObject: falsesendObject=trueYES
{sendObject:true}X-Booster-SendObject: truesendObject=falseNO
{sendObject:false}NO
{sendObject:false}sendObject=trueYES
{sendObject:false}X-Booster-SendObject: trueYES
{sendObject:false}X-Booster-SendObject: truesendObject=trueYES
{sendObject:false}X-Booster-SendObject: falsesendObject=trueYES
{sendObject:false}X-Booster-SendObject: truesendObject=falseNO
{sendObject:false}X-Booster-SendObject: truesendObject=falseNO

NOTE: booster init setting {sendObject:false} is the same as not setting an init param at all.

Optional options

opts is just a plain old JavaScript object with options. What goes into those options? That depends what you want to do with this resource and what path it should have.

You can have the resource accessible from multiple paths and behave differently in each of those paths, by having multiple opts. First, let's see what you can do with each opts, then we'll string multiple together.

Pluralize

If you prefer to stick with the commonly-accepted standard for plural route names, use the pluralize option:

booster.resource('comment',{pluralize:true});
	

This sets up a resource named 'comment', expecting a controller (or using the default) for comment, and a model (or using the default) for comment. As shown above, it creates six paths by default:

GET       /comments             comment#index()
GET       /comments/:comment    comment#show()
POST      /comments             comment#create()
PUT       /comments/:comment    comment#update()
PATCH     /comments/:comment    comment#patch()
DELETE    /comments/:comment    comment#destroy()

Note that the controller name, model name and parameter are unchanged as single, and you instantiate the resource with a single element; you just tell it that the base of the route should be plural.

Base path

If you prefer to have a base path to your resource, so the path is /api/comment, just do:

booster.resource('comment',{base:'/api'});
	

Which will give you

	GET       /api/comment             comment#index()
GET       /api/comment/:comment    comment#show()
POST      /api/comment             comment#create()
PUT       /api/comment/:comment    comment#update()
PATCH     /api/comment/:comment    comment#patch()
DELETE    /api/comment/:comment    comment#destroy()

If you want all of your routes to have a base path, instead of having to do:

booster.resource('post',{base:'/api'});
booster.resource('comment',{base:'/api'});
booster.resource('user',{base:'/api'});
	

You could simply do:

booster.init({base:'/api'});
booster.resource('post');
booster.resource('comment');
booster.resource('user');

If you do specify a base on a specific resource after already specifying the global base in init(), the resource-specific base will override the global one.

Different path name

Sometimes, you want to name your resource one thing, while the path to it should be something else. This is useful when you have multiple paths to a resource, or if the actual name of the path might conflict with an existing resource.

For example, what if you wanted to have a resource called 'user' under a 'post', but it is different than the actual user resource.

GET       /post             			post#index()
GET       /post/:post		    			post#show()
	GET				/post/:post/user				diffuser#index()
	GET				/post/:post/user/:user	diffuser#show()

To enable it, you use the name option:

booster.resource('diffuser',{parent:'post',name:'user'});

This will create the above paths. Note that the name of the parameter will be :user and not :diffuser. However, it will expect a controller file, if any, to be named diffuser.js, since that is the name of the resource.

Nested Resource

If you want to nest a resource, like in the example above, you just need to pass a parent option:

booster.resource('comment',{parent:'post'});

Which will give you

GET       /post/:post/comment             comment#index()
GET       /post/:post/comment/:comment    comment#show()
POST      /post/:post/comment             comment#create()
PUT       /post/:post/comment/:comment    comment#update()
PATCH     /post/:post/comment/:comment    comment#patch()
DELETE    /post/:post/comment/:comment    comment#destroy()

If you include both parent and base, it will ignore base.

You can nest as many layers deep as you want:

booster.resource('comment',{parent:'post'});
booster.resource('note',{parent:'comment'});
GET       /post/:post/comment             comment#index()
GET       /post/:post/comment/:comment    comment#show()
POST      /post/:post/comment             comment#create()
PUT       /post/:post/comment/:comment    comment#update()
PATCH     /post/:post/comment/:comment    comment#patch()
DELETE    /post/:post/comment/:comment    comment#destroy()
GET       /post/:post/comment/:comment/note					note#index()
GET       /post/:post/comment/:comment/note/:note		note#show()
POST      /post/:post/comment/:comment/note/:note		note#create()
PUT       /post/:post/comment/:comment/note/:note   note#update()
PATCH     /post/:post/comment/:comment/note/:note   note#patch()
DELETE    /post/:post/comment/:comment/note/:note   note#destroy()
Required Parent Param

Sometimes, you want to require that a nested resource has a property that matches the parent.

For example, if I am creating a new nested comment on post 2 as follows:

POST /post/2/comment {author:"john",content:"This is a comment"}

I might want the content to require the name of the parent as a property:

{author:"john",content:"This is a comment",post:"2"}
	

booster can enforce this if you tell it to! When sitting up a nested resource, just set the parentProperty to true as follows:

booster.resource('comment',{parent:'post',parentProperty:true});

If parentProperty is set to true, booster will insist that the posted body contains a property with the same name as the parent, and that its value precisely matches the value of the parent parameter. In other words, it will insist that req.params[parent] === req.body[parent]. If not, it will reject it with a 400 error.

This rule is enforced for all POST, PUT and PATCH.

If you have:

booster.resource('post');
booster.resource('comment',{parent:'post',parentProperty:true});

each of the following examples, it will show what works and what doesn't

POST   /post/3/comment    {post:"4"}  // FAIL: "4" !== "3"
POST   /post/3/comment    {post:"3"}  // PASS: "3" === "3"
POST   /post/3/comment    {}          // FAIL: missing "post" property
PUT    /post/3/comment/4  {post:"4"}  // FAIL: "4" !== "3"
PUT    /post/3/comment/4  {post:"3"}  // PASS: "3" === "3"
PUT    /post/3/comment/4  {}          // FAIL: missing "post" property and PUT means replace entirely
PATCH  /post/3/comment/4  {post:"4"}  // FAIL: "4" !== "3"
PATCH  /post/3/comment/4  {post:"3"}  // PASS: "3" === "3"
PATCH  /post/3/comment/4  {}          // PASS: missing "post" property, but PATCH is only an update, not a replace

Note that for POST or PUT, where the body is the entire new or replacement object, the property must exist, else it fails. However, for PATCH, where it only replaces those explicit fields, it the parent property is missing, it passes.

In the case of POST and PUT, if you want the field to default to the value of the parent property - in our above example, {post:"3"} - you can tell booster, "Hey, the field has to match, but if it isn't there at all, fill it in for me." Just tell the resource, in addition to setting parentProperty to true, set parentDefault to true as well:

booster.resource('post');
booster.resource('comment',{parent:'post',parentProperty:true,parentDefault:true});

In that case, all of the above examples where missing post caused the route to fail with a 400 will now pass, and the value will be set:

POST   /post/3/comment    {}          // PASS: will be sent to the model as {post:"3"}
PUT    /post/3/comment/4  {}          // PASS: will be sent to the model as {post:"3"}

Note that if you have a property set to {mutable:false} and the same property is {parentDefault:true}, it might conflict when doing a PUT. POST will never be a problem, since it does not yet exist, and PATCH is not a problem, since it only updates the given fields.

For example, what if the data in the database is:

{id:"1",post:"3",content:"Hello, I am here"}
	

and you initialize as

booster.resource('comment',{parent:'post',parentProperty:true,parentDefault:true});

If you do

PUT /post/3/comment/1  {content:"Now I am not"}
	

booster will look at the update (PUT) to comment with the id "1", see that it requires the parent property ("post") but that it isn't present, so it will add {post:"3"} before updating the model. This is the equivalent of:

PUT /post/3/comment/1  {content:"Now I am not",post:"3"}

This is great, but what if you defined the comment model making post immutable:

module.exports = {
	fields: {
		content:{required:true,mutable:true},
		post: {required:true,mutable:false},
		id: {required:true,mutable:false}
	}
}

After all, you do not want someone accidentally moving a comment from one post to another! But then the update of

PUT /post/3/comment/1  {content:"Now I am not",post:"3"}
	

will cause a 400 error, since it is trying to update the post property, and that one is immutable!

Not to worry; booster inteillgently handles this. If you actually try to update post, it will throw a 400. But if you did not set it, and you have parentDefault set, it will not set it unless the field is mutable.

Resource Property

What if you don't want to create a whole new resource, but have a separate property as part of a resource? For example, if you want to be able to PUT /post/:post/title and so change the title directly?

It already works! Yes, that's right. If you already created the resource booster.resource('post'), then unless you created a nested resource of exactly the same name, GET /post/:post/title will get you the title from /post/:post Similarly, you can PUT /post/:post/title to change it. But, no you cannot POST or PATCH it; they don't make much sense.

And you get all of the validations of models for free!

What if you don't want this to happen? Just set it in the controller:

module.exports = {
	getProperty: null // disable `GET /post/:post/property`
	setProperty: null // disable `PUT /post/:post/property`
}
Custom Properties

OK, so the above works great if you want /post/1/title to map to title of post 1, or /post/10/author to map to author of post 10. But what if you want all of the above and you want to map some special properties to their own handlers. For example, if a user is:

{id:"1",firstname:"john",lastname:"smith"}

so you want the following to work (and hey, booster already said you get it for free):

GET /user/1/firstname -> "john"
	GET /user/1/lastname -> "smith"
	GET /user/1 -> {id:"1",firstname:"john",lastname:"smith"}

But you also want to be able to do:

GET /user/1/groups -> [10,12,2678]
	

In other words, that special property groups is really not a property of a user object, but has its own logic. On the other hand, it behaves mightily like a property: it uses PUT and GET, and has meaning only in the context of a specific user. It isn't a first-class resource (the group and the user are), but that array of group IDs comes from somewhere else!

You know that I'm going to say, "it's easy!", right?

All you need to do is put in place that special controller file, and add properties to it.

module.exports = {
	properties: {
		groups: {
			get: function(req,res,next) {
				// do all of your get logic here
				// LOGIC A
			},
			set: function(req,res,next) {
				// do all of your set logic here
				// LOGIC B
			}
		},
		roles: {
			get: function(req,res,next) {
				// LOGIC C
			}
		},
		strange: {
			set: function(req,res,next) {
				// LOGIC D
			}
		}
	}
};

So when booster hits a property of a resource, like /user/1/someProperty, it says, if all of the following is true, use your function, else treat it just like a regular property:

(controller file exists) AND 
	  (controller has "properties" key) AND 
		  ("properties" key has appropriate property name) AND 
			  (  (property name has "get" key as function if request was GET) OR 
				   (property name has "get" key as function if request was PUT)  )

Going back to the above example, here is what will happen with each type of request and why:

GET /user/1/title -> get the property "title" of the object; properties.title not defined
GET /user/1/groups -> use function for LOGIC A; properties.groups.get defined
PUT /user/1/groups -> use function for LOGIC B; properties.groups.set defined
GET /user/1/roles -> use function for LOGIC C; properties.roles.get defined
PUT /user/1/roles -> get property "roles" of the object; properties.roles.set not defined
GET /user/1/strange -> get property "strange" of the object; properties.strange.get not defined
PUT /user/1/strange -> use function for LOGIC D; properties.strange.set defined
Resource as a Property (RaaP?)

Actually, it is even easier! A really common pattern is where a property of one resource is actually a reference to another resource that has some find restrictions. Take a look at the following:

GET /group         -> get all of the groups
	GET /group/10      -> get group whose ID is 10
GET /user/1/name   -> get the name of user 1, normal property
	GET /user/1/group -> Get all of the groups of which user "1" is a member, like GET /group?{user:1}

Since this is such a common pattern, let's make it easier for you!

booster.resource('group');
booster.resource('user',{resource:{group:["get"]}});

That is exactly the same as the following:

booster.resource('group');
booster.resource('user');

// and inside routes/user.js :
module.exports = {
	properties: {
		group: {
			get: function(req,res,next) {
				// get the groups of the user from the separate groups list and send them off
				req.booster.models.group.find({user:req.params.user},function (err,data) {
					if (err) {
						res.send(400,err);
					} else if (data && _.size(data) > 0) {
						res.send(200,data[0]);
					} else {
						next();
					}
				});
				
			}
		}
	}
}

But come on, is that not so much easier? You want to write 17 lines instead of 2, and in 2 different files? Go ahead. But I think 2 lines is just cooler.

What about the easier set? Can we do that? You know we can!

booster.resource('group');
booster.resource('user',{resource:{group:["set"]}});

This means, "whatever I sent in the body to /user/1/group should be all of the groups that have {user:1} in them, no more, no less."

It is exactly the same as the following:

booster.resource('group').resource('user');

// and inside routes/user.js :
module.exports = {
	properties: {
		group: {
			set: function(req,res,next) {
				var body = req.body ? [].concat(req.body) : [];
				req.booster.models.group.find({user:req.params.user},function (err,data) {
					var toRemove = [], toAdd = [];
					if (err) {
						res.send(400,err);
					} else {
						// add to toRemove all of those that are in data but not in body
						// add to toAdd all of those that are in body but not in data
						
						// now to the removal and creation
						async.series([
							function (cb) {
								async.each(toAdd,function (item,cb) {
									that.create(item,cb);
								},cb);
							},
							function (cb) {
								async.each(toRemove,function (item,cb) {
									that.destroy(item.id,cb);
								},cb);
							}
						],callback);						
					}
				});
			}

		}
	}
}
Plural Resource as a Property

If you made the child resource pluralized, you need to pluralize the resource-as-a-property as well.

So you can do this:

booster.resource('group'); // creates /api/group
booster.resource('user',{resource:{group:["set"]}}); // creates /api/user/123/group

In the above, the group in /api/user/123/group is singular (but still will return a list of group IDs).

You also can do:

booster.resource('group',{pluralize:true}); // creates /api/groups
booster.resource('user',{pluralize:true,resource:{groups:["set"]}}); // creates /api/users/123/groups

... wherein the group in /api/users/123/groups is plural.

But you cannot do:

booster.resource('group',{pluralize:true}); // creates /api/groups
booster.resource('user',{pluralize:true,resource:{group:["set"]}}); // creates /api/users/123/group

... because you pluralized group, so as a child resource / resource as a property, you must pluralize it as well.

Root path

If you want to have the resource called at the root path, you just need to pass a root option:

booster.resource('comment',{root:true})

Which will give you

GET       /             comment#index()
GET       /:comment     comment#show()
POST      /             comment#create()
PUT       /:comment     comment#update()
PATCH     /:comment     comment#patch()
DELETE    /:comment     comment#destroy()

Notice that the resource itself is still comment, but the path does not include the name comment.

Multiple Paths / Options

If you want a resource to exist in multiple locations, each with different (or similar) behaviour, you specify multiple options objects.

Let's say you want "post" to exist in 2 places:

GET /post								post#index()
	GET /post/:post					post#show()
GET /api/post						post#index()
	GET /api/post/:post			post#show()

Each "post" refers to the same resource, but is accessible at different paths. Perhaps you only allow updates at one path but not the other, e.g. if comments have a unique ID, so you can retrieve comments from anywhere, but update only via the "post":

GET /post/:post/comment							comment#index()
GET /post/:post/comment/:comment		comment#show()
PUT /post/:post/comment/:comment		comment#update()
PATCH /post/:post/comment/:comment	comment#patch()
DELETE /post/:post/comment/:comment	comment#destroy()
	GET /comment/:comment								comment#show()

These are the same "comment" resources, but accessible via different paths. All you need to do is have an opts for each one of them when declaring the resource.

booster.resource("post");
booster.resource('comment',{parent:"post"},{only:"show"});

The first options object {parent:"post"} sets up the path for a "comment" as child of a parent as /post/:post/comment. The second {only:"show"} sets up the path for a "comment" at the root, but only exposes "show".

Note: Normally, if you want an unmodified resource at the base path, you don't need to set options at all, just do booster.resource('comment');. However, with multiple options, booster cannot know that there is also a base path 'comment'. Thus, with multiple options, you must specify a blank options {} for base path. For example:

booster.resource("post");
booster.resource('comment',{parent:"post"},{}); // the second one is like booster.resource('comment');

Accessing Models

As you will see below, booster makes the models (and all of their functions) available in several places by passing them into calls, like filters and post-processors.

In addition, you have an even better way to access them: request.

Your express request will be loaded with req.booster, an object which includes req.booster.models. These are available on any handler after booster.init. For example:

app.use(handler1); // will NOT have req.booster
booster.init();
app.use(handler2); // will have req.booster

If you need to have req.booster available even before calling booster.init(), just add the reqLoader middleware:

app.use(booster.reqLoader);
app.use(handler1); // will have req.booster TOO
booster.init();
app.use(handler2); // will have req.booster

And don't worry... you know it is safe to run multiple times (idempotent, for those who prefer fancy words).

Controllers

The default controller provides the standard actions: index, show, create, update, patch, destroy. It interacts with the models of the same name (see models, below), and lists all of them, shows one, creates a new one, updates an existing one, or destroys one.

Customizing and Eliminating Actions

If you want to override one or more of the actions, just create a file with that name in controllerPath directory, either the default path or the one you provided when initializing booster. Each function should match express route signature. If you want to eliminate a route entirely, override it with a null. See the example below.

// <controllerPath>/post.js
module.exports = {
	index: function(req,res,next) {
		// do lots of stuff here
	}
	// because we only override index, all of the rest will just use the default
	update: null
	// because we actively made update null, the update() function and its corresponding PUT /post/:post will be disabled and return 404
};
Shortcuts

You can also take a shortcut to eliminating a route, using the except and only configuration parameter:

booster.resource('post',{except:"index"}); // will create all the routes except GET /post
booster.resource('post',{except:["update","patch"]}); // will create all the routes except PATCH /post/:post and PUT /post/:post
booster.resource('post',{only:"index"}); // will eliminate all the routes except GET /post
booster.resource('post',{only:["update","patch"]}); // will eliminate all the routes except PATCH /post/:post and PUT /post/:post

Note that if you are eliminating just one route, or keeping just one route, you can put it in an array, or just as a string.

If you use both only and except, the except will be ignored. In English, only means, "only this and no others", while except means, "all the others except this". These are mutually conflicting.

What does the default controller do?

Well, it looks like this but with real error-handling. Look at the source code in github if you want the real nitty-gritty.

module.exports = {
	index: function(req,res,next) {
		var data = model.find();
		res.send(200,data);
	},
	show: function(req,res,next) {
		var data = model.get(req.param(resource)); // 'resource' is the name you provided when you called booster.resource(name);
		res.send(200,data);
	},
	create: function(req,res,next) {
		model.create(req.body,function(){
			res.send(201);
		});
	},
	update: function(req,res,next) {
		model.update(req.body.id,req.body,function(){
			res.send(200);
		});
	},
	patch: function(req,res,next) {
		model.patch(req.body.id,req.body,function(){
			res.send(200);
		});
	},
	destroy: function(req,res,next) {
		model.destroy(req.param.id,function(){
			res.send(204);
		});
	}
};

It just calls the basic model functions.

Access models inside the controllers

If you need to access the model classes inside the controllers, no problem. They are always available on a request at req.booster.models. Each named resource has its own object.

booster.resource('user');

index: function(req,res,next) {
	var User = req.booster.models.user;
}

Note: req.booster is available in all express routes, not just those inside booster controllers. For example:

booster.resource('user');

app.get('/foo',function(req,res,next){
	req.booster.models.user(); // this works here!
});

Parameters to controller functions

And what if you want to pass some parameters to controller functions? For example, what if one of your controller functions needs to send emails, and you just happen to have a sendmail object in your app that knows how to do just that?

Well, you could require() it in each controller, but that really is rather messy, requires multiple configurations and calls (not very DRY), and would work much better if you could just inject the dependency.

booster supports that type of dependency injection. If you have configuration parameters you want available to your controllers, they are available, in true express style, on the req object as req.booster.param. You inject them by using the param property when calling booster.init()

booster.init({param:{sendmail:fn}});

index: function(req,res,next) {
	req.booster.param.sendmail();
	// do lots of stuff here
}

Controller Filters

Now, what if you don't want to entirely override the controller function, but perhaps put in a filter. You could easily just do:

module.exports = {
	show: function(req,res,next) {
		// handle everything here
	}
}

But you didn't want to have to recreate all of the model calls, error handling, all of the benefits of the default controller. You really just wanted to inject some middleware prior to the default show() being called!

Booster gives you two really good options for this: all global filter, and filters for individual ones.

Before you start jumping up and saying, "hey, that is business logic, which should go in the models! Fat models and skinny controllers!", you are right. But sometimes your controllers need to do filtering or post-processing that is appropriate at the controller level, hence Filters.

For filters (pre-index/show/update/create/delete) and post-processing (post-index/show/update/create/delete) at the model level, see in the section on models.

Global 'before' option for controllers

If you are writing controller method overrides, and you want a function that will always be executed before the usual index/show/update/create/destroy, just add an all function.

module.exports = {
	all: function(req,res,next) {
		// stuff here will always be done *before* the usual middleware methods
		next();
	}
};
Individual filters

If you want an individual filter to run on only one specific routing, e.g. user.index() or user.show(), you do the following:

module.exports = {
	filter: {
		show: function(res,res,next) {
			// do your filtering here
			// succeeded?
			next();
			// failed?
			res.send(400);
			// or else
			next(error);
		}
	}
};

Of course, you will ask, why the split language? Why is all a first-level property, but each other filter is below the filter property? Well, all can go in either space. Both of the following are valid:

module.exports = {
	// this one will get executed first
	all: function(res,res,next) {
		next();
	},
	filter: {
		// then this one
		all: function(res,res,next) {
			next();
		},
		// and this one last
		show: function(req,res,next) {
		}
	}
};

The order of execution is:

  1. all()
  2. filter.all()
  3. filter.show() (or any other specific one)

Note: Filters will run, independent of any path you use to access the resource. So if you have the following defined for comment:

booster.resource('comment',{},{parent:'post'});
	booster.resource('foo',{resource:{comment:['get']}});
	

Then you have three paths to list comment:

  • /comment
  • /post/:post/comment
  • /foo/:foo/comment

In all three paths, any filters defined for comment will run, whether it is top-level all, filter.all, or path specific, such as filter.index or filter.show:

all: function(req,res,next) {
		// do some filtering
	},
	filter: {
		all: function(req,res,next) {
			// do some other filtering
		},
		index: function(req,res,next) {
			// filter on /comment or /post/:post/comment or /foo/:foo/comment
		}
	}

Controller Global Filters

If you want to have filters that run on all resources, the above method will work - create a controller file for resourceA and another for resourceB and for resourceC, but that is pretty repetitive (and therefore not very DRY).

A better solution is "global filters". Global filters look exactly like per-resource filters, and have the exact same file structure - all, filter.all, filter.show, etc. - but apply to every resource. To enable it, when initializing booster, just tell it where it is:

booster.init({filters:'/path/to/global/filters/file'});
	

It is that simple!

The order of execution is:

  1. global all()
  2. global filter.all()
  3. global filter.show() (or any other specific one)
  4. controller-specific all()
  5. controller-specific filter.all()
  6. controller-specific filter.show() (or any other specific one)

Controller Post-Processors

A common use case is one where you want to do some post-processing before sending the response back to the client, for example, if you create with POST /users but before sending back that successful 201, you want to set up some activation stuff, perhaps using activator. Like with filters, you could override a controller method, like create, but then you lose all of the benefits.

The solution here is post-processors, methods that are called after a successful controller method, but before sending back the 200 or 201. Does booster support post-processors? Of course it does! (why are you not surprised?)

Like filters, post-processors are added as properties of the controller object.

module.exports = {
	// use "post" to indicate post-processors
	post: {
		all: function(req,res,next) {},  // "all" will always be called
		create: function(req,res,next) {}, // "create" will be called after each "create"
	}
};

The order of execution is:

  1. post.all()
  2. post.create() (or any other specific one)

Post-processor signatures look slightly different than most handlers, since they need to know what the result of the normal controller method was.

function(req,res,next,body) {
};

req, res, next are the normal arguments to a route handler. body is the body returned by the controller method, if any. If you want the res status as set until now, you can retrieve it with the standard res.statusCode.

Your post-processor filter has 3 options:

  • Do nothing: if it does not alternate the response at all, just call next() as usual.
  • Error: as usual, you can always call next(error) to invoke expressjs's error handler.
  • Change the response: if it want to alternate the response, call res.send() or anything else.

Sorting and Limiting

What if you want to sort the results of a GET (i.e. index) - ascending or descending - by a specific field like age, or limit the results to only, say, the first 20 results?

Turns out it is pretty easy to do... because you don't need booster to do it.

One way to do it is to GET the entire thing and then sort it client-side, or do it server-side in post-processors, as we just saw.

r.get('/users').end(function(err,res){
	// now we have all of the users over the network
	// all users in descending order
	var users = _.sortBy(res,"age").reverse();
	// now get the first 20
	users = users.slice(0,20);
});

But that is a royal pain, and incredibly inefficient to boot! You had to extract all of them from the database, send them all over the wire to the client (possibly a browser), then sort and reverse them all, then take the first 20. How would you like to do that with 10MM records?

You could save part of it by filtering it in the controller using post-processors:

{
	post: {
		index: function(req,res,next,body) {
			if (res.statusCode === 200 && body && body.length > 0) {
				// all users in descending order
				var users = _.sortBy(body,"age").reverse();
				// now get the first 20
				users = users.slice(0,20);
				// save it to the body
			}
		}
	}
}

But you still need to do a lot of extra work in the post-processor, and you are still retrieving too many records over the nearby network from the data store.

The correct way to do this is to have the database do the sorting and filtering. Most database drivers support some form of sorting by a field and limiting the count. The only question, then, is how we get the correct parameters to the database driver.

The solution is simple: query parameters.

As you have seen earlier, and will see again below, every query parameter except those beginning with $b. are passed to model.find() and hence db.find() as is. In order to sort and filter, all you need to do is pass those parameters to the request query.

Here is an example. Let's say your database driver supports a search parameter sort. If sort is present, it will sort the response by the field, descending if negative:

{sort:"age"} - sort by age ascending
{sort:"-age"} - sort by age descending

Similarly, it might support count:

{count:20} - return the first 20 matching records

In combination, you might have

{sort:"age",count:20} - give me the 20 youngest
{sort:"-age",count:20} - give me the 20 oldest

Since all of the query parameters are passed to the driver, just do:

GET /users?sort=-age&count=20

No booster magic involved!

Aha, you will ask, but does that not tie my REST API query directly to my database driver implementation? Where is the nimbleness I would get from indirection?!

Simple: the database "driver" that you pass to booster need not be the actual MySQL or Mongo or Oracle or whatever driver. It is just an object with a few key functions. Use your own that wraps the standard driver (that is what we do) . Then you can define your own terms and use them. Voilà.

Models

And what about all of our models? What do we do with them?

Models are just standard representations of back-end data from the database. Like controllers, models are completely optional. If you don't provide a model file, the default will be used. If you prefer to design your own, create it in modelPath directory, either the default or the one you provided when initializing booster.

So what is in a model? Actually, a model is an automatically generated JavaScript object handler. It is driven by a config file in which you tell it which you tell booster, for this particular model, how to manage data: name in the database, id field, validations, what should be unique, etc.

  • name: what the name of this model should be in your database. Optional. Defaults to the name of the file, which is the name of the resource you created in booster.resource('name').
  • fields: what fields this model should have, and what validations exist around those fields
  • unique: what unique fields need to exist for this model
  • uniqueerror: whether or not to return an error for a unique conflict
  • id: what the ID field is. We need this so that we can use "unique" comparisons and other services. Optional. Defaults to "id".
  • presave: a function to be executed immediately prior to saving a model via update or create. Optional.
  • extend: an object, with functions that will extend the model. Optional. See below.
  • delete: rules for creating, preventing or enforcing cascading deletes. Optional. See below.
  • filter: filters before performing a model action
  • post: actions to take after the model

An actual model instance is just a plain old javascript object (POJSO). The data returned from the database to a controller should be a POJSO, as should the data sent back.

name

The name is just an identifier for this model, and is optional. It defaults to the name of the file, which is the name of the resource you created.

It is used, however, as the value of the table passed to all the database calls. See below under Persistence.

fields

The fields is a list of all of the required fields and properties that are used for this model. It includes the following:

  • required: boolean, if the field is required. Ignored for PUT updates. Default false
  • createblank: boolean, if this field is optional during creation. ignored if required === false. Default false
  • mutable: boolean, if the field can be changed by a client request. Default true
  • visible: this field is one of: public (visible to all), private (visible only for explicit private viewers), secret (never sent off the server)
  • validation: validations to which to subject this field
  • association: how this field determines a relationship between this resource and another resource
  • type: if this field should be a particular type. If it is, then when doing a find(), it will cast it to the appropriate type, if possible.
  • default: if this field is not specified, a default value.
  • filter: if filtering should be done on this field in the case of index(). It should be an object with the following properties:
    • default: what filter value should be applied to this field, if none is given
    • clear: what value if applied should indicate no filter
  • cascade: if this field is changed, cascade the change down to dependent model items. See details below.

Example:

fields = {
	id: {required: true, createoptional: true, mutable: false, visible: "public"},
	name: {required: true, mutable:false, visible: "public", validation:["notblank","alphanumeric"]},
	email: {required: true, mutable:true, visible: "public", validation:"email"},
	time: {required:true,mutable:true,type:"integer"},
	somefield: {filter:{default:"foo",clear:"*"}}
}

The required boolean is ignored in two cases:

  1. PUT update: This makes sense. You might be updating a single field, why should it reject it just because you didn't update them all? If the name and email fields are required, but you just want to update email, you should be able to PUT the follwing {email:"mynewmail@email.com"} without triggering any "missing field required" validation errors.
  2. POST create and createoptional === true: If you flag a field as required, but also flag it as createoptional, then if you are creating it, validations will ignore the required flag. Well, that's why you set it up as createoptional in the first place, right?
default values

If you specify a default value for a field, then if the field is unspecified, it will be set to this value. Some important points:

  • This only applies to PUT and POST. PATCH leaves the previous value.
  • This will only apply if there is no value given at all. If it is an empty string or 0 or some other "falsy" value, it will not be applied.
  • If the field is required, and there is a default value is specified, then if the PUT or POST value of the field is blank, the default value will be applied and the required condition will be satsified.
filters

The filter option can be a little confusing, so here is a better explanation.

Let's say you have defined a resource play. Normal behaviour would be:

  • GET /play - list all of the play items
  • GET /play?somefield=foo - list all of the play items where somefield equals "foo"

However, you want it the other way around. You want GET /play to return only those items where somefield equals "foo", unless it explicitly gives something else.

  • GET /play - list all of the play items where somefield equals "foo"
  • GET /play?somefield=bar - list all of the play items where somefield equals "bar"

Setting the model as above with filter set on somefield will provide exactly this outcome:

somefield: {filter:{default:"foo"}}

But this creates a different problem. How do I tell the API, "send me all of the play items, whatever the value of somefield is"? Before adding the default filter, you would just do GET /play, but we added a filter to that!

The solution is to set a clear on the filter. Whatever the clear is set to, that is what, if the parameter is set, will match all. So using the setup from above:

somefield: {filter:{default:"foo",clear:"*"}}

We get the following:

  • GET /play - list all of the play items where somefield equals "foo"
  • GET /play?somefield=bar - list all of the play items where somefield equals "bar"
  • GET /play?somefield=* - list all of the play items

Of course, you can set the value of clear to anything you want: "*" (as in the above example), "any", "all", or "funny". Whatever works for you!

cascade

What if you have an item that has dependent items. When you make a change to a field here, you want to cascade similar changes to all of the "child" items? Here is an example.

I have two models, post and comment. They look like this:

	post: {
		id: {required:true,createoptional:true},
		content: {required:true},
		status: {required:true,default:"draft",validation:"list:draft,published"}
	}

And the comment:

	comment: {
		id: {required:true,createoptional:true},
		post: {required:true},
		content: {required:true},
		status: {required:true,default:"draft",validation:"list:draft,published"}
	}

What you want is when you publish a post by changing its status from "draft" to "published", that all dependent comment elements are changed to published as well. Now, you might not want that, in which case, well, do nothing! But if you do...

Of course, you can do that by putting a post processor in the controller. For example, the controller for post might be:

post: {
		patch: function(req,res,next,body) {
			if (req.body && req.body.status === "published") {
				req.booster.models.content.find({post:req.param("post")},function(err,data) {
					if (data && data.length > 0) {
						req.booster.models.content.patch(_.map(data,"id"),{status:"published"},function(err,data){
							next();
						});
					}
				});
			}
		}
	}

That would work, but requires extra work and more error-handling than I have done here. If you want 3-4 levels of cascade - publishing a category leads to publishing child post leads to publishing child comment leads to (you get the idea) - it gets terribly messy and long. It also means that if you make the changes somewhere inside your program - i.e. not through the controller, say, by modifying the model from elsewhere, you won't catch it. What a mess!

Instead, let's go the simple route! booster allows you to say, "if I change this field to a certain value, then all children should get the same change."

We can rewrite the model for post as follows:

post: {
		id: {required:true,createoptional:true},
		content: {required:true},
		status: {required:true,default:"draft",validation:"list:draft,published", 
		    cascade: {value:"published",children:"comment"}
			}
	}

Note what we added: cascade: {value:"published",children:"comment"}.

This means: if we change this field's value to "published", then all instances of comment which have the value of post (since our type is post) equal to our id should have their status (since our field is status) changed to "published" as well.

Here are the parts of cascade you can set:

  • value: setting which value should trigger a cascade. This can be a string or an array of strings. If not set, then all value changes trigger a cascade.
  • children: which children who have a field named the same as our current model and a value the same as this item's id should be cascaded to. This can be a string or an array of strings. If children is not set, nothing is ever cascaded (since we have nothing to cascade it to!).
Ignored Fields

As stated earlier, a field that appears in the model for a create, update or patch, or in the data retrieved from the data store for a get or find, must be in the list of fields, or you will get an unknwon field error.

The exception is any field that starts with $b.. (Sound familiar? It should; it is the exact same flag we used for queries.

Any field whose name starts with $b. will be passed to presave and filters and post processors, but not to the validation functions or the database updates.

What could that be useful for? Well, what if you are creating a group, and business logic dictates that when a group is created, add the creator as a member.

POST /groups {name:"My Group"}

Now, how do you add the user as a member? Well, the second member looks like this:

POST /memberships {user:25}
	

But what about the logic for the first user? No problem, this is domain logic, so let's add it to the model post processors:

module.exports = {
	fields: {
		name: {required:true,validation:"string"}
	},
	post: {
		create: function(model,models,err,res,callback) {
			// assuming res = ID of newly created group
			var uid = ???; // how do we get the user ID?!?!?
			models.memberships.create({group:res,user:uid},callback);
		}
	}
}

In theory, we could do some strange "get the context" stuff, but that really does break MVC, not to mention dependency injection. The model should not have to know about things like that! Only the controller should.

So we want the controller to inject the user into the model, so the post-processor can handle it. No problem, I will use a controller filter:

module.exports = {
	filter: {
		create: function(req,res,next){
			req.body.user = req.GetMyUser(); // the controller should know how to do this
		}
	}
}

You know what happens next, right? The field user was never defined as a field on the groups model, because it is not a property of the group. Our POST /groups {name:"My Group"} will return a 400 with the error {user:"unknownfield"}.

We need some way to have the filter add the user so it gets passed to the model filters and post-processors, but not the validators or database saves.

Hence $b.anything.

// controller
module.exports = {
	filter: {
		create: function(req,res,next){
			req.body['$b.user'] = req.GetMyUser(); // the controller should know how to do this
			// now it will safely skip the validators, but still get passed to filters and post-processors
		}
	}
}


// model
module.exports = {
	fields: {
		name: {required:true,validation:"string"}
	},
	post: {
		create: function(model,models,err,res,callback) {
			// assuming res = ID of newly created group
			var uid = model['$b.user']; // how do we get the user ID?!?!?
			models.memberships.create({group:res,user:uid},callback);
		}
	}
}

Validations

Validation for a field is an object that defines the validations to which any creation of an object will be subject. The actual validations themselves will be in the property valid.

For example, if you have a field called name, it will look like this:

name: {  validation:{valid:"alphanumeric"}  }

Or

name: {  validation:{valid:["email","alphanumeric"]}  }

Or is a function, like this:

name: {  validation:{valid:function(){...}}  }

However, you can just simplify it as follows:

name: {  validation: "alphanumeric"  }

Or

name: {  validation: ["email","alphanumeric"]  }

Or is a function, like this:

name: {  validation: function(){...} }

Validations - in the object format as the value of valid or in the simplified format as the top-level property - are one of:

  • Predefined validations that you can use to check the field
  • An arbitrary function that you can define to validate and even manipulate the field

Each of these is explained in more detail below.

Predefined validations

Predefined validations are a single string or an array of strings that name validations to which to subject each model before sending persisting them to the database or accepting them from the database.

The following validations exist as of this writing:

  • notblank: Is not null, undefined or a string made up entirely of whitespace
  • notpadded: Does not start or end with whitespace
  • email: Is a valid email pattern. Does not actually check the email address. For example, fooasao12122323_12saos@gmail.com is a valid email pattern, but I am pretty sure that the address is not in use.
  • integer: must be a valid javascript number. Note: JavaScript does not distinguish between different number types; all numbers are 64-bit floats, so neither do we.
  • number: same as integer
  • float: same as integer
  • double: same as integer
  • alphanumeric: must be a valid alphanumeric a-zA-Z0-9
  • string: must be a string
  • boolean: must be a boolean true or false
  • array: must be an array
  • integerArray: must be an array, every element of which must be a valid integer
  • stringArray; must be an array, every element of which must be a valid alphanumeric
  • unique: must be an array, no element of which may be repeated more than once
  • minimum:<n>: must be a string of minimum length n
  • list:item,item,item,...,item: must be one of the string items in the list

If two or more validations are provided in an array, then all of the validations must pass (logical AND).

Direct Access

You can directly access the predefined validations as:

var validator = require('booster').validator;

The validator is called with the item to validate and the validation:

validator("abcd","email"); //false
validator("abcd","alphanumeric"); // true
Validation Functions

Sometimes, the pre-defined validations just are not enough. You want to define your own validation functions. No problem! Instead of a string or array of strings, simply define a function, like so:

module.exports = {
	fields: {
		id: {required:true, validation: "integer"},
		name: {required:true, validation: "alphanumeric"},
		password: {required:true, validation: function(name,field,mode,attrs){
			// do whatever checks and changes you want here
		}}
	}
}

The validation function provides several parameters:

  • name: name of the model class you are validating, e.g. 'user' or 'post', helpful for generic functions.
  • field: name of the field you are validating, helpful for generic functions.
  • mode: what we were doing to get this mode for validating, helpful if you need to validate some things on save, but not load. One of: find get update create 'patch (which are the exact db methods. Smart, eh?)
  • attrs: the JavaScript object you are validating.
  • callback: OPTIONAL. Async callback.

And what should the validation function return? It can return one of three things:

  • true: the validation passed, go ahead and do whatever else you were going to do
  • false: the validation did not pass, call the callback with an error indicating
  • object: the validation might or might not have passed, but we want to do some more work:

The returned object should have the following properties:

  • valid: true or false if the validation passed
  • value: if this exists, then the value of this key on the object should be changed to the provided value before moving on. See the example below.
  • message: if the validation failed (valid === false), then this is the message to be passed
Sync/Async

Note that a validation function can be synchronous or asynchronous.

  • Sync: If the arity (number of arguments) of a validation function is four, then it is treated as synchronous, and the validation return is the return value of the function.
  • Async: If the arity of a validation function is five, then it is treated as asynchronous, and the fifth argument is the callback function. The callback function should have one argument exactly, the true/false/object that would be returned.

Note that sync is likely to be deprecated in future versions.

The classic example for this is a password field. Let's say the user updated their password, we don't just want to validate the password as alphanumeric or existing, we want to do two special things:

  1. We want to validate that the password is at least 8 characters (which john's new password is not)
  2. We want to one-way hash the password before putting it in the database

Our validation function will look like this:

module.exports = {
	fields: {
		id: {required:true, validation: "integer"},
		name: {required:true, validation: "alphanumeric"},
		password: {required:true, validation: function(name,field,attrs){
			var valid, newpass = attrs[field];
			// in create or update, we are OK with no password, but check length and hash it if it exists
			if (mode === "create" || mode === "update") {
				if (newpass === undefined) {
					valid = true;
				} else if (newpass.length < 8>) {
					valid = {valid:false,message:"password_too_short"};
				} else {
					valid = {valid:true, value: hashPass(newpass)}; // we want it set to "as
2.2.1

8 years ago

2.2.0

8 years ago

2.1.0

8 years ago

2.0.2

9 years ago

2.0.1

9 years ago

2.0.0

9 years ago

1.8.6

9 years ago

1.8.5

9 years ago

1.8.4

9 years ago

1.8.3

9 years ago

1.8.2

9 years ago

1.8.1

9 years ago

1.8.0

9 years ago

1.7.0

9 years ago

1.6.4

9 years ago

1.6.3

9 years ago

1.6.2

9 years ago

1.6.1

9 years ago

1.6.0

9 years ago

1.5.8

9 years ago

1.5.7

9 years ago

1.5.6

9 years ago

1.5.5

9 years ago

1.5.4

9 years ago

1.5.3

9 years ago

1.5.2

9 years ago

1.5.1

9 years ago

1.5.0

9 years ago

1.4.11

9 years ago

1.4.10

9 years ago

1.4.9

9 years ago

1.4.8

9 years ago

1.4.7

9 years ago

1.4.6

9 years ago

1.4.5

9 years ago

1.4.4

9 years ago

1.4.3

9 years ago

1.4.2

9 years ago

1.4.1

9 years ago

1.4.0

9 years ago

1.3.0

9 years ago

1.2.0

10 years ago

1.1.2

10 years ago

1.1.1

10 years ago

1.1.0

10 years ago

1.0.2

10 years ago

1.0.1

10 years ago

1.0.0

10 years ago

0.7.1

10 years ago

0.7.0

10 years ago

0.6.8

10 years ago

0.6.6

11 years ago

0.6.5

11 years ago

0.6.4

11 years ago

0.6.3

11 years ago

0.6.2

11 years ago

0.6.1

11 years ago

0.6.0

11 years ago

0.5.9

11 years ago

0.5.8

11 years ago

0.5.7

11 years ago

0.5.6

11 years ago

0.5.5

11 years ago

0.5.4

11 years ago

0.5.2

11 years ago

0.5.1

11 years ago

0.5.0

11 years ago

0.4.13

11 years ago

0.4.12

11 years ago

0.4.11

11 years ago

0.4.10

11 years ago

0.4.9

11 years ago

0.4.8

11 years ago

0.4.7

11 years ago

0.4.6

11 years ago

0.4.5

11 years ago

0.4.4

11 years ago

0.4.3

11 years ago

0.4.2

11 years ago

0.4.1

11 years ago

0.4.0

11 years ago

0.3.4

11 years ago

0.3.3

11 years ago

0.3.2

11 years ago

0.3.1

11 years ago

0.3.0

11 years ago

0.2.1

11 years ago

0.2.0

11 years ago