UNPKG

mvom

Version:

Multivalue Object Mapper

427 lines (400 loc) 14.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.ConnectionStatus = void 0; var _crypto = _interopRequireDefault(require("crypto")); var _asyncMutex = require("async-mutex"); var _axios = _interopRequireDefault(require("axios")); var _dateFns = require("date-fns"); var _compileModel = _interopRequireDefault(require("./compileModel")); var _constants = require("./constants"); var _DeploymentManager = _interopRequireDefault(require("./DeploymentManager")); var _errors = require("./errors"); var _LogHandler = _interopRequireDefault(require("./LogHandler")); // #region Types let ConnectionStatus = exports.ConnectionStatus = /*#__PURE__*/function (ConnectionStatus) { ConnectionStatus["disconnected"] = "disconnected"; ConnectionStatus["connected"] = "connected"; ConnectionStatus["connecting"] = "connecting"; return ConnectionStatus; }({}); /** Multivalue database server information */ // #endregion /** A connection object */ class Connection { /** Connection status */ status = ConnectionStatus.disconnected; /** Log handler instance used for diagnostic logging */ /** Maximum age of the cache before it must be refreshed */ /** Multivalue database server information */ /** Axios instance */ /** Deployment Manager instance */ /** Mutex on acquiring server information */ /** Maximum allowed return payload size in bytes */ constructor(/** URL of the MVIS which facilitates access to the mv database */ mvisUrl, /** Database account that connection will be used against */ account, /** Lifetime of cache of db server data (s) */ cacheMaxAge, /** Request timeout (ms) */ timeout, logHandler, deploymentManager, options) { const { httpAgent, httpsAgent, maxReturnPayloadSize } = options; this.cacheMaxAge = cacheMaxAge; this.logHandler = logHandler; this.deploymentManager = deploymentManager; this.maxReturnPayloadSize = maxReturnPayloadSize; const url = new URL(mvisUrl); url.pathname = url.pathname.replace(/\/?$/, `/${account}/subroutine/`); const baseURL = url.toString(); this.axiosInstance = _axios.default.create({ baseURL, timeout, transitional: { clarifyTimeoutError: true }, ...(httpAgent && { httpAgent }), ...(httpsAgent && { httpsAgent }) }); this.serverInfoMutex = new _asyncMutex.Mutex(); this.logHandler.debug('creating new connection instance'); } /** Returns the subroutine name that is used on the multivalue server */ get subroutineName() { return this.deploymentManager.subroutineName; } /** Create a connection */ static createConnection(/** URL of the MVIS which facilitates access to the mv database */ mvisUrl, /** URL of the MVIS Admin */ mvisAdminUrl, /** MVIS Admin Username */ mvisAdminUsername, /** MVIS Admin Password */ mvisAdminPassword, /** Database account that connection will be used against */ account, options = {}) { const { logger, cacheMaxAge = 3600, timeout = 0, httpAgent, httpsAgent, maxReturnPayloadSize = 100_000_000 } = options; if (!Number.isInteger(cacheMaxAge)) { throw new _errors.InvalidParameterError({ parameterName: 'cacheMaxAge' }); } if (!Number.isInteger(timeout)) { throw new _errors.InvalidParameterError({ parameterName: 'timeout' }); } if (maxReturnPayloadSize < 0) { throw new _errors.InvalidParameterError({ parameterName: 'maxReturnPayloadSize' }); } const logHandler = new _LogHandler.default(account, logger); const deploymentManager = _DeploymentManager.default.createDeploymentManager(mvisAdminUrl, account, mvisAdminUsername, mvisAdminPassword, logHandler, { timeout, httpAgent, httpsAgent }); return new Connection(mvisUrl, account, cacheMaxAge, timeout, logHandler, deploymentManager, { httpAgent, httpsAgent, maxReturnPayloadSize }); } /** Open a database connection */ async open(options = {}) { const { requestId, validateDeployment = true } = options; if (this.status !== ConnectionStatus.disconnected) { this.logHandler.error('Connection is not closed'); throw new _errors.ConnectionError({ message: 'Connection is not closed' }); } this.logHandler.info('opening connection'); this.status = ConnectionStatus.connecting; if (validateDeployment) { this.logHandler.info('Validating deployment'); await this.validateDeployment(); } else { this.logHandler.info('Skipping deployment validation'); } this.status = ConnectionStatus.connected; await this.getDbServerInfo({ requestId }); // establish baseline for database server information this.logHandler.info('connection opened'); } /** Deploy source code to MVIS & db server */ async deploy(sourceDir, options) { return this.deploymentManager.deploy(sourceDir, options); } /** Execute a database subroutine */ async executeDbSubroutine(subroutineName, options, setupOptions = {}, teardownOptions = {}) { if (this.status !== ConnectionStatus.connected) { this.logHandler.error('Cannot execute database features until database connection has been established'); throw new Error('Cannot execute database features until database connection has been established'); } const { maxReturnPayloadSize = this.maxReturnPayloadSize, requestId = _crypto.default.randomUUID() } = setupOptions; if (maxReturnPayloadSize < 0) { this.logHandler.error(`Maximum returned payload size of ${maxReturnPayloadSize} is invalid. Must be a positive integer`); throw new Error(`Maximum returned payload size of ${maxReturnPayloadSize} is invalid. Must be a positive integer`); } const updatedSetupOptions = { ...setupOptions, maxReturnPayloadSize, requestId }; this.logHandler.debug(`executing database subroutine "${subroutineName}"`); const data = { subroutineId: subroutineName, subroutineInput: options, setupOptions: updatedSetupOptions, teardownOptions }; let response; try { response = await this.axiosInstance.post(this.subroutineName, { input: data }, { headers: { 'X-MVIS-Trace-Id': requestId } }); } catch (err) { return _axios.default.isAxiosError(err) ? this.handleAxiosError(err) : this.handleUnexpectedError(err); } this.handleDbServerError(response, subroutineName, options, updatedSetupOptions); // return the relevant portion from the db server response return response.data.output; } /** Get the current ISOCalendarDate from the database */ async getDbDate({ requestId } = {}) { const { timeDrift } = await this.getDbServerInfo({ requestId }); return (0, _dateFns.format)((0, _dateFns.addMilliseconds)(Date.now(), timeDrift), _constants.ISOCalendarDateFormat); } /** Get the current ISOCalendarDateTime from the database */ async getDbDateTime({ requestId } = {}) { const { timeDrift } = await this.getDbServerInfo({ requestId }); return (0, _dateFns.format)((0, _dateFns.addMilliseconds)(Date.now(), timeDrift), _constants.ISOCalendarDateTimeFormat); } /** Get the current ISOTime from the database */ async getDbTime({ requestId } = {}) { const { timeDrift } = await this.getDbServerInfo({ requestId }); return (0, _dateFns.format)((0, _dateFns.addMilliseconds)(Date.now(), timeDrift), _constants.ISOTimeFormat); } /** Get the multivalue database server limits */ async getDbLimits({ requestId } = {}) { const { limits } = await this.getDbServerInfo({ requestId }); return limits; } /** Define a new model */ model(schema, file) { if (this.status !== ConnectionStatus.connected || this.dbServerInfo == null) { this.logHandler.error('Cannot create model until database connection has been established'); throw new Error('Cannot create model until database connection has been established'); } const { delimiters } = this.dbServerInfo; return (0, _compileModel.default)(this, schema, file, delimiters, this.logHandler); } /** Validate the multivalue subroutine deployment */ async validateDeployment() { const isValid = await this.deploymentManager.validateDeployment(); if (!isValid) { // prevent connection attempt if features are invalid this.logHandler.info('MVIS has not been configured for use with MVOM'); this.logHandler.error('Connection will not be opened'); this.status = ConnectionStatus.disconnected; throw new _errors.InvalidServerFeaturesError(); } } /** Get the db server information (date, time, etc.) */ async getDbServerInfo({ requestId } = {}) { // set a mutex on acquiring server information so multiple simultaneous requests are not modifying the cache return this.serverInfoMutex.runExclusive(async () => { if (this.dbServerInfo == null || Date.now() > this.dbServerInfo.cacheExpiry) { this.logHandler.debug('getting db server information'); const { date, time, delimiters, limits } = await this.executeDbSubroutine('getServerInfo', {}, { ...(requestId != null && { requestId }) }); const timeDrift = (0, _dateFns.differenceInMilliseconds)((0, _dateFns.addMilliseconds)((0, _dateFns.addDays)(_constants.mvEpoch, date), time), Date.now()); const cacheExpiry = Date.now() + this.cacheMaxAge * 1000; this.dbServerInfo = { cacheExpiry, timeDrift, delimiters, limits }; } return this.dbServerInfo; }); } /** * Handle error from the database server * @throws {@link ForeignKeyValidationError} A foreign key constraint was violated * @throws {@link RecordLockedError} A record was locked and could not be updated * @throws {@link RecordVersionError} A record changed between being read and written and could not be updated * @throws {@link DbServerError} An error was encountered on the database server * @throws {@link RecordNotFoundError} A record was not found and could not be updated */ handleDbServerError(response, subroutineName, options, setupOptions) { if (response.data.output == null) { // handle invalid response this.logHandler.error(`Response from db server was malformed when calling ${subroutineName}`); throw new _errors.DbServerError({ message: `Response from db server was malformed when calling ${subroutineName}` }); } if ('errorCode' in response.data.output) { const { errorCode } = response.data.output; switch (errorCode) { case _constants.dbErrors.foreignKeyValidation.code: { const { filename, id } = options; this.logHandler.debug(`foreign key violations found when saving record ${id} to ${filename}`); throw new _errors.ForeignKeyValidationError({ foreignKeyValidationErrors: response.data.output.foreignKeyValidationErrors, filename, recordId: id }); } case _constants.dbErrors.recordLocked.code: { const { filename, id } = options; if (subroutineName === 'deleteById') { this.logHandler.debug(`record locked when deleting record ${id} from ${filename}`); } else { this.logHandler.debug(`record locked when saving record ${id} to ${filename}`); } throw new _errors.RecordLockedError({ filename, recordId: id }); } case _constants.dbErrors.recordVersion.code: { const { filename, id } = options; this.logHandler.debug(`record version mismatch found when saving record ${id} to ${filename}`); throw new _errors.RecordVersionError({ filename, recordId: id }); } case _constants.dbErrors.maxPayloadExceeded.code: { const { maxReturnPayloadSize } = setupOptions; this.logHandler.debug(`Maximum return payload size of ${maxReturnPayloadSize} bytes exceeded`); throw new _errors.DbServerError({ message: `Maximum return payload size of ${maxReturnPayloadSize} exceeded` }); } case _constants.dbErrors.recordNotFound.code: { const { filename, id } = options; this.logHandler.debug(`record ${id} not found from ${filename} when incrementing`); throw new _errors.RecordNotFoundError({ filename, recordId: id }); } default: this.logHandler.error(`error code ${response.data.output.errorCode} occurred in database operation when calling ${subroutineName}`); throw new _errors.DbServerError({ errorCode: response.data.output.errorCode }); } } } /** Handle an axios error */ handleAxiosError(err) { if (err.code === 'ETIMEDOUT') { this.logHandler.error(`Timeout error occurred in MVIS request: ${err.message}`); throw new _errors.TimeoutError(err, { message: err.message }); } this.logHandler.error(`Error occurred in MVIS request: ${err.message}`); throw new _errors.MvisError(err, { message: err.message }); } /** Handle an unknown error */ handleUnexpectedError(err) { if (err instanceof Error) { this.logHandler.error(`Error occurred in MVIS request: ${err.message}`); throw new _errors.UnknownError(err, { message: err.message }); } this.logHandler.error('Unknown error occurred in MVIS request'); throw new _errors.UnknownError(err); } } var _default = exports.default = Connection;