diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f9e0d76aab..9e64b84777 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -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; } -// @public -export function createDatabaseClient( - dbConfig: Config, - overrides?: Partial, - deps?: { - lifecycle: LifecycleService; - pluginMetadata: PluginMetadataService; - }, -): knexFactory.Knex; - // @public export function createLegacyAuthAdapters< TOptions extends { @@ -315,13 +303,7 @@ export class DockerContainerRunner implements ContainerRunner { // @public export function dropDatabase( dbConfig: Config, - ...databases: Array -): Promise; - -// @public -export function ensureDatabaseExists( - dbConfig: Config, - ...databases: Array + ...databaseNames: string[] ): Promise; // @public diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index 6a461fad55..ba911b68b9 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -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', () => { ); }); }); + */ }); diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 66fef056bc..dd36cc3b4a 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -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.` 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, 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.` 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 { + 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); + } +} diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts deleted file mode 100644 index 875734aa19..0000000000 --- a/packages/backend-common/src/database/connection.ts +++ /dev/null @@ -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 = { -// 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, -// 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 -// ): Promise { -// 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 -// ): Promise { -// 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 -// ): Promise { -// 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 { -// 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 { -// 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 { -// if (typeof connection === 'undefined' || connection === null) { -// return {}; -// } - -// return typeof connection === 'string' || connection instanceof String -// ? parseConnectionString(connection as string, client) -// : connection; -// } diff --git a/packages/backend-common/src/database/connectors/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts index 418466e3c6..25594d5021 100644 --- a/packages/backend-common/src/database/connectors/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -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 { - 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 { + 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. * diff --git a/packages/backend-common/src/database/connectors/postgres.ts b/packages/backend-common/src/database/connectors/postgres.ts index 5cc81591ad..39a1073a30 100644 --- a/packages/backend-common/src/database/connectors/postgres.ts +++ b/packages/backend-common/src/database/connectors/postgres.ts @@ -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 ) { 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 { + return await dropPgDatabase(this.config, ...databaseNames); + } + /** * Provides the canonical database name for a given plugin. * diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index a9c2b93d1c..46ff756854 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -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 { - 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 { + // 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. * diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 464cf4a4de..a0a61ccdd0 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -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'; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 967de6966e..a9cceaa1f9 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -95,4 +95,6 @@ export interface Connector { pluginMetadata: PluginMetadataService; }, ): Promise; + + dropDatabase(...databaseNames: string[]): Promise; }