From be7e4eb48b3aa797ff618669aee81bdbb2c6b87a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 30 Apr 2026 11:34:51 +0200 Subject: [PATCH] cli-module-build: forward user config to embedded Postgres When using the embedded-postgres database client, user-provided connection config (host, port, user, password) is now forwarded to the embedded Postgres instance. Only values that the user hasn't configured are filled in with defaults and injected into the app config, preserving existing behavior when no config is provided. Signed-off-by: Patrik Oldsberg Made-with: Cursor --- .../forward-embedded-postgres-config.md | 5 + .../src/lib/runner/runBackend.test.ts | 117 +++++++++++++++++- .../src/lib/runner/runBackend.ts | 47 +++++-- .../src/lib/runner/startEmbeddedDb.ts | 32 ++++- 4 files changed, 183 insertions(+), 18 deletions(-) create mode 100644 .changeset/forward-embedded-postgres-config.md diff --git a/.changeset/forward-embedded-postgres-config.md b/.changeset/forward-embedded-postgres-config.md new file mode 100644 index 0000000000..cfae71e88c --- /dev/null +++ b/.changeset/forward-embedded-postgres-config.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-module-build': patch +--- + +The embedded Postgres database used during local development now respects user-provided connection configuration. If you configure `host`, `port`, `user`, or `password` under `backend.database.connection` alongside the `embedded-postgres` database client, those values will be forwarded to the embedded Postgres instance. Only values that you have not configured will be filled in with defaults. This makes it possible to run the embedded database on a specific host and port, for example to connect to it externally with `psql`. diff --git a/packages/cli-module-build/src/lib/runner/runBackend.test.ts b/packages/cli-module-build/src/lib/runner/runBackend.test.ts index 4f655b041c..1bf2ef1568 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.test.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.test.ts @@ -88,6 +88,7 @@ describe('runBackend', () => { mockToConfig.mockResolvedValue({ close: jest.fn(), getOptionalString: () => undefined, + getOptionalNumber: () => undefined, }); mockStartEmbeddedDb.mockReset(); }); @@ -180,11 +181,12 @@ describe('runBackend', () => { }); describe('embedded-postgres support', () => { - it('should start embedded DB and inject config when database client is embedded-postgres', async () => { + it('should start embedded DB and inject all defaults when no user connection config is provided', async () => { mockToConfig.mockResolvedValue({ close: jest.fn(), getOptionalString: (key: string) => key === 'backend.database.client' ? 'embedded-postgres' : undefined, + getOptionalNumber: () => undefined, }); mockStartEmbeddedDb.mockResolvedValue({ connection: { @@ -193,14 +195,19 @@ describe('runBackend', () => { password: 'password', port: 5555, }, + defaultedConnection: { + host: 'localhost', + user: 'postgres', + password: 'password', + port: 5555, + }, close: jest.fn(), }); runBackend({ entry: 'src/index' }); await jest.advanceTimersByTimeAsync(100); - expect(mockStartEmbeddedDb).toHaveBeenCalled(); - expect(mockSpawn).toHaveBeenCalled(); + expect(mockStartEmbeddedDb).toHaveBeenCalledWith(undefined); const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< string, string @@ -217,10 +224,112 @@ describe('runBackend', () => { }); }); + it('should forward user-provided connection config and only inject defaults for missing values', async () => { + const configValues: Record = { + 'backend.database.client': 'embedded-postgres', + 'backend.database.connection.host': '0.0.0.0', + 'backend.database.connection.port': 15432, + }; + mockToConfig.mockResolvedValue({ + close: jest.fn(), + getOptionalString: (key: string) => { + const val = configValues[key]; + return typeof val === 'string' ? val : undefined; + }, + getOptionalNumber: (key: string) => { + const val = configValues[key]; + return typeof val === 'number' ? val : undefined; + }, + }); + mockStartEmbeddedDb.mockResolvedValue({ + connection: { + host: '0.0.0.0', + user: 'postgres', + password: 'password', + port: 15432, + }, + defaultedConnection: { + user: 'postgres', + password: 'password', + }, + close: jest.fn(), + }); + + runBackend({ entry: 'src/index' }); + await jest.advanceTimersByTimeAsync(100); + + expect(mockStartEmbeddedDb).toHaveBeenCalledWith({ + host: '0.0.0.0', + port: 15432, + user: undefined, + password: undefined, + }); + const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< + string, + string + >; + const injected = JSON.parse(spawnEnv.APP_CONFIG_backend_database); + expect(injected).toEqual({ + client: 'pg', + connection: { + user: 'postgres', + password: 'password', + }, + }); + }); + + it('should not inject connection overrides when all values are user-provided', async () => { + const configValues: Record = { + 'backend.database.client': 'embedded-postgres', + 'backend.database.connection.host': '0.0.0.0', + 'backend.database.connection.port': 15432, + 'backend.database.connection.user': 'myuser', + 'backend.database.connection.password': 'mypass', + }; + mockToConfig.mockResolvedValue({ + close: jest.fn(), + getOptionalString: (key: string) => { + const val = configValues[key]; + return typeof val === 'string' ? val : undefined; + }, + getOptionalNumber: (key: string) => { + const val = configValues[key]; + return typeof val === 'number' ? val : undefined; + }, + }); + mockStartEmbeddedDb.mockResolvedValue({ + connection: { + host: '0.0.0.0', + user: 'myuser', + password: 'mypass', + port: 15432, + }, + defaultedConnection: {}, + close: jest.fn(), + }); + + runBackend({ entry: 'src/index' }); + await jest.advanceTimersByTimeAsync(100); + + expect(mockStartEmbeddedDb).toHaveBeenCalledWith({ + host: '0.0.0.0', + port: 15432, + user: 'myuser', + password: 'mypass', + }); + const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< + string, + string + >; + const injected = JSON.parse(spawnEnv.APP_CONFIG_backend_database); + expect(injected).toEqual({ client: 'pg' }); + }); + it('should resolve config paths relative to targetDir', async () => { mockToConfig.mockResolvedValue({ close: jest.fn(), getOptionalString: () => undefined, + getOptionalNumber: () => undefined, }); runBackend({ @@ -242,13 +351,13 @@ describe('runBackend', () => { close: jest.fn(), getOptionalString: (key: string) => key === 'backend.database.client' ? 'better-sqlite3' : undefined, + getOptionalNumber: () => undefined, }); runBackend({ entry: 'src/index' }); await jest.advanceTimersByTimeAsync(100); expect(mockStartEmbeddedDb).not.toHaveBeenCalled(); - expect(mockSpawn).toHaveBeenCalled(); const spawnEnv = mockSpawn.mock.calls[0][2]?.env as Record< string, string diff --git a/packages/cli-module-build/src/lib/runner/runBackend.ts b/packages/cli-module-build/src/lib/runner/runBackend.ts index 95dd8784fd..1046235d35 100644 --- a/packages/cli-module-build/src/lib/runner/runBackend.ts +++ b/packages/cli-module-build/src/lib/runner/runBackend.ts @@ -68,16 +68,19 @@ export async function runBackend(options: RunBackendOptions) { let embeddedDb: Awaited> | undefined; - const dbClient = await readDatabaseClient( + const dbConfig = await readDatabaseConfig( options.configPaths, options.targetDir, ); - if (dbClient === 'embedded-postgres') { - embeddedDb = await startEmbeddedDb(); - extraEnv.APP_CONFIG_backend_database = JSON.stringify({ + if (dbConfig?.client === 'embedded-postgres') { + embeddedDb = await startEmbeddedDb(dbConfig.connection); + const overrides: Record = { client: 'pg', - connection: embeddedDb.connection, - }); + }; + if (Object.keys(embeddedDb.defaultedConnection).length > 0) { + overrides.connection = embeddedDb.defaultedConnection; + } + extraEnv.APP_CONFIG_backend_database = JSON.stringify(overrides); } let exiting = false; @@ -221,10 +224,16 @@ export async function runBackend(options: RunBackendOptions) { return () => exitPromise; } -async function readDatabaseClient( +async function readDatabaseConfig( configPaths?: string[], targetDir?: string, -): Promise { +): Promise< + | { + client: string; + connection?: import('./startEmbeddedDb').EmbeddedDbConnectionConfig; + } + | undefined +> { const rootDir = targetPaths.rootDir; const configBaseDir = targetDir ?? rootDir; const source = ConfigSources.default({ @@ -238,7 +247,27 @@ async function readDatabaseClient( const config = await ConfigSources.toConfig(source); try { - return config.getOptionalString('backend.database.client'); + const client = config.getOptionalString('backend.database.client'); + if (!client) { + return undefined; + } + + const host = config.getOptionalString('backend.database.connection.host'); + const port = config.getOptionalNumber('backend.database.connection.port'); + const user = config.getOptionalString('backend.database.connection.user'); + const password = config.getOptionalString( + 'backend.database.connection.password', + ); + + const connection = + host !== undefined || + port !== undefined || + user !== undefined || + password !== undefined + ? { host, port, user, password } + : undefined; + + return { client, connection }; } finally { config.close(); } diff --git a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts index 334f76878e..7c8da74e00 100644 --- a/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts +++ b/packages/cli-module-build/src/lib/runner/startEmbeddedDb.ts @@ -52,7 +52,14 @@ async function cleanStaleDatabases() { ); } -export async function startEmbeddedDb() { +export interface EmbeddedDbConnectionConfig { + host?: string; + port?: number; + user?: string; + password?: string; +} + +export async function startEmbeddedDb(userConfig?: EmbeddedDbConnectionConfig) { console.warn( chalk.yellow( 'WARNING: Using embedded-postgres for local development is experimental and subject to change', @@ -72,10 +79,10 @@ export async function startEmbeddedDb() { await cleanStaleDatabases(); - const host = 'localhost'; - const user = 'postgres'; - const password = 'password'; - const port = await getPortPromise(); + const host = userConfig?.host ?? 'localhost'; + const user = userConfig?.user ?? 'postgres'; + const password = userConfig?.password ?? 'password'; + const port = userConfig?.port ?? (await getPortPromise()); const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), TEMP_DIR_PREFIX)); const pg = new EmbeddedPostgres({ @@ -100,6 +107,20 @@ export async function startEmbeddedDb() { throw error; } + const defaultedConnection: Record = {}; + if (!userConfig?.host) { + defaultedConnection.host = host; + } + if (!userConfig?.user) { + defaultedConnection.user = user; + } + if (!userConfig?.password) { + defaultedConnection.password = password; + } + if (!userConfig?.port) { + defaultedConnection.port = port; + } + return { connection: { host, @@ -107,6 +128,7 @@ export async function startEmbeddedDb() { password, port, }, + defaultedConnection, async close() { await pg.stop(); await fs.remove(tmpDir);