2146 lines
65 KiB
JavaScript
2146 lines
65 KiB
JavaScript
// Copyright IBM Corp. 2014,2019. All Rights Reserved.
|
|
// Node module: loopback-connector
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
'use strict';
|
|
const SG = require('strong-globalize');
|
|
const g = SG();
|
|
|
|
const util = require('util');
|
|
const async = require('async');
|
|
const assert = require('assert');
|
|
const Connector = require('./connector');
|
|
const debug = require('debug')('loopback:connector:sql');
|
|
const ParameterizedSQL = require('./parameterized-sql');
|
|
const Transaction = require('./transaction');
|
|
|
|
module.exports = SQLConnector;
|
|
|
|
function NOOP() {}
|
|
|
|
/**
|
|
* Base class for connectors that connect to relational databases using SQL
|
|
* @class
|
|
*/
|
|
function SQLConnector() {
|
|
// Call the super constructor
|
|
Connector.apply(this, [].slice.call(arguments));
|
|
}
|
|
|
|
// Inherit from the base Connector
|
|
util.inherits(SQLConnector, Connector);
|
|
|
|
// Export ParameterizedSQL
|
|
SQLConnector.ParameterizedSQL = ParameterizedSQL;
|
|
|
|
// The generic placeholder
|
|
const PLACEHOLDER = SQLConnector.PLACEHOLDER = ParameterizedSQL.PLACEHOLDER;
|
|
|
|
SQLConnector.Transaction = Transaction;
|
|
|
|
/**
|
|
* @private
|
|
* Set the relational property to indicate the backend is a relational DB
|
|
* @type {boolean}
|
|
*/
|
|
SQLConnector.prototype.relational = true;
|
|
|
|
/**
|
|
* Set the multiInsertSupported property to indicate if multiple value insert SQL dialect is supported or not
|
|
* This can be overridden by derived connectors to allow `insert multiple values` dialect for createAll
|
|
* By default, it is set to false for backward compatibility
|
|
* @type {boolean}
|
|
*/
|
|
SQLConnector.prototype.multiInsertSupported = false;
|
|
|
|
/**
|
|
* Invoke a prototype method on the super class
|
|
* @param {String} methodName Method name
|
|
*/
|
|
SQLConnector.prototype.invokeSuper = function(methodName) {
|
|
const args = [].slice.call(arguments, 1);
|
|
const superMethod = this.constructor.super_.prototype[methodName];
|
|
return superMethod.apply(this, args);
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
SQLConnector.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.eachSeries(models, function(model, done) {
|
|
if (!(model in self._models)) {
|
|
return process.nextTick(function() {
|
|
done(new Error(g.f('Model not found: %s', model)));
|
|
});
|
|
}
|
|
self.getTableStatus(model, function(err, fields, indexes, FKs) {
|
|
if (!err && fields.length) {
|
|
self.alterTable(model, fields, indexes, 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
|
|
*/
|
|
SQLConnector.prototype.isActual = 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);
|
|
|
|
let changes = [];
|
|
async.each(models, function(model, done) {
|
|
self.getTableStatus(model, function(err, fields) {
|
|
changes = changes.concat(self.getAddModifyColumns(model, fields));
|
|
changes = changes.concat(self.getDropColumns(model, fields));
|
|
done(err);
|
|
});
|
|
}, function done(err) {
|
|
if (err) {
|
|
return cb && cb(err);
|
|
}
|
|
const actual = (changes.length === 0);
|
|
if (cb) cb(null, actual);
|
|
});
|
|
};
|
|
|
|
SQLConnector.prototype.getAddModifyColumns = function(model, fields) {
|
|
let sql = [];
|
|
const self = this;
|
|
sql = sql.concat(self.getColumnsToAdd(model, fields));
|
|
return sql;
|
|
};
|
|
|
|
SQLConnector.prototype.getColumnsToAdd = function(model, fields) {
|
|
throw new Error(g.f('{{getColumnsToAdd()}} must be implemented by the connector'));
|
|
};
|
|
|
|
SQLConnector.prototype.getDropColumns = function(model, fields) {
|
|
let sql = [];
|
|
const self = this;
|
|
sql = sql.concat(self.getColumnsToDrop(model, fields));
|
|
return sql;
|
|
};
|
|
|
|
SQLConnector.prototype.getColumnsToDrop = function(model, fields) {
|
|
throw new Error(g.f('{{getColumnsToDrop()}} must be implemented by the connector'));
|
|
};
|
|
|
|
SQLConnector.prototype.searchForPropertyInActual = function(model, propName,
|
|
actualFields) {
|
|
const self = this;
|
|
let found = false;
|
|
actualFields.forEach(function(f) {
|
|
if (f.column === self.column(model, propName)) {
|
|
found = f;
|
|
return;
|
|
}
|
|
});
|
|
return found;
|
|
};
|
|
|
|
SQLConnector.prototype.addPropertyToActual = function(model, propName) {
|
|
const self = this;
|
|
const sqlCommand = self.columnEscaped(model, propName) +
|
|
' ' + self.columnDataType(model, propName) +
|
|
(self.isNullable(self.getPropertyDefinition(model, propName)) ?
|
|
'' : ' NOT NULL');
|
|
return sqlCommand;
|
|
};
|
|
|
|
SQLConnector.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 || prop.limit;
|
|
if (colType && colLength) {
|
|
return colType + '(' + colLength + ')';
|
|
}
|
|
return this.buildColumnType(prop);
|
|
};
|
|
|
|
SQLConnector.prototype.buildColumnType = function(property) {
|
|
throw new Error(g.f('{{buildColumnType()}} must be implemented by the connector'));
|
|
};
|
|
|
|
SQLConnector.prototype.propertyHasNotBeenDeleted = function(model, propName) {
|
|
return !!this.getModelDefinition(model).properties[propName];
|
|
};
|
|
|
|
SQLConnector.prototype.applySqlChanges = function(model, pendingChanges, cb) {
|
|
const self = this;
|
|
if (pendingChanges.length) {
|
|
let thisQuery = 'ALTER TABLE ' + self.tableEscaped(model);
|
|
let ranOnce = false;
|
|
pendingChanges.forEach(function(change) {
|
|
if (ranOnce) {
|
|
thisQuery = thisQuery + ' ';
|
|
}
|
|
thisQuery = thisQuery + ' ' + change;
|
|
ranOnce = true;
|
|
});
|
|
self.execute(thisQuery, cb);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Alters a table
|
|
* @param {String} model The model name
|
|
* @param {Object} fields Fields of the table
|
|
* @param {Object} indexes Indexes of the table
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.alterTable = function(model, fields, indexes, cb) {
|
|
throw new Error(g.f('{{alterTable()}} must be implemented by the connector'));
|
|
};
|
|
|
|
SQLConnector.prototype.checkFieldAndIndex = function(fields, indexes) {
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Get the status of a table
|
|
* @param {String} model The model name
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.getTableStatus = function(model, cb) {
|
|
let fields, indexes;
|
|
const self = this;
|
|
|
|
this.showFields(model, function(err, data) {
|
|
if (err) return cb(err);
|
|
fields = data;
|
|
|
|
self.showIndexes(model, function(err, data) {
|
|
if (err) return cb(err);
|
|
indexes = data;
|
|
|
|
if (self.checkFieldAndIndex(fields, indexes))
|
|
return cb(null, fields, indexes);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Get fields from a table
|
|
* @param {String} model The model name
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.showFields = function(model, cb) {
|
|
throw new Error(g.f('{{showFields()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Get indexes from a table
|
|
* @param {String} model The model name
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.showIndexes = function(model, cb) {
|
|
throw new Error(g.f('{{showIndexes()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Get types associated with the connector
|
|
* Returns {String[]} The types for the connector
|
|
*/
|
|
SQLConnector.prototype.getTypes = function() {
|
|
return ['db', 'rdbms', 'sql'];
|
|
};
|
|
|
|
/**
|
|
* Get the default data type for ID
|
|
* @param prop Property definition
|
|
* Returns {Function}
|
|
*/
|
|
SQLConnector.prototype.getDefaultIdType = function(prop) {
|
|
return Number;
|
|
};
|
|
|
|
/**
|
|
* Get the default database schema name
|
|
* @returns {string} The default schema name, such as 'public' or 'dbo'
|
|
*/
|
|
SQLConnector.prototype.getDefaultSchemaName = function() {
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Get the database schema name for the given model. The schema name can be
|
|
* customized at model settings or connector configuration level as `schema` or
|
|
* `schemaName`. For example,
|
|
*
|
|
* ```json
|
|
* "Customer": {
|
|
* "name": "Customer",
|
|
* "mysql": {
|
|
* "schema": "MYDB",
|
|
* "table": "CUSTOMER"
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {String} model The model name
|
|
* @returns {String} The database schema name
|
|
*/
|
|
SQLConnector.prototype.schema = function(model) {
|
|
// Check if there is a 'schema' property for connector
|
|
const dbMeta = this.getConnectorSpecificSettings(model);
|
|
const schemaName = (dbMeta && (dbMeta.schema || dbMeta.schemaName)) ||
|
|
(this.settings.schema || this.settings.schemaName) ||
|
|
this.getDefaultSchemaName();
|
|
return schemaName;
|
|
};
|
|
|
|
/**
|
|
* Get the table name for the given model. The table name can be customized
|
|
* at model settings as `table` or `tableName`. For example,
|
|
*
|
|
* ```json
|
|
* "Customer": {
|
|
* "name": "Customer",
|
|
* "mysql": {
|
|
* "table": "CUSTOMER"
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* Returns the table name (String).
|
|
* @param {String} model The model name
|
|
*/
|
|
SQLConnector.prototype.table = function(model) {
|
|
const dbMeta = this.getConnectorSpecificSettings(model);
|
|
let tableName;
|
|
if (dbMeta) {
|
|
tableName = dbMeta.table || dbMeta.tableName;
|
|
if (tableName) {
|
|
// Explicit table name, return as-is
|
|
return tableName;
|
|
}
|
|
}
|
|
tableName = model;
|
|
if (typeof this.dbName === 'function') {
|
|
tableName = this.dbName(tableName);
|
|
}
|
|
return tableName;
|
|
};
|
|
|
|
/**
|
|
* Get the column metadata for the given model property
|
|
* @param {String} model The model name
|
|
* @param {String} property The property name
|
|
* @returns {Object} The column metadata
|
|
*/
|
|
SQLConnector.prototype.columnMetadata = function(model, property) {
|
|
return this.getDataSource(model).columnMetadata(model, property);
|
|
};
|
|
|
|
/**
|
|
* Get the corresponding property name for the given column name
|
|
* @param {String} model The model name
|
|
* @param {String} column The column name
|
|
* @returns {String} The property name for a given column
|
|
*/
|
|
SQLConnector.prototype.propertyName = function(model, column) {
|
|
const props = this.getModelDefinition(model).properties;
|
|
for (const p in props) {
|
|
if (this.column(model, p) === column) {
|
|
return p;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the escaped id column name
|
|
* @param {String} model The model name
|
|
* @returns {String} the escaped id column name
|
|
*/
|
|
SQLConnector.prototype.idColumnEscaped = function(model) {
|
|
return this.escapeName(this.idColumn(model));
|
|
};
|
|
|
|
/**
|
|
* Get the escaped table name
|
|
* @param {String} model The model name
|
|
* @returns {String} the escaped table name
|
|
*/
|
|
SQLConnector.prototype.tableEscaped = function(model) {
|
|
return this.escapeName(this.table(model));
|
|
};
|
|
|
|
/**
|
|
* Get the escaped column name for a given model property
|
|
* @param {String} model The model name
|
|
* @param {String} property The property name
|
|
* @returns {String} The escaped column name
|
|
*/
|
|
SQLConnector.prototype.columnEscaped = function(model, property) {
|
|
return this.escapeName(this.column(model, property));
|
|
};
|
|
|
|
/*!
|
|
* Check if id value is set
|
|
* @param idValue
|
|
* @param cb
|
|
* @param returningNull
|
|
* @returns {boolean}
|
|
*/
|
|
function isIdValuePresent(idValue, cb, returningNull) {
|
|
try {
|
|
assert(idValue !== null && idValue !== undefined, 'id value is required');
|
|
return true;
|
|
} catch (err) {
|
|
process.nextTick(function() {
|
|
if (cb) cb(returningNull ? null : err);
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert the id value to the form required by database column
|
|
* @param {String} model The model name
|
|
* @param {*} idValue The id property value
|
|
* @returns {*} The escaped id column value
|
|
*/
|
|
SQLConnector.prototype.idColumnValue = function(model, idValue) {
|
|
const idProp = this.getDataSource(model).idProperty(model);
|
|
if (typeof this.toColumnValue === 'function') {
|
|
return this.toColumnValue(idProp, idValue);
|
|
} else {
|
|
return idValue;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Replace `?` with connector specific placeholders. For example,
|
|
*
|
|
* ```
|
|
* {sql: 'SELECT * FROM CUSTOMER WHERE NAME=?', params: ['John']}
|
|
* ==>
|
|
* {sql: 'SELECT * FROM CUSTOMER WHERE NAME=:1', params: ['John']}
|
|
* ```
|
|
* *LIMITATION*: We don't handle the ? inside escaped values, for example,
|
|
* `SELECT * FROM CUSTOMER WHERE NAME='J?hn'` will not be parameterized
|
|
* correctly.
|
|
*
|
|
* @param {ParameterizedSQL|Object} ps Parameterized SQL
|
|
* @returns {ParameterizedSQL} Parameterized SQL with the connector specific
|
|
* placeholders
|
|
*/
|
|
SQLConnector.prototype.parameterize = function(ps) {
|
|
ps = new ParameterizedSQL(ps);
|
|
|
|
// The value is parameterized, for example
|
|
// {sql: 'to_point(?,?)', values: [1, 2]}
|
|
const parts = ps.sql.split(PLACEHOLDER);
|
|
const clause = [];
|
|
for (let j = 0, m = parts.length; j < m; j++) {
|
|
// Replace ? with the keyed placeholder, such as :5
|
|
clause.push(parts[j]);
|
|
if (j !== parts.length - 1) {
|
|
clause.push(this.getPlaceholderForValue(j + 1));
|
|
}
|
|
}
|
|
ps.sql = clause.join('');
|
|
return ps;
|
|
};
|
|
|
|
/**
|
|
* Build the the `INSERT INTO` statement
|
|
* @param {String} model The model name
|
|
* @param {Object} fields Fields to be inserted
|
|
* @param {Object} options Options object
|
|
* @returns {ParameterizedSQL}
|
|
*/
|
|
SQLConnector.prototype.buildInsertInto = function(model, fields, options) {
|
|
const stmt = new ParameterizedSQL('INSERT INTO ' + this.tableEscaped(model));
|
|
const columnNames = fields.names.join(',');
|
|
if (columnNames) {
|
|
stmt.merge('(' + columnNames + ')', '');
|
|
}
|
|
return stmt;
|
|
};
|
|
|
|
/**
|
|
* Build the clause to return id values after insert
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model data object
|
|
* @param {Object} options Options object
|
|
* @returns {string}
|
|
*/
|
|
SQLConnector.prototype.buildInsertReturning = function(model, data, options) {
|
|
return '';
|
|
};
|
|
|
|
/**
|
|
* Build the clause for default values if the fields is empty
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model data object
|
|
* @param {Object} options Options object
|
|
* @returns {string} 'DEFAULT VALUES'
|
|
*/
|
|
SQLConnector.prototype.buildInsertDefaultValues = function(model, data, options) {
|
|
return 'VALUES()';
|
|
};
|
|
|
|
/**
|
|
* Build INSERT SQL statement
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model data object
|
|
* @param {Object} options The options object
|
|
* @returns {string} The INSERT SQL statement
|
|
*/
|
|
SQLConnector.prototype.buildInsert = function(model, data, options) {
|
|
const fields = this.buildFields(model, data);
|
|
const insertStmt = this.buildInsertInto(model, fields, options);
|
|
const columnValues = fields.columnValues;
|
|
const fieldNames = fields.names;
|
|
if (fieldNames.length) {
|
|
const values = ParameterizedSQL.join(columnValues, ',');
|
|
values.sql = 'VALUES(' + values.sql + ')';
|
|
insertStmt.merge(values);
|
|
} else {
|
|
insertStmt.merge(this.buildInsertDefaultValues(model, data, options));
|
|
}
|
|
const returning = this.buildInsertReturning(model, data, options);
|
|
if (returning) {
|
|
insertStmt.merge(returning);
|
|
}
|
|
return this.parameterize(insertStmt);
|
|
};
|
|
|
|
/**
|
|
* Build INSERT SQL statement for multiple values
|
|
* @param {String} model The model name
|
|
* @param {Object} data The array of model data object
|
|
* @param {Object} options The options object
|
|
* @returns {Object} The ParameterizedSQL Object with INSERT SQL statement
|
|
*/
|
|
SQLConnector.prototype.buildInsertAll = function(model, data, options) {
|
|
if (!this.multiInsertSupported) {
|
|
debug('multiple value insert SQL dialect is not supported by this connector');
|
|
// return immediately if multiInsertSupported=false in connector
|
|
return null;
|
|
}
|
|
const fieldsArray = this.buildFieldsFromArray(model, data);
|
|
if (Array.isArray(fieldsArray) && fieldsArray.length === 0) {
|
|
debug('no fields found for insert query');
|
|
// return immediately if no fields found
|
|
return null;
|
|
}
|
|
const insertStmt = this.buildInsertInto(model, fieldsArray[0], options);
|
|
|
|
for (let i = 0; i < fieldsArray.length; i++) {
|
|
const columnValues = fieldsArray[i].columnValues;
|
|
const isLast = (i === (fieldsArray.length - 1));
|
|
const isFirst = (i === 0);
|
|
|
|
if (columnValues.length) {
|
|
const values = ParameterizedSQL.join(columnValues, ',');
|
|
// Multi value query.
|
|
// This lets multiple row insertion in single query
|
|
values.sql = (isFirst ? 'VALUES ' : '') +
|
|
'(' + values.sql + ')' +
|
|
(isLast ? '' : ',');
|
|
insertStmt.merge(values);
|
|
} else {
|
|
// Insert default values if no values provided
|
|
insertStmt.merge(this.buildInsertDefaultValues(model, data, options));
|
|
}
|
|
}
|
|
|
|
const returning = this.buildInsertReturning(model, data, options);
|
|
if (returning) {
|
|
insertStmt.merge(returning);
|
|
}
|
|
return this.parameterize(insertStmt);
|
|
};
|
|
|
|
/**
|
|
* Execute a SQL statement with given parameters.
|
|
*
|
|
* @param {String} sql The SQL statement
|
|
* @param {Array} An array of parameter values
|
|
* @param {Object} [options] Options object
|
|
* @param {Function} [callback] The callback function
|
|
*/
|
|
SQLConnector.prototype.execute = function(sql, params, options, callback) {
|
|
assert(typeof sql === 'string', 'sql must be a string');
|
|
if (typeof params === 'function' && options === undefined &&
|
|
callback === undefined) {
|
|
// execute(sql, callback)
|
|
options = {};
|
|
callback = params;
|
|
params = [];
|
|
} else if (typeof options === 'function' && callback === undefined) {
|
|
// execute(sql, params, callback)
|
|
callback = options;
|
|
options = {};
|
|
}
|
|
params = params || [];
|
|
options = options || {};
|
|
assert(Array.isArray(params), 'params must be an array');
|
|
assert(typeof options === 'object', 'options must be an object');
|
|
assert(typeof callback === 'function', 'callback must be a function');
|
|
|
|
const self = this;
|
|
if (!this.dataSource.connected) {
|
|
// Prevent adding too many listeners to the 'connected' event on the datasource.
|
|
if (this.dataSource.listenerCount('connected') <
|
|
this.dataSource.getMaxOfflineRequests()) {
|
|
// allow this listener to be added to this event
|
|
return this.dataSource.once('connected', function() {
|
|
self.execute(sql, params, options, callback);
|
|
});
|
|
} else {
|
|
const limitReachedError = new Error(
|
|
g.f(
|
|
'Event listener limit reached. ' +
|
|
'Increase maxOfflineRequests value in datasources.json.',
|
|
),
|
|
);
|
|
callback(limitReachedError);
|
|
}
|
|
}
|
|
const context = {
|
|
req: {
|
|
sql: sql,
|
|
params: params,
|
|
},
|
|
options: options,
|
|
};
|
|
this.notifyObserversAround('execute', context, function(context, done) {
|
|
self.executeSQL(context.req.sql, context.req.params, context.options,
|
|
function(err, info) {
|
|
if (err) {
|
|
debug('Error: %j %j %j', err, context.req.sql, context.req.params);
|
|
}
|
|
|
|
if (!err && info != null) {
|
|
context.res = info;
|
|
}
|
|
// Don't pass more than one args as it will confuse async.waterfall
|
|
done(err, info);
|
|
});
|
|
}, callback);
|
|
};
|
|
|
|
/**
|
|
* Create the data model in MySQL
|
|
*
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model instance data
|
|
* @param {Object} options Options object
|
|
* @param {Function} [callback] The callback function
|
|
*/
|
|
SQLConnector.prototype.create = function(model, data, options, callback) {
|
|
const self = this;
|
|
const stmt = this.buildInsert(model, data, options);
|
|
this.execute(stmt.sql, stmt.params, options, function(err, info) {
|
|
if (err) {
|
|
callback(err);
|
|
} else {
|
|
const insertedId = self.getInsertedId(model, info);
|
|
callback(err, insertedId);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Create multiple data models in a single insert query
|
|
* Works only if `multiInsertSupported` is set to true for the connector
|
|
*
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model instances data
|
|
* @param {Object} options Options object
|
|
* @param {Function} [callback] The callback function
|
|
*/
|
|
SQLConnector.prototype.createAll = function(model, data, options, callback) {
|
|
const self = this;
|
|
const stmt = this.buildInsertAll(model, data, options);
|
|
if (!stmt) {
|
|
debug('empty SQL statement returned for insert into multiple values');
|
|
callback(new Error(
|
|
g.f('empty SQL statement returned for insert into multiple values'),
|
|
));
|
|
}
|
|
this.execute(stmt.sql, stmt.params, options, function(err, info) {
|
|
if (err) {
|
|
callback(err);
|
|
} else {
|
|
const returnData = self.getInsertedDataArray(model, info, data);
|
|
callback(err, returnData);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Save the model instance into the database
|
|
* @param {String} model The model name
|
|
* @param {Object} data The model instance data
|
|
* @param {Object} options Options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.save = function(model, data, options, cb) {
|
|
const idName = this.idName(model);
|
|
const idValue = data[idName];
|
|
|
|
if (!isIdValuePresent(idValue, cb)) {
|
|
return;
|
|
}
|
|
|
|
const where = {};
|
|
where[idName] = idValue;
|
|
|
|
let updateStmt = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model));
|
|
updateStmt.merge(this.buildFieldsForUpdate(model, data));
|
|
const whereStmt = this.buildWhere(model, where);
|
|
updateStmt.merge(whereStmt);
|
|
updateStmt = this.parameterize(updateStmt);
|
|
this.execute(updateStmt.sql, updateStmt.params, options,
|
|
function(err, result) {
|
|
if (cb) cb(err, result);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Check if a model instance exists for the given id value
|
|
* @param {String} model The model name
|
|
* @param {*} id The id value
|
|
* @param {Object} options Options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.exists = function(model, id, options, cb) {
|
|
if (!isIdValuePresent(id, cb, true)) {
|
|
return;
|
|
}
|
|
const idName = this.idName(model);
|
|
const where = {};
|
|
where[idName] = id;
|
|
let selectStmt = new ParameterizedSQL(
|
|
'SELECT 1 FROM ' + this.tableEscaped(model) +
|
|
' WHERE ' + this.idColumnEscaped(model),
|
|
);
|
|
selectStmt.merge(this.buildWhere(model, where));
|
|
selectStmt = this.applyPagination(model, selectStmt, {
|
|
limit: 1,
|
|
offset: 0,
|
|
order: [idName],
|
|
});
|
|
selectStmt = this.parameterize(selectStmt);
|
|
|
|
this.execute(selectStmt.sql, selectStmt.params, options, function(err, data) {
|
|
if (!cb) return;
|
|
if (err) {
|
|
cb(err);
|
|
} else {
|
|
cb(null, data.length >= 1);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* ATM, this method is not used by loopback-datasource-juggler dao, which
|
|
* maps `destroy` to `destroyAll` with a `where` filter that includes the `id`
|
|
* instead.
|
|
*
|
|
* Delete a model instance by id value
|
|
* @param {String} model The model name
|
|
* @param {*} id The id value
|
|
* @param {Object} options Options object
|
|
* @param {Function} cb The callback function
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype.destroy = function(model, id, options, cb) {
|
|
if (!isIdValuePresent(id, cb, true)) {
|
|
return;
|
|
}
|
|
const idName = this.idName(model);
|
|
const where = {};
|
|
where[idName] = id;
|
|
this.destroyAll(model, where, options, cb);
|
|
};
|
|
|
|
// Alias to `destroy`. Juggler checks `destroy` only.
|
|
Connector.defineAliases(SQLConnector.prototype, 'destroy',
|
|
['delete', 'deleteById', 'destroyById']);
|
|
|
|
/**
|
|
* Build the `DELETE FROM` SQL statement
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} options Options object
|
|
* @returns {ParameterizedSQL} The SQL DELETE FROM statement
|
|
*/
|
|
SQLConnector.prototype.buildDelete = function(model, where, options) {
|
|
const deleteStmt = new ParameterizedSQL('DELETE FROM ' +
|
|
this.tableEscaped(model));
|
|
deleteStmt.merge(this.buildWhere(model, where));
|
|
return this.parameterize(deleteStmt);
|
|
};
|
|
|
|
/**
|
|
* Delete all matching model instances
|
|
*
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.destroyAll = function(model, where, options, cb) {
|
|
const stmt = this.buildDelete(model, where, options);
|
|
this._executeAlteringQuery(model, stmt.sql, stmt.params, options, cb || NOOP);
|
|
};
|
|
|
|
// Alias to `destroyAll`. Juggler checks `destroyAll` only.
|
|
Connector.defineAliases(SQLConnector.prototype, 'destroyAll', ['deleteAll']);
|
|
|
|
/**
|
|
* ATM, this method is not used by loopback-datasource-juggler dao, which
|
|
* maps `updateAttributes` to `update` with a `where` filter that includes the
|
|
* `id` instead.
|
|
*
|
|
* Update attributes for a given model instance
|
|
* @param {String} model The model name
|
|
* @param {*} id The id value
|
|
* @param {Object} data The model data instance containing all properties to
|
|
* be updated
|
|
* @param {Object} options Options object
|
|
* @param {Function} cb The callback function
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype.updateAttributes = function(model, id, data, options, cb) {
|
|
if (!isIdValuePresent(id, cb)) return;
|
|
const where = this._buildWhereObjById(model, id, data);
|
|
this.updateAll(model, where, data, options, function(err, info) {
|
|
if (err) return cb(err);
|
|
if (info.count === 0) {
|
|
return cb(errorIdNotFoundForUpdate(where.id));
|
|
} else {
|
|
return cb(null, info);
|
|
}
|
|
});
|
|
};
|
|
|
|
function errorIdNotFoundForUpdate(idValue) {
|
|
const msg = g.f(
|
|
'Could not update attributes. {{Object}} with {{id}} %s does not exist!', idValue,
|
|
);
|
|
const error = new Error(msg);
|
|
error.statusCode = error.status = 404;
|
|
return error;
|
|
}
|
|
|
|
/**
|
|
* Replace attributes for a given model instance
|
|
* @param {String} model The model name
|
|
* @param {*} id The id value
|
|
* @param {Object} data The model data instance containing all properties to
|
|
* be replaced
|
|
* @param {Object} options Options object
|
|
* @param {Function} cb The callback function
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype.replaceById = function(model, id, data, options, cb) {
|
|
if (!isIdValuePresent(id, cb)) return;
|
|
const where = this._buildWhereObjById(model, id, data);
|
|
this._replace(model, where, data, options, cb);
|
|
};
|
|
|
|
/*
|
|
* @param model The model name.
|
|
* @param id The instance ID.
|
|
* @param {Object} data The data Object.
|
|
* @returns {Object} where The where object for a spcific instance.
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype._buildWhereObjById = function(model, id, data) {
|
|
const idName = this.idName(model);
|
|
delete data[idName];
|
|
const where = {};
|
|
where[idName] = id;
|
|
return where;
|
|
};
|
|
|
|
/**
|
|
* Build the UPDATE statement
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} data The data to be changed
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
* @returns {ParameterizedSQL} The UPDATE SQL statement
|
|
*/
|
|
SQLConnector.prototype.buildUpdate = function(model, where, data, options) {
|
|
const fields = this.buildFieldsForUpdate(model, data);
|
|
return this._constructUpdateQuery(model, where, fields);
|
|
};
|
|
|
|
/**
|
|
* Build the UPDATE statement for replacing
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} data The data to be changed
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
* @returns {ParameterizedSQL} The UPDATE SQL statement for replacing fields
|
|
*/
|
|
SQLConnector.prototype.buildReplace = function(model, where, data, options) {
|
|
const fields = this.buildFieldsForReplace(model, data);
|
|
return this._constructUpdateQuery(model, where, fields);
|
|
};
|
|
|
|
/*
|
|
* @param model The model name.
|
|
* @param {} where The where object.
|
|
* @param {Object} field The parameterizedSQL fileds.
|
|
* @returns {Object} update query Constructed update query.
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype._constructUpdateQuery = function(model, where, fields) {
|
|
const updateClause = new ParameterizedSQL('UPDATE ' + this.tableEscaped(model));
|
|
const whereClause = this.buildWhere(model, where);
|
|
updateClause.merge([fields, whereClause]);
|
|
return this.parameterize(updateClause);
|
|
};
|
|
|
|
/**
|
|
* Update all instances that match the where clause with the given data
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} data The property/value object representing changes
|
|
* to be made
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.update = function(model, where, data, options, cb) {
|
|
const stmt = this.buildUpdate(model, where, data, options);
|
|
this._executeAlteringQuery(model, stmt.sql, stmt.params, options, cb || NOOP);
|
|
};
|
|
|
|
/**
|
|
* Replace all instances that match the where clause with the given data
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} data The property/value object representing changes
|
|
* to be made
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype._replace = function(model, where, data, options, cb) {
|
|
const self = this;
|
|
const stmt = this.buildReplace(model, where, data, options);
|
|
this.execute(stmt.sql, stmt.params, options, function(err, info) {
|
|
if (err) return cb(err);
|
|
const affectedRows = self.getCountForAffectedRows(model, info);
|
|
const rowCount = typeof (affectedRows) === 'number' ?
|
|
affectedRows : info.affectedRows;
|
|
if (rowCount === 0) {
|
|
return cb(errorIdNotFoundForReplace(where.id));
|
|
} else {
|
|
return cb(null, info);
|
|
}
|
|
});
|
|
};
|
|
|
|
function errorIdNotFoundForReplace(idValue) {
|
|
const msg = g.f('Could not replace. Object with id %s does not exist!', idValue);
|
|
const error = new Error(msg);
|
|
error.statusCode = error.status = 404;
|
|
return error;
|
|
}
|
|
|
|
SQLConnector.prototype._executeAlteringQuery = function(model, sql, params, options, cb) {
|
|
const self = this;
|
|
this.execute(sql, params, options, function(err, info) {
|
|
const affectedRows = self.getCountForAffectedRows(model, info);
|
|
cb(err, {count: affectedRows});
|
|
});
|
|
};
|
|
|
|
// Alias to `update` and `replace`. Juggler checks `update` and `replace` only.
|
|
Connector.defineAliases(SQLConnector.prototype, 'update', ['updateAll']);
|
|
Connector.defineAliases(SQLConnector.prototype, 'replace', ['replaceAll']);
|
|
|
|
/**
|
|
* Build the SQL WHERE clause for the where object
|
|
* @param {string} model Model name
|
|
* @param {object} where An object for the where conditions
|
|
* @returns {ParameterizedSQL} The SQL WHERE clause
|
|
*/
|
|
SQLConnector.prototype.buildWhere = function(model, where) {
|
|
const whereClause = this._buildWhere(model, where);
|
|
if (whereClause.sql) {
|
|
whereClause.sql = 'WHERE ' + whereClause.sql;
|
|
}
|
|
return whereClause;
|
|
};
|
|
|
|
/**
|
|
* Build SQL expression
|
|
* @param {String} columnName Escaped column name
|
|
* @param {String} operator SQL operator
|
|
* @param {*} columnValue Column value
|
|
* @param {*} propertyValue Property value
|
|
* @returns {ParameterizedSQL} The SQL expression
|
|
*/
|
|
SQLConnector.prototype.buildExpression =
|
|
function(columnName, operator, columnValue, propertyValue) {
|
|
function buildClause(columnValue, separator, grouping) {
|
|
const values = [];
|
|
for (let i = 0, n = columnValue.length; i < n; i++) {
|
|
if (columnValue[i] instanceof ParameterizedSQL) {
|
|
values.push(columnValue[i]);
|
|
} else {
|
|
values.push(new ParameterizedSQL(PLACEHOLDER, [columnValue[i]]));
|
|
}
|
|
}
|
|
separator = separator || ',';
|
|
const clause = ParameterizedSQL.join(values, separator);
|
|
if (grouping) {
|
|
clause.sql = '(' + clause.sql + ')';
|
|
}
|
|
return clause;
|
|
}
|
|
|
|
let sqlExp = columnName;
|
|
let clause;
|
|
if (columnValue instanceof ParameterizedSQL) {
|
|
clause = columnValue;
|
|
} else {
|
|
clause = new ParameterizedSQL(PLACEHOLDER, [columnValue]);
|
|
}
|
|
switch (operator) {
|
|
case 'gt':
|
|
sqlExp += '>';
|
|
break;
|
|
case 'gte':
|
|
sqlExp += '>=';
|
|
break;
|
|
case 'lt':
|
|
sqlExp += '<';
|
|
break;
|
|
case 'lte':
|
|
sqlExp += '<=';
|
|
break;
|
|
case 'between':
|
|
sqlExp += ' BETWEEN ';
|
|
clause = buildClause(columnValue, ' AND ', false);
|
|
break;
|
|
case 'inq':
|
|
sqlExp += ' IN ';
|
|
clause = buildClause(columnValue, ',', true);
|
|
break;
|
|
case 'nin':
|
|
sqlExp += ' NOT IN ';
|
|
clause = buildClause(columnValue, ',', true);
|
|
break;
|
|
case 'neq':
|
|
if (columnValue == null) {
|
|
return new ParameterizedSQL(sqlExp + ' IS NOT NULL');
|
|
}
|
|
sqlExp += '!=';
|
|
break;
|
|
case 'like':
|
|
sqlExp += ' LIKE ';
|
|
break;
|
|
case 'nlike':
|
|
sqlExp += ' NOT LIKE ';
|
|
break;
|
|
// this case not needed since each database has its own regex syntax, but
|
|
// we leave the MySQL syntax here as a placeholder
|
|
case 'regexp':
|
|
sqlExp += ' REGEXP ';
|
|
break;
|
|
}
|
|
const stmt = ParameterizedSQL.join([sqlExp, clause], '');
|
|
return stmt;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @param model
|
|
* @param where
|
|
* @returns {ParameterizedSQL}
|
|
*/
|
|
SQLConnector.prototype._buildWhere = function(model, where) {
|
|
let columnValue, sqlExp;
|
|
if (!where) {
|
|
return new ParameterizedSQL('');
|
|
}
|
|
if (typeof where !== 'object' || Array.isArray(where)) {
|
|
debug('Invalid value for where: %j', where);
|
|
return new ParameterizedSQL('');
|
|
}
|
|
const self = this;
|
|
const props = self.getModelDefinition(model).properties;
|
|
|
|
const whereStmts = [];
|
|
for (const key in where) {
|
|
const stmt = new ParameterizedSQL('', []);
|
|
// Handle and/or operators
|
|
if (key === 'and' || key === 'or') {
|
|
const branches = [];
|
|
let branchParams = [];
|
|
const clauses = where[key];
|
|
if (Array.isArray(clauses)) {
|
|
for (let i = 0, n = clauses.length; i < n; i++) {
|
|
const stmtForClause = self._buildWhere(model, clauses[i]);
|
|
if (stmtForClause.sql) {
|
|
stmtForClause.sql = '(' + stmtForClause.sql + ')';
|
|
branchParams = branchParams.concat(stmtForClause.params);
|
|
branches.push(stmtForClause.sql);
|
|
}
|
|
}
|
|
stmt.merge({
|
|
sql: '(' + branches.join(' ' + key.toUpperCase() + ' ') + ')',
|
|
params: branchParams,
|
|
});
|
|
whereStmts.push(stmt);
|
|
continue;
|
|
}
|
|
// The value is not an array, fall back to regular fields
|
|
}
|
|
const p = props[key];
|
|
if (p == null) {
|
|
// Unknown property, ignore it
|
|
debug('Unknown property %s is skipped for model %s', key, model);
|
|
continue;
|
|
}
|
|
// eslint-disable one-var
|
|
let expression = where[key];
|
|
const columnName = self.columnEscaped(model, key);
|
|
// eslint-enable one-var
|
|
if (expression === null || expression === undefined) {
|
|
stmt.merge(columnName + ' IS NULL');
|
|
} else if (expression && expression.constructor === Object) {
|
|
const operator = Object.keys(expression)[0];
|
|
// Get the expression without the operator
|
|
expression = expression[operator];
|
|
if (operator === 'inq' || operator === 'nin' || operator === 'between') {
|
|
columnValue = [];
|
|
if (Array.isArray(expression)) {
|
|
// Column value is a list
|
|
for (let j = 0, m = expression.length; j < m; j++) {
|
|
columnValue.push(this.toColumnValue(p, expression[j]));
|
|
}
|
|
} else {
|
|
columnValue.push(this.toColumnValue(p, expression));
|
|
}
|
|
if (operator === 'between') {
|
|
// BETWEEN v1 AND v2
|
|
const v1 = columnValue[0] === undefined ? null : columnValue[0];
|
|
const v2 = columnValue[1] === undefined ? null : columnValue[1];
|
|
columnValue = [v1, v2];
|
|
} else {
|
|
// IN (v1,v2,v3) or NOT IN (v1,v2,v3)
|
|
if (columnValue.length === 0) {
|
|
if (operator === 'inq') {
|
|
columnValue = [null];
|
|
} else {
|
|
// nin () is true
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
} else if (operator === 'regexp' && expression instanceof RegExp) {
|
|
// do not coerce RegExp based on property definitions
|
|
columnValue = expression;
|
|
} else {
|
|
columnValue = this.toColumnValue(p, expression);
|
|
}
|
|
sqlExp = self.buildExpression(columnName, operator, columnValue, p);
|
|
stmt.merge(sqlExp);
|
|
} else {
|
|
// The expression is the field value, not a condition
|
|
columnValue = self.toColumnValue(p, expression);
|
|
if (columnValue === null) {
|
|
stmt.merge(columnName + ' IS NULL');
|
|
} else {
|
|
if (columnValue instanceof ParameterizedSQL) {
|
|
stmt.merge(columnName + '=').merge(columnValue);
|
|
} else {
|
|
stmt.merge({
|
|
sql: columnName + '=?',
|
|
params: [columnValue],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
whereStmts.push(stmt);
|
|
}
|
|
let params = [];
|
|
const sqls = [];
|
|
for (let k = 0, s = whereStmts.length; k < s; k++) {
|
|
if (!whereStmts[k].sql) continue;
|
|
sqls.push(whereStmts[k].sql);
|
|
params = params.concat(whereStmts[k].params);
|
|
}
|
|
const whereStmt = new ParameterizedSQL({
|
|
sql: sqls.join(' AND '),
|
|
params: params,
|
|
});
|
|
return whereStmt;
|
|
};
|
|
|
|
/**
|
|
* Build the ORDER BY clause
|
|
* @param {string} model Model name
|
|
* @param {string[]} order An array of sorting criteria
|
|
* @returns {string} The ORDER BY clause
|
|
*/
|
|
SQLConnector.prototype.buildOrderBy = function(model, order) {
|
|
if (!order) {
|
|
return '';
|
|
}
|
|
const self = this;
|
|
if (typeof order === 'string') {
|
|
order = [order];
|
|
}
|
|
const clauses = [];
|
|
for (let i = 0, n = order.length; i < n; i++) {
|
|
const t = order[i].split(/[\s,]+/);
|
|
if (t.length === 1) {
|
|
clauses.push(self.columnEscaped(model, order[i]));
|
|
} else {
|
|
clauses.push(self.columnEscaped(model, t[0]) + ' ' + t[1]);
|
|
}
|
|
}
|
|
return 'ORDER BY ' + clauses.join(',');
|
|
};
|
|
|
|
/**
|
|
* Build an array of fields for the database operation
|
|
* @param {String} model Model name
|
|
* @param {Object} data Model data object
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to false
|
|
* @returns {{names: Array, values: Array, properties: Array}}
|
|
*/
|
|
SQLConnector.prototype.buildFields = function(model, data, excludeIds) {
|
|
const keys = Object.keys(data);
|
|
return this._buildFieldsForKeys(model, data, keys, excludeIds);
|
|
};
|
|
|
|
/**
|
|
* Build an array of fields for the database operation from data array
|
|
* @param {String} model Model name
|
|
* @param {Object} data Array of Model data object
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to false
|
|
* @returns {[{names: Array, values: Array, properties: Array}]}
|
|
*/
|
|
SQLConnector.prototype.buildFieldsFromArray = function(model, data, excludeIds) {
|
|
const fields = [];
|
|
if (Array.isArray(data) && data.length > 0) {
|
|
const keys = Object.keys(data[0]);
|
|
for (let i = 0; i < data.length; i++) {
|
|
fields.push(this._buildFieldsForKeys(model, data[i], keys, excludeIds));
|
|
}
|
|
}
|
|
return fields;
|
|
};
|
|
|
|
/**
|
|
* Build an array of fields for the replace database operation
|
|
* @param {String} model Model name
|
|
* @param {Object} data Model data object
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to false
|
|
* @returns {{names: Array, values: Array, properties: Array}}
|
|
*/
|
|
SQLConnector.prototype.buildReplaceFields = function(model, data, excludeIds) {
|
|
const props = this.getModelDefinition(model).properties;
|
|
const keys = Object.keys(props);
|
|
return this._buildFieldsForKeys(model, data, keys, excludeIds);
|
|
};
|
|
|
|
/*
|
|
* @param {String} model The model name.
|
|
* @returns {Object} data The model data object.
|
|
* @returns {Array} keys The key fields for which need to be built.
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to false
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype._buildFieldsForKeys = function(model, data, keys, excludeIds) {
|
|
const props = this.getModelDefinition(model).properties;
|
|
const fields = {
|
|
names: [], // field names
|
|
columnValues: [], // an array of ParameterizedSQL
|
|
properties: [], // model properties
|
|
};
|
|
for (let i = 0, n = keys.length; i < n; i++) {
|
|
const key = keys[i];
|
|
const p = props[key];
|
|
if (p == null) {
|
|
// Unknown property, ignore it
|
|
debug('Unknown property %s is skipped for model %s', key, model);
|
|
continue;
|
|
}
|
|
|
|
if ((excludeIds && p.id) || p.readOnly) {
|
|
continue;
|
|
}
|
|
const k = this.columnEscaped(model, key);
|
|
const v = this.toColumnValue(p, data[key]);
|
|
if (v !== undefined) {
|
|
fields.names.push(k);
|
|
if (v instanceof ParameterizedSQL) {
|
|
fields.columnValues.push(v);
|
|
} else {
|
|
fields.columnValues.push(new ParameterizedSQL(PLACEHOLDER, [v]));
|
|
}
|
|
fields.properties.push(p);
|
|
}
|
|
}
|
|
return fields;
|
|
};
|
|
|
|
/**
|
|
* Build the SET clause for database update.
|
|
* @param {String} model Model name.
|
|
* @param {Object} data The model data object.
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to true.
|
|
* @returns {string} The list of fields for update query.
|
|
*/
|
|
SQLConnector.prototype.buildFieldsForUpdate = function(model, data, excludeIds) {
|
|
if (excludeIds === undefined) {
|
|
excludeIds = true;
|
|
}
|
|
const fields = this.buildFields(model, data, excludeIds);
|
|
return this._constructUpdateParameterizedSQL(fields);
|
|
};
|
|
|
|
/**
|
|
* Build the SET clause for database replace through update query.
|
|
* @param {String} model Model name.
|
|
* @param {Object} data The model data object.
|
|
* @param {Boolean} excludeIds Exclude id properties or not, default to true.
|
|
* @returns {string} The list of fields for update query.
|
|
*/
|
|
SQLConnector.prototype.buildFieldsForReplace = function(model, data, excludeIds) {
|
|
if (excludeIds === undefined) {
|
|
excludeIds = true;
|
|
}
|
|
const fields = this.buildReplaceFields(model, data, excludeIds);
|
|
return this._constructUpdateParameterizedSQL(fields);
|
|
};
|
|
|
|
/*
|
|
* @param {Object} field The fileds.
|
|
* @returns {Object} parameterizedSQL.
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype._constructUpdateParameterizedSQL = function(fields) {
|
|
const columns = new ParameterizedSQL('');
|
|
for (let i = 0, n = fields.names.length; i < n; i++) {
|
|
const clause = ParameterizedSQL.append(fields.names[i],
|
|
fields.columnValues[i], '=');
|
|
columns.merge(clause, ',');
|
|
}
|
|
columns.sql = 'SET ' + columns.sql;
|
|
return columns;
|
|
};
|
|
|
|
/**
|
|
* Build a list of escaped column names for the given model and fields filter
|
|
* @param {string} model Model name
|
|
* @param {object} filter The filter object
|
|
* @returns {string} Comma separated string of escaped column names
|
|
*/
|
|
SQLConnector.prototype.buildColumnNames = function(model, filter) {
|
|
const fieldsFilter = filter && filter.fields;
|
|
const cols = this.getModelDefinition(model).properties;
|
|
if (!cols) {
|
|
return '*';
|
|
}
|
|
const self = this;
|
|
let keys = Object.keys(cols);
|
|
if (Array.isArray(fieldsFilter) && fieldsFilter.length > 0) {
|
|
// Not empty array, including all the fields that are valid properties
|
|
keys = fieldsFilter.filter(function(f) {
|
|
return cols[f];
|
|
});
|
|
} else if ('object' === typeof fieldsFilter &&
|
|
Object.keys(fieldsFilter).length > 0) {
|
|
// { field1: boolean, field2: boolean ... }
|
|
const included = [];
|
|
const excluded = [];
|
|
keys.forEach(function(k) {
|
|
if (fieldsFilter[k]) {
|
|
included.push(k);
|
|
} else if ((k in fieldsFilter) && !fieldsFilter[k]) {
|
|
excluded.push(k);
|
|
}
|
|
});
|
|
if (included.length > 0) {
|
|
keys = included;
|
|
} else if (excluded.length > 0) {
|
|
excluded.forEach(function(e) {
|
|
const index = keys.indexOf(e);
|
|
keys.splice(index, 1);
|
|
});
|
|
}
|
|
}
|
|
const names = keys.map(function(c) {
|
|
return self.columnEscaped(model, c);
|
|
});
|
|
return names.join(',');
|
|
};
|
|
|
|
/**
|
|
* Build a SQL SELECT statement
|
|
* @param {String} model Model name
|
|
* @param {Object} filter Filter object
|
|
* @param {Object} options Options object
|
|
* @returns {ParameterizedSQL} Statement object {sql: ..., params: ...}
|
|
*/
|
|
SQLConnector.prototype.buildSelect = function(model, filter, options) {
|
|
if (!filter.order) {
|
|
const idNames = this.idNames(model);
|
|
if (idNames && idNames.length) {
|
|
filter.order = idNames;
|
|
}
|
|
}
|
|
|
|
let selectStmt = new ParameterizedSQL('SELECT ' +
|
|
this.buildColumnNames(model, filter) +
|
|
' FROM ' + this.tableEscaped(model));
|
|
|
|
if (filter) {
|
|
if (filter.where) {
|
|
const whereStmt = this.buildWhere(model, filter.where);
|
|
selectStmt.merge(whereStmt);
|
|
}
|
|
|
|
if (filter.order) {
|
|
selectStmt.merge(this.buildOrderBy(model, filter.order));
|
|
}
|
|
|
|
if (filter.limit || filter.skip || filter.offset) {
|
|
selectStmt = this.applyPagination(
|
|
model, selectStmt, filter,
|
|
);
|
|
}
|
|
}
|
|
return this.parameterize(selectStmt);
|
|
};
|
|
|
|
/**
|
|
* Transform the row data into a model data object
|
|
* @param {string} model Model name
|
|
* @param {object} rowData An object representing the row data from DB
|
|
* @returns {object} Model data object
|
|
*/
|
|
SQLConnector.prototype.fromRow = SQLConnector.prototype.fromDatabase =
|
|
function(model, rowData) {
|
|
if (rowData == null) {
|
|
return rowData;
|
|
}
|
|
const props = this.getModelDefinition(model).properties;
|
|
const data = {};
|
|
for (const p in props) {
|
|
const columnName = this.column(model, p);
|
|
// Load properties from the row
|
|
const columnValue = this.fromColumnValue(props[p], rowData[columnName]);
|
|
if (columnValue !== undefined) {
|
|
data[p] = columnValue;
|
|
}
|
|
}
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* Find matching model instances by the filter
|
|
*
|
|
* Please also note the name `all` is confusing. `Model.find` is to find all
|
|
* matching instances while `Model.findById` is to find an instance by id. On
|
|
* the other hand, `Connector.prototype.all` implements `Model.find` while
|
|
* `Connector.prototype.find` implements `Model.findById` due to the `bad`
|
|
* naming convention we inherited from juggling-db.
|
|
*
|
|
* @param {String} model The model name
|
|
* @param {Object} filter The filter
|
|
* @param {Function} [cb] The cb function
|
|
*/
|
|
SQLConnector.prototype.all = function find(model, filter, options, cb) {
|
|
const self = this;
|
|
// Order by id if no order is specified
|
|
filter = filter || {};
|
|
const stmt = this.buildSelect(model, filter, options);
|
|
this.execute(stmt.sql, stmt.params, options, function(err, data) {
|
|
if (err) {
|
|
return cb(err, []);
|
|
}
|
|
|
|
const objs = data.map(function(obj) {
|
|
return self.fromRow(model, obj);
|
|
});
|
|
if (filter && filter.include) {
|
|
self.getModelDefinition(model).model.include(
|
|
objs, filter.include, options, cb,
|
|
);
|
|
} else {
|
|
cb(null, objs);
|
|
}
|
|
});
|
|
};
|
|
|
|
// Alias to `all`. Juggler checks `all` only.
|
|
Connector.defineAliases(SQLConnector.prototype, 'all', ['findAll']);
|
|
|
|
/**
|
|
* ATM, this method is not used by loopback-datasource-juggler dao, which
|
|
* maps `findById` to `find` with a `where` filter that includes the `id`
|
|
* instead.
|
|
*
|
|
* Please also note the name `find` is confusing. `Model.find` is to find all
|
|
* matching instances while `Model.findById` is to find an instance by id. On
|
|
* the other hand, `Connector.prototype.find` is for `findById` and
|
|
* `Connector.prototype.all` is for `find` due the `bad` convention used by
|
|
* juggling-db.
|
|
*
|
|
* Find by id
|
|
* @param {String} model The Model name
|
|
* @param {*} id The id value
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
* @private
|
|
*/
|
|
SQLConnector.prototype.find = function(model, id, options, cb) {
|
|
if (id == null) {
|
|
process.nextTick(function() {
|
|
const err = new Error(g.f('id value is required'));
|
|
if (cb) {
|
|
cb(err);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
const where = {};
|
|
const idName = this.idName(model);
|
|
where[idName] = id;
|
|
|
|
const filter = {limit: 1, offset: 0, order: idName, where: where};
|
|
return this.all(model, filter, options, function(err, results) {
|
|
cb(err, (results && results[0]) || null);
|
|
});
|
|
};
|
|
// Alias to `find`. Juggler checks `findById` only.
|
|
Connector.defineAliases(SQLConnector.prototype, 'find', ['findById']);
|
|
|
|
/**
|
|
* Count all model instances by the where filter
|
|
*
|
|
* @param {String} model The model name
|
|
* @param {Object} where The where object
|
|
* @param {Object} options The options object
|
|
* @param {Function} cb The callback function
|
|
*/
|
|
SQLConnector.prototype.count = function(model, where, options, cb) {
|
|
if (typeof where === 'function') {
|
|
// Backward compatibility for 1.x style signature:
|
|
// count(model, cb, where)
|
|
const tmp = options;
|
|
cb = where;
|
|
where = tmp;
|
|
}
|
|
|
|
let stmt = new ParameterizedSQL('SELECT count(*) as "cnt" FROM ' +
|
|
this.tableEscaped(model));
|
|
stmt = stmt.merge(this.buildWhere(model, where));
|
|
stmt = this.parameterize(stmt);
|
|
this.execute(stmt.sql, stmt.params, options,
|
|
function(err, res) {
|
|
if (err) {
|
|
return cb(err);
|
|
}
|
|
const c = (res && res[0] && res[0].cnt) || 0;
|
|
// Some drivers return count as a string to contain bigint
|
|
// See https://github.com/brianc/node-postgres/pull/427
|
|
cb(err, Number(c));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Drop the table for the given model from the database
|
|
* @param {String} model The model name
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.dropTable = function(model, cb) {
|
|
this.execute('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb);
|
|
};
|
|
|
|
/**
|
|
* Create the table for the given model
|
|
* @param {String} model The model name
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.createTable = function(model, cb) {
|
|
const sql = 'CREATE TABLE ' + this.tableEscaped(model) +
|
|
' (\n ' + this.buildColumnDefinitions(model) + '\n)';
|
|
this.execute(sql, cb);
|
|
};
|
|
|
|
/**
|
|
* Recreate the tables for the given models
|
|
* @param {String|String[]} [models] A model name or an array of model names,
|
|
* if not present, apply to all models defined in the connector
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.automigrate = 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(self._models);
|
|
if (models.length === 0) {
|
|
return process.nextTick(cb);
|
|
}
|
|
|
|
const invalidModels = models.filter(function(m) {
|
|
return !(m in self._models);
|
|
});
|
|
if (invalidModels.length) {
|
|
return process.nextTick(function() {
|
|
cb(new Error(g.f('Cannot migrate models not attached to this datasource: %s',
|
|
invalidModels.join(' '))));
|
|
});
|
|
}
|
|
|
|
async.eachSeries(models, function(model, done) {
|
|
self.dropTable(model, function(err) {
|
|
if (err) {
|
|
// TODO(bajtos) should we abort here and call cb(err)?
|
|
// The original code in juggler ignored the error completely
|
|
console.error(err);
|
|
}
|
|
self.createTable(model, function(err, result) {
|
|
if (err) {
|
|
console.error(err);
|
|
}
|
|
done(err, result);
|
|
});
|
|
});
|
|
}, cb);
|
|
};
|
|
|
|
/**
|
|
* Serialize an object into JSON string or other primitive types so that it
|
|
* can be saved into a RDB column
|
|
* @param {Object} obj The object value
|
|
* @returns {*}
|
|
*/
|
|
SQLConnector.prototype.serializeObject = function(obj) {
|
|
let val;
|
|
if (obj && typeof obj.toJSON === 'function') {
|
|
obj = obj.toJSON();
|
|
}
|
|
if (typeof obj !== 'string') {
|
|
val = JSON.stringify(obj);
|
|
} else {
|
|
val = obj;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
/*!
|
|
* @param obj
|
|
*/
|
|
SQLConnector.prototype.escapeObject = function(obj) {
|
|
const val = this.serializeObject(obj);
|
|
return this.escapeValue(val);
|
|
};
|
|
|
|
/**
|
|
* The following _abstract_ methods have to be implemented by connectors that
|
|
* extend from SQLConnector to reuse the base implementations of CRUD methods
|
|
* from SQLConnector
|
|
*/
|
|
|
|
/**
|
|
* Converts a model property value into the form required by the
|
|
* database column. The result should be one of following forms:
|
|
*
|
|
* - {sql: "point(?,?)", params:[10,20]}
|
|
* - {sql: "'John'", params: []}
|
|
* - "John"
|
|
*
|
|
* @param {Object} propertyDef Model property definition
|
|
* @param {*} value Model property value
|
|
* @returns {ParameterizedSQL|*} Database column value.
|
|
*
|
|
*/
|
|
SQLConnector.prototype.toColumnValue = function(propertyDef, value) {
|
|
throw new Error(g.f('{{toColumnValue()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Convert the data from database column to model property
|
|
* @param {object} propertyDef Model property definition
|
|
* @param {*) value Column value
|
|
* @returns {*} Model property value
|
|
*/
|
|
SQLConnector.prototype.fromColumnValue = function(propertyDef, value) {
|
|
throw new Error(g.f('{{fromColumnValue()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Escape the name for the underlying database
|
|
* @param {String} name The name
|
|
* @returns {String} An escaped name for SQL
|
|
*/
|
|
SQLConnector.prototype.escapeName = function(name) {
|
|
throw new Error(g.f('{{escapeName()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Escape the name for the underlying database
|
|
* @param {String} value The value to be escaped
|
|
* @returns {*} An escaped value for SQL
|
|
*/
|
|
SQLConnector.prototype.escapeValue = function(value) {
|
|
throw new Error(g.f('{{escapeValue()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Get the place holder in SQL for identifiers, such as ??
|
|
* @param {String} key Optional key, such as 1 or id
|
|
* @returns {String} The place holder
|
|
*/
|
|
SQLConnector.prototype.getPlaceholderForIdentifier = function(key) {
|
|
throw new Error(g.f('{{getPlaceholderForIdentifier()}} must be implemented by ' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Get the place holder in SQL for values, such as :1 or ?
|
|
* @param {String} key Optional key, such as 1 or id
|
|
* @returns {String} The place holder
|
|
*/
|
|
SQLConnector.prototype.getPlaceholderForValue = function(key) {
|
|
throw new Error(g.f('{{getPlaceholderForValue()}} must be implemented by ' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Build a new SQL statement with pagination support by wrapping the given sql
|
|
* @param {String} model The model name
|
|
* @param {ParameterizedSQL} stmt The sql statement
|
|
* @param {Object} filter The filter object from the query
|
|
*/
|
|
SQLConnector.prototype.applyPagination = function(model, stmt, filter) {
|
|
throw new Error(g.f('{{applyPagination()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Parse the result for SQL UPDATE/DELETE/INSERT for the number of rows
|
|
* affected
|
|
* @param {String} model Model name
|
|
* @param {Object} info Status object
|
|
* @returns {Number} Number of rows affected
|
|
*/
|
|
SQLConnector.prototype.getCountForAffectedRows = function(model, info) {
|
|
throw new Error(g.f('{{getCountForAffectedRows()}} must be implemented by ' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Parse the result for SQL INSERT for newly inserted id
|
|
* @param {String} model Model name
|
|
* @param {Object} info The status object from driver
|
|
* @returns {*} The inserted id value
|
|
*/
|
|
SQLConnector.prototype.getInsertedId = function(model, info) {
|
|
throw new Error(g.f('{{getInsertedId()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Parse the result for SQL INSERT (bulk) for newly inserted ids
|
|
* @param {String} model Model name
|
|
* @param {Object} info The status object from driver
|
|
* @returns {*} The inserted id values as an array
|
|
*/
|
|
SQLConnector.prototype.getInsertedIds = function(model, info) {
|
|
throw new Error(g.f('{{getInsertedIds()}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Parse the result for SQL INSERT (bulk) for newly inserted data
|
|
* and return the data with the inserted id
|
|
* @param {String} model Model name
|
|
* @param {Object} info Result data from connector
|
|
* @param {Object[]} data Data to be inserted
|
|
* @returns {*} The inserted data array
|
|
*/
|
|
SQLConnector.prototype.getInsertedDataArray = function(model, info, data) {
|
|
const idProp = this.getDataSource(model).idProperty(model);
|
|
// If the id is generated by the database, we need to get the id
|
|
if (idProp.generated) {
|
|
const insertedIds = this.getInsertedIds(model, info);
|
|
// We need to parse the ids created and map them back into the model
|
|
const returnData = [];
|
|
const idPropName = this.propertyName(model, this.idColumn(model));
|
|
for (let i = 0; i < insertedIds.length; i++) {
|
|
const saved = Object.assign({}, data[i]);
|
|
saved[idPropName] = insertedIds[i];
|
|
returnData.push(saved);
|
|
}
|
|
return returnData;
|
|
} else {
|
|
// If the id is not generated by the database, we can just return the data
|
|
return data.map(d => Object.assign({}, d));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Execute a SQL statement with given parameters
|
|
* @param {String} sql The SQL statement
|
|
* @param {Array} [params] An array of parameter values
|
|
* @param {Object} [options] Options object
|
|
* @param {Function} [callback] The callback function
|
|
*/
|
|
SQLConnector.prototype.executeSQL = function(sql, params, options, callback) {
|
|
throw new Error(g.f('{{executeSQL()}} must be implemented by the connector'));
|
|
};
|
|
|
|
// Refactored Discovery methods
|
|
|
|
/**
|
|
* Build sql for listing schemas
|
|
* @param {Object} options Options for discoverDatabaseSchemas
|
|
*/
|
|
SQLConnector.prototype.buildQuerySchemas = function(options) {
|
|
const sql = 'SELECT catalog_name as "catalog",' +
|
|
' schema_name as "schema"' +
|
|
' FROM information_schema.schemata';
|
|
return this.paginateSQL(sql, 'schema_name', options);
|
|
};
|
|
|
|
/**
|
|
* Paginate the results returned from database
|
|
* @param {String} sql The sql to execute
|
|
* @param {Object} orderBy The property name by which results are ordered
|
|
* @param {Object} options Options for discoverDatabaseSchemas
|
|
*/
|
|
SQLConnector.prototype.paginateSQL = function(sql, orderBy, options) {
|
|
throw new Error(g.f('{{paginateSQL}} must be implemented by the connector'));
|
|
};
|
|
/**
|
|
* Discover database schemas
|
|
*
|
|
* @param {Object} options Options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverDatabaseSchemas = function(options, cb) {
|
|
if (!cb && typeof options === 'function') {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
options = options || {};
|
|
const self = this;
|
|
this.execute(self.buildQuerySchemas(options), cb);
|
|
};
|
|
|
|
/*!
|
|
* Build sql for listing tables
|
|
* @param options {all: for all owners, owner: for a given owner}
|
|
* @returns {string} The sql statement
|
|
*/
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryTables = function(options) {
|
|
throw new Error(g.f('{{buildQueryTables}} must be implemented by the connector'));
|
|
};
|
|
|
|
/*!
|
|
* Build sql for listing views
|
|
* @param options {all: for all owners, owner: for a given owner}
|
|
* @returns {string} The sql statement
|
|
*/
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryViews = function(options) {
|
|
throw new Error(g.f('{{buildQueryViews}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Discover model definitions
|
|
*
|
|
* @param {Object} options Options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverModelDefinitions = function(options, cb) {
|
|
if (!cb && typeof options === 'function') {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
options = options || {};
|
|
|
|
const self = this;
|
|
const calls = [function(callback) {
|
|
self.execute(self.buildQueryTables(options), callback);
|
|
}];
|
|
|
|
if (options.views) {
|
|
calls.push(function(callback) {
|
|
self.execute(self.buildQueryViews(options), callback);
|
|
});
|
|
}
|
|
async.parallel(calls, function(err, data) {
|
|
if (err) {
|
|
cb(err, data);
|
|
} else {
|
|
let merged = [];
|
|
merged = merged.concat(data.shift());
|
|
if (data.length) {
|
|
merged = merged.concat(data.shift());
|
|
}
|
|
cb(err, merged);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Build sql for listing columns
|
|
* @param {String} schema The schema name
|
|
* @param {String} table The table name
|
|
*/
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryColumns = function(schema, table, options) {
|
|
throw new Error(g.f('{{buildQueryColumns}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Map the property type from database to loopback
|
|
* @param {Object} columnDefinition The columnDefinition of the table/schema
|
|
* @param {Object} options The options for the connector
|
|
*/
|
|
SQLConnector.prototype.buildPropertyType = function(columnDefinition, options) {
|
|
throw new Error(g.f('{{buildPropertyType}} must be implemented by the connector'));
|
|
};
|
|
|
|
/*!
|
|
* Normalize the arguments
|
|
* @param table string, required
|
|
* @param options object, optional
|
|
* @param cb function, optional
|
|
*/
|
|
SQLConnector.prototype.getArgs = function(table, options, cb) {
|
|
throw new Error(g.f('{{getArgs}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Discover model properties from a table
|
|
* @param {String} table The table name
|
|
* @param {Object} options The options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverModelProperties = function(table, options, cb) {
|
|
const self = this;
|
|
const args = self.getArgs(table, options, cb);
|
|
let schema = args.schema || args.owner;
|
|
|
|
table = args.table;
|
|
options = args.options;
|
|
|
|
if (!schema) {
|
|
schema = self.getDefaultSchema();
|
|
}
|
|
|
|
self.setDefaultOptions(options);
|
|
cb = args.cb;
|
|
|
|
const sql = self.buildQueryColumns(schema, table, options);
|
|
const callback = function(err, results) {
|
|
if (err) {
|
|
cb(err, results);
|
|
} else {
|
|
results.map(function(r) {
|
|
r.type = self.buildPropertyType(r, options);
|
|
self.setNullableProperty(r);
|
|
});
|
|
cb(err, results);
|
|
}
|
|
};
|
|
this.execute(sql, callback);
|
|
};
|
|
|
|
/*!
|
|
* Build the sql statement for querying primary keys of a given table
|
|
* @param schema
|
|
* @param table
|
|
* @returns {string}
|
|
*/
|
|
// http://docs.oracle.com/javase/6/docs/api/java/sql/DatabaseMetaData.html
|
|
// #getPrimaryKeys(java.lang.String, java.lang.String, java.lang.String)
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryPrimaryKeys = function(schema, table) {
|
|
throw new Error(g.f('{{buildQueryPrimaryKeys}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Discover primary keys for a given table
|
|
* @param {String} table The table name
|
|
* @param {Object} options The options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverPrimaryKeys = function(table, options, cb) {
|
|
const self = this;
|
|
const args = self.getArgs(table, options, cb);
|
|
let schema = args.schema || args.owner;
|
|
|
|
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
|
|
schema = self.getDefaultSchema();
|
|
}
|
|
table = args.table;
|
|
options = args.options;
|
|
cb = args.cb;
|
|
|
|
const sql = self.buildQueryPrimaryKeys(schema, table);
|
|
this.execute(sql, cb);
|
|
};
|
|
|
|
/*!
|
|
* Build the sql statement for querying foreign keys of a given table
|
|
* @param schema
|
|
* @param table
|
|
* @returns {string}
|
|
*/
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryForeignKeys = function(schema, table) {
|
|
throw new Error(g.f('{{buildQueryForeignKeys}} must be implemented by the connector'));
|
|
};
|
|
|
|
/**
|
|
* Discover foreign keys for a given table
|
|
* @param {String} table The table name
|
|
* @param {Object} options The options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverForeignKeys = function(table, options, cb) {
|
|
const self = this;
|
|
const args = self.getArgs(table, options, cb);
|
|
let schema = args.schema || args.owner;
|
|
|
|
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
|
|
schema = self.getDefaultSchema();
|
|
}
|
|
table = args.table;
|
|
options = args.options;
|
|
cb = args.cb;
|
|
|
|
const sql = self.buildQueryForeignKeys(schema, table);
|
|
this.execute(sql, cb);
|
|
};
|
|
|
|
/**
|
|
* Retrieves a description of the foreign key columns that reference the
|
|
* given table's primary key columns (the foreign keys exported by a table).
|
|
* They are ordered by fkTableOwner, fkTableName, and keySeq.
|
|
* @param schema
|
|
* @param table
|
|
* @returns {string}
|
|
*/
|
|
// Due to the different implementation structure of information_schema across
|
|
// connectors, each connector will have to generate its own query
|
|
SQLConnector.prototype.buildQueryExportedForeignKeys = function(schema, table) {
|
|
throw new Error(g.f('{{buildQueryExportedForeignKeys}} must be implemented by' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Discover foreign keys that reference to the primary key of this table
|
|
* @param {String} table The table name
|
|
* @param {Object} options The options for discovery
|
|
* @param {Function} [cb] The callback function
|
|
*/
|
|
SQLConnector.prototype.discoverExportedForeignKeys = function(table, options, cb) {
|
|
const self = this;
|
|
const args = self.getArgs(table, options, cb);
|
|
let schema = args.schema || args.owner;
|
|
|
|
if (typeof(self.getDefaultSchema) === 'function' && !schema) {
|
|
schema = self.getDefaultSchema();
|
|
}
|
|
table = args.table;
|
|
options = args.options;
|
|
cb = args.cb;
|
|
|
|
const sql = self.buildQueryExportedForeignKeys(schema, table);
|
|
this.execute(sql, cb);
|
|
};
|
|
|
|
/**
|
|
* Discover default schema of a database
|
|
* @param {Object} options The options for discovery
|
|
*/
|
|
SQLConnector.prototype.getDefaultSchema = function(options) {
|
|
throw new Error(g.f('{{getDefaultSchema}} must be implemented by' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Set default options for the connector
|
|
* @param {Object} options The options for discovery
|
|
*/
|
|
SQLConnector.prototype.setDefaultOptions = function(options) {
|
|
throw new Error(g.f('{{setDefaultOptions}} must be implemented by' +
|
|
'the connector'));
|
|
};
|
|
|
|
/**
|
|
* Set the nullable value for the property
|
|
* @param {Object} property The property to set nullable
|
|
*/
|
|
SQLConnector.prototype.setNullableProperty = function(property) {
|
|
throw new Error(g.f('{{setNullableProperty}} must be implemented by' +
|
|
'the connector'));
|
|
};
|