Created
April 28, 2026 18:21
-
-
Save neolectron/10a254fa51deb2039d3ab6eed275d06c to your computer and use it in GitHub Desktop.
effect-kysely ugly wrapper pattern
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // oxlint-disable no-extra-bind | |
| import type { | |
| Compilable, | |
| Kysely, | |
| QueryExecutorProvider, | |
| QueryResult, | |
| RawBuilder, | |
| Transaction, | |
| TransactionBuilder, | |
| } from 'kysely'; | |
| import { SqlError } from '@effect/sql/SqlError'; | |
| import { Array, Data, Effect, Option } from 'effect'; | |
| // taken from https://github.com/Effect-TS/effect/pull/5156 | |
| interface EffectExecutor { | |
| executeRaw: <O>(query: QueryRaw<O>) => Effect.Effect<QueryResult<O>, SqlError>; | |
| execute: <O>(query: Query<O>) => Effect.Effect<O[], SqlError>; | |
| executeTakeFirstOption: <O>(query: Query<O>) => Effect.Effect<Option.Option<O>, SqlError>; | |
| executeTakeFirstOrUndefined: <O>(query: Query<O>) => Effect.Effect<O | undefined, SqlError>; | |
| executeTakeFirstOrError: <O>(query: Query<O>) => Effect.Effect<O, SqlError | SqlNoFirstResult>; | |
| executeTakeFirstUnsafe: <O>(query: Query<O>) => Effect.Effect<O, SqlError>; | |
| } | |
| interface EffectTransition<DB> | |
| extends | |
| Omit<Transaction<DB>, 'transaction' | 'startTransaction' | 'executeQuery'>, | |
| EffectExecutor {} | |
| export interface EffectKysely<DB> | |
| extends Omit<Kysely<DB>, 'transaction' | 'startTransaction' | 'executeQuery'>, EffectExecutor { | |
| transaction: () => Omit<TransactionBuilder<DB>, 'execute'> & { | |
| execute: <A, E>(f: (trx: EffectTransition<DB>) => Effect.Effect<A, E>) => Effect.Effect<A, E>; | |
| }; | |
| } | |
| const makeExecutor = <DB>(client: Kysely<DB>): EffectExecutor => ({ | |
| executeRaw: executeRaw(client).bind(client), | |
| execute: execute(client).bind(client), | |
| executeTakeFirstOption: executeTakeFirstOption(client).bind(client), | |
| executeTakeFirstOrUndefined: executeTakeFirstOrUndefined(client).bind(client), | |
| executeTakeFirstOrError: executeTakeFirstOrError(client).bind(client), | |
| executeTakeFirstUnsafe: executeTakeFirstUnsafe(client).bind(client), | |
| }); | |
| export const makeFromKysely = <DB>(kysely: Kysely<DB>): EffectKysely<DB> => { | |
| const kyselyTransaction = kysely.transaction.bind(kysely); | |
| return Object.assign(kysely, { | |
| ...makeExecutor(kysely), | |
| transaction: (() => { | |
| const builder = kyselyTransaction(); | |
| const kyselyBuilderExecute = builder.execute.bind(builder); | |
| return Object.assign(builder, { | |
| execute: (<A, E>(f: (trx: EffectTransition<DB>) => Effect.Effect<A, E>) => { | |
| return Effect.async<A, E>((resume) => { | |
| kyselyBuilderExecute((trx) => | |
| Effect.runPromise(f(Object.assign(trx, makeExecutor(trx)))), | |
| ) | |
| .then((a) => resume(Effect.succeed(a))) | |
| .catch((e) => resume(Effect.fail(e as E))); | |
| }); | |
| }).bind(builder), | |
| }); | |
| }).bind(kysely), | |
| }); | |
| }; | |
| interface Executable<O> extends Compilable<O> { | |
| execute: () => Promise<undefined | O[]>; | |
| } | |
| interface ExecutableRaw<O> extends Executable<O>, QueryExecutorProvider {} | |
| type Query<O> = Executable<O> | RawBuilder<O>; | |
| type QueryRaw<O> = ExecutableRaw<O> | RawBuilder<O>; | |
| const isRawBuilder = <O>(query: Executable<O> | RawBuilder<O>): query is RawBuilder<O> => { | |
| return `isRawBuilder` in query && query.isRawBuilder; | |
| }; | |
| const queryAsPromise = <DB, O>(client: Kysely<DB>, query: QueryRaw<O>): Promise<QueryResult<O>> => { | |
| if (isRawBuilder(query)) { | |
| return query.execute(client); | |
| } | |
| const executor = query.getExecutor(); | |
| const compiledQuery = query.compile(); | |
| return executor.executeQuery(compiledQuery); | |
| }; | |
| const executeSpan = <DB, TQuery extends Query<unknown> | QueryRaw<unknown>>( | |
| client: Kysely<DB>, | |
| query: TQuery, | |
| ) => { | |
| // oxlint-disable-next-line typescript/no-explicit-any -- kysely types require any for compile | |
| const compiled = isRawBuilder(query) ? query.compile(client as any) : query.compile(); | |
| return Effect.withSpan(`kysely.execute`, { | |
| kind: `client`, | |
| captureStackTrace: false, | |
| attributes: { | |
| sql: compiled.sql, | |
| }, | |
| }); | |
| }; | |
| const executeRaw = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: QueryRaw<O>) => | |
| Effect.tryPromise({ | |
| try: () => queryAsPromise(client, query), | |
| catch: (cause) => { | |
| return new SqlError({ | |
| cause, | |
| message: | |
| cause instanceof Error | |
| ? `[executeRaw] SqlError: ${cause.message}\n\n query:\n ${query.compile(client).sql}` | |
| : `[executeRaw] An error has occurred with the query: ${query.compile(client).sql}`, | |
| }); | |
| }, | |
| }).pipe(executeSpan(client, query)); | |
| const execute = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: Query<O>) => | |
| Effect.tryPromise({ | |
| try: () => | |
| isRawBuilder(query) | |
| ? queryAsPromise(client, query).then((result) => result.rows) | |
| : query.execute().then((result) => result ?? []), | |
| catch: (cause) => { | |
| return new SqlError({ | |
| cause, | |
| message: | |
| cause instanceof Error | |
| ? `[execute] SqlError: ${cause.message}\n\n query:\n ${query.compile(client).sql}` | |
| : `[execute] An error has occurred with the query: ${query.compile(client).sql}`, | |
| }); | |
| }, | |
| }).pipe(executeSpan(client, query)); | |
| const executeTakeFirstOption = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: Query<O>) => | |
| execute(client)(query).pipe( | |
| Effect.map((result) => | |
| Array.isNonEmptyReadonlyArray(result) ? Option.some(result[0]) : Option.none(), | |
| ), | |
| ); | |
| const executeTakeFirstOrUndefined = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: Query<O>) => | |
| executeTakeFirstOption(client)(query).pipe( | |
| Effect.map((result) => Option.getOrUndefined(result)), | |
| ); | |
| /** | |
| * An error that occurs when attempting to access the first returned row of a query result that is empty. | |
| */ | |
| export class SqlNoFirstResult extends Data.TaggedError(`SqlNoFirstResult`)<{}> { | |
| override toString(): string { | |
| return `SqlNoFirstResult: query result is empty, no first row available`; | |
| } | |
| } | |
| const executeTakeFirstOrError = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: Query<O>) => | |
| executeTakeFirstOption(client)(query).pipe( | |
| Effect.flatMap((result) => Effect.mapError(result, () => new SqlNoFirstResult())), | |
| ); | |
| const executeTakeFirstUnsafe = | |
| <DB>(client: Kysely<DB>) => | |
| <O>(query: Query<O>) => | |
| execute(client)(query).pipe(Effect.map((result) => Array.unsafeGet(result, 0))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment