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.
- package/CHANGELOG.md +162 -1
- package/LICENSE +26 -17
- package/README.md +195 -175
- package/connection/connection.d.ts +152 -13
- package/connection/connection.js +296 -49
- package/connection/copy-stream.d.ts +88 -0
- package/connection/copy-stream.js +244 -0
- package/connection/cursor.js +20 -13
- package/connection/intl-connection.d.ts +68 -3
- package/connection/intl-connection.js +414 -57
- package/connection/large-object.d.ts +65 -0
- package/connection/large-object.js +151 -0
- package/connection/logical-replication.d.ts +121 -0
- package/connection/logical-replication.js +279 -0
- package/connection/pool.d.ts +61 -4
- package/connection/pool.js +177 -29
- package/connection/portal.d.ts +18 -4
- package/connection/portal.js +53 -59
- package/connection/prepared-statement.d.ts +22 -1
- package/connection/prepared-statement.js +117 -97
- package/data-type-map.d.ts +2 -2
- package/data-type-map.js +4 -2
- package/data-types/bool-type.js +14 -10
- package/data-types/box-type.js +15 -8
- package/data-types/bytea-type.js +9 -5
- package/data-types/char-type.js +11 -5
- package/data-types/circle-type.js +15 -7
- package/data-types/date-type.js +25 -19
- package/data-types/float4-type.js +10 -3
- package/data-types/float8-type.js +12 -3
- package/data-types/int2-type.js +9 -4
- package/data-types/int2-vector-type.js +10 -4
- package/data-types/int4-type.js +11 -5
- package/data-types/int8-type.js +22 -7
- package/data-types/json-type.js +17 -8
- package/data-types/jsonb-type.js +20 -14
- package/data-types/lseg-type.js +16 -8
- package/data-types/numeric-type.d.ts +8 -0
- package/data-types/numeric-type.js +112 -8
- package/data-types/oid-type.d.ts +0 -1
- package/data-types/oid-type.js +9 -10
- package/data-types/oid-vector-type.d.ts +9 -0
- package/data-types/oid-vector-type.js +51 -0
- package/data-types/point-type.js +14 -6
- package/data-types/time-type.js +20 -13
- package/data-types/timestamp-type.js +32 -25
- package/data-types/timestamptz-type.d.ts +1 -1
- package/data-types/timestamptz-type.js +34 -21
- package/data-types/uuid-type.js +21 -11
- package/data-types/varchar-type.js +16 -4
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/interfaces/data-type.d.ts +5 -3
- package/interfaces/database-connection-params.d.ts +95 -0
- package/interfaces/query-options.d.ts +17 -0
- package/interfaces/script-execute-options.d.ts +17 -0
- package/package.json +3 -4
- package/protocol/backend.d.ts +6 -1
- package/protocol/backend.js +70 -45
- package/protocol/buffer-reader.d.ts +0 -2
- package/protocol/buffer-reader.js +3 -10
- package/protocol/cert-signature.d.ts +12 -0
- package/protocol/cert-signature.js +114 -0
- package/protocol/database-error.js +1 -0
- package/protocol/frontend.d.ts +16 -0
- package/protocol/frontend.js +58 -6
- package/protocol/pg-socket.d.ts +221 -10
- package/protocol/pg-socket.js +539 -102
- package/protocol/pgoutput.d.ts +76 -0
- package/protocol/pgoutput.js +155 -0
- package/protocol/protocol.d.ts +3 -4
- package/protocol/protocol.js +1 -1
- package/protocol/sasl.d.ts +11 -1
- package/protocol/sasl.js +35 -4
- package/protocol/smart-buffer.d.ts +0 -2
- package/protocol/smart-buffer.js +11 -16
- package/types.d.ts +5 -3
- package/util/abort-signal.d.ts +24 -0
- package/util/abort-signal.js +63 -0
- package/util/array-calculatedim.js +3 -1
- package/util/bigint-methods.js +1 -1
- package/util/connection-config.js +77 -3
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +15 -6
- package/util/encode-binaryarray.js +7 -3
- package/util/escape-identifier.d.ts +14 -0
- package/util/escape-identifier.js +32 -0
- package/util/escape-literal.js +3 -0
- package/util/fast-parseint.d.ts +1 -0
- package/util/fast-parseint.js +12 -17
- package/util/format-datetime.d.ts +10 -0
- package/util/format-datetime.js +71 -0
- package/util/get-parsers.js +31 -10
- package/util/parse-array.js +2 -2
- package/util/parse-bytea.d.ts +2 -0
- package/util/parse-bytea.js +83 -0
- package/util/parse-datetime.d.ts +3 -1
- package/util/parse-datetime.js +86 -17
- package/util/parse-row.d.ts +3 -1
- package/util/parse-row.js +60 -5
- package/util/parse-time.js +8 -1
- package/util/sql-tag.d.ts +60 -0
- package/util/sql-tag.js +202 -0
- package/util/starts-transaction.d.ts +38 -0
- package/util/starts-transaction.js +148 -0
- package/util/stringify-arrayliteral.js +13 -1
- package/util/wrap-row-description.js +13 -4
- package/util/convert-row-to-object.d.ts +0 -2
- 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
|
+
}
|
package/protocol/protocol.d.ts
CHANGED
|
@@ -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 =
|
|
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,//
|
|
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
|
-
|
|
121
|
+
data: Buffer;
|
|
123
122
|
}
|
|
124
123
|
interface ErrorResponseMessage {
|
|
125
124
|
severity?: string;
|
package/protocol/protocol.js
CHANGED
|
@@ -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"] =
|
|
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";
|
package/protocol/sasl.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/protocol/smart-buffer.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
13
|
+
export type DecodeTextFunction = (v: any, options: DataMappingOptions) => any;
|
|
13
14
|
export type EncodeTextFunction = (v: any, options: DataMappingOptions) => string;
|
|
14
|
-
export type
|
|
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
|
-
|
|
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);
|
package/util/bigint-methods.js
CHANGED
|
@@ -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')
|
|
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[]>;
|