loopback-connector-oracle
Version:
Loopback Oracle Connector
445 lines (406 loc) • 13.4 kB
JavaScript
// Copyright IBM Corp. 2015,2019. All Rights Reserved.
// Node module: loopback-connector-oracle
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
;
const async = require('async');
module.exports = mixinMigration;
function mixinMigration(Oracle) {
Oracle.prototype.showFields = function(model, cb) {
const sql = 'SELECT column_name AS "column", data_type AS "type",' +
' data_length AS "length", nullable AS "nullable"' + // , data_default AS "Default"'
' FROM "SYS"."USER_TAB_COLUMNS" WHERE table_name=\'' +
this.table(model) + '\'';
this.execute(sql, function(err, fields) {
if (err)
return cb(err);
else {
fields.forEach(function(field) {
field.type = mapOracleDatatypes(field.type);
});
cb(err, fields);
}
});
};
/*!
* Discover the properties from a table
* @param {String} model The model name
* @param {Function} cb The callback function
*/
Oracle.prototype.getTableStatus = function(model, cb) {
let fields;
const self = this;
this.showFields(model, function(err, data) {
if (err) return cb(err);
fields = data;
if (fields)
return cb(null, fields);
});
};
/**
* 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
* @param {Function} [cb] The callback function
*/
Oracle.prototype.autoupdate = function(models, cb) {
const 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.eachLimit(models, this.parallelLimit, function(model, done) {
if (!(model in self._models)) {
return process.nextTick(function() {
done(new Error('Model not found: ' + model));
});
}
self.getTableStatus(model, function(err, fields) {
if (!err && fields.length) {
self.alterTable(model, fields, done);
} else {
self.createTable(model, done);
}
});
}, cb);
};
/**
* 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
*/
Oracle.prototype.alterTable = function(model, actualFields, cb) {
const self = this;
const pendingChanges = self.getAddModifyColumns(model, actualFields);
if (pendingChanges.length > 0) {
self.applySqlChanges(model, pendingChanges, function(err, results) {
const dropColumns = self.getDropColumns(model, actualFields);
if (dropColumns.length > 0) {
self.applySqlChanges(model, dropColumns, cb);
} else {
if (cb) cb(err, results);
}
});
} else {
const dropColumns = self.getDropColumns(model, actualFields);
if (dropColumns.length > 0) {
self.applySqlChanges(model, dropColumns, cb);
} else {
if (cb) process.nextTick(cb.bind(null, null, []));
}
}
};
Oracle.prototype.getAddModifyColumns = function(model, actualFields) {
let sql = [];
const self = this;
sql = sql.concat(self.getColumnsToAdd(model, actualFields));
sql = sql.concat(self.getPropertiesToModify(model, actualFields));
return sql;
};
Oracle.prototype.getColumnsToAdd = function(model, actualFields) {
const self = this;
const m = self._models[model];
const propNames = Object.keys(m.properties);
let sql = [];
propNames.forEach(function(propName) {
if (self.id(model, propName)) {
return;
}
const found = self.searchForPropertyInActual(model,
self.column(model, propName), actualFields);
if (!found && self.propertyHasNotBeenDeleted(model, propName)) {
sql.push(self.addPropertyToActual(model, propName));
}
});
if (sql.length > 0) {
sql = ['ADD', '(' + sql.join(',') + ')'];
}
return sql;
};
Oracle.prototype.getPropertiesToModify = function(model, actualFields) {
const self = this;
let sql = [];
const m = self._models[model];
const propNames = Object.keys(m.properties);
let found;
propNames.forEach(function(propName) {
if (self.id(model, propName)) {
return;
}
found = self.searchForPropertyInActual(model, propName, actualFields);
if (found && self.propertyHasNotBeenDeleted(model, propName)) {
const column = self.columnEscaped(model, propName);
let clause = '';
if (datatypeChanged(propName, found)) {
clause = column + ' ' +
self.modifyDatatypeInActual(model, propName);
}
if (nullabilityChanged(propName, found)) {
if (!clause) {
clause = column;
}
clause = clause + ' ' +
self.modifyNullabilityInActual(model, propName);
}
if (clause) {
sql.push(clause);
}
}
});
if (sql.length > 0) {
sql = ['MODIFY', '(' + sql.join(',') + ')'];
}
return sql;
function datatypeChanged(propName, oldSettings) {
const newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
let oldType;
if (hasLength(self.columnDataType(model, propName))) {
oldType = oldSettings.type.toUpperCase() +
'(' + oldSettings.length + ')';
} else {
oldType = oldSettings.type.toUpperCase();
}
return oldType !== self.columnDataType(model, propName);
function hasLength(type) {
const hasLengthRegex = new RegExp(/^[A-Z0-9]*\([0-9]*\)$/);
return hasLengthRegex.test(type);
}
}
function nullabilityChanged(propName, oldSettings) {
const newSettings = m.properties[propName];
if (!newSettings) {
return false;
}
let changed = false;
if (oldSettings.nullable === 'Y' && !self.isNullable(newSettings)) {
changed = true;
}
if (oldSettings.nullable === 'N' && self.isNullable(newSettings)) {
changed = true;
}
return changed;
}
};
Oracle.prototype.modifyDatatypeInActual = function(model, propName) {
const self = this;
const sqlCommand = self.columnDataType(model, propName);
return sqlCommand;
};
Oracle.prototype.modifyNullabilityInActual = function(model, propName) {
const self = this;
let sqlCommand = '';
if (self.isNullable(self.getPropertyDefinition(model, propName))) {
sqlCommand = sqlCommand + 'NULL';
} else {
sqlCommand = sqlCommand + 'NOT NULL';
}
return sqlCommand;
};
Oracle.prototype.getColumnsToDrop = function(model, actualFields) {
const self = this;
let sql = [];
actualFields.forEach(function(actualField) {
if (self.idColumn(model) === actualField.column) {
return;
}
if (actualFieldNotPresentInModel(actualField, model)) {
sql.push(self.escapeName(actualField.column));
}
});
if (sql.length > 0) {
sql = ['DROP', '(' + sql.join(',') + ')'];
}
return sql;
function actualFieldNotPresentInModel(actualField, model) {
return !(self.propertyName(model, actualField.column));
}
};
/*!
* Build a list of columns for the given model
* @param {String} model The model name
* @returns {String}
*/
Oracle.prototype.buildColumnDefinitions = function(model) {
const self = this;
const sql = [];
const pks = this.idNames(model).map(function(i) {
return self.columnEscaped(model, i);
});
Object.keys(this.getModelDefinition(model).properties).
forEach(function(prop) {
const 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}
*/
Oracle.prototype.buildColumnDefinition = function(model, propName) {
const self = this;
let result = self.columnDataType(model, propName);
if (!self.isNullable(self.getPropertyDefinition(model, propName))) {
result = result + ' NOT NULL';
}
return result;
};
Oracle.prototype._isIdGenerated = function(model) {
const idNames = this.idNames(model);
if (!idNames) {
return false;
}
const idName = idNames[0];
const id = this.getModelDefinition(model).properties[idName];
const idGenerated = idNames.length > 1 ? false : id && id.generated;
return idGenerated;
};
/**
* Drop a table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
Oracle.prototype.dropTable = function(model, cb) {
const self = this;
const name = self.tableEscaped(model);
const seqName = self.escapeName(model + '_ID_SEQUENCE');
let count = 0;
const dropTableFun = function(callback) {
self.execute('DROP TABLE ' + name, function(err, data) {
if (err && err.toString().indexOf('ORA-00054') >= 0) {
count++;
if (count <= 5) {
self.debug('Retrying ' + count + ': ' + err);
// Resource busy, try again
setTimeout(dropTableFun, 200 * Math.pow(count, 2));
return;
}
}
if (err && err.toString().indexOf('ORA-00942') >= 0) {
err = null; // Ignore it
}
callback(err, data);
});
};
const tasks = [dropTableFun];
if (this._isIdGenerated(model)) {
tasks.push(
function(callback) {
self.execute('DROP SEQUENCE ' + seqName, function(err) {
if (err && err.toString().indexOf('ORA-02289') >= 0) {
err = null; // Ignore it
}
callback(err);
});
},
);
// Triggers will be dropped as part the drop table
}
async.series(tasks, cb);
};
/**
* Create a table for the given model
* @param {String} model The model name
* @param {Function} [cb] The callback function
*/
Oracle.prototype.createTable = function(model, cb) {
const self = this;
const name = self.tableEscaped(model);
const seqName = self.escapeName(model + '_ID_SEQUENCE');
const triggerName = self.escapeName(model + '_ID_TRIGGER');
const idName = self.idColumnEscaped(model);
const tasks = [
function(callback) {
self.execute('CREATE TABLE ' + name + ' (\n ' +
self.buildColumnDefinitions(model) + '\n)', callback);
}];
if (this._isIdGenerated(model)) {
tasks.push(
function(callback) {
self.execute('CREATE SEQUENCE ' + seqName +
' START WITH 1 INCREMENT BY 1 CACHE 100', callback);
},
);
tasks.push(
function(callback) {
self.execute('CREATE OR REPLACE TRIGGER ' + triggerName +
' BEFORE INSERT ON ' + name + ' FOR EACH ROW\n' +
'WHEN (new.' + idName + ' IS NULL)\n' +
'BEGIN\n' +
' SELECT ' + seqName + '.NEXTVAL INTO :new.' +
idName + ' FROM dual;\n' +
'END;', callback);
},
);
}
async.series(tasks, cb);
};
/*!
* 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
*/
Oracle.prototype.columnDataType = function(model, property) {
const columnMetadata = this.columnMetadata(model, property);
let colType = columnMetadata && columnMetadata.dataType;
if (colType) {
colType = colType.toUpperCase();
}
const prop = this.getModelDefinition(model).properties[property];
if (!prop) {
return null;
}
const colLength = columnMetadata && columnMetadata.dataLength ||
prop.length;
if (colType) {
if (colType === 'CLOB' || colType === 'BLOB') {
return colType;
}
return colType + (colLength ? '(' + colLength + ')' : '');
}
switch (prop.type.name) {
default:
case 'String':
case 'JSON':
return 'VARCHAR2' + (colLength ? '(' + colLength + ')' : '(1024)');
case 'Text':
return 'VARCHAR2' + (colLength ? '(' + colLength + ')' : '(1024)');
case 'Number':
return 'NUMBER';
case 'Date':
return 'DATE';
case 'Timestamp':
return 'TIMESTAMP(3)';
case 'Boolean':
return 'CHAR(1)'; // Oracle doesn't have built-in boolean
}
};
function mapOracleDatatypes(typeName) {
// TODO there are a lot of synonymous type names that should go here--
// this is just what i've run into so far
switch (typeName) {
case 'int4':
return 'NUMBER';
case 'bool':
return 'CHAR(1)';
default:
return typeName;
}
}
}