diff --git a/.changeset/five-donkeys-brake.md b/.changeset/five-donkeys-brake.md index 1e732b2b9a..d5bc695714 100644 --- a/.changeset/five-donkeys-brake.md +++ b/.changeset/five-donkeys-brake.md @@ -1,11 +1,10 @@ --- -'example-backend': minor '@backstage/backend-common': minor --- -Introduces `PluginConnectionDatabaseManager`, a backwards compatible database -connection manager which allows developers to configure database connections on -a per plugin basis. +Deprecates `SingleConnectionDatabaseManager` and provides an API compatible database +connection manager, `DatabaseManager`, which allows developers to configure database +connections on a per plugin basis. The `backend.database` config path allows you to set `prefix` to use an alternate prefix for automatically generated database names, the default is @@ -30,13 +29,13 @@ backend: connection: ':memory:' ``` -Existing backstage installations can be migrated by swapping out the database -manager under `packages/backend/src/index.ts` as shown below: +Migrate existing backstage installations by swapping out the database manager in the +`packages/backend/src/index.ts` file as shown below: ```diff import { - SingleConnectionDatabaseManager, -+ PluginConnectionDatabaseManager, ++ DatabaseManager, } from '@backstage/backend-common'; // ... @@ -44,7 +43,7 @@ import { function makeCreateEnv(config: Config) { // ... - const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); -+ const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); ++ const databaseManager = DatabaseManager.fromConfig(config); // ... } ``` diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b5e4b0a84e..d4d075004a 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -104,6 +104,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl; // @public (undocumented) export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise; +// @public (undocumented) +export class DatabaseManager { + forPlugin(pluginId: string): PluginDatabaseManager; + static fromConfig(config: Config): DatabaseManager; + } + // @public (undocumented) export class DockerContainerRunner implements ContainerRunner { constructor({ dockerClient }: { @@ -267,14 +273,6 @@ export type PluginCacheManager = { getClient: (options?: ClientOptions) => CacheClient; }; -// @public (undocumented) -export class PluginConnectionDatabaseManager { - // (undocumented) - static readonly DEFAULT_PREFIX = "backstage_plugin_"; - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): PluginConnectionDatabaseManager; - } - // @public export interface PluginDatabaseManager { getClient(): Promise; @@ -344,11 +342,8 @@ export type ServiceBuilder = { // @public (undocumented) export function setRootLogger(newLogger: winston.Logger): void; -// @public -export class SingleConnectionDatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; - static fromConfig(config: Config): SingleConnectionDatabaseManager; - } +// @public @deprecated +export const SingleConnectionDatabaseManager: typeof DatabaseManager; // @public export class SingleHostDiscovery implements PluginEndpointDiscovery { diff --git a/packages/backend-common/config.d.ts b/packages/backend-common/config.d.ts index d735f39935..3ada25dd8a 100644 --- a/packages/backend-common/config.d.ts +++ b/packages/backend-common/config.d.ts @@ -14,22 +14,15 @@ * limitations under the License. */ -export type PluginDatabaseConfig = - | { - /** Database client to use for plugin. */ - client?: 'sqlite3'; - /** Database connection to use with plugin. */ - connection?: ':memory:' | string | { filename: string }; - } - | { - /** Database client to use for plugin. */ - client?: 'pg'; - /** - * PostgreSQL connection string or knex configuration object for plugin. - * @secret - */ - connection?: string | object; - }; +export type PluginDatabaseConfig = { + /** Database client to use. */ + client?: 'sqlite3' | 'pg'; + /** + * Database connection to use. + * @secret + */ + connection?: string | object; +}; export interface Config { app: { @@ -70,32 +63,30 @@ export interface Config { }; }; - /** Database connection configuration, select database type using the `client` field */ - database: - | { - client: 'sqlite3'; - connection: ':memory:' | string | { filename: string }; - /** Optional sqlite3 database filename prefix. */ - prefix?: string; - /** Override database config per plugin. */ - plugin?: { - [pluginId: string]: PluginDatabaseConfig; - }; - } - | { - client: 'pg'; + /** Database connection configuration, select base database type using the `client` field */ + database: { + /** Default database client to use */ + client: 'sqlite3' | 'pg'; + /** + * Base database connection string or Knex object + * @secret + */ + connection: string | object; + /** Database name prefix override */ + prefix?: string; + /** Plugin specific database configuration and client override */ + plugin?: { + [pluginId: string]: { + /** Database client override */ + client?: 'sqlite3' | 'pg'; /** - * PostgreSQL connection string or knex configuration object. + * Database connection string or Knex object override * @secret */ - connection: string | object; - /** Optional PostgreSQL database prefix. */ - prefix?: string; - /** Override database config per plugin. */ - plugin?: { - [pluginId: string]: PluginDatabaseConfig; - }; + connection?: string | object; }; + }; + }; /** Cache connection configuration, select cache type using the `store` field */ cache?: diff --git a/packages/backend-common/src/database/PluginConnection.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts similarity index 88% rename from packages/backend-common/src/database/PluginConnection.test.ts rename to packages/backend-common/src/database/DatabaseManager.test.ts index 51684210f8..8f3331403d 100644 --- a/packages/backend-common/src/database/PluginConnection.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { omit } from 'lodash'; import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { PluginConnectionDatabaseManager } from './PluginConnection'; +import { DatabaseManager } from './DatabaseManager'; jest.mock('./connection', () => ({ ...jest.requireActual('./connection'), @@ -24,40 +24,35 @@ jest.mock('./connection', () => ({ ensureDatabaseExists: jest.fn(), })); -describe('PluginConnectionDatabaseManager', () => { +describe('DatabaseManager', () => { // This is similar to the ts-jest `mocked` helper. const mocked = (f: Function) => f as jest.Mock; afterEach(() => jest.resetAllMocks()); - describe('PluginConnectionDatabaseManager.fromConfig', () => { - const backendConfig = { - backend: { - database: { - client: 'pg', - connection: { - host: 'localhost', - user: 'foo', - password: 'bar', - database: 'foodb', + describe('DatabaseManager.fromConfig', () => { + it('accesses the backend.database key', () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, }, }, - }, - }; - const defaultConfig = () => new ConfigReader(backendConfig); + }); + const getConfigSpy = jest.spyOn(config, 'getConfig'); + DatabaseManager.fromConfig(config); - it('accesses the backend.database key', () => { - const getConfig = jest.fn(); - const config = defaultConfig(); - config.getConfig = getConfig; - - PluginConnectionDatabaseManager.fromConfig(config); - - expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); + expect(getConfigSpy).toHaveBeenCalledWith('backend.database'); }); }); - describe('PluginConnectionDatabaseManager.forPlugin', () => { + describe('DatabaseManager.forPlugin', () => { const config = { backend: { database: { @@ -92,9 +87,7 @@ describe('PluginConnectionDatabaseManager', () => { }, }, }; - const manager = PluginConnectionDatabaseManager.fromConfig( - new ConfigReader(config), - ); + const manager = DatabaseManager.fromConfig(new ConfigReader(config)); it('connects to a plugin database using default config', async () => { const pluginId = 'pluginwithoutconfig'; @@ -120,7 +113,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('provides a plugin db which uses components from top level connection string', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -153,7 +146,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('uses top level sqlite database filename if plugin config is not present', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -175,7 +168,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -280,7 +273,7 @@ describe('PluginConnectionDatabaseManager', () => { }); it('generates a database name override when prefix is not explicitly set', async () => { - const testManager = PluginConnectionDatabaseManager.fromConfig( + const testManager = DatabaseManager.fromConfig( new ConfigReader({ backend: { database: { @@ -302,7 +295,7 @@ describe('PluginConnectionDatabaseManager', () => { expect(overrides).toHaveProperty( 'connection.database', - expect.stringContaining(PluginConnectionDatabaseManager.DEFAULT_PREFIX), + expect.stringContaining('backstage_plugin_'), ); }); diff --git a/packages/backend-common/src/database/PluginConnection.ts b/packages/backend-common/src/database/DatabaseManager.ts similarity index 88% rename from packages/backend-common/src/database/PluginConnection.ts rename to packages/backend-common/src/database/DatabaseManager.ts index 4f41bd068e..34ceea55e3 100644 --- a/packages/backend-common/src/database/PluginConnection.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -28,11 +28,9 @@ function pluginPath(pluginId: string): string { return `plugin.${pluginId}`; } -export class PluginConnectionDatabaseManager { - static readonly DEFAULT_PREFIX = 'backstage_plugin_'; - +export class DatabaseManager { /** - * Creates a PluginConnectionDatabaseManager from `backend.database` config. + * Creates a DatabaseManager from `backend.database` config. * * 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 @@ -41,13 +39,19 @@ export class PluginConnectionDatabaseManager { * * @param config The loaded application configuration. */ - static fromConfig(config: Config): PluginConnectionDatabaseManager { - return new PluginConnectionDatabaseManager( - config.getConfig('backend.database'), + static fromConfig(config: Config): DatabaseManager { + const databaseConfig = config.getConfig('backend.database'); + + return new DatabaseManager( + databaseConfig, + databaseConfig.getOptionalString('prefix'), ); } - private constructor(private readonly config: Config) {} + private constructor( + private readonly config: Config, + private readonly prefix: string = 'backstage_plugin_', + ) {} /** * Generates a PluginDatabaseManager for consumption by plugins. @@ -70,8 +74,8 @@ export class PluginConnectionDatabaseManager { * * This method provides the effective database name which is determined using global * and plugin specific database config. If no explicit database name is configured, - * this method will provide a generated name which is the pluginId prefixed using - * the value from `PluginConnectionDatabaseManager.DEFAULT_PREFIX`. + * this method will provide a generated name which is the pluginId prefixed with + * 'backstage_plugin_'. * * @param pluginId Lookup the database name for given plugin */ @@ -85,10 +89,6 @@ export class PluginConnectionDatabaseManager { ? rootConnection : this.config.getOptionalString('connection.filename') ?? ':memory:'; - const prefix = - this.config.getOptionalString('prefix') ?? - PluginConnectionDatabaseManager.DEFAULT_PREFIX; - const isSqlite = this.config.getString('client') === 'sqlite3'; return ( // attempt to lookup pg and mysql database name @@ -98,7 +98,7 @@ export class PluginConnectionDatabaseManager { // if root is sqlite - attempt to use top level connection, fallback to :memory: (isSqlite ? rootSqliteName : null) ?? // generate a database name using prefix and pluginId - `${prefix}${pluginId}` + `${this.prefix}${pluginId}` ); } diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts index 231523c790..852dd33944 100644 --- a/packages/backend-common/src/database/SingleConnection.test.ts +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -18,7 +18,11 @@ import { ConfigReader } from '@backstage/config'; import { createDatabaseClient, ensureDatabaseExists } from './connection'; import { SingleConnectionDatabaseManager } from './SingleConnection'; -jest.mock('./connection'); +jest.mock('./connection', () => ({ + ...jest.requireActual('./connection'), + createDatabaseClient: jest.fn(), + ensureDatabaseExists: jest.fn(), +})); describe('SingleConnectionDatabaseManager', () => { const defaultConfigOptions = { @@ -43,9 +47,8 @@ describe('SingleConnectionDatabaseManager', () => { describe('SingleConnectionDatabaseManager.fromConfig', () => { it('accesses the backend.database key', () => { - const getConfig = jest.fn(); const config = defaultConfig(); - config.getConfig = getConfig; + const getConfig = jest.spyOn(config, 'getConfig'); SingleConnectionDatabaseManager.fromConfig(config); @@ -64,7 +67,6 @@ describe('SingleConnectionDatabaseManager', () => { const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); const callArgs = mockCalls[0]; - expect(callArgs[0].get()).toEqual(defaultConfigOptions.backend.database); expect(callArgs[1].connection.database).toEqual( `backstage_plugin_${pluginId}`, ); diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts index 1c5931a662..5bdd99c0ae 100644 --- a/packages/backend-common/src/database/SingleConnection.ts +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -14,69 +14,14 @@ * limitations under the License. */ -import { Knex } from 'knex'; -import { Config } from '@backstage/config'; -import { createDatabaseClient, ensureDatabaseExists } from './connection'; -import { PluginDatabaseManager } from './types'; +import { DatabaseManager } from './DatabaseManager'; /** * Implements a Database Manager which will automatically create new databases * for plugins when requested. All requested databases are created with the * credentials provided; if the database already exists no attempt to create * the database will be made. + * + * @deprecated Use `DatabaseManager` from `@backend-common` instead. */ -export class SingleConnectionDatabaseManager { - /** - * Creates a new SingleConnectionDatabaseManager instance by reading from the `backend` - * config section, specifically the `.database` key for discovering the management - * database configuration. - * - * @param config The loaded application configuration. - */ - static fromConfig(config: Config): SingleConnectionDatabaseManager { - return new SingleConnectionDatabaseManager( - config.getConfig('backend.database'), - ); - } - - private constructor(private readonly config: Config) {} - - /** - * Generates a PluginDatabaseManager for consumption by plugins. - * - * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique. - */ - forPlugin(pluginId: string): PluginDatabaseManager { - const _this = this; - - return { - getClient(): Promise { - return _this.getDatabase(pluginId); - }, - }; - } - - private async getDatabase(pluginId: string): Promise { - const config = this.config; - const overrides = SingleConnectionDatabaseManager.getDatabaseOverrides( - pluginId, - ); - const overrideConfig = overrides.connection as Knex.ConnectionConfig; - await this.ensureDatabase(overrideConfig.database); - - return createDatabaseClient(config, overrides); - } - - private static getDatabaseOverrides(pluginId: string): Knex.Config { - return { - connection: { - database: `backstage_plugin_${pluginId}`, - }, - }; - } - - private async ensureDatabase(database: string) { - const config = this.config; - await ensureDatabaseExists(config, database); - } -} +export const SingleConnectionDatabaseManager = DatabaseManager; diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 7a8a81e187..7dfb08359b 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -17,4 +17,4 @@ export * from './connection'; export * from './types'; export * from './SingleConnection'; -export * from './PluginConnection'; +export * from './DatabaseManager'; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 9cab64e797..67149c8163 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -29,7 +29,7 @@ import { getRootLogger, loadBackendConfig, notFoundHandler, - PluginConnectionDatabaseManager, + DatabaseManager, SingleHostDiscovery, UrlReaders, useHotMemoize, @@ -59,7 +59,7 @@ function makeCreateEnv(config: Config) { root.info(`Created UrlReader ${reader}`); - const databaseManager = PluginConnectionDatabaseManager.fromConfig(config); + const databaseManager = DatabaseManager.fromConfig(config); const cacheManager = CacheManager.fromConfig(config); return (plugin: string): PluginEnvironment => {