Skip to content
Open
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
48 changes: 47 additions & 1 deletion packages/3-extensions/postgres/src/runtime/binding.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InternalError } from '@internal/utils/internal-error';
import type { Client, Pool } from 'pg';
import { postgresError } from '../errors';
import { userInfo } from 'node:os';

export const isPgPool = (pg: Pool | Client): pg is Pool =>
'totalCount' in pg && 'idleCount' in pg && 'waitingCount' in pg;
Expand Down Expand Up @@ -36,6 +37,49 @@ type PostgresBindingFields = {
readonly pg?: Pool | Client;
};

function normalizePostgresUrlIfNeeded(trimmed: string): string {
// Fast-path: nothing to do if nothing missing
try {
const parsed = new URL(trimmed);
let changed = false;

// If username is not present in the URL *and* the original string did not
// include an explicit userinfo (`@`), default to the OS username. This
// mirrors libpq/psql behaviour where an omitted user falls back to the
// current user. We avoid overriding an explicitly empty user (`postgresql://@host/...`).
const authorityStart = trimmed.indexOf('//') + 2;
const authorityTail = trimmed.slice(authorityStart);
const authorityEnd = authorityTail.search(/[/?#]/);
const authority =
authorityEnd === -1 ? authorityTail : authorityTail.slice(0, authorityEnd);

if (!parsed.username && !authority.includes('@')) {
try {
const defaultUser = userInfo().username;
if (defaultUser) {
parsed.username = defaultUser;
changed = true;
}
} catch {
// If userInfo fails for any reason, skip defaulting the username.
}
}

// If no hostname is provided, fall back to localhost so node-postgres
// connects over TCP to the local server (common local-dev setup).
// This also prevents printing `undefined` for the host in diagnostics.
if (!parsed.hostname) {
parsed.hostname = 'localhost';
changed = true;
}

return changed ? parsed.toString() : trimmed;
} catch {
// If parsing fails here, let the caller handle the parse error path.
return trimmed;
}
}

function validatePostgresUrl(url: string): string {
const trimmed = url.trim();
if (trimmed.length === 0) {
Expand All @@ -61,7 +105,9 @@ function validatePostgresUrl(url: string): string {
);
}

return trimmed;
// Normalize missing user/host to sensible defaults so `postgresql:///db`
// and `postgresql://localhost/db` behave like libpq/psql on local dev.
return normalizePostgresUrlIfNeeded(trimmed);
}

export function resolvePostgresBinding(options: PostgresBindingInput): PostgresBinding {
Expand Down