sequelize-ibmi
Version:
Multi dialect ORM for Node.JS
288 lines (248 loc) • 8.92 kB
JavaScript
'use strict';
const _ = require('lodash');
const AbstractQuery = require('../abstract/query');
const parserStore = require('../parserStore')('ibmi');
const SqlString = require('../../sql-string');
const sequelizeErrors = require('../../errors');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('sql:ibmi');
class Query extends AbstractQuery {
getInsertIdField() {
return 'id';
}
static formatBindParameters(sql, values, dialect) {
let bindParams;
if (Array.isArray(values)) {
bindParams = values.slice();
} else if (typeof values === 'object') {
bindParams = { ...values };
}
const replacementFunc = (match, key, values_) => {
if (values_[key] !== undefined) {
delete bindParams[key];
if (values_[key] === null)
{
return 'CAST(NULL AS INTEGER)';
}
return SqlString.escape(values_[key]);
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
if (Array.isArray(bindParams)) {
bindParams = bindParams.filter(el => {
return el !== undefined;
});
}
return [sql, bindParams];
}
async run(sql, parameters) {
this.sql = sql.replace(/;$/, '');
return new Promise((resolve, reject) => {
const complete = this._logQuery(sql, debug, parameters);
this.connection.query(this.sql, parameters, (error, results) => {
if (error) {
const formattedError = this.formatError(error);
reject(formattedError);
return;
}
complete();
// parse the results to the format sequelize expects
for (const result of results) {
for (const column of results.columns) {
const typeId = column.dataType;
const parse = parserStore.get(typeId);
const value = result[column.name];
if (value !== null && parse) {
result[column.name] = parse(value);
}
}
}
resolve(results);
});
})
.then(results => this.formatResults(results));
}
/**
* High level function that handles the results of a query execution.
*
*
* Example:
* query.formatResults([
* {
* id: 1, // this is from the main table
* attr2: 'snafu', // this is from the main table
* Tasks.id: 1, // this is from the associated table
* Tasks.title: 'task' // this is from the associated table
* }
* ])
*
* @param {Array} data - The result of the query execution.
* @private
*/
formatResults(data) {
let result = this.instance;
if (this.isInsertQuery() || this.isUpdateQuery() || this.isUpsertQuery()) {
if (this.instance && this.instance.dataValues) {
for (const key in data[0]) {
if (Object.prototype.hasOwnProperty.call(data[0], key)) {
const record = data[0][key];
const attr = _.find(this.model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key);
this.instance.dataValues[attr && attr.fieldName || key] = record;
}
}
}
if (this.isUpsertQuery()) {
return [
this.instance,
null
];
}
return [
this.instance || data && (this.options.plain && data[0] || data) || undefined,
data.count
];
}
if (this.isSelectQuery()) {
return this.handleSelectQuery(data);
}
if (this.isShowTablesQuery()) {
return this.handleShowTablesQuery(data);
}
if (this.isShowIndexesQuery()) {
return this.handleShowIndexesQuery(data);
}
if (this.isDescribeQuery()) {
result = {};
for (const _result of data) {
const enumRegex = /^enum/i;
result[_result.COLUMN_NAME] = {
type: enumRegex.test(_result.Type) ? _result.Type.replace(enumRegex, 'ENUM') : _result.DATA_TYPE.toUpperCase(),
allowNull: _result.IS_NULLABLE === 'Y',
defaultValue: _result.COLUMN_DEFAULT,
primaryKey: _result.CONSTRAINT_TYPE === 'PRIMARY KEY',
autoIncrement: _result.IS_GENERATED !== 'IDENTITY_GENERATION'
};
}
return result;
}
if (this.isCallQuery()) {
return data[0];
}
if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
return data.count;
}
if (this.isVersionQuery()) {
return data[0].VERSION;
}
if (this.isForeignKeysQuery()) {
return data;
}
if (this.isInsertQuery(data)) {
// insert queries can't call count, because they are actually select queries wrapped around insert queries to get the inserted id. Need to count the number of results instead.
return [result, data.length];
}
if (this.isUpdateQuery()) {
return [result, data.count];
}
if (this.isShowConstraintsQuery()) {
return data;
}
if (this.isRawQuery()) {
// MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
return [data, data];
}
if (this.isShowIndexesQuery()) {
return data;
}
return result;
}
handleInsertQuery(results, metaData) {
if (this.instance) {
// add the inserted row id to the instance
const autoIncrementAttribute = this.model.autoIncrementAttribute.field;
let id = null;
id = id || results && results[autoIncrementAttribute];
id = id || metaData && metaData[autoIncrementAttribute];
this.instance[this.model.autoIncrementAttribute] = id;
}
}
handleShowIndexesQuery(data) {
const indexes = {};
data.forEach(item => {
if (Object.prototype.hasOwnProperty.call(indexes, item['NAME'])) {
indexes[item['NAME']].fields.push({ attribute: item['COLUMN_NAME'], length: undefined, order: undefined });
} else {
indexes[item['NAME']] = {
primary: item['CONSTRAINT_TYPE'] === 'PRIMARY KEY',
fields: [{ attribute: item['COLUMN_NAME'], length: undefined, order: undefined }],
name: item['NAME'],
tableName: item['TABLE_NAME'],
unique: item['CONSTRAINT_TYPE'] === 'PRIMARY KEY' || item['CONSTRAINT_TYPE'] === 'UNIQUE',
type: item['CONSTRAINT_TYPE']
};
}
});
return Object.values(indexes);
}
formatError(err) {
// Db2 for i uses the `odbc` connector. The `odbc` connector returns a list
// of odbc errors, each of which has a code and a state. To determine the
// type of SequelizeError, check the code and create the associated error.
// Error codes can be found at:
// https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzala/rzalaccl.htm
// some errors occur outside of ODBC (e.g. connection errors)
if (err.toString().includes('Error connecting to the database')) {
return new sequelizeErrors.ConnectionRefusedError(err);
}
if (Object.prototype.hasOwnProperty.call(err, 'odbcErrors') && err.odbcErrors.length > 0)
{
const odbcError = err.odbcErrors[0];
const foreignKeyConstraintCodes = [
-530, // The insert or update value of a foreign key is invalid.
-531, // The update or delete of a parent key is prevented by a NO ACTION update or delete rule.
-532 // The update or delete of a parent key is prevented by a NO ACTION update or delete rule.
];
const uniqueConstraintCodes = [
-803 // A violation of the constraint imposed by a unique index or a unique constraint occurred.
];
if (foreignKeyConstraintCodes.includes(odbcError.code)) {
return new sequelizeErrors.ForeignKeyConstraintError({
parent: err,
sql: {},
fields: {}
});
}
if (uniqueConstraintCodes.includes(odbcError.code)) {
return new sequelizeErrors.UniqueConstraintError({
errors: err.odbcErrors,
parent: err,
sql: {},
fields: {}
});
}
if (odbcError.code === -204) {
let constraintName = undefined;
let type = undefined;
const constraintNameRegex = /"([a-zA-Z0-9]+?)" in [^]+? type (\*\w+?) not found./;
const constraintNameRegexMatches = odbcError.message.match(constraintNameRegex);
if (constraintNameRegexMatches.length === 3)
{
constraintName = constraintNameRegexMatches[1];
type = constraintNameRegexMatches[2];
}
if (type === '*N') {
return new sequelizeErrors.UnknownConstraintError({
parent: err,
constraint: constraintName
});
}
return odbcError;
}
}
return err;
}
}
module.exports = Query;
module.exports.Query = Query;
module.exports.default = Query;