postgrejs 2.23.0 → 3.0.1

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +162 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +152 -13
  5. package/connection/connection.js +296 -49
  6. package/connection/copy-stream.d.ts +88 -0
  7. package/connection/copy-stream.js +244 -0
  8. package/connection/cursor.js +20 -13
  9. package/connection/intl-connection.d.ts +68 -3
  10. package/connection/intl-connection.js +414 -57
  11. package/connection/large-object.d.ts +65 -0
  12. package/connection/large-object.js +151 -0
  13. package/connection/logical-replication.d.ts +121 -0
  14. package/connection/logical-replication.js +279 -0
  15. package/connection/pool.d.ts +61 -4
  16. package/connection/pool.js +177 -29
  17. package/connection/portal.d.ts +18 -4
  18. package/connection/portal.js +53 -59
  19. package/connection/prepared-statement.d.ts +22 -1
  20. package/connection/prepared-statement.js +117 -97
  21. package/data-type-map.d.ts +2 -2
  22. package/data-type-map.js +4 -2
  23. package/data-types/bool-type.js +14 -10
  24. package/data-types/box-type.js +15 -8
  25. package/data-types/bytea-type.js +9 -5
  26. package/data-types/char-type.js +11 -5
  27. package/data-types/circle-type.js +15 -7
  28. package/data-types/date-type.js +25 -19
  29. package/data-types/float4-type.js +10 -3
  30. package/data-types/float8-type.js +12 -3
  31. package/data-types/int2-type.js +9 -4
  32. package/data-types/int2-vector-type.js +10 -4
  33. package/data-types/int4-type.js +11 -5
  34. package/data-types/int8-type.js +22 -7
  35. package/data-types/json-type.js +17 -8
  36. package/data-types/jsonb-type.js +20 -14
  37. package/data-types/lseg-type.js +16 -8
  38. package/data-types/numeric-type.d.ts +8 -0
  39. package/data-types/numeric-type.js +112 -8
  40. package/data-types/oid-type.d.ts +0 -1
  41. package/data-types/oid-type.js +9 -10
  42. package/data-types/oid-vector-type.d.ts +9 -0
  43. package/data-types/oid-vector-type.js +51 -0
  44. package/data-types/point-type.js +14 -6
  45. package/data-types/time-type.js +20 -13
  46. package/data-types/timestamp-type.js +32 -25
  47. package/data-types/timestamptz-type.d.ts +1 -1
  48. package/data-types/timestamptz-type.js +34 -21
  49. package/data-types/uuid-type.js +21 -11
  50. package/data-types/varchar-type.js +16 -4
  51. package/index.d.ts +5 -0
  52. package/index.js +5 -0
  53. package/interfaces/data-type.d.ts +5 -3
  54. package/interfaces/database-connection-params.d.ts +95 -0
  55. package/interfaces/query-options.d.ts +17 -0
  56. package/interfaces/script-execute-options.d.ts +17 -0
  57. package/package.json +3 -4
  58. package/protocol/backend.d.ts +6 -1
  59. package/protocol/backend.js +70 -45
  60. package/protocol/buffer-reader.d.ts +0 -2
  61. package/protocol/buffer-reader.js +3 -10
  62. package/protocol/cert-signature.d.ts +12 -0
  63. package/protocol/cert-signature.js +114 -0
  64. package/protocol/database-error.js +1 -0
  65. package/protocol/frontend.d.ts +16 -0
  66. package/protocol/frontend.js +58 -6
  67. package/protocol/pg-socket.d.ts +221 -10
  68. package/protocol/pg-socket.js +539 -102
  69. package/protocol/pgoutput.d.ts +76 -0
  70. package/protocol/pgoutput.js +155 -0
  71. package/protocol/protocol.d.ts +3 -4
  72. package/protocol/protocol.js +1 -1
  73. package/protocol/sasl.d.ts +11 -1
  74. package/protocol/sasl.js +35 -4
  75. package/protocol/smart-buffer.d.ts +0 -2
  76. package/protocol/smart-buffer.js +11 -16
  77. package/types.d.ts +5 -3
  78. package/util/abort-signal.d.ts +24 -0
  79. package/util/abort-signal.js +63 -0
  80. package/util/array-calculatedim.js +3 -1
  81. package/util/bigint-methods.js +1 -1
  82. package/util/connection-config.js +77 -3
  83. package/util/decode-binaryarray.d.ts +1 -1
  84. package/util/decode-binaryarray.js +15 -6
  85. package/util/encode-binaryarray.js +7 -3
  86. package/util/escape-identifier.d.ts +14 -0
  87. package/util/escape-identifier.js +32 -0
  88. package/util/escape-literal.js +3 -0
  89. package/util/fast-parseint.d.ts +1 -0
  90. package/util/fast-parseint.js +12 -17
  91. package/util/format-datetime.d.ts +10 -0
  92. package/util/format-datetime.js +71 -0
  93. package/util/get-parsers.js +31 -10
  94. package/util/parse-array.js +2 -2
  95. package/util/parse-bytea.d.ts +2 -0
  96. package/util/parse-bytea.js +83 -0
  97. package/util/parse-datetime.d.ts +3 -1
  98. package/util/parse-datetime.js +86 -17
  99. package/util/parse-row.d.ts +3 -1
  100. package/util/parse-row.js +60 -5
  101. package/util/parse-time.js +8 -1
  102. package/util/sql-tag.d.ts +60 -0
  103. package/util/sql-tag.js +202 -0
  104. package/util/starts-transaction.d.ts +38 -0
  105. package/util/starts-transaction.js +148 -0
  106. package/util/stringify-arrayliteral.js +13 -1
  107. package/util/wrap-row-description.js +13 -4
  108. package/util/convert-row-to-object.d.ts +0 -2
  109. package/util/convert-row-to-object.js +0 -9
