diff --git a/mongodb-4.js b/mongodb-4.js new file mode 100644 index 0000000..d9f2dd0 --- /dev/null +++ b/mongodb-4.js @@ -0,0 +1,2008 @@ +const database = require('mongodb'); +const Fs = require('fs'); +const columns_cache = {}; +const CONNECTIONS = {}; +const NOOP = function(){}; +const PROJECTION = { _id: 1 }; +const FILEREADERFILTER = {}; +const REG_APO = /'/; +const OPTIONS = { reconnectTries: 120, reconnectInterval: 1000 }; + +require('./index'); + +function SqlBuilder(skip, take, agent) { + this.agent = agent; + this.builder = {}; + this._order = null; + this._skip = skip >= 0 ? skip : 0; + this._take = take >= 0 ? take : 0; + this._set = null; + this._inc = null; + this._scope = 0; + this._fields; + this._is = false; + this._isfirst = false; + this._prepare; +} + +SqlBuilder.prototype = { + get data() { + var obj = {}; + if (this._set) + obj.$set = this._set; + if (this._inc) + obj.$inc = this._inc; + return obj; + } +}; + +SqlBuilder.prototype.callback = function(fn) { + this.$callback = fn; + return this; +}; + +SqlBuilder.prototype.assign = function() { + throw new Error('This method is not supported in MongoDB.'); +}; + +SqlBuilder.prototype.replace = function(builder, reference) { + var self = this; + + self.builder = reference ? builder.builder : copy(builder.builder); + self.scope = builder.scope; + + if (builder._order) + self._order = reference ? builder._order : copy(builder._order); + + self._skip = builder._skip; + self._take = builder._take; + + if (builder._set) + self._set = reference ? builder._set : copy(builder._set); + + if (builder._inc) + self._inc = reference ? builder._inc : copy(builder._inc); + + if (builder._prepare) + self._prepare = reference ? builder._prepare : copy(builder._prepare); + + if (builder._fields) + self._fields = reference ? builder._fields : copy(builder._fields); + + self._is = builder._is; + self._isfirst = builder._isfirst; + + return self; +}; + +SqlBuilder.prototype.debug = function(type) { + var obj = {}; + obj.type = type; + obj.condition = this.builder; + this._fields && (obj.project = this._fields); + this._order && (obj.sort = this._order); + this._set && (obj.set = this._set); + this._inc && (obj.inc = this._inc); + obj.take = this._take; + obj.skip = this._skip; + return obj; +}; + +function copy(source) { + + var keys = Object.keys(source); + var i = keys.length; + var target = {}; + + while (i--) { + var key = keys[i]; + target[key] = source[key]; + } + + return target; +} + +SqlBuilder.prototype.clone = function() { + var builder = new SqlBuilder(0, 0, this.agent); + return builder.replace(this); +}; + +SqlBuilder.prototype.join = function(name) { + throw new Error('SqlBuilder.join(' + name + ') is not supported.'); +}; + +SqlBuilder.prototype.set = function(name, value) { + var self = this; + if (!self._set) + self._set = {}; + + if (typeof(name) === 'string') { + self._set[name] = value; + return self; + } + + var keys = Object.keys(name); + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + if (key !== '_id' && key[0] !== '$' && name[key] !== undefined) + self._set[key] = name[key]; + } + + return self; +}; + +SqlBuilder.prototype.primary = SqlBuilder.prototype.primaryKey = function(name) { + console.log('SqlBuilder.primary(' + name + ') is not supported.'); + // not implemented + return this; +}; + +SqlBuilder.prototype.remove = SqlBuilder.prototype.rem = function(name) { + if (this._set) + delete this._set[name]; + if (this._inc) + delete this._inc[name]; + return this; +}; + +SqlBuilder.prototype.schema = function(name) { + console.log('SqlBuilder.schema(' + name + ') is not supported.'); + return this; +}; + +SqlBuilder.prototype.fields = function() { + var self = this; + + if (arguments[0] instanceof Array) { + var arr = arguments[0]; + for (var i = 0, length = arr.length; i < length; i++) + self.field(arr[i]); + return self; + } + + for (var i = 0; i < arguments.length; i++) + self.field(arguments[i]); + return self; +}; + +SqlBuilder.prototype.field = function(name, visible) { + var self = this; + if (!self._fields) + self._fields = {}; + self._fields[name] = visible === false ? 0 : 1; + return self; +}; + +SqlBuilder.prototype.raw = function(name, value) { + var self = this; + if (!self._set) + self._set = {}; + self._set[name] = value; + return self; +}; + +SqlBuilder.prototype.inc = function(name, type, value) { + + var self = this; + var can = false; + + if (!self._inc) + self._inc = {}; + + if (value === undefined) { + value = type; + type = '+'; + can = true; + } + + if (value === undefined) + value = 1; + + if (typeof(name) === 'string') { + + if (can && typeof(value) === 'string') { + type = value[0]; + switch (type) { + case '+': + case '-': + case '*': + case '/': + value = value.substring(1).parseFloat(); + break; + default: + type = '+'; + value = value.parseFloat(); + break; + } + } else { + if(type !== '-') + type = '+'; + if (value == null) + value = 1; + } + + if (!value) + return self; + + if (type === '-') + value = value * -1; + + if (value === '$') + throw new Error('SqlBuilder.inc(' + name + ') can\'t contain "$" value.'); + + self._inc[name] = value; + return self; + } + + var keys = Object.keys(name); + + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + name[key] && self.inc(key, name[key]); + } + + return self; +}; + +SqlBuilder.prototype.sort = function(name, desc) { + return this.order(name, desc); +}; + +SqlBuilder.prototype.order = function(name, desc) { + + var self = this; + if (self._order === null) + self._order = {}; + + var key = '<' + name + '.' + (desc || 'false') + '>'; + + if (columns_cache[key]) { + self._order[columns_cache[key].name] = columns_cache[key].value; + return; + } + + var lowered = name.toLowerCase(); + var index = lowered.lastIndexOf('desc'); + + if (index !== -1 || lowered.lastIndexOf('asc') !== -1) { + name = name.split(' ')[0]; + desc = index !== -1; + } + + columns_cache[key] = {}; + columns_cache[key].name = name; + columns_cache[key].value = self._order[name] = desc ? -1 : 1; + return self; +}; + +SqlBuilder.prototype.random = function() { + console.log('SqlBuilder.random() is not supported.'); + return this; +}; + +SqlBuilder.prototype.skip = function(value) { + var self = this; + self._skip = self.parseInt(value); + return self; +}; + +SqlBuilder.prototype.limit = function(value) { + return this.take(value); +}; + +SqlBuilder.prototype.page = function(value, max) { + var self = this; + value = self.parseInt(value) - 1; + max = self.parseInt(max); + if (value < 0) + value = 0; + self._skip = value * max; + self._take = max; + return self; +}; + +SqlBuilder.prototype.parseInt = function(num) { + if (typeof(num) === 'number') + return num; + if (!num) + return 0; + num = parseInt(num); + if (isNaN(num)) + num = 0; + return num; +}; + +SqlBuilder.prototype.take = function(value) { + var self = this; + self._take = self.parseInt(value); + return self; +}; + +SqlBuilder.prototype.first = function() { + var self = this; + self._skip = 0; + self._take = 1; + self._isfirst = true; + return self; +}; + +SqlBuilder.prototype.where = function(name, operator, value) { + return this.push(name, operator, value); +}; + +SqlBuilder.prototype.push = function(name, operator, value) { + var self = this; + + if (value === undefined) { + value = operator; + operator = '='; + } else if (operator === '!=') + operator = '<>'; + + var type = typeof(value); + + if (name[0] === '!' && type !== 'function') { + name = name.substring(1); + value = ObjectID.parse(value); + } + + switch (operator) { + case '=': + self.$scope(name, value, type, 1); + break; + case '<>': + self.$scope(name, { $ne: value }, type, 5); + break; + case '>': + self.$scope(name, { $gt: value }, type, 6); + break; + case '<': + self.$scope(name, { $lt: value }, type, 7); + break; + case '>=': + self.$scope(name, { $gte: value }, type, 8); + break; + case '<=': + self.$scope(name, { $lte: value }, type, 9); + break; + } + + self.checkOperator(); + self._is = true; + return self; +}; + +SqlBuilder.prototype.checkOperator = function() { + // Not implemented + return this; +}; + +SqlBuilder.prototype.clear = function() { + this._take = 0; + this._skip = 0; + this._scope = 0; + this._order = null; + this._set = null; + this._inc = null; + this.builder = {}; + return this; +}; + +SqlBuilder.escape = SqlBuilder.prototype.escape = function(value) { + console.log('SqlBuilder.escape() is not supported.'); + return value; +}; + +SqlBuilder.column = function(name) { + console.log('SqlBuilder.column() is not supported.'); + return name; +}; + +SqlBuilder.prototype.group = function() { + console.log('SqlBuilder.group() is not supported.'); + return this; +}; + +SqlBuilder.prototype.having = function() { + console.log('SqlBuilder.having() is not supported.'); + return this; +}; + +SqlBuilder.prototype.and = function() { + var self = this; + self._scope = 2; + return self; +}; + +SqlBuilder.prototype.or = function() { + var self = this; + self._scope = 1; + return self; +}; + +SqlBuilder.prototype.end = function() { + var self = this; + self._scope = 0; + return self; +}; + +SqlBuilder.prototype.scope = function(fn) { + var self = this; + fn.call(self); + self._scope = 0; + return self; +}; + +SqlBuilder.prototype.$scope = function(name, obj, type, code, raw) { + + var self = this; + var is = false; + + if (type === 'function') { + if (!self._prepare) + self._prepare = []; + self._prepare.push({ context: self.builder, name: name, value: obj, scope: self._scope, type: code, raw: raw }); + is = true; + } + + if (self._scope === 0) { + self.builder[name] = obj; + return self; + } + + if (self._scope === 1) { + if (!self.builder['$or']) + self.builder['$or'] = []; + var filter = {}; + filter[name] = obj; + self.builder['$or'].push(filter); + if (is) + self._prepare[self._prepare.length - 1].index = self.builder['$or'].length - 1; + } + + if (self._scope === 2) { + if (!self.builder['$and']) + self.builder['$and'] = []; + var filter = {}; + filter[name] = obj; + self.builder['$and'].push(filter); + if (is) + self._prepare[self._prepare.length - 1].index = self.builder['$and'].length - 1; + } + + return self; +}; + +SqlBuilder.prototype.in = function(name, value) { + var self = this; + self.$scope(name, { '$in': value }, typeof(value), 4); + return self; +}; + +SqlBuilder.prototype.like = function(name, value, where) { + var self = this; + var type = typeof(value); + var val = type === 'function' ? '' : value.toString(); + + switch (where) { + case 'beg': + case 'begin': + self.$scope(name, { $regex: '^' + val }, type, 2, value); + break; + case 'end': + self.$scope(name, { $regex: val + '$' }, type, 2, value); + break; + case '*': + default: + self.$scope(name, { $regex: val }, type, 2, value); + break; + } + + self._is = true; + return self; +}; + +SqlBuilder.prototype.between = function(name, valueA, valueB) { + var self = this; + var typeA = typeof(valueA); + var typeB = typeof(valueB); + self.$scope(name, { $gte: valueA, $lte: valueB }, typeA === 'function' || typeB === 'function' ? 'function' : typeA, 3); + self._is = true; + return self; +}; + +SqlBuilder.prototype.query = function(name, value) { + return this.$scope(name, value, undefined, 10); +}; + +SqlBuilder.prototype.sql = function() { + console.log('SqlBuilder.sql() is not supported.'); + return this; +}; + +SqlBuilder.prototype.toString = function() { + console.log('SqlBuilder.toString() is not supported.'); + return this; +}; + +SqlBuilder.prototype.toQuery = function() { + console.log('SqlBuilder.toQuery() is not supported.'); + return this; +}; + +SqlBuilder.prototype.prepare = function() { + + if (!this._prepare) + return this; + + for (var i = 0, length = this._prepare.length; i < length; i++) { + + var prepare = this._prepare[i]; + // prepare.type 1 - where "=" + // prepare.type 2 - like + // prepare.type 3 - between + // prepare.type 4 - in + + // or + if (prepare.scope === 1) { + prepare.context['$or'][prepare.index] = prepare.value(); + continue; + } + + // and + if (prepare.scope === 2) { + prepare.context['$and'][prepare.index] = prepare.value(); + continue; + } + + if (prepare.type === 1) { + prepare.context[prepare.name] = prepare.value(); + continue; + } + + if (prepare.type === 2) { + prepare.value.$regex += prepare.raw(); + continue; + } + + if (prepare.type === 3) { + if (typeof(prepare.value.$gte) === 'function') + prepare.value.$gte = prepare.value.$gte(); + if (typeof(prepare.value.$lte) === 'function') + prepare.value.$lte = prepare.value.$lte(); + continue; + } + + if (prepare.type === 4) { + prepare.value.$in = prepare.value.$in(); + continue; + } + + if (prepare.type === 5) { + prepare.value.$ne = prepare.value.$ne(); + continue; + } + + if (prepare.type === 6) { + prepare.value.$gt = prepare.value.$gt(); + continue; + } + + if (prepare.type === 7) { + prepare.value.$lt = prepare.value.$lt(); + continue; + } + + if (prepare.type === 8) { + prepare.value.$gte = prepare.value.$gte(); + continue; + } + + if (prepare.type === 9) { + prepare.value.$lte = prepare.value.$lte(); + continue; + } + } + + return this; +}; + +SqlBuilder.prototype.make = function(fn) { + var self = this; + fn.call(self, self); + return self.agent || self; +}; + +function Agent(name, error) { + this.connection = name; + this.isErrorBuilder = typeof(global.ErrorBuilder) !== 'undefined' ? true : false; + this.errors = this.isErrorBuilder ? error : null; + this.clear(); + this.$events = {}; + + // Hidden: + // this.$when; +} + +Agent.prototype = { + get $() { + return new SqlBuilder(0, 0, this); + }, + get $$() { + var self = this; + return function() { + return self.$id; + }; + } +}; + +Agent.embedded = function() { + require('mongodb-nosqlembedded').init(Agent); +}; + +// Debug mode (output to console) +Agent.debug = false; + +Agent.options = function(obj) { + var self = this; + Object.keys(obj).forEach(function(key) { + OPTIONS[key] = obj[key]; + }); + return self; +} + +Agent.connect = function(conn, callback) { + database.connect(conn, OPTIONS, function(err, db) { + if (err) { + if (callback) + return callback(err); + throw err; + } + + if (db.db) { + // new mongodb + if (conn[conn.length - 1] === '/') + conn = conn.substring(0, conn.length - 1); + db = db.db(conn.substring(conn.lastIndexOf('/') + 1)); + } + + CONNECTIONS[conn] = db; + callback && callback(); + }); + return function(error) { + return new Agent(conn, error); + }; +}; + +Agent.prototype.promise = function(index, fn) { + var self = this; + + if (typeof(index) === 'function') { + fn = index; + index = undefined; + } + + return new Promise(function(resolve, reject) { + self.exec(function(err, result) { + if (err) + reject(err); + else + resolve(fn ? fn(result) : result); + }, index); + }); +}; + +Agent.prototype.emit = function(name, a, b, c, d, e, f, g) { + var evt = this.$events[name]; + if (evt) { + var clean = false; + for (var i = 0, length = evt.length; i < length; i++) { + if (evt[i].$once) + clean = true; + evt[i].call(this, a, b, c, d, e, f, g); + } + if (clean) { + evt = evt.remove(n => n.$once); + if (evt.length) + this.$events[name] = evt; + else + this.$events[name] = undefined; + } + } + return this; +}; + +Agent.prototype.on = function(name, fn) { + + if (!fn.$once) + this.$free = false; + + if (this.$events[name]) + this.$events[name].push(fn); + else + this.$events[name] = [fn]; + return this; +}; + +Agent.prototype.once = function(name, fn) { + fn.$once = true; + return this.on(name, fn); +}; + +Agent.prototype.removeListener = function(name, fn) { + var evt = this.$events[name]; + if (evt) { + evt = evt.remove(n => n === fn); + if (evt.length) + this.$events[name] = evt; + else + this.$events[name] = undefined; + } + return this; +}; + +Agent.prototype.removeAllListeners = function(name) { + if (name === true) + this.$events = EMPTYOBJECT; + else if (name) + this.$events[name] = undefined; + else + this.$events[name] = {}; + return this; +}; + +Agent.prototype.clear = function() { + this.command = []; + this.db = null; + this.done = null; + this.last = null; + this.id = null; + this.$id = null; + this.isCanceled = false; + this.index = 0; + this.isPut = false; + this.skipCount = 0; + this.skips = {}; + this.$primary = '_id'; + this.results = {}; + this.builders = {}; + + if (this.$when) + this.$when = undefined; + + if (this.errors && this.isErrorBuilder) + this.errors.clear(); + else if (this.errors) + this.errors = null; + + return this; +}; + +Agent.prototype.when = function(name, fn) { + + if (!this.$when) + this.$when = {}; + + if (this.$when[name]) + this.$when[name].push(fn); + else + this.$when[name] = [fn]; + + return this; +}; + +Agent.prototype.priority = function() { + var self = this; + var length = self.command.length - 1; + + if (!length) + return self; + + var last = self.command[length]; + for (var i = length; i > -1; i--) + self.command[i] = self.command[i - 1]; + + self.command[0] = last; + return self; +}; + +Agent.prototype.default = function(fn) { + fn.call(this.results, this.results); + return this; +}; + +Agent.query = function() { + console.log('Agent.query() is not supported.'); + return Agent; +}; + +Agent.prototype.skip = function(name) { + var self = this; + if (name) + self.skips[name] = true; + else + self.skipCount++; + return self; +}; + +Agent.prototype.primaryKey = Agent.prototype.primary = function() { + console.log('Agent.primary() is not supported.'); + return this; +}; + +Agent.prototype.expected = function(name, index, property) { + + var self = this; + + if (typeof(index) === 'string') { + property = index; + index = undefined; + } + + return function() { + var output = self.results[name]; + if (!output) + return null; + if (index === undefined) + return property === undefined ? output : get(output, property); + output = output[index]; + return output ? get(output, property) : null; + }; +}; + +Agent.prototype.prepare = function(fn) { + var self = this; + self.command.push({ type: 'prepare', fn: fn }); + return self; +}; + +Agent.prototype.modify = function(fn) { + var self = this; + self.command.push({ type: 'modify', fn: fn }); + return self; +}; + +Agent.prototype.bookmark = function(fn) { + var self = this; + self.command.push({ type: 'bookmark', fn: fn }); + return self; +}; + +Agent.prototype.put = function(value) { + var self = this; + self.command.push({ type: 'put', params: value, disable: value == null }); + return self; +}; + +Agent.prototype.lock = function() { + return this.put(this.$$); +}; + +Agent.prototype.unlock = function() { + this.command.push({ 'type': 'unput' }); + return this; +}; + +Agent.prototype.query = function(name, query, params) { + return this.push(name, query, params); +}; + +Agent.prototype.push = function(name, table, fn) { + var self = this; + + if (typeof(table) !== 'string') { + fn = table; + table = name; + name = self.index++; + } + + self.command.push({ type: 'push', name: name, table: table, fn: fn }); + return self; +}; + +Agent.prototype.validate = function(fn, error, reverse) { + var self = this; + var type = typeof(fn); + + if (typeof(error) === 'boolean') { + reverse = error; + error = undefined; + } + + if (type === 'string' && error === undefined) { + // checks the last result + error = fn; + fn = undefined; + } + + if (type === 'function') { + self.command.push({ type: 'validate', fn: fn, error: error }); + return self; + } + + if (type === 'string' && typeof(error) === 'function' && typeof(reverse) === 'string') + return self.validate2(fn, error, reverse); + + var exec; + + if (reverse) { + exec = function(err, results, next) { + var id = fn == null ? self.last : fn; + if (id == null) + return next(true); + var r = results[id]; + if (r instanceof Array) + return next(r.length === 0); + if (r) + return next(false); + next(true); + }; + } else { + exec = function(err, results, next) { + var id = fn == null ? self.last : fn; + if (id == null) + return next(false); + var r = results[id]; + if (r instanceof Array) + return next(r.length > 0); + if (r) + return next(true); + next(false); + }; + } + + self.command.push({ type: 'validate', fn: exec, error: error }); + return self; +}; + +// validate2('result', n => n.length > 0, 'error'); +Agent.prototype.validate2 = function(name, fn, err) { + var self = this; + var type = typeof(fn); + + if (type === 'string') { + type = err; + err = fn; + fn = type; + } + + var validator = function(err, results, next) { + if (fn(results[name])) + return next(true); + err.push(err || name); + next(false); + }; + + self.command.push({ type: 'validate', fn: validator, error: err }); + return self; +}; + +Agent.prototype.cancel = function(fn) { + return this.validate(fn); +}; + +Agent.prototype.begin = function() { + var self = this; + console.log('Agent.begin() is not supported.'); + return self; +}; + +Agent.prototype.end = function() { + var self = this; + console.log('Agent.end() is not supported.'); + return self; +}; + +Agent.prototype.commit = function() { + console.log('Agent.commit() is not supported.'); + return this; +}; + +Agent.prototype.save = function(name, table, insert, maker) { + + if (typeof(table) === 'boolean') { + maker = insert; + insert = table; + table = name; + name = undefined; + } + + var self = this; + if (insert) { + maker(self.insert(name, table), true); + return self; + } + + var builder = self.update(name, table); + builder.first(); + maker(builder, false); + + return self; +}; + +Agent.prototype.insert = function(name, table) { + + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + var fn = function(db, builder, helper, callback) { + + builder.prepare(); + + if (!builder._set && !builder._inc) { + var err = new Error('No data for inserting.'); + builder.$callback && builder.$callback(err); + callback(err, null); + return; + } + + var data = builder.data; + + if (data.$inc) { + + if (!data.$set) + data.$set = {}; + + Object.keys(data.$inc).forEach(function(key) { + data.$set[key] = data.$inc[key]; + }); + + data.$inc = undefined; + } + + self.$events.query && self.emit('query', name, builder.debug('insert')); + + db.insertOne(data.$set, function(err, response) { + var id = response ? (response.insertedCount ? response.insertedId : null) : null; + self.id = id; + if (!self.isPut) + self.$id = self.id; + var data = id ? { identity: id } : null; + builder.$callback && builder.$callback(err, data); + callback(err, data); + }); + }; + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.listing = function(name, table) { + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + + var fn = function(db, builder, helper, callback) { + + builder.prepare(); + builder._isfirst && console.warn('You can\'t use "builder.first()" for ".listing()".'); + + var cursor = db.find(builder.builder); + cursor.project(PROJECTION); + cursor.count(function(err, count) { + + if (err) + return callback(err); + + self.$events.query && self.emit('query', name, builder.debug('listing')); + var output = {}; + output.count = count; + cursor = db.find(builder.builder); + builder._fields && cursor.project(builder._fields); + builder._order && cursor.sort(builder._order); + builder._take && cursor.limit(builder._take); + builder._skip && cursor.skip(builder._skip); + cursor.toArray(function(err, docs) { + if (err) + return callback(err); + output.items = docs; + output.page = ((builder._skip || 0) / (builder._take || 0)) + 1; + output.limit = builder._take || 0; + output.pages = Math.ceil(output.count / output.limit); + builder && builder.$callback && builder.$callback(null, output); + callback(null, output); + }); + }); + }; + + self.command.push({ type: 'query', name: name, table: table, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.select = function(name, table) { + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + + var fn = function(db, builder, helper, callback) { + + var cb = function(err, data) { + builder.$callback && builder.$callback(err, data); + callback(err, data); + }; + + builder.prepare(); + self.$events.query && self.emit('query', name, builder.debug('select')); + + if (builder._isfirst && !builder._order) { + if (builder._fields) + db.findOne(builder.builder, { projection: builder._fields }, cb); + else + db.findOne(builder.builder, cb); + } else { + var cursor = db.find(builder.builder); + builder._fields && cursor.project(builder._fields); + builder._order && cursor.sort(builder._order); + builder._take && cursor.limit(builder._take); + builder._skip && cursor.skip(builder._skip); + cursor.toArray(cb); + } + }; + + self.command.push({ type: 'query', name: name, table: table, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.compare = function(name, table, obj, keys) { + + var self = this; + + if (typeof(table) !== 'string') { + keys = obj; + obj = table; + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + condition.first(); + + var fn = function(db, builder, helper, callback) { + + var prop = keys ? keys : builder._fields ? Object.keys(builder._fields) : Object.keys(obj); + + !builder._fields && builder.fields.apply(builder, prop); + builder.prepare(); + self.$events.query && self.emit('query', name, builder.debug('compare')); + + db.findOne(builder.builder, builder._fields ? { projection: builder._fields } : null, function(err, doc) { + + if (err) + return callback(err); + + var val = doc; + var diff; + + if (val) { + diff = []; + for (var i = 0, length = prop.length; i < length; i++) { + var key = prop[i]; + var a = val[key]; + var b = obj[key]; + a !== b && diff.push(key); + } + } else + diff = prop; + + callback(null, diff.length ? { diff: diff, record: val, value: obj } : false); + }); + }; + + self.command.push({ type: 'query', name: name, table: table, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.find = Agent.prototype.builder = function(name) { + return this.builders[name]; +}; + +const EMPTYEXISTS = { projection: { _id: 1 }}; + +Agent.prototype.exists = function(name, table) { + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + condition.fields('_id'); + + var fn = function(db, builder, helper, callback) { + builder.prepare(); + self.$events.query && self.emit('query', name, builder.debug('exists')); + db.findOne(builder.builder, EMPTYEXISTS, function(err, doc) { + builder.$callback && builder.$callback(err, !!doc); + callback(err, !!doc); + }); + }; + + self.command.push({ type: 'query', name: name, table: table, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.count = function(name, table, column) { + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + condition.fields(column || '_id'); + + var fn = function(db, builder, helper, callback) { + builder.prepare(); + self.$events.query && self.emit('query', name, builder.debug('count')); + db.find(builder.builder).count(function(err, count) { + builder.$callback && builder.$callback(err, count); + callback(err, count); + }); + }; + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.max = function(name, table, column) { + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var self = this; + var fn = function(db, builder, helper, callback) { + + builder.prepare(); + builder.first(); + self.$events.query && self.emit('query', name, builder.debug('max')); + + var cursor = db.find(builder.builder); + cursor.sort(builder._order); + cursor.project(builder._fields); + cursor.limit(1); + cursor.toArray(function(err, response) { + var data = response && response.length ? response[0][helper] : 0; + builder.$callback && builder.$callback(err, data); + callback(err, data); + }); + }; + + var condition = new SqlBuilder(0, 0, self); + condition.fields(column); + condition.sort(column, true); + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn, helper: column }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.min = function(name, table, column) { + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var self = this; + var fn = function(db, builder, helper, callback) { + + builder.prepare(); + builder.first(); + self.$events.query && self.emit('query', name, builder.debug('min')); + + var cursor = db.find(builder.builder); + cursor.sort(builder._order); + cursor.project(builder._fields); + cursor.limit(1); + cursor.toArray(function(err, response) { + var data = response && response.length ? response[0][helper] : 0; + builder.$callback && builder.$callback(err, data); + callback(err, data); + }); + }; + + var condition = new SqlBuilder(0, 0, self); + condition.fields(column); + condition.sort(column, false); + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn, helper: column }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.avg = function(name) { + throw new Error('Agent.avg(' + name + ') is not supported now.'); +}; + +Agent.prototype.updateOnly = function(name, table, values) { + throw new Error('Agent.updateOnly(' + name + ') is not supported now.'); +}; + +Agent.prototype.update = function(name, table) { + + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + var fn = function(db, builder, helper, callback) { + + builder.prepare(); + + if (!builder._set && !builder._inc) { + var err = new Error('No data for update.'); + builder.$callback && builder.$callback(err); + callback(err, null); + return; + } + + self.$events.query && self.emit('query', name, builder.debug('update')); + + if (builder._isfirst) { + db.updateOne(builder.builder, builder.data, function(err, response) { + var data = response ? (response.result.nModified || response.result.n) : 0; + builder.$callback && builder.$callback(err, data); + callback(err, data); + }); + } else { + db.updateMany(builder.builder, builder.data, { multi: true }, function(err, response) { + var data = response ? (response.result.nModified || response.result.n) : 0; + builder.$callback && builder.$callback(err, data); + callback(err, data); + }); + } + }; + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.delete = function(name, table) { + + var self = this; + + if (typeof(table) !== 'string') { + table = name; + name = self.index++; + } + + var condition = new SqlBuilder(0, 0, self); + var fn = function(db, builder, helper, callback) { + builder.prepare(); + self.$events.query && self.emit('query', name, builder.debug('delete')); + if (builder._isfirst) { + db.removeOne(builder.builder, { single: true }, function(err, response) { + var data = response ? (response.result.nRemoved || response.result.n) : 0; + builder.$callback && builder.$callback(data); + callback(err, data); + }); + } else { + db.removeMany(builder.builder, function(err, response) { + var data = response ? (response.result.nRemoved || response.result.n) : 0; + builder.$callback && builder.$callback(data); + callback(err, data); + }); + } + }; + + self.command.push({ type: 'query', table: table, name: name, condition: condition, fn: fn }); + self.builders[name] = condition; + return condition; +}; + +Agent.prototype.remove = function(name, table) { + return this.delete(name, table); +}; + +Agent.prototype.ifnot = function(name, fn) { + var self = this; + self.prepare(function(error, response, resume) { + var value = response[name]; + if (value instanceof Array) { + if (value.length) + return resume(); + } else if (value) + return resume(); + fn.call(self, error, response, value); + setImmediate(resume); + }); + return self; +}; + +Agent.prototype.ifexists = function(name, fn) { + var self = this; + self.prepare(function(error, response, resume) { + + var value = response[name]; + if (value instanceof Array) { + if (!value.length) + return resume(); + } else if (!value) + return resume(); + + fn.call(self, error, response, value); + setImmediate(resume); + }); + return self; +}; + +Agent.prototype.destroy = function(name) { + var self = this; + for (var i = 0, length = self.command.length; i < length; i++) { + var item = self.command[i]; + if (item.name !== name) + continue; + self.command.splice(i, 1); + delete self.builders[name]; + return true; + } + return false; +}; + +Agent.prototype.close = function() { + var self = this; + self.done && self.done(); + self.done = null; + return self; +}; + +Agent.prototype.rollback = function(where, e, next) { + var self = this; + self.errors && self.errors.push(e); + next(); +}; + +Agent.prototype._prepare = function(callback) { + + var self = this; + + if (!self.errors) + self.errors = self.isErrorBuilder ? new global.ErrorBuilder() : []; + + self.command.sqlagent(function(item, next) { + + if (item.type === 'validate') { + try { + var tmp = item.fn(self.errors, self.results, function(output) { + if (output === true || output === undefined) + return next(); + // reason + if (typeof(output) === 'string') + self.errors.push(output); + else if (item.error) + self.errors.push(item.error); + next(false); + }); + + var type = typeof(tmp); + if (type !== 'boolean' && type !== 'string') + return; + if (tmp === true || tmp === undefined) + return next(); + if (typeof(tmp) === 'string') + self.errors.push(tmp); + else if (item.error) + self.errors.push(item.error); + next(false); + } catch (e) { + self.rollback('validate', e, next); + } + return; + } + + if (item.type === 'bookmark') { + try { + item.fn(self.errors, self.results); + return next(); + } catch (e) { + self.rollback('bookmark', e, next); + } + } + + if (item.type === 'primary') { + self.$primary = item.name; + next(); + return; + } + + if (item.type === 'modify') { + try { + item.fn(self.results); + next(); + } catch (e) { + self.rollback('modify', e, next); + } + return; + } + + if (item.type === 'prepare') { + try { + item.fn(self.errors, self.results, () => next()); + } catch (e) { + self.rollback('prepare', e, next); + } + return; + } + + if (item.type === 'unput') { + self.isPut = false; + next(); + return; + } + + if (item.type === 'put') { + if (item.disable) + self.$id = null; + else + self.$id = typeof(item.params) === 'function' ? item.params() : item.params; + self.isPut = !self.disable; + next(); + return; + } + + if (self.skipCount) { + self.skipCount--; + next(); + return; + } + + if (typeof(item.name) === 'string') { + if (self.skips[item.name] === true) { + next(); + return; + } + } + + if (item.type === 'push') { + item.fn(self.db.collection(item.table), function(err, response) { + + self.last = item.name; + + if (err) { + self.errors.push(item.name + ': ' + err.message); + next(); + return; + } + + self.results[item.name] = response; + self.$events.data && self.emit('data', item.name, response); + next(); + }); + return; + } + + if (item.type !== 'query') { + next(); + return; + } + + item.fn(self.db.collection(item.table), item.condition, item.helper, function(err, response) { + + self.last = item.name; + + if (err) { + self.errors.push(item.name + ': ' + err.message); + next(); + return; + } + + var val = item.condition._isfirst && item.condition._order ? (response instanceof Array ? response[0] : response) : response; + self.results[item.name] = val; + self.$events.data && self.emit('data', item.name, val); + + if (!self.$when) { + next(); + return; + } + + var tmp = self.$when[item.name]; + if (tmp) { + for (var i = 0, length = tmp.length; i < length; i++) + tmp[i](self.errors, self.results, self.results[item.name]); + } + next(); + }); + + }, function() { + + if (Agent.debug || self.debug) { + self.time = Date.now() - self.debugtime; + console.log(self.debugname, '----- done (' + self.time + ' ms)'); + } + + self.index = 0; + self.done && self.done(); + self.done = null; + var err = null; + + if (self.isErrorBuilder) { + if (self.errors.hasError()) + err = self.errors; + } else if (self.errors.length) + err = self.errors; + + self.$events.end && self.emit('end', err, self.results, self.time); + callback && callback(err, self.returnIndex !== undefined ? self.results[self.returnIndex] : self.results); + }); + + return self; +}; + +Agent.prototype.exec = function(callback, returnIndex) { + + var self = this; + + if (Agent.debug || self.debug) { + self.debugname = 'sqlagent/mongodb (' + Math.floor(Math.random() * 1000) + ')'; + self.debugtime = Date.now(); + } + + if (returnIndex !== undefined && typeof(returnIndex) !== 'boolean') + self.returnIndex = returnIndex; + else + self.returnIndex = undefined; + + if (!self.command.length) { + callback && callback.call(self, null, {}); + return self; + } + + (Agent.debug || self.debug) && console.log(self.debugname, '----- exec'); + + connect(self.connection, function(err, db) { + + if (err) { + !self.errors && (self.errors = self.isErrorBuilder ? new global.ErrorBuilder() : []); + self.errors.push(err); + callback && callback.call(self, self.errors, {}); + return; + } + + self.db = db; + self._prepare(callback); + }); + + return self; +}; + +function connect(conn, callback, index) { + var db = CONNECTIONS[conn]; + if (db) + return callback(null, db); + + if (index > 60) { + callback(new Error('SQLAgent: timeout to connect into the database.')); + return; + } + + if (index === undefined) + index = 1; + + setTimeout(() => connect(conn, callback, index + 1), 100); +} + +Agent.prototype.$$exec = function(returnIndex) { + var self = this; + return function(callback) { + return self.exec(callback, returnIndex); + }; +}; + +Agent.destroy = function() { + throw new Error('Not supported.'); +}; + +Agent.prototype.readFile = function(id, options, callback) { + + if (typeof(options) === 'function') { + callback = options; + options = null; + } + + connect(this.connection, function(err, db) { + + if (err) + return callback(err); + + var bucket = new database.GridFSBucket(db, options); + + if (bucket.openUploadStream) { + console.error('SQLAgent error: readFile() is not supported for MongoDB, use readStream().'); + } else { + FILEREADERFILTER._id = id; + bucket.find(FILEREADERFILTER).nextObject(function(err, doc) { + if (!err && !doc) + err = new Error('File not found.'); + if (err) + return callback(err, null, NOOP); + callback(null, new GridFSObject(doc._id, doc.metadata, doc.filename, doc.length, doc.contentType, bucket), NOOP, doc.metadata, doc.length, doc.filename); + }); + } + }); +}; + +Agent.prototype.readStream = function(id, options, callback) { + + if (typeof(options) === 'function') { + callback = options; + options = null; + } + + connect(this.connection, function(err, db) { + + if (err) + return callback(err); + + var bucket = new database.GridFSBucket(db, options); + + if (bucket.openUploadStream) { + FILEREADERFILTER._id = id; + db.collection('fs.files').findOne(FILEREADERFILTER, function(err, doc) { + if (!err && !doc) + err = new Error('File not found.'); + if (err) + return callback(err, null, NOOP); + + callback(null, bucket.openDownloadStream(id), doc.metadata, doc.length, doc.filename); + }); + } else { + FILEREADERFILTER._id = id; + + bucket.find(FILEREADERFILTER).nextObject(function(err, doc) { + if (!err && !doc) + err = new Error('File not found.'); + if (err) + return callback(err); + callback(null, new GridFSObject(doc._id, doc.metadata, doc.filename, doc.length, doc.contentType, bucket).stream(true), doc.metadata, doc.length, doc.filename); + }); + } + }); + + return this; +}; + +Agent.prototype.writeFile = function(id, file, name, meta, options, callback) { + var self = this; + + if (typeof(options) === 'function') { + callback = options; + options = null; + } + + if (typeof(meta) === 'function') { + var tmp = callback; + callback = meta; + meta = tmp; + } + + connect(self.connection, function(err, db) { + + if (err) { + self.errors && self.errors.push(err); + return callback(err); + } + + var bucket = new database.GridFSBucket(db, options); + var upload = bucket.openUploadStreamWithId(id, name, meta ? { metadata: meta } : undefined); + var stream = typeof(file.pipe) === 'function' ? file : Fs.createReadStream(file); + + stream.pipe(upload).once('finish', function() { + callback && callback(null); + callback = null; + }).once('error', function(err) { + self.errors && self.errors.push(err); + callback && callback(err); + callback = null; + }); + }); + + return self; +}; + +Agent.prototype.writeStream = function(id, stream, name, meta, options, callback) { + var self = this; + + if (!callback) + callback = NOOP; + + if (typeof(options) === 'function') { + callback = options; + options = null; + } + + if (typeof(meta) === 'function') { + var tmp = callback; + callback = meta; + meta = tmp; + } + + connect(self.connection, function(err, db) { + + if (err) { + self.errors && self.errors.push(err); + callback && callback(err); + return; + } + + var bucket = new database.GridFSBucket(db, options); + var upload = bucket.openUploadStreamWithId(id, name, meta ? { metadata: meta } : undefined); + + upload.once('finish', function(err) { + self.errors && self.errors.push(err); + callback && callback(err); + callback = null; + }); + + stream.pipe(upload); + }); + + return self; +}; + +Agent.prototype.writeBuffer = function(id, buffer, name, meta, options, callback) { + var self = this; + + if (!callback) + callback = NOOP; + + if (typeof(options) === 'function') { + callback = options; + options = null; + } + + if (typeof(meta) === 'function') { + var tmp = callback; + callback = meta; + meta = tmp; + } + + connect(self.connection, function(err, db) { + + if (err) { + self.errors && self.errors.push(err); + callback && callback(err); + return; + } + + var bucket = new database.GridFSBucket(db, options); + var upload = bucket.openUploadStreamWithId(id, name, meta ? { metadata: meta } : undefined); + + upload.end(buffer, function(err) { + self.errors && self.errors.push(err); + callback && callback(err); + callback = null; + }); + }); + + return self; +}; + +Agent.init = function(conn, debug) { + Agent.debug = debug ? true : false; + + F.wait('database'); + + database.connect(conn, OPTIONS, function(err, db) { + if (err) + throw err; + + if (db.db) { + // new mongodb + if (conn[conn.length - 1] === '/') + conn = conn.substring(0, conn.length - 1); + db = db.db(conn.substring(conn.lastIndexOf('/') + 1)); + } + + CONNECTIONS[conn] = db; + F.wait('database'); + EMIT('database', conn); + }); + + F.database = function(errorBuilder) { + return new Agent(conn, errorBuilder); + }; + + return Agent; +}; + +module.exports = Agent; +global.SqlBuilder = SqlBuilder; +global.ObjectID = database.ObjectID; +global.GridStore = database.GridStore; + +ObjectID.parse = function(value, isArray) { + if (value instanceof ObjectID) + return value; + if (isArray || value instanceof Array) + return ObjectID.parseArray(value); + try { + return new ObjectID(value); + } catch (e) { + return null; + } +}; + +ObjectID.parseArray = function(value) { + + if (typeof(value) === 'string') + value = value.split(','); + + var arr = []; + + if (!(value instanceof Array)) + return arr; + + for (var i = 0, length = value.length; i < length; i++) { + var id = ObjectID.parse(value[i]); + id && arr.push(id); + } + + return arr; +}; + +function get(obj, path) { + + var cachekey = '=' + path; + + if (columns_cache[cachekey]) + return columns_cache[cachekey](obj); + + var arr = path.split('.'); + var builder = []; + var p = ''; + + for (var i = 0, length = arr.length - 1; i < length; i++) { + var tmp = arr[i]; + var index = tmp.lastIndexOf('['); + if (index !== -1) + builder.push('if(!w.' + (p ? p + '.' : '') + tmp.substring(0, index) + ')return'); + p += (p !== '' ? '.' : '') + arr[i]; + builder.push('if(!w.' + p + ')return'); + } + + var fn = (new Function('w', builder.join(';') + ';return w.' + path.replace(REG_APO, '\''))); + columns_cache[cachekey] = fn; + return fn(obj); +} + +function GridFSObject(id, meta, filename, length, type, bucket) { + this.id = id; + this.meta = meta; + this.filename = filename; + this.length = length; + this.type = type; + this.bucket = bucket; +} + +GridFSObject.prototype.stream = function() { + return this.bucket.openDownloadStream(this.id); +};