Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I'm building a NodeJS application, but I'm not sure what will be the best way to structure my code.

I have the following model:

/**
 * Module loading
 */

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var ShortId = require('mongoose-minid');
var async = require('async');
var regex = require('../utils/regex');

/**
 * Define user schema
 */

var NodesSchema = new Schema({
    // Instead of ObjectID, we'll be using custom ShortID plugin, so the node IDs will be much shorter
    _id: {
        type: ShortId,
        len: 7,
        unique: true
    },

    // node(file/folder) name
    name: {
        type: String,
        required: true,
        validate: [
            regex.fileName,
            'INVALID_NAME'
        ]
    },

    // is the node file or folder.
    // if file, a "data" field should be specified
    isFile: {
        type: Boolean,
        required: true
    },

    // We're saving both id and username of the user,
    // so there is no need to perform second query for username,
    // since the user renames are less likely to occur
    owner: {
        _id: {
            type: ShortId,
            required: true
        },
        username: {
            type: 'String',
            required: true
        }
    },

    // Again, we're saving both id and username of the user,
    // so an addition query won't be needed to get the usernames
    sharedWith: [{
        _id: {
            type: ShortId,
        },
        username: {
            type: String
        }
    }],

    // Can the node be accessed from user without rights/not logged in
    isPublic: {
        type: Boolean,
        default: false
    },

    // We're saving both id and location name, see @owner field for reference
    location: {
        _id: {
            type: ShortId,
            default: null,
        },
        // we have an array of all parents, so we can know the full path to the file anytime,
        // without recursive queries and additional computation
        parents: [{
            _id: {
                type: ShortId,
                default: null
            },
            name: {
                type: String,
                default: null
            },
        }]
    },

    // Optional field for files
    // it has TEXT INDEX
    data: {
        type: String
    },

    // Optional field for files
    sourceDocument: {
        type: String,
    },

    // Log creation date
    createdAt: {
        type: Date,
        default: Date.now
    }
});

/**
 * Get all items in the requested directory, excluding deep childs
 * if no ID is presented it gets all items from root user directoory
 *
 * @param {String} folderid
 * @param {String} userid
 * @param {Function} response callback
 * @api public
 */

NodesSchema.statics.getDirectoryFiles = function(folderid, userid, callback) {
    var query = {
        'location._id': null,
        'owner._id': userid
    };

    if (folderid) {
        query['location._id'] = folderid;
    }

    this.find(query, callback);
};

/**
 * Deletes an folder/file
 * If the ID is folder, it will do a recursive delete and it will delete
 * everything within this node and its childs
 * If the ID is file, it will just delete it and the queue won't be executed
 *
 * @param {String} id - NodeID
 * @param {Boolean} isFile - should it perform a recursive delete or no
 * @param {Function} response callback
 * @api public
 */

NodesSchema.statics.recursiveDelete = function(id, isFile, callback) {
    // add the delete request item to the delete list, so it's always deleted
    var deletequeue = [id];
    var _this = this;

    // it will find all items in the current location
    // if the items are folders it will add them back to the queue,
    // so we can get their children and make sure all items are deleted
    // if the node is file, the queue won't be executed
    var q = async.queue(function(task, queue_callback) {
        _this.find({
            'location._id': task._id
        }, function(err, data) {
            if (err) {
                q.kill();
                return;
            }

            data.forEach(function(i) {
                deletequeue.push(i._id);

                //add to queue
                if (!i.isFile) {
                    q.push({
                        _id: i._id
                    });
                }
            });

            queue_callback();
        });
    }, 1);


    // when the queue is done, delete all found items
    q.drain = function() {
        q.kill();
        _this.remove({
            _id: {
                $in: deletequeue
            }
        }, callback);
    }

    if (isFile) {
        // if it's file we dont need to even enter the queue and the first element in the array 
        // is already the item id, so we just call drain to remove the current file
        q.drain();
    } else {
        // push the folderId to the queue
        q.push({
            _id: id
        });
    }
};


/**
 * Waterfall file creating
 * It will do a series of checks to make sure:
 *     - location exists
 *     - such file doesn't already exists
 *     - creation completed successfully
 * It processes to next waterfall level, when the callback is called
 *
 * @param {Object} opts - object with file specifications, data, location, etc
 * @param {Object} user - object of the current logged user
 * @param {Function} response callback when the waterfall completes
 * @api public
 */

NodesSchema.statics.createFile = function(opts, user, cb) {
    var _this = this;

    async.waterfall([

        //returns a function, avoid code dublication with file/folder create
        locationExists(opts, _this),

        //returns a function, avoid code dublication with file/folder create
        itemExists(opts, _this, user),

        getLocationTree(_this),

        //if location exist, and item with such name doesn't exists, create new item
        function(locationData, callback) {
            var currentFile = new NodesModel({
                name: opts.name,
                location: locationData,
                owner: {
                    _id: user._id,
                    username: user.username
                },
                data: opts.data.text,
                isFile: true
            });

            currentFile.save(callback);
        }
    ], cb);
};

/**
 * Waterfall folder creating
 * It will do a series of checks to make sure:
 *     - location exists
 *     - such file doesn't already exists
 *     - creation completed successfully
 * It processes to next waterfall level, when the callback is called
 *
 * @param {Object} opts - object with folder specifications, location, etc
 * @param {Object} user - object of the current logged user
 * @param {Function} response callback when the waterfall completes
 * @api public
 */

