UNPKG

@unclepaul/allcountjs

Version:

The open source framework for rapid business application development with Node.js

477 lines (429 loc) 20.2 kB
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); var R = require('ramda'); module.exports = function (dbUrl, injection, dbClient) { var service = {}; var db; var onConnectListeners = []; var knex = require('knex')({ client: dbClient, connection: dbUrl, pool: { afterCreate: function (connection, callback) { if (db) { callback(null, connection); } db = true; return onConnectListeners.map(function (listener) { return function () { return listener() }}).reduce(Q.when, Q(null)).thenResolve(connection).nodeify(callback); } } }); service.addOnConnectListener = function (listener) { if (db) { Q(listener()).done(); } else { onConnectListeners.push(listener); } }; var entityTypeIdToTable = {}; service.setupModel = function (table) { entityTypeIdToTable[table.entityTypeId] = table; }; service.findCount = function (table, filteringAndSorting) { var where = queryFor(table, filteringAndSorting); return Q(where(knex(table.tableName)).count('id as count')).then(function (result) { return result[0].count; }); }; service.findQuery = function (table, filteringAndSorting) { var where = queryFor(table, filteringAndSorting); var order = sortingFor(filteringAndSorting, table.fields); var joins = referenceJoins(table); var select = selectFields(table); return R.compose(where, order, joins, select); }; service.findAll = function (table, filteringAndSorting) { var query = this.findQuery(table, filteringAndSorting); return Q(query(knex(table.tableName))) .then(function (result) { return result.map(fromBson(table.fields)) }); }; service.findRange = function (table, filteringAndSorting, start, count) { var query = this.findQuery(table, filteringAndSorting); return Q(query(knex(table.tableName)).limit(parseInt(count)).offset(parseInt(start))) .then(function (result) { return result.map(fromBson(table.fields)) }); }; service.checkUserPassword = function (table, entityId, passwordField, password) { if (!entityId) { throw new Error('entityId should be defined for checkUserPassword()'); } return Q(knex(table.tableName).where('id', entityId)).then(function (user) { user = user[0]; if (!user) { return false; } var salt = user.passwordHash.split('.')[0]; var digest = service.passwordHash(salt, password); return digest === user.passwordHash ? fromBson(table.fields)(user) : false; }); }; function sortingFor(filteringAndSorting, fields) { return function (query) { _.forEach(_.object(_.union(filteringAndSorting && filteringAndSorting.sorting && filteringAndSorting.sorting.filter(function (i) { return !!fields[i[0]]}) || [], [['modifyTime', -1]])), function (dir, field) { query = query.orderBy(field, dir > 0 ? 'asc' : 'desc'); }); return query; }; } var systemFields = { createTime: { fieldType: { id: 'date' } }, modifyTime: { fieldType: { id: 'date' } } }; function getAllFields(table) { return _.extend({}, table.fields, systemFields); } function queryFor(table, filteringAndSorting) { return function (query) { if (filteringAndSorting) { if (filteringAndSorting.query) { //query = _.clone(filteringAndSorting.query); //TODO } if (filteringAndSorting.textSearch) { var split = splitText(filteringAndSorting.textSearch); /*if (split.length > 0) { //TODO query.$and = split.map(function (value) { return {__textIndex: { $regex: value + ".*" }}}); }*/ } if (filteringAndSorting.filtering) { var allFields = getAllFields(table); _.each(allFields, function (field, fieldName) { if (_.isUndefined(filteringAndSorting.filtering[fieldName])) { return; } var filterValue = filteringAndSorting.filtering[fieldName]; if (field.fieldType.id == 'reference') { query = query.where(fieldName, filteringAndSorting.filtering[fieldName]); } else if (field.fieldType.id == 'checkbox') { query = query.where(fieldName, filteringAndSorting.filtering[fieldName]); /// TODO: ? filteringAndSorting.filtering[fieldName] : {$in: [false, null]}; } else if (field.fieldType.id == 'date') { if (filterValue.op === 'gt') { query = query.where(fieldName, '>', filteringAndSorting.filtering[fieldName]); //TODO convert from string? } else if (filterValue.op === 'lt') { query = query.where(fieldName, '<', filteringAndSorting.filtering[fieldName]); //TODO convert from string? } else { query = query.where(fieldName, filteringAndSorting.filtering[fieldName]); } } else if (field.fieldType.id == 'text') { if (filterValue.op === 'startsWith') { query = query.where(fieldName, 'like', filterValue.value + "%"); } else if (filterValue.op === 'in') { query = query.whereIn(fieldName, filterValue.value); } else { query = query.where(fieldName, filterValue); } } else { query = query.where(fieldName, filteringAndSorting.filtering[fieldName]); } }); } } return query; } } function referenceJoins(table) { return function (query) { var allFields = getAllFields(table); _.each(allFields, function (field, fieldName) { if (field.fieldType.id == 'reference') { var referenceTable = entityTypeIdToTable[field.fieldType.referenceEntityTypeId]; var referenceTableName = referenceTable.tableName; query = query.leftOuterJoin(referenceTableName, table.tableName + '.' + fieldName, referenceTableName + '.id') .select(referenceTable.fieldsToSelectForReferenceName.map(function (n) { return referenceTableName + '.' + n + ' as ' + fieldName + '_' + n})); } }); return query; } } function selectFields(table) { return function (query) { return query.select(table.tableName + '.*'); } } service.queryFor = queryFor; service.newEntityId = function () { return (new ObjectId()).toString(); }; service.createEntity = function (table, entity) { return callBeforeCrudListeners(table, null, entity).then(function () { var toInsert = toBson(table.fields)(entity); toInsert.createTime = new Date(); toInsert.modifyTime = new Date(); setAuxiliaryFields(table.fields, toInsert, toInsert); return Q(knex(table.tableName).insert(toInsert, 'id')) .then(callAfterCrudListeners(table, null, fromBson(table.fields)(toInsert))) .then(function (result) { return result[0]; }); }); }; service.aggregateQuery = function (table, aggregatePipeline) { //TODO 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.readEntity = function (table, entityId) { if (!entityId) { throw new Error('entityId should be defined for readEntity()'); } return Q(knex(table.tableName).where('id', entityId)).then(function (result) { result = result[0]; return result && fromBson(table.fields)(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.fields)(_.extendOwn({}, newEntity)); toUpdate.modifyTime = new Date(); return Q(knex(table.tableName).where('id', entity.id).update(toUpdate)) .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); if (_.size(update)) { return Q(knex(table.tableName).where('id', entity.id).update(update)).thenResolve(result); } else { return result; } }); }); }); }); }; service.deleteEntity = function (table, entityId) { return service.readEntity(table, entityId).then(function (oldEntity) { return callBeforeCrudListeners(table, oldEntity, null).then(function () { return Q(knex(table.tableName).where('id', entityId).del()).then(callAfterCrudListeners(table, oldEntity, null)); }); }) }; 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) { //TODO var strings = convertEntity(fields, function (value, field) { if (field.fieldType.id == 'reference' && value) { return value.name && value.name.toString() || undefined; } return value && value.toString() || undefined; }, entity); toUpdate.__textIndex = _.chain(strings).map(splitText).flatten().unique().value(); } function splitText(str) { return _.filter(str.toLowerCase().split(/\s/), function (str) { return str.trim().length > 0; }); } function fromBson(fields) { return function (entity) { var result = convertEntity(fields, fromBsonValue, entity); if (entity.id) { result.id = entity.id.toString(); } return result; } } service.fromBson = fromBson; function toBson(fields) { //TODO it doesn't convert id return function (entity) { return convertEntity(fields, toBsonValue, 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; } function fromBsonValue(value, field, entity, fieldName) { if (field.fieldType.id == 'date' && value) { return moment(value).format('YYYY-MM-DD'); //TODO move to REST layer? } else if (field.fieldType.id == 'money' && value) { return value.toString(10).replace(/\./, ''); } /*else if (field.fieldType.id == 'checkbox' && field.fieldType.storeAsArrayField) { //TODO return entity[field.fieldType.storeAsArrayField] && _.isArray(entity[field.fieldType.storeAsArrayField]) && entity[field.fieldType.storeAsArrayField].indexOf(field.name) != -1 }*/ else if (field.fieldType.id == 'password') { return ''; } else if (field.fieldType.id == 'reference' && value) { var referenceTable = entityTypeIdToTable[field.fieldType.referenceEntityTypeId]; var reference = _.chain(referenceTable.fieldsToSelectForReferenceName).map(function (n) { return [n, entity[fieldName + '_' + n]] }).object().value(); return {id: value, name: referenceTable.referenceName(reference) }; } return value; } function toBsonValue(value, field, entity, fieldName) { if (field.fieldType.id == 'date' && value) { if (_.isDate(value)) { return value; } return moment(value, 'YYYY-MM-DD').toDate(); //TODO move to REST layer? } else if (field.fieldType.id == 'reference' && value) { if (!value.id) { throw new Error("Reference value without id was passed for field '" + fieldName + "'"); } return value.id } else if (field.fieldType.id == 'money' && value) { return value.toString().slice(0, -2) + '.' + value.toString().slice(-2) } /*else if (field.fieldType.id == 'checkbox' && field.fieldType.storeAsArrayField && !_.isUndefined(value)) { //TODO return value ? { $$push: { field: field.fieldType.storeAsArrayField, value: field.name } } : { $$pull: { field: field.fieldType.storeAsArrayField, value: field.name } }; }*/ else if (field.fieldType.id == 'password' && value) { return service.passwordHash(crypto.randomBytes(16).toString('hex'), value); } return value; } service.passwordHash = function (salt, password) { var hash = crypto.createHmac('sha1', salt.toString()); hash.update(password, 'utf8'); return salt + '.' + 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); }; service.addBeforeCrudListener = function (table, listener) { addCrudListener(beforeCrudListeners, table, listener); }; function addCrudListener(listeners, table, listener) { if (!listeners[table.tableName]) { listeners[table.tableName] = []; } listeners[table.tableName].push(listener); } service.closeConnection = function () { var defer = Q.defer(); service.addOnConnectListener(function () { connection.close(function () { defer.resolve(null); }); }); return defer.promise; }; return service; };