diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts index 254bdf6c9d..debbaf79d0 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.test.ts @@ -17,10 +17,12 @@ import { Config, ConfigReader } from '@backstage/config'; import { buildPgDatabaseConfig, + computePgPluginConfig, createPgDatabaseClient, getPgConnectionConfig, parsePgConnectionString, } from './postgres'; +import { type Knex } from 'knex'; jest.mock('@google-cloud/cloud-sql-connector'); @@ -394,3 +396,493 @@ describe('postgres', () => { }); }); }); + +describe('computePgPluginConfig', () => { + const prefix = 'backstage_plugin_'; + + describe('client', () => { + it('uses base client when no plugin client specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.client).toBe('pg'); + expect(result.clientOverridden).toBe(false); + }); + + it('uses plugin client when specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + plugin: { + catalog: { + client: 'better-sqlite3', + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.client).toBe('better-sqlite3'); + expect(result.clientOverridden).toBe(true); + }); + }); + + describe('role', () => { + it('returns undefined when no role specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.role).toBeUndefined(); + }); + + it('uses base role when no plugin role specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + role: 'base_role', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.role).toBe('base_role'); + }); + + it('uses plugin role when specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + role: 'base_role', + plugin: { + catalog: { + role: 'plugin_role', + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.role).toBe('plugin_role'); + }); + }); + + describe('additionalKnexConfig', () => { + it('returns empty object when no knexConfig specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.additionalKnexConfig).toEqual({}); + }); + + it('uses base knexConfig when no plugin config', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + knexConfig: { debug: true }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.additionalKnexConfig).toEqual({ debug: true }); + }); + + it('merges base and plugin knexConfig', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + knexConfig: { debug: true, pool: { min: 0 } }, + plugin: { + catalog: { + knexConfig: { pool: { max: 10 } }, + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.additionalKnexConfig).toEqual({ + debug: true, + pool: { min: 0, max: 10 }, + }); + }); + }); + + describe('ensureExists', () => { + it('defaults to true when not specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.ensureExists).toBe(true); + }); + + it('uses base ensureExists when no plugin setting', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + ensureExists: false, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.ensureExists).toBe(false); + }); + + it('uses plugin ensureExists when specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + ensureExists: true, + plugin: { + catalog: { + ensureExists: false, + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.ensureExists).toBe(false); + }); + }); + + describe('ensureSchemaExists', () => { + it('defaults to false when not specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.ensureSchemaExists).toBe(false); + }); + + it('uses base ensureSchemaExists when no plugin setting', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + ensureSchemaExists: true, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.ensureSchemaExists).toBe(true); + }); + }); + + describe('pluginDivisionMode', () => { + it('defaults to database when not specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.pluginDivisionMode).toBe('database'); + }); + + it('uses specified pluginDivisionMode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.pluginDivisionMode).toBe('schema'); + }); + }); + + describe('connection', () => { + it('sets application_name to plugin id', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection).toMatchObject({ + application_name: 'backstage_plugin_catalog', + }); + }); + + it('preserves existing application_name', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', application_name: 'custom_name' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection).toMatchObject({ + application_name: 'custom_name', + }); + }); + + it('omits database from base connection when pluginDivisionMode is database', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', database: 'shared_db' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection.database).toBeUndefined(); + }); + + it('keeps database from base connection when pluginDivisionMode is schema', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', database: 'shared_db' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection.database).toBe('shared_db'); + }); + + it('merges plugin connection with base connection', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', user: 'base_user' }, + plugin: { + catalog: { + connection: { password: 'plugin_pass' }, + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection).toMatchObject({ + host: 'localhost', + user: 'base_user', + password: 'plugin_pass', + }); + }); + + it('excludes base connection when client is overridden', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', user: 'base_user' }, + plugin: { + catalog: { + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection.host).toBeUndefined(); + expect(result.connection.user).toBeUndefined(); + expect( + (result.connection as Knex.BetterSqlite3ConnectionConfig).filename, + ).toBe(':memory:'); + }); + + it('parses connection string in base config', () => { + const config = new ConfigReader({ + client: 'pg', + connection: 'postgresql://user:pass@localhost:5432/mydb', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection).toMatchObject({ + host: 'localhost', + user: 'user', + password: 'pass', + port: '5432', + }); + }); + + it('parses connection string in plugin config', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'base-host' }, + plugin: { + catalog: { + connection: 'postgresql://plugin:pass@plugin-host:5432/plugindb', + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.connection).toMatchObject({ + host: 'plugin-host', + user: 'plugin', + password: 'pass', + port: '5432', + }); + }); + }); + + describe('databaseName', () => { + it('auto-generates database name with prefix in database mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseName).toBe('backstage_plugin_catalog'); + }); + + it('uses connection database when specified in database mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + plugin: { + catalog: { + connection: { database: 'custom_db' }, + }, + }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseName).toBe('custom_db'); + }); + + it('uses connection database in schema mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', database: 'shared_db' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseName).toBe('shared_db'); + }); + + it('returns undefined when no database in schema mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseName).toBeUndefined(); + }); + }); + + describe('databaseClientOverrides', () => { + it('sets connection.database when databaseName exists', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseClientOverrides).toEqual({ + connection: { database: 'backstage_plugin_catalog' }, + }); + }); + + it('adds searchPath when in schema mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost', database: 'shared_db' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseClientOverrides).toEqual({ + connection: { database: 'shared_db' }, + searchPath: ['catalog'], + }); + }); + + it('returns empty object when no databaseName in schema mode', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + pluginDivisionMode: 'schema', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.databaseClientOverrides).toEqual({ + searchPath: ['catalog'], + }); + }); + }); + + describe('knexConfig', () => { + it('includes client and connection', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.knexConfig.client).toBe('pg'); + expect(result.knexConfig.connection).toBeDefined(); + }); + + it('includes role when specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + role: 'my_role', + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect((result.knexConfig as any).role).toBe('my_role'); + }); + + it('does not include role when not specified', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect((result.knexConfig as any).role).toBeUndefined(); + }); + + it('includes additionalKnexConfig properties', () => { + const config = new ConfigReader({ + client: 'pg', + connection: { host: 'localhost' }, + knexConfig: { debug: true, pool: { min: 2 } }, + }); + + const result = computePgPluginConfig(config, 'catalog', prefix); + + expect(result.knexConfig.debug).toBe(true); + expect(result.knexConfig.pool).toEqual({ min: 2 }); + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts index 1522222b6f..380ff2bd0c 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts @@ -310,7 +310,7 @@ export interface PgPluginDatabaseConfig { /** The plugin division mode ('database' or 'schema') */ pluginDivisionMode: string; /** The connection configuration */ - connection: Knex.StaticConnectionConfig; + connection: Knex.PgConnectionConfig; /** The database name, if any */ databaseName: string | undefined; /** Database client overrides including schema overrides if applicable */ @@ -394,7 +394,7 @@ export function computePgPluginConfig( // Include base connection if client type has not been overridden ...(clientOverridden ? {} : baseConnection), ...pluginConnection, - } as Knex.StaticConnectionConfig; + } as Knex.PgConnectionConfig; // Database name const connectionDatabaseName = (connection as Knex.ConnectionConfig)