File size: 3,430 Bytes
4bbfe8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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)))