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