| 1 | import { |
| 2 | resolveConnectionSource, |
| 3 | staticConnectionSource, |
| 4 | type ConnectionSource, |
| 5 | } from "alchemy/SQL/ConnectionSource"; |
| 6 | import { openMySQLPool } from "alchemy/SQL/MySQLDriver"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Layer from "effect/Layer"; |
| 9 | import type * as Redacted from "effect/Redacted"; |
| 10 | import type * as Scope from "effect/Scope"; |
| 11 | import type { PoolOptions } from "mysql2/promise"; |
| 12 | import { |
| 13 | Database, |
| 14 | type DatabaseService, |
| 15 | type DirectDatabase, |
| 16 | } from "./Database.ts"; |
| 17 | import { makeMigrateSupport, type SqlLayerOptions } from "./Postgres.ts"; |
| 18 | |
| 19 | export interface MySQLOptions extends SqlLayerOptions { |
| 20 | readonly pool?: Omit<PoolOptions, "uri">; |
| 21 | } |
| 22 | |
| 23 | /** |
| 24 | * Build the MySQL {@link DatabaseService}. Better Auth's Kysely path |
| 25 | * expects the `mysql2/promise` pool: it passes the pool itself as the |
| 26 | * MysqlDialect config, and the promise pool's `.pool` property is the |
| 27 | * callback pool Kysely drives. |
| 28 | * |
| 29 | * @internal |
| 30 | */ |
| 31 | export const makeMySQLService = ( |
| 32 | source: ConnectionSource, |
| 33 | options: MySQLOptions | undefined, |
| 34 | ): Effect.Effect<DatabaseService> => |
| 35 | Effect.gen(function* () { |
| 36 | const urlEffect = yield* resolveConnectionSource(source); |
| 37 | const migrateSource = staticConnectionSource(source, options?.migrate); |
| 38 | const open = (url: Effect.Effect<Redacted.Redacted<string>>) => |
| 39 | openMySQLPool(url, options?.pool) as unknown as Effect.Effect< |
| 40 | DirectDatabase, |
| 41 | never, |
| 42 | Scope.Scope |
| 43 | >; |
| 44 | |
| 45 | return { |
| 46 | provider: "mysql", |
| 47 | runtime: open(urlEffect) as DatabaseService["runtime"], |
| 48 | ...(migrateSource === undefined |
| 49 | ? {} |
| 50 | : { |
| 51 | migrate: makeMigrateSupport( |
| 52 | migrateSource, |
| 53 | open, |
| 54 | "Failed to load `mysql2` — install it to migrate a MySQL-backed BetterAuth", |
| 55 | ), |
| 56 | }), |
| 57 | } satisfies DatabaseService; |
| 58 | }) as Effect.Effect<DatabaseService>; |
| 59 | |
| 60 | /** |
| 61 | * Generic TCP MySQL database layer for Better Auth, from a connection |
| 62 | * string. |
| 63 | * |
| 64 | * Works with PlanetScale MySQL, Cloudflare Hyperdrive MySQL origins, AWS |
| 65 | * RDS MySQL, or any literal URL. One `mysql2` pool per execution, closed |
| 66 | * when the event settles. |
| 67 | * |
| 68 | * |
| 69 | * ### Connecting to PlanetScale MySQL |
| 70 | * PlanetScale requires TLS — pass it in the URL's `ssl` query parameter |
| 71 | * (mysql2 parses it as JSON). |
| 72 | * **Example:** PlanetScale MySQL with TLS |
| 73 | * ```typescript |
| 74 | * import { BetterAuth } from "@alchemy.run/better-auth"; |
| 75 | * import { MySQL } from "@alchemy.run/better-auth/MySQL"; |
| 76 | * |
| 77 | * const password = yield* Planetscale.MySQLPassword("auth-db", { |
| 78 | * database, |
| 79 | * role: "admin", |
| 80 | * }); |
| 81 | * const url = |
| 82 | * `mysql://${password.username}:...@${password.host}/${database.name}` + |
| 83 | * `?ssl=${encodeURIComponent('{"rejectUnauthorized":true}')}`; |
| 84 | * |
| 85 | * Effect.gen(function* () { |
| 86 | * const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } }); |
| 87 | * return { fetch: ... }; |
| 88 | * }).pipe(Effect.provide(MySQL(url))) |
| 89 | * ``` |
| 90 | * |
| 91 | * @layer |
| 92 | * @provides BetterAuth.Database |
| 93 | * @peer mysql2 |
| 94 | * @product MySQL |
| 95 | */ |
| 96 | export const MySQL = ( |
| 97 | url: ConnectionSource, |
| 98 | options?: MySQLOptions, |
| 99 | ): Layer.Layer<Database> => |
| 100 | Layer.effect( |
| 101 | Database, |
| 102 | makeMySQLService(url, options), |
| 103 | ) as Layer.Layer<Database>; |