@unclepaul/allcountjs
Version:
The open source framework for rapid business application development with Node.js
511 lines (444 loc) • 20.5 kB
JavaScript
var _ = require('underscore');
var Q = require('q');
var moment = require('moment');
var crypto = require('crypto');
var mongoose = require('mongoose');
var mongo = mongoose.mongo;
var GridStore = mongo.GridStore;
var ObjectId = mongoose.Types.ObjectId;
require('mongoose-long')(mongoose);
module.exports = function (dbUrl, injection, appUtil) {
var service = {};
var db;
var models = {};
var connection = mongoose.createConnection(dbUrl);
var onConnectedDeferred = Q.defer();
var onConnectedPromise = onConnectedDeferred.promise;
injection.bindFactory('ObjectId', function() {
return x => new ObjectId(x);
});
connection.on('connected', function () {
db = connection.db;
onConnectedDeferred.resolve();
});
service.addOnConnectListener = function (listener) {
if (db) {
return Q(listener());
} else {
return onConnectedPromise = onConnectedPromise.then(function () {
return listener();
});
}
};
service.mongooseConnection = function () {
return connection;
};
service.mongooseModels = function () {
return models;
};
service.serializers = {};
function modelFor(table) {
if (!table.entityTypeId) {
throw new Error("entityTypeId not defined for: " + JSON.stringify(table));
}
if (!models[table.entityTypeId]) {
throw new Error("Can't find model for entity: " + table.entityTypeId);
}
return models[table.entityTypeId];
}
service.findCount = function (table, filteringAndSorting) {
return onlyFilteringEnsureIndexes(table, filteringAndSorting).then(function () {
return Q(modelFor(table).count(queryFor(table, filteringAndSorting)).exec());
})
};
service.findAll = function (table, filteringAndSorting) {
return ensureIndexes(table, filteringAndSorting).then(function () {
return Q(modelFor(table).find(queryFor(table, filteringAndSorting)).sort(sortingFor(filteringAndSorting, table.fields)).exec())
.then(function (result) { return result.map(fromBson(table)) });
});
};
service.findRange = function (table, filteringAndSorting, start, count) {
return ensureIndexes(table, filteringAndSorting).then(function () {
return Q(modelFor(table).find(queryFor(table, filteringAndSorting)).sort(sortingFor(filteringAndSorting, table.fields)).limit(count).skip(start).exec())
.then(function (result) { return result.map(fromBson(table)) });
});
};
service.checkUserPassword = function (table, entityId, passwordField, password) {
if (!entityId) {
throw new Error('entityId should be defined for checkUserPassword()');
}
return Q(modelFor(table).findById(toMongoId(entityId)).exec()).then(function (user) {
if (!user) {
return false;
}
var userId = user._id.toString();
var digest = service.passwordHash(userId, password);
return digest === user.passwordHash ? fromBson(table)(user) : false;
});
};
function sortingFor(filteringAndSorting, fields) {
return _.object(_.union(filteringAndSorting && filteringAndSorting.sorting && filteringAndSorting.sorting.filter(function (i) { return !!fields[i[0]] }) || [], [['modifyTime', -1]]));
}
function ensureIndexes(table, filteringAndSorting) {
var indexFieldHash = {};
var indexes = [];
indexes = _.map(sortingFor(filteringAndSorting, table.fields), function (order, fieldName) { return [fieldName, order] });
_.forEach(indexes, function (i) { indexFieldHash[i[0]] = true });
return ensureIndexesWith(indexes, indexFieldHash, table, filteringAndSorting);
}
function onlyFilteringEnsureIndexes(table, filteringAndSorting) {
return ensureIndexesWith([], {}, table, filteringAndSorting);
}
function ensureIndexesWith(indexes, indexFieldHash, table, filteringAndSorting) {
var query = queryFor(table, filteringAndSorting);
function addIndexField(q, fieldName) {
if (fieldName === '$and' || fieldName === '$or') {
_.forEach(q, function (qObj) { _.forEach(qObj, addIndexField) });
} else if (!indexFieldHash[fieldName]) {
indexes.unshift([fieldName, 1]);
indexFieldHash[fieldName] = true;
}
}
_.forEach(query, addIndexField);
if (indexes.length > 0) {
var collection = db.collection(table.tableName);
return Q.nfbind(collection.ensureIndex.bind(collection))(indexes, { name: crypto.createHash('sha1').update(JSON.stringify(indexes)).digest('hex') });
} else {
return Q(null);
}
}
var systemFields = { //TODO doubling
createTime: {
fieldType: {
id: 'date'
}
},
modifyTime: {
fieldType: {
id: 'date'
}
}
};
function getAllFields(table) {
return _.extend({}, table.fields, systemFields);
}
function copyFilter(allFields, elements) {
var query = {};
_.each(elements, function (filterValue, filterName) {
if (filterName === '$and' || filterName === '$or') {
if (_.isArray(filterValue)) {
var tmparray = [];
_.each(filterValue, function (filters) {
var tmp = copyFilter(allFields, filters)
if (!_.isEmpty(tmp)) {
tmparray.push(tmp);
}
});
if (!_.isEmpty(tmparray)) {
query[filterName] = tmparray;
}
}
} else if (!_.isUndefined(allFields[filterName])) {
fieldName = filterName;
field = allFields[filterName];
if (field.fieldType.id == 'reference' || field.fieldType.id == 'multiReference') {
var referenceId = _.isUndefined(filterValue.id) ? filterValue : filterValue.id;
query[filterName + '.id'] = toMongoId(referenceId)
} else if (field.fieldType.id == 'checkbox') {
query[filterName] = filterValue ? filterValue : { $in: [false, null] };
} else if (field.fieldType.id == 'date') {
if (filterValue.op === 'gt') {
query[filterName] = { $gt: filterValue.value }; //TODO convert from string?
} else if (filterValue.op === 'lt') {
query[filterName] = { $lt: filterValue.value }; //TODO convert from string?
} else {
query[filterName] = filterValue;
}
} else if (field.fieldType.id == 'text') {
if (filterValue.op === 'startsWith') {
query[filterName] = { $regex: "^" + filterValue.value + ".*" };
} else if (filterValue.op === 'in') {
query[filterName] = { $in: filterValue.value };
} else {
query[filterName] = filterValue;
}
} else {
query[filterName] = filterValue;
}
}
});
return query;
}
function queryFor(table, filteringAndSorting) {
var query = {};
if (filteringAndSorting) {
if (filteringAndSorting.query) {
query = _.clone(filteringAndSorting.query); //TODO merging filtering and query
}
if (filteringAndSorting.textSearch) {
var split = splitText(filteringAndSorting.textSearch);
if (split.length > 0) {
query.$and = split.map(function (value) { return { __textIndex: { $elemMatch: { $regex: "^" + value + ".*" } } } });
}
}
if (filteringAndSorting.filtering) {
var allFields = getAllFields(table);
_.extend(query, copyFilter(allFields, filteringAndSorting.filtering));
}
}
return query;
}
service.queryFor = queryFor;
service.newEntityId = function () {
return (new ObjectId()).toString();
};
service.createEntity = function (table, entity) {
return callBeforeCrudListeners(table, null, entity).then(function () {
var objectID = entity.id && toMongoId(entity.id) || new ObjectId();
entity.id = (objectID).toString();
var toInsert = toBson(table)(entity);
toInsert._id = objectID;
toInsert.createTime = new Date();
toInsert.modifyTime = new Date();
setAuxiliaryFields(table.fields, toInsert, toInsert);
return Q(modelFor(table).create(toInsert)).catch(function (err) {
if (err.message.indexOf('duplicate key error index') !== -1) {
var fieldToMessage = {};
_.forEach(toInsert, function (v, name) {
if (err.message.indexOf(name) !== -1) {
fieldToMessage[name] = 'Should be unique';
}
});
throw new appUtil.ValidationError(fieldToMessage);
}
throw err;
})
.then(callAfterCrudListeners(table, null, fromBson(table)(toInsert)))
.then(function (result) {
return result._id;
});
});
};
service.aggregateQuery = function (table, aggregatePipeline) {
var collection = db.collection(table.tableName);
return Q.nfbind(collection.aggregate.bind(collection))(aggregatePipeline).then(function (rows) {
return rows.map(function (row) {
return _.extend(row, fromBson(table.fields)(row));
});
})
};
function padId(id) {
return id.length < 12 ? _.range(0, 12 - id.length).map(function () { return " " }).join("") + id : id;
}
function toMongoId(entityId) {
return new ObjectId(padId(entityId));
}
service.toMongoId = toMongoId;
service.readEntity = function (table, entityId) {
if (!entityId) {
throw new Error('entityId should be defined for readEntity()');
}
return Q(modelFor(table).findById(toMongoId(entityId)).exec()).then(function (result) {
return result && fromBson(table)(result) || result;
});
};
service.updateEntity = function (table, entity) {
return service.readEntity(table, entity.id).then(function (oldEntity) {
var newEntity = _.extend(Object.create(oldEntity), entity);
return callBeforeCrudListeners(table, oldEntity, newEntity).then(function () {
var toUpdate = toBson(table)(_.extendOwn({}, newEntity));
toUpdate.modifyTime = new Date();
return Q(modelFor(table).findOneAndUpdate({ _id: toMongoId(entity.id) }, toUpdate).exec())
.then(callAfterCrudListeners(table, oldEntity, newEntity)) //TODO REST layer should convert all data types
.then(function () {
return service.readEntity(table, entity.id).then(function (result) {
var update = {};
setAuxiliaryFields(table.fields, result, update);
return Q(modelFor(table).findOneAndUpdate({ _id: toMongoId(entity.id) }, update).exec());
});
}).then(function (result) {
return fromBson(table)(result);
});
});
});
};
service.deleteEntity = function (table, entityId) {
return service.readEntity(table, entityId).then(function (oldEntity) {
return callBeforeCrudListeners(table, oldEntity, null).then(function () {
return Q(modelFor(table).findOneAndRemove({ _id: toMongoId(entityId) }).exec()).then(callAfterCrudListeners(table, oldEntity, null));
}).then(function (result) {
return fromBson(table)(result);
});
})
};
function callAfterCrudListeners(table, oldEntity, newEntity) {
return function (result) {
return invokeCrudListeners(table.tableName, afterCrudListeners[table.tableName], oldEntity, newEntity).thenResolve(result);
}
}
function callBeforeCrudListeners(table, oldEntity, newEntity) {
return invokeCrudListeners(table.tableName, beforeCrudListeners[table.tableName], oldEntity, newEntity);
}
function invokeCrudListeners(tableName, listenerArray, oldEntity, newEntity) {
if (injection.inject('inCrudListener_' + tableName, true)) {
return Q(null);
}
return (listenerArray || []).map(function (listener) {
return function () {
var scope = {};
scope['inCrudListener_' + tableName] = true;
return injection.inScope(scope, function () {
return listener(oldEntity, newEntity)
});
}
}).reduce(Q.when, Q(null));
}
function setAuxiliaryFields(fields, entity, toUpdate) {
setTextIndex(fields, entity, toUpdate);
}
function setTextIndex(fields, entity, toUpdate) {
var strings = convertEntity(fields, function (value, field) {
if (field.fieldType.id == 'reference' && value) {
return value.name && value.name.toString() || undefined;
} else if (field.fieldType.id == 'multiReference' && value) {
return value.map(_.property('name')).join(' ');
} else if (field.fieldType.id == 'json' && value) {
return getPropertyRecursive(value).join(' ');
}
return value && value.toString() || undefined;
}, entity);
toUpdate.__textIndex = _.chain(strings).map(splitText).flatten().unique().value();
}
function getPropertyRecursive(obj) {
var values = [];
_.each(obj, function (value, key) {
if (_.isObject(value)) {
values = values.concat(getPropertyRecursive(value));
}
else values.push(value);
});
return values;
}
function splitText(str) {
return _.filter(str.toLowerCase().split(/\s/), function (str) {
return str.trim().length > 0;
});
}
function fromBson(table) {
return function (entity) {
var result = convertEntity(table.fields, function (value, field, entity, fieldName) {
return service.serializers[table.entityTypeId][fieldName].fromBsonValue(value, field, entity, fieldName);
}, entity);
if (entity._id) {
result.id = entity._id.toString();
}
return result;
}
}
function toBson(table) { //TODO it doesn't convert id
return function (entity) {
return convertEntity(table.fields, function (value, field, entity, fieldName) {
return service.serializers[table.entityTypeId][fieldName].toBsonValue(value, field, entity, fieldName);
}, entity);
}
}
function convertEntity(fields, convertFun, entity) {
var result = {};
_.each(fields, function (field, fieldName) {
var value = convertFun(entity[fieldName], field, entity, fieldName);
if (value && (value.$$push || value.$$pull)) {
var mergeOp = value.$$push || value.$$pull;
if (!result[mergeOp.field] || !_.isArray(result[mergeOp.field])) {
result[mergeOp.field] = [];
}
if (value.$$push && !_.contains(result[mergeOp.field], mergeOp.value)) {
result[mergeOp.field].push(mergeOp.value);
} else if (value.$$pull && _.contains(result[mergeOp.field], mergeOp.value)) {
result[mergeOp.field].splice(result[mergeOp.field].indexOf(mergeOp.value), 1);
}
} else if (!_.isUndefined(value)) {
result[fieldName] = value;
}
});
return result;
}
service.passwordHash = function (objectId, password) {
var hash = crypto.createHmac('sha1', objectId.toString());
hash.update(password, 'utf8');
return hash.digest('hex');
};
service.createFile = function (fileName, readableStream) {
var fileId = new ObjectId();
var gridStore = new GridStore(db, fileId, fileName, "w");
var open = Q.nfbind(gridStore.open.bind(gridStore));
return open().then(function (store) {
var write = Q.nfbind(store.write.bind(store));
var writeChain = Q(null);
readableStream.on('data', function (chunk) {
writeChain = writeChain.then(function () {
return write(chunk);
});
});
var deferredEnd = Q.defer();
readableStream.on('end', function () {
writeChain.then(function (store) {
var close = Q.nfbind(store.close.bind(store));
return close().then(function () {
deferredEnd.resolve(fileId);
}, function (err) {
deferredEnd.reject(err);
});
})
});
readableStream.on('error', function (err) {
deferredEnd.reject(err);
});
return deferredEnd.promise;
});
};
service.getFile = function (fileId) {
var gridStore = new GridStore(db, toMongoId(fileId), "r");
var open = Q.nfbind(gridStore.open.bind(gridStore));
return open().then(function (store) {
return { fileName: store.filename, stream: store.stream(true) };
});
};
service.removeFile = function (fileId) { //TODO enable file removing when file owning security checks will be ready
var gridStore = new GridStore(db, toMongoId(fileId), "r");
return Q.nfbind(gridStore.unlink.bind(gridStore))();
};
var afterCrudListeners = {};
var beforeCrudListeners = {};
//TODO addEntityListener -- deprecated
service.addEntityListener = service.addAfterCrudListener = function (table, listener) {
addCrudListener(afterCrudListeners, table, listener);
return {
remove: function () { return removeListener(afterCrudListeners, table, listener) }
}
};
service.addBeforeCrudListener = function (table, listener) {
addCrudListener(beforeCrudListeners, table, listener);
return {
remove: function () { return removeListener(beforeCrudListeners, table, listener) }
}
};
function addCrudListener(listeners, table, listener) {
if (!listeners[table.tableName]) {
listeners[table.tableName] = [];
}
listeners[table.tableName].push(listener);
}
function removeListener(listeners, table, listener) {
listeners[table.tableName].splice(listeners[table.tableName].indexOf(listener), 1);
}
service.closeConnection = function () {
var defer = Q.defer();
service.addOnConnectListener(function () {
connection.close(function () {
defer.resolve(null);
});
});
return defer.promise;
};
return service;
};