@effect/sql-pg 4.0.0-rc.115 → 4.0.0-rc.117
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/README.md +125 -1
- package/ai-docs/package.json +3 -3
- package/dist/PgClient.d.ts +27 -3
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +1 -0
- package/dist/PgClient.js.map +1 -1
- package/dist/PgConnection.d.ts +54 -14
- package/dist/PgConnection.d.ts.map +1 -1
- package/dist/PgConnection.js +121 -67
- package/dist/PgConnection.js.map +1 -1
- package/dist/PgMigrator.d.ts.map +1 -1
- package/dist/PgMigrator.js +15 -10
- package/dist/PgMigrator.js.map +1 -1
- package/dist/PgPool.d.ts.map +1 -1
- package/dist/PgPool.js +4 -7
- package/dist/PgPool.js.map +1 -1
- package/dist/PgTypes.d.ts +20 -10
- package/dist/PgTypes.d.ts.map +1 -1
- package/dist/PgTypes.js +64 -44
- package/dist/PgTypes.js.map +1 -1
- package/dist/internal/password.d.ts +4 -0
- package/dist/internal/password.d.ts.map +1 -0
- package/dist/internal/password.js +4 -0
- package/dist/internal/password.js.map +1 -0
- package/package.json +5 -5
- package/src/PgClient.ts +29 -4
- package/src/PgConnection.ts +194 -98
- package/src/PgMigrator.ts +18 -19
- package/src/PgPool.ts +4 -7
- package/src/PgTypes.ts +71 -50
- package/src/internal/connection.ts +2 -2
- package/src/internal/password.ts +11 -0
package/dist/PgConnection.js
CHANGED
|
@@ -17,6 +17,7 @@ import { randomBytes } from "node:crypto";
|
|
|
17
17
|
import * as Net from "node:net";
|
|
18
18
|
import * as Tls from "node:tls";
|
|
19
19
|
import { internalsKey } from "./internal/connection.js";
|
|
20
|
+
import * as PasswordInternal from "./internal/password.js";
|
|
20
21
|
import { classifySqlState, validateChannelName } from "./internal/sqlError.js";
|
|
21
22
|
import * as PgAuth from "./PgAuth.js";
|
|
22
23
|
import * as PgProtocol from "./PgProtocol.js";
|
|
@@ -49,19 +50,22 @@ export const PgConnection = /*#__PURE__*/Context.Service("@effect/sql-pg/PgConne
|
|
|
49
50
|
*
|
|
50
51
|
* **Details**
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
-
* under `connectTimeout` (5 seconds by default). The effect
|
|
54
|
-
* backend sends `ReadyForQuery`.
|
|
55
|
-
*
|
|
53
|
+
* Password resolution, transport, optional `SSLRequest`, startup, and
|
|
54
|
+
* authentication run under `connectTimeout` (5 seconds by default). The effect
|
|
55
|
+
* resolves when the backend sends `ReadyForQuery`. Closing the scope sends
|
|
56
|
+
* `Terminate` and ends the socket.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* explicitly.
|
|
58
|
+
* Use `sslmode=require` or explicit `ssl: true` to require encryption.
|
|
59
|
+
* Unix sockets and custom streams should set `ssl.servername` explicitly.
|
|
60
60
|
*
|
|
61
61
|
* @category constructors
|
|
62
62
|
* @since 4.0.0
|
|
63
63
|
*/
|
|
64
|
-
export const make = options => Effect.flatMap(resolveConfig(options), config => Effect.acquireRelease(Effect.
|
|
64
|
+
export const make = options => Effect.flatMap(resolveConfig(options), config => Effect.acquireRelease(Effect.gen(function* () {
|
|
65
|
+
const password = yield* PasswordInternal.resolve(config.password);
|
|
66
|
+
const session = yield* connect(config, password);
|
|
67
|
+
return new PgConnectionImpl(options, config, session, options.types);
|
|
68
|
+
}), connection => Effect.sync(() => connection.closeUnsafe()), {
|
|
65
69
|
interruptible: true
|
|
66
70
|
}).pipe(Effect.timeoutOrElse({
|
|
67
71
|
duration: config.connectTimeout,
|
|
@@ -74,6 +78,9 @@ export const make = options => Effect.flatMap(resolveConfig(options), config =>
|
|
|
74
78
|
}))
|
|
75
79
|
})));
|
|
76
80
|
const abortDrainTimeoutMillis = 5000;
|
|
81
|
+
const abortDrainGraceMillis = 10;
|
|
82
|
+
/** PostgreSQL `query_canceled` SQLSTATE. */
|
|
83
|
+
const queryCanceledCode = "57014";
|
|
77
84
|
const cancelRequestTimeoutMillis = 5000;
|
|
78
85
|
/** How many statements a multiplexed session keeps on the wire at once. */
|
|
79
86
|
const maxPipelineDepth = 128;
|
|
@@ -98,8 +105,9 @@ class PgConnectionImpl {
|
|
|
98
105
|
consumer;
|
|
99
106
|
deadWith;
|
|
100
107
|
closed = false;
|
|
108
|
+
cancelPending = false;
|
|
101
109
|
channels = /*#__PURE__*/new Map();
|
|
102
|
-
|
|
110
|
+
retireHooks = /*#__PURE__*/new Set();
|
|
103
111
|
/** Queued but not yet written; drained into `pipelineInFlight` on flush. */
|
|
104
112
|
pipelinePending = [];
|
|
105
113
|
/** Written and awaiting their `ReadyForQuery`, oldest first. */
|
|
@@ -122,7 +130,7 @@ class PgConnectionImpl {
|
|
|
122
130
|
this[internalsKey] = {
|
|
123
131
|
base: this,
|
|
124
132
|
deadError: () => this.deadWith,
|
|
125
|
-
|
|
133
|
+
retireHooks: this.retireHooks
|
|
126
134
|
};
|
|
127
135
|
this.pinnedView = new PinnedPgConnection(this);
|
|
128
136
|
session.socket.on("data", this.onData);
|
|
@@ -144,6 +152,15 @@ class PgConnectionImpl {
|
|
|
144
152
|
dispatch = message => {
|
|
145
153
|
if (this.deadWith !== undefined) return;
|
|
146
154
|
switch (message._tag) {
|
|
155
|
+
case "ErrorResponse":
|
|
156
|
+
if (message.fields.code === queryCanceledCode) this.cancelPending = false;
|
|
157
|
+
break;
|
|
158
|
+
case "ReadyForQuery":
|
|
159
|
+
if (this.cancelPending) {
|
|
160
|
+
this.cancelPending = false;
|
|
161
|
+
this.retire();
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
147
164
|
case "NotificationResponse":
|
|
148
165
|
{
|
|
149
166
|
const queues = this.channels.get(message.channel);
|
|
@@ -181,12 +198,15 @@ class PgConnectionImpl {
|
|
|
181
198
|
consumer?.onFatal(error);
|
|
182
199
|
const sets = Array.from(this.channels.values());
|
|
183
200
|
this.channels.clear();
|
|
201
|
+
const cause = this.closed ? Cause.interrupt() : Cause.fail(error);
|
|
184
202
|
for (const set of sets) {
|
|
185
|
-
for (const queue of set) Queue.failCauseUnsafe(queue,
|
|
186
|
-
}
|
|
187
|
-
if (!this.closed) {
|
|
188
|
-
for (const hook of this.fatalHooks) hook();
|
|
203
|
+
for (const queue of set) Queue.failCauseUnsafe(queue, cause);
|
|
189
204
|
}
|
|
205
|
+
this.retire();
|
|
206
|
+
}
|
|
207
|
+
retire() {
|
|
208
|
+
if (this.closed) return;
|
|
209
|
+
for (const hook of this.retireHooks) hook();
|
|
190
210
|
}
|
|
191
211
|
closeUnsafe() {
|
|
192
212
|
this.closed = true;
|
|
@@ -373,7 +393,9 @@ class PgConnectionImpl {
|
|
|
373
393
|
/** Sends a `CancelRequest` for this session on a side connection. */
|
|
374
394
|
cancel = /*#__PURE__*/Effect.suspend(() => {
|
|
375
395
|
if (this.deadWith !== undefined) return Effect.void;
|
|
376
|
-
|
|
396
|
+
this.cancelPending = true;
|
|
397
|
+
if (this.consumer === undefined) this.retire();
|
|
398
|
+
return sendCancelRequest(this.resolved, this.session);
|
|
377
399
|
});
|
|
378
400
|
pin = /*#__PURE__*/Effect.suspend(() => {
|
|
379
401
|
const reserve = this[internalsKey].reserve;
|
|
@@ -457,13 +479,7 @@ const inferScalar = value => {
|
|
|
457
479
|
// or timestamp column the way it did with the text-protocol drivers.
|
|
458
480
|
return inferredParameter(0, value);
|
|
459
481
|
}
|
|
460
|
-
if (value instanceof Date)
|
|
461
|
-
const time = value.getTime();
|
|
462
|
-
if (Number.isNaN(time)) throw new PgTypes.CodecError({
|
|
463
|
-
message: "Invalid Date parameter"
|
|
464
|
-
});
|
|
465
|
-
return inferredParameter(PgTypes.OID.timestamptz, time);
|
|
466
|
-
}
|
|
482
|
+
if (value instanceof Date) return inferredParameter(PgTypes.OID.timestamptz, value);
|
|
467
483
|
if (value instanceof Uint8Array) return inferredParameter(PgTypes.OID.bytea, value);
|
|
468
484
|
if (value instanceof Int8Array) {
|
|
469
485
|
return inferredParameter(PgTypes.OID.bytea, new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
|
|
@@ -662,6 +678,7 @@ const encodeQuery = (sql, params, registry, encodeBind, cache) => {
|
|
|
662
678
|
class PreparedCache {
|
|
663
679
|
max;
|
|
664
680
|
statements = /*#__PURE__*/new Map();
|
|
681
|
+
namespace = /*#__PURE__*/randomBytes(8).toString("hex");
|
|
665
682
|
closes;
|
|
666
683
|
counter = 0;
|
|
667
684
|
constructor(max) {
|
|
@@ -679,7 +696,7 @@ class PreparedCache {
|
|
|
679
696
|
return found;
|
|
680
697
|
}
|
|
681
698
|
const prepared = {
|
|
682
|
-
name: `
|
|
699
|
+
name: `effect_${this.namespace}_${++this.counter}`,
|
|
683
700
|
key,
|
|
684
701
|
ready: false,
|
|
685
702
|
parsing: false,
|
|
@@ -1018,6 +1035,21 @@ class QueryMachine {
|
|
|
1018
1035
|
}
|
|
1019
1036
|
const emptyRows = [];
|
|
1020
1037
|
const emptyValues = [];
|
|
1038
|
+
/** Drains an aborted statement, sending a cancel after a short grace period and failing if it stalls. */
|
|
1039
|
+
const drainAborted = (conn, timeoutMessage, abort) => {
|
|
1040
|
+
const drained = Deferred.makeUnsafe();
|
|
1041
|
+
const timeout = setTimeout(() => conn.fatal(connectionQueryError(new Error(timeoutMessage), `PgConnection: ${timeoutMessage}`)), abortDrainTimeoutMillis);
|
|
1042
|
+
abort(() => {
|
|
1043
|
+
clearTimeout(timeout);
|
|
1044
|
+
Deferred.doneUnsafe(drained, Effect.void);
|
|
1045
|
+
});
|
|
1046
|
+
const grace = Effect.callback(resume => {
|
|
1047
|
+
const timer = setTimeout(() => resume(Effect.void), abortDrainGraceMillis);
|
|
1048
|
+
return Effect.sync(() => clearTimeout(timer));
|
|
1049
|
+
});
|
|
1050
|
+
const cancelAndDrain = Effect.andThen(conn.cancel, Deferred.await(drained));
|
|
1051
|
+
return Effect.andThen(Effect.raceFirst(Deferred.await(drained), grace), Effect.suspend(() => Deferred.isDoneUnsafe(drained) ? Effect.void : cancelAndDrain));
|
|
1052
|
+
};
|
|
1021
1053
|
/** One cycle on a connection that carries a single statement at a time. */
|
|
1022
1054
|
const runQuery = (conn, plan, wantRows) => Effect.callback(resume => {
|
|
1023
1055
|
if (conn.deadWith !== undefined) {
|
|
@@ -1037,19 +1069,12 @@ const runQuery = (conn, plan, wantRows) => Effect.callback(resume => {
|
|
|
1037
1069
|
} catch (cause) {
|
|
1038
1070
|
conn.fatal(connectionQueryError(cause, "PgConnection: Failed to write query"));
|
|
1039
1071
|
}
|
|
1040
|
-
// On interruption: cancel the statement and drain the connection back to
|
|
1041
|
-
// ReadyForQuery so it stays usable, destroying it when the drain stalls.
|
|
1042
1072
|
return Effect.suspend(() => {
|
|
1043
1073
|
if (machine.isDone()) return Effect.void;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
conn.consumer = undefined;
|
|
1049
|
-
resumeWait(Effect.void);
|
|
1050
|
-
});
|
|
1051
|
-
});
|
|
1052
|
-
return Effect.andThen(conn.cancel, wait);
|
|
1074
|
+
return drainAborted(conn, "Query cancellation timed out", onDrained => machine.abort(() => {
|
|
1075
|
+
conn.consumer = undefined;
|
|
1076
|
+
onDrained();
|
|
1077
|
+
}));
|
|
1053
1078
|
});
|
|
1054
1079
|
});
|
|
1055
1080
|
const emptyFields = [];
|
|
@@ -1262,21 +1287,14 @@ const streamRows = (conn, pin, sql, params) => Stream.fromChannel(Channel.fromTr
|
|
|
1262
1287
|
if (done || aborted) return;
|
|
1263
1288
|
if (pending !== undefined) deliver();else if (buffer.length >= streamPauseThreshold) setPaused(true);
|
|
1264
1289
|
};
|
|
1265
|
-
// On early abort: cancel the statement and drain back to ReadyForQuery so
|
|
1266
|
-
// the pinned connection stays usable, destroying it when the drain stalls.
|
|
1267
1290
|
yield* Scope.addFinalizer(scope, Effect.suspend(() => {
|
|
1268
1291
|
if (done) return Effect.void;
|
|
1269
1292
|
aborted = true;
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (done) return resumeWait(Effect.void);
|
|
1273
|
-
const timer = setTimeout(() => conn.fatal(connectionQueryError(new Error("Stream cancellation timed out"), "PgConnection: Stream cancellation timed out")), abortDrainTimeoutMillis);
|
|
1274
|
-
drainDone = () => {
|
|
1275
|
-
clearTimeout(timer);
|
|
1276
|
-
resumeWait(Effect.void);
|
|
1277
|
-
};
|
|
1293
|
+
const drain = drainAborted(conn, "Stream cancellation timed out", onDrained => {
|
|
1294
|
+
drainDone = onDrained;
|
|
1278
1295
|
});
|
|
1279
|
-
|
|
1296
|
+
setPaused(false);
|
|
1297
|
+
return drain;
|
|
1280
1298
|
}));
|
|
1281
1299
|
conn.consumer = {
|
|
1282
1300
|
onMessage,
|
|
@@ -1330,7 +1348,7 @@ const listenChannel = (conn, pin, channel) => Effect.uninterruptibleMask(restore
|
|
|
1330
1348
|
return queue;
|
|
1331
1349
|
})).pipe(Effect.tapCause(cause => Scope.close(scope, Exit.failCause(cause))));
|
|
1332
1350
|
}));
|
|
1333
|
-
const sendCancelRequest = (config,
|
|
1351
|
+
const sendCancelRequest = (config, session) => Effect.callback(resume => {
|
|
1334
1352
|
let done = false;
|
|
1335
1353
|
let socket;
|
|
1336
1354
|
let timer;
|
|
@@ -1342,8 +1360,8 @@ const sendCancelRequest = (config, pid, secret) => Effect.callback(resume => {
|
|
|
1342
1360
|
resume(Effect.void);
|
|
1343
1361
|
};
|
|
1344
1362
|
const frame = PgProtocol.encodeCancelRequest({
|
|
1345
|
-
pid,
|
|
1346
|
-
secret
|
|
1363
|
+
pid: session.processId,
|
|
1364
|
+
secret: session.secretKey
|
|
1347
1365
|
});
|
|
1348
1366
|
// After the frame is written the server processes the request and closes
|
|
1349
1367
|
// the connection, which lands in the `close` handler.
|
|
@@ -1354,8 +1372,8 @@ const sendCancelRequest = (config, pid, secret) => Effect.callback(resume => {
|
|
|
1354
1372
|
if (config.ssl === false) return send();
|
|
1355
1373
|
socket.once("data", chunk => {
|
|
1356
1374
|
if (done) return;
|
|
1357
|
-
//
|
|
1358
|
-
|
|
1375
|
+
// A TLS session's cancel secret must stay encrypted.
|
|
1376
|
+
if (chunk.length === 1 && chunk[0] === 0x4e && config.sslOptional && !session.encrypted) return send();
|
|
1359
1377
|
if (chunk.length !== 1 || chunk[0] !== 0x53) return finish();
|
|
1360
1378
|
const raw = socket;
|
|
1361
1379
|
raw.off("error", finish);
|
|
@@ -1390,9 +1408,10 @@ const sendCancelRequest = (config, pid, secret) => Effect.callback(resume => {
|
|
|
1390
1408
|
if (config.stream !== undefined) begin();else socket.once("connect", begin);
|
|
1391
1409
|
return Effect.sync(finish);
|
|
1392
1410
|
});
|
|
1393
|
-
const connect = config => Effect.callback(resume => {
|
|
1411
|
+
const connect = (config, resolvedPassword) => Effect.callback(resume => {
|
|
1394
1412
|
let done = false;
|
|
1395
1413
|
let socket;
|
|
1414
|
+
let encrypted = false;
|
|
1396
1415
|
let parser;
|
|
1397
1416
|
let sslErrorParser;
|
|
1398
1417
|
let scram;
|
|
@@ -1419,11 +1438,11 @@ const connect = config => Effect.callback(resume => {
|
|
|
1419
1438
|
const onError = cause => failConnect(cause, "PgConnection: Failed to connect");
|
|
1420
1439
|
const onClose = () => failConnect(new Error("Connection closed unexpectedly"), "PgConnection: Connection closed during startup");
|
|
1421
1440
|
const password = () => {
|
|
1422
|
-
if (
|
|
1441
|
+
if (resolvedPassword === undefined) {
|
|
1423
1442
|
failAuth(new Error("The server requested password authentication"), "PgConnection: No password configured");
|
|
1424
1443
|
return undefined;
|
|
1425
1444
|
}
|
|
1426
|
-
return
|
|
1445
|
+
return resolvedPassword;
|
|
1427
1446
|
};
|
|
1428
1447
|
const handleMessage = message => {
|
|
1429
1448
|
switch (message._tag) {
|
|
@@ -1525,6 +1544,7 @@ const connect = config => Effect.callback(resume => {
|
|
|
1525
1544
|
socket.on("error", ignoreError);
|
|
1526
1545
|
resume(Effect.succeed({
|
|
1527
1546
|
socket,
|
|
1547
|
+
encrypted,
|
|
1528
1548
|
parser: parser,
|
|
1529
1549
|
processId,
|
|
1530
1550
|
secretKey
|
|
@@ -1551,11 +1571,7 @@ const connect = config => Effect.callback(resume => {
|
|
|
1551
1571
|
maxMessageSize: config.maxMessageSize
|
|
1552
1572
|
});
|
|
1553
1573
|
socket.on("data", onData);
|
|
1554
|
-
socket.write(PgProtocol.encodeStartupMessage(
|
|
1555
|
-
user: config.username,
|
|
1556
|
-
database: config.database,
|
|
1557
|
-
application_name: config.applicationName
|
|
1558
|
-
}));
|
|
1574
|
+
socket.write(PgProtocol.encodeStartupMessage(config.startupParameters));
|
|
1559
1575
|
};
|
|
1560
1576
|
const onSslResponse = chunk => {
|
|
1561
1577
|
if (done) return;
|
|
@@ -1586,6 +1602,7 @@ const connect = config => Effect.callback(resume => {
|
|
|
1586
1602
|
return failConnect(response.failure, "PgConnection: Invalid SSLRequest response");
|
|
1587
1603
|
}
|
|
1588
1604
|
if (response.success === "N") {
|
|
1605
|
+
if (config.sslOptional) return startup();
|
|
1589
1606
|
return failConnect(new Error("The server does not support TLS"), "PgConnection: Server refused TLS");
|
|
1590
1607
|
}
|
|
1591
1608
|
const raw = socket;
|
|
@@ -1599,7 +1616,10 @@ const connect = config => Effect.callback(resume => {
|
|
|
1599
1616
|
});
|
|
1600
1617
|
socket.on("error", onError);
|
|
1601
1618
|
socket.on("close", onClose);
|
|
1602
|
-
socket.once("secureConnect",
|
|
1619
|
+
socket.once("secureConnect", () => {
|
|
1620
|
+
encrypted = true;
|
|
1621
|
+
startup();
|
|
1622
|
+
});
|
|
1603
1623
|
};
|
|
1604
1624
|
const begin = () => {
|
|
1605
1625
|
if (config.ssl === false) return startup();
|
|
@@ -1649,7 +1669,7 @@ const configError = (message, cause) => new SqlError({
|
|
|
1649
1669
|
})
|
|
1650
1670
|
});
|
|
1651
1671
|
const resolveConfig = options => Effect.suspend(() => {
|
|
1652
|
-
const parsed = options.url !== undefined ? parseUrl(Redacted.value(options.url)
|
|
1672
|
+
const parsed = options.url !== undefined ? parseUrl(Redacted.value(options.url)) : EffectResult.succeed({});
|
|
1653
1673
|
if (EffectResult.isFailure(parsed)) return Effect.fail(parsed.failure);
|
|
1654
1674
|
const url = parsed.success;
|
|
1655
1675
|
const host = options.host ?? url.host ?? "localhost";
|
|
@@ -1658,16 +1678,47 @@ const resolveConfig = options => Effect.suspend(() => {
|
|
|
1658
1678
|
if (username === undefined) {
|
|
1659
1679
|
return Effect.fail(configError("No username configured"));
|
|
1660
1680
|
}
|
|
1681
|
+
const named = Object.create(null);
|
|
1682
|
+
for (const [name, value] of Object.entries(options.startupParameters ?? {})) {
|
|
1683
|
+
const key = name.toLowerCase();
|
|
1684
|
+
if (key === "user" || key === "database" || key === "replication" || key === "options") {
|
|
1685
|
+
return Effect.fail(configError(`Reserved startup parameter: "${name}"`));
|
|
1686
|
+
}
|
|
1687
|
+
if (key === "client_encoding") {
|
|
1688
|
+
const encoding = value.toUpperCase();
|
|
1689
|
+
if (encoding !== "UTF8" && encoding !== "UTF-8") {
|
|
1690
|
+
return Effect.fail(configError("Startup parameter client_encoding must be UTF8 or UTF-8"));
|
|
1691
|
+
}
|
|
1692
|
+
named[key] = "UTF8";
|
|
1693
|
+
} else {
|
|
1694
|
+
named[key] = value;
|
|
1695
|
+
}
|
|
1696
|
+
if (name === "" || name.includes("\0") || value.includes("\0")) {
|
|
1697
|
+
return Effect.fail(configError("Startup parameter names must be nonempty and names/values must not contain NUL"));
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const startupParameters = {
|
|
1701
|
+
...named,
|
|
1702
|
+
user: username,
|
|
1703
|
+
database: options.database ?? url.database,
|
|
1704
|
+
application_name: options.applicationName ?? named.application_name ?? url.applicationName ?? "@effect/sql-pg",
|
|
1705
|
+
options: options.startupOptions ?? url.options
|
|
1706
|
+
};
|
|
1707
|
+
for (const [name, value] of Object.entries(startupParameters)) {
|
|
1708
|
+
if (value?.includes("\0")) {
|
|
1709
|
+
return Effect.fail(configError(`Startup parameter "${name}" must not contain NUL`));
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1661
1712
|
return Effect.succeed({
|
|
1662
1713
|
host,
|
|
1663
1714
|
port,
|
|
1664
1715
|
path: options.path ?? (host.startsWith("/") ? `${host}/.s.PGSQL.${port}` : undefined),
|
|
1665
|
-
ssl: options.ssl ?? url.ssl ?? false,
|
|
1666
|
-
|
|
1716
|
+
ssl: options.ssl ?? (url.ssl === "prefer" ? true : url.ssl ?? false),
|
|
1717
|
+
sslOptional: options.ssl === undefined && url.ssl === "prefer",
|
|
1667
1718
|
username,
|
|
1668
|
-
password: options.password !== undefined ? Redacted.
|
|
1719
|
+
password: options.password ?? (url.password !== undefined ? Redacted.make(url.password) : undefined),
|
|
1669
1720
|
connectTimeout: Duration.fromInputUnsafe(options.connectTimeout ?? url.connectTimeout ?? Duration.seconds(5)),
|
|
1670
|
-
|
|
1721
|
+
startupParameters,
|
|
1671
1722
|
stream: options.stream,
|
|
1672
1723
|
maxMessageSize: options.maxMessageSize
|
|
1673
1724
|
});
|
|
@@ -1683,7 +1734,7 @@ const parsePort = (value, what) => {
|
|
|
1683
1734
|
const port = Number(value);
|
|
1684
1735
|
return !Number.isInteger(port) || port < 1 || port > 65535 ? EffectResult.fail(configError(`Invalid port in URL ${what}: "${value}"`)) : EffectResult.succeed(port);
|
|
1685
1736
|
};
|
|
1686
|
-
const parseUrl =
|
|
1737
|
+
const parseUrl = raw => {
|
|
1687
1738
|
let url;
|
|
1688
1739
|
try {
|
|
1689
1740
|
url = new URL(raw);
|
|
@@ -1745,6 +1796,9 @@ const parseUrl = (raw, hasExplicitSsl) => {
|
|
|
1745
1796
|
case "application_name":
|
|
1746
1797
|
config.applicationName = value;
|
|
1747
1798
|
break;
|
|
1799
|
+
case "options":
|
|
1800
|
+
config.options = value;
|
|
1801
|
+
break;
|
|
1748
1802
|
case "connect_timeout":
|
|
1749
1803
|
{
|
|
1750
1804
|
const seconds = Number(value);
|
|
@@ -1766,8 +1820,8 @@ const parseUrl = (raw, hasExplicitSsl) => {
|
|
|
1766
1820
|
break;
|
|
1767
1821
|
case "prefer":
|
|
1768
1822
|
case "allow":
|
|
1769
|
-
|
|
1770
|
-
|
|
1823
|
+
config.ssl = "prefer";
|
|
1824
|
+
break;
|
|
1771
1825
|
default:
|
|
1772
1826
|
return EffectResult.fail(configError(`Unrecognized sslmode in URL: "${value}"`));
|
|
1773
1827
|
}
|