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.