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