express-resource
Advanced tools
Comparing version 0.1.0 to 0.2.0
0.2.0 / 2011-04-09 | ||
================== | ||
* Added basic content-negotiation support via format extensions | ||
* Added nested resource support | ||
* Added auto-loading support, populating `req.user` etc automatically | ||
* Added another options param to `app.resource()` | ||
* Added `Resource#[http-verb]()` methods to define additional routes | ||
* Added `Resource#map(method, path, fn)` | ||
* Changed; every `Resource` has a `.base` | ||
* Changed; resource id is no longer "id", it's a singular version of the resource name, aka `req.params.user` etc | ||
0.1.0 / 2011-03-27 | ||
@@ -3,0 +15,0 @@ ================== |
188
index.js
@@ -13,3 +13,5 @@ | ||
var express = require('express'); | ||
var express = require('express') | ||
, lingo = require('lingo') | ||
, en = lingo.en; | ||
@@ -26,46 +28,177 @@ /** | ||
var Resource = module.exports = function Resource(name, actions, app) { | ||
this.base = '/'; | ||
this.name = name; | ||
this.app = app; | ||
this.actions = actions | ||
this.id = actions.id || 'id'; | ||
this.routes = {}; | ||
actions = actions || {}; | ||
this.format = actions.format; | ||
this.id = actions.id || this.defaultId; | ||
this.param = ':' + this.id; | ||
// default actions | ||
for (var key in actions) { | ||
this.defineAction(key, actions[key]); | ||
this.mapDefaultAction(key, actions[key]); | ||
} | ||
// auto-loader | ||
if (actions.load) this.load(actions.load); | ||
}; | ||
/** | ||
* Define the given action `name` with a callback `fn()`. | ||
* Set the auto-load `fn`. | ||
* | ||
* @param {String} key | ||
* @param {Function} fn | ||
* @return {Resource} for chaining | ||
* @api public | ||
*/ | ||
Resource.prototype.defineAction = function(key, fn){ | ||
var app = this.app | ||
, id = this.id | ||
, name = '/' + (this.name || '') | ||
, path = this.name ? name + '/' : '/'; | ||
Resource.prototype.load = function(fn){ | ||
var self = this | ||
, id = this.id; | ||
this.loadFunction = fn; | ||
this.app.param(this.id, function(req, res, next){ | ||
fn(req.params[id], function(err, obj){ | ||
if (err) return next(err); | ||
// TODO: ideally we should next() passed the | ||
// route handler | ||
if (null == obj) return res.send(404); | ||
req[id] = obj; | ||
next(); | ||
}); | ||
}); | ||
return this; | ||
}; | ||
/** | ||
* Retun this resource's default id string. | ||
* | ||
* @return {String} | ||
* @api private | ||
*/ | ||
Resource.prototype.__defineGetter__('defaultId', function(){ | ||
return this.name | ||
? en.singularize(this.name) | ||
: 'id'; | ||
}); | ||
/** | ||
* Map http `method` and optional `path` to `fn`. | ||
* | ||
* @param {String} method | ||
* @param {String|Function|Object} path | ||
* @param {Function} fn | ||
* @return {Resource} for chaining | ||
* @api public | ||
*/ | ||
Resource.prototype.map = function(method, path, fn){ | ||
var self = this; | ||
if (method instanceof Resource) return this.add(method); | ||
if ('function' == typeof path) fn = path, path = ''; | ||
if ('object' == typeof path) fn = path, path = ''; | ||
if ('/' == path[0]) path = path.substr(1); | ||
method = method.toLowerCase(); | ||
// setup route pathname | ||
var route = this.base + (this.name || ''); | ||
route += (this.name && path) ? '/' : ''; | ||
route += path; | ||
route += '.:format?'; | ||
// register the route so we may later remove it | ||
(this.routes[method] = this.routes[method] || {})[route] = { | ||
method: method | ||
, path: route | ||
, orig: path | ||
, fn: fn | ||
}; | ||
// apply the route | ||
this.app[method](route, function(req, res, next){ | ||
req.format = req.params.format || self.format; | ||
if (req.format) res.contentType(req.format); | ||
if ('object' == typeof fn) { | ||
if (req.format && fn[req.format]) { | ||
fn[req.format](req, res, next); | ||
} else if (fn.default) { | ||
fn.default(req, res, next); | ||
} else { | ||
res.send(415); | ||
} | ||
} else { | ||
fn(req, res, next); | ||
} | ||
}); | ||
return this; | ||
}; | ||
/** | ||
* Nest the given `resource`. | ||
* | ||
* @param {Resource} resource | ||
* @return {Resource} for chaining | ||
* @see Resource#map() | ||
* @api public | ||
*/ | ||
Resource.prototype.add = function(resource){ | ||
var router = this.app.router | ||
, routes | ||
, route; | ||
// relative base | ||
resource.base = this.base + this.name + '/' + this.param + '/'; | ||
// re-define previous actions | ||
for (var method in resource.routes) { | ||
routes = resource.routes[method]; | ||
for (var key in routes) { | ||
route = routes[key]; | ||
delete routes[key]; | ||
router.remove(key, route.method); | ||
resource.map(route.method, route.orig, route.fn); | ||
} | ||
} | ||
return this; | ||
}; | ||
/** | ||
* Map the given action `name` with a callback `fn()`. | ||
* | ||
* @param {String} key | ||
* @param {Function} fn | ||
* @api private | ||
*/ | ||
Resource.prototype.mapDefaultAction = function(key, fn){ | ||
var id = this.param; | ||
switch (key) { | ||
case 'index': | ||
app.get(name, fn); | ||
this.get(fn); | ||
break; | ||
case 'new': | ||
app.get(path + 'new', fn); | ||
this.get('new', fn); | ||
break; | ||
case 'create': | ||
app.post(name, fn); | ||
this.post(fn); | ||
break; | ||
case 'show': | ||
app.get(path + ':' + id, fn); | ||
this.get(id, fn); | ||
break; | ||
case 'edit': | ||
app.get(path + ':' + id + '/edit', fn); | ||
this.get(id + '/edit', fn); | ||
break; | ||
case 'update': | ||
app.put(path + ':' + id, fn); | ||
this.put(id, fn); | ||
break; | ||
case 'destroy': | ||
app.del(path + ':' + id, fn); | ||
this.del(id, fn); | ||
break; | ||
@@ -76,2 +209,15 @@ } | ||
/** | ||
* Setup http verb methods. | ||
*/ | ||
express.router.methods.concat(['del', 'all']).forEach(function(method){ | ||
Resource.prototype[method] = function(path, fn){ | ||
if ('function' == typeof path | ||
|| 'object' == typeof path) fn = path, path = ''; | ||
this.map(method, path, fn); | ||
return this; | ||
} | ||
}); | ||
/** | ||
* Define a resource with the given `name` and `actions`. | ||
@@ -86,7 +232,11 @@ * | ||
express.HTTPServer.prototype.resource = | ||
express.HTTPSServer.prototype.resource = function(name, actions){ | ||
express.HTTPSServer.prototype.resource = function(name, actions, opts){ | ||
var options = actions || {}; | ||
if ('object' == typeof name) actions = name, name = null; | ||
if (options.id) actions.id = options.id; | ||
this.resources = this.resources || {}; | ||
if (!actions) return this.resources[name] || new Resource(name, null, this); | ||
for (var key in opts) options[key] = opts[key]; | ||
var res = this.resources[name] = new Resource(name, actions, this); | ||
return res; | ||
}; |
{ "name": "express-resource" | ||
, "description": "Resourceful routing for express" | ||
, "version": "0.1.0" | ||
, "version": "0.2.0" | ||
, "author": "TJ Holowaychuk <tj@vision-media.ca>" | ||
@@ -8,2 +8,3 @@ , "contributors": [ | ||
] | ||
, "dependencies": { "lingo": ">= 0.0.4" } | ||
, "keywords": ["express", "rest", "resource"] | ||
@@ -10,0 +11,0 @@ , "main": "index" |
109
Readme.md
@@ -14,3 +14,3 @@ | ||
To get started simply `require('express-resource')`, and this module will monkey-patch the `express.Server`, enabling resourceful routing. A "resource" is simply an object, which defines one of more of the supported "actions" listed below: | ||
To get started simply `require('express-resource')`, and this module will monkey-patch Express, enabling resourceful routing by providing the `app.resource()` method. A "resource" is simply an object, which defines one of more of the supported "actions" listed below: | ||
@@ -45,12 +45,4 @@ exports.index = function(req, res){ | ||
The _id_ option can be specified to prevent collisions: | ||
The `app.resource()` method returns a new `Resource` object, which can be used to further map pathnames, nest resources, and more. | ||
exports.id = 'uid'; | ||
exports.destroy = function(req, res) { | ||
res.send('destroy user ' + req.params.uid); | ||
}; | ||
The `app.resource()` method will create and return a new `Resource`: | ||
var express = require('express') | ||
@@ -62,18 +54,17 @@ , Resource = require('express-resource') | ||
Actions are then mapped as follows (by default): | ||
## Default Action Mapping | ||
GET /forums -> index | ||
GET /forums/new -> new | ||
POST /forums -> create | ||
GET /forums/:id -> show | ||
GET /forums/:id/edit -> edit | ||
PUT /forums/:id -> update | ||
DELETE /forums/:id -> destroy | ||
Actions are then mapped as follows (by default), providing `req.params.forum` which contains the substring where ":forum" is shown below: | ||
GET /forums -> index | ||
GET /forums/new -> new | ||
POST /forums -> create | ||
GET /forums/:forum -> show | ||
GET /forums/:forum/edit -> edit | ||
PUT /forums/:forum -> update | ||
DELETE /forums/:forum -> destroy | ||
Specify a top-level resource using the empty string: | ||
## Top-Level Resource | ||
var express = require('express') | ||
, Resource = require('express-resource') | ||
, app = express.createServer(); | ||
Specify a top-level resource by omitting the resource name: | ||
@@ -92,5 +83,77 @@ app.resource(require('./forum')); | ||
## Auto-Loading | ||
__NOTE:__ this functionality will surely grow with time, and as data store clients evolve we can provide close integration. | ||
Resources have the concept of "auto-loading" associated data. For example we can pass a "load" property along with our actions, which should invoke the callback function with an error, or the object such as a `User`: | ||
User.load = function(id, fn) { | ||
fn(null, users[id]); | ||
}; | ||
app.resource('users', { show: ..., load: User.load }); | ||
With the auto-loader defined, the `req.user` object will be available now be available to the actions automatically. We may pass the "load" option as the third param as well, although this is equivalent to above, but allows you to either export ".load" along with your actions, or passing it explicitly: | ||
app.resource('users', require('./user'), { load: User.load }); | ||
Finally we can utilize the `Resource#load(fn)` method, which again is functionally equivalent: | ||
var user = app.resource('users', require('./user')); | ||
user.load(User.load); | ||
This functionality works when nesting resources as well, for example suppose we have a forum, which contains threads, our setup may look something like below: | ||
var forums = app.resource('forums', require('resources/forums'), { load: Forum.get }); | ||
var threads = app.resources('threads', require('resources/threads'), { load: Thread.get }); | ||
forums.add(threads); | ||
Now when we request `GET /forums/5/threads/12` both the `req.forum` object, and `req.thread` will be available to thread's _show_ action. | ||
## Content-Negotiation | ||
Currently express-resource supports basic content-negotiation support utilizing extnames or "formats". This can currently be done two ways, first we may define actions as we normally would, and utilize the `req.format` property, and respond accordingly. The following would respond to `GET /pets.xml`, and `GET /pets.json`. | ||
var pets = ['tobi', 'jane', 'loki']; | ||
exports.index = function(req, res){ | ||
switch (req.format) { | ||
case 'json': | ||
res.send(pets); | ||
break; | ||
case 'xml': | ||
res.send('<pets>' + pets.map(function(pet){ | ||
return '<pet>' + pet + '</pet>'; | ||
}).join('') + '</pets>'); | ||
break; | ||
default: | ||
res.send(415); | ||
} | ||
}; | ||
The following is equivalent, however we separate the logic into several callbacks, each representing a format. | ||
exports.index = { | ||
json: function(req, res){ | ||
res.send(pets); | ||
}, | ||
xml: function(req, res){ | ||
res.send('<pets>' + pets.map(function(pet){ | ||
return '<pet>' + pet + '</pet>'; | ||
}).join('') + '</pets>'); | ||
} | ||
}; | ||
We may also provide a `default` format, invoked when either no extension is given, or one that does not match another method is given: | ||
exports.default = function(req, res){ | ||
res.send('Unsupported format "' + req.format + '"', 415); | ||
}; | ||
To assign a default format to an existing method, we can provide the `format` option to the resource. With the following definition both `GET /users/5` and `GET /users/5.json` will invoke the `show.json` action, or `show` with `req.format = 'json'`. | ||
app.resource('users', actions, { format: 'json' }); | ||
## Running Tests | ||
@@ -97,0 +160,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
13989
205
190
1
+ Addedlingo@>= 0.0.4
+ Addedlingo@0.0.5(transitive)