import { Client } from "@planetscale/database" import { drizzle } from "drizzle-orm/planetscale-serverless" import { migrate as drizzleMigrate } from "drizzle-orm/planetscale-serverless/migrator" import { Config, ConfigProvider, Effect, Layer, Schema } from "effect" import * as Context from "effect/Context" import * as schema from "./database/schema" import { Resource } from "sst/resource" export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl")) export type DatabaseUrl = typeof DatabaseUrl.Type export class DatabaseSettings extends Schema.Class("DatabaseSettings")({ url: DatabaseUrl, migrationsDir: Schema.NonEmptyString, }) {} const decodeDatabaseSettings = Schema.decodeUnknownSync(DatabaseSettings) const config = Config.all({ url: Config.nonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)), migrationsDir: Config.nonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")), }).pipe(Config.map(decodeDatabaseSettings)) export class DatabaseConfig extends Context.Service()( "@opencode/stats/DatabaseConfig", ) { static readonly config = config static readonly layer: Layer.Layer = Layer.effect( DatabaseConfig, config.parse(ConfigProvider.fromEnv()).pipe(Effect.orDie), ) } function makeDrizzle(settings: DatabaseSettings) { return drizzle({ client: new Client({ url: settings.url }), schema }) } export type Drizzle = ReturnType export class DrizzleClient extends Context.Service()("@opencode/stats/DrizzleClient") { static readonly layer: Layer.Layer = Layer.effect( DrizzleClient, Effect.map(DatabaseConfig, makeDrizzle), ) } export class DatabaseError extends Error { readonly _tag = "DatabaseError" constructor(input: { cause: unknown }) { super("Database operation failed", { cause: input.cause }) this.name = "DatabaseError" } static make(input: { cause: unknown }) { return new DatabaseError(input) } } export const catchDbError = Effect.mapError((cause) => DatabaseError.make({ cause })) export class MigrationError extends Error { readonly _tag = "MigrationError" constructor(input: { message: string; cause?: unknown }) { super(input.message, { cause: input.cause }) this.name = "MigrationError" } } export const migrate = Effect.fn("Database.migrate")(function* () { const settings = yield* DatabaseConfig yield* Effect.logInfo("applying database migrations").pipe( Effect.annotateLogs({ migrationsDir: settings.migrationsDir }), ) const result = yield* Effect.tryPromise({ try: () => drizzleMigrate(drizzle({ client: new Client({ url: settings.url }) }), { migrationsFolder: settings.migrationsDir, }), catch: (cause) => new MigrationError({ message: "Failed to apply database migrations", cause }), }) if (result) return yield* Effect.fail( new MigrationError({ message: `Failed to initialize database migrations: ${result.exitCode}`, }), ) yield* Effect.logInfo("database migrations complete").pipe( Effect.annotateLogs({ migrationsDir: settings.migrationsDir }), ) }) export const layer = Layer.mergeAll(DatabaseConfig.layer, DrizzleClient.layer.pipe(Layer.provide(DatabaseConfig.layer)))