UNPKG

@breautek/storm

Version:

Object-Oriented REST API framework

305 lines (302 loc) 13 kB
"use strict"; /* Copyright 2017-2021 Norman Breau Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.MySQLConnection = void 0; const tslib_1 = require("tslib"); const DatabaseConnection_1 = require("./DatabaseConnection"); const DatabaseQueryError_1 = require("./DatabaseQueryError"); const instance_1 = require("./instance"); const StartTransactionQuery_1 = require("./private/StartTransactionQuery"); const CommitQuery_1 = require("./private/CommitQuery"); const RollbackQuery_1 = require("./private/RollbackQuery"); const SQLFormatter = tslib_1.__importStar(require("sql-formatter")); const interfaces_1 = require("@arashi/interfaces"); const DeadLockError_1 = require("./DeadLockError"); const SetIsolationLevelQuery_1 = require("./private/SetIsolationLevelQuery"); const LockWaitTimeoutError_1 = require("./LockWaitTimeoutError"); const GetSlavePositionQuery_1 = require("./private/GetSlavePositionQuery"); const GetMasterPositionQuery_1 = require("./private/GetMasterPositionQuery"); const TransactionAccessLevel_1 = require("./TransactionAccessLevel"); const GetProcessList_1 = require("./private/GetProcessList"); const GetMySQLVersion_1 = require("./GetMySQLVersion"); const GetPrimaryPositionQuery_1 = require("./private/GetPrimaryPositionQuery"); const queryFormatter_1 = require("./mysql/queryFormatter"); const DEFAULT_HIGH_WATERMARK = 512; // in number of result objects const TAG = 'MySQLConnection'; const SQL_FORMATTING_OPTIONS = { tabWidth: 4, keywordCase: 'upper', identifierCase: 'preserve', useTabs: false, indentStyle: 'standard', logicalOperatorNewline: 'after', linesBetweenQueries: 1, denseOperators: false, newlineBeforeSemicolon: false, expressionWidth: 4, dataTypeCase: 'upper', functionCase: 'upper' }; let commitQuery = new CommitQuery_1.CommitQuery(); let rollbackQuery = new RollbackQuery_1.RollbackQuery(); class MySQLConnection extends DatabaseConnection_1.DatabaseConnection { constructor(connection, instantiationStack, isReadOnly = true, activeConnectionsGauge = null) { connection.config.namedPlaceholders = true; if (!(0, instance_1.getInstance)().getConfig().enableMySQL2BreakingChanges) { connection.config.typeCast = function (field, next) { // TODO: Expose a setting to opt out of these backwards-compatibility type casting // So we can have a chance of a rolling refactor. if ([ 'DECIMAL', 'NEWDECIMAL', 'DOUBLE' ].indexOf(field.type) > -1) { let parsed = parseFloat(field.string('utf-8')); if (isNaN(parsed)) { return null; } return parsed; } else if (([ 'JSON' ]).indexOf(field.type) > -1) { return field.string('utf-8'); } else { return next(); } }; } super(connection, isReadOnly, instantiationStack); this.$hasReplicationEnabled = false; this.$opened = true; this.$transaction = false; this.$isMasterConnection = null; this.$activeConnectionsGauge = activeConnectionsGauge; } /** * @internal - Do not use in application code */ async __internal_init() { let result = await new GetSlavePositionQuery_1.GetSlavePositionQuery().execute(this); if (result !== null) { this.$hasReplicationEnabled = true; this.$isMasterConnection = false; } else { this.$isMasterConnection = true; let processList = await new GetProcessList_1.GetProcessList().execute(this); for (let i = 0; i < processList.length; i++) { let p = processList[i]; if (p.Command === 'Binlog Dump') { this.$hasReplicationEnabled = true; break; } } } } async getVersion() { if (!this.$version) { this.$version = await new GetMySQLVersion_1.GetMySQLVersion().execute(this); } return { ...this.$version }; } formatQuery(query) { return this.getAPI().format(query.getQuery(this), query.getParametersForQuery()); } /** * Returns true if this server is the source. * Will also return true if there is no replication detected. */ isMaster() { return this.$isMasterConnection; } /** * Returns true if this server is part of a replication cluster. */ hasReplicationEnabled() { return this.$hasReplicationEnabled; } /** * Returns true if this server is a replication server. */ isReplication() { return !this.isMaster(); } isTransaction() { return this.$transaction; } isOpen() { return this.$opened; } async getCurrentDatabasePosition() { let version = await this.getVersion(); let statusQuery = null; if ((version.major === 8 && version.minor >= 4) || version.major >= 9) { // >= 8.4 we need to use a different setof master/slave queries statusQuery = this.isReplication() ? new GetSlavePositionQuery_1.GetSlavePositionQuery() : new GetPrimaryPositionQuery_1.GetPrimaryPositionQuery(); } else { statusQuery = this.isReplication() ? new GetSlavePositionQuery_1.GetSlavePositionQuery() : new GetMasterPositionQuery_1.GetMasterPositionQuery(); } return await statusQuery.execute(this); } _query(query, params) { let logger = (0, instance_1.getInstance)().getLogger(); // MySQL2 doesn't seem to be reliable for their named parameter support, // so we will fall back to our original implementation, but prep the query // string manually via queryFormatter (which makes use of MySQL.escape). // This means we won't use the params argument on the main query call. let preparedQuery = (0, queryFormatter_1.queryFormatter)(query, params); return new Promise((resolve, reject) => { let queryObject = this.getAPI().query({ sql: preparedQuery, timeout: this.getTimeout() }, null, (error, results) => { if (error) { let sql = queryObject.sql; // Formatting queries can be an expensive task, so only do it if the log level is actually silly. if (logger.getLogLevel() === interfaces_1.LogLevel.SILLY) { try { // SQLFormatter doesn't understand all MySQL syntaxes, so this is to prevent // potentially valid queries from becoming errors simply because we couldn't // log them. sql = SQLFormatter.formatDialect(preparedQuery, { ...SQL_FORMATTING_OPTIONS, dialect: SQLFormatter.mysql }); } catch (ex) { logger.warn(TAG, 'Unable to format query...'); logger.warn(TAG, ex); } } let e = null; if (error.code === 'ER_LOCK_DEADLOCK') { e = new DeadLockError_1.DeadLockError(sql, error); // When deadlocks occur, the transaction is automatically rollback, so we can clear transanction status. this.$transaction = false; } else if (error.code === 'ER_LOCK_WAIT_TIMEOUT') { // lock wait may not rollback the transaction (depends on how the server is configured) // however the transaction is retryable. The user shall configure // if they want to retry on timeouts. e = new LockWaitTimeoutError_1.LockWaitTimeoutError(sql, error); } else { e = new DatabaseQueryError_1.DatabaseQueryError(sql, error); } return reject(e); } return resolve(results); }); // Formatting queries can be an expensive task, so only do it if the log level is actually silly. let sql = queryObject.sql; if (logger.getLogLevel() === interfaces_1.LogLevel.SILLY) { sql = SQLFormatter.format(queryObject.sql, SQL_FORMATTING_OPTIONS); } (0, instance_1.getInstance)().getLogger().trace(TAG, sql); }); } _stream(query, params, streamOptions) { if (!streamOptions) { streamOptions = {}; } if (!streamOptions.highWatermark) { streamOptions.highWatermark = DEFAULT_HIGH_WATERMARK; } let queryObject = this.getAPI().query({ sql: query, timeout: this.getTimeout() }, params); let logger = (0, instance_1.getInstance)().getLogger(); if (logger.getLogLevel() === interfaces_1.LogLevel.SILLY) { (0, instance_1.getInstance)().getLogger().trace(TAG, SQLFormatter.format(queryObject.sql, SQL_FORMATTING_OPTIONS)); } return queryObject.stream(streamOptions); } async startTransaction(isolationLevel, accessLevel = TransactionAccessLevel_1.TransactionAccessLevel.RW) { if (this.isReadOnly() && accessLevel === TransactionAccessLevel_1.TransactionAccessLevel.RW) { throw new Error('A readonly connection cannot start a read/write transaction.'); } if (this.isTransaction()) { throw new Error('Connection is already in a transaction.'); } this.$transaction = true; try { if (isolationLevel) { await new SetIsolationLevelQuery_1.SetIsolationLevelQuery(isolationLevel).execute(this); } await new StartTransactionQuery_1.StartTransactionQuery({ accessLevel: accessLevel }).execute(this); } catch (ex) { this.$transaction = false; (0, instance_1.getInstance)().getLogger().error(TAG, ex); throw ex; } } endTransaction(requiresRollback = false) { return (requiresRollback) ? this.rollback() : this.commit(); } rollback() { if (!this.isTransaction()) { return Promise.reject(new Error('Cannot rollback when there is no active transaction.')); } return new Promise((resolve, reject) => { this.query(rollbackQuery).then(() => { this.$transaction = false; resolve(); }).catch((ex) => { (0, instance_1.getInstance)().getLogger().error(TAG, ex); reject(ex); }); }); } commit() { if (!this.isTransaction()) { return Promise.reject(new Error('Cannot commit when there is no active transaction.')); } return new Promise((resolve, reject) => { this.query(commitQuery).then(() => { this.$transaction = false; resolve(); }).catch((ex) => { (0, instance_1.getInstance)().getLogger().error(TAG, ex); reject(ex); }); }); } async _close(forceClose) { if (!forceClose && this.isTransaction()) { return Promise.reject(new Error('Cannot close a connection while there is an active transaction. Use commit or rollback first.')); } this.$opened = false; this.$activeConnectionsGauge?.dec(); if (forceClose) { if (this.isTransaction()) { try { await this.rollback(); } catch (ex) { (0, instance_1.getInstance)().getLogger().error(TAG, ex); } } } this.getAPI().release(); } } exports.MySQLConnection = MySQLConnection; //# sourceMappingURL=MySQLConnection.js.map