mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 14:19:11 +03:00
222 lines
6.8 KiB
TypeScript
222 lines
6.8 KiB
TypeScript
import type { ConnectionStringMethod } from './Connect.constants'
|
|
import type { ConnectionStringPooler } from './Connect.types'
|
|
|
|
export const DEFAULT_PORT = '5432'
|
|
export const PASSWORD_PLACEHOLDER = '[YOUR-PASSWORD]'
|
|
export const TOKEN_PASSWORD_PLACEHOLDER = '[YOUR-ACCESS-TOKEN]'
|
|
export const TEMPORARY_ACCESS_POOLER_OPTIONS = 'options=-c%20jit%3dtrue'
|
|
|
|
/** Appends query params to a connection string, joining with `?` or `&` as needed */
|
|
export const appendConnectionStringParams = (uri: string, params: string) =>
|
|
!uri || !params ? uri : `${uri}${uri.includes('?') ? '&' : '?'}${params}`
|
|
|
|
export type ConnectionParams = {
|
|
host: string
|
|
port: string
|
|
user: string
|
|
database: string
|
|
/** Raw query string including the leading `?`, or '' when the URI has none */
|
|
search: string
|
|
}
|
|
|
|
export const resolveConnectionString = ({
|
|
connectionMethod,
|
|
useSharedPooler,
|
|
connectionStringPooler,
|
|
}: {
|
|
connectionMethod: ConnectionStringMethod
|
|
useSharedPooler: boolean
|
|
connectionStringPooler: ConnectionStringPooler | undefined
|
|
}) => {
|
|
if (!connectionStringPooler) return ''
|
|
|
|
if (connectionMethod === 'direct') {
|
|
return connectionStringPooler.direct ?? ''
|
|
}
|
|
|
|
if (connectionMethod === 'session') {
|
|
return connectionStringPooler.sessionShared ?? ''
|
|
}
|
|
|
|
if (useSharedPooler || !connectionStringPooler.transactionDedicated) {
|
|
return connectionStringPooler.transactionShared ?? ''
|
|
}
|
|
|
|
return connectionStringPooler.transactionDedicated ?? ''
|
|
}
|
|
|
|
export const parseConnectionParams = (connectionString: string): ConnectionParams => {
|
|
if (!connectionString) {
|
|
return {
|
|
host: 'hidden',
|
|
port: DEFAULT_PORT,
|
|
user: 'hidden',
|
|
database: 'hidden',
|
|
search: '',
|
|
}
|
|
}
|
|
|
|
try {
|
|
const parsed = new URL(connectionString)
|
|
// The URL parser percent-encodes characters that aren't valid in user-info
|
|
// (e.g. brackets in the self-hosted `postgres.[POOLER_TENANT_ID]` placeholder).
|
|
// Decode so the displayed string matches the literal we wrote.
|
|
const decode = (value: string) => {
|
|
try {
|
|
return decodeURIComponent(value)
|
|
} catch {
|
|
return value
|
|
}
|
|
}
|
|
return {
|
|
host: parsed.hostname || 'hidden',
|
|
port: parsed.port || DEFAULT_PORT,
|
|
user: parsed.username ? decode(parsed.username) : 'hidden',
|
|
database: parsed.pathname?.replace(/^\//, '') || 'hidden',
|
|
search: parsed.search,
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
host: 'hidden',
|
|
port: DEFAULT_PORT,
|
|
user: 'hidden',
|
|
database: 'hidden',
|
|
search: '',
|
|
}
|
|
}
|
|
}
|
|
|
|
export const buildSafeConnectionString = (
|
|
connectionString: string,
|
|
params: ConnectionParams,
|
|
passwordPlaceholder: string = PASSWORD_PLACEHOLDER
|
|
): string => {
|
|
if (!connectionString) return ''
|
|
|
|
return `postgresql://${params.user}:${passwordPlaceholder}@${params.host}:${params.port}/${params.database}${params.search}`
|
|
}
|
|
|
|
export const buildPsqlCommand = (params: ConnectionParams) =>
|
|
params.search
|
|
? // Query params (e.g. sslmode) can't be expressed as psql flags, so fall
|
|
// back to the URI form — psql prompts for the password.
|
|
`psql "postgresql://${params.user}@${params.host}:${params.port}/${params.database}${params.search}"`
|
|
: `psql -h ${params.host} -p ${params.port} -d ${params.database} -U ${params.user}`
|
|
|
|
export const buildJdbcString = (
|
|
params: ConnectionParams,
|
|
passwordPlaceholder: string = PASSWORD_PLACEHOLDER
|
|
) => {
|
|
// pgJDBC (42.7.4+) spells libpq's `sslnegotiation` as `sslNegotiation`
|
|
const extraParams = params.search
|
|
? `&${params.search.slice(1).replace('sslnegotiation=', 'sslNegotiation=')}`
|
|
: ''
|
|
return `jdbc:postgresql://${params.host}:${params.port}/${params.database}?user=${params.user}&password=${passwordPlaceholder}${extraParams}`
|
|
}
|
|
|
|
export const buildConnectionStringWithPassword = (
|
|
connectionString: string,
|
|
password: string
|
|
): string => {
|
|
if (!connectionString || !password) return connectionString
|
|
|
|
const encodedPassword = (() => {
|
|
try {
|
|
return encodeURIComponent(password)
|
|
} catch {
|
|
return password
|
|
}
|
|
})()
|
|
|
|
return connectionString.split(PASSWORD_PLACEHOLDER).join(encodedPassword)
|
|
}
|
|
|
|
export const buildConnectionParameters = (params: ConnectionParams) => [
|
|
{ key: 'host', value: params.host },
|
|
{ key: 'port', value: params.port },
|
|
{ key: 'database', value: params.database },
|
|
{ key: 'user', value: params.user },
|
|
]
|
|
|
|
/**
|
|
* Replaces the Postgres role in a connection username, keeping any pooler
|
|
* suffix (`postgres.projref` → `{role}.projref`).
|
|
*/
|
|
export const replaceConnectionUser = (user: string, role: string): string => {
|
|
if (!user || !role) return user
|
|
const suffixIndex = user.indexOf('.')
|
|
if (suffixIndex === -1) return role
|
|
return `${role}${user.slice(suffixIndex)}`
|
|
}
|
|
|
|
const replaceUserInConnectionString = (uri: string, role: string): string => {
|
|
const match = uri.match(/^(postgres(?:ql)?:\/\/)([^:/@]+)(:[^@]*)?@/)
|
|
if (!match) return uri
|
|
|
|
const [, protocol, rawUser, passwordPart = ''] = match
|
|
let user = rawUser
|
|
try {
|
|
user = decodeURIComponent(rawUser)
|
|
} catch {
|
|
// Keep the raw username when it is not valid percent-encoding.
|
|
}
|
|
|
|
return `${protocol}${replaceConnectionUser(user, role)}${passwordPart}@${uri.slice(match[0].length)}`
|
|
}
|
|
|
|
const hasTemporaryAccessPoolerOption = (uri: string) =>
|
|
uri.includes('jit%3dtrue') || uri.includes('jit=true')
|
|
|
|
export const applyTemporaryAccessToConnectionString = (
|
|
uri: string,
|
|
{
|
|
role,
|
|
addPoolerJitOption = false,
|
|
}: {
|
|
role: string
|
|
addPoolerJitOption?: boolean
|
|
}
|
|
): string => {
|
|
if (!uri || !role) return uri
|
|
|
|
const withRole = replaceUserInConnectionString(uri, role)
|
|
const withToken = withRole.split(PASSWORD_PLACEHOLDER).join(TOKEN_PASSWORD_PLACEHOLDER)
|
|
|
|
if (!addPoolerJitOption || hasTemporaryAccessPoolerOption(withToken)) {
|
|
return withToken
|
|
}
|
|
|
|
return appendConnectionStringParams(withToken, TEMPORARY_ACCESS_POOLER_OPTIONS)
|
|
}
|
|
|
|
export const applyTemporaryAccessToPooler = (
|
|
pooler: ConnectionStringPooler,
|
|
role: string
|
|
): ConnectionStringPooler => {
|
|
const rewrite = (uri: string | undefined, addPoolerJitOption: boolean) =>
|
|
uri ? applyTemporaryAccessToConnectionString(uri, { role, addPoolerJitOption }) : uri
|
|
|
|
return {
|
|
...pooler,
|
|
direct: rewrite(pooler.direct, false),
|
|
transactionShared: rewrite(pooler.transactionShared, true),
|
|
sessionShared: rewrite(pooler.sessionShared, true),
|
|
transactionDedicated: rewrite(pooler.transactionDedicated, false),
|
|
sessionDedicated: rewrite(pooler.sessionDedicated, false),
|
|
}
|
|
}
|
|
|
|
export const shouldAddTemporaryAccessPoolerOption = ({
|
|
connectionMethod,
|
|
useSharedPooler,
|
|
hasDedicatedPooler,
|
|
}: {
|
|
connectionMethod: ConnectionStringMethod
|
|
useSharedPooler: boolean
|
|
hasDedicatedPooler: boolean
|
|
}) => {
|
|
if (connectionMethod === 'direct') return false
|
|
if (connectionMethod === 'session') return true
|
|
return useSharedPooler || !hasDedicatedPooler
|
|
}
|