NateFerrero/amna

View on GitHub
src/lib/collection.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

/**
 * AMNA: Amazing Mongoose Node.js API
 *
 * @author Nate Ferrero
 * @url https://github.com/NateFerrero/amna
 *
 * AMNA - Collection
 */
var q = require('q');

module.exports = function (amna) {

    amna.mongoId = function (param) {
        return ':' + param + '([0-9a-f]{24})';
    };

    /**
     * Handle pagination and deleted records
     */
    amna.pageAndDeleteQuery = function (self) {
        var query = {};
        var qOpts = self.req.jsonQueryOptions;

        /**
         * Populate for collectionGet
         */
        if ('populate' in qOpts) {
            self.req.populate = qOpts.populate.map(function (pop) {
                return Array.isArray(pop) ? pop : [pop];
            });
            delete qOpts.populate;
        }

        /**
         * Check for allowed keys in JSON Query options
         */
        var defaultOptions = {'limit': 20, 'page': 1, 'deleted': false, 'sort': null};
        var keys = Object.keys(defaultOptions);
        Object.keys(qOpts).forEach(function (key) {
            if (keys.indexOf(key) === -1) { // key not allowed
                self.err('JSON query option \'.\' is not valid'.replace('.', key));
            }
            else if (qOpts[key] === undefined || qOpts[key] === null) {
                qOpts[key] = defaultOptions[key];
            }
            else if (typeof defaultOptions[key] === 'number' &&
                    (typeof qOpts[key] !== 'number' ||
                        qOpts[key] % 1 !== 0 ||
                        qOpts[key] < 0)
                ) {
                self.err('JSON query option \'.\' must be a positive integer'.replace('.', key));
            }
        });

        Object.keys(defaultOptions).forEach(function (key) {
            if (!qOpts.hasOwnProperty(key)) {
                qOpts[key] = defaultOptions[key];
            }
        });

        if (qOpts.page < 1) {
            self.err('JSON query option \'page\' must be greater than or equal to 1');
        }

        /**
         * Default: do not include deleted documents
         */
        if (qOpts.deleted === false) {
            query.deleted = {$ne: true};
        }

        /**
         * Show only deleted documents
         */
        else if (qOpts.deleted === true) {
            query.deleted = true;
        }

        return {
            query: query,
            options: {
                limit: qOpts.limit,
                skip: (qOpts.page - 1) * qOpts.limit,
                sort: qOpts.sort
            }
        };
    };

    var setupCollectionSchema = function (collection, options) {
        /**
         * GET /collection/schema
         */
        if (options.collectionGetSchema !== false) {
            collection.routes.collectionGetSchema = collection.collectionGet('/schema', function (self) {
                self.done(collection.thing.jsonSchema);
            });
        }
    };

    var setupCollectionAutocomplete = function (collection, options) {
        /**
         * GET /collection/autocomplete
         */
        if (options.collectionGetAutocomplete !== false) {
            collection.routes.collectionGetAutocomplete = collection.collectionGet('/autocomplete', function (self) {
                var info = amna.pageAndDeleteQuery(self);
                Object.keys(info.query).forEach(function (key) {
                    self.req.jsonQuery[key] = info.query[key];
                });
                collection.thing.autocomplete(self.req.jsonQuery, info.options, self.noerr(self.done));
            });
        }
    };

    var setupCollectionGet = function (collection, options) {
        /**
         * GET /collection
         */
        if (options.collectionGet !== false) {
            collection.routes.collectionGet = collection.collectionGet('', function (self) {
                if (self.value) {
                    return self.done(self.value);
                }
                var info = amna.pageAndDeleteQuery(self);
                Object.keys(info.query).forEach(function (key) {
                    self.req.jsonQuery[key] = info.query[key];
                });
                var query = collection.thing.find(self.req.jsonQuery, info.options);
                if (Array.isArray(self.req.populate)) {
                    self.req.populate.forEach(function (args) {
                        query.populate.apply(query, args);
                    });
                }
                query.exec(self.noerr(self.done));
            });
        }
    };

    var setupCollectionPost = function (collection, options) {
        /**
         * POST /collection
         */
        if (!options.readOnly && options.collectionPost !== false) {
            collection.routes.collectionPost = collection.collectionPost('', function (self) {
                var Model = collection.thing.model;

                var filter = function (data) {
                    /**
                     * Create blacklist
                     */
                    delete self.req.body._id;
                    delete self.req.body.__v;
                    delete self.req.body.updatedAt;
                    delete self.req.body.createdAt;
                    delete self.req.body.createdBy;

                    /**
                     * Add correct user
                     */
                    data.createdBy = self.req.user;
                }

                /**
                 * Handle POST [{...}, ...]
                 */
                if (Array.isArray(self.req.body)) {
                    q.all(self.req.body.map(function (data) {
                        var deferred = q.defer();
                        filter(data);
                        new Model(data).save(function (err, doc) {
                            deferred.resolve(
                                err ? amna.responses.error(err.message) : amna.responses.ok(doc)
                            );
                        });
                        return deferred.promise;
                    }))
                    .done(self.raw);
                }

                /**
                 * Handle POST {...}
                 */
                else {
                    var data = self.req.body;
                    filter(data);
                    new Model(data).save(self.noerr(self.done));
                }
            });
        }
    };

    var setupCollectionPut = function (collection, options) {
        /**
         * PUT /collection
         */
        if (!options.readOnly && options.collectionPut !== false) {
            collection.routes.collectionPut = collection.collectionPut('', function (self) {
                self.err('Not Implemented');
            });
        }
    };

    var setupCollectionDelete = function (collection, options) {
        /**
         * DELETE /collection
         */
        if (!options.readOnly && options.collectionDelete !== false) {
            collection.routes.collectionDelete = collection.collectionDelete('', function (self) {
                self.err('Not Implemented');
            });
        }
    };

    var setupDocumentGet = function (collection, options) {
        /**
         * GET /collection/:id
         */
        if (options.documentGet !== false) {
            collection.routes.documentGet = collection.documentGet('', function (self) {
                self.done(self.doc);
            });
        }
    };

    var setupDocumentPost = function (collection, options) {
        /**
         * POST /collection/:id
         */
        if (!options.readOnly && options.documentPost !== false) {
            collection.routes.documentPost = collection.documentPost('', function (self) {
                self.err('Not Implemented');
            });
        }
    };

    var setupDocumentPut = function (collection, options) {
        /**
         * PUT /collection/:id
         */
        if (!options.readOnly && options.documentPut !== false) {
            collection.routes.documentPut = collection.documentPut('', function (self) {
                /**
                 * Ensure that the user has access to the doc
                 */
                if (!self.permissionGranted &&
                    (!self.doc.createdBy || !self.req.user._id.equals(self.doc.createdBy))) {
                    return self.noaccess();
                }

                /**
                 * Update blacklist
                 */
                delete self.req.body._id;
                delete self.req.body.__v;
                delete self.req.body.updatedAt;
                delete self.req.body.createdAt;
                delete self.req.body.createdBy;

                /**
                 * Update the document
                 */
                Object.keys(self.req.body).forEach(function (key) {
                    self.doc[key] = self.req.body[key];
                });
                self.doc.updatedAt = Date.now()
                self.doc.save(self.noerr(self.done));
            });
        }
    };

    var setupDocumentDelete = function (collection, options) {
        /**
         * DELETE /collection/:id
         */
        if (!options.readOnly && options.documentDelete !== false) {
            collection.routes.documentDelete = collection.documentDelete('', function (self) {
                /**
                 * Ensure that the user has access to the document
                 */
                if (!self.permissionGranted &&
                    (!self.doc.createdBy || !self.req.user._id.equals(self.doc.createdBy))) {
                    return self.noaccess();
                }

                /**
                 * Delete the document
                 */
                self.doc.deleted = true;
                self.doc.save(self.noerr(self.done));
            });
        }
    }

    var Collection = function (thing, options) {
        if (!thing) {
            throw new Error('collection requires a thing');
        }

        if (!options || typeof options !== 'object') {
            options = {}
        }

        this.routes = {};
        this.thing = thing;
        this.controller = amna.controller();

        this.preStack = amna.stack();

        setupCollectionSchema(this, options);
        setupCollectionAutocomplete(this, options);
        setupCollectionGet(this, options);
        setupCollectionPost(this, options);
        setupCollectionPut(this, options);
        setupCollectionDelete(this, options);
        setupDocumentGet(this, options);
        setupDocumentPost(this, options);
        setupDocumentPut(this, options);
        setupDocumentDelete(this, options);
    };

    Collection.prototype.pre = function (handler) {
        this.preStack.push(handler);
    };

    Collection.prototype.__defineGetter__('$repr', function () {
        return '<Collection ' + this.thing.$repr + '>';
    });

    ['Get', 'Post', 'Put', 'Delete'].forEach(function (name) {
        var method = name.toLowerCase();

        /**
         * Register authenticated and unauthenticated collection route methods
         */
        ['collection', 'unauthenticatedCollection'].forEach(function (realm) {
            Collection.prototype[realm + name] = function (url, handler) {
                var collection = this;
                var route = collection.controller[method](url, function (self) {
                    /**
                     * Ensure that there is a user present for normal (as opposed to unauthenticated) routes
                     */
                    if (realm === 'collection' && !self.req.user) {
                        return self.noauth();
                    }

                    /**
                     * Run the collection pre stack
                     */
                    collection.preStack.run(self, function () {
                        /**
                         * Run the mid stack
                         */
                        route.midStack.run(self, function () {
                            /**
                             * Continue with route handler
                             */
                            handler(self);
                        });
                    });
                });
                return route;
            };
        });

        /**
         * Register authenticated and unauthenticated document route methods
         */
        ['document', 'unauthenticatedDocument'].forEach(function (realm) {
            Collection.prototype[realm + name] = function (url, handler) {
                var collection = this;
                var route = collection.controller[method]('/' + amna.mongoId('id') + url, function (self) {
                    /**
                     * Ensure that there is a user present for normal (as opposed to unauthenticated) routes
                     */
                    if (realm === 'document' && !self.req.user) {
                        return self.noauth();
                    }

                    /**
                     * Run the collection pre stack
                     */
                    collection.preStack.run(self, function () {
                        /**
                         * Find the document
                         */
                        collection.thing.model.findById(self.params.id, self.noerr(function (doc) {
                            /**
                             * If no doc found
                             */
                            if (!doc) {
                                return self.notfound();
                            }

                            /**
                             * Add doc to the interaction
                             */
                            self.doc = doc;

                            /**
                             * Run the mid stack
                             */
                            route.midStack.run(self, function () {
                                /**
                                 * Continue with route handler
                                 */
                                handler(self);
                            });
                        }));
                    });
                });
                return route;
            };
        });
    });

    Collection.prototype.register = function register(prefix) {
        this.controller.register(prefix);
    };

    return function collection(thing, options) {
        return new Collection(thing, options);
    };
};