NodesSchema.statics.createFolder = function(opts, user, cb) {
    var _this = this;

    async.waterfall([

        //returns a function, avoid code dublication with file/folder create
        locationExists(opts, _this),

        //returns a function, avoid code dublication with file/folder create
        itemExists(opts, _this, user),

        getLocationTree(_this),

        //if location exist, and item with such name doesn't exists, create new item
        function(locationData, callback) {
            var currentFolder = new NodesModel({
                name: opts.name,
                location: locationData,
                owner: {
                    _id: user._id,
                    username: user.username
                },
                isFile: false
            });

            currentFolder.save(callback);
        }
    ], cb);
};


var getLocationTree = function(db) {
    return function(locationData, callback) {
        var path = [];

        var complete = function() {
            callback(null, {
                _id: locationData._id,
                parents: path.reverse()
            });
        };

        if (locationData._id == null) {
            return complete();
        }

        var q = async.queue(function(task, queue_callback) {
            var query = db.findOne({
                _id: task._id
            });

            query.exec(function(err, data) {
                if (err) {
                    q.kill();
                    return callback(err);
                }

                path.push({
                    _id: data._id,
                    name: data.name
                });

                if (data.location._id !== null) {
                    q.push({
                        _id: data.location._id
                    });
                }

                queue_callback();
            });
        }, 1);

        q.drain = complete;

        q.push({
            _id: locationData._id
        });
    }
};

/**
 * Used to check if the save location exists
 * Location check is separated in function to avoid code dublication in both waterfalls
 * Accepts options and db object and returns functions
 *
 * @param {Object} opts - object with folder specifications, location, etc
 * @param {Object} db   - Mongoose object
 * @returns {Function}
 * @api private
 */

var locationExists = function(opts, db) {

    return function(callback) {
        if (opts.location === null) {
            return callback(null, {
                _id: null,
                name: null
            });
        }

        db.findOne({
            _id: opts.location
        }, function(err, locationData) {
            if (err) {
                return callback(err);
            }

            if (!locationData) {
                return callback('INVALID_LOCATION');
            }

            callback(null, locationData);
        });
    };

};

/**
 * Checks if exact file exists
 * Item exists check is separated in function to avoid code dublication in both waterfalls
 * Accepts options,db and user object and returns functions
 *
 * @param {Object} opts - object with folder specifications, location, etc
 * @param {Object} db   - Mongoose object
 * @param {Object} user - user object
 * @returns {Function}
 * @api private
 */

var itemExists = function(opts, db, user) {
    return function(locationData, callback) {
        db.findOne({
            'location._id': opts.location,
            'owner._id': user._id,
            name: opts.name
        }, function(err, data) {
            if (err) {
                return callback(err);
            }

            if (data) {
                return callback('ITEM_EXISTS');
            }

            callback(null, locationData);
        });
    }
};


/**
 * Renames given folder and changes all name references to it
 * Renames give file
 * If step 1(change folder name completes successfully), then step 2(change folder name reference) is executed.
 * When both complete, a callback is called
 *
 * @param  {ObjectId}   id - id of the folder
 * @param  {String}   newName - new name of the folder
 * @param  {ObjectId}   userid - current user id
 * @param  {Function} callback - callback funtion
 */
NodesSchema.statics.rename = function(id, newName, userid, callback) {
    var _this = this;

    async.waterfall([

        function(innerCallback) {

            _this.findOne({
                _id: id,
                'owner._id': userid
            }, function(err, data) {
                if (!data) {
                    return innerCallback('ITEM_NOT_FOUND');
                }

                data.name = newName;
                data.save(function(err, data) {
                    if (err) {
                        return innerCallback(err);
                    }

                    innerCallback(null, data.isFile);
                });
            });

        },
        function(isFile, innerCallback) {
            if (isFile) {
                return innerCallback(null);
            }

            _this.update({
                'location._id': id
            }, {
                'location.name': newName
            }, {
                multi: true
            }, function(err, status) {
                if (err) {
                    return innerCallback(err);
                }
            });

            innerCallback(null);
        }
    ], callback);
};


/**
 * Creates model from schema
 */
var NodesModel = mongoose.model('nodes', NodesSchema);


/**
 * Exports model
 */
module.exports = NodesModel;

Take a look at the NodesSchema.statics.createFolder.

I'm calling the createFolder from the controller like this:

app.post('/api/nodes/createfolder', utils.shouldBeLoggedIn, function(req, res) {

        // if no location is presented, set location to users root directory
        var location = (!req.body.location || req.body.location == 'null') ? null : req.body.location;
        var name = req.body.name;

        // Mongoose will handle validation, no need to add it here
        NodesModel.createFolder({
            location: location,
            name: name
        }, req.user, function(err, data) {
            if (err) {
                return res.status(400).json({
                    message: utils.getVerbalizedError(err)
                });
            }

            res.json({
                message: 'FOLDER_CREATED_SUCCESSFUL'
            });
        });
    });

Should I move the code from the waterfall to Mongoose pre-save middleware like this?

NodesSchema.pre('remove', function(next) {
    // check for location if its valid

    // then run getLocationTree\
    // if error: return next(err);

    next();
});

And create folders from controller like this?

var model = new NodesModel({
       params: params
});

model.save(function(err, data){ 
    // here the err will not be null if pre-save fails
});

I would also like to know if I'm doing something wrong.

share|improve this question

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.