| 1 | import type { Input, RuntimeContext } from "alchemy"; |
| 2 | import type { BetterAuthOptions } from "better-auth"; |
| 3 | import * as Context from "effect/Context"; |
| 4 | import type * as Effect from "effect/Effect"; |
| 5 | import type * as Scope from "effect/Scope"; |
| 6 | import type { BetterAuthMigrationError } from "./Errors.ts"; |
| 7 | |
| 8 | /** The database dialect family, as Better Auth's Kysely adapter names them. */ |
| 9 | export type Provider = "sqlite" | "postgres" | "mysql"; |
| 10 | |
| 11 | /** |
| 12 | * Everything Better Auth accepts as `options.database`: a raw driver object |
| 13 | * (pg `Pool`, mysql2 pool, `bun:sqlite` `Database`, Cloudflare `D1Database`, |
| 14 | * ...), a Kysely dialect form, or an adapter factory |
| 15 | * (`drizzleAdapter(...)`, `memoryAdapter(...)`, custom adapters). |
| 16 | */ |
| 17 | export type DatabaseInput = NonNullable<BetterAuthOptions["database"]>; |
| 18 | |
| 19 | /** |
| 20 | * The Kysely-resolvable subset of {@link DatabaseInput} — raw driver objects |
| 21 | * and dialect forms, excluding adapter factories. |
| 22 | * |
| 23 | * This is the only shape `migrate.connect` may yield: Better Auth's |
| 24 | * `getMigrations` supports exactly these forms and hard-`process.exit(1)`s |
| 25 | * on adapter factories, so excluding them statically keeps that landmine |
| 26 | * unreachable. |
| 27 | */ |
| 28 | export type DirectDatabase = Exclude< |
| 29 | DatabaseInput, |
| 30 | (...args: never[]) => unknown |
| 31 | >; |
| 32 | |
| 33 | export interface DatabaseService { |
| 34 | /** Dialect family of the database this layer connects to. */ |
| 35 | readonly provider: Provider; |
| 36 | /** |
| 37 | * Build the runtime `options.database` input. Evaluated once per |
| 38 | * execution (memoized on the execution Scope by {@link BetterAuth}), so |
| 39 | * disposable resources acquired with `Effect.acquireRelease` — pg/mysql |
| 40 | * pools — are closed when the event settles. That is the only legal |
| 41 | * pooling shape on workerd (sockets are IoContext-pinned). |
| 42 | * |
| 43 | * Deploy/plan evaluation never runs this effect: construction is deferred |
| 44 | * to the first `api`/`fetch`/`getSession` use inside an execution scope. |
| 45 | */ |
| 46 | readonly runtime: Effect.Effect< |
| 47 | DatabaseInput, |
| 48 | never, |
| 49 | RuntimeContext | Scope.Scope |
| 50 | >; |
| 51 | /** |
| 52 | * Deploy-time migration support. `undefined` means this platform cannot |
| 53 | * auto-migrate (memory, drizzle, custom adapters): {@link BetterAuth} |
| 54 | * skips migration registration, and fails synth if `migrate: true` was |
| 55 | * explicitly requested. |
| 56 | */ |
| 57 | readonly migrate?: { |
| 58 | /** |
| 59 | * Non-secret, canonically-hashable identity of the target database |
| 60 | * (resource Outputs allowed) — part of the migration Action's input, so |
| 61 | * migrations re-run when the target database is replaced. |
| 62 | * |
| 63 | * MUST NOT contain the connection string: Action input is persisted to |
| 64 | * alchemy state. |
| 65 | */ |
| 66 | readonly identity: Input<Record<string, unknown>>; |
| 67 | /** |
| 68 | * Two-stage connection for the migration Action. |
| 69 | * |
| 70 | * The OUTER effect is the Action's *init* half: it runs at stack-eval |
| 71 | * time under the Action's capture context — this is where resource |
| 72 | * Outputs (`database.databaseId`, a connection-string attr) MUST be |
| 73 | * yielded so the engine records them as dependency captures and |
| 74 | * resolves them at apply. No I/O here. |
| 75 | * |
| 76 | * The INNER effect is the *apply* half: it runs inside the Action body |
| 77 | * with the resolved captures ambient, acquires the actual client/pool |
| 78 | * on the Scope (`pool.end` / `db.close` ride it), and yields a |
| 79 | * Kysely-resolvable database. |
| 80 | */ |
| 81 | readonly connect: Effect.Effect< |
| 82 | Effect.Effect<DirectDatabase, BetterAuthMigrationError, Scope.Scope>, |
| 83 | never, |
| 84 | RuntimeContext |
| 85 | >; |
| 86 | }; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * The database dependency of {@link BetterAuth}. |
| 91 | * |
| 92 | * Platform layers (`Memory`, `SQLite`, `CloudflareD1`, `Postgres`, `MySQL`, |
| 93 | * `Drizzle`, ...) provide this service; `yield* BetterAuth({...})` consumes |
| 94 | * it. Provide the layer on the surrounding impl effect: |
| 95 | * |
| 96 | * ```typescript |
| 97 | * Effect.gen(function* () { |
| 98 | * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } }); |
| 99 | * return { fetch: ... }; |
| 100 | * }).pipe(Effect.provide(CloudflareD1(Db))) |
| 101 | * ``` |
| 102 | */ |
| 103 | export class Database extends Context.Service<Database, DatabaseService>()( |
| 104 | "BetterAuth.Database", |
| 105 | ) {} |