refactor: rename and deprecate database manager

Changes:
- Deprecates SingleConnectionManager and aliases to DatabaseManager.
- Simplifies database config typing in `config.d.ts`.
- Drops implementation specific assert in SingleConnectionManager test.
- Move database prefix reference and drop static property for default value.

Signed-off-by: Minn Soe <contributions@minn.io>
This commit is contained in:
Minn Soe
2021-05-25 18:05:21 +01:00
parent ef1d705b76
commit 2976f3bae3
9 changed files with 97 additions and 172 deletions
+7 -8
View File
@@ -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);
// ...
}
```
+8 -13
View File
@@ -104,6 +104,12 @@ export function createServiceBuilder(_module: NodeModule): ServiceBuilderImpl;
// @public (undocumented)
export function createStatusCheckRouter(options: StatusCheckRouterOptions): Promise<express.Router>;
// @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<Knex>;
@@ -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 {
+29 -38
View File
@@ -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?:
@@ -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_'),
);
});
@@ -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.<pluginId>` 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}`
);
}
@@ -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}`,
);
@@ -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<Knex> {
return _this.getDatabase(pluginId);
},
};
}
private async getDatabase(pluginId: string): Promise<Knex> {
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;
@@ -17,4 +17,4 @@
export * from './connection';
export * from './types';
export * from './SingleConnection';
export * from './PluginConnection';
export * from './DatabaseManager';
+2 -2
View File
@@ -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 => {