Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ee/packages/den-db/src/mysql-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ type ParsedMySqlConfig = {
user: string
password: string
database: string
// Unix socket path for the MySQL connection (e.g. Cloud SQL's
// `/cloudsql/<project>:<region>:<instance>`). When set, `mysql2` ignores
// `host`/`port` and connects via the socket.
socketPath?: string
ssl?: {
rejectUnauthorized: boolean
}
Expand All @@ -29,6 +33,14 @@ function readSslSettings(parsed: URL) {
return { rejectUnauthorized }
}

function readSocketPath(parsed: URL): string | undefined {
const value =
parsed.searchParams.get("socketPath")?.trim() ||
parsed.searchParams.get("socket")?.trim() ||
""
return value || undefined
}

export function parseMySqlConnectionConfig(databaseUrl: string): ParsedMySqlConfig {
const parsed = new URL(databaseUrl)
const database = parsed.pathname.replace(/^\//, "")
Expand All @@ -37,12 +49,15 @@ export function parseMySqlConnectionConfig(databaseUrl: string): ParsedMySqlConf
throw new Error("DATABASE_URL must include host, username, and database for mysql mode")
}

const socketPath = readSocketPath(parsed)

return {
host: parsed.hostname,
port: Number(parsed.port || "3306"),
user: decodeURIComponent(parsed.username),
password: decodeURIComponent(parsed.password),
database,
...(socketPath ? { socketPath } : {}),
ssl: readSslSettings(parsed),
}
}
84 changes: 84 additions & 0 deletions ee/packages/den-db/test/mysql-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test"

import { parseMySqlConnectionConfig } from "../src/mysql-config.ts"

describe("parseMySqlConnectionConfig", () => {
test("parses a basic TCP connection URL", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@127.0.0.1:3306/openwork_den",
)
expect(config).toEqual({
host: "127.0.0.1",
port: 3306,
user: "root",
password: "password",
database: "openwork_den",
})
})

test("defaults the port to 3306 when missing", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@db.internal/openwork_den",
)
expect(config.port).toBe(3306)
})

test("decodes percent-encoded username and password", () => {
const config = parseMySqlConnectionConfig(
"mysql://us%40er:p%40ss%2Fword@host/db",
)
expect(config.user).toBe("us@er")
expect(config.password).toBe("p@ss/word")
})

test("populates socketPath from ?socketPath query parameter (Cloud SQL)", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@localhost/openwork_den?socketPath=/cloudsql/project:region:instance",
)
expect(config.socketPath).toBe("/cloudsql/project:region:instance")
// host/port still present; mysql2 ignores them when socketPath is set.
expect(config.host).toBe("localhost")
expect(config.port).toBe(3306)
})

test("accepts the ?socket alias", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@localhost/openwork_den?socket=/var/run/mysqld/mysqld.sock",
)
expect(config.socketPath).toBe("/var/run/mysqld/mysqld.sock")
})

test("prefers ?socketPath over ?socket when both are present", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@localhost/openwork_den?socketPath=/a&socket=/b",
)
expect(config.socketPath).toBe("/a")
})

test("omits socketPath when neither parameter is present", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@127.0.0.1:3306/openwork_den",
)
expect(config).not.toHaveProperty("socketPath")
})

test("treats an empty ?socketPath as absent", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@127.0.0.1:3306/openwork_den?socketPath=",
)
expect(config).not.toHaveProperty("socketPath")
})

test("retains SSL settings alongside socketPath", () => {
const config = parseMySqlConnectionConfig(
"mysql://root:password@localhost/openwork_den?socketPath=/cloudsql/x&sslmode=require",
)
expect(config.socketPath).toBe("/cloudsql/x")
expect(config.ssl).toEqual({ rejectUnauthorized: true })
})

test("throws when host, username, or database is missing", () => {
expect(() => parseMySqlConnectionConfig("mysql://root@/db")).toThrow()
expect(() => parseMySqlConnectionConfig("mysql://root@host/")).toThrow()
})
})