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.
441 lines (397 loc) • 12.9 kB
JavaScript
/**
* Created by tushar-chauhan on 15-Sep-2015
*/
var async = require('async');
module.exports = mixinMigration;
function mixinMigration(SQLiteDB) {
function mapSQLiteDatatypes(typeName) {
return typeName;
}
/*!
* Discover the properties from a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
function getTableStatus(model, cb) {
function decoratedCallback(err, data) {
if (err) {
console.error(err);
}
if (!err) {
data.forEach(function (field) {
field.type = mapSQLiteDatatypes(field.type);
});
}
cb(err, data);
}
var sql = null;
sql = 'PRAGMA table_info(' + this.tableEscaped(model) +')';
var params = [];
params.status = true;
this.executeSQL(sql, params, decoratedCallback);
}
/**
* Perform autoupdate for the given models
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
* @callback {Function} [callback] The callback function
* @param {String|Error} err The error string or object
*/
SQLiteDB.prototype.autoupdate = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(this._models);
async.each(models, function(model, done) {
if (!(model in self._models)) {
return process.nextTick(function() {
done(new Error('Model not found: ' + model));
});
}
getTableStatus.call(self, model, function(err, fields) {
if (!err && fields.length) {
self.alterTable(model, fields, done);
} else {
self.createTable(model, done);
}
});
}, cb);
};
/*!
* Check if the models exist
* @param {String[]} [models] A model name or an array of model names. If not present, apply to all models
* @param {Function} [cb] The callback function
*/
SQLiteDB.prototype.isActual = function(models, cb) {
var self = this;
if ((!cb) && ('function' === typeof models)) {
cb = models;
models = undefined;
}
// First argument is a model name
if ('string' === typeof models) {
models = [models];
}
models = models || Object.keys(this._models);
var changes = [];
async.each(models, function(model, done) {
getTableStatus.call(self, model, function(err, fields) {
changes = changes.concat(getAddModifyColumns.call(self, model, fields));
changes = changes.concat(getDropColumns.call(self, model, fields));
done(err);
});
}, function done(err) {
if (err) {
return cb && cb(err);
}
var actual = (changes.length === 0);
cb && cb(null, actual);
});
};
/*!
* Alter the table for the given model
* @param {String} model The model name
* @param {Object[]} actualFields Actual columns in the table
* @param {Function} [cb] The callback function
*/
SQLiteDB.prototype.alterTable = function (model, actualFields, cb) {
var self = this;
var pendingChanges = getAddModifyColumns.call(self, model, actualFields);
if (pendingChanges.length > 0) {
applySqlChanges.call(self, model, pendingChanges, function (err, results) {
var dropColumns = getDropColumns.call(self, model, actualFields);
if (dropColumns.length > 0) {
applySqlChanges.call(self, model, dropColumns, cb);
} else {
cb && cb(err, results);
}
});
} else {
var dropColumns = getDropColumns.call(self, model, actualFields);
if (dropColumns.length > 0) {
applySqlChanges.call(self, model, dropColumns, cb);
} else {
cb && process.nextTick(cb.bind(null, null, []));
}
}
};
function getAddModifyColumns(model, actualFields) {
var sql = [];
var self = this;
sql = sql.concat(getColumnsToAdd.call(self, model, actualFields));
var drops = getPropertiesToModify.call(self, model, actualFields);
if (drops.length > 0) {
if (sql.length > 0) {
sql = sql.concat(', ');
}
sql = sql.concat(drops);
}
// sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
return sql;
}
function getDropColumns(model, actualFields) {
var sql = [];
var self = this;
// sql = sql.concat(getColumnsToDrop.call(self, model, actualFields));
return sql;
}
function getColumnsToAdd(model, actualFields) {
var self = this;
var m = self._models[model];
var propNames = Object.keys(m.properties);
var sql = [];
propNames.forEach(function (propName) {
if (self.id(model, propName)) return;
var found = searchForPropertyInActual.call(self, model, self.column(model, propName), actualFields);
if (!found && propertyHasNotBeenDeleted.call(self, model, propName)) {
sql.push('ADD COLUMN ' + addPropertyToActual.call(self, model, propName));
}
});
return sql;
}
function propertyHasNotBeenDeleted(model, propName) {
return !!this._models[model].properties[propName];
}
function addPropertyToActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) + ' ' +
self.columnDataType(model, propName) + (propertyCanBeNull.call(self, model, propName) ? "" : " NOT NULL");
return sqlCommand;
}
function searchForPropertyInActual(model, propName, actualFields) {
var self = this;
var found = false;
actualFields.forEach(function (f) {
if (f.column === self.column(model, propName)) {
found = f;
return;
}
});
return found;
}
function getPropertiesToModify(model, actualFields) {
var self = this;
var sql = [];
var m = self._models[model];
var propNames = Object.keys(m.properties);
var found;
propNames.forEach(function (propName) {
if (self.id(model, propName)) {
return;
}
found = searchForPropertyInActual.call(self, model, propName, actualFields);
if (found && propertyHasNotBeenDeleted.call(self, model, propName)) {
if (datatypeChanged(propName, found)) {
sql.push('ALTER COLUMN ' + modifyDatatypeInActual.call(self, model, propName));
}
if (nullabilityChanged(propName, found)) {
sql.push('ALTER COLUMN' + modifyNullabilityInActual.call(self, model, propName));
}
}
});
if (sql.length > 0) {
sql = [sql.join(', ')];
}
return sql;
function datatypeChanged(propName, oldSettings) {
var newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
return oldSettings.type.toUpperCase() !== self.columnDataType(model, propName);
}
function isNullable(p) {
return !(p.required ||
p.id ||
p.allowNull === false ||
p.null === false ||
p.nullable === false);
}
function nullabilityChanged(propName, oldSettings) {
var newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
var changed = false;
if (oldSettings.nullable === 'YES' && !isNullable(newSettings)) {
changed = true;
}
if (oldSettings.nullable === 'NO' && isNullable(newSettings)) {
changed = true;
}
return changed;
}
}
function modifyDatatypeInActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) + ' TYPE ' +
self.columnDataType(model, propName);
return sqlCommand;
}
function modifyNullabilityInActual(model, propName) {
var self = this;
var sqlCommand = self.columnEscaped(model, propName) + ' ';
if (propertyCanBeNull.call(self, model, propName)) {
sqlCommand = sqlCommand + "DROP ";
} else {
sqlCommand = sqlCommand + "SET ";
}
sqlCommand = sqlCommand + "NOT NULL";
return sqlCommand;
}
function getColumnsToDrop(model, actualFields) {
var self = this;
var sql = [];
actualFields.forEach(function (actualField) {
if (self.idColumn(model) === actualField.column) {
return;
}
if (actualFieldNotPresentInModel(actualField, model)) {
sql.push('DROP COLUMN ' + self.escapeName(actualField.column));
}
});
if (sql.length > 0) {
sql = [sql.join(', ')];
}
return sql;
function actualFieldNotPresentInModel(actualField, model) {
return !(self.propertyName(model, actualField.column));
}
}
function applySqlChanges(model, pendingChanges, cb) {
var self = this;
if (pendingChanges.length) {
var changesPrefix = 'ALTER TABLE ' + self.tableEscaped(model);
var done = 0;
var globalRes = [];
pendingChanges.forEach(function (change) {
self.query(changesPrefix + ' ' + change, function (err, res) {
globalRes.push(res);
if (++done === pendingChanges.length) {
cb && cb(err ? err : null, globalRes);
}
});
});
}
}
/*!
* Build a list of columns for the given model
* @param {String} model The model name
* @returns {String}
*/
SQLiteDB.prototype.buildColumnDefinitions = SQLiteDB.prototype.propertiesSQL = function (model) {
var self = this;
var sql = [];
var pks = this.idNames(model).map(function (i) {
return self.columnEscaped(model, i);
});
Object.keys(this._models[model].properties).forEach(function (prop) {
var colName = self.columnEscaped(model, prop);
sql.push(colName + ' ' + self.buildColumnDefinition(model, prop));
});
if (pks.length > 0) {
sql.push('PRIMARY KEY(' + pks.join(',') + ')');
}
return sql.join(',\n ');
};
/*!
* Build settings for the model property
* @param {String} model The model name
* @param {String} propName The property name
* @returns {*|string}
*/
SQLiteDB.prototype.buildColumnDefinition = function (model, propName) {
var self = this;
if (this.id(model, propName) && this._models[model].properties[propName].generated) {
return 'INTEGER';
}
var result = self.columnDataType(model, propName);
if (!propertyCanBeNull.call(self, model, propName)) result = result + ' NOT NULL';
result += self.columnDbDefault(model, propName);
return result;
};
/*!
* Get the database-default value for column from given model property
*
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column default value
*/
SQLiteDB.prototype.columnDbDefault = function(model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colDefault = columnMetadata && columnMetadata.dbDefault;
return colDefault ? (' DEFAULT ' + columnMetadata.dbDefault): '';
};
SQLiteDB.prototype.mapToDB = function (model, data) {
var dbData = {};
if (!data) {
return dbData;
}
var props = this._models[model].properties;
for (var p in data) {
if(props[p]) {
var pType = props[p].type && props[p].type.name;
if (pType === 'GeoPoint' && data[p]) {
dbData[p] = '(' + data[p].lat + ',' + data[p].lng + ')';
} else {
dbData[p] = data[p];
}
}
}
return dbData;
};
/*!
* Find the column type for a given model property
*
* @param {String} model The model name
* @param {String} property The property name
* @returns {String} The column type
*/
SQLiteDB.prototype.columnDataType = function (model, property) {
var columnMetadata = this.columnMetadata(model, property);
var colType = columnMetadata && columnMetadata.dataType;
if (colType) {
colType = colType.toUpperCase();
}
var prop = this._models[model].properties[property];
if (!prop) {
return null;
}
var colLength = columnMetadata && columnMetadata.dataLength || prop.length;
if (colType) {
return colType + (colLength ? '(' + colLength + ')' : '');
}
switch (prop.type.name) {
default:
case 'String':
case 'JSON':
return 'TEXT';
case 'Text':
return 'TEXT';
case 'Number':
return 'INTEGER';
case 'Date':
return 'DATETIME';
case 'Timestamp':
return 'DATETIME';
case 'GeoPoint':
case 'Point':
return 'POINT';
case 'Boolean':
return 'BOOLEAN';
}
};
function propertyCanBeNull(model, propName) {
var p = this._models[model].properties[propName];
if (p.required || p.id) {
return false;
}
return !(p.allowNull === false ||
p['null'] === false || p.nullable === false);
}
}