Last active
January 12, 2026 02:28
-
-
Save rohovskoi/7bd905eb5e965011708898d6d78cfd6e to your computer and use it in GitHub Desktop.
(POC): DrizzleEffectBridge translates drizzle-orm/effect-postgres's Effect-based execution model to Promise-based expectations from third-party libraries.
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
| import * as Effect from 'effect/Effect'; | |
| import * as Runtime from 'effect/Runtime'; | |
| /** Represents any object that can be wrapped by the proxy */ | |
| type AnyObject = Record<string | symbol, unknown>; | |
| /** The thenable interface that a third-party library expects when awaiting queries */ | |
| type ThenableFunction<T> = ( | |
| onfulfilled?: (value: T) => unknown, | |
| onrejected?: (reason: unknown) => unknown, | |
| ) => Promise<unknown>; | |
| /** An Effect-based Drizzle query builder with an execute method */ | |
| interface EffectQueryBuilder { | |
| readonly [Effect.EffectTypeId]: unknown; | |
| execute: () => Effect.Effect<unknown>; | |
| } | |
| /** Transaction callback signature expected by Better Auth */ | |
| type TransactionCallback<T> = (tx: T) => Promise<unknown>; | |
| /** | |
| * Type guard: Check if an object has the Effect protocol (for proxied objects). | |
| */ | |
| const hasEffectTypeId = ( | |
| obj: unknown, | |
| ): obj is { [Effect.EffectTypeId]: unknown } => { | |
| return obj !== null && typeof obj === 'object' && Effect.EffectTypeId in obj; | |
| }; | |
| /** | |
| * Type guard: Check if an object is an Effect query builder. | |
| * These are drizzle-orm/effect-postgres query builders (Select, Insert, Delete, Update) | |
| * that have the Effect protocol mixed in via applyEffectWrapper. | |
| */ | |
| const isEffectQueryBuilder = (obj: unknown): obj is EffectQueryBuilder => { | |
| return ( | |
| hasEffectTypeId(obj) && | |
| 'execute' in obj && | |
| typeof obj.execute === 'function' | |
| ); | |
| }; | |
| /** | |
| * Type guard: Check if a value is an object (not null, not primitive). | |
| */ | |
| const isObject = (value: unknown): value is AnyObject => { | |
| return value !== null && typeof value === 'object'; | |
| }; | |
| /** | |
| * Type guard: Check if an object is a native Promise or thenable (but not an Effect). | |
| */ | |
| const isNativePromiseOrThenable = (obj: unknown): obj is Promise<unknown> => { | |
| if (!isObject(obj)) return false; | |
| if (obj instanceof Promise) return true; | |
| return ( | |
| 'then' in obj && typeof obj.then === 'function' && !hasEffectTypeId(obj) | |
| ); | |
| }; /** | |
| * DrizzleEffectBridge bridges Promise-based expectations | |
| * with drizzle-orm/effect-postgres's Effect-based execution model. | |
| * | |
| * It wraps the Effect-based Drizzle database instance with a Proxy that: | |
| * 1. Intercepts `.then` calls to execute Effects and return Promises (handled by Effect Runtime) | |
| * 2. Recursively wraps query builders to maintain the bridge through method chaining | |
| * 3. Handles transactions by wrapping the transaction context | |
| */ | |
| export class DrizzleEffectBridge extends Effect.Service<DrizzleEffectBridge>()( | |
| 'DrizzleEffectBridge', | |
| { | |
| effect: Effect.gen(function* () { | |
| const runtime = yield* Effect.runtime<never>(); | |
| const runPromise = Runtime.runPromise(runtime); | |
| const cache = new WeakMap<AnyObject, AnyObject>(); | |
| /** | |
| * Wraps a Drizzle Effect-based database/query builder to make it | |
| * compatible with Better Auth's Promise-based expectations. | |
| * | |
| * Uses a generic type parameter to preserve the original type through | |
| * the proxy wrapper, ensuring Better Auth retains full type inference. | |
| */ | |
| const customEffectToPromiseWrapper = <T>(obj: T): T => { | |
| if (!isObject(obj)) { | |
| return obj; | |
| } | |
| if (isNativePromiseOrThenable(obj)) { | |
| return obj; | |
| } | |
| const cached = cache.get(obj); | |
| if (cached !== undefined) { | |
| return cached as T; | |
| } | |
| const proxy = new Proxy(obj, { | |
| get( | |
| target: AnyObject, | |
| prop: string | symbol, | |
| receiver: unknown, | |
| ): unknown { | |
| // Preserve Symbol properties for Effect and Drizzle internals | |
| if (typeof prop === 'symbol') { | |
| return Reflect.get(target, prop, target); | |
| } | |
| // Preserve Drizzle's internal state accessor | |
| if (prop === '_') { | |
| return Reflect.get(target, prop, target); | |
| } | |
| // Intercept `.then` to bridge Effect → Promise | |
| // When Better Auth awaits a query, this converts the Effect execution to a Promise | |
| if (prop === 'then') { | |
| return createThenHandler(target); | |
| } | |
| const value = Reflect.get(target, prop, receiver); | |
| // Wrap functions to ensure their return values are also wrapped | |
| if (typeof value === 'function') { | |
| if (prop === 'transaction') { | |
| return createTransactionHandler(target); | |
| } | |
| return createMethodWrapper(target, value); | |
| } | |
| // Recursively wrap nested objects (e.g., db.query) | |
| if (isObject(value)) { | |
| return customEffectToPromiseWrapper(value); | |
| } | |
| return value; | |
| }, | |
| }); | |
| cache.set(obj, proxy); | |
| return proxy as T; | |
| }; | |
| /** | |
| * Creates a `.then` handler that executes the Effect and returns a Promise. | |
| * Returns undefined for non-Effect objects to prevent false thenable detection. | |
| */ | |
| const createThenHandler = ( | |
| target: AnyObject, | |
| ): ThenableFunction<unknown> | undefined => { | |
| // Effect query builders: call execute() to get the Effect, then run it | |
| if (isEffectQueryBuilder(target)) { | |
| return (onfulfilled, onrejected) => { | |
| const effect = target.execute(); | |
| return runPromise(effect).then(onfulfilled, onrejected); | |
| }; | |
| } | |
| // Raw Effects (e.g., from transaction results): run directly | |
| if (hasEffectTypeId(target)) { | |
| return (onfulfilled, onrejected) => { | |
| return runPromise(target as Effect.Effect<unknown>).then( | |
| onfulfilled, | |
| onrejected, | |
| ); | |
| }; | |
| } | |
| // Not an Effect - return undefined so it's not treated as a thenable | |
| return undefined; | |
| }; | |
| /** | |
| * Creates a transaction handler that wraps the transaction context | |
| * and bridges the Effect-based transaction with Promise-based callbacks. | |
| */ | |
| const createTransactionHandler = (target: AnyObject) => { | |
| return ( | |
| callback: TransactionCallback<AnyObject>, | |
| config?: unknown, | |
| ): Promise<unknown> => { | |
| const txMethod = target.transaction as ( | |
| cb: (tx: unknown) => Effect.Effect<unknown>, | |
| config?: unknown, | |
| ) => Effect.Effect<unknown>; | |
| const effect = txMethod( | |
| (tx: unknown) => | |
| Effect.promise(() => | |
| callback(customEffectToPromiseWrapper(tx as AnyObject)), | |
| ), | |
| config, | |
| ); | |
| return runPromise(effect); | |
| }; | |
| }; | |
| /** | |
| * Creates a wrapper for methods that ensures return values are also wrapped. | |
| * This maintains the Effect to Promise bridge through method chaining. | |
| */ | |
| const createMethodWrapper = (target: AnyObject, method: Function) => { | |
| return (...args: Array<unknown>): unknown => { | |
| const result = method.apply(target, args); | |
| if (isObject(result)) { | |
| return customEffectToPromiseWrapper(result); | |
| } | |
| return result; | |
| }; | |
| }; | |
| return { | |
| customEffectToPromiseWrapper, | |
| } as const; | |
| }), | |
| }, | |
| ) {} | |
| export const DrizzleEffectBridgeLive = DrizzleEffectBridge.Default; | |
| // ------------------------------------------------------------------ | |
| // USAGE: | |
| // Inject your DrizzleEffectBridge into `Effect.gen()`, | |
| // and don't forget to add its DrizzleEffectBridgeLive layer to the list of `dependencies: [DrizzleEffectBridgeLive]`. | |
| // If you don't provide the DrizzleEffectBridgeLive to the layer your run, you will get an unsatisfied requirement error. | |
| const { customEffectToPromiseWrapper } = yield* DrizzleEffectBridge; | |
| const translatedDb = customEffectToPromiseWrapper(db); | |
| const auth = betterAuth({ | |
| baseURL: APP.API_URL, | |
| basePath: '/auth', | |
| database: drizzleAdapter(translatedDb, { | |
| provider: 'pg', | |
| schema: { | |
| user, | |
| account, | |
| verification, | |
| invitation, | |
| session | |
| }, | |
| transaction: false, | |
| }) | |
| }) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment