database.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import { Client } from "@planetscale/database"
  2. import { drizzle } from "drizzle-orm/planetscale-serverless"
  3. import { migrate as drizzleMigrate } from "drizzle-orm/planetscale-serverless/migrator"
  4. import { Config, ConfigProvider, Effect, Layer, Schema } from "effect"
  5. import * as Context from "effect/Context"
  6. import * as schema from "./database/schema"
  7. import { Resource } from "sst/resource"
  8. export const DatabaseUrl = Schema.NonEmptyString.pipe(Schema.brand("DatabaseUrl"))
  9. export type DatabaseUrl = typeof DatabaseUrl.Type
  10. export class DatabaseSettings extends Schema.Class<DatabaseSettings>("DatabaseSettings")({
  11. url: DatabaseUrl,
  12. migrationsDir: Schema.NonEmptyString,
  13. }) {}
  14. const decodeDatabaseSettings = Schema.decodeUnknownSync(DatabaseSettings)
  15. const config = Config.all({
  16. url: Config.nonEmptyString("DATABASE_URL").pipe(Config.withDefault(Resource.StatsDatabase.url)),
  17. migrationsDir: Config.nonEmptyString("DATABASE_MIGRATIONS_DIR").pipe(Config.withDefault("./migrations")),
  18. }).pipe(Config.map(decodeDatabaseSettings))
  19. export class DatabaseConfig extends Context.Service<DatabaseConfig, DatabaseSettings>()(
  20. "@opencode/stats/DatabaseConfig",
  21. ) {
  22. static readonly config = config
  23. static readonly layer: Layer.Layer<DatabaseConfig, never, never> = Layer.effect(
  24. DatabaseConfig,
  25. config.parse(ConfigProvider.fromEnv()).pipe(Effect.orDie),
  26. )
  27. }
  28. function makeDrizzle(settings: DatabaseSettings) {
  29. return drizzle({ client: new Client({ url: settings.url }), schema })
  30. }
  31. export type Drizzle = ReturnType<typeof makeDrizzle>
  32. export class DrizzleClient extends Context.Service<DrizzleClient, Drizzle>()("@opencode/stats/DrizzleClient") {
  33. static readonly layer: Layer.Layer<DrizzleClient, never, DatabaseConfig> = Layer.effect(
  34. DrizzleClient,
  35. Effect.map(DatabaseConfig, makeDrizzle),
  36. )
  37. }
  38. export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()("DatabaseError", {
  39. cause: Schema.Defect,
  40. }) {}
  41. export const catchDbError = Effect.mapError((cause) => DatabaseError.make({ cause }))
  42. export class MigrationError extends Schema.TaggedErrorClass<MigrationError>()("MigrationError", {
  43. message: Schema.String,
  44. cause: Schema.optional(Schema.Defect),
  45. }) {}
  46. export const migrate = Effect.fn("Database.migrate")(function* () {
  47. const settings = yield* DatabaseConfig
  48. yield* Effect.logInfo("applying database migrations").pipe(
  49. Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
  50. )
  51. const result = yield* Effect.tryPromise({
  52. try: () =>
  53. drizzleMigrate(drizzle({ client: new Client({ url: settings.url }) }), {
  54. migrationsFolder: settings.migrationsDir,
  55. }),
  56. catch: (cause) => new MigrationError({ message: "Failed to apply database migrations", cause }),
  57. })
  58. if (result)
  59. return yield* new MigrationError({
  60. message: `Failed to initialize database migrations: ${result.exitCode}`,
  61. })
  62. yield* Effect.logInfo("database migrations complete").pipe(
  63. Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
  64. )
  65. })
  66. export const layer = Layer.mergeAll(DatabaseConfig.layer, DrizzleClient.layer.pipe(Layer.provide(DatabaseConfig.layer)))