| 1 | import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; |
| 2 | import type { RuntimeContext } from "alchemy"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Scope from "effect/Scope"; |
| 6 | import { Database, type DatabaseInput, type Provider } from "./Database.ts"; |
| 7 | |
| 8 | type DrizzleDatabase = Parameters<typeof drizzleAdapter>[0]; |
| 9 | type DrizzleDatabaseEffect = Effect.Effect< |
| 10 | DrizzleDatabase, |
| 11 | never, |
| 12 | RuntimeContext | Scope.Scope |
| 13 | >; |
| 14 | |
| 15 | const isDatabaseEffect = ( |
| 16 | value: DrizzleDatabase | DrizzleDatabaseEffect, |
| 17 | ): value is DrizzleDatabaseEffect => Effect.isEffect(value); |
| 18 | |
| 19 | export interface DrizzleLayerConfig { |
| 20 | /** The SQL dialect of the underlying drizzle database. */ |
| 21 | readonly provider: "pg" | "mysql" | "sqlite"; |
| 22 | /** |
| 23 | * The drizzle schema containing the Better Auth tables (generate it with |
| 24 | * `npx auth@1.7.5 generate`). When omitted, the adapter resolves tables |
| 25 | * off the db instance's registered schema. |
| 26 | */ |
| 27 | readonly schema?: Record<string, unknown>; |
| 28 | /** @default false */ |
| 29 | readonly usePlural?: boolean; |
| 30 | /** @default false (snake_case column names) */ |
| 31 | readonly camelCase?: boolean; |
| 32 | } |
| 33 | |
| 34 | /** |
| 35 | * Use an existing Drizzle database as Better Auth's storage via |
| 36 | * better-auth's Relations v2 `drizzleAdapter`. |
| 37 | * |
| 38 | * Accepts a plain drizzle instance, or an Effect resolving to one for |
| 39 | * databases that only materialize at runtime. NOTE: alchemy's own |
| 40 | * `Drizzle.Postgres`/`Drizzle.D1` chainable proxies yield Effects rather |
| 41 | * than thenables and cannot back the (promise-based) adapter — pass the |
| 42 | * raw `drizzle(...)` instance instead. |
| 43 | * |
| 44 | * Schema management is yours: this layer has no automatic migration |
| 45 | * support (`npx auth@1.7.5 generate` + your drizzle-kit flow own the |
| 46 | * tables). Spread generated `authRelations` after your app's |
| 47 | * `defineRelations` when constructing the db. The CLI configuration must |
| 48 | * use `drizzleAdapter` from `@better-auth/drizzle-adapter/relations-v2`. |
| 49 | * Run `npx auth@1.7.5 generate --config ./auth.cli.ts`. |
| 50 | * Passing `--adapter drizzle` selects the CLI's legacy Relations v1 generator. |
| 51 | * |
| 52 | * ### Bringing your own Drizzle db |
| 53 | * **Example:** Postgres drizzle instance with generated auth relations |
| 54 | * ```typescript |
| 55 | * import { BetterAuth } from "@alchemy.run/better-auth"; |
| 56 | * import { Drizzle } from "@alchemy.run/better-auth/Drizzle"; |
| 57 | * import { drizzle } from "drizzle-orm/node-postgres"; |
| 58 | * import * as schema from "./auth-schema.ts"; |
| 59 | * import { relations } from "./app-schema.ts"; |
| 60 | * |
| 61 | * const db = drizzle({ |
| 62 | * client: pool, |
| 63 | * relations: { ...relations, ...schema.authRelations }, |
| 64 | * }); |
| 65 | * |
| 66 | * Effect.gen(function* () { |
| 67 | * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } }); |
| 68 | * // ... |
| 69 | * }).pipe(Effect.provide(Drizzle(db, { provider: "pg", schema }))) |
| 70 | * ``` |
| 71 | * |
| 72 | * @layer |
| 73 | * @provides BetterAuth.Database |
| 74 | * @peer drizzle-orm |
| 75 | * @peer @better-auth/drizzle-adapter |
| 76 | * @product Drizzle |
| 77 | */ |
| 78 | export const Drizzle = ( |
| 79 | db: DrizzleDatabase | DrizzleDatabaseEffect, |
| 80 | config: DrizzleLayerConfig, |
| 81 | ): Layer.Layer<Database> => |
| 82 | Layer.sync(Database, () => ({ |
| 83 | provider: (config.provider === "pg" |
| 84 | ? "postgres" |
| 85 | : config.provider) as Provider, |
| 86 | runtime: Effect.gen(function* () { |
| 87 | const database = isDatabaseEffect(db) ? yield* db : db; |
| 88 | return drizzleAdapter(database, { |
| 89 | provider: config.provider, |
| 90 | ...(config.schema !== undefined ? { schema: config.schema } : {}), |
| 91 | ...(config.usePlural !== undefined |
| 92 | ? { usePlural: config.usePlural } |
| 93 | : {}), |
| 94 | ...(config.camelCase !== undefined |
| 95 | ? { camelCase: config.camelCase } |
| 96 | : {}), |
| 97 | }) as DatabaseInput; |
| 98 | }), |
| 99 | })); |