@effect/sql-pg 4.0.0-beta.1 → 4.0.0-beta.100
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 +1 -1
- package/dist/PgClient.d.ts +119 -38
- package/dist/PgClient.d.ts.map +1 -1
- package/dist/PgClient.js +421 -198
- package/dist/PgClient.js.map +1 -1
- package/dist/PgMigrator.d.ts +25 -9
- package/dist/PgMigrator.d.ts.map +1 -1
- package/dist/PgMigrator.js +65 -60
- package/dist/PgMigrator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/package.json +11 -12
- package/src/PgClient.ts +628 -272
- package/src/PgMigrator.ts +91 -65
- package/src/index.ts +3 -3
package/src/PgClient.ts
CHANGED
|
@@ -1,25 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Connects Effect SQL to PostgreSQL using the `pg` package.
|
|
3
|
+
*
|
|
4
|
+
* This module provides constructors and layers for building a PostgreSQL
|
|
5
|
+
* client from pool settings, a managed `pg.Client`, an existing `pg.Pool`, or
|
|
6
|
+
* custom connection code. The client runs Effect SQL queries against
|
|
7
|
+
* PostgreSQL, including transactions and streamed results, and adds helpers for
|
|
8
|
+
* JSON values and LISTEN/NOTIFY messages. It also maps common PostgreSQL
|
|
9
|
+
* failures, such as connection, authentication, constraint, timeout, and
|
|
10
|
+
* deadlock errors, into Effect SQL errors.
|
|
11
|
+
*
|
|
12
|
+
* @since 4.0.0
|
|
3
13
|
*/
|
|
4
14
|
import * as Arr from "effect/Array"
|
|
5
15
|
import * as Cause from "effect/Cause"
|
|
6
16
|
import * as Channel from "effect/Channel"
|
|
7
17
|
import * as Config from "effect/Config"
|
|
18
|
+
import * as Context from "effect/Context"
|
|
8
19
|
import * as Duration from "effect/Duration"
|
|
9
20
|
import * as Effect from "effect/Effect"
|
|
10
21
|
import * as Fiber from "effect/Fiber"
|
|
11
22
|
import * as Layer from "effect/Layer"
|
|
12
23
|
import * as Number from "effect/Number"
|
|
24
|
+
import * as Option from "effect/Option"
|
|
13
25
|
import * as Queue from "effect/Queue"
|
|
14
26
|
import * as RcRef from "effect/RcRef"
|
|
15
27
|
import * as Redacted from "effect/Redacted"
|
|
16
28
|
import * as Scope from "effect/Scope"
|
|
17
|
-
import * as
|
|
29
|
+
import * as Semaphore from "effect/Semaphore"
|
|
18
30
|
import * as Stream from "effect/Stream"
|
|
19
31
|
import * as Reactivity from "effect/unstable/reactivity/Reactivity"
|
|
20
32
|
import * as Client from "effect/unstable/sql/SqlClient"
|
|
21
33
|
import type { Connection } from "effect/unstable/sql/SqlConnection"
|
|
22
|
-
import
|
|
34
|
+
import type * as SqlConnection from "effect/unstable/sql/SqlConnection"
|
|
35
|
+
import {
|
|
36
|
+
AuthenticationError,
|
|
37
|
+
AuthorizationError,
|
|
38
|
+
ConnectionError,
|
|
39
|
+
ConstraintError,
|
|
40
|
+
DeadlockError,
|
|
41
|
+
LockTimeoutError,
|
|
42
|
+
SerializationError,
|
|
43
|
+
SqlError,
|
|
44
|
+
SqlSyntaxError,
|
|
45
|
+
StatementTimeoutError,
|
|
46
|
+
UniqueViolation,
|
|
47
|
+
UnknownError
|
|
48
|
+
} from "effect/unstable/sql/SqlError"
|
|
23
49
|
import type { Custom, Fragment } from "effect/unstable/sql/Statement"
|
|
24
50
|
import * as Statement from "effect/unstable/sql/Statement"
|
|
25
51
|
import type { Duplex } from "node:stream"
|
|
@@ -28,26 +54,27 @@ import * as Pg from "pg"
|
|
|
28
54
|
import * as PgConnString from "pg-connection-string"
|
|
29
55
|
import Cursor from "pg-cursor"
|
|
30
56
|
|
|
31
|
-
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
32
|
-
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
33
|
-
const ATTR_SERVER_ADDRESS = "server.address"
|
|
34
|
-
const ATTR_SERVER_PORT = "server.port"
|
|
35
|
-
|
|
36
57
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
58
|
+
* Runtime type identifier used to mark `PgClient` values.
|
|
59
|
+
*
|
|
60
|
+
* @category type IDs
|
|
61
|
+
* @since 4.0.0
|
|
39
62
|
*/
|
|
40
63
|
export const TypeId: TypeId = "~@effect/sql-pg/PgClient"
|
|
41
64
|
|
|
42
65
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
66
|
+
* Type-level identifier used to mark `PgClient` values.
|
|
67
|
+
*
|
|
68
|
+
* @category type IDs
|
|
69
|
+
* @since 4.0.0
|
|
45
70
|
*/
|
|
46
71
|
export type TypeId = "~@effect/sql-pg/PgClient"
|
|
47
72
|
|
|
48
73
|
/**
|
|
74
|
+
* PostgreSQL client service, extending `SqlClient` with JSON parameter fragments and LISTEN/NOTIFY helpers.
|
|
75
|
+
*
|
|
49
76
|
* @category models
|
|
50
|
-
* @since
|
|
77
|
+
* @since 4.0.0
|
|
51
78
|
*/
|
|
52
79
|
export interface PgClient extends Client.SqlClient {
|
|
53
80
|
readonly [TypeId]: TypeId
|
|
@@ -58,14 +85,22 @@ export interface PgClient extends Client.SqlClient {
|
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
88
|
+
* Service tag for the PostgreSQL client service.
|
|
89
|
+
*
|
|
90
|
+
* **When to use**
|
|
91
|
+
*
|
|
92
|
+
* Use to access or provide a PostgreSQL client through the Effect context.
|
|
93
|
+
*
|
|
94
|
+
* @category services
|
|
95
|
+
* @since 4.0.0
|
|
63
96
|
*/
|
|
64
|
-
export const PgClient =
|
|
97
|
+
export const PgClient = Context.Service<PgClient>("@effect/sql-pg/PgClient")
|
|
65
98
|
|
|
66
99
|
/**
|
|
100
|
+
* Configuration for a PostgreSQL client, including connection, TLS, custom stream, application name, type parser, JSON transform, and query/result name transform options.
|
|
101
|
+
*
|
|
67
102
|
* @category constructors
|
|
68
|
-
* @since
|
|
103
|
+
* @since 4.0.0
|
|
69
104
|
*/
|
|
70
105
|
export interface PgClientConfig {
|
|
71
106
|
readonly url?: Redacted.Redacted | undefined
|
|
@@ -78,14 +113,9 @@ export interface PgClientConfig {
|
|
|
78
113
|
readonly username?: string | undefined
|
|
79
114
|
readonly password?: Redacted.Redacted | undefined
|
|
80
115
|
|
|
81
|
-
readonly
|
|
82
|
-
|
|
83
|
-
readonly idleTimeout?: Duration.DurationInput | undefined
|
|
84
|
-
readonly connectTimeout?: Duration.DurationInput | undefined
|
|
116
|
+
readonly connectTimeout?: Duration.Input | undefined
|
|
85
117
|
|
|
86
|
-
readonly
|
|
87
|
-
readonly minConnections?: number | undefined
|
|
88
|
-
readonly connectionTTL?: Duration.DurationInput | undefined
|
|
118
|
+
readonly stream?: (() => Duplex) | undefined
|
|
89
119
|
|
|
90
120
|
readonly applicationName?: string | undefined
|
|
91
121
|
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
@@ -97,12 +127,26 @@ export interface PgClientConfig {
|
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/**
|
|
130
|
+
* PostgreSQL pool configuration, extending `PgClientConfig` with idle timeout, pool size, and connection lifetime settings.
|
|
131
|
+
*
|
|
100
132
|
* @category constructors
|
|
101
|
-
* @since
|
|
133
|
+
* @since 4.0.0
|
|
102
134
|
*/
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
135
|
+
export interface PgPoolConfig extends PgClientConfig {
|
|
136
|
+
readonly idleTimeout?: Duration.Input | undefined
|
|
137
|
+
|
|
138
|
+
readonly maxConnections?: number | undefined
|
|
139
|
+
readonly minConnections?: number | undefined
|
|
140
|
+
readonly connectionTTL?: Duration.Input | undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates a scoped PostgreSQL client backed by a managed `pg` connection pool.
|
|
145
|
+
*
|
|
146
|
+
* @category constructors
|
|
147
|
+
* @since 4.0.0
|
|
148
|
+
*/
|
|
149
|
+
export const make = (options: PgPoolConfig): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
106
150
|
fromPool({
|
|
107
151
|
...options,
|
|
108
152
|
acquire: Effect.gen(function*() {
|
|
@@ -116,15 +160,15 @@ export const make = (
|
|
|
116
160
|
port: options.port,
|
|
117
161
|
...(options.stream ? { stream: options.stream } : {}),
|
|
118
162
|
connectionTimeoutMillis: options.connectTimeout
|
|
119
|
-
? Duration.toMillis(Duration.
|
|
163
|
+
? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout))
|
|
120
164
|
: undefined,
|
|
121
165
|
idleTimeoutMillis: options.idleTimeout
|
|
122
|
-
? Duration.toMillis(Duration.
|
|
166
|
+
? Duration.toMillis(Duration.fromInputUnsafe(options.idleTimeout))
|
|
123
167
|
: undefined,
|
|
124
168
|
max: options.maxConnections,
|
|
125
169
|
min: options.minConnections,
|
|
126
170
|
maxLifetimeSeconds: options.connectionTTL
|
|
127
|
-
? Duration.toSeconds(Duration.
|
|
171
|
+
? Duration.toSeconds(Duration.fromInputUnsafe(options.connectionTTL))
|
|
128
172
|
: undefined,
|
|
129
173
|
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
130
174
|
types: options.types
|
|
@@ -135,20 +179,24 @@ export const make = (
|
|
|
135
179
|
yield* Effect.acquireRelease(
|
|
136
180
|
Effect.tryPromise({
|
|
137
181
|
try: () => pool.query("SELECT 1"),
|
|
138
|
-
catch: (cause) => new SqlError({ cause,
|
|
182
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
139
183
|
}),
|
|
140
184
|
() =>
|
|
141
185
|
Effect.promise(() => pool.end()).pipe(
|
|
142
186
|
Effect.timeoutOption(1000)
|
|
143
|
-
)
|
|
187
|
+
),
|
|
188
|
+
{ interruptible: true }
|
|
144
189
|
).pipe(
|
|
145
190
|
Effect.timeoutOrElse({
|
|
146
191
|
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
147
|
-
|
|
192
|
+
orElse: () =>
|
|
148
193
|
Effect.fail(
|
|
149
194
|
new SqlError({
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
reason: new ConnectionError({
|
|
196
|
+
cause: new Error("Connection timed out"),
|
|
197
|
+
message: "PgClient: Connection timed out",
|
|
198
|
+
operation: "connect"
|
|
199
|
+
})
|
|
152
200
|
})
|
|
153
201
|
)
|
|
154
202
|
})
|
|
@@ -159,8 +207,69 @@ export const make = (
|
|
|
159
207
|
})
|
|
160
208
|
|
|
161
209
|
/**
|
|
210
|
+
* Creates a scoped PostgreSQL client backed by a managed single `pg` client, optionally acquiring a separate client for streaming and LISTEN operations.
|
|
211
|
+
*
|
|
212
|
+
* @category constructors
|
|
213
|
+
* @since 4.0.0
|
|
214
|
+
*/
|
|
215
|
+
export const makeClient = (
|
|
216
|
+
options: PgClientConfig & {
|
|
217
|
+
/**
|
|
218
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen
|
|
219
|
+
*/
|
|
220
|
+
readonly acquireForStream?: boolean | undefined
|
|
221
|
+
}
|
|
222
|
+
): Effect.Effect<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> =>
|
|
223
|
+
fromClient({
|
|
224
|
+
...options,
|
|
225
|
+
acquire: Effect.acquireRelease(
|
|
226
|
+
Effect.tryPromise({
|
|
227
|
+
try: async () => {
|
|
228
|
+
const client = new Pg.Client({
|
|
229
|
+
connectionString: options.url ? Redacted.value(options.url) : undefined,
|
|
230
|
+
user: options.username,
|
|
231
|
+
host: options.host,
|
|
232
|
+
database: options.database,
|
|
233
|
+
password: options.password ? Redacted.value(options.password) : undefined,
|
|
234
|
+
ssl: options.ssl,
|
|
235
|
+
port: options.port,
|
|
236
|
+
...(options.stream ? { stream: options.stream } : {}),
|
|
237
|
+
application_name: options.applicationName ?? "@effect/sql-pg",
|
|
238
|
+
types: options.types
|
|
239
|
+
})
|
|
240
|
+
await client.connect()
|
|
241
|
+
return client
|
|
242
|
+
},
|
|
243
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") })
|
|
244
|
+
}),
|
|
245
|
+
(client) =>
|
|
246
|
+
Effect.promise(() => client.end()).pipe(
|
|
247
|
+
Effect.timeoutOption(1000)
|
|
248
|
+
),
|
|
249
|
+
{ interruptible: true }
|
|
250
|
+
).pipe(
|
|
251
|
+
Effect.timeoutOrElse({
|
|
252
|
+
duration: options.connectTimeout ?? Duration.seconds(5),
|
|
253
|
+
orElse: () =>
|
|
254
|
+
Effect.fail(
|
|
255
|
+
new SqlError({
|
|
256
|
+
reason: new ConnectionError({
|
|
257
|
+
cause: new Error("Connection timed out"),
|
|
258
|
+
message: "PgClient: Connection timed out",
|
|
259
|
+
operation: "connect"
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
),
|
|
265
|
+
acquireForStream: options.acquireForStream ?? false
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool.
|
|
270
|
+
*
|
|
162
271
|
* @category constructors
|
|
163
|
-
* @since
|
|
272
|
+
* @since 4.0.0
|
|
164
273
|
*/
|
|
165
274
|
export const fromPool = Effect.fnUntraced(function*(
|
|
166
275
|
options: {
|
|
@@ -175,197 +284,149 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
175
284
|
readonly types?: Pg.CustomTypesConfig | undefined
|
|
176
285
|
}
|
|
177
286
|
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
178
|
-
const compiler = makeCompiler(
|
|
179
|
-
options.transformQueryNames,
|
|
180
|
-
options.transformJson
|
|
181
|
-
)
|
|
182
|
-
const transformRows = options.transformResultNames ?
|
|
183
|
-
Statement.defaultTransforms(
|
|
184
|
-
options.transformResultNames,
|
|
185
|
-
options.transformJson
|
|
186
|
-
).array :
|
|
187
|
-
undefined
|
|
188
|
-
|
|
189
287
|
const pool = yield* options.acquire
|
|
190
288
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (this.pg !== undefined) {
|
|
199
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
200
|
-
f(this.pg!, resume)
|
|
201
|
-
return makeCancel(pool, this.pg!)
|
|
202
|
-
})
|
|
203
|
-
}
|
|
204
|
-
return Effect.callback<A, SqlError>((resume) => {
|
|
205
|
-
let done = false
|
|
206
|
-
let cancel: Effect.Effect<void> | undefined = undefined
|
|
207
|
-
let client: Pg.PoolClient | undefined = undefined
|
|
208
|
-
function onError(cause: Error) {
|
|
209
|
-
cleanup(cause)
|
|
210
|
-
resume(Effect.fail(new SqlError({ cause, message: "Connection error" })))
|
|
211
|
-
}
|
|
212
|
-
function cleanup(cause?: Error) {
|
|
213
|
-
if (!done) client?.release(cause)
|
|
214
|
-
done = true
|
|
215
|
-
client?.off("error", onError)
|
|
216
|
-
}
|
|
217
|
-
pool.connect((cause, client_) => {
|
|
218
|
-
if (cause) {
|
|
219
|
-
return resume(Effect.fail(new SqlError({ cause, message: "Failed to acquire connection" })))
|
|
220
|
-
} else if (!client_) {
|
|
221
|
-
return resume(
|
|
222
|
-
Effect.fail(
|
|
223
|
-
new SqlError({ message: "Failed to acquire connection", cause: new Error("No client returned") })
|
|
224
|
-
)
|
|
225
|
-
)
|
|
226
|
-
} else if (done) {
|
|
227
|
-
client_.release()
|
|
228
|
-
return
|
|
229
|
-
}
|
|
230
|
-
client = client_
|
|
231
|
-
client.once("error", onError)
|
|
232
|
-
cancel = makeCancel(pool, client)
|
|
233
|
-
f(client, (eff) => {
|
|
234
|
-
cleanup()
|
|
235
|
-
resume(eff)
|
|
289
|
+
const makeConection = (client?: Pg.PoolClient) =>
|
|
290
|
+
new ConnectionImpl(
|
|
291
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
292
|
+
if (client !== undefined) {
|
|
293
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
294
|
+
f(client!, resume)
|
|
295
|
+
return makeCancel(pool, client!)
|
|
236
296
|
})
|
|
237
|
-
}
|
|
238
|
-
return Effect.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
249
|
-
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
250
|
-
client.query(query, params as any, (err, result) => {
|
|
251
|
-
if (err) {
|
|
252
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
253
|
-
} else {
|
|
254
|
-
// Multi-statement queries return an array of results
|
|
255
|
-
resume(Effect.succeed(
|
|
256
|
-
Array.isArray(result)
|
|
257
|
-
? result.map((r) => r.rows ?? [])
|
|
258
|
-
: result.rows ?? []
|
|
259
|
-
))
|
|
297
|
+
}
|
|
298
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
299
|
+
let done = false
|
|
300
|
+
let cancel: Effect.Effect<void> | undefined = undefined
|
|
301
|
+
let client: Pg.PoolClient | undefined = undefined
|
|
302
|
+
function onError(cause: Error) {
|
|
303
|
+
cleanup(cause)
|
|
304
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(cause, "Connection error", "acquireConnection") })))
|
|
260
305
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
execute(
|
|
266
|
-
sql: string,
|
|
267
|
-
params: ReadonlyArray<unknown>,
|
|
268
|
-
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
269
|
-
) {
|
|
270
|
-
return transformRows
|
|
271
|
-
? Effect.map(this.run(sql, params), transformRows)
|
|
272
|
-
: this.run(sql, params)
|
|
273
|
-
}
|
|
274
|
-
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
275
|
-
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
276
|
-
client.query(sql, params as any, (err, result) => {
|
|
277
|
-
if (err) {
|
|
278
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
279
|
-
} else {
|
|
280
|
-
resume(Effect.succeed(result))
|
|
306
|
+
function cleanup(cause?: Error) {
|
|
307
|
+
if (!done) client?.release(cause)
|
|
308
|
+
done = true
|
|
309
|
+
client?.off("error", onError)
|
|
281
310
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
pool.connect((cause, client_) => {
|
|
312
|
+
if (cause) {
|
|
313
|
+
return resume(
|
|
314
|
+
Effect.fail(
|
|
315
|
+
new SqlError({
|
|
316
|
+
reason: classifyError(cause, "Failed to acquire connection", "acquireConnection")
|
|
317
|
+
})
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
} else if (!client_) {
|
|
321
|
+
return resume(
|
|
322
|
+
Effect.fail(
|
|
323
|
+
new SqlError({
|
|
324
|
+
reason: new ConnectionError({
|
|
325
|
+
message: "Failed to acquire connection",
|
|
326
|
+
cause: new Error("No client returned"),
|
|
327
|
+
operation: "acquireConnection"
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
} else if (done) {
|
|
333
|
+
client_.release()
|
|
334
|
+
return
|
|
301
335
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
sql: string,
|
|
315
|
-
params: ReadonlyArray<unknown>,
|
|
316
|
-
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
317
|
-
) {
|
|
318
|
-
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
319
|
-
const self = this
|
|
320
|
-
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
321
|
-
const client = self.pg ?? (yield* Scope.provide(reserveRaw, scope))
|
|
322
|
-
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
323
|
-
const cursor = client.query(new Cursor(sql, params as any))
|
|
324
|
-
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
325
|
-
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
326
|
-
cursor.read(128, (err, rows) => {
|
|
327
|
-
if (err) {
|
|
328
|
-
resume(Effect.fail(new SqlError({ cause: err, message: "Failed to execute statement" })))
|
|
329
|
-
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
330
|
-
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
331
|
-
} else {
|
|
332
|
-
resume(Cause.done())
|
|
336
|
+
client = client_
|
|
337
|
+
client.once("error", onError)
|
|
338
|
+
cancel = makeCancel(pool, client)
|
|
339
|
+
f(client, (eff) => {
|
|
340
|
+
cleanup()
|
|
341
|
+
resume(eff)
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
return Effect.suspend(() => {
|
|
345
|
+
if (!cancel) {
|
|
346
|
+
cleanup()
|
|
347
|
+
return Effect.void
|
|
333
348
|
}
|
|
349
|
+
return Effect.ensuring(cancel, Effect.sync(cleanup))
|
|
334
350
|
})
|
|
335
351
|
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
},
|
|
353
|
+
client ? Effect.succeed(client) : reserveRaw
|
|
354
|
+
)
|
|
339
355
|
|
|
340
356
|
const reserveRaw = Effect.callback<Pg.PoolClient, SqlError, Scope.Scope>((resume) => {
|
|
341
357
|
const fiber = Fiber.getCurrent()!
|
|
342
|
-
const scope =
|
|
358
|
+
const scope = Context.getUnsafe(fiber.context, Scope.Scope)
|
|
343
359
|
let cause: Error | undefined = undefined
|
|
360
|
+
function onError(cause_: Error) {
|
|
361
|
+
cause = cause_
|
|
362
|
+
}
|
|
344
363
|
pool.connect((err, client, release) => {
|
|
345
364
|
if (err) {
|
|
346
|
-
resume(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
return resume(
|
|
366
|
+
Effect.fail(
|
|
367
|
+
new SqlError({
|
|
368
|
+
reason: classifyError(
|
|
369
|
+
err,
|
|
370
|
+
"Failed to acquire connection for transaction",
|
|
371
|
+
"acquireConnection"
|
|
372
|
+
)
|
|
354
373
|
})
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
} else if (!client) {
|
|
377
|
+
return resume(
|
|
378
|
+
Effect.fail(
|
|
379
|
+
new SqlError({
|
|
380
|
+
reason: new ConnectionError({
|
|
381
|
+
message: "Failed to acquire connection for transaction",
|
|
382
|
+
cause: new Error("No client returned"),
|
|
383
|
+
operation: "acquireConnection"
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
)
|
|
387
|
+
)
|
|
361
388
|
}
|
|
362
|
-
client
|
|
389
|
+
client.on("error", onError)
|
|
390
|
+
resume(Effect.as(
|
|
391
|
+
Scope.addFinalizer(
|
|
392
|
+
scope,
|
|
393
|
+
Effect.sync(() => {
|
|
394
|
+
client.off("error", onError)
|
|
395
|
+
release(cause)
|
|
396
|
+
})
|
|
397
|
+
),
|
|
398
|
+
client
|
|
399
|
+
))
|
|
363
400
|
})
|
|
364
401
|
})
|
|
365
|
-
const reserve = Effect.map(reserveRaw,
|
|
402
|
+
const reserve = Effect.map(reserveRaw, makeConection)
|
|
403
|
+
|
|
404
|
+
const onListenClientError = (_: Error) => {
|
|
405
|
+
}
|
|
366
406
|
|
|
367
|
-
const
|
|
368
|
-
acquire:
|
|
407
|
+
const listenAcquirer = yield* RcRef.make({
|
|
408
|
+
acquire: Effect.acquireRelease(
|
|
409
|
+
Effect.tryPromise({
|
|
410
|
+
try: async () => {
|
|
411
|
+
const client = new Pg.Client(pool.options)
|
|
412
|
+
await client.connect()
|
|
413
|
+
client.on("error", onListenClientError)
|
|
414
|
+
return client
|
|
415
|
+
},
|
|
416
|
+
catch: (cause) =>
|
|
417
|
+
new SqlError({
|
|
418
|
+
reason: classifyError(cause, "Failed to acquire connection for listen", "acquireConnection")
|
|
419
|
+
})
|
|
420
|
+
}),
|
|
421
|
+
(client) =>
|
|
422
|
+
Effect.promise(() => {
|
|
423
|
+
client.off("error", onListenClientError)
|
|
424
|
+
return client.end()
|
|
425
|
+
}).pipe(
|
|
426
|
+
Effect.timeoutOption(1000)
|
|
427
|
+
),
|
|
428
|
+
{ interruptible: true }
|
|
429
|
+
)
|
|
369
430
|
})
|
|
370
431
|
|
|
371
432
|
let config: PgClientConfig = {
|
|
@@ -386,7 +447,7 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
386
447
|
config = {
|
|
387
448
|
...config,
|
|
388
449
|
host: config.host ?? parsed.host ?? undefined,
|
|
389
|
-
port: config.port ?? (parsed.port ? Number.parse(parsed.port) : undefined),
|
|
450
|
+
port: config.port ?? (parsed.port ? Option.getOrUndefined(Number.parse(parsed.port)) : undefined),
|
|
390
451
|
username: config.username ?? parsed.user ?? undefined,
|
|
391
452
|
password: config.password ?? (parsed.password ? Redacted.make(parsed.password) : undefined),
|
|
392
453
|
database: config.database ?? parsed.database ?? undefined
|
|
@@ -396,10 +457,136 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
396
457
|
}
|
|
397
458
|
}
|
|
398
459
|
|
|
460
|
+
return yield* makeWith({
|
|
461
|
+
acquirer: Effect.succeed(makeConection()),
|
|
462
|
+
transactionAcquirer: reserve,
|
|
463
|
+
listenAcquirer: RcRef.get(listenAcquirer),
|
|
464
|
+
config,
|
|
465
|
+
spanAttributes: options.spanAttributes,
|
|
466
|
+
transformResultNames: options.transformResultNames,
|
|
467
|
+
transformQueryNames: options.transformQueryNames,
|
|
468
|
+
transformJson: options.transformJson
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Builds a PostgreSQL client from a scoped `pg` client acquisition effect, serializing access when sharing the client and optionally using separate clients for streams and LISTEN.
|
|
474
|
+
*
|
|
475
|
+
* @category constructors
|
|
476
|
+
* @since 4.0.0
|
|
477
|
+
*/
|
|
478
|
+
export const fromClient = Effect.fnUntraced(function*(
|
|
479
|
+
options: {
|
|
480
|
+
readonly acquire: Effect.Effect<Pg.Client, SqlError, Scope.Scope>
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Whether to acquire a separate client for each sql.stream / sql.listen.
|
|
484
|
+
*/
|
|
485
|
+
readonly acquireForStream: boolean
|
|
486
|
+
|
|
487
|
+
readonly applicationName?: string | undefined
|
|
488
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
489
|
+
|
|
490
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
491
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
492
|
+
readonly transformJson?: boolean | undefined
|
|
493
|
+
readonly types?: Pg.CustomTypesConfig | undefined
|
|
494
|
+
}
|
|
495
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
496
|
+
function onError() {}
|
|
497
|
+
const acquireWithErrorHandler = options.acquire.pipe(
|
|
498
|
+
Effect.tap((client) => {
|
|
499
|
+
client.on("error", onError)
|
|
500
|
+
return Effect.addFinalizer(() => {
|
|
501
|
+
client.off("error", onError)
|
|
502
|
+
return Effect.void
|
|
503
|
+
})
|
|
504
|
+
})
|
|
505
|
+
)
|
|
506
|
+
const client = yield* acquireWithErrorHandler
|
|
507
|
+
|
|
508
|
+
const semaphore = Semaphore.makeUnsafe(1)
|
|
509
|
+
let streamClient = options.acquireForStream ? acquireWithErrorHandler : Effect.acquireRelease(
|
|
510
|
+
Effect.as(semaphore.take(1), client),
|
|
511
|
+
() => semaphore.release(1)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
const makeConection = (client: Pg.Client) =>
|
|
515
|
+
new ConnectionImpl(
|
|
516
|
+
function runWithClient<A>(f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void) {
|
|
517
|
+
return Effect.callback<A, SqlError>((resume) => {
|
|
518
|
+
f(client, resume)
|
|
519
|
+
})
|
|
520
|
+
},
|
|
521
|
+
streamClient
|
|
522
|
+
)
|
|
523
|
+
const connection = makeConection(client)
|
|
524
|
+
const acquirer = semaphore.withPermit(Effect.succeed(connection))
|
|
525
|
+
|
|
526
|
+
const config: PgClientConfig = {
|
|
527
|
+
...options,
|
|
528
|
+
host: client.host,
|
|
529
|
+
port: client.port,
|
|
530
|
+
database: client.database,
|
|
531
|
+
username: client.user,
|
|
532
|
+
password: typeof client.password === "string" ? Redacted.make(client.password) : undefined,
|
|
533
|
+
ssl: client.ssl
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return yield* makeWith({
|
|
537
|
+
acquirer,
|
|
538
|
+
transactionAcquirer: acquirer,
|
|
539
|
+
listenAcquirer: streamClient,
|
|
540
|
+
config,
|
|
541
|
+
spanAttributes: options.spanAttributes,
|
|
542
|
+
transformResultNames: options.transformResultNames,
|
|
543
|
+
transformQueryNames: options.transformQueryNames,
|
|
544
|
+
transformJson: options.transformJson
|
|
545
|
+
})
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Creates a `PgClient` from SQL connection acquirers, a LISTEN acquirer, client configuration, and transformation options.
|
|
550
|
+
*
|
|
551
|
+
* **When to use**
|
|
552
|
+
*
|
|
553
|
+
* Use to build a PostgreSQL client from custom connection acquisition logic
|
|
554
|
+
* instead of the built-in pool or single-client constructors.
|
|
555
|
+
*
|
|
556
|
+
* @category constructors
|
|
557
|
+
* @since 4.0.0
|
|
558
|
+
*/
|
|
559
|
+
export const makeWith = Effect.fnUntraced(function*(
|
|
560
|
+
options: {
|
|
561
|
+
readonly acquirer: SqlConnection.Acquirer
|
|
562
|
+
readonly transactionAcquirer: SqlConnection.Acquirer
|
|
563
|
+
readonly listenAcquirer: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
564
|
+
|
|
565
|
+
readonly config: PgClientConfig
|
|
566
|
+
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
567
|
+
|
|
568
|
+
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
569
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
570
|
+
readonly transformJson?: boolean | undefined
|
|
571
|
+
}
|
|
572
|
+
): Effect.fn.Return<PgClient, SqlError, Scope.Scope | Reactivity.Reactivity> {
|
|
573
|
+
const compiler = makeCompiler(
|
|
574
|
+
options.transformQueryNames,
|
|
575
|
+
options.transformJson
|
|
576
|
+
)
|
|
577
|
+
const transformRows = options.transformResultNames ?
|
|
578
|
+
Statement.defaultTransforms(
|
|
579
|
+
options.transformResultNames,
|
|
580
|
+
options.transformJson
|
|
581
|
+
).array :
|
|
582
|
+
undefined
|
|
583
|
+
|
|
584
|
+
const config = options.config
|
|
585
|
+
|
|
399
586
|
return Object.assign(
|
|
400
587
|
yield* Client.make({
|
|
401
|
-
acquirer:
|
|
402
|
-
transactionAcquirer:
|
|
588
|
+
acquirer: options.acquirer,
|
|
589
|
+
transactionAcquirer: options.transactionAcquirer,
|
|
403
590
|
compiler,
|
|
404
591
|
spanAttributes: [
|
|
405
592
|
...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
|
|
@@ -412,11 +599,11 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
412
599
|
}),
|
|
413
600
|
{
|
|
414
601
|
[TypeId]: TypeId as TypeId,
|
|
415
|
-
config,
|
|
602
|
+
config: options.config,
|
|
416
603
|
json: (_: unknown) => Statement.fragment([PgJson(_)]),
|
|
417
604
|
listen: (channel: string) =>
|
|
418
605
|
Stream.callback<string, SqlError>(Effect.fnUntraced(function*(queue) {
|
|
419
|
-
const client = yield*
|
|
606
|
+
const client = yield* options.listenAcquirer
|
|
420
607
|
function onNotification(msg: Pg.Notification) {
|
|
421
608
|
if (msg.channel === channel && msg.payload) {
|
|
422
609
|
Queue.offerUnsafe(queue, msg.payload)
|
|
@@ -430,24 +617,136 @@ export const fromPool = Effect.fnUntraced(function*(
|
|
|
430
617
|
)
|
|
431
618
|
yield* Effect.tryPromise({
|
|
432
619
|
try: () => client.query(`LISTEN ${Pg.escapeIdentifier(channel)}`),
|
|
433
|
-
catch: (cause) => new SqlError({ cause,
|
|
620
|
+
catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to listen", "listen") })
|
|
434
621
|
})
|
|
435
622
|
client.on("notification", onNotification)
|
|
436
623
|
})),
|
|
437
624
|
notify: (channel: string, payload: string) =>
|
|
438
|
-
Effect.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else {
|
|
443
|
-
resume(Effect.void)
|
|
444
|
-
}
|
|
445
|
-
})
|
|
446
|
-
})
|
|
625
|
+
Effect.asVoid(Effect.scoped(Effect.flatMap(
|
|
626
|
+
options.acquirer,
|
|
627
|
+
(conn) => conn.executeRaw(`SELECT pg_notify($1, $2)`, [channel, payload])
|
|
628
|
+
)))
|
|
447
629
|
}
|
|
448
630
|
)
|
|
449
631
|
})
|
|
450
632
|
|
|
633
|
+
class ConnectionImpl implements Connection {
|
|
634
|
+
constructor(
|
|
635
|
+
runWithClient: <A>(
|
|
636
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
637
|
+
) => Effect.Effect<A, SqlError>,
|
|
638
|
+
reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
639
|
+
) {
|
|
640
|
+
this.runWithClient = runWithClient
|
|
641
|
+
this.reserve = reserve
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private readonly runWithClient: <A>(
|
|
645
|
+
f: (client: Pg.ClientBase, resume: (_: Effect.Effect<A, SqlError>) => void) => void
|
|
646
|
+
) => Effect.Effect<A, SqlError>
|
|
647
|
+
private readonly reserve: Effect.Effect<Pg.ClientBase, SqlError, Scope.Scope>
|
|
648
|
+
|
|
649
|
+
private run(query: string, params: ReadonlyArray<unknown>) {
|
|
650
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
651
|
+
client.query(query, params as any, (err, result) => {
|
|
652
|
+
if (err) {
|
|
653
|
+
resume(
|
|
654
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
655
|
+
)
|
|
656
|
+
} else {
|
|
657
|
+
// Multi-statement queries return an array of results
|
|
658
|
+
resume(Effect.succeed(
|
|
659
|
+
Array.isArray(result)
|
|
660
|
+
? result.map((r) => r.rows ?? [])
|
|
661
|
+
: result.rows ?? []
|
|
662
|
+
))
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
execute(
|
|
669
|
+
sql: string,
|
|
670
|
+
params: ReadonlyArray<unknown>,
|
|
671
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
672
|
+
) {
|
|
673
|
+
return transformRows
|
|
674
|
+
? Effect.map(this.run(sql, params), transformRows)
|
|
675
|
+
: this.run(sql, params)
|
|
676
|
+
}
|
|
677
|
+
executeRaw(sql: string, params: ReadonlyArray<unknown>) {
|
|
678
|
+
return this.runWithClient<Pg.Result>((client, resume) => {
|
|
679
|
+
client.query(sql, params as any, (err, result) => {
|
|
680
|
+
if (err) {
|
|
681
|
+
resume(
|
|
682
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
683
|
+
)
|
|
684
|
+
} else {
|
|
685
|
+
resume(Effect.succeed(result))
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
executeWithoutTransform(sql: string, params: ReadonlyArray<unknown>) {
|
|
691
|
+
return this.run(sql, params)
|
|
692
|
+
}
|
|
693
|
+
executeValues(sql: string, params: ReadonlyArray<unknown>) {
|
|
694
|
+
return this.runWithClient<ReadonlyArray<any>>((client, resume) => {
|
|
695
|
+
client.query(
|
|
696
|
+
{
|
|
697
|
+
text: sql,
|
|
698
|
+
rowMode: "array",
|
|
699
|
+
values: params as Array<string>
|
|
700
|
+
},
|
|
701
|
+
(err, result) => {
|
|
702
|
+
if (err) {
|
|
703
|
+
resume(
|
|
704
|
+
Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "execute") }))
|
|
705
|
+
)
|
|
706
|
+
} else {
|
|
707
|
+
resume(Effect.succeed(result.rows))
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
)
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
executeValuesUnprepared(sql: string, params: ReadonlyArray<unknown>) {
|
|
714
|
+
return this.executeValues(sql, params)
|
|
715
|
+
}
|
|
716
|
+
executeUnprepared(
|
|
717
|
+
sql: string,
|
|
718
|
+
params: ReadonlyArray<unknown>,
|
|
719
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
720
|
+
) {
|
|
721
|
+
return this.execute(sql, params, transformRows)
|
|
722
|
+
}
|
|
723
|
+
executeStream(
|
|
724
|
+
sql: string,
|
|
725
|
+
params: ReadonlyArray<unknown>,
|
|
726
|
+
transformRows: (<A extends object>(row: ReadonlyArray<A>) => ReadonlyArray<A>) | undefined
|
|
727
|
+
) {
|
|
728
|
+
// oxlint-disable-next-line @typescript-eslint/no-this-alias
|
|
729
|
+
const self = this
|
|
730
|
+
return Stream.fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) {
|
|
731
|
+
const client = yield* Scope.provide(self.reserve, scope)
|
|
732
|
+
yield* Scope.addFinalizer(scope, Effect.promise(() => cursor.close()))
|
|
733
|
+
const cursor = client.query(new Cursor(sql, params as any))
|
|
734
|
+
// @effect-diagnostics-next-line returnEffectInGen:off
|
|
735
|
+
return Effect.callback<Arr.NonEmptyReadonlyArray<any>, SqlError | Cause.Done>((resume) => {
|
|
736
|
+
cursor.read(128, (err, rows) => {
|
|
737
|
+
if (err) {
|
|
738
|
+
resume(Effect.fail(new SqlError({ reason: classifyError(err, "Failed to execute statement", "stream") })))
|
|
739
|
+
} else if (Arr.isArrayNonEmpty(rows)) {
|
|
740
|
+
resume(Effect.succeed(transformRows ? transformRows(rows) as any : rows))
|
|
741
|
+
} else {
|
|
742
|
+
resume(Cause.done())
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
})
|
|
746
|
+
})))
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
451
750
|
const cancelEffects = new WeakMap<Pg.PoolClient, Effect.Effect<void> | undefined>()
|
|
452
751
|
const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
453
752
|
if (cancelEffects.has(client)) {
|
|
@@ -471,64 +770,52 @@ const makeCancel = (pool: Pg.Pool, client: Pg.PoolClient) => {
|
|
|
471
770
|
}
|
|
472
771
|
|
|
473
772
|
/**
|
|
773
|
+
* Creates a layer from an effect that acquires a `PgClient`, providing both `PgClient` and `SqlClient`.
|
|
774
|
+
*
|
|
474
775
|
* @category layers
|
|
475
|
-
* @since
|
|
776
|
+
* @since 4.0.0
|
|
476
777
|
*/
|
|
477
|
-
export const
|
|
478
|
-
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
ServiceMap.make(PgClient, client).pipe(
|
|
487
|
-
ServiceMap.add(Client.SqlClient, client)
|
|
488
|
-
)
|
|
489
|
-
)
|
|
490
|
-
)
|
|
491
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
778
|
+
export const layerFrom = <E, R>(
|
|
779
|
+
acquire: Effect.Effect<PgClient, E, R>
|
|
780
|
+
): Layer.Layer<PgClient | Client.SqlClient, E, Exclude<R, Scope.Scope | Reactivity.Reactivity>> =>
|
|
781
|
+
Layer.effectContext(
|
|
782
|
+
Effect.map(acquire, (client) =>
|
|
783
|
+
Context.make(PgClient, client).pipe(
|
|
784
|
+
Context.add(Client.SqlClient, client)
|
|
785
|
+
))
|
|
786
|
+
).pipe(Layer.provide(Reactivity.layer)) as any
|
|
492
787
|
|
|
493
788
|
/**
|
|
789
|
+
* Creates a layer from a `Config`-wrapped PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
790
|
+
*
|
|
494
791
|
* @category layers
|
|
495
|
-
* @since
|
|
792
|
+
* @since 4.0.0
|
|
496
793
|
*/
|
|
497
|
-
export const
|
|
498
|
-
config:
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
)
|
|
794
|
+
export const layerConfig: (
|
|
795
|
+
config: Config.Wrap<PgPoolConfig>
|
|
796
|
+
) => Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> = (
|
|
797
|
+
config: Config.Wrap<PgPoolConfig>
|
|
798
|
+
): Layer.Layer<PgClient | Client.SqlClient, Config.ConfigError | SqlError> =>
|
|
799
|
+
layerFrom(Effect.flatMap(
|
|
800
|
+
Config.unwrap(config),
|
|
801
|
+
make
|
|
802
|
+
))
|
|
506
803
|
|
|
507
804
|
/**
|
|
805
|
+
* Creates a layer from a concrete PostgreSQL pool configuration, providing both `PgClient` and `SqlClient`.
|
|
806
|
+
*
|
|
508
807
|
* @category layers
|
|
509
|
-
* @since
|
|
808
|
+
* @since 4.0.0
|
|
510
809
|
*/
|
|
511
|
-
export const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
readonly applicationName?: string | undefined
|
|
515
|
-
readonly spanAttributes?: Record<string, unknown> | undefined
|
|
516
|
-
|
|
517
|
-
readonly transformResultNames?: ((str: string) => string) | undefined
|
|
518
|
-
readonly transformQueryNames?: ((str: string) => string) | undefined
|
|
519
|
-
readonly transformJson?: boolean | undefined
|
|
520
|
-
readonly types?: Pg.CustomTypesConfig | undefined
|
|
521
|
-
}): Layer.Layer<PgClient | Client.SqlClient, SqlError> =>
|
|
522
|
-
Layer.effectServices(
|
|
523
|
-
Effect.map(fromPool(options), (client) =>
|
|
524
|
-
ServiceMap.make(PgClient, client).pipe(
|
|
525
|
-
ServiceMap.add(Client.SqlClient, client)
|
|
526
|
-
))
|
|
527
|
-
).pipe(Layer.provide(Reactivity.layer))
|
|
810
|
+
export const layer = (
|
|
811
|
+
config: PgPoolConfig
|
|
812
|
+
): Layer.Layer<PgClient | Client.SqlClient, SqlError> => layerFrom(make(config))
|
|
528
813
|
|
|
529
814
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
815
|
+
* Creates the PostgreSQL statement compiler, using `$1` placeholders, double-quoted identifiers, PostgreSQL returning clauses, and optional JSON value transformation.
|
|
816
|
+
*
|
|
817
|
+
* @category constructors
|
|
818
|
+
* @since 4.0.0
|
|
532
819
|
*/
|
|
533
820
|
export const makeCompiler = (
|
|
534
821
|
transform?: (_: string) => string,
|
|
@@ -576,18 +863,87 @@ export const makeCompiler = (
|
|
|
576
863
|
const escape = Statement.defaultEscape("\"")
|
|
577
864
|
|
|
578
865
|
/**
|
|
866
|
+
* PostgreSQL-specific custom statement fragments supported by the compiler, currently JSON parameter fragments.
|
|
867
|
+
*
|
|
579
868
|
* @category custom types
|
|
580
|
-
* @since
|
|
869
|
+
* @since 4.0.0
|
|
581
870
|
*/
|
|
582
871
|
export type PgCustom = PgJson
|
|
583
872
|
|
|
584
873
|
/**
|
|
585
874
|
* @category custom types
|
|
586
|
-
* @since
|
|
875
|
+
* @since 4.0.0
|
|
587
876
|
*/
|
|
588
877
|
interface PgJson extends Custom<"PgJson", unknown> {}
|
|
589
878
|
/**
|
|
590
879
|
* @category custom types
|
|
591
|
-
* @since
|
|
880
|
+
* @since 4.0.0
|
|
592
881
|
*/
|
|
593
882
|
const PgJson = Statement.custom<PgJson>("PgJson")
|
|
883
|
+
|
|
884
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name"
|
|
885
|
+
const ATTR_DB_NAMESPACE = "db.namespace"
|
|
886
|
+
const ATTR_SERVER_ADDRESS = "server.address"
|
|
887
|
+
const ATTR_SERVER_PORT = "server.port"
|
|
888
|
+
|
|
889
|
+
const pgCodeFromCause = (cause: unknown): string | undefined => {
|
|
890
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause)) {
|
|
891
|
+
return undefined
|
|
892
|
+
}
|
|
893
|
+
const code = cause.code
|
|
894
|
+
return typeof code === "string" ? code : undefined
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const pgConstraintFromCause = (cause: unknown): string => {
|
|
898
|
+
if (typeof cause !== "object" || cause === null || !("constraint" in cause)) {
|
|
899
|
+
return "unknown"
|
|
900
|
+
}
|
|
901
|
+
const constraint = cause.constraint
|
|
902
|
+
if (typeof constraint !== "string") {
|
|
903
|
+
return "unknown"
|
|
904
|
+
}
|
|
905
|
+
const normalized = constraint.trim()
|
|
906
|
+
return normalized.length === 0 ? "unknown" : normalized
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const classifyError = (
|
|
910
|
+
cause: unknown,
|
|
911
|
+
message: string,
|
|
912
|
+
operation: string
|
|
913
|
+
) => {
|
|
914
|
+
const props = { cause, message, operation }
|
|
915
|
+
const code = pgCodeFromCause(cause)
|
|
916
|
+
if (code !== undefined) {
|
|
917
|
+
if (code.startsWith("08")) {
|
|
918
|
+
return new ConnectionError(props)
|
|
919
|
+
}
|
|
920
|
+
if (code.startsWith("28")) {
|
|
921
|
+
return new AuthenticationError(props)
|
|
922
|
+
}
|
|
923
|
+
if (code === "42501") {
|
|
924
|
+
return new AuthorizationError(props)
|
|
925
|
+
}
|
|
926
|
+
if (code.startsWith("42")) {
|
|
927
|
+
return new SqlSyntaxError(props)
|
|
928
|
+
}
|
|
929
|
+
if (code === "23505") {
|
|
930
|
+
return new UniqueViolation({ ...props, constraint: pgConstraintFromCause(cause) })
|
|
931
|
+
}
|
|
932
|
+
if (code.startsWith("23")) {
|
|
933
|
+
return new ConstraintError(props)
|
|
934
|
+
}
|
|
935
|
+
if (code === "40P01") {
|
|
936
|
+
return new DeadlockError(props)
|
|
937
|
+
}
|
|
938
|
+
if (code === "40001") {
|
|
939
|
+
return new SerializationError(props)
|
|
940
|
+
}
|
|
941
|
+
if (code === "55P03") {
|
|
942
|
+
return new LockTimeoutError(props)
|
|
943
|
+
}
|
|
944
|
+
if (code === "57014") {
|
|
945
|
+
return new StatementTimeoutError(props)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return new UnknownError(props)
|
|
949
|
+
}
|