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