UNPKG

43.9 kBJavaScriptView Raw
1(function () {
2
3 ejs = require('ejs');
4 inflect = require('i')();
5 _ = require('lodash');
6 when = require('when');
7 crypto = require('crypto');
8 toRegexp = require('path-to-regexp');
9
10 var MIN_MONGOOSE_VERSION = '3.6.15';
11
12 var mongoose, ObjectId, acreAdminModel;
13
14 var createVerb = 'put';
15 var updateVerb = 'post';
16
17 function UserCallback (operation, mode, path, callback)
18 {
19 this.operation = operation;
20 this.mode = mode;
21 this.path = path;
22 this.callback = callback;
23 }
24
25 function DynamicRouteData (viewPath, contentType, options, modelName)
26 {
27 this.viewPath = viewPath;
28 this.contentType = contentType;
29 this.options = _.cloneDeep(options);
30 this.modelName = modelName;
31 }
32
33 var userCallbacks = [];
34 var dynamicRouteData = {};
35
36 var getUserCallback = function(operation, mode, path){
37 for (var i in userCallbacks)
38 {
39 var userCallback = userCallbacks[i];
40 if (userCallback.operation === operation && userCallback.mode === mode && userCallback.path.test(path))
41 {
42 return userCallback.callback;
43 }
44 }
45 return null;
46 };
47
48 var setRESTPaths = function (app, path, Model)
49 {
50 var preCreate = getUserCallback(acre.CREATE, acre.PRE, path);
51 var preRetrieve = getUserCallback(acre.RETRIEVE, acre.PRE, path);
52 var preUpdate = getUserCallback(acre.UPDATE, acre.PRE, path);
53 var preDelete = getUserCallback(acre.REMOVE, acre.PRE, path);
54
55 var postCreate = getUserCallback(acre.CREATE, acre.POST, path);
56 var postRetrieve = getUserCallback(acre.RETRIEVE, acre.POST, path);
57 var postUpdate = getUserCallback(acre.UPDATE, acre.POST, path);
58 var postDelete = getUserCallback(acre.REMOVE, acre.POST, path);
59
60 var overrideCreate = getUserCallback(acre.CREATE, acre.OVERRIDE, path);
61 var overrideRetrieve = getUserCallback(acre.RETRIEVE, acre.OVERRIDE, path);
62 var overrideUpdate = getUserCallback(acre.UPDATE, acre.OVERRIDE, path);
63 var overrideDelete = getUserCallback(acre.REMOVE, acre.OVERRIDE, path);
64
65 var createPrivate = getUserCallback(acre.CREATE, acre.PRIVATE, path);
66 var retrievePrivate = getUserCallback(acre.RETRIEVE, acre.PRIVATE, path);
67 var updatePrivate = getUserCallback(acre.UPDATE, acre.PRIVATE, path);
68 var deletePrivate = getUserCallback(acre.REMOVE, acre.PRIVATE, path);
69
70 console.log("setting up CRUD REST paths for: " + path);
71
72 // Create
73 if (overrideCreate)
74 {
75 console.log(createVerb.toUpperCase() + " is user overriden for path: " + path);
76 }
77 else
78 {
79 var middleware = [];
80 if (createPrivate)
81 {
82 middleware.push(forcePrivateAccess);
83 }
84
85 if (preCreate)
86 {
87 middleware.push(preCreate);
88 }
89
90 if (middleware.length)
91 {
92 app[createVerb](path, middleware, function(request, response){
93 create(Model, request, response, postCreate);
94 });
95 }
96 else
97 {
98 app[createVerb](path, function(request, response){
99 create(Model, request, response, postCreate);
100 });
101 }
102 }
103
104 // Retrieve
105 if (overrideRetrieve)
106 {
107 console.log("GET is user overriden for path: " + path);
108 }
109 else
110 {
111 var middleware = [];
112 if (retrievePrivate)
113 {
114 middleware.push(forcePrivateAccess);
115 }
116
117 if (preRetrieve)
118 {
119 middleware.push(preRetrieve);
120 }
121
122 if (middleware.length)
123 {
124 app.get(path, middleware, function(request, response){
125 retrieve(Model, request, response, postRetrieve);
126 });
127 }
128 else
129 {
130 app.get(path, function(request, response){
131 retrieve(Model, request, response, postRetrieve);
132 });
133 }
134 }
135
136 // Update
137 if (overrideUpdate)
138 {
139 console.log(updateVerb.toUpperCase() + " is user overriden for path: " + path);
140 }
141 else
142 {
143 var middleware = [];
144 if (updatePrivate)
145 {
146 middleware.push(forcePrivateAccess);
147 }
148
149 if (preUpdate)
150 {
151 middleware.push(preUpdate);
152 }
153
154 if (middleware.length)
155 {
156 app[updateVerb](path, middleware, function(request, response){
157 update(Model, request, response, postUpdate);
158 });
159 }
160 else
161 {
162 app[updateVerb](path, function(request, response){
163 update(Model, request, response, postUpdate);
164 });
165 }
166 }
167
168 // Delete
169 if (overrideDelete)
170 {
171 console.log("DELETE is user overriden for path: " + path);
172 }
173 else
174 {
175 var middleware = [];
176 if (deletePrivate)
177 {
178 middleware.push(forcePrivateAccess);
179 }
180
181 if (preDelete)
182 {
183 middleware.push(preDelete);
184 }
185
186 if (middleware.length)
187 {
188 app["delete"](path, middleware, function(request, response){
189 remove(Model, request, response, postDelete);
190 });
191 }
192 else
193 {
194 app["delete"](path, function(request, response){
195 remove(Model, request, response, postDelete);
196 });
197 }
198 }
199 };
200
201 var findModel = function(modelName)
202 {
203 for (i in mongoose.models)
204 {
205 var Model = mongoose.models[i];
206 if (Model.modelName === modelName)
207 {
208 return when.resolve(Model);
209 }
210 }
211
212 return when.reject('failed to find model: ' + modelName);
213 };
214
215 var normalizePath = function(path)
216 {
217 var _path = path.replace(new RegExp('^' + acre.options.rootPath), '');
218 if (_path.charAt(0) !== '/')
219 {
220 _path = '/' + _path;
221 }
222
223 return _path;
224 };
225
226 var storeForeignKeys = function(Models)
227 {
228 var i, result = {};
229 var _storeForeignKeys = function(tree, path, Model)
230 {
231 var i;
232 for (i in tree)
233 {
234 if (i === 'id' || i === '_id')
235 {
236 continue;
237 }
238
239 var _path = (path === '') ? i : path + '.' + i;
240
241 if (
242 (_.isPlainObject(tree[i]) && !_.isUndefined(tree[i].ref)) ||
243 (_.isArray(tree[i]) && tree[i].length === 1 && !_.isUndefined(tree[i][0].ref))
244 )
245 {
246 if (_.isUndefined(acre.foreignKeys[Model.modelName]))
247 {
248 acre.foreignKeys[Model.modelName] = {};
249 }
250
251 acre.foreignKeys[Model.modelName][_path] = (_.isArray(tree[i])) ? tree[i][0].ref : tree[i].ref;
252 }
253 else if (_.isPlainObject(tree[i]))
254 {
255 _storeForeignKeys(tree[i], _path, Model);
256 }
257 else if (_.isArray(tree[i]))
258 {
259 _storeForeignKeys(tree[i][0], _path, Model);
260 }
261 }
262 };
263
264 for (i in Models)
265 {
266 _storeForeignKeys(Models[i].schema.tree, '', Models[i]);
267 }
268 };
269
270 var initializeAdminModel = function(app)
271 {
272 var AcreAdminSchema = new mongoose.Schema({
273 username: {
274 type: String,
275 required: true,
276 unique: true,
277 index: true,
278 validate: [/[\s\S]/, 'admin username cannot be empty']
279 },
280 password: {
281 type: String,
282 required: true,
283 validate: [/[\s\S]/, 'admin password cannot be empty']
284 }
285 });
286
287 acreAdminModel = mongoose.model('AcreAdmin', AcreAdminSchema);
288
289 acre.private(acre.CREATE, [
290 acre.options.rootPath + '/acreadmins/:id/username',
291 acre.options.rootPath + '/acreadmins/:id/password',
292 acre.options.rootPath + '/acreadmins/:id',
293 acre.options.rootPath + '/acreadmins'
294 ]);
295 acre.private(acre.RETRIEVE, [
296 acre.options.rootPath + '/acreadmins/:id/username',
297 acre.options.rootPath + '/acreadmins/:id/password',
298 acre.options.rootPath + '/acreadmins/:id',
299 acre.options.rootPath + '/acreadmins'
300 ]);
301 acre.private(acre.UPDATE, [
302 acre.options.rootPath + '/acreadmins/:id/username',
303 acre.options.rootPath + '/acreadmins/:id/password',
304 acre.options.rootPath + '/acreadmins/:id',
305 acre.options.rootPath + '/acreadmins'
306 ]);
307 acre.private(acre.REMOVE, [
308 acre.options.rootPath + '/acreadmins/:id/username',
309 acre.options.rootPath + '/acreadmins/:id/password',
310 acre.options.rootPath + '/acreadmins/:id',
311 acre.options.rootPath + '/acreadmins'
312 ]);
313 acre.pre(acre.CREATE, acre.options.rootPath + '/acreadmins', function(request, response, next){
314 if (request.body && request.body.password)
315 {
316 request.body.password = crypto.createHmac('sha256', acre.options.adminPasswordSalt).update(request.body.password).digest('hex');
317 }
318 next();
319 });
320
321 app.post(acre.options.rootPath + '/acreadmins/login', function(request, response){
322 adminLogin(request)
323 .then(function(){
324 respondSuccess(response, 'login successful');
325 }, function(err){
326 console.log('login err: ');
327 console.log(err);
328 respondError(response, 401, 'Unauthorized');
329 });
330 });
331
332 app.post(acre.options.rootPath + '/acreadmins/logout', function(request, response){
333 adminLogout(request);
334 respondSuccess(response, 'logout successful');
335 });
336
337 app.get(acre.options.rootPath + '/acreadmins/test', forcePrivateAccess, function(request, response){
338 respondSuccess(response, '200 OK');
339 });
340 };
341
342 var adminLogin = function(request)
343 {
344 var deferred = when.defer();
345
346 if (_.isUndefined(request.session))
347 {
348 deferred.reject('acre admin portal requires express session middleware');
349 }
350 else
351 {
352 adminLogout(request);
353 acreAdminModel.find(function(error, admins){
354 if (error)
355 {
356 deferred.reject(error);
357 }
358 else
359 {
360 // if there are no admins, permit anyone to see the admin portal
361 // this way admins can be configured
362 if (_.isArray(admins))
363 {
364 var i = 0, found = false;
365 for (i in admins)
366 {
367 if (admins[i].username === request.body.username)
368 {
369 if (admins[i].password === crypto.createHmac('sha256', acre.options.adminPasswordSalt).update(request.body.password).digest('hex'))
370 {
371 request.session.acre_admin_id = admins[i]._id.toString();
372 found = true;
373 }
374 break;
375 }
376 }
377
378 if (found)
379 {
380 deferred.resolve();
381 }
382 else
383 {
384 deferred.reject('login failed');
385 }
386 }
387 else
388 {
389 deferred.reject('login failed');
390 }
391 }
392 });
393 }
394
395 return deferred.promise;
396 };
397
398 var adminLogout = function(request)
399 {
400 if (!_.isUndefined(request.session))
401 {
402 delete request.session.acre_admin_id;
403 }
404 };
405
406 var forcePrivateAccess = function(request, response, next)
407 {
408 if (_.isUndefined(request.session))
409 {
410 respondError(response, 400, 'acre admin portal requires express session middleware');
411 }
412 else
413 {
414 acreAdminModel.find(function(error, admins){
415 if (error)
416 {
417 respondError(response, 500, error.message);
418 }
419 else
420 {
421 // if there are no admins, permit anyone to see the admin portal
422 // this way admins can be configured
423 if (!_.isArray(admins) || admins.length == 0)
424 {
425 next();
426 }
427 else
428 {
429 if (!_.isUndefined(request.session.acre_admin_id))
430 {
431 var i = 0, found = false;
432 for (i in admins)
433 {
434 if (admins[i]._id.toString() === request.session.acre_admin_id)
435 {
436 found = true;
437 break;
438 }
439 }
440
441 if (found)
442 {
443 next();
444 }
445 else
446 {
447 respondError(response, 401, 'Unauthorized');
448 }
449 }
450 else
451 {
452 respondError(response, 401, 'Unauthorized');
453 }
454 }
455 }
456 });
457 }
458 };
459
460 /**
461 * Given a model, composes a default instantiation of a mongoose model
462 * @param Model - mongoose model descriptor
463 * @returns - an array containing default instantiated model, and a version of the default instantiated model for form generation
464 */
465 var composeDefaultInstance = function(Model)
466 {
467 var modelInstance, formInstance, i, j, k;
468
469 /*
470 * strips the tree down to bare data, leaf elements will either be
471 * empty strings ("") or a list of options in the case of a ref
472 * field
473 */
474 var stripTree = function(tree, path)
475 {
476 var i, result = {};
477 for (i in tree)
478 {
479 if (i === 'id' || i === '_id')
480 {
481 continue;
482 }
483
484 var _path = (path === '') ? i : path + '.' + i;
485
486 if (
487 (_.isPlainObject(tree[i]) && !_.isUndefined(tree[i].ref)) ||
488 (_.isArray(tree[i]) && tree[i].length === 1 && !_.isUndefined(tree[i][0].ref))
489 )
490 {
491 result[i] = (_.isArray(tree[i])) ? [] : '';
492 }
493 else if (_.isPlainObject(tree[i]))
494 {
495 result[i] = stripTree(tree[i], _path);
496 }
497 else if (_.isArray(tree[i]))
498 {
499 if (i === 'validate')
500 {
501 continue;
502 }
503 else
504 {
505 result[i] = [stripTree(tree[i][0], _path)];
506 }
507 }
508 }
509
510 if (Object.keys(result).length === 0)
511 {
512 return "";
513 }
514
515 return result;
516 };
517
518 var tree = _.cloneDeep(Model.schema.tree);
519 modelInstance = stripTree(tree, '');
520 formInstance = _.cloneDeep(modelInstance);
521
522 if (_.isUndefined(acre.foreignKeys) ||
523 Object.keys(acre.foreignKeys).length === 0 ||
524 _.isUndefined(acre.foreignKeys[Model.modelName]) ||
525 Object.keys(acre.foreignKeys[Model.modelName]).length === 0)
526 {
527 return when.resolve([modelInstance, formInstance]);
528 }
529 else
530 {
531 var foreignKeys = Object.keys(acre.foreignKeys[Model.modelName]), defereds = {}, defered = when.defer();
532 _.each(foreignKeys, function(key){
533 var defered = when.defer();
534 defereds[key] = defered;
535 findModel(acre.foreignKeys[Model.modelName][key])
536 .then(
537 function(_Model){
538 _Model.find(function(error, models){
539 if (error)
540 {
541 defered.reject('failed to fetch models for ref: ' + _Model.modelName);
542 }
543 else
544 {
545 if (!models)
546 {
547 models = [];
548 }
549
550 var crumbs = key.split('.');
551 var ref = formInstance, obj, field;
552
553 for (j = 0; j < crumbs.length; j++)
554 {
555 var crumb = crumbs[j];
556 if (j === crumbs.length - 1)
557 {
558 if (_.isArray(ref[crumb]))
559 {
560 obj = ref[crumb];
561 field = 0;
562 }
563 else
564 {
565 obj = ref;
566 field = crumb;
567 }
568 break;
569 }
570 else
571 {
572 if (_.isPlainObject(ref[crumb]))
573 {
574 ref = ref[crumb];
575 }
576 else if (_.isArray(ref[crumb]))
577 {
578 ref = ref[crumb][0];
579 }
580 else
581 {
582 defered.reject('cannot handle object type: ' + typeof(ref[crumb]));
583 break;
584 }
585 }
586 }
587
588 if (_.isUndefined(obj) || _.isUndefined(field))
589 {
590 defered.reject('failed to find ref for ' + _Model.modelName + ' in: ' + JSON.stringify(formInstance));
591 }
592 else
593 {
594 if (_.isArray(obj[field]))
595 {
596 obj[field] = [{
597 type: 'select',
598 options: models
599 }];
600 }
601 else
602 {
603 obj[field] = {
604 type: 'select',
605 options: models
606 };
607 }
608
609 defered.resolve();
610 }
611 }
612 });
613 },
614 function(err){
615 defered.reject(err);
616 }
617 );
618 });
619
620 var promises = _.values(defereds).map(function(defered){
621 return defered.promise;
622 });
623
624 when.all(promises)
625 .then(
626 function(){
627 defered.resolve([modelInstance, formInstance]);
628 },
629 function(err){
630 defered.reject(err);
631 }
632 );
633
634 return defered.promise;
635 }
636 };
637
638 var heirarchy = {
639 ROOT: "HEIRARCHY_ROOT",
640 BRANCH: "HEIRARCHY_BRANCH",
641 LEAF: "HEIRARCHY_LEAF"
642 };
643
644 var traverseObjectHeirarchy = function(model, request, response, callback){
645 if (!model || !model._id)
646 {
647 respondError(response, 500, "invalid model");
648 callback("invalid model", null, null);
649 }
650 else
651 {
652 var path = normalizePath(request.route.path);
653 var keys = path.split("/");
654
655 //remove first three elements
656 //these correspond to blank(""), model and its id
657 keys.shift();
658 keys.shift();
659 keys.shift();
660
661 var traverseSubObjectHeirarchy = function(_object, keys, callback)
662 {
663 var key, param, i, type, found;
664
665 if (keys.length === 0)
666 {
667 // return root resource
668 console.log("returning root resource");
669 callback(null, heirarchy.ROOT, _object, null);
670 }
671 else if (keys.length === 1)
672 {
673 // returning nested array or object
674 if (keys[0].indexOf(":") === 0)
675 {
676 key = keys[0].replace(":", "");
677 param = request.params[key];
678 if (Object.prototype.toString.call(_object) === '[object Array]')
679 {
680 found = false;
681 for (i in _object)
682 {
683 if ((_object[i] instanceof ObjectId && _object[i] == param) || _object[i]._id == param)
684 {
685 console.log("returning nested array object");
686 type = Object.prototype.toString.call(_object[i]);
687 switch (type)
688 {
689 case '[object Object]':
690 if (_object[i] instanceof ObjectId)
691 {
692 callback(null, heirarchy.LEAF, _object, i);
693 }
694 else
695 {
696 callback(null, heirarchy.BRANCH, _object, i);
697 }
698 break;
699
700 case '[object String]':
701 case '[object Date]':
702 case '[object Number]':
703 case '[object Boolean]':
704 callback(null, heirarchy.LEAF, _object, i);
705 break;
706
707 default:
708 respondError(response, 400, "nested property of type: " + type + " not supported");
709 callback("nested property of type: " + type + " not supported", null, null);
710 break;
711 }
712
713 found = true;
714 break;
715 }
716 }
717
718 if (!found)
719 {
720 console.log("object with id: " + param + " doesnt exist in array");
721 respondError(response, 404, "target object not found");
722 callback("object with id: " + param + " doesnt exist in array", null, null);
723 }
724 }
725 else
726 {
727 console.log("current item isnt an array, 400");
728 respondError(response, 400, "requested array operation on non array resource");
729 callback("requested array operation on non array resource, 400", null, null);
730 }
731 }
732 // fetching nested property
733 else
734 {
735 if (Object.prototype.toString.call(_object) === '[object Object]')
736 {
737 type = Object.prototype.toString.call(_object[keys[0]]);
738 switch (type)
739 {
740 case '[object Object]':
741 console.log("returning object property");
742 if(_object[keys[0]] instanceof ObjectId)
743 {
744 // represents an id, so actually a leaf
745 callback(null, heirarchy.LEAF, _object, keys[0]);
746 }
747 else
748 {
749 callback(null, heirarchy.BRANCH, _object, keys[0]);
750 }
751 break;
752
753 case '[object Array]':
754 console.log("returning object property");
755 callback(null, heirarchy.BRANCH, _object, keys[0]);
756 break;
757
758 case '[object String]':
759 case '[object Date]':
760 case '[object Number]':
761 case '[object Boolean]':
762 console.log("returning object property");
763 callback(null, heirarchy.LEAF, _object, keys[0]);
764 break;
765
766 default:
767 console.log("nested property of type: " + type + " not supported");
768 respondError(response, 400, "nested property of type: " + type + " not supported");
769 callback("nested property of type: " + type + " not supported", null, null);
770 break;
771 }
772 }
773 else
774 {
775 console.log("asked for property but no object found");
776 respondError(response, 400, "bad request");
777 callback("asked for property but no object found", null, null);
778 }
779 }
780 }
781 else
782 {
783 if (keys[0].indexOf(":") === 0)
784 {
785 key = keys[0].replace(":", "");
786 param = request.params[key];
787 if (Object.prototype.toString.call(_object) === '[object Array]')
788 {
789 found = false;
790 for (i in _object)
791 {
792 if (_object[i]._id == param)
793 {
794 console.log("recursing on nested array object");
795 traverseSubObjectHeirarchy(_object[i], keys.slice(1), callback);
796 found = true;
797 break;
798 }
799 }
800
801 if (!found)
802 {
803 console.log("uri specifies id, but array has no object with that id");
804 respondError(response, 404, "target object not found");
805 callback("uri specifies id, but array has no object with that id", null, null);
806 }
807 }
808 else if (_.isPlainObject(_object))
809 {
810 if (_object._id == param)
811 {
812 console.log("recursing on nested object");
813 traverseSubObjectHeirarchy(_object, keys.slice(1), callback);
814 }
815 else
816 {
817 console.log("asked for object with id that doesnt match");
818 respondError(response, 400, "bad request");
819 callback("asked for object with id that doesnt match", null, null);
820 }
821 }
822 else
823 {
824 console.log("uri specifies id, but object is neither array nor object");
825 respondError(response, 400, "bad request");
826 callback("uri specifies id, but object is neither array nor object", null, null);
827 }
828 }
829 else
830 {
831 if (!_object[keys[0]])
832 {
833 console.log("failed to find key: " + keys[0] + " in object");
834 respondError(response, 400, "bad request");
835 callback("failed to find key: " + keys[0] + " in object", null, null);
836 }
837 else
838 {
839 console.log("recursing with object at key: " + keys[0]);
840 traverseSubObjectHeirarchy(_object[keys[0]], keys.slice(1), callback);
841 }
842 }
843 }
844 };
845
846 traverseSubObjectHeirarchy(model, keys, callback);
847 }
848 };
849
850 var acre = {};
851
852 acre.options = {
853 rootPath: '',
854 putIsCreate: true,
855 adminPortal: true,
856 adminRoute: '/admin',
857 appName: 'Acre App',
858 adminPasswordSalt: '87fc9f12e96ad14e1899ff5e429d5b9066cb3aecb80bc62d52967662590dacd7'
859 };
860
861 acre.foreignKeys = {};
862
863 acre.CREATE = "acre.CREATE";
864 acre.RETRIEVE = "acre.RETRIEVE";
865 acre.UPDATE = "acre.UPDATE";
866 acre.REMOVE = "acre.REMOVE";
867
868 acre.PRE = "acre.PRE";
869 acre.POST = "acre.POST";
870 acre.OVERRIDE = "acre.OVERRIDE";
871 acre.PRIVATE = "acre.PRIVATE";
872
873 acre.bodyParser = function(app)
874 {
875 app.use(function(req, res, next){
876 if (req.is('text/*'))
877 {
878 req.text = '';
879 req.setEncoding('utf8');
880 req.on('data', function(chunk){
881 req.text += chunk;
882 });
883 req.on('end', next);
884 }
885 else
886 {
887 next();
888 }
889 });
890 };
891
892 acre.init = function(_mongoose, app, options)
893 {
894 /*
895 * initialize mongoose
896 */
897 mongoose = _mongoose;
898 ObjectId = mongoose.Types.ObjectId;
899
900 if (mongoose.version !== MIN_MONGOOSE_VERSION)
901 {
902 return when.reject('using incompatible mongoose version: ' + mongoose.version + ', acre requires: ' + MIN_MONGOOSE_VERSION);
903 }
904
905 /*
906 * initialize acre options
907 */
908 _.merge(this.options, options);
909
910 if (!this.options.putIsCreate)
911 {
912 createVerb = "post";
913 updateVerb = "put";
914 }
915
916 /*
917 * setup acre admin model
918 */
919 initializeAdminModel(app);
920
921
922 var promises = [], models = _.values(mongoose.models);
923
924 /*
925 * store foreign keys
926 */
927 storeForeignKeys(models);
928
929 /*
930 * setup REST CRUD paths for models
931 */
932 promises.push(serveModels(app, models));
933
934 /*
935 * setup admin portal
936 */
937 if (this.options.adminPortal)
938 {
939 promises.push(bindAdminRoutes(app, models));
940 }
941
942 return when.all(promises);
943 };
944
945 acre.pre = function(operation, path, callback)
946 {
947 var _path = toRegexp(path);
948 if ([acre.CREATE, acre.RETRIEVE, acre.UPDATE, acre.REMOVE].indexOf(operation) !== -1)
949 {
950 var userCallback = new UserCallback(operation, acre.PRE, _path, callback);
951 userCallbacks.push(userCallback);
952 }
953 else
954 {
955 throw operation + " is not a valid acre operation, must use one of: acre.CREATE, acre.RETRIEVE, acre.UPDATE, or acre.REMOVE";
956 }
957 };
958
959 acre.post = function(operation, path, callback)
960 {
961 var _path = toRegexp(path);
962 if ([acre.CREATE, acre.RETRIEVE, acre.UPDATE, acre.REMOVE].indexOf(operation) !== -1)
963 {
964 var userCallback = new UserCallback(operation, acre.POST, _path, callback);
965 userCallbacks.push(userCallback);
966 }
967 else
968 {
969 throw operation + " is not a valid acre operation, must use one of: acre.CREATE, acre.RETRIEVE, acre.UPDATE, or acre.REMOVE";
970 }
971 };
972
973 acre.override = function(operation, paths)
974 {
975 if (Object.prototype.toString.call(paths) !== '[object Array]')
976 {
977 paths = [paths];
978 }
979
980 for (var i in paths)
981 {
982 var path = toRegexp(paths[i]);
983 if ([acre.CREATE, acre.RETRIEVE, acre.UPDATE, acre.REMOVE].indexOf(operation) !== -1)
984 {
985 var userCallback = new UserCallback(operation, acre.OVERRIDE, path, true);
986 userCallbacks.push(userCallback);
987 }
988 else
989 {
990 throw operation + " is not a valid acre operation, must use one of: acre.CREATE, acre.RETRIEVE, acre.UPDATE, or acre.REMOVE";
991 }
992 }
993 };
994
995 acre.private = function(operation, paths)
996 {
997 if (Object.prototype.toString.call(paths) !== '[object Array]')
998 {
999 paths = [paths];
1000 }
1001
1002 for (var i in paths)
1003 {
1004 var path = toRegexp(paths[i]);
1005 if ([acre.CREATE, acre.RETRIEVE, acre.UPDATE, acre.REMOVE].indexOf(operation) !== -1)
1006 {
1007 var userCallback = new UserCallback(operation, acre.PRIVATE, path, true);
1008 userCallbacks.push(userCallback);
1009 }
1010 else
1011 {
1012 throw operation + " is not a valid acre operation, must use one of: acre.CREATE, acre.RETRIEVE, acre.UPDATE, or acre.REMOVE";
1013 }
1014 }
1015 };
1016
1017 var serveModels = function(app, Models)
1018 {
1019 var bindRoutes = function (uriPath, schemaPaths, resourceIsArray, Model) {
1020 var processedObjects = [];
1021 var resource = uriPath.split("/").pop();
1022 var nestedUriPath;
1023 if (resourceIsArray)
1024 {
1025 var modelName = (isModelRootPath(Model, uriPath)) ? Model.modelName : inflect.singularize(resource).toLowerCase();
1026 nestedUriPath = uriPath + "/:" + modelName + "_id";
1027 }
1028 else
1029 {
1030 nestedUriPath = uriPath;
1031 }
1032
1033 for (var path in schemaPaths)
1034 {
1035 var propertyDesc = Model.schema.paths[path];
1036 if (String(path).indexOf(".") !== -1)
1037 {
1038 // nested object detected
1039 var objName = path.split(".")[0];
1040 if (processedObjects.indexOf(objName) === -1)
1041 {
1042 var newPaths = {};
1043 for (var _path in schemaPaths)
1044 {
1045 if (_path.indexOf(objName+".") === 0)
1046 {
1047 newPaths[_path.replace(objName+".", "")] = schemaPaths[_path];
1048 }
1049 }
1050 processedObjects.push(objName);
1051 bindRoutes(nestedUriPath + "/" + objName, newPaths, false, Model);
1052 }
1053 }
1054 else if (schemaPaths[path].schema)
1055 {
1056 // nested array of objects detected
1057 bindRoutes(nestedUriPath + "/" + path, schemaPaths[path].schema.paths, true, Model);
1058 }
1059 else
1060 {
1061 // we do not want to expose any mods to _id attribute
1062 if (path !== '_id' && path !== '__v')
1063 {
1064 // property detected
1065 setRESTPaths(app, nestedUriPath + "/" + path, Model);
1066
1067 if (!_.isUndefined(propertyDesc) && !_.isUndefined(propertyDesc.options))
1068 {
1069 if (_.isArray(propertyDesc.options.type) && propertyDesc.options.type.length > 0)
1070 {
1071 if (!_.isUndefined(propertyDesc.options.type[0].ref))
1072 {
1073 // add REST paths for refs
1074 setRESTPaths(app, nestedUriPath + "/" + path + "/:" + propertyDesc.options.type[0].ref + "_id", Model);
1075 }
1076 else
1077 {
1078 // add REST paths for array of plain strings (this cld be ObjectIds, or strings)
1079 setRESTPaths(app, nestedUriPath + "/" + path + "/:" + inflect.singularize(path) + "_id", Model);
1080 }
1081 }
1082 }
1083 }
1084 }
1085 }
1086
1087 /**
1088 * CRUD for root resource
1089 */
1090 setRESTPaths(app, nestedUriPath, Model);
1091
1092 if (nestedUriPath != uriPath)
1093 {
1094 /**
1095 * CRUD for root
1096 */
1097 setRESTPaths(app, uriPath, Model);
1098 }
1099 };
1100
1101 for (var i in Models)
1102 {
1103 var Model = Models[i];
1104 console.log('serving model: ' + Model.modelName);
1105 var path = acre.options.rootPath + "/" + Model.collection.name;
1106 bindRoutes(path, Model.schema.paths, true, Model);
1107 }
1108
1109 return when.resolve();
1110 };
1111
1112 var bindAdminRoutes = function(app, models)
1113 {
1114 var i, j, k, routePath, viewPath;
1115 var jsPaths = [], cssPaths = [], _models = [];
1116
1117 var serveRoute = function()
1118 {
1119 return function(request, response){
1120 var routeData = dynamicRouteData[request.path];
1121 if (_.isUndefined(routeData))
1122 {
1123 respondError(response, 500, 'failed to find data for route: ' + request.originalUrl);
1124 }
1125 else
1126 {
1127 var viewPath = routeData.viewPath,
1128 contentType = routeData.contentType,
1129 options = routeData.options,
1130 modelName = routeData.modelName;
1131 function renderFile(options)
1132 {
1133 ejs.renderFile(viewPath, options, function(error, page){
1134 if (error)
1135 {
1136 respondError(response, 500, 'ejs failed to render page: " + viewPath + ", error: ' + error);
1137 }
1138 else
1139 {
1140 response.set('Content-Type', contentType);
1141 response.send(page);
1142 }
1143 });
1144 }
1145
1146 if (modelName)
1147 {
1148 // we have to load this everytime
1149 findModel(modelName)
1150 .then(
1151 function(Model){
1152 composeDefaultInstance(Model)
1153 .then(
1154 function(instances){
1155 options.instance = instances[0];
1156 options.formDesc = instances[1];
1157 renderFile(options);
1158 },
1159 function(err){
1160 respondError(response, 500, 'failed to compose default instance for: ' + modelName + ', err: ' + err);
1161 }
1162 );
1163 },
1164 function(err){
1165 respondError(response, 500, 'failed to find model: ' + modelName + ', err: ' + err);
1166 }
1167 );
1168 }
1169 else
1170 {
1171 renderFile(options);
1172 }
1173 }
1174 };
1175 };
1176
1177 var serveFile = function(path)
1178 {
1179 app.get(acre.options.adminRoute + path, function(request, response){
1180 response.sendfile(__dirname + '/portal/public' + path);
1181 });
1182
1183 return acre.options.adminRoute + path;
1184 };
1185
1186 cssPaths.push(serveFile('/css/bootstrap-combined.min.css'));
1187 cssPaths.push(serveFile('/css/acre.css'));
1188 jsPaths.push(serveFile('/js/angular.min.js'));
1189 jsPaths.push(serveFile('/js/angular-resource.min.js'));
1190 jsPaths.push(serveFile('/js/ui-bootstrap-tpls-0.3.0.min.js'));
1191 jsPaths.push(serveFile('/js/lodash.min.js'));
1192 jsPaths.push(serveFile('/js/restangular.min.js'));
1193 jsPaths.push(serveFile('/js/ng-form-direct.js'));
1194 jsPaths.push(serveFile('/js/ng-object-view.js'));
1195 jsPaths.push(serveFile('/js/inflect.min.js'));
1196 jsPaths.push(serveFile('/js/controller-utils.js'));
1197
1198 for (i in models)
1199 {
1200 var modelName = models[i].modelName;
1201 var createRoutePath = acre.options.adminRoute + "/" + modelName + "-create.html";
1202 var retrieveRoutePath = acre.options.adminRoute + "/" + modelName + "-retrieve.html";
1203 var updateRoutePath = acre.options.adminRoute + "/" + modelName + "-update.html";
1204 var controllersPath = acre.options.adminRoute + "/js/" + modelName + "-controllers.js";
1205
1206 var model = {
1207 name: modelName,
1208 module: {
1209 name: modelName
1210 },
1211 collection: models[i].collection.name,
1212 controllers: {
1213 create: modelName + "CreateController",
1214 retrieve: modelName + "RetrieveController",
1215 update: modelName + "UpdateController"
1216 },
1217 views: {
1218 create: createRoutePath,
1219 retrieve: retrieveRoutePath,
1220 update: updateRoutePath
1221 }
1222 };
1223
1224 dynamicRouteData[createRoutePath] = new DynamicRouteData(__dirname + '/portal/views/admin-model-create-view.ejs', 'text/html', {model: model}, modelName);
1225 dynamicRouteData[retrieveRoutePath] = new DynamicRouteData(__dirname + '/portal/views/admin-model-retrieve-view.ejs', 'text/html', {model: model}, null);
1226 dynamicRouteData[updateRoutePath] = new DynamicRouteData(__dirname + '/portal/views/admin-model-update-view.ejs', 'text/html', {model: model}, modelName);
1227 dynamicRouteData[controllersPath] = new DynamicRouteData(__dirname + '/portal/views/js/admin-model-controllers.ejs', 'text/javascript', {model: model, createVerb: createVerb.toUpperCase(), updateVerb: updateVerb.toUpperCase()}, modelName);
1228
1229 app.get(createRoutePath, serveRoute());
1230 app.get(retrieveRoutePath, serveRoute());
1231 app.get(updateRoutePath, serveRoute());
1232 app.get(controllersPath, serveRoute());
1233
1234 jsPaths.push(controllersPath);
1235 _models.push(model);
1236 }
1237
1238 routePath = acre.options.adminRoute + '/js/admin-controllers.js';
1239 dynamicRouteData[_.clone(routePath)] = new DynamicRouteData(__dirname + '/portal/views/js/admin-controllers.ejs', 'text/javascript', {models: _models}, null);
1240 app.get(_.clone(routePath), serveRoute());
1241 jsPaths.push(_.clone(routePath));
1242
1243 routePath = acre.options.adminRoute + "/js/admin-module.js";
1244 dynamicRouteData[_.clone(routePath)] = new DynamicRouteData(__dirname + '/portal/views/js/admin-module.ejs', 'text/javascript', {models: _models, apiRoute: acre.options.rootPath}, null);
1245 app.get(_.clone(routePath), serveRoute());
1246 jsPaths.push(_.clone(routePath));
1247
1248 routePath = acre.options.adminRoute;
1249 dynamicRouteData[_.clone(routePath)] = new DynamicRouteData(__dirname + '/portal/views/admin-home.ejs', 'text/html', {
1250 models: _models,
1251 js: jsPaths,
1252 css: cssPaths,
1253 apiRoute: acre.options.rootPath,
1254 appName: acre.options.appName
1255 }, null);
1256 app.get(_.clone(routePath), serveRoute());
1257
1258 return when.resolve();
1259 };
1260
1261 var create = function(Model, request, response, callback)
1262 {
1263 var path = request.route.path;
1264 console.log("create path: " + path);
1265
1266 if (isModelRootPath(Model, path))
1267 {
1268 Model.create(request.body, function(error, model){
1269 if (error)
1270 {
1271 respondMongooseError(response, error);
1272 }
1273 else
1274 {
1275 if (callback)
1276 {
1277 callback(request, response, model);
1278 }
1279 else
1280 {
1281 respondSuccess(response, "created " + Model.modelName);
1282 }
1283 }
1284 });
1285 }
1286 else
1287 {
1288 // permit addition of objects to arrays
1289 var modelId = request.params[Model.modelName + "_id"];
1290 Model.findById(modelId, function(error, model) {
1291 if (error)
1292 {
1293 respondMongooseError(response, error);
1294 }
1295 else
1296 {
1297 if (!model)
1298 {
1299 respondError(response, 404, "failed to find " + Model.modelName + " with id: " + modelId);
1300 }
1301 else
1302 {
1303 traverseObjectHeirarchy(model, request, response, function(error, object_heirarchy, parent, key){
1304 if (error)
1305 {
1306 console.log("error occured : " + error);
1307 }
1308 else
1309 {
1310 if (_.isArray(parent[key]))
1311 {
1312 if (request.is('text/*'))
1313 {
1314 if (!_.isUndefined(request.text))
1315 {
1316 // adding to array of refs
1317 parent[key].push(request.text);
1318 }
1319 else
1320 {
1321 console.log("request.text undefined, acre bodyParser probably not set up");
1322 respondError(response, 500, "request.text undefined, acre bodyParser not setup");
1323 }
1324 }
1325 else
1326 {
1327 // adding nested resource to nested array
1328 parent[key].push(request.body);
1329 }
1330
1331 model.save(function(error, model){
1332 if (error)
1333 {
1334 console.log("error saving object to: " + request.originalUrl + " error: " + error.message);
1335 respondMongooseError(response, error);
1336 }
1337 else
1338 {
1339 if (callback)
1340 {
1341 callback(request, response, model);
1342 }
1343 else
1344 {
1345 respondSuccess(response, "create done");
1346 }
1347 }
1348 });
1349 }
1350 else
1351 {
1352 console.log("cannot " + createVerb + " to " + request.originalUrl);
1353 respondError(response, 405, "cannot " + createVerb + " to " + request.originalUrl);
1354 }
1355 }
1356 });
1357 }
1358 }
1359 });
1360 }
1361 };
1362
1363 var retrieve = function(Model, request, response, callback)
1364 {
1365 var path = request.route.path;
1366 console.log("retrieve path: " + path);
1367 var handleFind, hndl = null;
1368
1369 if (isModelRootPath(Model, path))
1370 {
1371 handleFind = function(error, models)
1372 {
1373 if (error)
1374 {
1375 respondMongooseError(response, error);
1376 }
1377 else
1378 {
1379 if (!models)
1380 {
1381 models = [];
1382 }
1383
1384 if (callback)
1385 {
1386 callback(request, response, models);
1387 }
1388 else
1389 {
1390 respondJson(response, models);
1391 }
1392 }
1393 };
1394
1395 try
1396 {
1397 if (request.query.q)
1398 {
1399 var query = new Buffer(request.query.q, 'base64').toString('ascii');
1400 query = JSON.parse(query);
1401 console.log('query: ');
1402 console.log(query);
1403 hndl = Model.find(query);
1404 }
1405 else
1406 {
1407 hndl = Model.find();
1408 }
1409
1410 if (request.query.l)
1411 {
1412 var limit = parseInt(request.query.l);
1413 console.log('limit: ' + limit);
1414 hndl.limit(limit);
1415 }
1416
1417 if (request.query.srt)
1418 {
1419 var sort = request.query.srt;
1420 console.log('sort: ' + sort);
1421 hndl.sort(sort);
1422 }
1423 }
1424 catch(e)
1425 {
1426 hndl = null;
1427 respondError(response, 400, 'Bad Request');
1428 }
1429 }
1430 else
1431 {
1432 var modelId = request.params[Model.modelName + "_id"];
1433
1434 handleFind = function(error, model)
1435 {
1436 if (error)
1437 {
1438 console.log("failed to find model with id: " + modelId);
1439 respondMongooseError(response, error);
1440 }
1441 else
1442 {
1443 if (!model)
1444 {
1445 console.log("failed to find model with id: " + modelId);
1446 respondError(response, 404, "failed to find " + Model.modelName + " with id: " + modelId);
1447 }
1448 else
1449 {
1450 traverseObjectHeirarchy(model, request, response, function(error, object_heirarchy, parent, key){
1451 if (error)
1452 {
1453 console.log("error occred: " + error);
1454 }
1455 else
1456 {
1457 var object = (key) ? parent[key] : parent;
1458 if (callback)
1459 {
1460 callback(request, response, object);
1461 }
1462 else
1463 {
1464 //TODO for some reason _.isPlainObject and _.isArray always return false here
1465 switch (Object.prototype.toString.call(object))
1466 {
1467 case '[object Array]':
1468 case '[object Object]':
1469 respondJson(response, object);
1470 break;
1471
1472 default:
1473 respondSuccess(response, object);
1474 break;
1475 }
1476 }
1477 }
1478 });
1479 }
1480 }
1481 };
1482
1483 var hndl = Model.findById(modelId);
1484 }
1485
1486 if (!_.isUndefined(acre.foreignKeys[Model.modelName]) && _.isPlainObject(acre.foreignKeys[Model.modelName]))
1487 {
1488 var refs = _.keys(acre.foreignKeys[Model.modelName]);
1489 _.each(refs, function(ref){
1490 hndl.populate(ref);
1491 });
1492 }
1493
1494 if (hndl)
1495 {
1496 hndl.exec(handleFind);
1497 }
1498 };
1499
1500 var update = function(Model, request, response, callback)
1501 {
1502 var path = request.route.path;
1503 console.log("update path: " + path);
1504
1505 if (isModelRootPath(Model, path))
1506 {
1507 respondError(response, 405, "not allowed to " + updateVerb + " to " + Model.collection.name + ", must only update leaf elements");
1508 }
1509 else
1510 {
1511 var modelId = request.params[Model.modelName + "_id"];
1512 Model.findById(modelId, function(error, model) {
1513 if (error)
1514 {
1515 respondMongooseError(response, error);
1516 }
1517 else
1518 {
1519 if (!model)
1520 {
1521 respondError(response, 404, "failed to find " + Model.modelName + " with id: " + modelId);
1522 }
1523 else
1524 {
1525 traverseObjectHeirarchy(model, request, response, function(error, object_heirarchy, parent, key){
1526 if (error)
1527 {
1528 console.log("error occured: " + error);
1529 }
1530 else
1531 {
1532 if (object_heirarchy === heirarchy.LEAF && !_.isArray(parent))
1533 {
1534 if (!_.isUndefined(request.text))
1535 {
1536 parent[key] = request.text;
1537 model.save(function(error, model){
1538 if (error)
1539 {
1540 console.log("error occured updating object at path: " + request.originalUrl + " error: " + error.message);
1541 respondMongooseError(response, error);
1542 }
1543 else
1544 {
1545 console.log("update done");
1546 if (callback)
1547 {
1548 callback(request, response, model);
1549 }
1550 else
1551 {
1552 respondSuccess(response, "update sucessful");
1553 }
1554 }
1555 });
1556 }
1557 else
1558 {
1559 console.log("request.text undefined, acre bodyParser probably not set up");
1560 respondError(response, 500, "request.text undefined, acre bodyParser not setup");
1561 }
1562 }
1563 else
1564 {
1565 // not permitted to update a string in an array (eg a ref)
1566 console.log("update not allowed to: " + request.originalUrl);
1567 respondError(response, 405, "update not allowed to: " + request.originalUrl);
1568 }
1569 }
1570 });
1571 }
1572 }
1573 });
1574 }
1575 };
1576
1577 var remove = function(Model, request, response, callback)
1578 {
1579 var path = request.route.path;
1580 console.log("delete path: " + path);
1581
1582 if (isModelRootPath(Model, path))
1583 {
1584 // delete all resources
1585 Model.remove(function(error, models){
1586 if (error)
1587 {
1588 respondMongooseError(response, error);
1589 }
1590 else
1591 {
1592 if (callback)
1593 {
1594 callback(request, response, models);
1595 }
1596 else
1597 {
1598 respondSuccess(response, "deleted all " + Model.collection.name);
1599 }
1600 }
1601 });
1602 }
1603 else
1604 {
1605 var modelId = request.params[Model.modelName + "_id"];
1606 if (isModelRootResourcePath(Model, path))
1607 {
1608 // delete single resource
1609 Model.findByIdAndRemove(modelId, function(error, model){
1610 if (error)
1611 {
1612 console.log('failed to delete: ' + Model.modelName + ' with id: ' + modelId + ', error: ');
1613 console.log(error);
1614 respondMongooseError(response, error);
1615 }
1616 else
1617 {
1618 if (callback)
1619 {
1620 callback(request, response, model);
1621 }
1622 else
1623 {
1624 respondSuccess(response, "deleted " + Model.modelName + " at id: " + modelId);
1625 }
1626 }
1627 });
1628 }
1629 else
1630 {
1631 // permit deletion of objects from arrays
1632 Model.findById(modelId, function(error, model) {
1633 if (error)
1634 {
1635 console.log("failed to find object with id: " + modelId + ", error: " + error.message);
1636 respondMongooseError(response, error);
1637 }
1638 else
1639 {
1640 if (!model)
1641 {
1642 respondError(response, 404, "failed to find " + Model.modelName + " with id: " + modelId);
1643 }
1644 else
1645 {
1646 traverseObjectHeirarchy(model, request, response, function(error, object_heirarchy, parent, key){
1647 if (error)
1648 {
1649 console.log("error occured: " + error);
1650 }
1651 else
1652 {
1653 if (_.isArray(parent))
1654 {
1655 parent.splice(key, 1);
1656 model.save(function(error, model){
1657 if (error)
1658 {
1659 console.log("failed to delete array item, error: " + error.message);
1660 respondMongooseError(response, error);
1661 }
1662 else
1663 {
1664 console.log("delete done");
1665 if (callback)
1666 {
1667 callback(request, response, model);
1668 }
1669 else
1670 {
1671 respondSuccess(response, "delete done");
1672 }
1673 }
1674 });
1675 }
1676 else
1677 {
1678 console.log("unsupported delete operation");
1679 respondError(response, 405, "cannot delete " + request.originalUrl);
1680 }
1681 }
1682 });
1683 }
1684 }
1685 });
1686 }
1687 }
1688 };
1689
1690 var isModelRootPath = function(Model, path)
1691 {
1692 return acre.options.rootPath + "/" + Model.collection.name === path;
1693 };
1694
1695 var isModelRootResourcePath = function(Model, path)
1696 {
1697 return acre.options.rootPath + "/" + Model.collection.name + "/:" + Model.modelName + "_id" === path;
1698 };
1699
1700 var respondSuccess = function(response, str) {
1701 response.send(200, String(str));
1702 };
1703
1704 var respondJson = function(response, json) {
1705 response.json(json);
1706 };
1707
1708 var respondMongooseError = function(response, err)
1709 {
1710 var regexps = [
1711 /validation failed/gi,
1712 /duplicate key error/gi
1713 ];
1714 var code = 500;
1715
1716 for (i in regexps)
1717 {
1718 if (regexps[i].test(err.message))
1719 {
1720 code = 400;
1721 break;
1722 }
1723 }
1724
1725 response.send(code, err);
1726 };
1727
1728 var respondError = function(response, code, err)
1729 {
1730 response.send(code, err);
1731 };
1732
1733 module.exports = acre;
1734}());
\No newline at end of file