UNPKG

@backstage/backend-test-utils

Version:

Test helpers library for Backstage backends

189 lines (183 loc) • 5.77 kB
'use strict'; var errors = require('@backstage/errors'); var crypto = require('crypto'); var knexFactory = require('knex'); var uuid = require('uuid'); var yn = require('yn'); var types = require('./types.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var knexFactory__default = /*#__PURE__*/_interopDefaultCompat(knexFactory); var yn__default = /*#__PURE__*/_interopDefaultCompat(yn); async function waitForMysqlReady(connection) { const startTime = Date.now(); let lastError; let attempts = 0; for (; ; ) { attempts += 1; let knex; try { knex = knexFactory__default.default({ client: "mysql2", connection: { // make a copy because the driver mutates this ...connection } }); const result = await knex.select(knex.raw("version() AS version")); if (Array.isArray(result) && result[0]?.version) { return; } } catch (e) { lastError = e; } finally { await knex?.destroy(); } if (Date.now() - startTime > 3e4) { throw new Error( `Timed out waiting for the database to be ready for connections, ${attempts} attempts, ${lastError ? `last error was ${errors.stringifyError(lastError)}` : "(no errors thrown)"}` ); } await new Promise((resolve) => setTimeout(resolve, 100)); } } async function startMysqlContainer(image) { const user = "root"; const password = uuid.v4(); const { GenericContainer } = require("testcontainers"); const container = await new GenericContainer(image).withExposedPorts(3306).withEnvironment({ MYSQL_ROOT_PASSWORD: password }).withTmpFs({ "/var/lib/mysql": "rw" }).start(); const host = container.getHost(); const port = container.getMappedPort(3306); const connection = { host, port, user, password }; const stopContainer = async () => { await container.stop({ timeout: 1e4 }); }; await waitForMysqlReady(connection); return { connection, stopContainer }; } function parseMysqlConnectionString(connectionString) { try { const { protocol, username, password, port, hostname, pathname, searchParams } = new URL(connectionString); if (protocol !== "mysql:") { throw new Error(`Unknown protocol ${protocol}`); } else if (!username || !password) { throw new Error(`Missing username/password`); } else if (!pathname.match(/^\/[^/]+$/)) { throw new Error(`Expected single path segment`); } const result = { user: username, password, host: hostname, port: Number(port || 3306), database: decodeURIComponent(pathname.substring(1)) }; const ssl = searchParams.get("ssl"); if (ssl) { result.ssl = ssl; } const debug = searchParams.get("debug"); if (debug) { result.debug = yn__default.default(debug); } return result; } catch (e) { throw new Error(`Error while parsing MySQL connection string, ${e}`, e); } } class MysqlEngine { static async create(properties) { const { connectionStringEnvironmentVariableName, dockerImageName } = properties; if (connectionStringEnvironmentVariableName) { const connectionString = process.env[connectionStringEnvironmentVariableName]; if (connectionString) { const connection = parseMysqlConnectionString(connectionString); return new MysqlEngine( properties, connection ); } } if (dockerImageName) { const { connection, stopContainer } = await startMysqlContainer( dockerImageName ); return new MysqlEngine(properties, connection, stopContainer); } throw new Error(`Test databasee for ${properties.name} not configured`); } #properties; #connection; #knexInstances; #databaseNames; #stopContainer; constructor(properties, connection, stopContainer) { this.#properties = properties; this.#connection = connection; this.#knexInstances = []; this.#databaseNames = []; this.#stopContainer = stopContainer; } async createDatabaseInstance() { const adminConnection = this.#connectAdmin(); try { const databaseName = `db${crypto.randomBytes(16).toString("hex")}`; await adminConnection.raw("CREATE DATABASE ??", [databaseName]); this.#databaseNames.push(databaseName); const knexInstance = knexFactory__default.default({ client: this.#properties.driver, connection: { ...this.#connection, database: databaseName }, ...types.LARGER_POOL_CONFIG }); this.#knexInstances.push(knexInstance); return knexInstance; } finally { await adminConnection.destroy(); } } async shutdown() { for (const instance of this.#knexInstances) { await instance.destroy(); } const adminConnection = this.#connectAdmin(); try { for (const databaseName of this.#databaseNames) { await adminConnection.raw("DROP DATABASE ??", [databaseName]); } } finally { await adminConnection.destroy(); } await this.#stopContainer?.(); } #connectAdmin() { const connection = { ...this.#connection, database: null }; return knexFactory__default.default({ client: this.#properties.driver, connection, pool: { min: 0, max: 1, acquireTimeoutMillis: 2e4, createTimeoutMillis: 2e4, createRetryIntervalMillis: 1e3 } }); } } exports.MysqlEngine = MysqlEngine; exports.parseMysqlConnectionString = parseMysqlConnectionString; exports.startMysqlContainer = startMysqlContainer; //# sourceMappingURL=mysql.cjs.js.map