clean up and expose dropDatabase again

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-04-28 22:17:12 +02:00
parent 5aad3c0d59
commit 419d4cac7c
9 changed files with 218 additions and 481 deletions
+1 -19
View File
@@ -34,8 +34,6 @@ import { HostDiscovery as HostDiscovery_2 } from '@backstage/backend-app-api';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { IdentityService } from '@backstage/backend-plugin-api';
import { isChildPath } from '@backstage/cli-common';
import { Knex } from 'knex';
import knexFactory from 'knex';
import { KubeConfig } from '@kubernetes/client-node';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
@@ -225,16 +223,6 @@ export interface ContainerRunner {
runContainer(opts: RunContainerOptions): Promise<void>;
}
// @public
export function createDatabaseClient(
dbConfig: Config,
overrides?: Partial<Knex.Config>,
deps?: {
lifecycle: LifecycleService;
pluginMetadata: PluginMetadataService;
},
): knexFactory.Knex<any, any[]>;
// @public
export function createLegacyAuthAdapters<
TOptions extends {
@@ -315,13 +303,7 @@ export class DockerContainerRunner implements ContainerRunner {
// @public
export function dropDatabase(
dbConfig: Config,
...databases: Array<string>
): Promise<void>;
// @public
export function ensureDatabaseExists(
dbConfig: Config,
...databases: Array<string>
...databaseNames: string[]
): Promise<void>;
// @public
@@ -13,44 +13,91 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader } from '@backstage/config';
import { omit } from 'lodash';
import path from 'path';
import { DatabaseManager } from './DatabaseManager';
import {
createDatabaseClient,
ensureDatabaseExists,
ensureSchemaExists,
} from './connection';
import { DatabaseManagerImpl } from './DatabaseManager';
import { Connector } from './types';
jest.mock('./connection', () => ({
...jest.requireActual('./connection'),
createDatabaseClient: jest.fn(),
ensureDatabaseExists: jest.fn(),
ensureSchemaExists: jest.fn(),
}));
describe('DatabaseManagerImpl', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('DatabaseManager', () => {
// This is similar to the ts-jest `mocked` helper.
const mocked = (f: Function) => f as jest.Mock;
it('calls the right connector, only once per plugin id', async () => {
const connector1 = {
getClient: jest.fn(),
dropDatabase: jest.fn(),
} satisfies Connector;
const connector2 = {
getClient: jest.fn(),
dropDatabase: jest.fn(),
} satisfies Connector;
afterEach(() => jest.resetAllMocks());
const impl = new DatabaseManagerImpl(
new ConfigReader({
client: 'pg',
}),
{
pg: connector1,
notpg: connector2,
},
);
describe('DatabaseManager.fromConfig', () => {
const backendConfig = {
backend: {
database: {
client: 'pg',
connection: {
host: 'localhost',
user: 'foo',
password: 'bar',
database: 'foodb',
await impl.forPlugin('plugin1').getClient();
expect(connector1.getClient).toHaveBeenCalledTimes(1);
expect(connector1.getClient).toHaveBeenLastCalledWith('plugin1', undefined);
expect(connector2.getClient).toHaveBeenCalledTimes(0);
await impl.forPlugin('plugin1').getClient();
expect(connector1.getClient).toHaveBeenCalledTimes(1);
expect(connector1.getClient).toHaveBeenLastCalledWith('plugin1', undefined);
expect(connector2.getClient).toHaveBeenCalledTimes(0);
await impl.forPlugin('plugin2').getClient();
expect(connector1.getClient).toHaveBeenCalledTimes(2);
expect(connector1.getClient).toHaveBeenLastCalledWith('plugin2', undefined);
expect(connector2.getClient).toHaveBeenCalledTimes(0);
});
it('respects per-plugin overridden connectors', async () => {
const connector1 = {
getClient: jest.fn(),
dropDatabase: jest.fn(),
} satisfies Connector;
const connector2 = {
getClient: jest.fn(),
dropDatabase: jest.fn(),
} satisfies Connector;
const impl = new DatabaseManagerImpl(
new ConfigReader({
client: 'pg',
plugin: {
plugin2: {
client: 'mysql',
},
},
}),
{
pg: connector1,
mysql: connector2,
},
};
);
await impl.forPlugin('plugin1').getClient();
expect(connector1.getClient).toHaveBeenCalledTimes(1);
expect(connector1.getClient).toHaveBeenLastCalledWith('plugin1', undefined);
expect(connector2.getClient).toHaveBeenCalledTimes(0);
await impl.forPlugin('plugin2').getClient();
expect(connector1.getClient).toHaveBeenCalledTimes(1);
expect(connector1.getClient).toHaveBeenLastCalledWith('plugin1', undefined);
expect(connector2.getClient).toHaveBeenCalledTimes(1);
expect(connector2.getClient).toHaveBeenLastCalledWith('plugin2', undefined);
});
// eslint-disable-next-line jest/no-commented-out-tests
/*
it('accesses the backend.database key', () => {
const config = new ConfigReader(backendConfig);
const getConfigSpy = jest.spyOn(config, 'getConfig');
@@ -848,4 +895,5 @@ describe('DatabaseManager', () => {
);
});
});
*/
});
@@ -54,44 +54,10 @@ export type LegacyRootDatabaseService = {
};
/**
* Manages database connections for Backstage backend plugins.
*
* @public
* @remarks
*
* The database manager allows the user to set connection and client settings on
* a per pluginId basis by defining a database config block under
* `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may
* set `prefix` which is used to prefix generated database names if config is
* not provided.
* Testable implementation class for {@link DatabaseManager} below.
*/
export class DatabaseManager implements LegacyRootDatabaseService {
/**
* Creates a {@link DatabaseManager} from `backend.database` config.
*
* @param config - The loaded application configuration.
* @param options - An optional configuration object.
*/
static fromConfig(
config: Config,
options?: DatabaseManagerOptions,
): DatabaseManager {
const databaseConfig = config.getConfig('backend.database');
const prefix = databaseConfig.getOptionalString('prefix');
return new DatabaseManager(
databaseConfig,
{
pg: new PgConnector(config, prefix),
sqlite3: new Sqlite3Connector(config, prefix),
'better-sqlite3': new Sqlite3Connector(config, prefix),
mysql: new MysqlConnector(config, prefix),
mysql2: new MysqlConnector(config, prefix),
},
options,
);
}
private constructor(
export class DatabaseManagerImpl implements LegacyRootDatabaseService {
constructor(
private readonly config: Config,
private readonly connectors: Record<string, Connector>,
private readonly options?: DatabaseManagerOptions,
@@ -204,3 +170,84 @@ export class DatabaseManager implements LegacyRootDatabaseService {
}, 60 * 1000);
}
}
// NOTE: This class looks odd but is kept around for API compatibility reasons
/**
* Manages database connections for Backstage backend plugins.
*
* @public
* @remarks
*
* The database manager allows the user to set connection and client settings on
* a per pluginId basis by defining a database config block under
* `plugin.<pluginId>` in addition to top level defaults. Optionally, a user may
* set `prefix` which is used to prefix generated database names if config is
* not provided.
*/
export class DatabaseManager implements LegacyRootDatabaseService {
/**
* Creates a {@link DatabaseManager} from `backend.database` config.
*
* @param config - The loaded application configuration.
* @param options - An optional configuration object.
*/
static fromConfig(
config: Config,
options?: DatabaseManagerOptions,
): DatabaseManager {
const databaseConfig = config.getConfig('backend.database');
const prefix =
databaseConfig.getOptionalString('prefix') || 'backstage_plugin_';
return new DatabaseManager(
new DatabaseManagerImpl(
databaseConfig,
{
pg: new PgConnector(databaseConfig, prefix),
sqlite3: new Sqlite3Connector(databaseConfig),
'better-sqlite3': new Sqlite3Connector(databaseConfig),
mysql: new MysqlConnector(databaseConfig, prefix),
mysql2: new MysqlConnector(databaseConfig, prefix),
},
options,
),
);
}
private constructor(private readonly impl: DatabaseManagerImpl) {}
/**
* Generates a PluginDatabaseManager for consumption by plugins.
*
* @param pluginId - The plugin that the database manager should be created for. Plugin names
* should be unique as they are used to look up database config overrides under
* `backend.database.plugin`.
*/
forPlugin(
pluginId: string,
deps?: {
lifecycle: LifecycleService;
pluginMetadata: PluginMetadataService;
},
): PluginDatabaseManager {
return this.impl.forPlugin(pluginId, deps);
}
}
/**
* Helper for deleting databases, only exists for backend-test-utils for now.
*
* @public
*/
export async function dropDatabase(
dbConfig: Config,
...databaseNames: string[]
): Promise<void> {
const client = dbConfig.getString('client');
const prefix = dbConfig.getOptionalString('prefix') || 'backstage_plugin_';
if (client === 'pg') {
await new PgConnector(dbConfig, prefix).dropDatabase(...databaseNames);
} else if (client === 'mysql' || client === 'mysql2') {
await new MysqlConnector(dbConfig, prefix).dropDatabase(...databaseNames);
}
}
@@ -1,218 +0,0 @@
/*
* Copyright 2024 The Backstage Authors
*
* 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.
*/
// * Copyright 2020 The Backstage Authors
// *
// * 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.
// */
// import { Config } from '@backstage/config';
// import { JsonObject } from '@backstage/types';
// import { InputError } from '@backstage/errors';
// import knexFactory, { Knex } from 'knex';
// import limiterFactory from 'p-limit';
// import { mergeDatabaseConfig } from './config';
// import { DatabaseConnector } from './types';
// import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors';
// import {
// LifecycleService,
// PluginMetadataService,
// } from '@backstage/backend-plugin-api';
// type DatabaseClient =
// | 'pg'
// | 'better-sqlite3'
// | 'sqlite3'
// | 'mysql'
// | 'mysql2'
// | string;
// // This limits the number of concurrent CREATE DATABASE and CREATE SCHEMA
// // commands, globally, to just one. This is overly defensive, and was added as
// // an attempt to counteract the pool issues on recent node versions. See
// // https://github.com/backstage/backstage/pull/19988
// const ddlLimiter = limiterFactory(1);
// /**
// * Mapping of client type to supported database connectors
// *
// * Database connectors can be aliased here, for example mysql2 uses
// * the same connector as mysql.
// */
// const ConnectorMapping: Record<DatabaseClient, DatabaseConnector> = {
// pg: pgConnector,
// 'better-sqlite3': sqlite3Connector,
// sqlite3: sqlite3Connector,
// mysql: mysqlConnector,
// mysql2: mysqlConnector,
// };
// /**
// * Creates a knex database connection
// *
// * @public
// * @param dbConfig - The database config
// * @param overrides - Additional options to merge with the config
// */
// export function createDatabaseClient(
// dbConfig: Config,
// overrides?: Partial<Knex.Config>,
// deps?: {
// lifecycle: LifecycleService;
// pluginMetadata: PluginMetadataService;
// },
// ) {
// const client: DatabaseClient = dbConfig.getString('client');
// return (
// ConnectorMapping[client]?.createClient(dbConfig, overrides, deps) ??
// knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides))
// );
// }
// /**
// * Ensures that the given databases all exist, creating them if they do not.
// *
// * @public
// */
// export async function ensureDatabaseExists(
// dbConfig: Config,
// ...databases: Array<string>
// ): Promise<void> {
// const client: DatabaseClient = dbConfig.getString('client');
// return await ddlLimiter(() =>
// ConnectorMapping[client]?.ensureDatabaseExists?.(dbConfig, ...databases),
// );
// }
// /**
// * Drops the given databases.
// *
// * @public
// */
// export async function dropDatabase(
// dbConfig: Config,
// ...databases: Array<string>
// ): Promise<void> {
// const client: DatabaseClient = dbConfig.getString('client');
// return await ddlLimiter(() =>
// ConnectorMapping[client]?.dropDatabase?.(dbConfig, ...databases),
// );
// }
// /**
// * Ensures that the given schemas all exist, creating them if they do not.
// *
// * @public
// */
// export async function ensureSchemaExists(
// dbConfig: Config,
// ...schemas: Array<string>
// ): Promise<void> {
// const client: DatabaseClient = dbConfig.getString('client');
// return await ddlLimiter(() =>
// ConnectorMapping[client]?.ensureSchemaExists?.(dbConfig, ...schemas),
// );
// }
// /**
// * Provides a `Knex.Config` object with the provided database name for a given
// * client.
// */
// export function createNameOverride(
// client: string,
// name: string,
// ): Partial<Knex.Config> {
// try {
// return ConnectorMapping[client].createNameOverride(name);
// } catch (e) {
// throw new InputError(
// `Unable to create database name override for '${client}' connector`,
// e,
// );
// }
// }
// /**
// * Provides a `Knex.Config` object with the provided database schema for a given
// * client. Currently only supported by `pg`.
// */
// export function createSchemaOverride(
// client: string,
// name: string,
// ): Partial<Knex.Config | undefined> {
// try {
// return ConnectorMapping[client]?.createSchemaOverride?.(name);
// } catch (e) {
// throw new InputError(
// `Unable to create database schema override for '${client}' connector`,
// e,
// );
// }
// }
// /**
// * Parses a connection string for a given client and provides a connection config.
// */
// export function parseConnectionString(
// connectionString: string,
// client?: string,
// ): Knex.StaticConnectionConfig {
// if (typeof client === 'undefined' || client === null) {
// throw new InputError(
// 'Database connection string client type auto-detection is not yet supported.',
// );
// }
// try {
// return ConnectorMapping[client].parseConnectionString(connectionString);
// } catch (e) {
// throw new InputError(
// `Unable to parse connection string for '${client}' connector`,
// );
// }
// }
// /**
// * Normalizes a connection config or string into an object which can be passed
// * to Knex.
// */
// export function normalizeConnection(
// connection: Knex.StaticConnectionConfig | JsonObject | string | undefined,
// client: string,
// ): Partial<Knex.StaticConnectionConfig> {
// if (typeof connection === 'undefined' || connection === null) {
// return {};
// }
// return typeof connection === 'string' || connection instanceof String
// ? parseConnectionString(connection as string, client)
// : connection;
// }
@@ -23,7 +23,6 @@ import { InputError } from '@backstage/errors';
import { JsonObject } from '@backstage/types';
import knexFactory, { Knex } from 'knex';
import { merge, omit } from 'lodash';
import path from 'path';
import yn from 'yn';
import { Connector, DatabaseConnector } from '../types';
import defaultNameOverride from './defaultNameOverride';
@@ -236,17 +235,6 @@ export const mysqlConnector: DatabaseConnector = Object.freeze({
dropDatabase: dropMysqlDatabase,
});
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Provides a config lookup path for a plugin's config block.
*/
@@ -267,20 +255,6 @@ function normalizeConnection(
: connection;
}
function createSchemaOverride(
client: string,
name: string,
): Partial<Knex.Config | undefined> {
try {
return mysqlConnector.createSchemaOverride?.(name);
} catch (e) {
throw new InputError(
`Unable to create database schema override for '${client}' connector`,
e,
);
}
}
function createNameOverride(
client: string,
name: string,
@@ -298,7 +272,7 @@ function createNameOverride(
export class MysqlConnector implements Connector {
constructor(
private readonly config: Config,
private readonly prefix: string = 'backstage_plugin_',
private readonly prefix: string,
) {}
async getClient(
@@ -315,7 +289,7 @@ export class MysqlConnector implements Connector {
const databaseName = this.getDatabaseName(pluginId);
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
try {
await mysqlConnector.ensureDatabaseExists(pluginConfig, databaseName);
await mysqlConnector.ensureDatabaseExists!(pluginConfig, databaseName);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
@@ -323,24 +297,16 @@ export class MysqlConnector implements Connector {
}
}
let schemaOverrides;
if (this.getPluginDivisionModeConfig() === 'schema') {
schemaOverrides = this.getSchemaOverrides(pluginId);
if (this.getEnsureExistsConfig(pluginId)) {
try {
await mysqlConnector.ensureSchemaExists(pluginConfig, pluginId);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
);
}
}
const pluginDivisionMode = this.getPluginDivisionModeConfig();
if (pluginDivisionMode !== 'database') {
throw new Error(
`The MySQL driver does not suppoert plugin division mode '${pluginDivisionMode}'`,
);
}
const databaseClientOverrides = mergeDatabaseConfig(
{},
this.getDatabaseOverrides(pluginId),
schemaOverrides,
);
const client = mysqlConnector.createClient(
@@ -352,29 +318,24 @@ export class MysqlConnector implements Connector {
return client;
}
async dropDatabase(...databaseNames: string[]): Promise<void> {
return await dropMysqlDatabase(this.config, ...databaseNames);
}
/**
* Provides the canonical database name for a given plugin.
*
* This method provides the effective database name which is determined using global
* and plugin specific database config. If no explicit database name is configured
* and `pluginDivisionMode` is not `schema`, this method will provide a generated name
* which is the pluginId prefixed with 'backstage_plugin_'. If `pluginDivisionMode` is
* `schema`, it will fallback to using the default database for the knex instance.
* This method provides the effective database name which is determined using
* global and plugin specific database config. If no explicit database name,
* this method will provide a generated name which is the pluginId prefixed
* with 'backstage_plugin_'.
*
* @param pluginId - Lookup the database name for given plugin
* @returns String representing the plugin's database name
*/
private getDatabaseName(pluginId: string): string | undefined {
const connection = this.getConnectionConfig(pluginId);
const databaseName = (connection as Knex.ConnectionConfig)?.database;
// `pluginDivisionMode` as `schema` should use overridden databaseName if supplied or fallback to default knex database
if (this.getPluginDivisionModeConfig() === 'schema') {
return databaseName;
}
// all other supported databases should fallback to an auto-prefixed name
return databaseName ?? `${this.prefix}${pluginId}`;
}
@@ -500,17 +461,6 @@ export class MysqlConnector implements Connector {
};
}
/**
* Provides a partial `Knex.Config` database schema override for a given
* plugin.
*
* @param pluginId - Target plugin to get database schema override
* @returns Partial `Knex.Config` with database schema override
*/
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
}
/**
* Provides a partial `Knex.Config`• database name override for a given plugin.
*
@@ -23,7 +23,6 @@ import { ForwardedError, InputError } from '@backstage/errors';
import { JsonObject } from '@backstage/types';
import knexFactory, { Knex } from 'knex';
import { merge, omit } from 'lodash';
import path from 'path';
import { Client } from 'pg';
import { Connector, DatabaseConnector } from '../types';
import defaultNameOverride from './defaultNameOverride';
@@ -216,11 +215,15 @@ export async function dropPgDatabase(
...databases: Array<string>
) {
const admin = createPgDatabaseClient(dbConfig);
await Promise.all(
databases.map(async database => {
await admin.raw(`DROP DATABASE ??`, [database]);
}),
);
try {
await Promise.all(
databases.map(async database => {
await admin.raw(`DROP DATABASE ??`, [database]);
}),
);
} finally {
await admin.destroy();
}
}
export const pgConnector: DatabaseConnector = Object.freeze({
@@ -233,17 +236,6 @@ export const pgConnector: DatabaseConnector = Object.freeze({
dropDatabase: dropPgDatabase,
});
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Provides a config lookup path for a plugin's config block.
*/
@@ -295,7 +287,7 @@ function createNameOverride(
export class PgConnector implements Connector {
constructor(
private readonly config: Config,
private readonly prefix: string = 'backstage_plugin_',
private readonly prefix: string,
) {}
async getClient(
@@ -312,7 +304,7 @@ export class PgConnector implements Connector {
const databaseName = this.getDatabaseName(pluginId);
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
try {
await pgConnector.ensureDatabaseExists(pluginConfig, databaseName);
await pgConnector.ensureDatabaseExists!(pluginConfig, databaseName);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
@@ -325,7 +317,7 @@ export class PgConnector implements Connector {
schemaOverrides = this.getSchemaOverrides(pluginId);
if (this.getEnsureExistsConfig(pluginId)) {
try {
await pgConnector.ensureSchemaExists(pluginConfig, pluginId);
await pgConnector.ensureSchemaExists!(pluginConfig, pluginId);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
@@ -349,6 +341,10 @@ export class PgConnector implements Connector {
return client;
}
async dropDatabase(...databaseNames: string[]): Promise<void> {
return await dropPgDatabase(this.config, ...databaseNames);
}
/**
* Provides the canonical database name for a given plugin.
*
@@ -168,17 +168,6 @@ export const sqliteConnector: DatabaseConnector = Object.freeze({
parseConnectionString: parseSqliteConnectionString,
});
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Provides a config lookup path for a plugin's config block.
*/
@@ -199,20 +188,6 @@ function normalizeConnection(
: connection;
}
function createSchemaOverride(
client: string,
name: string,
): Partial<Knex.Config | undefined> {
try {
return sqliteConnector.createSchemaOverride?.(name);
} catch (e) {
throw new InputError(
`Unable to create database schema override for '${client}' connector`,
e,
);
}
}
function createNameOverride(
client: string,
name: string,
@@ -228,10 +203,7 @@ function createNameOverride(
}
export class Sqlite3Connector implements Connector {
constructor(
private readonly config: Config,
private readonly prefix: string = 'backstage_plugin_',
) {}
constructor(private readonly config: Config) {}
async getClient(
pluginId: string,
@@ -244,35 +216,16 @@ export class Sqlite3Connector implements Connector {
this.getConfigForPlugin(pluginId) as JsonObject,
);
const databaseName = this.getDatabaseName(pluginId);
if (databaseName && this.getEnsureExistsConfig(pluginId)) {
try {
await sqliteConnector.ensureDatabaseExists(pluginConfig, databaseName);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`,
);
}
}
let schemaOverrides;
if (this.getPluginDivisionModeConfig() === 'schema') {
schemaOverrides = this.getSchemaOverrides(pluginId);
if (this.getEnsureExistsConfig(pluginId)) {
try {
await sqliteConnector.ensureSchemaExists(pluginConfig, pluginId);
} catch (error) {
throw new Error(
`Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`,
);
}
}
const pluginDivisionMode = this.getPluginDivisionModeConfig();
if (pluginDivisionMode !== 'database') {
throw new Error(
`The SQLite driver does not suppoert plugin division mode '${pluginDivisionMode}'`,
);
}
const databaseClientOverrides = mergeDatabaseConfig(
{},
this.getDatabaseOverrides(pluginId),
schemaOverrides,
);
const client = sqliteConnector.createClient(
@@ -284,6 +237,10 @@ export class Sqlite3Connector implements Connector {
return client;
}
async dropDatabase(..._databaseNames: string[]): Promise<void> {
// do nothing
}
/**
* Provides the canonical database name for a given plugin.
*
@@ -299,30 +256,18 @@ export class Sqlite3Connector implements Connector {
private getDatabaseName(pluginId: string): string | undefined {
const connection = this.getConnectionConfig(pluginId);
if (this.getClientType(pluginId).client.includes('sqlite3')) {
const sqliteFilename: string | undefined = (
connection as Knex.Sqlite3ConnectionConfig
).filename;
const sqliteFilename: string | undefined = (
connection as Knex.Sqlite3ConnectionConfig
).filename;
if (sqliteFilename === ':memory:') {
return sqliteFilename;
}
const sqliteDirectory =
(connection as { directory?: string }).directory ?? '.';
return path.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`);
if (sqliteFilename === ':memory:') {
return sqliteFilename;
}
const databaseName = (connection as Knex.ConnectionConfig)?.database;
const sqliteDirectory =
(connection as { directory?: string }).directory ?? '.';
// `pluginDivisionMode` as `schema` should use overridden databaseName if supplied or fallback to default knex database
if (this.getPluginDivisionModeConfig() === 'schema') {
return databaseName;
}
// all other supported databases should fallback to an auto-prefixed name
return databaseName ?? `${this.prefix}${pluginId}`;
return path.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`);
}
/**
@@ -377,14 +322,6 @@ export class Sqlite3Connector implements Connector {
return merge(baseConfig, pluginConfig);
}
private getEnsureExistsConfig(pluginId: string): boolean {
const baseConfig = this.config.getOptionalBoolean('ensureExists') ?? true;
return (
this.config.getOptionalBoolean(`${pluginPath(pluginId)}.ensureExists`) ??
baseConfig
);
}
private getPluginDivisionModeConfig(): string {
return this.config.getOptionalString('pluginDivisionMode') ?? 'database';
}
@@ -459,17 +396,6 @@ export class Sqlite3Connector implements Connector {
};
}
/**
* Provides a partial `Knex.Config` database schema override for a given
* plugin.
*
* @param pluginId - Target plugin to get database schema override
* @returns Partial `Knex.Config` with database schema override
*/
private getSchemaOverrides(pluginId: string): Knex.Config | undefined {
return createSchemaOverride(this.getClientType(pluginId).client, pluginId);
}
/**
* Provides a partial `Knex.Config` database name override for a given plugin.
*
@@ -14,7 +14,11 @@
* limitations under the License.
*/
export * from './DatabaseManager';
export { DatabaseManager, dropDatabase } from './DatabaseManager';
export type {
DatabaseManagerOptions,
LegacyRootDatabaseService,
} from './DatabaseManager';
export type { PluginDatabaseManager } from './types';
export { isDatabaseConflictError } from './util';
@@ -95,4 +95,6 @@ export interface Connector {
pluginMetadata: PluginMetadataService;
},
): Promise<Knex>;
dropDatabase(...databaseNames: string[]): Promise<void>;
}