@backstage/backend-test-utils
Version:
Test helpers library for Backstage backends
189 lines (183 loc) • 5.77 kB
JavaScript
;
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