diff --git a/db/index.test.ts b/db/index.test.ts index 18314d3..d89d30e 100644 --- a/db/index.test.ts +++ b/db/index.test.ts @@ -8,24 +8,47 @@ describe("db connection", () => { it("creates and caches the native connection lazily", async () => { const database = { sql: {} }; + const getConnectionString = vi.fn(() => "postgres://app:secret@database.example/test"); const getDatabase = vi.fn(() => database); - vi.doMock("@netlify/database", () => ({ getDatabase })); + vi.doMock("@netlify/database", () => ({ getConnectionString, getDatabase })); const mod = await import("./index"); + expect(getConnectionString).not.toHaveBeenCalled(); expect(getDatabase).not.toHaveBeenCalled(); expect(mod.getDb()).toBe(database); expect(mod.getDb()).toBe(database); + expect(getConnectionString).toHaveBeenCalledOnce(); expect(getDatabase).toHaveBeenCalledOnce(); + expect(getDatabase).toHaveBeenCalledWith(); + }); + + it("adds the username missing from Netlify's local Database URL", async () => { + const database = { sql: {} }; + const getConnectionString = vi.fn(() => "postgres://localhost:5432/postgres"); + const getDatabase = vi.fn(() => database); + vi.doMock("@netlify/database", () => ({ getConnectionString, getDatabase })); + + const mod = await import("./index"); + + expect(mod.getDb()).toBe(database); + expect(mod.getDb()).toBe(database); + expect(getConnectionString).toHaveBeenCalledOnce(); + expect(getDatabase).toHaveBeenCalledOnce(); + expect(getDatabase).toHaveBeenCalledWith({ + connectionString: "postgres://postgres@localhost:5432/postgres", + }); }); it("does not hide a missing Database configuration", async () => { - const getDatabase = vi.fn(() => { + const getConnectionString = vi.fn(() => { throw new Error("Database is not configured"); }); - vi.doMock("@netlify/database", () => ({ getDatabase })); + const getDatabase = vi.fn(); + vi.doMock("@netlify/database", () => ({ getConnectionString, getDatabase })); const mod = await import("./index"); expect(() => mod.getDb()).toThrow("Database is not configured"); + expect(getDatabase).not.toHaveBeenCalled(); }); }); diff --git a/db/index.ts b/db/index.ts index d0dd9b8..ef3503e 100644 --- a/db/index.ts +++ b/db/index.ts @@ -1,11 +1,26 @@ -import { getDatabase, type DatabaseConnection } from "@netlify/database"; +import { getConnectionString, getDatabase, type DatabaseConnection } from "@netlify/database"; let connection: DatabaseConnection | undefined; +const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]); + +function createConnection(): DatabaseConnection { + const connectionString = getConnectionString(); + const url = new URL(connectionString); + + if (!url.username && LOOPBACK_HOSTS.has(url.hostname)) { + // XXX(serhalp): Remove once Netlify's local Database URL includes a username. + url.username = "postgres"; + return getDatabase({ connectionString: url.toString() }); + } + + return getDatabase(); +} + // Keep connection creation lazy. An audit can still run and stream its result // when Database has not been provisioned; only the final save then fails, and // audit-stream already reports that failure separately from the audit itself. export function getDb(): DatabaseConnection { - connection ??= getDatabase(); + connection ??= createConnection(); return connection; }