effect-start 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/Entity.ts +6 -6
- package/src/FileRouterCodegen.ts +4 -4
- package/src/FileSystem.ts +4 -8
- package/src/RouteHook.ts +1 -1
- package/src/RouteSse.ts +3 -3
- package/src/SqlIntrospect.ts +2 -2
- package/src/Start.ts +102 -2
- package/src/Values.ts +11 -0
- package/src/bun/BunRoute.ts +1 -1
- package/src/bun/BunRuntime.ts +5 -5
- package/src/hyper/HyperHtml.ts +11 -7
- package/src/hyper/jsx.d.ts +1 -1
- package/src/lint/plugin.js +174 -4
- package/src/sql/SqlClient.ts +355 -0
- package/src/sql/bun/index.ts +117 -50
- package/src/sql/index.ts +1 -1
- package/src/sql/libsql/index.ts +91 -77
- package/src/sql/libsql/libsql.d.ts +4 -1
- package/src/sql/mssql/index.ts +141 -108
- package/src/sql/mssql/mssql.d.ts +1 -0
- package/src/testing/TestLogger.ts +4 -4
- package/src/x/tailwind/compile.ts +6 -14
- package/src/console/Console.ts +0 -42
- package/src/console/ConsoleErrors.ts +0 -213
- package/src/console/ConsoleLogger.ts +0 -56
- package/src/console/ConsoleMetrics.ts +0 -72
- package/src/console/ConsoleProcess.ts +0 -59
- package/src/console/ConsoleStore.ts +0 -187
- package/src/console/ConsoleTracer.ts +0 -107
- package/src/console/Simulation.ts +0 -814
- package/src/console/console.html +0 -340
- package/src/console/index.ts +0 -3
- package/src/console/routes/errors/route.tsx +0 -97
- package/src/console/routes/fiberDetail.tsx +0 -54
- package/src/console/routes/fibers/route.tsx +0 -45
- package/src/console/routes/git/route.tsx +0 -64
- package/src/console/routes/layout.tsx +0 -4
- package/src/console/routes/logs/route.tsx +0 -77
- package/src/console/routes/metrics/route.tsx +0 -36
- package/src/console/routes/route.tsx +0 -8
- package/src/console/routes/routes/route.tsx +0 -30
- package/src/console/routes/services/route.tsx +0 -21
- package/src/console/routes/system/route.tsx +0 -43
- package/src/console/routes/traceDetail.tsx +0 -22
- package/src/console/routes/traces/route.tsx +0 -81
- package/src/console/routes/tree.ts +0 -30
- package/src/console/ui/Errors.tsx +0 -76
- package/src/console/ui/Fibers.tsx +0 -321
- package/src/console/ui/Git.tsx +0 -182
- package/src/console/ui/Logs.tsx +0 -46
- package/src/console/ui/Metrics.tsx +0 -78
- package/src/console/ui/Routes.tsx +0 -125
- package/src/console/ui/Services.tsx +0 -273
- package/src/console/ui/Shell.tsx +0 -62
- package/src/console/ui/System.tsx +0 -131
- package/src/console/ui/Traces.tsx +0 -426
- package/src/sql/Sql.ts +0 -51
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import * as Clock from "effect/Clock"
|
|
2
|
+
import * as Context from "effect/Context"
|
|
3
|
+
import * as Data from "effect/Data"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Exit from "effect/Exit"
|
|
6
|
+
import * as FiberRef from "effect/FiberRef"
|
|
7
|
+
import * as GlobalValue from "effect/GlobalValue"
|
|
8
|
+
import type * as Scope from "effect/Scope"
|
|
9
|
+
import * as Values from "../Values.ts"
|
|
10
|
+
|
|
11
|
+
export class SqlError extends Data.TaggedError("SqlError")<{
|
|
12
|
+
readonly code: string
|
|
13
|
+
readonly message: string
|
|
14
|
+
readonly cause?: unknown
|
|
15
|
+
}> {}
|
|
16
|
+
|
|
17
|
+
export interface DialectConfig {
|
|
18
|
+
readonly placeholder: (index: number) => string
|
|
19
|
+
readonly identifier: (name: string) => string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SqlRow = Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
export interface Connection {
|
|
25
|
+
/**
|
|
26
|
+
* Execute a parameterized query via tagged template literal
|
|
27
|
+
*/
|
|
28
|
+
<A extends object = SqlRow>(
|
|
29
|
+
strings: TemplateStringsArray,
|
|
30
|
+
...values: Array<unknown>
|
|
31
|
+
): Effect.Effect<ReadonlyArray<A>, SqlError>
|
|
32
|
+
/**
|
|
33
|
+
* Create a safely-escaped SQL identifier from a string
|
|
34
|
+
*/
|
|
35
|
+
(name: string): unknown
|
|
36
|
+
/**
|
|
37
|
+
* Build a VALUES clause from an object or array of objects,
|
|
38
|
+
* optionally picking specific columns
|
|
39
|
+
*/
|
|
40
|
+
<T extends Record<string, unknown>, K extends keyof T>(
|
|
41
|
+
obj: T | ReadonlyArray<T>,
|
|
42
|
+
columns?: [K, ...Array<K>],
|
|
43
|
+
): unknown
|
|
44
|
+
/**
|
|
45
|
+
* Build an IN-list from an array of primitive values
|
|
46
|
+
*/
|
|
47
|
+
(values: ReadonlyArray<string | number | boolean | null>): unknown
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Execute a raw SQL string without tagged template escaping
|
|
51
|
+
*/
|
|
52
|
+
readonly unsafe: <A extends object = SqlRow>(
|
|
53
|
+
query: string,
|
|
54
|
+
values?: Array<unknown>,
|
|
55
|
+
) => Effect.Effect<ReadonlyArray<A>, SqlError>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SqlClient extends Connection {
|
|
59
|
+
/**
|
|
60
|
+
* Run an effect inside a transaction
|
|
61
|
+
*/
|
|
62
|
+
readonly withTransaction: <A, E, R>(
|
|
63
|
+
self: Effect.Effect<A, E, R>,
|
|
64
|
+
) => Effect.Effect<A, SqlError | E, R>
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reserve a dedicated connection from the pool
|
|
68
|
+
*/
|
|
69
|
+
readonly reserve: Effect.Effect<Connection, SqlError, Scope.Scope>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Access the underlying database driver directly
|
|
73
|
+
*/
|
|
74
|
+
readonly use: <T>(fn: (driver: any) => Promise<T> | T) => Effect.Effect<T, SqlError>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const SqlClient: Context.Tag<SqlClient, SqlClient> = Context.GenericTag<SqlClient>(
|
|
78
|
+
"effect-start/Sql/SqlClient",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
export type TaggedQuery = <A extends object = SqlRow>(
|
|
82
|
+
strings: TemplateStringsArray,
|
|
83
|
+
...values: Array<unknown>
|
|
84
|
+
) => Effect.Effect<ReadonlyArray<A>, SqlError>
|
|
85
|
+
|
|
86
|
+
export interface MakeOptions {
|
|
87
|
+
readonly withTransaction: SqlClient["withTransaction"]
|
|
88
|
+
readonly reserve: SqlClient["reserve"]
|
|
89
|
+
readonly use: SqlClient["use"]
|
|
90
|
+
readonly unsafe: Connection["unsafe"]
|
|
91
|
+
readonly query: TaggedQuery
|
|
92
|
+
readonly spanAttributes: ReadonlyArray<readonly [string, unknown]>
|
|
93
|
+
readonly dialect: DialectConfig
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function make(options: MakeOptions): SqlClient {
|
|
97
|
+
const trace = makeTraceOptions(options.spanAttributes, options.dialect)
|
|
98
|
+
const query = withExecuteSpan(options.query, trace)
|
|
99
|
+
const unsafe = withUnsafeExecuteSpan(options.unsafe, trace)
|
|
100
|
+
const withTransaction = withTransactionSpan(options.withTransaction, trace.spanAttributes)
|
|
101
|
+
|
|
102
|
+
return Object.assign(dispatchCallable(query), {
|
|
103
|
+
...options,
|
|
104
|
+
query,
|
|
105
|
+
unsafe,
|
|
106
|
+
withTransaction,
|
|
107
|
+
}) as unknown as SqlClient
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function connection(
|
|
111
|
+
query: TaggedQuery,
|
|
112
|
+
unsafe: Connection["unsafe"],
|
|
113
|
+
options?: {
|
|
114
|
+
readonly spanAttributes?: ReadonlyArray<readonly [string, unknown]>
|
|
115
|
+
readonly dialect?: DialectConfig
|
|
116
|
+
},
|
|
117
|
+
): Connection {
|
|
118
|
+
const trace = makeTraceOptions(options?.spanAttributes, options?.dialect)
|
|
119
|
+
const tracedQuery = withExecuteSpan(query, trace)
|
|
120
|
+
const tracedUnsafe = withUnsafeExecuteSpan(unsafe, trace)
|
|
121
|
+
return Object.assign(dispatchCallable(tracedQuery), { unsafe: tracedUnsafe }) as unknown as Connection
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function dispatchCallable(query: TaggedQuery) {
|
|
125
|
+
return (first: any, ...rest: Array<any>) => {
|
|
126
|
+
if (Values.isTemplateStringsArray(first)) return query(first, ...rest)
|
|
127
|
+
if (typeof first === "string") return makeIdentifier(first)
|
|
128
|
+
if (
|
|
129
|
+
Array.isArray(first) &&
|
|
130
|
+
(first.length === 0 || typeof first[0] !== "object" || first[0] === null)
|
|
131
|
+
)
|
|
132
|
+
return makeList(first)
|
|
133
|
+
return makeValues(first, rest[0])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Interpolate fragments into a SQL string and parameter array.
|
|
139
|
+
**/
|
|
140
|
+
export function interpolate(
|
|
141
|
+
dialect: DialectConfig,
|
|
142
|
+
strings: TemplateStringsArray,
|
|
143
|
+
interpolations: Array<unknown>,
|
|
144
|
+
): { readonly sql: string; readonly parameters: Array<unknown> } {
|
|
145
|
+
const parts: Array<string> = []
|
|
146
|
+
const parameters: Array<unknown> = []
|
|
147
|
+
let pi = 1
|
|
148
|
+
|
|
149
|
+
const pushItems = (items: ReadonlyArray<unknown>) => {
|
|
150
|
+
const ph: Array<string> = []
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
ph.push(dialect.placeholder(pi++))
|
|
153
|
+
parameters.push(item)
|
|
154
|
+
}
|
|
155
|
+
return ph.join(", ")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
parts.push(strings[0])
|
|
159
|
+
for (let i = 0; i < interpolations.length; i++) {
|
|
160
|
+
const frag = isSqlFragment(interpolations[i])
|
|
161
|
+
if (frag) {
|
|
162
|
+
const tag = frag[SqlFragmentTag]
|
|
163
|
+
if (tag === "Identifier") parts.push(dialect.identifier(frag.name))
|
|
164
|
+
else if (tag === "List") parts.push(`(${pushItems(frag.items)})`)
|
|
165
|
+
else if (tag === "Values") {
|
|
166
|
+
const cols = frag.columns.map((c) => dialect.identifier(c as string)).join(", ")
|
|
167
|
+
const rows = frag.value
|
|
168
|
+
.map((row) => `(${pushItems(frag.columns.map((c) => row[c as string]))})`)
|
|
169
|
+
.join(", ")
|
|
170
|
+
parts.push(`(${cols}) VALUES ${rows}`)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
parts.push(dialect.placeholder(pi++))
|
|
174
|
+
parameters.push(interpolations[i])
|
|
175
|
+
}
|
|
176
|
+
parts.push(strings[i + 1])
|
|
177
|
+
}
|
|
178
|
+
return { sql: parts.join(""), parameters }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function hasFragments(values: Array<unknown>): boolean {
|
|
182
|
+
return values.some((v) => isSqlFragment(v) !== undefined)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const postgresDialect: DialectConfig = {
|
|
186
|
+
placeholder: (i) => `$${i}`,
|
|
187
|
+
identifier: (name) => `"${name.replace(/"/g, '""')}"`,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const sqliteDialect: DialectConfig = {
|
|
191
|
+
placeholder: () => "?",
|
|
192
|
+
identifier: (name) => `"${name.replace(/"/g, '""')}"`,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const mssqlDialect: DialectConfig = {
|
|
196
|
+
placeholder: (i) => `@p${i}`,
|
|
197
|
+
identifier: (name) => `[${name.replace(/\]/g, "]]")}]`,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const SqlFragmentTag = Symbol.for("effect-start/Sql/SqlFragment")
|
|
201
|
+
|
|
202
|
+
type SqlFragment =
|
|
203
|
+
| { readonly [SqlFragmentTag]: "Identifier"; readonly name: string }
|
|
204
|
+
| {
|
|
205
|
+
readonly [SqlFragmentTag]: "Values"
|
|
206
|
+
readonly value: ReadonlyArray<Record<string, unknown>>
|
|
207
|
+
readonly columns: ReadonlyArray<string>
|
|
208
|
+
}
|
|
209
|
+
| { readonly [SqlFragmentTag]: "List"; readonly items: ReadonlyArray<unknown> }
|
|
210
|
+
|
|
211
|
+
const makeIdentifier = (name: string): SqlFragment => ({ [SqlFragmentTag]: "Identifier", name })
|
|
212
|
+
|
|
213
|
+
const makeValues = <T extends Record<string, unknown>>(
|
|
214
|
+
obj: T | ReadonlyArray<T>,
|
|
215
|
+
columns?: Array<keyof T & string>,
|
|
216
|
+
): SqlFragment => {
|
|
217
|
+
const items = Array.isArray(obj) ? obj : [obj]
|
|
218
|
+
const cols = columns && columns.length > 0 ? columns : (Object.keys(items[0]) as Array<string>)
|
|
219
|
+
return { [SqlFragmentTag]: "Values", value: items, columns: cols }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const makeList = (items: ReadonlyArray<unknown>): SqlFragment => ({
|
|
223
|
+
[SqlFragmentTag]: "List",
|
|
224
|
+
items,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const isSqlFragment = (value: unknown): SqlFragment | undefined =>
|
|
228
|
+
value !== null && typeof value === "object" && SqlFragmentTag in value
|
|
229
|
+
? (value as SqlFragment)
|
|
230
|
+
: undefined
|
|
231
|
+
|
|
232
|
+
const currentTransactionDepth = GlobalValue.globalValue(
|
|
233
|
+
Symbol.for("effect-start/sql/currentTransactionDepth"),
|
|
234
|
+
() => FiberRef.unsafeMake(0),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const withExecuteSpan =
|
|
238
|
+
(
|
|
239
|
+
query: TaggedQuery,
|
|
240
|
+
options: {
|
|
241
|
+
readonly spanAttributes: Record<string, unknown>
|
|
242
|
+
readonly dialect: DialectConfig
|
|
243
|
+
},
|
|
244
|
+
): TaggedQuery =>
|
|
245
|
+
<A extends object = SqlRow>(
|
|
246
|
+
strings: TemplateStringsArray,
|
|
247
|
+
...values: Array<unknown>
|
|
248
|
+
): Effect.Effect<ReadonlyArray<A>, SqlError> =>
|
|
249
|
+
query<A>(strings, ...values).pipe(
|
|
250
|
+
Effect.withSpan("sql.execute", {
|
|
251
|
+
kind: "client",
|
|
252
|
+
attributes: {
|
|
253
|
+
...options.spanAttributes,
|
|
254
|
+
"db.operation.name": "execute",
|
|
255
|
+
"db.query.text": renderTemplateSql(options.dialect, strings, values),
|
|
256
|
+
},
|
|
257
|
+
captureStackTrace: false,
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const withUnsafeExecuteSpan =
|
|
262
|
+
(
|
|
263
|
+
unsafe: Connection["unsafe"],
|
|
264
|
+
options: {
|
|
265
|
+
readonly spanAttributes: Record<string, unknown>
|
|
266
|
+
},
|
|
267
|
+
): Connection["unsafe"] =>
|
|
268
|
+
<A extends object = SqlRow>(
|
|
269
|
+
query: string,
|
|
270
|
+
values?: Array<unknown>,
|
|
271
|
+
): Effect.Effect<ReadonlyArray<A>, SqlError> =>
|
|
272
|
+
unsafe<A>(query, values).pipe(
|
|
273
|
+
Effect.withSpan("sql.execute", {
|
|
274
|
+
kind: "client",
|
|
275
|
+
attributes: {
|
|
276
|
+
...options.spanAttributes,
|
|
277
|
+
"db.operation.name": "executeRaw",
|
|
278
|
+
"db.query.text": query,
|
|
279
|
+
},
|
|
280
|
+
captureStackTrace: false,
|
|
281
|
+
}),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const withTransactionSpan =
|
|
285
|
+
(
|
|
286
|
+
withTransaction: SqlClient["withTransaction"],
|
|
287
|
+
spanAttributes: Record<string, unknown>,
|
|
288
|
+
): SqlClient["withTransaction"] =>
|
|
289
|
+
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, SqlError | E, R> =>
|
|
290
|
+
Effect.flatMap(FiberRef.get(currentTransactionDepth), (depth) =>
|
|
291
|
+
Effect.useSpan(
|
|
292
|
+
"sql.transaction",
|
|
293
|
+
{
|
|
294
|
+
kind: "client",
|
|
295
|
+
attributes: spanAttributes,
|
|
296
|
+
captureStackTrace: false,
|
|
297
|
+
},
|
|
298
|
+
(span) =>
|
|
299
|
+
Effect.flatMap(
|
|
300
|
+
Effect.exit(
|
|
301
|
+
Effect.withParentSpan(
|
|
302
|
+
Effect.locally(
|
|
303
|
+
withTransaction(self),
|
|
304
|
+
currentTransactionDepth,
|
|
305
|
+
depth + 1,
|
|
306
|
+
),
|
|
307
|
+
span,
|
|
308
|
+
),
|
|
309
|
+
),
|
|
310
|
+
(exit) =>
|
|
311
|
+
Effect.flatMap(Clock.currentTimeNanos, (timestamp) =>
|
|
312
|
+
Effect.zipRight(
|
|
313
|
+
Effect.sync(() =>
|
|
314
|
+
span.event(
|
|
315
|
+
Exit.isSuccess(exit)
|
|
316
|
+
? depth === 0
|
|
317
|
+
? "db.transaction.commit"
|
|
318
|
+
: "db.transaction.savepoint"
|
|
319
|
+
: "db.transaction.rollback",
|
|
320
|
+
timestamp,
|
|
321
|
+
),
|
|
322
|
+
),
|
|
323
|
+
exit,
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
const makeTraceOptions = (
|
|
331
|
+
spanAttributes: ReadonlyArray<readonly [string, unknown]> | undefined,
|
|
332
|
+
dialect: DialectConfig | undefined,
|
|
333
|
+
): {
|
|
334
|
+
readonly spanAttributes: Record<string, unknown>
|
|
335
|
+
readonly dialect: DialectConfig
|
|
336
|
+
} => ({
|
|
337
|
+
spanAttributes: spanAttributes ? Object.fromEntries(spanAttributes) : {},
|
|
338
|
+
dialect: dialect ?? sqliteDialect,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const renderTemplateSql = (
|
|
342
|
+
dialect: DialectConfig,
|
|
343
|
+
strings: TemplateStringsArray,
|
|
344
|
+
values: Array<unknown>,
|
|
345
|
+
): string => {
|
|
346
|
+
if (hasFragments(values)) {
|
|
347
|
+
return interpolate(dialect, strings, values).sql
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let sql = strings[0]
|
|
351
|
+
for (let i = 0; i < values.length; i++) {
|
|
352
|
+
sql += dialect.placeholder(i + 1) + strings[i + 1]
|
|
353
|
+
}
|
|
354
|
+
return sql
|
|
355
|
+
}
|
package/src/sql/bun/index.ts
CHANGED
|
@@ -4,7 +4,8 @@ import * as FiberRef from "effect/FiberRef"
|
|
|
4
4
|
import * as GlobalValue from "effect/GlobalValue"
|
|
5
5
|
import * as Layer from "effect/Layer"
|
|
6
6
|
import * as Option from "effect/Option"
|
|
7
|
-
import * as Sql from "../
|
|
7
|
+
import * as Sql from "../SqlClient.ts"
|
|
8
|
+
import * as Values from "../../Values.ts"
|
|
8
9
|
|
|
9
10
|
const errorCode = (error: unknown): string => {
|
|
10
11
|
const e = error as any
|
|
@@ -22,20 +23,14 @@ const wrapError = (error: unknown): Sql.SqlError =>
|
|
|
22
23
|
const wrap = <T>(fn: () => PromiseLike<T>): Effect.Effect<T, Sql.SqlError> =>
|
|
23
24
|
Effect.tryPromise({ try: () => Promise.resolve(fn()), catch: wrapError })
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
const items = Array.isArray(obj) ? obj : [obj]
|
|
27
|
-
const cols = columns.length > 0 ? columns : Object.keys(items[0])
|
|
28
|
-
return { value: items, columns: cols }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface TxState {
|
|
26
|
+
interface TransactionConnection {
|
|
32
27
|
readonly conn: any
|
|
33
28
|
readonly depth: number
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
const currentTransaction = GlobalValue.globalValue(
|
|
37
32
|
Symbol.for("effect-start/sql/bun/currentTransaction"),
|
|
38
|
-
() => FiberRef.unsafeMake<Option.Option<
|
|
33
|
+
() => FiberRef.unsafeMake<Option.Option<TransactionConnection>>(Option.none()),
|
|
39
34
|
)
|
|
40
35
|
|
|
41
36
|
const makeRun =
|
|
@@ -45,6 +40,72 @@ const makeRun =
|
|
|
45
40
|
wrap(() => fn(Option.isSome(txOpt) ? txOpt.value.conn : bunSql)),
|
|
46
41
|
)
|
|
47
42
|
|
|
43
|
+
const detectDialect = (bunSql: any): Sql.DialectConfig => {
|
|
44
|
+
const adapter = bunSql?.options?.adapter ?? bunSql?.adapter
|
|
45
|
+
if (adapter === "sqlite") return Sql.sqliteDialect
|
|
46
|
+
return Sql.postgresDialect
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const makeSpanAttributes = (config: ConstructorParameters<typeof Bun.SQL>[0]): Record<string, unknown> => {
|
|
50
|
+
const c = config as Record<string, unknown>
|
|
51
|
+
const adapter = c.adapter
|
|
52
|
+
if (adapter === "sqlite") {
|
|
53
|
+
return Values.compact({
|
|
54
|
+
...(c.spanAttributes as Record<string, unknown> | undefined),
|
|
55
|
+
"db.system.name": "sqlite",
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parsed = (() => {
|
|
60
|
+
if (typeof c.url !== "string") return undefined
|
|
61
|
+
try {
|
|
62
|
+
return new URL(c.url)
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
})()
|
|
67
|
+
const dbFromPath = parsed?.pathname.replace(/^\/+/, "") || undefined
|
|
68
|
+
const parsedPort = parsed?.port ? Number(parsed.port) : undefined
|
|
69
|
+
|
|
70
|
+
return Values.compact({
|
|
71
|
+
...(c.spanAttributes as Record<string, unknown> | undefined),
|
|
72
|
+
"db.system.name": "postgresql",
|
|
73
|
+
"db.namespace": (c.database as string | undefined) ?? dbFromPath,
|
|
74
|
+
"server.address": ((c.hostname ?? c.host) as string | undefined) ?? parsed?.hostname,
|
|
75
|
+
"server.port": (c.port as number | undefined) ?? parsedPort,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const makeTaggedTemplate = (
|
|
80
|
+
run: <T>(fn: (conn: any) => PromiseLike<T>) => Effect.Effect<T, Sql.SqlError>,
|
|
81
|
+
dialect: Sql.DialectConfig,
|
|
82
|
+
) => {
|
|
83
|
+
const unsafeFn = <T = any>(query: string, values?: Array<unknown>) =>
|
|
84
|
+
run<ReadonlyArray<T>>((conn) => conn.unsafe(query, values))
|
|
85
|
+
|
|
86
|
+
return <T = any>(
|
|
87
|
+
strings: TemplateStringsArray,
|
|
88
|
+
...values: Array<unknown>
|
|
89
|
+
): Effect.Effect<ReadonlyArray<T>, Sql.SqlError> => {
|
|
90
|
+
if (Sql.hasFragments(values)) {
|
|
91
|
+
const compiled = Sql.interpolate(dialect, strings, values)
|
|
92
|
+
return unsafeFn<T>(compiled.sql, compiled.parameters)
|
|
93
|
+
}
|
|
94
|
+
return run<ReadonlyArray<T>>((conn) => conn(strings, ...values))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const makeQuery = (
|
|
99
|
+
run: <T>(fn: (conn: any) => PromiseLike<T>) => Effect.Effect<T, Sql.SqlError>,
|
|
100
|
+
dialect: Sql.DialectConfig,
|
|
101
|
+
spanAttributes: ReadonlyArray<readonly [string, unknown]>,
|
|
102
|
+
): Sql.Connection => {
|
|
103
|
+
const query = makeTaggedTemplate(run, dialect)
|
|
104
|
+
const unsafe: Sql.Connection["unsafe"] = <T = any>(query: string, values?: Array<unknown>) =>
|
|
105
|
+
run<ReadonlyArray<T>>((conn) => conn.unsafe(query, values))
|
|
106
|
+
return Sql.connection(query, unsafe, { spanAttributes, dialect })
|
|
107
|
+
}
|
|
108
|
+
|
|
48
109
|
const makeWithTransaction =
|
|
49
110
|
(bunSql: any) =>
|
|
50
111
|
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, Sql.SqlError | E, R> =>
|
|
@@ -98,50 +159,56 @@ const makeWithTransaction =
|
|
|
98
159
|
)
|
|
99
160
|
|
|
100
161
|
export const layer = (
|
|
101
|
-
config: ConstructorParameters<typeof Bun.SQL>[0]
|
|
162
|
+
config: ConstructorParameters<typeof Bun.SQL>[0] & {
|
|
163
|
+
readonly spanAttributes?: Record<string, unknown>
|
|
164
|
+
},
|
|
102
165
|
): Layer.Layer<Sql.SqlClient, Sql.SqlError> =>
|
|
103
166
|
Layer.scoped(
|
|
104
167
|
Sql.SqlClient,
|
|
105
|
-
Effect.
|
|
106
|
-
Effect.
|
|
107
|
-
try
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
Effect.map(
|
|
169
|
+
Effect.acquireRelease(
|
|
170
|
+
Effect.try({
|
|
171
|
+
try: () => {
|
|
172
|
+
const driverConfig = { ...config } as Record<string, unknown>
|
|
173
|
+
delete driverConfig.spanAttributes
|
|
174
|
+
const bunSql = new Bun.SQL(driverConfig as any)
|
|
175
|
+
const run = makeRun(bunSql)
|
|
176
|
+
const dialect = detectDialect(bunSql)
|
|
177
|
+
const spanAttributes = Object.entries(makeSpanAttributes(config))
|
|
178
|
+
const unsafeFn: Sql.Connection["unsafe"] = <T = any>(
|
|
179
|
+
query: string,
|
|
180
|
+
values?: Array<unknown>,
|
|
181
|
+
) => run<ReadonlyArray<T>>((conn) => conn.unsafe(query, values))
|
|
182
|
+
const query = makeTaggedTemplate(run, dialect)
|
|
183
|
+
const use: Sql.SqlClient["use"] = (fn) =>
|
|
184
|
+
Effect.tryPromise({ try: () => Promise.resolve(fn(bunSql)), catch: wrapError })
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
client: Sql.make({
|
|
188
|
+
query,
|
|
189
|
+
unsafe: unsafeFn,
|
|
190
|
+
withTransaction: makeWithTransaction(bunSql),
|
|
191
|
+
spanAttributes,
|
|
192
|
+
dialect,
|
|
193
|
+
reserve: Effect.acquireRelease(
|
|
194
|
+
wrap(() => bunSql.reserve()),
|
|
195
|
+
(reserved: any) => Effect.sync(() => reserved.release()),
|
|
196
|
+
).pipe(
|
|
197
|
+
Effect.map((reserved: any): Sql.Connection => {
|
|
198
|
+
const reservedRun = <T>(fn: (conn: any) => PromiseLike<T>) =>
|
|
199
|
+
wrap<T>(() => fn(reserved))
|
|
200
|
+
return makeQuery(reservedRun, dialect, spanAttributes)
|
|
201
|
+
}),
|
|
135
202
|
),
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
(
|
|
203
|
+
use,
|
|
204
|
+
}),
|
|
205
|
+
close: use((bunSql) => bunSql.close()),
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
catch: wrapError,
|
|
209
|
+
}),
|
|
210
|
+
(handle) => handle.close.pipe(Effect.orDie),
|
|
211
|
+
),
|
|
212
|
+
(handle) => handle.client,
|
|
146
213
|
),
|
|
147
214
|
)
|
package/src/sql/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * as
|
|
1
|
+
export * as SqlClient from "./SqlClient.ts"
|