| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394 |
- 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>("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<DatabaseConfig, DatabaseSettings>()(
- "@opencode/stats/DatabaseConfig",
- ) {
- static readonly config = config
- static readonly layer: Layer.Layer<DatabaseConfig, never, never> = 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<typeof makeDrizzle>
- export class DrizzleClient extends Context.Service<DrizzleClient, Drizzle>()("@opencode/stats/DrizzleClient") {
- static readonly layer: Layer.Layer<DrizzleClient, never, DatabaseConfig> = 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)))
|