diff --git a/cf/src/index.js b/cf/src/index.js index ffbe7ae..992009b 100644 --- a/cf/src/index.js +++ b/cf/src/index.js @@ -464,8 +464,8 @@ function parseOptions(a, b) { } return { - host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), - port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), + host : Array.isArray(host) ? host : host.split(',').map(x => parseHost(x)[0]), + port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(parseHost(x)[1] || port)), path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, user : user, @@ -501,6 +501,13 @@ function parseOptions(a, b) { } } +function parseHost(x) { + const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(x) + return bracketed + ? [bracketed[1], bracketed[2]] + : x.indexOf(':') !== x.lastIndexOf(':') ? [x] : x.split(':') +} + function tsa(o, url, env) { const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) diff --git a/cjs/src/index.js b/cjs/src/index.js index f09c61c..e6b68f7 100644 --- a/cjs/src/index.js +++ b/cjs/src/index.js @@ -463,8 +463,8 @@ function parseOptions(a, b) { } return { - host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), - port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), + host : Array.isArray(host) ? host : host.split(',').map(x => parseHost(x)[0]), + port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(parseHost(x)[1] || port)), path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, user : user, @@ -500,6 +500,13 @@ function parseOptions(a, b) { } } +function parseHost(x) { + const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(x) + return bracketed + ? [bracketed[1], bracketed[2]] + : x.indexOf(':') !== x.lastIndexOf(':') ? [x] : x.split(':') +} + function tsa(o, url, env) { const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) diff --git a/cjs/tests/index.js b/cjs/tests/index.js index 85d1aa4..08cfaf0 100644 --- a/cjs/tests/index.js +++ b/cjs/tests/index.js @@ -1901,6 +1901,41 @@ t('Multiple hosts', { return [[id1, id2, id1].join(','), result.join(',')] }) +t('Bracketed IPv6 host with port', async() => { + const sql = postgres('postgres://[::1]:5432') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Bracketed IPv6 host uses the default port', async() => { + const sql = postgres('postgres://[2001:db8::1]') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["2001:db8::1"],[5432]]', actual] +}) + +t('Multiple bracketed IPv6 hosts keep their ports', async() => { + const sql = postgres('postgres://[::1]:5432,[2001:db8::2]:5433') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1","2001:db8::2"],[5432,5433]]', actual] +}) + +t('Bare IPv6 host option uses the default port', async() => { + const sql = postgres({ host: '::1' }) + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Multiple hostname ports still parse', async() => { + const sql = postgres('postgres://h1:1,h2:2') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["h1","h2"],[1,2]]', actual] +}) + t('Escaping supports schemas and tables', async() => { await sql`create schema a` await sql`create table a.b (c int)` diff --git a/deno/src/index.js b/deno/src/index.js index b6d23db..12b1bfb 100644 --- a/deno/src/index.js +++ b/deno/src/index.js @@ -464,8 +464,8 @@ function parseOptions(a, b) { } return { - host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), - port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), + host : Array.isArray(host) ? host : host.split(',').map(x => parseHost(x)[0]), + port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(parseHost(x)[1] || port)), path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, user : user, @@ -501,6 +501,13 @@ function parseOptions(a, b) { } } +function parseHost(x) { + const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(x) + return bracketed + ? [bracketed[1], bracketed[2]] + : x.indexOf(':') !== x.lastIndexOf(':') ? [x] : x.split(':') +} + function tsa(o, url, env) { const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) diff --git a/deno/tests/index.js b/deno/tests/index.js index cc2a251..921f943 100644 --- a/deno/tests/index.js +++ b/deno/tests/index.js @@ -1903,6 +1903,41 @@ t('Multiple hosts', { return [[id1, id2, id1].join(','), result.join(',')] }) +t('Bracketed IPv6 host with port', async() => { + const sql = postgres('postgres://[::1]:5432') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Bracketed IPv6 host uses the default port', async() => { + const sql = postgres('postgres://[2001:db8::1]') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["2001:db8::1"],[5432]]', actual] +}) + +t('Multiple bracketed IPv6 hosts keep their ports', async() => { + const sql = postgres('postgres://[::1]:5432,[2001:db8::2]:5433') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1","2001:db8::2"],[5432,5433]]', actual] +}) + +t('Bare IPv6 host option uses the default port', async() => { + const sql = postgres({ host: '::1' }) + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Multiple hostname ports still parse', async() => { + const sql = postgres('postgres://h1:1,h2:2') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["h1","h2"],[1,2]]', actual] +}) + t('Escaping supports schemas and tables', async() => { await sql`create schema a` await sql`create table a.b (c int)` diff --git a/src/index.js b/src/index.js index c7fba3d..09a0192 100644 --- a/src/index.js +++ b/src/index.js @@ -463,8 +463,8 @@ function parseOptions(a, b) { } return { - host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), - port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), + host : Array.isArray(host) ? host : host.split(',').map(x => parseHost(x)[0]), + port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(parseHost(x)[1] || port)), path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, user : user, @@ -500,6 +500,13 @@ function parseOptions(a, b) { } } +function parseHost(x) { + const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(x) + return bracketed + ? [bracketed[1], bracketed[2]] + : x.indexOf(':') !== x.lastIndexOf(':') ? [x] : x.split(':') +} + function tsa(o, url, env) { const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) diff --git a/tests/index.js b/tests/index.js index 845c6ce..b20103c 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1982,6 +1982,41 @@ t('Multiple hosts', { return [[id1, id2, id1].join(','), result.join(',')] }) +t('Bracketed IPv6 host with port', async() => { + const sql = postgres('postgres://[::1]:5432') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Bracketed IPv6 host uses the default port', async() => { + const sql = postgres('postgres://[2001:db8::1]') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["2001:db8::1"],[5432]]', actual] +}) + +t('Multiple bracketed IPv6 hosts keep their ports', async() => { + const sql = postgres('postgres://[::1]:5432,[2001:db8::2]:5433') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1","2001:db8::2"],[5432,5433]]', actual] +}) + +t('Bare IPv6 host option uses the default port', async() => { + const sql = postgres({ host: '::1' }) + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["::1"],[5432]]', actual] +}) + +t('Multiple hostname ports still parse', async() => { + const sql = postgres('postgres://h1:1,h2:2') + const actual = JSON.stringify([sql.options.host, sql.options.port]) + await sql.end() + return ['[["h1","h2"],[1,2]]', actual] +}) + t('Escaping supports schemas and tables', async() => { await sql`create schema a` await sql`create table a.b (c int)`