1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | var 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 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | exports.createRouter = function (resource, options) {
|
27 | return new ResourcefulRouter(resource, options);
|
28 | };
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | exports.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 |
|
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 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | var ResourcefulRouter = exports.ResourcefulRouter = function (resource, options) {
|
77 | options = options || {};
|
78 |
|
79 |
|
80 |
|
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 |
|
92 |
|
93 | util.inherits(ResourcefulRouter, director.http.Router);
|
94 |
|
95 |
|
96 |
|
97 |
|
98 | exports.name = 'restful';
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 | exports.init = function (done) {
|
105 | done();
|
106 | };
|
107 |
|
108 | exports.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 |
|
118 |
|
119 |
|
120 |
|
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 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 | exports.extendRouter = function (router, resources, options, respond) {
|
145 | options = options || {};
|
146 |
|
147 |
|
148 |
|
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 |
|
171 |
|
172 | router.get('/', function () {
|
173 | var rsp = '';
|
174 |
|
175 |
|
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 |
|
193 | function _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 |
|
204 |
|
205 | if (resource._children && resource._children.length > 0) {
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 | resource._children.forEach(function(child){
|
212 | var childResource = resourceful.resources[child],
|
213 | clonedOptions = utile.clone(options);
|
214 |
|
215 |
|
216 |
|
217 |
|
218 | clonedOptions.parent = resource;
|
219 |
|
220 |
|
221 |
|
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 |
|
234 |
|
235 |
|
236 | if (!options.strict) {
|
237 | _extendWithNonStrictRoutes(router, resource, options, respond);
|
238 | }
|
239 |
|
240 |
|
241 |
|
242 |
|
243 | router.path(options.prefix + '/' + entity, function () {
|
244 |
|
245 |
|
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 |
|
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 |
|
276 |
|
277 | this.path('/' + param, function () {
|
278 |
|
279 |
|
280 |
|
281 |
|
282 | if (options.exposeMethods) {
|
283 |
|
284 |
|
285 |
|
286 |
|
287 | for (var m in resource) {
|
288 | if(typeof resource[m] === "function" && resource[m].remote === true) {
|
289 | var self = this;
|
290 |
|
291 |
|
292 |
|
293 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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") {
|
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 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 | function _extendWithNonStrictRoutes(router, resource, options, respond) {
|
424 | var entity = resource._resource.toLowerCase(),
|
425 | param = options.param || ':id';
|
426 |
|
427 |
|
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") {
|
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 |
|
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 |
|
515 |
|
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 |
|
533 |
|
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") {
|
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 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 |
|
577 | function 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 |
|
594 |
|
595 |
|
596 |
|
597 |
|
598 |
|
599 | function preprocessRequest(req, resource, action, data, error) {
|
600 |
|
601 | data = data || {};
|
602 | error = error || null;
|
603 | req.body = req.body || {};
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
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 |
|
630 |
|
631 | for (var p in req.body) {
|
632 | if (resource.schema.properties[p] && resource.schema.properties[p].type === "array") {
|
633 |
|
634 |
|
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 |
|
651 |
|
652 | utile.mixin(data, req.body, params);
|
653 |
|
654 |
|
655 |
|
656 |
|
657 | req.restful = {
|
658 | action: action,
|
659 | resource: resource,
|
660 | data: data,
|
661 | error: error
|
662 | };
|
663 |
|
664 |
|
665 |
|
666 |
|
667 | |
668 |
|
669 |
|
670 |
|
671 |
|
672 |
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 | }
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 | function inflect (str) {
|
687 | return utile.inflect.pluralize(str);
|
688 | }
|
689 |
|
690 | function 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 | }
|