UNPKG

20.4 kBJavaScriptView Raw
1/*
2 * restful.js: Restful routing using resourceful and director.
3 *
4 * (C) 2012, Nodejitsu Inc.
5 *
6 */
7
8var director = require('director'),
9 resourceful = require('resourceful'),
10 de = require('director-explorer'),
11 controller = require('./restful/controller'),
12 url = require('url'),
13 qs = require('qs'),
14 util = require('util'),
15 utile = require('utile'),
16 http = require('http');
17
18//
19// ### function createRouter (resource, options)
20// #### @resource {resourceful.Resource} Resource to use for the router.
21// #### @options {Object} Options to use when attaching routes
22//
23// Creates a new "ResourcefulRouter" instance that will dispatch RESTFul urls
24// for specified resource
25//
26exports.createRouter = function (resource, options) {
27 return new ResourcefulRouter(resource, options);
28};
29
30//
31// ### function createServer (resources)
32// #### @resources {resourceful.Resource} Resource(s) to use for the router.
33//
34// Responds with an `http.Server` instance with a `RestfulRouter` for the
35// specified `resources`.
36//
37exports.createServer = function (resources, options, handler) {
38 var router = exports.createRouter(resources, options),
39 server = http.createServer(function (req, res) {
40 req.chunks = [];
41 req.on('data', function (chunk) {
42 req.chunks.push(chunk.toString());
43 });
44
45 router.dispatch(req, res, function (err) {
46 if (err) {
47 //
48 // TODO: Dont always respond with 404
49 //
50 res.writeHead(404);
51 res.end();
52 }
53 console.log('Served ' + req.url);
54 });
55 });
56
57 server.router = router;
58 return server;
59
60};
61
62//
63// ### function ResourcefulRouter (resource, options)
64// #### @resource {resourceful.Resource} Resource to use for the router.
65// #### @options {Object} Options to use when attaching routes
66//
67// "ResourcefulRouter" Constructor function that will dispatch RESTFul urls
68// for specified resource
69//
70// POST /creature => Creature.create()
71// GET /creature => Creature.all()
72// GET /creature/1 => Creature.show()
73// PUT /creature/1 => Creature.update()
74// DELETE /creature/1 => Creature.destroy()
75//
76var ResourcefulRouter = exports.ResourcefulRouter = function (resource, options) {
77 options = options || {};
78
79 //
80 // ResourcefulRouter inherits from director.http.Router
81 //
82 director.http.Router.call(this, options);
83
84 this.resource = resource;
85 this.strict = options.strict || false;
86
87 exports.extendRouter(this, resource, options);
88};
89
90//
91// Inherit from `director.http.Router`.
92//
93util.inherits(ResourcefulRouter, director.http.Router);
94
95//
96// Name this `broadway` plugin.
97//
98exports.name = 'restful';
99
100//
101// ### function init ()
102// Initializes the `restful` plugin with the App.
103//
104exports.init = function (done) {
105 done();
106};
107
108exports.attach = function (options) {
109 var app = this;
110 if (app.resources) {
111 Object.keys(app.resources).forEach(function (resource) {
112 resourceful.register(resource, app.resources[resource]);
113 });
114 Object.keys(app.resources).forEach(function (resource) {
115 var _options = options || app.resources[resource].restful || {};
116 //
117 // Only exposes resources as restful if they have set:
118 //
119 // Resource.restful = true;
120 // Resource.restful = { param: ':custom' };
121 //
122 if (app.resources[resource].restful) {
123 exports.extendRouter(
124 app.router,
125 app.resources[resource],
126 _options
127 );
128 }
129 });
130 }
131}
132
133//
134// ### @public function extendRouter (router, resource, options, respond)
135// #### @router {director.http.Router} Router to extend with routes
136// #### @resources {resourceful.Resource} Resource(s) to use in routes.
137// #### @options {Object} or {Boolean} Options for routes added.
138// #### @respond {function} Function to write to the outgoing response
139//
140// Extends the `router` with routes for the `resources` supplied and the
141// specified `options` and `respond` function to write to outgoing
142// `http.ServerResponse` streams.
143//
144exports.extendRouter = function (router, resources, options, respond) {
145 options = options || {};
146 //
147 // Remark: If resource.restful has been set to "true",
148 // use default options
149 //
150 if(typeof options === "boolean" && options) {
151 options = {};
152 }
153
154 options.prefix = options.prefix || '';
155 options.strict = options.strict || false;
156 options.exposeMethods = options.exposeMethods || true;
157
158 if(typeof options.explore === "undefined") {
159 options.explore = true;
160 }
161
162 respond = respond || respondWithResult;
163
164 if (!Array.isArray(resources)){
165 resources = [resources];
166 }
167
168 if (options.explore) {
169 //
170 // Bind GET / to a generic explorer view of routing map ( using `director-explorer` )
171 //
172 router.get('/', function () {
173 var rsp = '';
174 //
175 // Remark: Output the basic routing map for every resource using https://github.com/flatiron/director-reflector
176 //
177 rsp += de.table(router);
178 this.res.end(rsp);
179 });
180 } else {
181 router.get('/', function (_id) {
182 var res = this.res,
183 req = this.req;
184 if (!options.strict) {
185 preprocessRequest(req, resources, 'index');
186 }
187 respond(req, res, 200, '', resources);
188 });
189 }
190 _extend(router, resources, options, respond);
191};
192
193function _extend (router, resources, options, respond) {
194
195 if (!Array.isArray(resources)){
196 resources = [resources];
197 }
198
199 resources.forEach(function (resource) {
200 var entity = resource._resource.toLowerCase(),
201 param = options.param || ':id';
202 //
203 // Check to see if resource has any children
204 //
205 if (resource._children && resource._children.length > 0) {
206 //
207 // For every child the resource has,
208 // recursively call the extendRouter method,
209 // prefixing the current resource as the base path
210 //
211 resource._children.forEach(function(child){
212 var childResource = resourceful.resources[child],
213 clonedOptions = utile.clone(options);
214 //
215 // Remark: Create a new instance of options since we don't want,
216 // to modify the reference scope inside this extendRouter call
217 //
218 clonedOptions.parent = resource;
219
220 //
221 // Also, extend the router to expose child resource as child of parent
222 //
223 if(resource._parents.length === 0) {
224 clonedOptions.prefix = clonedOptions.prefix + '/' + entity + '/:id/';
225 } else {
226 clonedOptions.prefix = '/' + entity + '/:id/';
227 }
228 _extend(router, childResource, clonedOptions, respond);
229 });
230 }
231
232 //
233 // If we are not in strict mode, then extend the router with,
234 // some potentially helpful non-restful routes
235 //
236 if (!options.strict) {
237 _extendWithNonStrictRoutes(router, resource, options, respond);
238 }
239
240 //
241 // Scope all routes under /:resource
242 //
243 router.path(options.prefix + '/' + entity, function () {
244 //
245 // Bind resource.all ( show all ) to GET /:resource
246 //
247 this.get(function () {
248 var res = this.res,
249 req = this.req;
250 resource.all(function (err, results) {
251 if (!options.strict) {
252 preprocessRequest(req, resource, 'list', results);
253 }
254 return err
255 ? respond(req, res, 500, err)
256 : respond(req, res, 200, entity, results);
257 });
258 });
259
260 //
261 // Bind POST /:resource to resource.create()
262 //
263 this.post(function (_id) {
264 var res = this.res,
265 req = this.req;
266 if (!options.strict) {
267 preprocessRequest(req, resource);
268 }
269 var cloned = utile.clone(options);
270 cloned.parentID = _id;
271 controller.create(req, res, resource, cloned, respond);
272 });
273
274 //
275 // Bind /:resource/:param path
276 //
277 this.path('/' + param, function () {
278
279 //
280 // If we are going to expose Resource methods to the router interface
281 //
282 if (options.exposeMethods) {
283 //
284 // Find every function on the resource,
285 // which has the "remote" property set to "true"
286 //
287 for (var m in resource) {
288 if(typeof resource[m] === "function" && resource[m].remote === true) {
289 var self = this;
290
291 //
292 // For every function we intent to expose remotely,
293 // bind a GET and POST route to the method
294 //
295 (function(m){
296 self.path('/' + m.toLowerCase(), function(){
297 this.get(function (_id) {
298 var req = this.req,
299 res = this.res;
300 resource[m](_id, req.body, function(err, result){
301 return err
302 ? respond(req, res, 500, err)
303 : respond(req, res, 200, 'result', result);
304 });
305 });
306 this.post(function (_id) {
307 var req = this.req,
308 res = this.res;
309 resource[m](_id, req.body, function(err, result){
310 return err
311 ? respond(req, res, 500, err)
312 : respond(req, res, 200, 'result', result);
313 });
314 });
315 });
316 })(m)
317 }
318 }
319 }
320
321 //
322 // Bind POST /:resource/:id to resource.create(_id)
323 //
324 this.post(function (_id, childID) {
325 var res = this.res,
326 req = this.req;
327
328 if (!options.strict) {
329 preprocessRequest(req, resource);
330 }
331
332 var cloned = utile.clone(options);
333 cloned._id = _id;
334 cloned.childID = childID;
335 controller.create(req, res, resource, cloned, respond);
336
337 });
338
339 //
340 // Bind GET /:resource/:id to resource.get
341 //
342 this.get(function (_id, childID) {
343 var req = this.req,
344 res = this.res;
345 if (!options.strict) {
346 preprocessRequest(req, resource, 'show');
347 }
348 var cloned = utile.clone(options);
349 cloned._id = _id;
350 cloned.childID = childID;
351 controller.get(req, res, resource, cloned, respond);
352 });
353
354 //
355 // Bind DELETE /:resource/:id to resource.destroy
356 //
357 this.delete(function (_id, childID) {
358 var req = this.req,
359 res = this.res;
360
361 if (options.parent && typeof childID !== 'undefined') {
362 _id = options.parent._resource.toLowerCase() + '/' + _id + '/' + childID;
363 }
364
365 resource.destroy(_id, function (err, result) {
366 return err
367 ? respond(req, res, 500, err)
368 : respond(req, res, 204);
369 });
370 });
371
372 //
373 // Bind PUT /:resource/:id to resource.update
374 //
375 this.put(function (_id, childID) {
376 var req = this.req,
377 res = this.res;
378 if (!options.strict) {
379 preprocessRequest(req, resource);
380 }
381 if (options.parent && typeof childID !== 'undefined') {
382 _id = options.parent._resource.toLowerCase() + '/' + _id + '/' + childID;
383 }
384 resource.update(_id, req.body, function (err, result) {
385 var status = 204;
386 if (err) {
387 status = 500;
388 if (typeof err === "object") { // && key.valid === false
389 status = 422;
390 }
391 }
392 return err
393 ? respond(req, res, status, err)
394 : respond(req, res, status);
395 });
396 });
397 });
398 });
399 });
400}
401
402//
403// ### @private function _extendWithNonStrictRoutes (router, resource, options, respond)
404// #### @router {director.http.Router} Router to extend with non-strict routes
405// #### @resource {resourceful.Resource} Resource to use in routes.
406// #### @options {Object} Options for routes added.
407// #### @respond function
408//
409// Since not all HTTP clients support PUT and DELETE verbs ( such as forms in web browsers ),
410// restful will also map the following browser friendly routes:
411//
412// If you prefer to not use this option, set { strict: true }
413//
414// POST /creature/1/update => Creature.update()
415// POST /creature/1/destroy => Creature.destroy()
416//
417// You might also want to consider using a rails-like approach which uses
418// the convention of a reserved <form> input field called "_method" which contains either
419// "PUT" or "DELETE"
420//
421// see: https://github.com/senchalabs/connect/blob/master/lib/middleware/methodOverride.js
422//
423function _extendWithNonStrictRoutes(router, resource, options, respond) {
424 var entity = resource._resource.toLowerCase(),
425 param = options.param || ':id';
426 //
427 // Bind POST /new to resource.create
428 //
429 router.post(options.prefix + '/' + entity + '/new', function (_id) {
430 var res = this.res,
431 req = this.req;
432
433 if(typeof _id !== 'undefined') {
434 _id = _id.toString();
435 }
436
437 var action = "show";
438 preprocessRequest(req, resource, action);
439 resource.create(req.body, function (err, result) {
440 var status = 201;
441 if (err) {
442 status = 500;
443 action = "create";
444 if (typeof err === "object") { // && key.valid === false
445 status = 422;
446 }
447 }
448 preprocessRequest(req, resource, action, result, err);
449 return err
450 ? respond(req, res, status, err)
451 : respond(req, res, status, entity, result);
452 });
453 });
454
455
456 router.get(options.prefix + '/' + entity + '/find', function () {
457 var res = this.res,
458 req = this.req;
459 preprocessRequest(req, resource, 'find');
460 resource.find(req.restful.data, function(err, result){
461 respond(req, res, 200, entity, result);
462 });
463 });
464
465 router.post(options.prefix + '/' + entity + '/find', function () {
466 var res = this.res,
467 req = this.req;
468 preprocessRequest(req, resource, 'find');
469 resource.find(req.restful.data, function(err, result){
470 respond(req, res, 200, entity, result);
471 });
472 });
473
474 router.get(options.prefix + '/' + entity + '/new', function (_id) {
475 var res = this.res,
476 req = this.req;
477 preprocessRequest(req, resource, 'create');
478 respond(req, res, 200, '', {});
479 });
480
481 //
482 // Bind /:resource/:param path
483 //
484 router.path(options.prefix + '/' + entity + '/' + param, function () {
485
486 this.get('/update', function (_id) {
487 var res = this.res,
488 req = this.req;
489 preprocessRequest(req, resource, 'update');
490 resource.get(_id, function(err, result){
491 preprocessRequest(req, resource, 'update', result, err);
492 return err
493 ? respond(req, res, 500, err)
494 : respond(req, res, 200, entity, result);
495 })
496 });
497
498 this.get('/destroy', function (_id) {
499 var res = this.res,
500 req = this.req;
501 preprocessRequest(req, resource, 'destroy');
502 resource.get(_id, function(err, result){
503 preprocessRequest(req, resource, 'destroy', result, err);
504 if(err) {
505 req.restful.data = _id;
506 }
507 return err
508 ? respond(req, res, 500, err)
509 : respond(req, res, 200, entity, result);
510 })
511 });
512
513 //
514 // Bind POST /:resource/:id/destroy to resource.destroy
515 // Remark: Not all browsers support DELETE verb, so we have to fake it
516 //
517 this.post('/destroy', function (_id) {
518 var req = this.req,
519 res = this.res;
520 if (!options.strict) {
521 preprocessRequest(req, resource, 'destroy');
522 }
523 resource.destroy(_id, function (err, result) {
524 req.restful.data = _id;
525 return err
526 ? respond(req, res, 500, err)
527 : respond(req, res, 204);
528 });
529 });
530
531 //
532 // Bind POST /:resource/:id/update to resource.update
533 // Remark: Not all browsers support PUT verb, so we have to fake it
534 //
535 this.post('/update', function (_id) {
536 var req = this.req,
537 res = this.res;
538
539 if (!options.strict) {
540 preprocessRequest(req, resource, 'update');
541 }
542
543 resource.update(_id, this.req.body, function (err, result) {
544 var status = 204;
545
546 if (err) {
547 status = 500;
548 if (typeof err === "object") { // && key.valid === false
549 status = 422;
550 }
551 }
552
553 return err
554 ? respond(req, res, status, err)
555 : respond(req, res, status, entity, result);
556 });
557 });
558
559
560 });
561}
562
563//
564// ### @private function respondWithResult (req, res, status, options, value)
565// #### @req {http.ServerRequest} Incoming Server request
566// #### @res {http.ServerResponse} Server respond to write to
567// #### @status {number} Status code to respond with
568// #### @key {Object|string} Object to respond with or key to set for `value`
569// #### @value {Object} **Optional** Value to set in the result for the specified `key`
570//
571// Helper function for responding from `restful` routes:
572//
573// respond(req, res, 200);
574// respond(req, res, 500, err);
575// respond(req, res, 200, 'users', [{...}, {...}, ...]);
576//
577function respondWithResult(req, res, status, key, value) {
578 var result;
579 res.writeHead(status);
580
581 if (arguments.length === 5) {
582 result = {};
583 result[key] = value;
584 }
585 else {
586 result = key;
587 }
588
589 res.end(result ? JSON.stringify(result) : '');
590}
591
592//
593// ### function (req, resource)
594// #### @req {http.ServerRequest} Server request to preprocess
595// #### @resource {resourceful.Resource} Resource to preprocess against req
596//
597// Preprocesses "numbery" strings in the `req` body.
598//
599function preprocessRequest(req, resource, action, data, error) {
600
601 data = data || {};
602 error = error || null;
603 req.body = req.body || {};
604
605 //
606 // Remark: `restful` generates a REST interface for a Resource.
607 // Since Resources inheritently has more functionality then HTTP can provide out of the box,
608 // we are required to perform some type cohersions for non-strict mode.
609 //
610 // For instance: If we know a property type to be Number and we are using a,
611 // HTML4 input to submit it's value...it will always come in as a "numbery" String.
612 //
613 // This will cause the Number validation in Resourceful to fail since 50 !== "50"
614
615 //
616 // Number: Attempt to coerce any incoming properties know to be Numbers to a Number
617 //
618 for (var p in req.body) {
619 if (resource.schema.properties[p] && resource.schema.properties[p].type === "number") {
620 req.body[p] = Number(req.body[p]);
621 if (req.body[p].toString() === "NaN") {
622 req.body[p] = "";
623 }
624 }
625 }
626
627
628 //
629 // Array: Attempt to coerce any incoming properties know to be Arrays to an Array
630 //
631 for (var p in req.body) {
632 if (resource.schema.properties[p] && resource.schema.properties[p].type === "array") {
633 //
634 // TODO: Better array creation than eval
635 //
636 try {
637 req.body[p] = eval(req.body[p]);
638 } catch (err) {
639 }
640 if (!Array.isArray(req.body[p])) {
641 req.body[p] = [];
642 }
643 }
644 }
645
646 var query = url.parse(req.url),
647 params = qs.parse(query.query);
648
649 //
650 // Merge query and form data
651 //
652 utile.mixin(data, req.body, params);
653
654 //
655 // Remark: Append a new object to the req for additional processing down the middleware chain
656 //
657 req.restful = {
658 action: action,
659 resource: resource,
660 data: data,
661 error: error
662 };
663
664 //
665 // TODO: If there is no in-coming ID, check to see if we have any attempted secondary keys
666 //
667 /*
668 if (_id.length === 0) {
669 ('check for alts');
670 if(req.body.name) {
671 _id = req.body.name;
672 }
673 }*/
674
675 //
676 // Remark: Not returning any values since "req" is referenced in parent scope.
677 //
678}
679
680//
681// ### function inflect (str)
682// #### @str {string} String to inflect
683//
684// Responds with a properly pluralized string for `str`.
685//
686function inflect (str) {
687 return utile.inflect.pluralize(str);
688}
689
690function prettyPrint (resources) {
691 var str = '';
692 resources.forEach(function(resource){
693 str += '\n\n';
694 str += '## ' + resource._resource + ' - schema \n\n';
695 str += JSON.stringify(resource.schema.properties, true, 2) + '\n\n';
696 });
697 return str;
698}