UNPKG

loopback-connector-sqlite

Version:

LoopBack SQLite3 Database Connector. LoopBack is an API Mgmt/MBaas platform built on top of express by StrongLoop. It supports the notion of connectors to connect to databases, restful web services, soap web services etc.

1,002 lines (879 loc) 26 kB
/** * Created by tushar-chauhan on 13-Mar-2015 */ var SqlConnector = require('loopback-connector').SqlConnector, sqlite3db = require('sqlite3'), util = require('util'), async = require('async'), debug = require('debug')('loopback:connector:sqlite'); var NAME = 'sqlite3'; /** * Initialize the SQLite connector for the given data source * @param {DataSource} dataSource The data source instance * @param {Function} [callback] The callback function */ exports.initialize = function initializeDataSource(dataSource, callback) { if (!sqlite3db) { return; } dataSource.driver = sqlite3db; var dbSettings = dataSource.settings || {}; var connector = new SQLiteDB(sqlite3db, dbSettings); dataSource.connector = connector; dataSource.connector.dataSource = dataSource; dataSource.connector.dataSource.log = console.log; if(callback){ dataSource.connecting = true; dataSource.connector.connect(callback); // process.nextTick(callback); } }; /** * Constructor for SQLite connector * @param {Object} settings The settings object * @param {DataSource} dataSource The data source * instance * @constructor */ var SQLiteDB = function(sqlite3, dbSettings) { if (!(this instanceof SQLiteDB)) { return new SQLiteDB(sqlite3, dbSettings); } this.constructor.super_.call(this, NAME, dbSettings); this.name = NAME; this.settings = dbSettings; this.sqlite3 = sqlite3; this.debug = dbSettings.debug; this.current_order = []; this.file_name = dbSettings.file_name; if (this.debug) { debug('Settings %j', dbSettings); } }; util.inherits(SQLiteDB, SqlConnector); /** * Get the default data type for ID * @returns {Function} The default type for ID */ SQLiteDB.prototype.getDefaultIdType = function () { return Number; }; /** * Connect to the Database * @param callback */ SQLiteDB.prototype.connect = function (callback) { callback = bindDomain(process.domain, callback); var self = this; var file_name = ((self.file_name !== null) ? self.file_name : ":memory:"); var err = null; if(self.client === undefined) { self.client = new self.sqlite3.Database(file_name, self.sqlite3.OPEN_READWRITE | self.sqlite3.OPEN_CREATE, function(error){ if(error !== null){ err = error; if(self.debug) { self.log("Not Connected", err); } } else { self.dataSource.connected = true; if(self.debug) { self.log("Connected to Database!"); } } }); callback(err, self.client); } else { callback(err, self.client); } }; SQLiteDB.prototype.getTypes = function onGetTypes() { return ['db', 'sqlite', 'sqlite3']; }; SQLiteDB.prototype.debug = function () { if (this.settings.debug) { debug.apply(debug, arguments); } }; SQLiteDB.prototype.disconnect = function disconnect(cb) { this.client.close(cb); }; SQLiteDB.prototype.ping = function(cb) { this.query('SELECT 100 AS result', [], cb); }; SQLiteDB.prototype.executeSQL = function (sql, params, options, callback) { if(callback === undefined && typeof options === 'function') { callback = options; } callback = bindDomain(process.domain, callback); var self = this; var time = Date.now(); var log = self.log; if (self.settings.debug) { if (params && params.length > 0) { debug('SQL: ' + sql + '\nParameters: ' + params); } else { debug('SQL: ' + sql); } } if(params.status && sql.startsWith('PRAGMA')) { var stmt = self.client.prepare(sql); self.client.all(sql, function(err, rows) { // if(err) console.error(err); if (err && self.settings.debug) { debug(err); } if (self.settings.debug && rows) debug("%j", rows); if (self.debug) { log(sql, time); // log(rows, time); } var new_rows = []; for(var i=0; i < rows.length; i++) { var temp = {}; temp.column = rows[i].name; temp.type = rows[i].type; temp.nullable = (rows[i].notnull === 0) ? 'YES' : 'NO'; new_rows.push(temp); } callback(err ? err : null, new_rows); }); } else if(sql.startsWith('INSERT') || sql.startsWith('UPDATE') || sql.startsWith('DELETE') || sql.startsWith('CREATE') || sql.startsWith('DROP')) { if(params.length > 0) { for(var i = 0; i < params.length; i++) { if(typeof params[i] === 'object' && params[i] !== null && !(params[i] instanceof Date)){ params[i] = JSON.stringify(params[i]); } } } self.client.run(sql, params, function(err) { var data = this; if (err && self.settings.debug) { debug(err); } if (self.settings.debug && data) debug("%j", data); if (self.debug) { log(sql, time); // log("Last inserted id: " + data.lastID, time); } var result = null; if(sql.startsWith('UPDATE') || sql.startsWith('DELETE')) { result = {count: data.changes}; } else { result = data.lastID; } if(err) { log(sql, err); } callback(err ? err : null, result); }); } else { self.client.all(sql, params, function(err, rows) { if (err && self.settings.debug) { debug(err); } if (self.settings.debug && rows) debug("%j", rows); if (self.debug) { log(sql, time); // log(rows, time); } callback(err ? err : null, rows); }); } }; SQLiteDB.prototype.query = function (sql, params, callback) { if (!callback && typeof params === 'function') { callback = params; params = []; } for(var i=0; i < params.length; i++){ if(typeof params[i] == 'object') { // Exclude Date objects from getting converted if(isNaN(new Date(params[i]).getTime())) { params[i] = JSON.stringify(params[i]); } } } this.executeSQL(sql, params, callback); }; SQLiteDB.prototype.tableEscaped = function (model) { return this.escapeName(this.table(model)); }; SQLiteDB.prototype.escapeName = function (name) { if (!name) { return name; } return '"' + name.replace(/\./g, '"."') + '"'; }; /*! * Build a list of column name/value pairs * * @param {String} The model name * @param {Object} The model instance data * @param {Boolean} forCreate Indicate if it's for creation */ SQLiteDB.prototype.toFields = function (model, data, forCreate) { var self = this; var props = self._categorizeProperties(model, data); var dataIdNames = props.idsInData; var nonIdsInData = props.nonIdsInData; var query = []; if (forCreate) { if(nonIdsInData.length === 0 && dataIdNames.length === 0) { return 'default values '; } query.push('('); query.push(nonIdsInData.map(function (key) { return self.columnEscaped(model, key); }).join(',')); if (dataIdNames.length > 0) { if (nonIdsInData.length > 0) { query.push(','); } query.push(dataIdNames.map(function (key) { return self.columnEscaped(model, key); }).join(',')); } query.push(') VALUES ('); for (var i = 1, len = nonIdsInData.length + dataIdNames.length; i <= len; i++) { query.push('$', i); if (i !== len) { query.push(','); } } query.push(') '); } else { query.push(nonIdsInData.map(function (key, i) { return self.columnEscaped(model, key) + "=$" + (i + 1); }).join(',')); } return query.join(''); }; /*! * Convert name/value to database value * * @param {String} prop The property name * @param {*} val The property value */ SQLiteDB.prototype.toDatabase = function (prop, val) { if (val === null || val === undefined) { if (prop.autoIncrement) { return 'DEFAULT'; } else { return 'NULL'; } } if (val.constructor.name === 'Object') { var operator = Object.keys(val)[0]; val = val[operator]; if (operator === 'between') { return this.toDatabase(prop, val[0]) + ' AND ' + this.toDatabase(prop, val[1]); } if (operator === 'inq' || operator === 'nin') { var newVal = []; for (var i = 0; i < val.length; i++) { newVal[i] = escape(val[i]); } return newVal.join(','); } return this.toDatabase(prop, val); } if (prop.type.name === 'Number') { if (!val && val !== 0) { if (prop.autoIncrement) { return 'DEFAULT'; } else { return 'NULL'; } } return escape(val); } if (prop.type.name === 'Date' || prop.type.name === 'DATETIME') { if (!val) { if (prop.autoIncrement) { return 'DEFAULT'; } else { return 'NULL'; } } if (!val) { if (prop.autoIncrement) { return 'DEFAULT'; } else { return 'NULL'; } } // Convert Date to timestamp to work with SQLite return val.getTime(); } if (prop.type.name === 'Boolean') { if (val) { return 1; } else { return 0; } } if (prop.type.name === 'GeoPoint') { if (val) { return '(' + escape(val.lat) + ',' + escape(val.lng) + ')'; } else { return 'NULL'; } } return escape(val.toString()); }; /*! * Convert the data from database to JSON * * @param {String} model The model name * @param {Object} data The data from DB */ SQLiteDB.prototype.fromDatabase = function (model, data) { if (!data) { return null; } var props = this._models[model].properties; var json = {}; for (var p in props) { var key = this.column(model, p); var val = data[key]; if (val === undefined) { continue; } var prop = props[p]; var type = prop.type && prop.type.name; if (prop && type === 'Boolean') { if(typeof val === 'number') { json[p] = ((val == 1) ? true : false); } else { json[p] = (val === 'Y' || val === 'y' || val === 'T' || val === 't' || val === '1'); } } else if (prop && type === 'GeoPoint' || type === 'Point') { if (typeof val === 'string') { // The point format is (x,y) var point = val.split(/[\(\)\s,]+/).filter(Boolean); json[p] = { lat: +point[0], lng: +point[1] }; } else if (typeof val === 'object' && val !== null) { // converts point to {x: lat, y: lng} json[p] = { lat: val.x, lng: val.y }; } else { json[p] = val; } } else if (prop && (type === 'Date') && (val !== null)) { json[p] = new Date(val); } else { json[p] = val; } } if (this.debug) { debug('JSON data: %j', json); } return json; }; /*! * Categorize the properties for the given model and data * @param {String} model The model name * @param {Object} data The data object * @returns {{ids: String[], idsInData: String[], nonIdsInData: String[]}} * @private */ SQLiteDB.prototype._categorizeProperties = function(model, data) { var ids = this.idNames(model); var idsInData = ids.filter(function(key) { return data[key] !== null && data[key] !== undefined; }); var props = Object.keys(this._models[model].properties); var nonIdsInData = Object.keys(data).filter(function(key) { return props.indexOf(key) !== -1 && ids.indexOf(key) === -1 && data[key] !== undefined; }); return { ids: ids, idsInData: idsInData, nonIdsInData: nonIdsInData }; }; /** * Create a new model instance */ SQLiteDB.prototype.create = function create(model, data, callback) { var self = this; data = self.mapToDB(model, data); var props = self._categorizeProperties(model, data); var sql = []; sql.push('INSERT INTO ', self.tableEscaped(model), ' ', self.toFields(model, data, true)); var idColName = self.idColumn(model); this.query(sql.join(''), generateQueryParams(data, props), function (err, lastUpdatedID) { if (err) { if(err.message.startsWith('SQLITE_CONSTRAINT: UNIQUE constraint failed')) { err.message = "Cannot create Duplicate id"; } return callback(err); } callback(err, data[idColName] || lastUpdatedID); }); }; /** * Save the model instance to SQLite DB * @param {String} model The model name * @param {Object} data The model instance data * @callback {Function} [callback] The callback function * @param {String|Error} err The error string or object */ SQLiteDB.prototype.save = function (model, data, callback) { var self = this; data = self.mapToDB(model, data); var props = self._categorizeProperties(model, data); var sql = []; sql.push('UPDATE ', self.tableEscaped(model), ' SET ', self.toFields(model, data)); sql.push(' WHERE '); props.ids.forEach(function (id, i) { sql.push((i > 0) ? ' AND ' : ' ', self.idColumnEscaped(model), ' = $', (props.nonIdsInData.length + i + 1)); }); self.query(sql.join(''), generateQueryParams(data, props), function (err) { callback(err); }); }; SQLiteDB.prototype.update = SQLiteDB.prototype.updateAll = function (model, where, data, options, callback) { var whereClause = this.buildWhere(model, where); var sql = ['UPDATE ', this.tableEscaped(model), ' SET ', this.toFields(model, data), ' ', whereClause].join(''); data = this.mapToDB(model, data); var props = this._categorizeProperties(model, data); this.query(sql, generateQueryParams(data, props), function (err, result) { if (callback) { callback(err, result); } }); }; /** * Check if a model instance exists by id */ SQLiteDB.prototype.exists = function (model, id, callback) { var sql = 'SELECT 1 FROM ' + this.tableEscaped(model); if (id) { sql += ' WHERE ' + this.idColumnEscaped(model) + ' = ' + id + ' LIMIT 1'; } else { sql += ' WHERE ' + this.idColumnEscaped(model) + ' IS NULL LIMIT 1'; } this.query(sql, function (err, data) { if (err) return callback(err); callback(null, data.length === 1); }); }; /** * Find a model instance by id * @param {String} model The model name * @param {*} id The id value * @callback {Function} [callback] The callback function * @param {String|Error} err The error string or object * @param {Object} The model instance */ SQLiteDB.prototype.find = function find(model, id, callback) { var sql = 'SELECT * FROM ' + this.tableEscaped(model); if (id) { var idVal = this.toDatabase(this._models[model].properties[this.idName(model)], id); sql += ' WHERE ' + this.idColumnEscaped(model) + ' = ' + idVal + ' LIMIT 1'; } else { sql += ' WHERE ' + this.idColumnEscaped(model) + ' IS NULL LIMIT 1'; } this.query(sql, function (err, data) { if (data && data.length === 1) { // data[0][this.idColumn(model)] = id; } else { data = [null]; } callback(err, this.fromDatabase(model, data[0])); }.bind(this)); }; /*! * Get a list of columns based on the fields pattern * * @param {String} model The model name * @param {Object|String[]} props Fields pattern * @returns {String} */ SQLiteDB.prototype.getColumns = function (model, props) { var cols = this._models[model].properties; var self = this; var keys = Object.keys(cols); if (Array.isArray(props) && props.length > 0) { // No empty array, including all the fields keys = props; } else if ('object' === typeof props && Object.keys(props).length > 0) { // { field1: boolean, field2: boolean ... } var included = []; var excluded = []; keys.forEach(function (k) { if (props[k]) { included.push(k); } else if ((k in props) && !props[k]) { excluded.push(k); } }); if (included.length > 0) { keys = included; } else if (excluded.length > 0) { excluded.forEach(function (e) { var index = keys.indexOf(e); keys.splice(index, 1); }); } } var names = keys.map(function (c) { return self.columnEscaped(model, c); }); return names.join(', '); }; /** * Find matching model instances by the filter * * @param {String} model The model name * @param {Object} filter The filter * @callback {Function} [callback] The callback function * @param {String|Error} err The error string or object * @param {Object[]} The matched model instances */ SQLiteDB.prototype.all = function all(model, filter, options, callback) { var self = this; if (callback === undefined && options === undefined && typeof filter === 'function') { callback = filter; filter = []; options = {}; } if (callback === undefined && typeof options === 'function') { callback = options; options = {}; } // SQLite has a characterstic of sorting the resultset by 'id' // So, we reset the resutset to mimic the normal resultset // 'ORDER BY' is only performed when explicitely asked. filter = filter || []; if (!filter.order) { var idNames = this.idNames(model); if (idNames && idNames.length) { filter.order = idNames; } if(filter.where !== undefined) { if(filter.order === 'id' && filter.where.id !== undefined) { if(filter.where.id.inq !== undefined){ for(var i=0; i < filter.where.id.inq.length; i++) { if(self.current_order.indexOf(filter.where.id.inq[i]) < 0 ) self.current_order.push(filter.where.id.inq[i]); } } } } } function reset_order(data) { var temp_data = []; for(var i=0; i< self.current_order.length; i++) { for(var j=0; j< data.length; j++) { if(self.current_order[i] == data[j].id){ temp_data.push(data[j]); break; } } } return temp_data; } this.query('SELECT ' + this.getColumns(model, filter.fields) + ' FROM ' + this.toFilter(model, filter), function (err, data) { if (err) { return callback(err, []); } if (data) { for (var i = 0; i < data.length; i++) { data[i] = this.fromDatabase(model, data[i]); } } if (filter && filter.include) { this._models[model].model.include(data, filter.include, callback); } else { var data_temp = []; if(self.current_order != []) { data_temp = reset_order(data); self.current_order = []; } callback(null, (data_temp.length !== 0) ? data_temp : data); } }.bind(this)); }; /** * Delete all model instances */ SQLiteDB.prototype.destroyAll = function destroyAll(model, where, options, callback) { if (!callback && !options && 'function' === typeof where) { callback = where; options = undefined; where = undefined; } if (!callback && 'function' === typeof options) { callback = options; options = undefined; } this.query('DELETE FROM ' + ' ' + this.toFilter(model, where && {where: where}), function (err, data) { callback && callback(err, data); }.bind(this)); }; /** * Count the number of instances for the given model * * @param {String} model The model name * @param {Function} [callback] The callback function * @param {Object} filter The filter for where * */ SQLiteDB.prototype.count = function count(model, callback, filter) { this.query('SELECT count(*) as "cnt" FROM ' + ' ' + this.toFilter(model, filter && {where: filter}), function (err, data) { if (err) return callback(err); var c = data && data[0] && data[0].cnt; callback(err, Number(c)); }.bind(this)); }; function getPagination(filter) { var pagination = []; if (filter && (filter.limit || filter.offset || filter.skip)) { var limit = Number(filter.limit); if (limit) { pagination.push('LIMIT ' + limit); } var offset = Number(filter.offset); if (!offset) { offset = Number(filter.skip); } if (offset) { pagination.push('OFFSET ' + offset); } else { offset = 0; } } return pagination; } SQLiteDB.prototype.buildWhere = function (model, conds) { var where = this._buildWhere(model, conds); if (where) { return ' WHERE ' + where; } else { return ''; } }; SQLiteDB.prototype._buildWhere = function (model, conds) { if (!conds) { return ''; } var self = this; var props = self._models[model].properties; var fields = []; if (typeof conds === 'string') { fields.push(conds); } else if (util.isArray(conds)) { var query = conds.shift().replace(/\?/g, function (s) { return escape(conds.shift()); }); fields.push(query); } else { var sqlCond = null; Object.keys(conds).forEach(function (key) { if (key === 'and' || key === 'or') { var clauses = conds[key]; if (Array.isArray(clauses)) { clauses = clauses.map(function (c) { return '(' + self._buildWhere(model, c) + ')'; }); return fields.push(clauses.join(' ' + key.toUpperCase() + ' ')); } // The value is not an array, fall back to regular fields } if (conds[key] && conds[key].constructor.name === 'RegExp') { var regex = conds[key]; sqlCond = self.columnEscaped(model, key); if (regex.ignoreCase) { sqlCond += ' ~* '; } else { sqlCond += ' ~ '; } sqlCond += "'" + regex.source + "'"; fields.push(sqlCond); return; } if (props[key]) { var filterValue = self.toDatabase(props[key], conds[key]); if (filterValue === 'NULL') { fields.push(self.columnEscaped(model, key) + ' IS ' + filterValue); } else if (conds[key].constructor.name === 'Object') { var condType = Object.keys(conds[key])[0]; sqlCond = self.columnEscaped(model, key); if ((condType === 'inq' || condType === 'nin') && filterValue.length === 0) { fields.push(condType === 'inq' ? '1 = 2' : '1 = 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 '; break; case 'inq': sqlCond += ' IN '; break; case 'nin': sqlCond += ' NOT IN '; break; case 'neq': sqlCond += ' != '; break; case 'like': sqlCond += ' LIKE '; filterValue += "ESCAPE '\\'"; break; case 'nlike': sqlCond += ' NOT LIKE '; filterValue += "ESCAPE '\\'"; break; default: sqlCond += ' ' + condType + ' '; break; } sqlCond += (condType === 'inq' || condType === 'nin') ? '(' + filterValue + ')' : filterValue; fields.push(sqlCond); } else { fields.push(self.columnEscaped(model, key) + ' = ' + filterValue); } } }); } return fields.join(' AND '); }; /*! * Build the SQL clause * @param {String} model The model name * @param {Object} filter The filter * @returns {*} */ SQLiteDB.prototype.toFilter = function (model, filter) { var self = this; if (filter && typeof filter.where === 'function') { return self.tableEscaped(model) + ' ' + filter.where(); } if (!filter) { return self.tableEscaped(model); } var out = self.tableEscaped(model) + ' '; var where = self.buildWhere(model, filter.where); if (where) { out += where; } var pagination = getPagination(filter); var order = filter.order; if (order) { if (typeof order === 'string') { order = [order]; } var orderBy = ''; var escapedOrder = []; for (var i = 0, n = order.length; i < n; i++) { var t = order[i].split(/[\s]+/); var field = t[0], dir = t[1]; escapedOrder.push(self.columnEscaped(model, field) + (dir ? ' ' + dir : '')); } orderBy = ' ORDER BY ' + escapedOrder.join(','); if (pagination.length) { out = out + ' ' + orderBy + ' ' + pagination.join(' '); } else { out = out + ' ' + orderBy; } } else { if (pagination.length) { out = out + ' ' + pagination.join(' '); } } return out; }; function escape(val) { if (val === undefined || val === null) { return 'NULL'; } switch (typeof val) { case 'boolean': return (val) ? "true" : "false"; case 'number': return val + ''; } if (typeof val === 'object') { val = (typeof val.toISOString === 'function') ? val.toISOString() : val.toString(); } val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function (s) { switch (s) { case "\0": return "\\0"; case "\n": return "\\n"; case "\r": return "\\r"; case "\b": return "\\b"; case "\t": return "\\t"; case "\x1a": return "\\Z"; case "\'": return "''"; case "\"": return s; default: return "\\" + s; } }); // return "q'#"+val+"#'"; return "'" + val + "'"; } function generateQueryParams(data, props) { var queryParams = []; function pushToQueryParams(key) { queryParams.push(data[key] !== undefined ? data[key] : null); } props.nonIdsInData.forEach(pushToQueryParams); props.idsInData.forEach(pushToQueryParams); return queryParams; } function bindDomain(domain, cb) { if (domain && cb) { cb = domain.bind(cb); } return cb; } // Polyfill for String startsWith if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position) { position = position || 0; return this.lastIndexOf(searchString, position) === position; }; } require('./migration')(SQLiteDB);