UNPKG

effect-sql-kysely

Version:

A full-featured integration between `@effect/sql` and `Kysely` that provides type-safe database operations with Effect's powerful error handling and resource management.

126 lines (114 loc) 4.25 kB
import * as Sql from "@effect/sql"; import type { SqlConnection } from "@effect/sql"; import type{ Primitive } from "@effect/sql/Statement"; import { Chunk, Effect, Exit, Stream } from "effect"; import { CompiledQuery, type Kysely } from "kysely"; import { beginConnection } from "./beginConnection.js"; import { Reactivity } from "@effect/experimental"; export function makeSqlClient<DB>({ database, compiler, spanAttributes = [], chunkSize = 16, }: { database: Kysely<DB>; compiler: Sql.Statement.Compiler; spanAttributes?: ReadonlyArray<readonly [string, string]>; chunkSize?: number; }): Effect.Effect<Sql.SqlClient.SqlClient, never, Reactivity.Reactivity> { const transformRows = Sql.Statement.defaultTransforms((s) => s, false).array; // A Connection is a wrapper around a Kysely database connection, or Transaction, that provides // the ability to run queries within Effects and captures any errors that may occur. class ConnectionImpl implements SqlConnection.Connection { constructor(private readonly db: Kysely<DB>) {} executeUnprepared( sql: string, params?: ReadonlyArray<Primitive> | undefined ): Effect.Effect<ReadonlyArray<unknown>, Sql.SqlError.SqlError> { return Effect.tryPromise({ try: () => this.db .executeQuery(compileSqlQuery(sql, params)) .then((r) => transformRows(r.rows)), catch: (cause) => new Sql.SqlError.SqlError({ cause }), }); } execute(sql: string, params: ReadonlyArray<Primitive>) { return Effect.tryPromise({ try: () => this.db .executeQuery(compileSqlQuery(sql, params)) .then((r) => transformRows(r.rows)), catch: (cause) => new Sql.SqlError.SqlError({ cause }), }); } executeWithoutTransform(sql: string, params: ReadonlyArray<Primitive>) { return Effect.tryPromise({ try: () => this.db .executeQuery(compileSqlQuery(sql, params)) .then((r) => r.rows), catch: (cause) => new Sql.SqlError.SqlError({ cause }), }); } executeValues(sql: string, params: ReadonlyArray<Primitive>) { return Effect.map(this.executeRaw(sql, params), (results) => results.map((x) => Object.values(x as Record<string, Primitive>)) ); } executeRaw(sql: string, params?: ReadonlyArray<Primitive>) { return Effect.tryPromise({ try: () => this.db .executeQuery(compileSqlQuery(sql, params)) .then((r) => transformRows(r.rows)), catch: (cause) => new Sql.SqlError.SqlError({ cause }), }); } executeStream(sql: string, params: ReadonlyArray<Primitive>) { const query = compileSqlQuery(sql, params); return Stream.suspend(() => Stream.mapChunks( Stream.fromAsyncIterable( this.db .getExecutor() .stream(query, chunkSize), (cause) => new Sql.SqlError.SqlError({ cause }) ), Chunk.flatMap((result) => Chunk.unsafeFromArray(result.rows)) ) ); } } const acquirer = Effect.succeed(new ConnectionImpl(database)); return Sql.SqlClient.make({ // Our default connection is managed by Kysely acquirer, // Our SQL statement compiler compiler, // We don't utilize db.transaction() because Sql.client.make will handle the actual transaction // But we do ensure that all queries are run within a single connection transactionAcquirer: Effect.map( Effect.acquireRelease( Effect.promise(() => beginConnection(database)), (conn, exit) => Effect.promise(() => Exit.match(exit, { // If the scope fails we rollback the transaction onFailure: () => conn.fail(), // If the scope succeeds we commit the transaction onSuccess: () => conn.success(), }) ) ), ({ conn }) => new ConnectionImpl(conn) ), spanAttributes, }) } function compileSqlQuery( sql: string, params?: ReadonlyArray<Primitive> ): CompiledQuery<object> { return CompiledQuery.raw(sql, params as unknown[]) as CompiledQuery<object>; }