@steadybrew/postgresjs 0.0.0-stage → 4.0.0-rc1

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.
@@ -0,0 +1,53 @@
1
+ export class PostgresError extends Error {
2
+ constructor(x) {
3
+ super(x.message)
4
+ this.name = this.constructor.name
5
+ Object.assign(this, x)
6
+ }
7
+ }
8
+
9
+ export const Errors = {
10
+ connection,
11
+ postgres,
12
+ generic,
13
+ notSupported
14
+ }
15
+
16
+ function connection(x, options, socket) {
17
+ const { host, port } = socket || options
18
+ const error = Object.assign(
19
+ new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))),
20
+ {
21
+ code: x,
22
+ errno: x,
23
+ address: options.path || host
24
+ }, options.path ? {} : { port: port }
25
+ )
26
+ Error.captureStackTrace(error, connection)
27
+ return error
28
+ }
29
+
30
+ function postgres(x) {
31
+ const error = new PostgresError(x)
32
+ Error.captureStackTrace(error, postgres)
33
+ return error
34
+ }
35
+
36
+ function generic(code, message) {
37
+ const error = Object.assign(new Error(code + ': ' + message), { code })
38
+ Error.captureStackTrace(error, generic)
39
+ return error
40
+ }
41
+
42
+ /* c8 ignore next 10 */
43
+ function notSupported(x) {
44
+ const error = Object.assign(
45
+ new Error(x + ' (B) is not supported'),
46
+ {
47
+ code: 'MESSAGE_NOT_SUPPORTED',
48
+ name: x
49
+ }
50
+ )
51
+ Error.captureStackTrace(error, notSupported)
52
+ return error
53
+ }
@@ -0,0 +1,648 @@
1
+ import { process } from '../polyfills.js'
2
+ import { os } from '../polyfills.js'
3
+ import { fs } from '../polyfills.js'
4
+
5
+ import {
6
+ mergeUserTypes,
7
+ addDefaultArrayTypes,
8
+ inferType,
9
+ Parameter,
10
+ Identifier,
11
+ Builder,
12
+ toPascal,
13
+ pascal,
14
+ toCamel,
15
+ camel,
16
+ toKebab,
17
+ kebab,
18
+ fromPascal,
19
+ fromCamel,
20
+ fromKebab
21
+ } from './types.js'
22
+
23
+ import Connection from './connection.js'
24
+ import { Query, CLOSE } from './query.js'
25
+ import Queue from './queue.js'
26
+ import { Errors, PostgresError } from './errors.js'
27
+ import Subscribe from './subscribe.js'
28
+ import largeObject from './large.js'
29
+
30
+ Object.assign(Postgres, {
31
+ PostgresError,
32
+ toPascal,
33
+ pascal,
34
+ toCamel,
35
+ camel,
36
+ toKebab,
37
+ kebab,
38
+ fromPascal,
39
+ fromCamel,
40
+ fromKebab,
41
+ BigInt: {
42
+ to: 20,
43
+ from: [20],
44
+ parse: x => BigInt(x), // eslint-disable-line
45
+ serialize: x => x.toString()
46
+ }
47
+ })
48
+
49
+ export default Postgres
50
+
51
+ function Postgres(a, b) {
52
+ const options = parseOptions(a, b)
53
+ , subscribe = options.no_subscribe || Subscribe(Postgres, { ...options })
54
+
55
+ let ending = false
56
+
57
+ const queries = Queue()
58
+ , connecting = Queue()
59
+ , reserved = Queue()
60
+ , closed = Queue()
61
+ , ended = Queue()
62
+ , open = Queue()
63
+ , busy = Queue()
64
+ , full = Queue()
65
+ , queues = { connecting, reserved, closed, ended, open, busy, full }
66
+
67
+ const connections = [...Array(options.max)].map(() => Connection(options, queues, { onopen, onend, onclose, ondrain }))
68
+
69
+ const sql = Sql(handler)
70
+
71
+ Object.assign(sql, {
72
+ get parameters() { return options.parameters },
73
+ largeObject: largeObject.bind(null, sql),
74
+ subscribe,
75
+ CLOSE,
76
+ END: CLOSE,
77
+ PostgresError,
78
+ options,
79
+ reserve,
80
+ listen,
81
+ begin,
82
+ close,
83
+ end
84
+ })
85
+
86
+ return sql
87
+
88
+ function Sql(handler) {
89
+ handler.debug = options.debug
90
+
91
+ Object.entries(options.types).reduce((acc, [name, type]) => {
92
+ acc[name] = (x) => new Parameter(x, type.to)
93
+ return acc
94
+ }, typed)
95
+
96
+ Object.assign(sql, {
97
+ types: typed,
98
+ typed,
99
+ unsafe,
100
+ notify,
101
+ array,
102
+ json,
103
+ file
104
+ })
105
+
106
+ return sql
107
+
108
+ function typed(value, type) {
109
+ return new Parameter(value, type)
110
+ }
111
+
112
+ function sql(strings, ...args) {
113
+ const query = strings && Array.isArray(strings.raw)
114
+ ? new Query(strings, args, handler, cancel)
115
+ : typeof strings === 'string' && !args.length
116
+ ? new Identifier(options.transform.column.to ? options.transform.column.to(strings) : strings)
117
+ : new Builder(strings, args)
118
+ return query
119
+ }
120
+
121
+ function unsafe(string, args = [], options = {}) {
122
+ arguments.length === 2 && !Array.isArray(args) && (options = args, args = [])
123
+ const query = new Query([string], args, handler, cancel, {
124
+ prepare: false,
125
+ ...options,
126
+ simple: 'simple' in options ? options.simple : args.length === 0
127
+ })
128
+ return query
129
+ }
130
+
131
+ function file(path, args = [], options = {}) {
132
+ arguments.length === 2 && !Array.isArray(args) && (options = args, args = [])
133
+ const query = new Query([], args, (query) => {
134
+ fs.readFile(path, 'utf8', (err, string) => {
135
+ if (err)
136
+ return query.reject(err)
137
+
138
+ query.strings = [string]
139
+ handler(query)
140
+ })
141
+ }, cancel, {
142
+ ...options,
143
+ simple: 'simple' in options ? options.simple : args.length === 0
144
+ })
145
+ return query
146
+ }
147
+ }
148
+
149
+ async function listen(name, fn, onlisten) {
150
+ const listener = { fn, onlisten }
151
+
152
+ const sql = listen.sql || (listen.sql = Postgres({
153
+ ...options,
154
+ max: 1,
155
+ idle_timeout: null,
156
+ max_lifetime: null,
157
+ fetch_types: false,
158
+ onclose() {
159
+ Object.entries(listen.channels).forEach(([name, { listeners }]) => {
160
+ delete listen.channels[name]
161
+ Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ })))
162
+ })
163
+ },
164
+ onnotify(c, x) {
165
+ c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x))
166
+ }
167
+ }))
168
+
169
+ const channels = listen.channels || (listen.channels = {})
170
+ , exists = name in channels
171
+
172
+ if (exists) {
173
+ channels[name].listeners.push(listener)
174
+ const result = await channels[name].result
175
+ listener.onlisten && listener.onlisten()
176
+ return { state: result.state, unlisten }
177
+ }
178
+
179
+ channels[name] = { result: sql`listen ${
180
+ sql.unsafe('"' + name.replace(/"/g, '""') + '"')
181
+ }`, listeners: [listener] }
182
+ const result = await channels[name].result
183
+ listener.onlisten && listener.onlisten()
184
+ return { state: result.state, unlisten }
185
+
186
+ async function unlisten() {
187
+ if (name in channels === false)
188
+ return
189
+
190
+ channels[name].listeners = channels[name].listeners.filter(x => x !== listener)
191
+ if (channels[name].listeners.length)
192
+ return
193
+
194
+ delete channels[name]
195
+ return sql`unlisten ${
196
+ sql.unsafe('"' + name.replace(/"/g, '""') + '"')
197
+ }`
198
+ }
199
+ }
200
+
201
+ async function notify(channel, payload) {
202
+ return await sql`select pg_notify(${ channel }, ${ '' + payload })`
203
+ }
204
+
205
+ async function lease(auto) {
206
+ if (ending)
207
+ throw Errors.connection('CONNECTION_ENDED', options)
208
+
209
+ const l = { connection: null, closed: null, onclose: null, reserved: null, queue: Queue() }
210
+ open.length
211
+ ? own(open.shift(), l)
212
+ : auto && !closed.length && busy.length
213
+ ? own(busy.shift(), l)
214
+ : await new Promise((resolve, reject) => {
215
+ const query = { reserve: c => resolve(own(c, l)), reject }
216
+ closed.length
217
+ ? connect(closed.shift(), query)
218
+ : queries.push(query)
219
+ })
220
+ return l
221
+ }
222
+
223
+ function own(c, l) {
224
+ l.connection = c
225
+ c.lease = l
226
+ move(c, reserved)
227
+ c.reserved = l.reserved = () => l.queue.length
228
+ ? c.execute(l.queue.shift())
229
+ : move(c, reserved)
230
+ c.reserved.release = true
231
+ return c
232
+ }
233
+
234
+ function valid(l) {
235
+ return !l.closed && l.connection.lease === l && l.connection.reserved === l.reserved
236
+ }
237
+
238
+ function send(l, q) {
239
+ const c = l.connection
240
+ valid(l)
241
+ ? c.queue === full
242
+ ? l.queue.push(q)
243
+ : c.execute(q) || move(c, full)
244
+ : q.reject(gone(l))
245
+ }
246
+
247
+ function gone(l) {
248
+ return Errors.connection(l.closed && l.closed.code === 'CONNECTION_ENDED' ? 'CONNECTION_ENDED' : 'CONNECTION_CLOSED', options)
249
+ }
250
+
251
+ function finish(l) {
252
+ const c = l.connection
253
+ if (l.closed || c.lease !== l)
254
+ return false
255
+
256
+ const held = c.reserved === l.reserved
257
+ c.lease = null
258
+ l.closed = Errors.connection('CONNECTION_ENDED', options)
259
+ held && (c.reserved = null)
260
+ while (l.queue.length)
261
+ l.queue.shift().reject(gone(l))
262
+ return held
263
+ }
264
+
265
+ async function reserve() {
266
+ const l = await lease(false)
267
+ const sql = Sql(q => send(l, q))
268
+ sql.release = () => finish(l) && l.connection.release()
269
+ return sql
270
+ }
271
+
272
+ async function begin(options, fn) {
273
+ !fn && (fn = options, options = '')
274
+ const l = await lease(true)
275
+ let savepoints = 0
276
+ , prepare = null
277
+ , settled = false
278
+
279
+ try {
280
+ const closing = new Promise((_, reject) => l.onclose = reject)
281
+ return await Promise.race([transaction(), closing])
282
+ } finally {
283
+ finish(l) && (settled ? l.connection.release() : l.connection.terminate())
284
+ }
285
+
286
+ async function transaction() {
287
+ await Sql(q => send(l, q)).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
288
+ e instanceof PostgresError && (settled = true)
289
+ throw e
290
+ })
291
+ return scope(fn)
292
+ }
293
+
294
+ async function scope(fn, name) {
295
+ const sql = Sql(handler)
296
+ sql.savepoint = savepoint
297
+ sql.prepare = x => {
298
+ if (typeof x !== 'string' || !x || x.includes('\0'))
299
+ throw Errors.generic('INVALID_TRANSACTION_NAME', 'sql.prepare requires a non-empty string without null bytes')
300
+
301
+ prepare = 'E\'' + x.replace(/\\/g, '\\\\').replace(/'/g, '\'\'') + '\''
302
+ }
303
+ let uncaughtError
304
+ , result
305
+
306
+ name && await sql`savepoint ${ sql(name) }`
307
+ try {
308
+ result = await new Promise((resolve, reject) => {
309
+ const x = fn(sql)
310
+ Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject)
311
+ })
312
+
313
+ if (uncaughtError)
314
+ throw uncaughtError
315
+ } catch (e) {
316
+ l.closed || await (name
317
+ ? sql`rollback to ${ sql(name) }`
318
+ : sql`rollback`
319
+ )
320
+ name || (settled = true)
321
+ throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
322
+ }
323
+
324
+ if (l.closed)
325
+ throw l.closed
326
+
327
+ if (!name) {
328
+ prepare
329
+ ? await sql`prepare transaction ${ sql.unsafe(prepare) }`
330
+ : await sql`commit`
331
+ settled = true
332
+ }
333
+
334
+ return result
335
+
336
+ function savepoint(name, fn) {
337
+ if (name && Array.isArray(name.raw))
338
+ return savepoint(sql => sql.apply(sql, arguments))
339
+
340
+ arguments.length === 1 && (fn = name, name = null)
341
+ return scope(fn, 's' + savepoints++ + (name ? '_' + name : ''))
342
+ }
343
+
344
+ function handler(q) {
345
+ q.catch(e => uncaughtError || (uncaughtError = e))
346
+ send(l, q)
347
+ }
348
+ }
349
+ }
350
+
351
+ function move(c, queue) {
352
+ c.queue.remove(c)
353
+ queue.push(c)
354
+ c.queue = queue
355
+ queue === open
356
+ ? c.idleTimer.start()
357
+ : c.idleTimer.cancel()
358
+ return c
359
+ }
360
+
361
+ function json(x) {
362
+ return new Parameter(x, 3802)
363
+ }
364
+
365
+ function array(x, type) {
366
+ if (!Array.isArray(x))
367
+ return array(Array.from(arguments))
368
+
369
+ return new Parameter(x, type || (x.length ? inferType(x) || 25 : 0), options.shared.typeArrayMap)
370
+ }
371
+
372
+ function handler(query) {
373
+ if (ending)
374
+ return query.reject(Errors.connection('CONNECTION_ENDED', options, options))
375
+
376
+ if (open.length)
377
+ return go(open.shift(), query)
378
+
379
+ if (closed.length)
380
+ return connect(closed.shift(), query)
381
+
382
+ busy.length
383
+ ? go(busy.shift(), query)
384
+ : queries.push(query)
385
+ }
386
+
387
+ function go(c, query) {
388
+ return c.execute(query)
389
+ ? move(c, busy)
390
+ : move(c, full)
391
+ }
392
+
393
+ function cancel(query) {
394
+ return new Promise((resolve, reject) => {
395
+ query.state
396
+ ? query.active
397
+ ? Connection(options).cancel(query.state, resolve, reject)
398
+ : query.cancelled = { resolve, reject }
399
+ : (
400
+ queries.remove(query),
401
+ query.cancelled = true,
402
+ query.reject(Errors.generic('57014', 'canceling statement due to user request')),
403
+ resolve()
404
+ )
405
+ })
406
+ }
407
+
408
+ async function end({ timeout = null } = {}) {
409
+ if (ending)
410
+ return ending
411
+
412
+ await 1
413
+ let timer
414
+ return ending = Promise.resolve().then(() => Promise.race([
415
+ new Promise(r => timeout !== null && (timer = setTimeout(() => destroy(r), timeout * 1000))),
416
+ Promise.all(connections.map(c => c.end()).concat(
417
+ listen.sql ? listen.sql.end({ timeout: 0 }) : [],
418
+ subscribe.sql ? subscribe.sql.end({ timeout: 0 }) : []
419
+ ))
420
+ ])).then(() => {
421
+ clearTimeout(timer)
422
+ while (queries.length)
423
+ queries.shift().reject(Errors.connection('CONNECTION_CLOSED', options))
424
+ })
425
+ }
426
+
427
+ async function close() {
428
+ await Promise.all(connections.map(c => c.end()))
429
+ }
430
+
431
+ async function destroy(resolve) {
432
+ while (queries.length)
433
+ queries.shift().reject(Errors.connection('CONNECTION_DESTROYED', options))
434
+ await Promise.all(connections.map(c => c.terminate()))
435
+ resolve()
436
+ }
437
+
438
+ function connect(c, query) {
439
+ move(c, connecting)
440
+ c.connect(query)
441
+ return c
442
+ }
443
+
444
+ function onend(c) {
445
+ move(c, ended)
446
+ }
447
+
448
+ function onopen(c, initial) {
449
+ if (initial && initial.reserve)
450
+ return initial.reserve(move(c, reserved))
451
+
452
+ if (queries.length === 0)
453
+ return move(c, open)
454
+
455
+ let max = Math.ceil(queries.length / (connecting.length + 1))
456
+ , ready = true
457
+
458
+ while (ready && queries.length && max-- > 0) {
459
+ const query = queries.shift()
460
+ if (query.reserve)
461
+ return query.reserve(move(c, reserved))
462
+
463
+ ready = c.execute(query)
464
+ }
465
+
466
+ ready
467
+ ? move(c, busy)
468
+ : move(c, full)
469
+ }
470
+
471
+ function ondrain(c) {
472
+ if (!ending)
473
+ return false
474
+
475
+ let drained = false
476
+ let ready = true
477
+ while (ready && queries.length) {
478
+ const query = queries.shift()
479
+ query.reserve
480
+ ? query.reject(Errors.connection('CONNECTION_ENDED', options))
481
+ : (drained = true, ready = c.execute(query))
482
+ }
483
+ return drained
484
+ }
485
+
486
+ function abandon(l, e) {
487
+ l.connection.lease = null
488
+ l.closed = e || Errors.connection('CONNECTION_CLOSED', options)
489
+ while (l.queue.length)
490
+ l.queue.shift().reject(gone(l))
491
+ l.onclose && l.onclose(e)
492
+ }
493
+
494
+ function onclose(c, e) {
495
+ move(c, closed)
496
+ c.reserved = null
497
+ c.lease && abandon(c.lease, e)
498
+ options.onclose && options.onclose(c.id)
499
+ if (ending) {
500
+ while (queries.length)
501
+ queries.shift().reject(e)
502
+ } else if (queries.length && c.queue === closed) {
503
+ connect(c, queries.shift())
504
+ }
505
+ }
506
+ }
507
+
508
+ function parseOptions(a, b) {
509
+ if (a && a.shared)
510
+ return a
511
+
512
+ const env = process.env // eslint-disable-line
513
+ , o = (!a || typeof a === 'string' ? b : a) || {}
514
+ , { url, multihost } = parseUrl(a)
515
+ , query = [...url.searchParams].reduce((a, [b, c]) => (a[b] = c, a), {})
516
+ , host = o.hostname || o.host || multihost || url.hostname || env.PGHOST || 'localhost'
517
+ , port = o.port || url.port || env.PGPORT || 5432
518
+ , user = o.user || o.username || url.username || env.PGUSERNAME || env.PGUSER || osUsername()
519
+
520
+ o.no_prepare && (o.prepare = false)
521
+ query.sslmode && (query.ssl = query.sslmode, delete query.sslmode)
522
+ 'timeout' in o && (console.log('The timeout option is deprecated, use idle_timeout instead'), o.idle_timeout = o.timeout) // eslint-disable-line
523
+ query.sslrootcert === 'system' && (query.ssl = 'verify-full')
524
+
525
+ const ints = ['idle_timeout', 'connect_timeout', 'max_lifetime', 'max_pipeline', 'backoff', 'keep_alive']
526
+ const defaults = {
527
+ max : globalThis.Cloudflare ? 3 : 10,
528
+ ssl : false,
529
+ sslnegotiation : null,
530
+ idle_timeout : null,
531
+ connect_timeout : 30,
532
+ max_lifetime : max_lifetime,
533
+ max_pipeline : 100,
534
+ backoff : backoff,
535
+ keep_alive : 60,
536
+ prepare : true,
537
+ debug : false,
538
+ fetch_types : true,
539
+ publications : 'alltables',
540
+ target_session_attrs: null
541
+ }
542
+
543
+ const options = {
544
+ host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]),
545
+ port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)),
546
+ path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port,
547
+ database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user,
548
+ user : user,
549
+ pass : o.pass || o.password || url.password || env.PGPASSWORD || '',
550
+ ...Object.entries(defaults).reduce(
551
+ (acc, [k, d]) => {
552
+ const value = k in o ? o[k] : k in query
553
+ ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k])
554
+ : env['PG' + k.toUpperCase()] || d
555
+ acc[k] = typeof value === 'string' && ints.includes(k)
556
+ ? +value
557
+ : value
558
+ return acc
559
+ },
560
+ {}
561
+ ),
562
+ connection : {
563
+ application_name: env.PGAPPNAME || 'postgres.js',
564
+ ...o.connection,
565
+ ...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {})
566
+ },
567
+ types : o.types || {},
568
+ target_session_attrs: tsa(o, url, env),
569
+ onnotice : o.onnotice,
570
+ onnotify : o.onnotify,
571
+ onclose : o.onclose,
572
+ onparameter : o.onparameter,
573
+ socket : o.socket,
574
+ transform : parseTransform(o.transform || { undefined: undefined }),
575
+ parameters : {},
576
+ shared : { retries: 0, typeArrayMap: {} },
577
+ ...mergeUserTypes(o.types)
578
+ }
579
+
580
+ !options.fetch_types && addDefaultArrayTypes(options)
581
+ return options
582
+ }
583
+
584
+ function tsa(o, url, env) {
585
+ const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS
586
+ if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x))
587
+ return x
588
+
589
+ throw new Error('target_session_attrs ' + x + ' is not supported')
590
+ }
591
+
592
+ function backoff(retries) {
593
+ return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
594
+ }
595
+
596
+ function max_lifetime() {
597
+ return 60 * (30 + Math.random() * 30)
598
+ }
599
+
600
+ function parseTransform(x) {
601
+ return {
602
+ undefined: x.undefined,
603
+ column: {
604
+ from: typeof x.column === 'function' ? x.column : x.column && x.column.from,
605
+ to: x.column && x.column.to
606
+ },
607
+ value: {
608
+ from: typeof x.value === 'function' ? x.value : x.value && x.value.from,
609
+ to: x.value && x.value.to
610
+ },
611
+ row: {
612
+ from: typeof x.row === 'function' ? x.row : x.row && x.row.from,
613
+ to: x.row && x.row.to
614
+ }
615
+ }
616
+ }
617
+
618
+ function parseUrl(url) {
619
+ if (!url || typeof url !== 'string')
620
+ return { url: { searchParams: new Map() } }
621
+
622
+ let host = url
623
+ host = host.slice(host.indexOf('://') + 3).split(/[?/]/)[0]
624
+ host = decodeURIComponent(host.slice(host.indexOf('@') + 1))
625
+
626
+ const urlObj = new URL(url.replace(host, host.split(',')[0]))
627
+
628
+ return {
629
+ url: {
630
+ username: decodeURIComponent(urlObj.username),
631
+ password: decodeURIComponent(urlObj.password),
632
+ host: urlObj.host,
633
+ hostname: urlObj.hostname,
634
+ port: urlObj.port,
635
+ pathname: urlObj.pathname,
636
+ searchParams: urlObj.searchParams
637
+ },
638
+ multihost: host.indexOf(',') > -1 && host
639
+ }
640
+ }
641
+
642
+ function osUsername() {
643
+ try {
644
+ return os.userInfo().username // eslint-disable-line
645
+ } catch (_) {
646
+ return process.env.USERNAME || process.env.USER || process.env.LOGNAME // eslint-disable-line
647
+ }
648
+ }