"use strict";
var Cursor = require("../cursor"),
Provider = require("../provider"),
CollErr = require("../utils/collective-error"),
Err = require("../utils/error"),
object = require("../utils/object"),
util = require("util"),
sift = require("sift"),
path = require("path"),
uuid = require("node-uuid"),
fs = require("fs"),
async = require("async"),
_ = require("lodash");
function FsCursor(provider, query, options) {
this._files = null;
if (query) {
this.sifter = sift(query);
}
Cursor.call(this, provider, query, options);
this.reset();
}
util.inherits(FsCursor, Cursor);
FsCursor.prototype.reset = function () {
this.current = this.skipValue;
if (this.current !== 0 && this.limitValue !== 0) {
this.limitValue += this.current;
}
if (this.mapping && !_.isArray(this.mapping)) {
if (_.isObject(this.mapping)) {
this.mapping = _.keys(this.mapping);
} else if (_.isString(this.mapping)) {
this.mapping = [this.mapping];
} else if (!_.isFunction(this.mapping)) {
throw new Err("UNSUP_MAP_TYPE");
}
}
return this;
};
FsCursor.prototype._isMatch = function (item) {
if (this.sifter) {
return this.sifter.test(item);
}
return true;
};
FsCursor.prototype._map = function (item) {
if (item && this.mapping) {
if (_.isFunction(this.mapping)) {
return this.mapping(item);
}
return _.pick.apply(null, [item].concat(this.mapping));
}
return item;
};
FsCursor.prototype._nextObject = function (callback) {
var self = this;
function nextItem() {
var item, file;
if (self.limitValue === 0 || self.current < self.limitValue) {
file = self._files[self.current++];
while (file) {
item = require(path.join(self.provider.dir, file));
if (self._isMatch(item)) {
break;
}
item = null;
file = self._files[self.current++];
}
}
callback(null, self._map(item));
}
if (!self._files) {
fs.readdir(self.provider.dir, function (err, files) {
if (err) {
return callback(err);
}
self._files = files;
nextItem();
});
} else {
nextItem();
}
};
FsCursor.prototype.count = function (callback) {
var that = this;
that.exec(function (err) {
if (err) {
callback(err);
} else {
if (that.items) {
callback(null, that.items.length);
} else {
if (that.query) {
that.toArray(function (err, list) {
if (err) {
return callback(err);
}
return callback(err, list.length);
});
} else {
fs.readdir(that.provider.dir, function (err, files) {
if (err) {
return callback(err);
}
that._files = files;
callback(null, that._files.length);
});
}
}
}
});
return this;
};
FsCursor.prototype._exec = function (callback) {
var that = this;
if (this.criteria) {
this.toArray(function (err, list) {
if (!err) {
if (_.isFunction(that.criteria)) {
list.sort(that.criteria);
} else {
list.sort(function (a, b) {
return that.comparer(a, b);
});
}
that.items = list;
}
return callback(err);
});
} else {
callback();
}
};
/**
* Constructor for the file system provider. This class inherits from {@link Provider}.
*
* Each data collection in EntreeJS represents a data provider instance.
* A sub directory will be created for every data collection using this provider.
* The directories will be placed in the data root specified in the options `connStr` argument and
* will have the same names as their corresponding collection names.
*
* For usage reference, please refer to [Provider documentation]{@link Provider}.
*
* Options
*
* - `connStr` {String} <required> Specifies the root directory relative to `process.cwd()`. **Example:** `./data`
*
* @class Represents a provider for local file system storage.
* @param {object} opts - Additional options. The only required option is `connStr`.
* @param {object=} schema - Defines the fields of the collection represented by the provider instance.
*/
function FsProvider(opts, schema) {
Provider.call(this, opts, schema);
var orgCwd,
connStr = path.resolve(process.cwd(), opts.connStr);
this.dir = path.join(connStr, schema.__collName);
if (!fs.existsSync(this.dir)) {
orgCwd = process.cwd();
process.chdir(connStr);
fs.mkdirSync(schema.__collName);
process.chdir(orgCwd);
}
}
util.inherits(FsProvider, Provider);
FsProvider.prototype._insert = function (items, callback, update) {
var that = this,
idKey = this._getIdKey(),
error = new CollErr(),
i;
function storeItem(item, cb) {
var id = that._getId(item),
filePath;
if (!id) {
id = uuid.v1();
that._setId(item, id);
}
filePath = path.join(that.dir, id + ".json");
fs.exists(filePath, function (exists) {
if (exists) {
error.push("ITEM_EXISTS");
return cb();
}
if (update) {
try {
item = object.update(item, {}, true, idKey);
} catch (err) {
error.push(err);
return cb();
}
item[idKey] = id;
}
fs.writeFile(filePath, JSON.stringify(item), function (err) {
if (err) {
error.push(err);
}
cb();
});
});
}
function completeRequest(err) {
if (err) {
error.push(err);
}
if (error.errors.length > 0) {
that.handleError(error, callback);
} else {
callback(null, items);
}
}
if (_.isArray(items)) {
for (i = 0; i < items.length; i++) {
if (object.validateKeys(items[i]) !== "fields") {
return that.handleError("OPERS_NOT_ALLOWED", callback);
}
}
async.each(items, storeItem, completeRequest);
} else {
if (!update && object.validateKeys(items) !== "fields") {
return that.handleError("OPERS_NOT_ALLOWED", callback);
}
storeItem(items, completeRequest);
}
};
FsProvider.prototype._upsert = function (item, callback) {
var that = this,
id = that._getId(item),
filePath;
if (!id) {
return that._insert(item, callback, true);
}
filePath = path.join(this.dir, id + ".json");
fs.exists(filePath, function (exists) {
if (exists) {
that._update(item, callback);
} else {
that._insert(item, callback, true);
}
});
};
FsProvider.prototype._update = function (item, callback) {
var that = this;
that._get(item, function (err, res) {
if (err) {
return that.handleError(err, callback);
}
var id = that._getId(item),
idKey = that._getIdKey(),
filePath = path.join(that.dir, id + ".json");
try {
item = object.update(item, res, idKey);
} catch (err) {
that.handleError(err, callback);
}
item[idKey] = id;
fs.writeFile(filePath, JSON.stringify(item), function (err) {
if (err) {
that.handleError(err, callback);
} else {
callback(null, item);
}
});
});
};
FsProvider.prototype._get = function (item, callback) {
var that = this,
id = that._getId(item),
filePath;
if (!id) {
return that.handleError("MISSING_ID", callback);
}
filePath = path.join(this.dir, id + ".json");
fs.readFile(filePath, { encoding: "utf8" }, function (err, data) {
if (err) {
if (err.code === "ENOENT") {
that.handleError("ITEM_DOESNOT_EXIST", callback);
} else {
that.handleError(err, callback);
}
} else {
callback(null, JSON.parse(data));
}
});
};
FsProvider.prototype._delete = function (item, callback) {
var that = this,
id = that._getId(item);
if (!id) {
return this.handleError("MISSING_ID", callback);
}
var filePath = path.join(this.dir, id + ".json");
fs.unlink(filePath, function (err) {
if (err) {
if (err.code === "ENOENT") {
that.handleError("ITEM_DOESNOT_EXIST", callback);
} else {
that.handleError(err, callback);
}
} else {
callback(null, item);
}
});
};
FsProvider.prototype._select = function (args, callback) {
var cursor = new FsCursor(this, args.query, args.options);
if (callback) {
callback(null, cursor);
} else {
return cursor;
}
};
module.exports = FsProvider;