@@ -0,0 +1,76 @@
1
+ import { BufferReader } from './buffer-reader.js';
2
+ export interface PgOutputColumn {
3
+ name: string;
4
+ dataTypeId: number;
5
+ typeModifier: number;
6
+ /** Part of the replica identity, so it can identify a row on its own. */
7
+ isKey: boolean;
8
+ }
9
+ export interface PgOutputRelation {
10
+ relationId: number;
11
+ schema: string;
12
+ name: string;
13
+ replicaIdentity: string;
14
+ columns: PgOutputColumn[];
15
+ }
16
+ export type PgOutputMessage = {
17
+ kind: 'begin';
18
+ finalLsn: bigint;
19
+ commitTime: Date;
20
+ xid: number;
21
+ } | {
22
+ kind: 'commit';
23
+ commitLsn: bigint;
24
+ endLsn: bigint;
25
+ commitTime: Date;
26
+ } | {
27
+ kind: 'relation';
28
+ relation: PgOutputRelation;
29
+ } | {
30
+ kind: 'type';
31
+ dataTypeId: number;
32
+ schema: string;
33
+ name: string;
34
+ } | {
35
+ kind: 'insert';
36
+ relation: PgOutputRelation;
37
+ row: TupleValues;
38
+ } | {
39
+ kind: 'update';
40
+ relation: PgOutputRelation;
41
+ row: TupleValues;
42
+ oldRow?: TupleValues;
43
+ } | {
44
+ kind: 'delete';
45
+ relation: PgOutputRelation;
46
+ oldRow: TupleValues;
47
+ } | {
48
+ kind: 'truncate';
49
+ relations: PgOutputRelation[];
50
+ } | {
51
+ kind: 'origin';
52
+ commitLsn: bigint;
53
+ name: string;
54
+ } | {
55
+ kind: 'message';
56
+ prefix: string;
57
+ content: Buffer;
58
+ lsn: bigint;
59
+ } | {
60
+ kind: 'unknown';
61
+ code: string;
62
+ };
63
+ /**
64
+ * A row as pgoutput sends it: column name to its raw text, or null. A value
65
+ * left out because it did not change and lives in TOAST storage is absent
66
+ * from the map entirely - which is not the same as being null.
67
+ */
68
+ export type TupleValues = Record<string, string | null>;
69
+ export declare class PgOutputDecoder {
70
+ protected readonly _relations: Map<number, PgOutputRelation>;
71
+ decode(data: Buffer): PgOutputMessage;
72
+ protected _relation(relationId: number): PgOutputRelation;
73
+ protected _tuple(io: BufferReader, relation: PgOutputRelation): TupleValues;
74
+ }
75
+ /** `0/16B3748`, the form PostgreSQL prints an LSN in. */
76
+ export declare function formatLsn(lsn: bigint): string;
@@ -0,0 +1,155 @@
1
+ import { BufferReader } from './buffer-reader.js';
2
+ /**
3
+ * Decoder for `pgoutput`, the logical decoding plugin PostgreSQL ships with,
4
+ * at protocol version 1.
5
+ *
6
+ * The server does not repeat a table's shape with every row: a Relation
7
+ * message describes it once and later Insert/Update/Delete messages refer to
8
+ * it by OID, so a decoder has to remember relations as it goes. That is why
9
+ * this is a class rather than a function.
10
+ */
11
+ /** Microseconds since 2000-01-01, which is how this protocol tells time. */
12
+ const PG_EPOCH_MS = Date.UTC(2000, 0, 1);
13
+ export class PgOutputDecoder {
14
+ _relations = new Map();
15
+ decode(data) {
16
+ const io = new BufferReader(data);
17
+ const code = String.fromCharCode(io.readUInt8());
18
+ switch (code) {
19
+ case 'B':
20
+ return {
21
+ kind: 'begin',
22
+ finalLsn: readLsn(io),
23
+ commitTime: readTime(io),
24
+ xid: io.readUInt32BE(),
25
+ };
26
+ case 'C': {
27
+ io.readUInt8(); // flags, unused at this version
28
+ return {
29
+ kind: 'commit',
30
+ commitLsn: readLsn(io),
31
+ endLsn: readLsn(io),
32
+ commitTime: readTime(io),
33
+ };
34
+ }
35
+ case 'O':
36
+ return {
37
+ kind: 'origin',
38
+ commitLsn: readLsn(io),
39
+ name: io.readCString(),
40
+ };
41
+ case 'R': {
42
+ const relation = {
43
+ relationId: io.readUInt32BE(),
44
+ schema: io.readCString(),
45
+ name: io.readCString(),
46
+ replicaIdentity: String.fromCharCode(io.readUInt8()),
47
+ columns: [],
48
+ };
49
+ const count = io.readUInt16BE();
50
+ for (let i = 0; i < count; i++) {
51
+ relation.columns.push({
52
+ isKey: io.readUInt8() === 1,
53
+ name: io.readCString(),
54
+ dataTypeId: io.readUInt32BE(),
55
+ typeModifier: io.readInt32BE(),
56
+ });
57
+ }
58
+ this._relations.set(relation.relationId, relation);
59
+ return { kind: 'relation', relation };
60
+ }
61
+ case 'Y':
62
+ return {
63
+ kind: 'type',
64
+ dataTypeId: io.readUInt32BE(),
65
+ schema: io.readCString(),
66
+ name: io.readCString(),
67
+ };
68
+ case 'I': {
69
+ const relation = this._relation(io.readUInt32BE());
70
+ io.readUInt8(); // 'N'
71
+ return { kind: 'insert', relation, row: this._tuple(io, relation) };
72
+ }
73
+ case 'U': {
74
+ const relation = this._relation(io.readUInt32BE());
75
+ let oldRow;
76
+ // 'K' is the old row's key columns, 'O' the whole old row; either
77
+ // appears only when the table's replica identity provides it, and
78
+ // the 'N' tag for the new row follows in every case.
79
+ const tag = String.fromCharCode(io.readUInt8());
80
+ if (tag === 'K' || tag === 'O') {
81
+ oldRow = this._tuple(io, relation);
82
+ io.readUInt8(); // 'N'
83
+ }
84
+ return {
85
+ kind: 'update',
86
+ relation,
87
+ row: this._tuple(io, relation),
88
+ oldRow,
89
+ };
90
+ }
91
+ case 'D': {
92
+ const relation = this._relation(io.readUInt32BE());
93
+ io.readUInt8(); // 'K' or 'O'
94
+ return { kind: 'delete', relation, oldRow: this._tuple(io, relation) };
95
+ }
96
+ case 'T': {
97
+ const count = io.readUInt32BE();
98
+ io.readUInt8(); // flags: cascade / restart identity
99
+ const relations = [];
100
+ for (let i = 0; i < count; i++)
101
+ relations.push(this._relation(io.readUInt32BE()));
102
+ return { kind: 'truncate', relations };
103
+ }
104
+ case 'M': {
105
+ io.readUInt8(); // flags
106
+ const lsn = readLsn(io);
107
+ const prefix = io.readCString();
108
+ const length = io.readUInt32BE();
109
+ return { kind: 'message', prefix, lsn, content: io.readBuffer(length) };
110
+ }
111
+ default:
112
+ return { kind: 'unknown', code };
113
+ }
114
+ }
115
+ _relation(relationId) {
116
+ const relation = this._relations.get(relationId);
117
+ if (!relation)
118
+ throw new Error(`Logical replication: a change arrived for relation ${relationId} before its description did`);
119
+ return relation;
120
+ }
121
+ _tuple(io, relation) {
122
+ const count = io.readUInt16BE();
123
+ const out = {};
124
+ for (let i = 0; i < count; i++) {
125
+ const column = relation.columns[i];
126
+ const kind = String.fromCharCode(io.readUInt8());
127
+ if (kind === 'n') {
128
+ out[column.name] = null;
129
+ }
130
+ else if (kind === 'u') {
131
+ // Unchanged and stored out of line: the server did not send it, and
132
+ // leaving the key out says so rather than claiming it is null.
133
+ continue;
134
+ }
135
+ else {
136
+ const length = io.readUInt32BE();
137
+ out[column.name] = io.readBuffer(length).toString('utf8');
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+ }
143
+ function readLsn(io) {
144
+ const hi = BigInt(io.readUInt32BE());
145
+ const lo = BigInt(io.readUInt32BE());
146
+ return (hi << 32n) | lo;
147
+ }
148
+ function readTime(io) {
149
+ const micros = (BigInt(io.readUInt32BE()) << 32n) | BigInt(io.readUInt32BE());
150
+ return new Date(PG_EPOCH_MS + Number(micros / 1000n));
151
+ }
152
+ /** `0/16B3748`, the form PostgreSQL prints an LSN in. */
153
+ export function formatLsn(lsn) {
154
+ return `${(lsn >> 32n).toString(16).toUpperCase()}/${(lsn & 0xffffffffn).toString(16).toUpperCase()}`;
155
+ }
@@ -1,4 +1,3 @@
1
- import type { Nullable } from '../types.js';
2
1
  export declare namespace Protocol {
3
2
  const VERSION_MAJOR = 3;
4
3
  const VERSION_MINOR = 0;
@@ -10,7 +9,7 @@ export declare namespace Protocol {
10
9
  CommandComplete = 67,// C
11
10
  CopyData = 100,// d
12
11
  CopyDone = 99,// c
13
- CopyInResponse = 103,// G
12
+ CopyInResponse = 71,// G
14
13
  CopyOutResponse = 72,// H
15
14
  CopyBothResponse = 87,// W
16
15
  DataRow = 68,// D
@@ -29,7 +28,7 @@ export declare namespace Protocol {
29
28
  RowDescription = 84
30
29
  }
31
30
  enum FrontendMessageCode {
32
- Bind = 66,// R
31
+ Bind = 66,// B
33
32
  Close = 67,// C
34
33
  CopyData = 100,// d
35
34
  CopyDone = 99,// c
@@ -119,7 +118,7 @@ export declare namespace Protocol {
119
118
  }
120
119
  interface DataRowMessage {
121
120
  columnCount: number;
122
- columns: Nullable<Buffer>[];
121
+ data: Buffer;
123
122
  }
124
123
  interface ErrorResponseMessage {
125
124
  severity?: string;
@@ -12,7 +12,7 @@ export var Protocol;
12
12
  BackendMessageCode[BackendMessageCode["CommandComplete"] = 67] = "CommandComplete";
13
13
  BackendMessageCode[BackendMessageCode["CopyData"] = 100] = "CopyData";
14
14
  BackendMessageCode[BackendMessageCode["CopyDone"] = 99] = "CopyDone";
15
- BackendMessageCode[BackendMessageCode["CopyInResponse"] = 103] = "CopyInResponse";
15
+ BackendMessageCode[BackendMessageCode["CopyInResponse"] = 71] = "CopyInResponse";
16
16
  BackendMessageCode[BackendMessageCode["CopyOutResponse"] = 72] = "CopyOutResponse";
17
17
  BackendMessageCode[BackendMessageCode["CopyBothResponse"] = 87] = "CopyBothResponse";
18
18
  BackendMessageCode[BackendMessageCode["DataRow"] = 68] = "DataRow";
@@ -6,8 +6,18 @@ export declare namespace SASL {
6
6
  clientFirstMessage: string;
7
7
  clientFinalMessage: string;
8
8
  serverSignature: string;
9
+ /** The GS2 header this session announced; repeated in the final message. */
10
+ gs2Header: string;
11
+ /** tls-server-end-point data, present only for SCRAM-SHA-256-PLUS. */
12
+ bindingData?: Buffer;
9
13
  }
10
- function createSession(username: string, mechanism: string): Session;
14
+ /**
15
+ * @param bindingData - the server certificate's hash, for
16
+ * SCRAM-SHA-256-PLUS. Pass undefined for the plain mechanism, and
17
+ * `supportsBinding` to say whether the client could have done it, which
18
+ * is what tells the server a stripped -PLUS offer would be an attack.
19
+ */
20
+ function createSession(username: string, mechanism: string, bindingData?: Buffer, supportsBinding?: boolean): Session;
11
21
  function continueSession(session: Session, password: string, data: string): void;
12
22
  function finalizeSession(session: Session, data: string): void;
13
23
  }
package/protocol/sasl.js CHANGED
@@ -3,14 +3,36 @@ export var SASL;
3
3
  (function (SASL) {
4
4
  const CLIENT_KEY = 'Client Key';
5
5
  const SERVER_KEY = 'Server Key';
6
- const GS2_HEADER = 'n,,';
7
- function createSession(username, mechanism) {
6
+ /**
7
+ * The GS2 header states what the client can do about channel binding, and
8
+ * the server checks it against what it offered. `n` means the client does
9
+ * not support it; `y` means it does but saw no -PLUS mechanism offered -
10
+ * which is what lets the server notice an attacker having stripped it;
11
+ * `p=...` means it is in use.
12
+ */
13
+ const GS2_NONE = 'n,,';
14
+ const GS2_SUPPORTED_UNUSED = 'y,,';
15
+ const GS2_BOUND = 'p=tls-server-end-point,,';
16
+ /**
17
+ * @param bindingData - the server certificate's hash, for
18
+ * SCRAM-SHA-256-PLUS. Pass undefined for the plain mechanism, and
19
+ * `supportsBinding` to say whether the client could have done it, which
20
+ * is what tells the server a stripped -PLUS offer would be an attack.
21
+ */
22
+ function createSession(username, mechanism, bindingData, supportsBinding) {
8
23
  const nonce = crypto.randomBytes(18).toString('base64');
9
- const clientFirstMessage = `${GS2_HEADER}${firstMessageBare(username, nonce)}`;
24
+ const gs2Header = bindingData
25
+ ? GS2_BOUND
26
+ : supportsBinding
27
+ ? GS2_SUPPORTED_UNUSED
28
+ : GS2_NONE;
29
+ const clientFirstMessage = `${gs2Header}${firstMessageBare(username, nonce)}`;
10
30
  return {
11
31
  username,
12
32
  mechanism,
13
33
  nonce,
34
+ gs2Header,
35
+ bindingData,
14
36
  clientFirstMessage,
15
37
  };
16
38
  }
@@ -45,7 +67,16 @@ export var SASL;
45
67
  if (!nonce.startsWith(session.nonce))
46
68
  throw new Error('SASL: Server nonce does not start with client nonce');
47
69
  const serverFirstMessage = `r=${nonce},s=${salt},i=${iteration}`;
48
- const clientFinalMessageWithoutProof = `c=${encode64(GS2_HEADER)},r=${nonce}`;
70
+ // c= carries the header again, with the binding data appended when the
71
+ // channel is bound - so an attacker who relayed the exchange over a
72
+ // different TLS connection produces a different proof.
73
+ const cbind = session.bindingData
74
+ ? Buffer.concat([
75
+ Buffer.from(session.gs2Header, 'utf8'),
76
+ session.bindingData,
77
+ ]).toString('base64')
78
+ : encode64(session.gs2Header);
79
+ const clientFinalMessageWithoutProof = `c=${cbind},r=${nonce}`;
49
80
  const authMessage = `${firstMessageBare(session.username, session.nonce)},${serverFirstMessage},${clientFinalMessageWithoutProof}`;
50
81
  const saltPass = hi(password, salt, iteration);
51
82
  const clientKey = hmac(saltPass, CLIENT_KEY);
@@ -9,7 +9,6 @@ export declare class SmartBuffer extends BufferReader {
9
9
  static DEFAULT_MAX_SIZE: number;
10
10
  private readonly _houseKeepInterval;
11
11
  private _houseKeepTimer?;
12
- private _lastHouseKeep;
13
12
  private _stMaxPages;
14
13
  private _length;
15
14
  readonly pageSize: number;
@@ -20,7 +19,6 @@ export declare class SmartBuffer extends BufferReader {
20
19
  start(): this;
21
20
  flush(): Buffer;
22
21
  growSize(len: number): this;
23
- fill(value?: number, len?: number): this;
24
22
  writeCString(str: string, encoding?: BufferEncoding): this;
25
23
  writeLString(str?: string, encoding?: BufferEncoding): this;
26
24
  writeString(str: string, encoding?: BufferEncoding): this;
@@ -2,11 +2,10 @@ import * as os from 'os';
2
2
  import { writeBigUInt64BE } from '../util/bigint-methods.js';
3
3
  import { BufferReader } from './buffer-reader.js';
4
4
  export class SmartBuffer extends BufferReader {
5
- static DEFAULT_PAGE_SIZE = 4096;
5
+ static DEFAULT_PAGE_SIZE = 512;
6
6
  static DEFAULT_MAX_SIZE = Math.min(Math.floor(os.totalmem() / 2), 1024 * 1024 * 1024 * 2);
7
7
  _houseKeepInterval;
8
8
  _houseKeepTimer;
9
- _lastHouseKeep = 0;
10
9
  _stMaxPages = 1;
11
10
  _length = 0;
12
11
  pageSize;
@@ -38,15 +37,16 @@ export class SmartBuffer extends BufferReader {
38
37
  clearTimeout(this._houseKeepTimer);
39
38
  const length = this.length;
40
39
  this._length = 0;
41
- const out = this.buffer.slice(0, length);
40
+ const out = Buffer.from(this.buffer.subarray(0, length));
42
41
  const pages = length ? Math.ceil(length / this.pageSize) : 1;
43
42
  this._stMaxPages = Math.max(this._stMaxPages, pages);
44
- if (this._lastHouseKeep < Date.now() + this._houseKeepInterval)
45
- this._houseKeep();
46
- this._houseKeepTimer = setTimeout(() => {
47
- this._houseKeepTimer = undefined;
48
- this._houseKeep();
49
- }, this._houseKeepInterval).unref();
43
+ this._houseKeep();
44
+ if (this.buffer.length > this.pageSize) {
45
+ this._houseKeepTimer = setTimeout(() => {
46
+ this._houseKeepTimer = undefined;
47
+ this._houseKeep();
48
+ }, this._houseKeepInterval).unref();
49
+ }
50
50
  return out;
51
51
  }
52
52
  growSize(len) {
@@ -54,7 +54,8 @@ export class SmartBuffer extends BufferReader {
54
54
  if (this.capacity < endOffset) {
55
55
  if (endOffset > this.maxSize)
56
56
  throw new Error('Buffer limit exceeded.');
57
- const newSize = Math.ceil(endOffset / this.pageSize) * this.pageSize;
57
+ const byPage = Math.ceil(endOffset / this.pageSize) * this.pageSize;
58
+ const newSize = Math.min(Math.max(byPage, this.capacity * 2), this.maxSize);
58
59
  const newBuffer = Buffer.allocUnsafe(newSize);
59
60
  this.buffer.copy(newBuffer);
60
61
  this.buffer = newBuffer;
@@ -62,12 +63,6 @@ export class SmartBuffer extends BufferReader {
62
63
  this._length = Math.max(this.length, endOffset);
63
64
  return this;
64
65
  }
65
- fill(value = 0, len = 1) {
66
- this.ensureSize(len);
67
- this.buffer.fill(value, this.offset, this.offset + len);
68
- this.offset += len;
69
- return this;
70
- }
71
66
  writeCString(str, encoding) {
72
67
  const len = str ? Buffer.byteLength(str, encoding) : 0;
73
68
  this.ensureSize(len + 1);
package/types.d.ts CHANGED
@@ -5,11 +5,13 @@ export type Maybe<T> = T | undefined;
5
5
  export type Nullable<T> = T | null;
6
6
  export type Row = any;
7
7
  export type Callback = (err?: Error | null) => void;
8
- export type DecodeBinaryFunction = (buf: Buffer, options: DataMappingOptions & Record<string, any>) => any;
8
+ export type DecodeBinaryFunction = (buf: Buffer, offset: number, options: DataMappingOptions & Record<string, any>) => any;
9
+ export type DecodeTextBufferFunction = (buf: Buffer, offset: number, len: number, options: DataMappingOptions & Record<string, any>) => any;
9
10
  export type EncodeBinaryFunction = (buf: SmartBuffer, v: any, options: DataMappingOptions) => void;
10
11
  export type EncodeCalculateDimFunction = (v: any[]) => number[];
11
12
  export type EncodeAsNullFunction = (v: any, options: DataMappingOptions) => boolean;
12
- export type ParseTextFunction = (v: any, options: DataMappingOptions) => any;
13
+ export type DecodeTextFunction = (v: any, options: DataMappingOptions) => any;
13
14
  export type EncodeTextFunction = (v: any, options: DataMappingOptions) => string;
14
- export type AnyParseFunction = ParseTextFunction | DecodeBinaryFunction;
15
+ export type ResolvedColumnParser = (data: Buffer, offset: number, len: number, options: DataMappingOptions & Record<string, any>) => any;
16
+ export type AnyParseFunction = ResolvedColumnParser;
15
17
  export type DebugLogger = (namespace: string, format: any, ...args: any[]) => void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The error an aborted operation rejects with.
3
+ *
4
+ * Prefers the signal's own reason, so `AbortSignal.timeout()` still reports
5
+ * itself as a TimeoutError and a caller's `controller.abort(myError)` still
6
+ * arrives intact. The database error that actually ended the query (SQLSTATE
7
+ * 57014) is attached as `cause` rather than replacing it: the reason is what
8
+ * callers branch on, the cause is what they need when reporting it.
9
+ */
10
+ export declare function abortError(signal: AbortSignal, cause?: unknown): Error;
11
+ /**
12
+ * Runs `run()` under an AbortSignal, asking `cancel()` to interrupt it if the
13
+ * signal fires.
14
+ *
15
+ * Cancelling a PostgreSQL query is a request, not a guarantee: it travels on
16
+ * its own connection and the statement may well finish first. So this does
17
+ * not settle early - it lets the query end the way the server ends it and
18
+ * only then reports the abort, which is also what keeps the connection
19
+ * usable, since its response still has to be read either way.
20
+ *
21
+ * The listener is always removed again: one long-lived signal shared by many
22
+ * queries would otherwise accumulate a listener per query.
23
+ */
24
+ export declare function withAbortSignal<T>(signal: AbortSignal | undefined, cancel: () => Promise<void>, run: () => Promise<T>): Promise<T>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The error an aborted operation rejects with.
3
+ *
4
+ * Prefers the signal's own reason, so `AbortSignal.timeout()` still reports
5
+ * itself as a TimeoutError and a caller's `controller.abort(myError)` still
6
+ * arrives intact. The database error that actually ended the query (SQLSTATE
7
+ * 57014) is attached as `cause` rather than replacing it: the reason is what
8
+ * callers branch on, the cause is what they need when reporting it.
9
+ */
10
+ export function abortError(signal, cause) {
11
+ const reason = signal.reason;
12
+ const err = reason instanceof Error
13
+ ? reason
14
+ : Object.assign(new Error('The operation was aborted'), {
15
+ name: 'AbortError',
16
+ });
17
+ if (cause !== undefined && err.cause === undefined) {
18
+ try {
19
+ err.cause = cause;
20
+ }
21
+ catch {
22
+ // A reason the caller froze; the error itself is still correct.
23
+ }
24
+ }
25
+ return err;
26
+ }
27
+ /**
28
+ * Runs `run()` under an AbortSignal, asking `cancel()` to interrupt it if the
29
+ * signal fires.
30
+ *
31
+ * Cancelling a PostgreSQL query is a request, not a guarantee: it travels on
32
+ * its own connection and the statement may well finish first. So this does
33
+ * not settle early - it lets the query end the way the server ends it and
34
+ * only then reports the abort, which is also what keeps the connection
35
+ * usable, since its response still has to be read either way.
36
+ *
37
+ * The listener is always removed again: one long-lived signal shared by many
38
+ * queries would otherwise accumulate a listener per query.
39
+ */
40
+ export async function withAbortSignal(signal, cancel, run) {
41
+ if (!signal)
42
+ return run();
43
+ // Already aborted: nothing has been sent, so nothing needs cancelling.
44
+ if (signal.aborted)
45
+ throw abortError(signal);
46
+ const onAbort = () => {
47
+ // A failed cancel (the session already gone, say) must not replace the
48
+ // error the query itself is about to report.
49
+ cancel().catch(() => undefined);
50
+ };
51
+ signal.addEventListener('abort', onAbort, { once: true });
52
+ try {
53
+ return await run();
54
+ }
55
+ catch (e) {
56
+ if (signal.aborted)
57
+ throw abortError(signal, e);
58
+ throw e;
59
+ }
60
+ finally {
61
+ signal.removeEventListener('abort', onAbort);
62
+ }
63
+ }
@@ -3,7 +3,9 @@ export function arrayCalculateDim(arr) {
3
3
  return [0];
4
4
  const dim = [arr.length];
5
5
  const iterate = (a, level) => {
6
- for (let i = 0; i < a.length; i++) {
6
+ let i;
7
+ const l = a.length;
8
+ for (i = 0; i < l; i++) {
7
9
  if (Array.isArray(a[i])) {
8
10
  dim[level] = Math.max(dim[level] || 0, a[i].length);
9
11
  iterate(a[i], level + 1);
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-useless-assignment */
1
+ /* eslint-disable */
2
2
  const big0 = BigInt(0);
3
3
  const beAnd = BigInt('0xffffffff');
4
4
  const big32 = BigInt(32);
@@ -9,20 +9,74 @@ export function getConnectionConfig(config) {
9
9
  merge(cfg, config);
10
10
  }
11
11
  if (cfg.host) {
12
+ const explicitRequireSSL = cfg.requireSSL;
12
13
  const x = parseConnectionString('' + cfg.host);
13
14
  merge(cfg, x);
15
+ if (explicitRequireSSL !== undefined)
16
+ cfg.requireSSL = explicitRequireSSL;
14
17
  }
18
+ // A comma-separated host is the same thing as a `hosts` list; normalise
19
+ // to one shape so nothing downstream has to know both.
20
+ if (!cfg.hosts?.length && cfg.host?.includes(','))
21
+ cfg.hosts = splitHosts(cfg.host, cfg.port);
22
+ if (cfg.hosts?.length) {
23
+ // Resolve each candidate's port now, from the shared `port` where it
24
+ // has none of its own. Doing it later would mean reading `cfg.port`
25
+ // after the line below has overwritten it with the first host's, so a
26
+ // second candidate without a port would inherit the first one's.
27
+ const sharedPort = cfg.port;
28
+ cfg.hosts = cfg.hosts.map(x => ({
29
+ host: x.host,
30
+ port: x.port ?? sharedPort,
31
+ }));
32
+ cfg.host = cfg.hosts[0].host;
33
+ cfg.port = cfg.hosts[0].port;
34
+ }
35
+ /*c8 ignore else */
15
36
  cfg.user = cfg.user || 'postgres';
37
+ /*c8 ignore else */
16
38
  cfg.database = cfg.database || 'postgres';
17
39
  cfg.host = cfg.host || '127.0.0.1';
18
40
  return cfg;
19
41
  }
42
+ const TARGET_SESSION_ATTRS = [
43
+ 'read-write',
44
+ 'read-only',
45
+ 'primary',
46
+ 'standby',
47
+ 'prefer-standby',
48
+ ];
49
+ /** `a:5432,b` -> [{host:'a',port:5432},{host:'b',port:fallback}] */
50
+ function splitHosts(value, fallbackPort) {
51
+ return value
52
+ .split(',')
53
+ .map(s => s.trim())
54
+ .filter(Boolean)
55
+ .map(entry => {
56
+ const i = entry.lastIndexOf(':');
57
+ // A bare IPv6 address has colons of its own; only treat the last one
58
+ // as a port when what follows it is a number.
59
+ if (i > 0 && /^\d+$/.test(entry.substring(i + 1)))
60
+ return { host: entry.substring(0, i), port: +entry.substring(i + 1) };
61
+ return { host: entry, port: fallbackPort };
62
+ });
63
+ }
20
64
  export function parseConnectionString(str) {
21
65
  if (str.startsWith('/'))
22
66
  str = 'socket:/' + str;
23
67
  if (!str.includes('://'))
24
68
  str = 'postgres://' + str;
69
+ // URL() cannot parse a multi-host authority, so it is parsed with the
70
+ // first host in place and the full list kept separately.
71
+ const authority = str.slice(str.indexOf('://') + 3).split(/[?/]/)[0];
72
+ const hostPart = authority.slice(authority.indexOf('@') + 1);
73
+ const multiHost = hostPart.includes(',') ? hostPart : undefined;
74
+ if (multiHost)
75
+ str = str.replace(hostPart, hostPart.split(',')[0]);
25
76
  const parsed = new URL(str);
77
+ // URLSearchParams.get() only ever returns string | null, never an array,
78
+ // so the array branch below is unreachable in practice; kept defensively
79
+ // since the parameter type is technically wider.
26
80
  const getFirst = (v) => typeof v === 'string' ? v : Array.isArray(v) ? v[0] : '';
27
81
  const cfg = {};
28
82
  cfg.host = decodeURI(parsed.hostname || '');
@@ -49,9 +103,29 @@ export function parseConnectionString(str) {
49
103
  cfg.applicationName = decodeURI(getFirst(parsed.searchParams.get('application_name')));
50
104
  }
51
105
  if (parsed.username)
52
- cfg.user = parsed.username;
106
+ cfg.user = decodeURIComponent(parsed.username);
53
107
  if (parsed.password)
54
- cfg.password = parsed.password;
55
- cfg.requireSSL = parsed.searchParams.get('sslmode') === 'require';
108
+ cfg.password = decodeURIComponent(parsed.password);
109
+ cfg.requireSSL = ['require', 'verify-ca', 'verify-full'].includes(parsed.searchParams.get('sslmode') || '');
110
+ const cb = parsed.searchParams.get('channel_binding');
111
+ if (cb) {
112
+ if (cb !== 'prefer' && cb !== 'require' && cb !== 'disable')
113
+ throw new Error(`channel_binding "${cb}" is not supported; use "prefer", "require" or "disable"`);
114
+ cfg.channelBinding = cb;
115
+ }
116
+ const sslneg = parsed.searchParams.get('sslnegotiation');
117
+ if (sslneg) {
118
+ if (sslneg !== 'postgres' && sslneg !== 'direct')
119
+ throw new Error(`sslnegotiation "${sslneg}" is not supported; use "postgres" or "direct"`);
120
+ cfg.sslNegotiation = sslneg;
121
+ }
122
+ const tsa = parsed.searchParams.get('target_session_attrs');
123
+ if (tsa) {
124
+ if (!TARGET_SESSION_ATTRS.includes(tsa))
125
+ throw new Error(`target_session_attrs "${tsa}" is not supported`);
126
+ cfg.targetSessionAttrs = tsa;
127
+ }
128
+ if (multiHost)
129
+ cfg.hosts = splitHosts(multiHost, cfg.port);
56
130
  return cfg;
57
131
  }
@@ -1,3 +1,3 @@
1
1
  import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
2
2
  import type { DecodeBinaryFunction, Nullable } from '../types.js';
3
- export declare function decodeBinaryArray<T = any>(buf: Buffer, decoder: DecodeBinaryFunction, options?: DataMappingOptions): Nullable<T[]>;
3
+ export declare function decodeBinaryArray<T = any>(buf: Buffer, offset: number, decoder: DecodeBinaryFunction, options?: DataMappingOptions, fixedBinarySize?: number): Nullable<T[]>;