mvom
Version:
Multivalue Object Mapper
427 lines (400 loc) • 14.5 kB
JavaScript
"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;