caminte
Version:
ORM for every database: redis, mysql, neo4j, mongodb, rethinkdb, postgres, sqlite, tingodb
865 lines (800 loc) • 28.2 kB
JavaScript
/**
* Module dependencies
*/
var utils = require('../utils');
var safeRequire = utils.safeRequire;
var sqlite3 = safeRequire('sqlite3');
var util = require('util');
var BaseSQL = require('../sql');
exports.initialize = function initializeSchema(schema, callback) {
if (!sqlite3) {
return;
}
var s = schema.settings;
var Database = sqlite3.verbose().Database;
var db = new Database(s.database);
schema.client = db;
schema.adapter = new SQLite3(schema, schema.client);
schema.client.run('PRAGMA encoding = "UTF-8"', function () {
if (s.database === ':memory:') {
schema.adapter.automigrate(callback);
} else {
process.nextTick(callback);
}
});
};
function SQLite3(schema, client) {
this.name = 'sqlite3';
this._models = {};
this.client = client;
this.schema = schema;
}
util.inherits(SQLite3, BaseSQL);
SQLite3.prototype.query = function (sql, callback) {
this.queryAll(sql, callback);
};
SQLite3.prototype.command = function () {
this.run('run', [].slice.call(arguments));
};
SQLite3.prototype.execSql = function () {
this.run('exec', [].slice.call(arguments));
};
SQLite3.prototype.queryAll = function () {
this.run('all', [].slice.call(arguments));
};
SQLite3.prototype.queryOne = function () {
this.run('get', [].slice.call(arguments));
};
SQLite3.prototype.run = function (method, args) {
var time = Date.now();
var log = this.log;
var cb = args.pop();
if (typeof cb === 'function') {
args.push(function (err, data) {
if (log)
log(args[0], time);
cb.call(this, err, data);
});
} else {
args.push(cb);
args.push(function (err, data) {
log(args[0], time);
});
}
this.client[method].apply(this.client, args);
};
SQLite3.prototype.save = function (model, data, callback) {
var queryParams = [];
var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' +
Object.keys(data).map(function (key) {
queryParams.push(data[key]);
return key + ' = ?';
}).join(', ') + ' WHERE id = ' + data.id;
this.command(sql, queryParams, function (err) {
callback(err);
});
};
/**
* Must invoke callback(err, id)
* @param {Object} model
* @param {Object} data
* @param {Function} callback
*/
SQLite3.prototype.create = function (model, data, callback) {
data = data || {};
var questions = [];
var values = Object.keys(data).map(function (key) {
questions.push('?');
return data[key];
});
var sql = 'INSERT INTO ' + this.tableEscaped(model) + ' (' + Object.keys(data).join(',') + ') VALUES (';
sql += questions.join(',');
sql += ')';
this.command(sql, values, function (err) {
callback(err, this && this.lastID);
});
};
SQLite3.prototype.updateOrCreate = function (model, filter, data, callback) {
filter = filter || {};
var self = this, Model = self._models[model].model;
self.all(model, {where: filter}, function (err, found) {
if (err || (found && found.length > 1)) {
return callback(err || new Error("Found multiple instances"));
}
if (found && found.length === 1) {
var inst = self.fromDatabase(model, found[0]);
var obj = new Model();
obj._initProperties(inst, false);
obj.updateAttributes(data, function (err) {
callback(err, obj);
});
} else {
Object.keys(filter).forEach(function (key) {
data[key] = filter[key];
});
self.create(model, data, callback)
}
});
};
/**
* Update rows
* @param {String} model
* @param {Object} filter
* @param {Object} data
* @param {Function} callback
*/
SQLite3.prototype.update = function (model, filter, data, callback) {
if ('function' === typeof filter) {
return filter(new Error("Get parametrs undefined"), null);
}
if ('function' === typeof data) {
return data(new Error("Set parametrs undefined"), null);
}
filter = filter.where ? filter.where : filter;
var self = this;
var combined = [];
var queryParams = [];
var props = self._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key] || key === 'id') {
var k = '`' + key + '`';
var v;
if (key !== 'id') {
v = self.toDatabase(props[key], data[key]);
} else {
v = data[key];
}
combined.push(k + ' = ' + v);
}
});
var sql = 'UPDATE ' + self.tableEscaped(model);
sql += ' SET ' + combined.join(', ');
sql += ' ' + self.buildWhere(filter, self, model);
self.command(sql, queryParams, function (err, affected) {
return callback && callback(err, affected || 0);
});
};
SQLite3.prototype.toFields = function (model, data) {
var self = this, fields = [];
var props = this._models[model].properties;
Object.keys(data).forEach(function (key) {
if (props[key]) {
fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + self.toDatabase(props[key], data[key]));
}
}.bind(self));
return fields.join(',');
};
SQLite3.prototype.toDatabase = function (prop, val) {
if (val === null || val === undefined) {
return 'NULL';
}
if (val.constructor.name === 'Object') {
var operator = Object.keys(val)[0];
val = val[operator];
if (operator === 'between') {
if (prop.type.name === 'Date') {
return 'strftime(' + this.toDatabase(prop, val[0]) + ')' +
' AND strftime(' +
this.toDatabase(prop, val[1]) + ')';
} else {
return this.toDatabase(prop, val[0]) +
' AND ' +
this.toDatabase(prop, val[1]);
}
} else if (operator === 'in' || operator === 'inq' || operator === 'nin') {
if (!(val.propertyIsEnumerable('length')) && typeof val === 'object' && typeof val.length === 'number') { //if value is array
for (var i = 0; i < val.length; i++) {
val[i] = this.escape(val[i]);
}
return val.join(',');
} else {
return val;
}
}
}
if (!prop)
return val;
if (prop.type.name === 'Number' || prop.type.name === 'Integer' || prop.type.name === 'Real')
return val;
if (prop.type.name === 'Date') {
if (!val) {
return 'NULL';
}
if (!val.toUTCString) {
val = new Date(val).getTime();
} else if (val.getTime) {
val = val.getTime();
}
return val;
}
if (prop.type.name === "Boolean") {
return val ? 1 : 0;
}
val = val.toString();
return /^"(?:\\"|.)*?"$/gi.test(val) ? val : this.escape(val);
};
SQLite3.prototype.fromDatabase = function (model, data) {
var self = this;
if (!data) {
return null;
}
var props = self._models[model].properties;
Object.keys(data).forEach(function (key) {
var val = data[key], type = (props[key].type.name || '').toString().toLowerCase();
if (props[key]) {
if (type === 'json' && typeof val == "string") {
if ((props[key].type.name || '').toString().toLowerCase() === 'json' && typeof val == "string") {
try {
data[key] = JSON.parse(val);
} catch (err) {
data[key] = val;
}
}
} else if (type === 'date') {
if (!val) {
val = null;
}
if (typeof val === 'string') {
val = val.split('.')[0].replace('T', ' ');
val = Date.parse(val);
}
if (typeof val === 'number') {
val = new Date(val);
}
data[key] = val;
} else {
data[key] = val;
}
}
});
return data;
};
SQLite3.prototype.escape = function (val) {
return typeof val === 'string' ? '"' + val + '"' : val;
};
SQLite3.prototype.escapeName = function (name) {
return '`' + name + '`';
};
SQLite3.prototype.exists = function (model, id, callback) {
var sql = 'SELECT 1 FROM ' + this.tableEscaped(model) + ' WHERE id = ' + id + ' LIMIT 1';
this.queryOne(sql, function (err, data) {
if (err) {
return callback(err);
}
callback(null, data && data['1'] === 1);
});
};
SQLite3.prototype.findById = function findById(model, id, callback) {
var sql = 'SELECT * FROM ' + this.tableEscaped(model) + ' WHERE id = ' + id + ' LIMIT 1';
this.queryOne(sql, function (err, data) {
if (data) {
data.id = id;
} else {
data = null;
}
callback(err, this.fromDatabase(model, data));
}.bind(this));
};
SQLite3.prototype.all = function all(model, filter, callback) {
if ('function' === typeof filter) {
callback = filter;
filter = {};
}
if (!filter) {
filter = {};
}
var sql = 'SELECT * FROM ' + this.tableEscaped(model);
var self = this, queryParams = [];
if (filter) {
if (filter.where) {
sql += ' ' + this.buildWhere(filter.where, self, model);
}
if (filter.order) {
sql += ' ' + this.buildOrderBy(filter.order);
}
if (filter.group) {
sql += ' ' + self.buildGroupBy(filter.group);
}
if (filter.limit) {
sql += ' ' + this.buildLimit(filter.limit, filter.offset || filter.skip || 0);
}
}
self.queryAll(sql, function (err, data) {
if (err) {
return callback(err, []);
}
data = data.map(function (obj) {
return self.fromDatabase(model, obj);
}.bind(self));
return callback && callback(null, data);
}.bind(self));
};
SQLite3.prototype.disconnect = function disconnect() {
this.client.close();
};
SQLite3.prototype.autoupdate = function (cb) {
var self = this;
var wait = 0;
Object.keys(this._models).forEach(function (model) {
wait += 1;
self.queryAll('PRAGMA TABLE_INFO(' + self.tableEscaped(model) + ');', function (err, fields) {
if (err) done(err);
self.queryAll('PRAGMA INDEX_LIST(' + self.tableEscaped(model) + ');', function (err, indexes) {
if (err) done(err);
if (!err && fields.length) {
self.alterTable(model, fields, indexes, done);
} else {
self.createTable(model, indexes, done);
}
});
});
});
function done(err) {
if (err) {
console.log(err);
}
if (--wait === 0 && cb) {
cb(err);
}
}
};
SQLite3.prototype.isActual = function (cb) {
var ok = false;
var self = this;
var wait = 0;
Object.keys(this._models).forEach(function (model) {
wait += 1;
self.queryAll('PRAGMA TABLE_INFO(' + self.tableEscaped(model) + ')', function (err, fields) {
self.queryAll('PRAGMA INDEX_LIST(' + self.tableEscaped(model) + ')', function (err, indexes) {
if (!err && fields.length) {
self.alterTable(model, fields, indexes, done, true);
}
});
});
});
function done(err, needAlter) {
if (err) {
console.log(err);
}
ok = ok || needAlter;
if (--wait === 0 && cb) {
cb(null, !ok);
}
}
};
SQLite3.prototype.alterTable = function (model, actualFields, indexes, done, checkOnly) {
var self = this, m = self._models[model];
var defIndexes = m.settings.indexes;
var propNames = Object.keys(m.properties);
var sql = [], isql = [], reBuild = false;
// change/add new fields
propNames.forEach(function (propName) {
if (propName === 'id') {
return;
}
var found;
actualFields.forEach(function (f) {
if (f.name === propName) {
found = f;
}
});
if (found) {
actualize(propName, found);
} else {
if (m.properties[propName] !== false) {
sql.push('ADD COLUMN `' + propName + '` ' + self.propertySettingsSQL(model, propName));
}
}
});
// drop columns
actualFields.forEach(function (f) {
var notFound = !~propNames.indexOf(f.name);
if (f.name === 'id') {
return;
}
if (notFound || !m.properties[f.name]) {
reBuild = true;
}
});
for (var fieldName in m.properties) {
var idx = m.properties[fieldName];
if ('undefined' !== typeof idx['index']
|| 'undefined' !== typeof idx['unique']) {
var foundKey = false, UNIQ = '',
kuniq = !idx['unique'] ? 0 : idx['unique'],
ikey = (model + '_' + fieldName).toString();
kuniq = kuniq === false ? 0 : 1;
if (idx['index'] !== false) {
indexes.forEach(function (index) {
if (ikey === index.name) {
if (index.unique !== kuniq) {
UNIQ = kuniq === 1 ? ' UNIQUE ' : '';
isql.push('DROP INDEX `' + ikey + '`;');
// isql.push('CREATE ' + UNIQ + ' INDEX `' + ikey + '` ON ' + self.tableEscaped(model) + ' (`' + fieldName + '` ASC);');
reBuild = true;
}
foundKey = index.name;
}
});
if (!foundKey) {
UNIQ = 'undefined' !== typeof m.properties[fieldName]['unique'] ? ' UNIQUE ' : '';
isql.push('CREATE ' + UNIQ + ' INDEX `' + ikey + '` ON ' + self.tableEscaped(model) + ' (`' + fieldName + '` ASC);');
}
} else {
reBuild = true;
}
}
}
if (defIndexes) {
for (var fieldName in defIndexes) {
var foundKey = false, ikey = (model + '_' + fieldName).toString();
indexes.forEach(function (index) {
if (ikey === index.name) {
foundKey = index.name;
}
});
if (!foundKey) {
var fields = [], columns = defIndexes[fieldName]['columns'] || [];
if (Object.prototype.toString.call(columns) === '[object Array]') {
fields = columns;
} else if (typeof columns === 'string') {
columns = (columns || '').replace(/,/g,' ').split(/\s+/);
}
if (columns.length) {
columns = columns.map(function (column) {
return '`'+column.replace(/,/g,'') + '` ASC';
});
var UNIQ = 'undefined' !== typeof defIndexes[fieldName]['unique'] ? ' UNIQUE ' : '';
isql.push('CREATE ' + UNIQ + ' INDEX `' + ikey + '` ON ' + self.tableEscaped(model) + ' (' + columns.join(',') + ');');
}
}
}
}
var tSql = [];
if (sql.length) {
tSql.push('ALTER TABLE ' + self.tableEscaped(model) + ' ' + sql.join(',\n'));
}
if (isql.length) {
tSql = tSql.concat(isql);
}
if (tSql.length) {
if (checkOnly) {
return done(null, true, {
statements: tSql,
query: ''
});
} else {
var tlen = tSql.length;
tSql.forEach(function (tsql) {
return self.command(tsql, function (err) {
if (err) console.log(err, tsql);
if (--tlen === 0) {
if (reBuild) {
return rebuid(model, m.properties, actualFields, indexes, done);
} else {
return done();
}
}
});
});
}
} else {
if (checkOnly) {
return done(null, reBuild, {
statements: tSql,
query: ''
});
} else {
if (reBuild) {
return rebuid(model, m.properties, actualFields, indexes, done);
} else {
return done && done();
}
}
}
function actualize(propName, oldSettings) {
var newSettings = m.properties[propName];
if (newSettings && changed(newSettings, oldSettings)) {
reBuild = true;
}
}
function changed(newSettings, oldSettings) {
var dflt_value = (newSettings.default || null);
var notnull = (newSettings.null === false ? 1 : 0);
if (oldSettings.notnull !== notnull
|| oldSettings.dflt_value !== dflt_value) {
return true;
}
if (oldSettings.type.toUpperCase() !== datatype(newSettings)) {
return true;
}
return false;
}
function rebuid(model, oldSettings, newSettings, indexes, done) {
var nsst = [];
if (newSettings) {
newSettings.forEach(function (newSetting) {
if (oldSettings[newSetting.name] !== false) {
nsst.push(newSetting.name);
}
});
}
var rbSql = 'ALTER TABLE `' + model + '` RENAME TO `tmp_' + model + '`;';
var inSql = 'INSERT INTO `' + model + '` (' + nsst.join(',') + ') '
+ 'SELECT ' + nsst.join(',') + ' FROM `tmp_' + model + '`;';
var dpSql = 'DROP TABLE `tmp_' + model + '`;';
return self.command(rbSql, function (err) {
if (err) console.log(err, rbSql);
return self.createTable(model, indexes, function (err) {
if (err) console.log('createTable', err);
return self.command(inSql, function (err) {
if (err) console.log(err, inSql);
return self.command(dpSql, function () {
if (err) console.log(err, dpSql);
self.createIndexes(model, self._models[model], done)
});
});
});
});
}
};
/**
* Create multi column index callback(err, index)
* @param {Object} model
* @param {Object} fields
* @param {Object} params
* @param {Function} callback
*/
SQLite3.prototype.ensureIndex = function (model, fields, params, done) {
var self = this, sql = "", keyName = params.name || null, afld = [], kind = "";
Object.keys(fields).forEach(function (field) {
if (!keyName) {
keyName = model + '_' + field;
}
afld.push('`' + field + '` ASC');
});
if (params.unique) {
kind = "UNIQUE";
}
sql += 'CREATE ' + kind + ' INDEX `' + keyName + '` ON ' + self.tableEscaped(model) + ' (' + afld.join(', ') + ')';
self.command(sql, done);
};
/**
* Create index callback(err, index)
* @param {Object} model
* @param {Object} fields
* @param {Object} params
* @param {Function} callback
*/
SQLite3.prototype.createIndexes = function (model, props, done) {
var self = this, sql = [], m = props, s = m.settings;
for (var fprop in m.properties) {
var idx = m.properties[fprop];
if ('undefined' !== typeof idx['index']
|| 'undefined' !== typeof idx['unique']) {
if (idx['index'] !== false) {
var UNIQ = 'undefined' !== typeof m.properties[fprop]['unique'] ? ' UNIQUE ' : '';
sql.push('CREATE ' + UNIQ + ' INDEX `' + model + '_' + fprop + '` ON ' + self.tableEscaped(model) + ' (`' + fprop + '` ASC)');
}
}
}
if (s.indexes) {
for (var tprop in s.indexes) {
var fields = [], columns = s.indexes[tprop]['columns'] || [];
if (Object.prototype.toString.call(columns) === '[object Array]') {
fields = columns;
} else if (typeof columns === 'string') {
columns = (columns || '').replace(',', ' ').split(/\s+/);
}
if (columns.length) {
columns = columns.map(function (column) {
return '`' + column + '` ASC';
});
var UNIQ = 'undefined' !== typeof s.indexes[tprop]['unique'] ? ' UNIQUE ' : '';
sql.push(' CREATE ' + UNIQ + ' INDEX `' + model + '_' + tprop + '` ON ' + self.tableEscaped(model) + ' (' + columns.join(', ') + ')');
}
}
}
if (sql.length) {
var tsqls = sql.length;
sql.forEach(function (query) {
self.command(query, function () {
if (--tsqls === 0) done();
});
});
} else {
done();
}
};
SQLite3.prototype.propertiesSQL = function (model) {
var self = this, id = false, sql = [], props = Object.keys(self._models[model].properties);
var primaryKeys = this._models[model].settings.primaryKeys || [];
primaryKeys = primaryKeys.slice(0);
props.forEach(function (prop) {
if (prop === 'id') {
return;
}
if (self._models[model].properties[prop] !== false) {
return sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop));
}
});
if (primaryKeys.length) {
for (var i = 0, length = primaryKeys.length; i < length; i++) {
if (props.indexOf(primaryKeys[i]) === -1) {
if (primaryKeys[i] === 'id') {
id = true;
sql.push('`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL');
} else {
sql.push('`' + primaryKeys[i].toString().replace(/,\s|,/,'`,`') + '` ' + self.propertySettingsSQL(model, primaryKeys[i]));
}
}
}
if (!id) {
sql.push('PRIMARY KEY (`' + primaryKeys.join('`, `').toString().replace(/,\s|,/,'`,`') + '`)');
}
} else {
if (!id) {
sql.push('`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL');
}
}
return sql.join(',\n ');
};
SQLite3.prototype.propertySettingsSQL = function (model, prop) {
'use strict';
var p = this._models[ model ].properties[ prop ], field = [];
field.push( datatype( p ) );
field.push( p.allowNull === false || (typeof p[ 'default' ] !== 'undefined' && acceptedDefaults( p )) ? 'NOT NULL' : 'NULL' );
if ( typeof p[ 'default' ] !== 'undefined' && acceptedDefaults( p ) && typeof p[ 'default' ] !== 'function' ) {
field.push( 'DEFAULT ' + getDefaultValue( p ) );
}
if ( p.unique === true ) {
field.push( 'UNIQUE' );
}
return field.join( " " );
};
function acceptedDefaults(prop) {
'use strict';
if ( /^INT|^BIGINT|^VAR|^TINY/i.test( datatype( prop ) ) ) {
return true;
} else {
return false;
}
}
function getDefaultValue(prop) {
'use strict';
if ( /^INT|^BIGINT/i.test( prop.Type || datatype( prop ) ) ) {
return parseInt( prop[ 'default' ] || prop[ 'Default' ] || 0 );
} else if ( /^TINY/i.test( prop.Type || datatype( prop ) ) ) {
return prop[ 'default' ] || prop[ 'Default' ] ? 1 : 0;
} else {
return "'" + (prop[ 'default' ] || prop[ 'Default' ] || '') + "'";
}
}
function datatype(p) {
switch ((p.type.name || 'string').toLowerCase()) {
case 'string':
case 'varchar':
return 'VARCHAR(' + (p.limit || 255) + ')';
case 'int':
case 'integer':
case 'number':
return 'INTEGER(' + (p.limit || 11) + ')';
case 'real':
case 'float':
case 'double':
return 'REAL';
case 'date':
case 'timestamp':
return 'DATETIME';
case 'boolean':
case 'bool':
return 'BOOL';
default:
return 'TEXT';
}
}
SQLite3.prototype.buildWhere = function buildWhere(conds, adapter, model) {
var cs = [], or = [],
self = adapter,
props = self._models[model].properties;
Object.keys(conds).forEach(function (key) {
if (key !== 'or') {
cs = parseCond(cs, key, props, conds, self);
} else {
conds[key].forEach(function (oconds) {
Object.keys(oconds).forEach(function (okey) {
or = parseCond(or, okey, props, oconds, self);
});
});
}
});
if (cs.length === 0 && or.length === 0) {
return '';
}
var orop = "";
if (or.length) {
orop = ' (' + or.join(' OR ') + ') ';
}
orop += (orop !== "" && cs.length > 0) ? ' AND ' : '';
return 'WHERE ' + orop + cs.join(' AND ');
};
function parseCond(cs, key, props, conds, self) {
var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`';
var val = self.toDatabase(props[key], conds[key]);
if (conds[key] === null || conds[key] === undefined) {
cs.push(keyEscaped + ' IS NULL');
} else if (conds[key].constructor.name === 'Object') {
Object.keys(conds[key]).forEach(function (condType) {
var inq = 'in,inq,nin'.indexOf(condType) > -1 ? 1 : 0;
val = self.toDatabase(props[key], conds[key][condType]);
var sqlCond = keyEscaped;
if (inq === 1 && val.length === 0) {
cs.push(condType === 'inq' ? 0 : 1);
return true;
}
switch (condType) {
case 'gt':
sqlCond += ' > ';
break;
case 'gte':
sqlCond += ' >= ';
break;
case 'lt':
sqlCond += ' < ';
break;
case 'lte':
sqlCond += ' <= ';
break;
case 'between':
sqlCond += ' BETWEEN ';
val = self.toDatabase(props[key], conds[key]);
break;
case 'inq':
case 'in':
sqlCond += ' IN ';
break;
case 'nin':
sqlCond += ' NOT IN ';
break;
case 'neq':
case 'ne':
sqlCond += ' != ';
break;
case 'regex':
sqlCond += ' REGEXP ';
break;
case 'like':
val = (val || '').replace(new RegExp('%25', 'gi'), '%');
sqlCond += ' LIKE ';
break;
case 'nlike':
val = (val || '').replace(new RegExp('%25', 'gi'), '%');
sqlCond += ' NOT LIKE ';
break;
default:
sqlCond += ' ' + condType + ' ';
break;
}
sqlCond += inq === 1 ? '(' + val + ')' : val;
cs.push(sqlCond);
});
} else {
cs.push(keyEscaped + ' = ' + val);
}
return cs;
}
SQLite3.prototype.buildOrderBy = function buildOrderBy(order) {
if (typeof order === 'string') {
order = [order];
}
return 'ORDER BY ' + order.join(', ');
};
SQLite3.prototype.buildLimit = function buildLimit(limit, offset) {
return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit);
};
SQLite3.prototype.buildGroupBy = function buildGroupBy(group) {
if (typeof group === 'string') {
group = [group];
}
return 'GROUP BY ' + group.join(', ');
};