database.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  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 Error {
  39. readonly _tag = "DatabaseError"
  40. constructor(input: { cause: unknown }) {
  41. super("Database operation failed", { cause: input.cause })
  42. this.name = "DatabaseError"
  43. }
  44. static make(input: { cause: unknown }) {
  45. return new DatabaseError(input)
  46. }
  47. }
  48. export const catchDbError = Effect.mapError((cause) => DatabaseError.make({ cause }))
  49. export class MigrationError extends Error {
  50. readonly _tag = "MigrationError"
  51. constructor(input: { message: string; cause?: unknown }) {
  52. super(input.message, { cause: input.cause })
  53. this.name = "MigrationError"
  54. }
  55. }
  56. export const migrate = Effect.fn("Database.migrate")(function* () {
  57. const settings = yield* DatabaseConfig
  58. yield* Effect.logInfo("applying database migrations").pipe(
  59. Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
  60. )
  61. const result = yield* Effect.tryPromise({
  62. try: () =>
  63. drizzleMigrate(drizzle({ client: new Client({ url: settings.url }) }), {
  64. migrationsFolder: settings.migrationsDir,
  65. }),
  66. catch: (cause) => new MigrationError({ message: "Failed to apply database migrations", cause }),
  67. })
  68. if (result)
  69. return yield* Effect.fail(
  70. new MigrationError({
  71. message: `Failed to initialize database migrations: ${result.exitCode}`,
  72. }),
  73. )
  74. yield* Effect.logInfo("database migrations complete").pipe(
  75. Effect.annotateLogs({ migrationsDir: settings.migrationsDir }),
  76. )
  77. })
  78. export const layer = Layer.mergeAll(DatabaseConfig.layer, DrizzleClient.layer.pipe(Layer.provide(DatabaseConfig.layer)))