@bytebase/dbhub 1.3.1 → 1.5.0
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/dist/{chunk-ABTVMHD6.js → chunk-55X2VY5D.js} +1 -1
- package/dist/chunk-5XSBOK3W.js +63 -0
- package/dist/{chunk-5FL4XMWG.js → chunk-PDKPK25P.js} +356 -106
- package/dist/chunk-SJ5BRPQA.js +500 -0
- package/dist/{chunk-YIGJHEU6.js → chunk-X5OCFRNL.js} +170 -27
- package/dist/index.js +116 -32
- package/dist/mariadb-RWOMZTA6.js +270 -0
- package/dist/mysql-ZXO4TWLG.js +312 -0
- package/dist/{oracle-OUKBU52X.js → oracle-UAVN3F4U.js} +2 -2
- package/dist/{postgres-6XJ7V4J4.js → postgres-KCFNDQLG.js} +179 -29
- package/dist/{registry-ZTHDPLKP.js → registry-ZIVIYCB2.js} +2 -1
- package/dist/{sqlite-AO2BKIC6.js → sqlite-WKY6543Q.js} +2 -2
- package/dist/{sqlserver-AJUK6L55.js → sqlserver-25MAE77Q.js} +312 -372
- package/package.json +1 -1
- package/dist/chunk-6SZKW6GD.js +0 -179
- package/dist/mariadb-L7Z55ROY.js +0 -551
- package/dist/mysql-XXIHWQGR.js +0 -601
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CANCEL_QUERY_TIMEOUT_MS,
|
|
3
|
+
clientQueryTimeoutMs,
|
|
4
|
+
isClientSideTimeout
|
|
5
|
+
} from "./chunk-5XSBOK3W.js";
|
|
1
6
|
import {
|
|
2
7
|
toNullableNumber
|
|
3
8
|
} from "./chunk-FU2ZJE4E.js";
|
|
@@ -7,8 +12,8 @@ import {
|
|
|
7
12
|
import {
|
|
8
13
|
SQLRowLimiter,
|
|
9
14
|
closeQuietly
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-X5OCFRNL.js";
|
|
16
|
+
import "./chunk-55X2VY5D.js";
|
|
12
17
|
import {
|
|
13
18
|
ConnectorRegistry,
|
|
14
19
|
SafeURL,
|
|
@@ -17,10 +22,7 @@ import {
|
|
|
17
22
|
} from "./chunk-IXC4NQGC.js";
|
|
18
23
|
|
|
19
24
|
// src/connectors/postgres/index.ts
|
|
20
|
-
import
|
|
21
|
-
import os from "os";
|
|
22
|
-
import path from "path";
|
|
23
|
-
import pg2 from "pg";
|
|
25
|
+
import pg3 from "pg";
|
|
24
26
|
|
|
25
27
|
// src/connectors/postgres/failed-to-read-certificate.ts
|
|
26
28
|
var FailedToReadCertificate = class extends Error {
|
|
@@ -56,9 +58,83 @@ var postgresTypeParsers = {
|
|
|
56
58
|
}
|
|
57
59
|
};
|
|
58
60
|
|
|
61
|
+
// src/connectors/postgres/ssl-pems.ts
|
|
62
|
+
import fs from "fs";
|
|
63
|
+
import os from "os";
|
|
64
|
+
import path from "path";
|
|
65
|
+
import pg2 from "pg";
|
|
66
|
+
var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
|
|
67
|
+
var SUPPORTED_SSL_MODES = ["disable", ...CLIENT_CERT_SSL_MODES];
|
|
68
|
+
var SSL_ROOT_CERT_LABEL = "SSL root certificate";
|
|
69
|
+
var SSL_CLIENT_CERT_LABEL = "SSL client certificate";
|
|
70
|
+
var SSL_CLIENT_KEY_LABEL = "SSL client key";
|
|
71
|
+
function resolvePemPath(filePath) {
|
|
72
|
+
return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
|
|
73
|
+
}
|
|
74
|
+
function failedToRead(label, resolved, err) {
|
|
75
|
+
return new FailedToReadCertificate(
|
|
76
|
+
`Failed to read ${label} at '${resolved}': ${err instanceof Error ? err.message : String(err)}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
async function readPemFile(filePath, label) {
|
|
80
|
+
const resolved = resolvePemPath(filePath);
|
|
81
|
+
try {
|
|
82
|
+
return await fs.promises.readFile(resolved, "utf-8");
|
|
83
|
+
} catch (err) {
|
|
84
|
+
throw failedToRead(label, resolved, err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function readPemFileSync(filePath, label) {
|
|
88
|
+
const resolved = resolvePemPath(filePath);
|
|
89
|
+
try {
|
|
90
|
+
return fs.readFileSync(resolved, "utf-8");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw failedToRead(label, resolved, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isEncryptedPemKey(pem) {
|
|
96
|
+
return pem.includes("ENCRYPTED PRIVATE KEY") || pem.includes("Proc-Type: 4,ENCRYPTED");
|
|
97
|
+
}
|
|
98
|
+
function encryptedKeyMessage(keyPath) {
|
|
99
|
+
return `SSL client key at '${keyPath}' is encrypted; encrypted private keys are not supported. Decrypt it first, e.g. 'openssl pkey -in client.key -out client-plain.key'`;
|
|
100
|
+
}
|
|
101
|
+
function loadPems(paths) {
|
|
102
|
+
const pems = {};
|
|
103
|
+
if (paths.sslrootcert !== void 0) {
|
|
104
|
+
pems.ca = readPemFileSync(paths.sslrootcert, SSL_ROOT_CERT_LABEL);
|
|
105
|
+
}
|
|
106
|
+
if (paths.sslcert !== void 0 && paths.sslkey !== void 0) {
|
|
107
|
+
const key = readPemFileSync(paths.sslkey, SSL_CLIENT_KEY_LABEL);
|
|
108
|
+
if (isEncryptedPemKey(key)) {
|
|
109
|
+
throw new FailedToReadCertificate(encryptedKeyMessage(paths.sslkey));
|
|
110
|
+
}
|
|
111
|
+
pems.key = key;
|
|
112
|
+
pems.cert = readPemFileSync(paths.sslcert, SSL_CLIENT_CERT_LABEL);
|
|
113
|
+
}
|
|
114
|
+
return pems;
|
|
115
|
+
}
|
|
116
|
+
function rotatingPemClient(paths) {
|
|
117
|
+
return class RotatingPemClient extends pg2.Client {
|
|
118
|
+
connect(callback) {
|
|
119
|
+
let pems;
|
|
120
|
+
try {
|
|
121
|
+
pems = loadPems(paths);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
124
|
+
if (callback) {
|
|
125
|
+
callback(error);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
return Promise.reject(error);
|
|
129
|
+
}
|
|
130
|
+
Object.assign(this.ssl, pems);
|
|
131
|
+
return callback ? super.connect(callback) : super.connect();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
59
136
|
// src/connectors/postgres/index.ts
|
|
60
|
-
var { Pool } =
|
|
61
|
-
var POSTGRES_CLIENT_QUERY_TIMEOUT_GRACE_MS = 5e3;
|
|
137
|
+
var { Pool } = pg3;
|
|
62
138
|
var PostgresDSNParser = class {
|
|
63
139
|
async parse(dsn, config) {
|
|
64
140
|
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
|
|
@@ -81,17 +157,43 @@ Expected: ${expectedFormat}`
|
|
|
81
157
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
82
158
|
// Remove leading '/' if exists
|
|
83
159
|
user: url.username,
|
|
84
|
-
password: url.password
|
|
160
|
+
password: config?.password ?? url.password
|
|
85
161
|
};
|
|
86
162
|
let sslmode;
|
|
87
163
|
let sslrootcert;
|
|
164
|
+
let sslcert;
|
|
165
|
+
let sslkey;
|
|
88
166
|
url.forEachSearchParam((value, key) => {
|
|
89
167
|
if (key === "sslmode") {
|
|
90
168
|
sslmode = value;
|
|
91
169
|
} else if (key === "sslrootcert") {
|
|
92
170
|
sslrootcert = value;
|
|
171
|
+
} else if (key === "sslcert") {
|
|
172
|
+
sslcert = value;
|
|
173
|
+
} else if (key === "sslkey") {
|
|
174
|
+
sslkey = value;
|
|
93
175
|
}
|
|
94
176
|
});
|
|
177
|
+
if (sslmode !== void 0 && !SUPPORTED_SSL_MODES.includes(sslmode)) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Unsupported sslmode '${sslmode}'. Valid values: ${SUPPORTED_SSL_MODES.join(", ")}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (sslrootcert !== void 0 && sslmode !== "verify-ca" && sslmode !== "verify-full") {
|
|
183
|
+
throw new FailedToReadCertificate(
|
|
184
|
+
`sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got '${sslmode ?? "not set"}'). Use sslmode=verify-ca to verify the server certificate against it, or remove sslrootcert.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (sslcert === void 0 !== (sslkey === void 0)) {
|
|
188
|
+
throw new FailedToReadCertificate(
|
|
189
|
+
"sslcert and sslkey must be set together for client certificate authentication"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (sslcert !== void 0 && !CLIENT_CERT_SSL_MODES.includes(sslmode ?? "")) {
|
|
193
|
+
throw new FailedToReadCertificate(
|
|
194
|
+
`sslcert/sslkey require sslmode to be one of ${CLIENT_CERT_SSL_MODES.join(", ")} (got '${sslmode ?? "not set"}')`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
95
197
|
if (sslmode === "disable") {
|
|
96
198
|
poolConfig.ssl = false;
|
|
97
199
|
} else if (sslmode === "require") {
|
|
@@ -102,18 +204,22 @@ Expected: ${expectedFormat}`
|
|
|
102
204
|
sslConfig.checkServerIdentity = () => void 0;
|
|
103
205
|
}
|
|
104
206
|
if (sslrootcert) {
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
sslConfig.ca = await fs.promises.readFile(certPath, "utf-8");
|
|
108
|
-
} catch (err) {
|
|
109
|
-
throw new FailedToReadCertificate(
|
|
110
|
-
`Failed to read SSL root certificate at '${certPath}': ${err instanceof Error ? err.message : String(err)}`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
207
|
+
sslConfig.ca = await readPemFile(sslrootcert, SSL_ROOT_CERT_LABEL);
|
|
113
208
|
}
|
|
114
209
|
poolConfig.ssl = sslConfig;
|
|
115
|
-
}
|
|
116
|
-
|
|
210
|
+
}
|
|
211
|
+
if (sslcert !== void 0 && sslkey !== void 0) {
|
|
212
|
+
const key = await readPemFile(sslkey, SSL_CLIENT_KEY_LABEL);
|
|
213
|
+
if (isEncryptedPemKey(key)) {
|
|
214
|
+
throw new FailedToReadCertificate(encryptedKeyMessage(sslkey));
|
|
215
|
+
}
|
|
216
|
+
Object.assign(poolConfig.ssl, {
|
|
217
|
+
cert: await readPemFile(sslcert, SSL_CLIENT_CERT_LABEL),
|
|
218
|
+
key
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (sslrootcert !== void 0 || sslcert !== void 0) {
|
|
222
|
+
poolConfig.Client = rotatingPemClient({ sslrootcert, sslcert, sslkey });
|
|
117
223
|
}
|
|
118
224
|
if (connectionTimeoutSeconds !== void 0) {
|
|
119
225
|
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
|
|
@@ -121,7 +227,7 @@ Expected: ${expectedFormat}`
|
|
|
121
227
|
if (queryTimeoutSeconds !== void 0) {
|
|
122
228
|
const queryTimeoutMs = queryTimeoutSeconds * 1e3;
|
|
123
229
|
poolConfig.statement_timeout = queryTimeoutMs;
|
|
124
|
-
poolConfig.query_timeout = queryTimeoutMs
|
|
230
|
+
poolConfig.query_timeout = clientQueryTimeoutMs(queryTimeoutMs);
|
|
125
231
|
}
|
|
126
232
|
if (poolMaxConnections !== void 0) {
|
|
127
233
|
poolConfig.max = poolMaxConnections;
|
|
@@ -595,6 +701,8 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
595
701
|
throw new Error("Not connected to database");
|
|
596
702
|
}
|
|
597
703
|
const client = await this.pool.connect();
|
|
704
|
+
const backendPid = client.processID;
|
|
705
|
+
let isClientDiscarded = false;
|
|
598
706
|
try {
|
|
599
707
|
const statements = splitSQLStatements(sql, "postgres");
|
|
600
708
|
if (statements.length === 1) {
|
|
@@ -616,10 +724,7 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
616
724
|
SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
|
|
617
725
|
return { resultSets: [resultSet2] };
|
|
618
726
|
} catch (error) {
|
|
619
|
-
|
|
620
|
-
await client.query("ROLLBACK");
|
|
621
|
-
} catch {
|
|
622
|
-
}
|
|
727
|
+
await this.rollbackQuietly(client, error);
|
|
623
728
|
throw error;
|
|
624
729
|
}
|
|
625
730
|
}
|
|
@@ -660,16 +765,61 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
660
765
|
}
|
|
661
766
|
await client.query("COMMIT");
|
|
662
767
|
} catch (error) {
|
|
663
|
-
|
|
664
|
-
await client.query("ROLLBACK");
|
|
665
|
-
} catch {
|
|
666
|
-
}
|
|
768
|
+
await this.rollbackQuietly(client, error);
|
|
667
769
|
throw error;
|
|
668
770
|
}
|
|
669
771
|
return { resultSets };
|
|
670
772
|
}
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (isClientSideTimeout(error)) {
|
|
775
|
+
isClientDiscarded = true;
|
|
776
|
+
client.release(true);
|
|
777
|
+
await this.cancelBackend(backendPid);
|
|
778
|
+
}
|
|
779
|
+
throw error;
|
|
671
780
|
} finally {
|
|
672
|
-
|
|
781
|
+
if (!isClientDiscarded) {
|
|
782
|
+
client.release();
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Best-effort rollback after a failed statement, so a failed ROLLBACK (e.g.
|
|
788
|
+
* a dropped connection) cannot mask the original error. Skipped for a
|
|
789
|
+
* client-side timeout: the client is still busy with the abandoned
|
|
790
|
+
* statement, so the ROLLBACK would queue behind it (see isClientSideTimeout).
|
|
791
|
+
* executeSQL discards that client instead.
|
|
792
|
+
*/
|
|
793
|
+
async rollbackQuietly(client, error) {
|
|
794
|
+
if (isClientSideTimeout(error)) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
await client.query("ROLLBACK");
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Best-effort server-side cancel for a statement abandoned by the
|
|
804
|
+
* client-side timeout, sent over a separate pooled connection. Closing the
|
|
805
|
+
* abandoned client's socket is not enough: PostgreSQL only notices a
|
|
806
|
+
* vanished client when it next writes to it, so a statement that is
|
|
807
|
+
* sleeping or computing would run to completion.
|
|
808
|
+
*
|
|
809
|
+
* Bounded by its own short timeout (CANCEL_QUERY_TIMEOUT_MS), independent
|
|
810
|
+
* of the user's query_timeout. pool.query discards its client on error, so
|
|
811
|
+
* a cancel that itself times out does not leak a stuck client.
|
|
812
|
+
*/
|
|
813
|
+
async cancelBackend(backendPid) {
|
|
814
|
+
if (!this.pool || backendPid === null) return;
|
|
815
|
+
try {
|
|
816
|
+
const cancel = {
|
|
817
|
+
text: "SELECT pg_cancel_backend($1)",
|
|
818
|
+
values: [backendPid],
|
|
819
|
+
query_timeout: CANCEL_QUERY_TIMEOUT_MS
|
|
820
|
+
};
|
|
821
|
+
await this.pool.query(cancel);
|
|
822
|
+
} catch {
|
|
673
823
|
}
|
|
674
824
|
}
|
|
675
825
|
};
|