feat(effect-drizzle-sqlite): add sqlite adapter
This commit is contained in:
parent
0e13279545
commit
89efce865d
5 changed files with 425 additions and 0 deletions
228
packages/effect-drizzle-sqlite/src/index.ts
Normal file
228
packages/effect-drizzle-sqlite/src/index.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import { Database } from "bun:sqlite"
|
||||
import { drizzle as drizzleBun, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
|
||||
import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations"
|
||||
import { SQLiteCountBuilder } from "drizzle-orm/sqlite-core/query-builders/count"
|
||||
import { SQLiteDeleteBase } from "drizzle-orm/sqlite-core/query-builders/delete"
|
||||
import { SQLiteInsertBase } from "drizzle-orm/sqlite-core/query-builders/insert"
|
||||
import { SQLiteRelationalQuery, SQLiteSyncRelationalQuery } from "drizzle-orm/sqlite-core/query-builders/_query"
|
||||
import { SQLiteSelectBase } from "drizzle-orm/sqlite-core/query-builders/select"
|
||||
import { SQLiteUpdateBase } from "drizzle-orm/sqlite-core/query-builders/update"
|
||||
import type { SQLiteTransaction, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session"
|
||||
import { SQLitePreparedQuery } from "drizzle-orm/sqlite-core/session"
|
||||
import type { DrizzleConfig } from "drizzle-orm/utils"
|
||||
import { Cause, Effect, Exit, Schema } from "effect"
|
||||
import { pipeArguments } from "effect/Pipeable"
|
||||
|
||||
export class EffectDrizzleQueryError extends Schema.TaggedErrorClass<EffectDrizzleQueryError>()(
|
||||
"EffectDrizzleQueryError",
|
||||
{
|
||||
query: Schema.String,
|
||||
params: Schema.Array(Schema.Unknown),
|
||||
cause: Schema.Unknown,
|
||||
},
|
||||
) {
|
||||
override get message() {
|
||||
return `Failed query: ${this.query}\nparams: ${JSON.stringify(this.params)}`
|
||||
}
|
||||
}
|
||||
|
||||
export type EffectSQLiteDatabase<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
> = SQLiteBunDatabase<TSchema, TRelations> & {
|
||||
readonly $client: Database
|
||||
readonly withTransaction: <A, E>(
|
||||
transaction: (tx: SQLiteTransaction<"sync", void, TSchema, TRelations>) => Effect.Effect<A, E>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
) => Effect.Effect<A, E>
|
||||
}
|
||||
|
||||
export type MakeConfig<
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
> = DrizzleConfig<TSchema, TRelations> & {
|
||||
readonly client?: Database
|
||||
readonly filename?: string
|
||||
}
|
||||
|
||||
type EffectLikeQuery<A = unknown> = {
|
||||
readonly asEffect?: () => Effect.Effect<A, EffectDrizzleQueryError>
|
||||
readonly toSQL?: () => { readonly sql: string; readonly params?: readonly unknown[] }
|
||||
}
|
||||
|
||||
type PreparedLike<A = unknown> = EffectLikeQuery<A> & {
|
||||
readonly execute: () => unknown
|
||||
readonly getQuery?: () => { readonly sql: string; readonly params?: readonly unknown[] }
|
||||
}
|
||||
|
||||
type SelectLike<A = unknown> = EffectLikeQuery<A> & {
|
||||
readonly all: () => A
|
||||
}
|
||||
|
||||
type MutationLike<A = unknown> = EffectLikeQuery<A> & {
|
||||
readonly all: () => A
|
||||
readonly run: () => A
|
||||
readonly config?: { readonly returning?: unknown }
|
||||
}
|
||||
|
||||
type CountLike = EffectLikeQuery<number> & PromiseLike<number>
|
||||
|
||||
class TransactionFailure extends Error {
|
||||
constructor(readonly effectCause: Cause.Cause<unknown>) {
|
||||
super("Effect transaction failed")
|
||||
}
|
||||
}
|
||||
|
||||
const EffectTypeId = "~effect/Effect"
|
||||
const EffectIdentifier = `${EffectTypeId}/identifier`
|
||||
const EffectEvaluate = `${EffectTypeId}/evaluate`
|
||||
|
||||
const effectVariance = {
|
||||
_A: (value: unknown) => value,
|
||||
_E: (value: unknown) => value,
|
||||
_R: (value: unknown) => value,
|
||||
}
|
||||
|
||||
const queryInfo = (query: EffectLikeQuery | PreparedLike) => {
|
||||
const info = "getQuery" in query && typeof query.getQuery === "function" ? query.getQuery() : query.toSQL?.()
|
||||
return {
|
||||
query: info?.sql ?? "<unknown>",
|
||||
params: [...(info?.params ?? [])],
|
||||
}
|
||||
}
|
||||
|
||||
const queryError = (query: EffectLikeQuery | PreparedLike, cause: unknown) =>
|
||||
new EffectDrizzleQueryError({
|
||||
...queryInfo(query),
|
||||
cause,
|
||||
})
|
||||
|
||||
const fromSync = <A>(query: EffectLikeQuery, run: () => A) =>
|
||||
Effect.try({
|
||||
try: run,
|
||||
catch: (cause) => queryError(query, cause),
|
||||
})
|
||||
|
||||
const fromMutation = (query: MutationLike) => fromSync(query, () => (query.config?.returning ? query.all() : query.run()))
|
||||
|
||||
const fromExecuteResult = (result: unknown) => {
|
||||
if (result && typeof result === "object" && "sync" in result && typeof result.sync === "function") {
|
||||
return result.sync()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const queryEffectProto = {
|
||||
[EffectTypeId]: effectVariance,
|
||||
pipe() {
|
||||
return pipeArguments(this, arguments)
|
||||
},
|
||||
[Symbol.iterator]() {
|
||||
let done = false
|
||||
const self = this
|
||||
return {
|
||||
next(value: unknown) {
|
||||
if (done) return { done: true, value }
|
||||
done = true
|
||||
return { done: false, value: self }
|
||||
},
|
||||
[Symbol.iterator]() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
},
|
||||
[EffectIdentifier]: "DrizzleSqliteQuery",
|
||||
[EffectEvaluate](this: EffectLikeQuery) {
|
||||
return this.asEffect?.() ?? Effect.die("Drizzle SQLite query is missing asEffect()")
|
||||
},
|
||||
}
|
||||
|
||||
const patchClass = <A>(ctor: { readonly prototype: object }, asEffect: (self: A) => Effect.Effect<unknown, EffectDrizzleQueryError>) => {
|
||||
if (Object.prototype.hasOwnProperty.call(ctor.prototype, "asEffect")) return
|
||||
Object.assign(ctor.prototype, queryEffectProto, {
|
||||
asEffect(this: A) {
|
||||
return asEffect(this)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const patchQueryBuilders = (() => {
|
||||
let patched = false
|
||||
return () => {
|
||||
if (patched) return
|
||||
patched = true
|
||||
|
||||
patchClass(SQLitePreparedQuery, (query: PreparedLike) => fromSync(query, () => fromExecuteResult(query.execute())))
|
||||
patchClass(SQLiteSelectBase, (query: SelectLike) => fromSync(query, () => query.all()))
|
||||
patchClass(SQLiteInsertBase, fromMutation)
|
||||
patchClass(SQLiteUpdateBase, fromMutation)
|
||||
patchClass(SQLiteDeleteBase, fromMutation)
|
||||
patchClass(SQLiteRelationalQuery, (query: EffectLikeQuery & { readonly executeRaw: () => unknown }) =>
|
||||
fromSync(query, () => query.executeRaw()),
|
||||
)
|
||||
patchClass(SQLiteSyncRelationalQuery, (query: EffectLikeQuery & { readonly executeRaw: () => unknown }) =>
|
||||
fromSync(query, () => query.executeRaw()),
|
||||
)
|
||||
patchClass(SQLiteCountBuilder, (query: CountLike) =>
|
||||
Effect.tryPromise({
|
||||
try: () => Promise.resolve(query),
|
||||
catch: (cause) => queryError(query, cause),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
const attachTransaction = <
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
>(db: SQLiteBunDatabase<TSchema, TRelations> & { readonly $client: Database }): EffectSQLiteDatabase<TSchema, TRelations> => {
|
||||
const runTransaction = db.transaction.bind(db) as (
|
||||
transaction: (tx: SQLiteTransaction<"sync", void, TSchema, TRelations>) => unknown,
|
||||
config?: SQLiteTransactionConfig,
|
||||
) => unknown
|
||||
|
||||
return Object.assign(db, {
|
||||
withTransaction: <A, E>(
|
||||
transaction: (tx: SQLiteTransaction<"sync", void, TSchema, TRelations>) => Effect.Effect<A, E>,
|
||||
config?: SQLiteTransactionConfig,
|
||||
) =>
|
||||
Effect.sync(
|
||||
() =>
|
||||
runTransaction(
|
||||
(tx) =>
|
||||
Exit.match(Effect.runSyncExit(transaction(tx)), {
|
||||
onSuccess: (value) => value,
|
||||
onFailure: (cause) => {
|
||||
throw new TransactionFailure(cause)
|
||||
},
|
||||
}),
|
||||
config,
|
||||
) as A,
|
||||
).pipe(
|
||||
Effect.catchDefect((defect) =>
|
||||
defect instanceof TransactionFailure ? Effect.failCause(defect.effectCause as Cause.Cause<E>) : Effect.die(defect),
|
||||
),
|
||||
),
|
||||
}) as EffectSQLiteDatabase<TSchema, TRelations>
|
||||
}
|
||||
|
||||
export const make = <
|
||||
TSchema extends Record<string, unknown> = Record<string, never>,
|
||||
TRelations extends AnyRelations = EmptyRelations,
|
||||
>(config: MakeConfig<TSchema, TRelations> = {}): EffectSQLiteDatabase<TSchema, TRelations> => {
|
||||
patchQueryBuilders()
|
||||
return attachTransaction(
|
||||
drizzleBun({
|
||||
...config,
|
||||
client: config.client ?? new Database(config.filename ?? ":memory:"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const drizzle = make
|
||||
|
||||
declare module "drizzle-orm/query-promise" {
|
||||
interface QueryPromise<T> extends Effect.Effect<T, EffectDrizzleQueryError> {
|
||||
asEffect(): Effect.Effect<T, EffectDrizzleQueryError>
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue