@supabase/pg-delta 1.0.0-alpha.15 → 1.0.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -45,8 +45,38 @@ interface CreatePoolOptions extends Partial<PoolConfig> {
|
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Create a Pool with custom type handlers and optional event listeners.
|
|
48
|
+
*
|
|
49
|
+
* `connectionString` may be `undefined` when the caller needs pg to rely on
|
|
50
|
+
* explicit `host`/`port`/`user`/... fields from `options` instead — notably
|
|
51
|
+
* the bracketed-IPv6 workaround in {@link poolConfigFromUrl}, where passing
|
|
52
|
+
* the connection string would cause `pg-connection-string` to re-inject the
|
|
53
|
+
* bracketed host that breaks `getaddrinfo`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createPool(connectionString: string | undefined, options?: CreatePoolOptions): Pool;
|
|
56
|
+
/**
|
|
57
|
+
* Build a pg {@link PoolConfig} from a cleaned connection URL.
|
|
58
|
+
*
|
|
59
|
+
* For most URLs this just returns `{ connectionString }` and pg does its
|
|
60
|
+
* normal parsing. But for URLs whose hostname is a bracketed IPv6 literal
|
|
61
|
+
* (e.g. `postgresql://user@[::1]:5432/db`, as produced by
|
|
62
|
+
* {@link normalizeConnectionUrl}), we expand the URL into explicit
|
|
63
|
+
* `host`/`port`/`user`/`password`/`database` fields with a **bare** IPv6
|
|
64
|
+
* host — no brackets.
|
|
65
|
+
*
|
|
66
|
+
* This works around a `pg-connection-string` quirk: its parser sets
|
|
67
|
+
* `config.host` to the WHATWG `URL.hostname`, which keeps the surrounding
|
|
68
|
+
* `[...]` for IPv6 literals. That bracketed value is then passed verbatim to
|
|
69
|
+
* `getaddrinfo`, which rejects it with `ENOTFOUND`. Since
|
|
70
|
+
* `pg`'s connection-parameters module does
|
|
71
|
+
* `Object.assign({}, config, parse(connectionString))`, any `host` we pass
|
|
72
|
+
* alongside `connectionString` gets clobbered — so we drop `connectionString`
|
|
73
|
+
* entirely on this path and hand pg the parsed fields directly.
|
|
74
|
+
*
|
|
75
|
+
* Remaining query parameters (e.g. `application_name`, `options`,
|
|
76
|
+
* `connect_timeout`) are forwarded as top-level config keys, mirroring how
|
|
77
|
+
* `pg-connection-string` would normally surface them.
|
|
48
78
|
*/
|
|
49
|
-
export declare function
|
|
79
|
+
export declare function poolConfigFromUrl(cleanedUrl: string): PoolConfig;
|
|
50
80
|
/**
|
|
51
81
|
* End a pool and wait for all client sockets to fully close.
|
|
52
82
|
*
|
|
@@ -182,11 +182,17 @@ export async function connectWithRetry(opts) {
|
|
|
182
182
|
}
|
|
183
183
|
/**
|
|
184
184
|
* Create a Pool with custom type handlers and optional event listeners.
|
|
185
|
+
*
|
|
186
|
+
* `connectionString` may be `undefined` when the caller needs pg to rely on
|
|
187
|
+
* explicit `host`/`port`/`user`/... fields from `options` instead — notably
|
|
188
|
+
* the bracketed-IPv6 workaround in {@link poolConfigFromUrl}, where passing
|
|
189
|
+
* the connection string would cause `pg-connection-string` to re-inject the
|
|
190
|
+
* bracketed host that breaks `getaddrinfo`.
|
|
185
191
|
*/
|
|
186
192
|
export function createPool(connectionString, options) {
|
|
187
193
|
const { onConnect, onError, onAcquire, onRemove, ...config } = options ?? {};
|
|
188
194
|
const pool = new Pool({
|
|
189
|
-
connectionString,
|
|
195
|
+
...(connectionString ? { connectionString } : {}),
|
|
190
196
|
max: DEFAULT_POOL_MAX,
|
|
191
197
|
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
192
198
|
...config,
|
|
@@ -242,6 +248,51 @@ export function createPool(connectionString, options) {
|
|
|
242
248
|
pool.on("remove", onRemove);
|
|
243
249
|
return pool;
|
|
244
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Build a pg {@link PoolConfig} from a cleaned connection URL.
|
|
253
|
+
*
|
|
254
|
+
* For most URLs this just returns `{ connectionString }` and pg does its
|
|
255
|
+
* normal parsing. But for URLs whose hostname is a bracketed IPv6 literal
|
|
256
|
+
* (e.g. `postgresql://user@[::1]:5432/db`, as produced by
|
|
257
|
+
* {@link normalizeConnectionUrl}), we expand the URL into explicit
|
|
258
|
+
* `host`/`port`/`user`/`password`/`database` fields with a **bare** IPv6
|
|
259
|
+
* host — no brackets.
|
|
260
|
+
*
|
|
261
|
+
* This works around a `pg-connection-string` quirk: its parser sets
|
|
262
|
+
* `config.host` to the WHATWG `URL.hostname`, which keeps the surrounding
|
|
263
|
+
* `[...]` for IPv6 literals. That bracketed value is then passed verbatim to
|
|
264
|
+
* `getaddrinfo`, which rejects it with `ENOTFOUND`. Since
|
|
265
|
+
* `pg`'s connection-parameters module does
|
|
266
|
+
* `Object.assign({}, config, parse(connectionString))`, any `host` we pass
|
|
267
|
+
* alongside `connectionString` gets clobbered — so we drop `connectionString`
|
|
268
|
+
* entirely on this path and hand pg the parsed fields directly.
|
|
269
|
+
*
|
|
270
|
+
* Remaining query parameters (e.g. `application_name`, `options`,
|
|
271
|
+
* `connect_timeout`) are forwarded as top-level config keys, mirroring how
|
|
272
|
+
* `pg-connection-string` would normally surface them.
|
|
273
|
+
*/
|
|
274
|
+
export function poolConfigFromUrl(cleanedUrl) {
|
|
275
|
+
const urlObj = new URL(cleanedUrl);
|
|
276
|
+
if (!urlObj.hostname.startsWith("[")) {
|
|
277
|
+
return { connectionString: cleanedUrl };
|
|
278
|
+
}
|
|
279
|
+
const config = {
|
|
280
|
+
host: urlObj.hostname.slice(1, -1),
|
|
281
|
+
};
|
|
282
|
+
if (urlObj.port)
|
|
283
|
+
config.port = Number(urlObj.port);
|
|
284
|
+
if (urlObj.username)
|
|
285
|
+
config.user = decodeURIComponent(urlObj.username);
|
|
286
|
+
if (urlObj.password)
|
|
287
|
+
config.password = decodeURIComponent(urlObj.password);
|
|
288
|
+
if (urlObj.pathname.length > 1) {
|
|
289
|
+
config.database = decodeURIComponent(urlObj.pathname.slice(1));
|
|
290
|
+
}
|
|
291
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
292
|
+
config[key] = value;
|
|
293
|
+
}
|
|
294
|
+
return config;
|
|
295
|
+
}
|
|
245
296
|
/**
|
|
246
297
|
* End a pool and wait for all client sockets to fully close.
|
|
247
298
|
*
|
|
@@ -269,7 +320,11 @@ export async function createManagedPool(url, options) {
|
|
|
269
320
|
// Non-IPv6 hosts are returned unchanged.
|
|
270
321
|
const normalizedUrl = normalizeConnectionUrl(url);
|
|
271
322
|
const sslConfig = await parseSslConfig(normalizedUrl, options?.label ?? "target");
|
|
272
|
-
|
|
323
|
+
// Expand bracketed-IPv6 URLs into explicit pg fields so the brackets never
|
|
324
|
+
// reach `getaddrinfo` — see `poolConfigFromUrl` for the full rationale.
|
|
325
|
+
const connectionConfig = poolConfigFromUrl(sslConfig.cleanedUrl);
|
|
326
|
+
const pool = createPool(connectionConfig.connectionString, {
|
|
327
|
+
...connectionConfig,
|
|
273
328
|
...(sslConfig.ssl !== undefined ? { ssl: sslConfig.ssl } : {}),
|
|
274
329
|
onError: (err) => {
|
|
275
330
|
if (err.code !== "57P01") {
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
connectWithRetry,
|
|
4
4
|
isRetryableConnectError,
|
|
5
|
+
poolConfigFromUrl,
|
|
5
6
|
} from "./postgres-config.ts";
|
|
6
7
|
|
|
7
8
|
function makeError(message: string, code?: string): Error {
|
|
@@ -239,3 +240,97 @@ describe("connectWithRetry", () => {
|
|
|
239
240
|
expect(attempts).toBe(1);
|
|
240
241
|
});
|
|
241
242
|
});
|
|
243
|
+
|
|
244
|
+
describe("poolConfigFromUrl", () => {
|
|
245
|
+
describe("non-IPv6 URLs pass through as connectionString", () => {
|
|
246
|
+
test("DNS hostname", () => {
|
|
247
|
+
const url = "postgresql://user:pass@db.example.com:5432/mydb";
|
|
248
|
+
expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("IPv4 host", () => {
|
|
252
|
+
const url = "postgresql://user:pass@127.0.0.1:5432/mydb";
|
|
253
|
+
expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("DNS hostname with query params", () => {
|
|
257
|
+
const url =
|
|
258
|
+
"postgresql://user:pass@db.example.com:5432/mydb?application_name=test";
|
|
259
|
+
expect(poolConfigFromUrl(url)).toEqual({ connectionString: url });
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("bracketed IPv6 URLs expand to explicit fields with no brackets", () => {
|
|
264
|
+
test("full 8-group IPv6 — host has no brackets and no connectionString", () => {
|
|
265
|
+
const url =
|
|
266
|
+
"postgresql://user:pass@[2600:1f16:1cd0:3340:f92e:f4cb:7a52:10a1]:5432/mydb";
|
|
267
|
+
const config = poolConfigFromUrl(url);
|
|
268
|
+
expect(config.connectionString).toBeUndefined();
|
|
269
|
+
expect(config.host).toBe("2600:1f16:1cd0:3340:f92e:f4cb:7a52:10a1");
|
|
270
|
+
expect(config.port).toBe(5432);
|
|
271
|
+
expect(config.user).toBe("user");
|
|
272
|
+
expect(config.password).toBe("pass");
|
|
273
|
+
expect(config.database).toBe("mydb");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("compressed ::1 form", () => {
|
|
277
|
+
const url = "postgresql://user:pass@[::1]:5432/mydb";
|
|
278
|
+
const config = poolConfigFromUrl(url);
|
|
279
|
+
expect(config.host).toBe("::1");
|
|
280
|
+
expect(config.port).toBe(5432);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("host bracket strip survives percent-decoded username/password", () => {
|
|
284
|
+
const url = "postgresql://user:p%40ss%2Fword@[::1]:5432/mydb";
|
|
285
|
+
const config = poolConfigFromUrl(url);
|
|
286
|
+
expect(config.host).toBe("::1");
|
|
287
|
+
expect(config.user).toBe("user");
|
|
288
|
+
expect(config.password).toBe("p@ss/word");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("works without port", () => {
|
|
292
|
+
const url = "postgresql://user:pass@[::1]/mydb";
|
|
293
|
+
const config = poolConfigFromUrl(url);
|
|
294
|
+
expect(config.host).toBe("::1");
|
|
295
|
+
expect(config.port).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("works without database (pathname='/')", () => {
|
|
299
|
+
const url = "postgresql://user:pass@[::1]:5432/";
|
|
300
|
+
const config = poolConfigFromUrl(url);
|
|
301
|
+
expect(config.host).toBe("::1");
|
|
302
|
+
expect(config.database).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("works without userinfo", () => {
|
|
306
|
+
const url = "postgresql://[::1]:5432/mydb";
|
|
307
|
+
const config = poolConfigFromUrl(url);
|
|
308
|
+
expect(config.host).toBe("::1");
|
|
309
|
+
expect(config.user).toBeUndefined();
|
|
310
|
+
expect(config.password).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("query params are forwarded as top-level config keys", () => {
|
|
314
|
+
const url =
|
|
315
|
+
"postgresql://user:pass@[::1]:5432/mydb?application_name=pgdelta&connect_timeout=5";
|
|
316
|
+
const config = poolConfigFromUrl(url) as unknown as Record<
|
|
317
|
+
string,
|
|
318
|
+
unknown
|
|
319
|
+
>;
|
|
320
|
+
expect(config.application_name).toBe("pgdelta");
|
|
321
|
+
expect(config.connect_timeout).toBe("5");
|
|
322
|
+
expect(config.host).toBe("::1");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("IPv4-mapped IPv6 is stripped of brackets (WHATWG canonicalisation is fine)", () => {
|
|
326
|
+
// WHATWG URL canonicalises `::ffff:192.0.2.1` to `::ffff:c000:201`;
|
|
327
|
+
// either form resolves to the same IPv6 address, and the point of this
|
|
328
|
+
// test is purely that no brackets escape to pg.
|
|
329
|
+
const url = "postgresql://user:pass@[::ffff:192.0.2.1]:5432/mydb";
|
|
330
|
+
const config = poolConfigFromUrl(url);
|
|
331
|
+
expect(config.host).not.toContain("[");
|
|
332
|
+
expect(config.host).not.toContain("]");
|
|
333
|
+
expect(config.host).toBe("::ffff:c000:201");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -225,14 +225,20 @@ interface CreatePoolOptions extends Partial<PoolConfig> {
|
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
227
|
* Create a Pool with custom type handlers and optional event listeners.
|
|
228
|
+
*
|
|
229
|
+
* `connectionString` may be `undefined` when the caller needs pg to rely on
|
|
230
|
+
* explicit `host`/`port`/`user`/... fields from `options` instead — notably
|
|
231
|
+
* the bracketed-IPv6 workaround in {@link poolConfigFromUrl}, where passing
|
|
232
|
+
* the connection string would cause `pg-connection-string` to re-inject the
|
|
233
|
+
* bracketed host that breaks `getaddrinfo`.
|
|
228
234
|
*/
|
|
229
235
|
export function createPool(
|
|
230
|
-
connectionString: string,
|
|
236
|
+
connectionString: string | undefined,
|
|
231
237
|
options?: CreatePoolOptions,
|
|
232
238
|
): Pool {
|
|
233
239
|
const { onConnect, onError, onAcquire, onRemove, ...config } = options ?? {};
|
|
234
240
|
const pool = new Pool({
|
|
235
|
-
connectionString,
|
|
241
|
+
...(connectionString ? { connectionString } : {}),
|
|
236
242
|
max: DEFAULT_POOL_MAX,
|
|
237
243
|
connectionTimeoutMillis: DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
238
244
|
...config,
|
|
@@ -301,6 +307,50 @@ export function createPool(
|
|
|
301
307
|
return pool;
|
|
302
308
|
}
|
|
303
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Build a pg {@link PoolConfig} from a cleaned connection URL.
|
|
312
|
+
*
|
|
313
|
+
* For most URLs this just returns `{ connectionString }` and pg does its
|
|
314
|
+
* normal parsing. But for URLs whose hostname is a bracketed IPv6 literal
|
|
315
|
+
* (e.g. `postgresql://user@[::1]:5432/db`, as produced by
|
|
316
|
+
* {@link normalizeConnectionUrl}), we expand the URL into explicit
|
|
317
|
+
* `host`/`port`/`user`/`password`/`database` fields with a **bare** IPv6
|
|
318
|
+
* host — no brackets.
|
|
319
|
+
*
|
|
320
|
+
* This works around a `pg-connection-string` quirk: its parser sets
|
|
321
|
+
* `config.host` to the WHATWG `URL.hostname`, which keeps the surrounding
|
|
322
|
+
* `[...]` for IPv6 literals. That bracketed value is then passed verbatim to
|
|
323
|
+
* `getaddrinfo`, which rejects it with `ENOTFOUND`. Since
|
|
324
|
+
* `pg`'s connection-parameters module does
|
|
325
|
+
* `Object.assign({}, config, parse(connectionString))`, any `host` we pass
|
|
326
|
+
* alongside `connectionString` gets clobbered — so we drop `connectionString`
|
|
327
|
+
* entirely on this path and hand pg the parsed fields directly.
|
|
328
|
+
*
|
|
329
|
+
* Remaining query parameters (e.g. `application_name`, `options`,
|
|
330
|
+
* `connect_timeout`) are forwarded as top-level config keys, mirroring how
|
|
331
|
+
* `pg-connection-string` would normally surface them.
|
|
332
|
+
*/
|
|
333
|
+
export function poolConfigFromUrl(cleanedUrl: string): PoolConfig {
|
|
334
|
+
const urlObj = new URL(cleanedUrl);
|
|
335
|
+
if (!urlObj.hostname.startsWith("[")) {
|
|
336
|
+
return { connectionString: cleanedUrl };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const config: Record<string, unknown> = {
|
|
340
|
+
host: urlObj.hostname.slice(1, -1),
|
|
341
|
+
};
|
|
342
|
+
if (urlObj.port) config.port = Number(urlObj.port);
|
|
343
|
+
if (urlObj.username) config.user = decodeURIComponent(urlObj.username);
|
|
344
|
+
if (urlObj.password) config.password = decodeURIComponent(urlObj.password);
|
|
345
|
+
if (urlObj.pathname.length > 1) {
|
|
346
|
+
config.database = decodeURIComponent(urlObj.pathname.slice(1));
|
|
347
|
+
}
|
|
348
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
349
|
+
config[key] = value;
|
|
350
|
+
}
|
|
351
|
+
return config as PoolConfig;
|
|
352
|
+
}
|
|
353
|
+
|
|
304
354
|
/**
|
|
305
355
|
* End a pool and wait for all client sockets to fully close.
|
|
306
356
|
*
|
|
@@ -334,7 +384,11 @@ export async function createManagedPool(
|
|
|
334
384
|
normalizedUrl,
|
|
335
385
|
options?.label ?? "target",
|
|
336
386
|
);
|
|
337
|
-
|
|
387
|
+
// Expand bracketed-IPv6 URLs into explicit pg fields so the brackets never
|
|
388
|
+
// reach `getaddrinfo` — see `poolConfigFromUrl` for the full rationale.
|
|
389
|
+
const connectionConfig = poolConfigFromUrl(sslConfig.cleanedUrl);
|
|
390
|
+
const pool = createPool(connectionConfig.connectionString, {
|
|
391
|
+
...connectionConfig,
|
|
338
392
|
...(sslConfig.ssl !== undefined ? { ssl: sslConfig.ssl } : {}),
|
|
339
393
|
onError: (err: Error & { code?: string }) => {
|
|
340
394
|
if (err.code !== "57P01") {
|