All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pillars.db_doobie.db.scala Maven / Gradle / Ivy

Go to download

pillars-db-doobie is a scala 3 library providing database services for writing backend applications using doobie

There is a newer version: 0.3.13
Show newest version
// Copyright (c) 2024-2024 by Raphaël Lemaitre and Contributors
// This software is licensed under the Eclipse Public License v2.0 (EPL-2.0).
// For more information see LICENSE or https://opensource.org/license/epl-2-0

package pillars.db_doobie

import cats.effect.*
import cats.effect.std.Console
import cats.syntax.all.*
import com.zaxxer.hikari.HikariConfig
import doobie.*
import doobie.hikari.HikariTransactor
import doobie.implicits.*
import fs2.io.file.Files
import fs2.io.net.Network
import io.circe.Codec
import io.circe.Decoder as CirceDecoder
import io.circe.Encoder as CirceEncoder
import io.circe.derivation.Configuration
import io.github.iltotore.iron.*
import io.github.iltotore.iron.circe.given
import io.github.iltotore.iron.constraint.all.*
import java.util.Properties
import org.typelevel.otel4s.trace.Tracer
import pillars.Config.*
import pillars.Module
import pillars.Modules
import pillars.ModuleSupport
import pillars.Pillars
import pillars.probes.*

final case class DB[F[_]: MonadCancelThrow](config: DatabaseConfig, transactor: Transactor[F]) extends Module[F]:
    override type ModuleConfig = DatabaseConfig

    override def probes: List[Probe[F]] =
        val probe = new Probe[F]:
            override def component: Component = Component(Component.Name("db"), Component.Type.Datastore)
            override def check: F[Boolean]    = sql"select true".query[Boolean].unique.transact(transactor)
        probe.pure[List]
    end probes
end DB

def db[F[_]](using p: Pillars[F]): DB[F] = p.module[DB[F]](DB.Key)

object DB extends ModuleSupport:
    case object Key extends Module.Key:
        override val name: String = "db-doobie"

    override type M[F[_]] = DB[F]
    override val key: Module.Key = DB.Key

    def load[F[_]: Async: Network: Tracer: Console](
        context: ModuleSupport.Context[F],
        modules: Modules[F]
    ): Resource[F, DB[F]] =
        import context.*
        given Files[F] = Files.forAsync[F]
        for
            _      <- Resource.eval(logger.info("Loading DB module"))
            config <- Resource.eval(reader.read[DatabaseConfig]("db"))
            _      <- Resource.eval(logger.info("DB module loaded"))
            xa     <- HikariTransactor.fromHikariConfig[F](config.toHikariConfig)
        yield DB(config, xa)
        end for
    end load
end DB

final case class DatabaseConfig(
    driverClassName: DriverClassName,
    url: JdbcUrl,
    username: DatabaseUser,
    password: Secret[DatabasePassword],
    systemSchema: DatabaseSchema = DatabaseSchema.public,
    appSchema: DatabaseSchema = DatabaseSchema.public,
    poolSize: PoolSize = PoolSize(32),
    statementCache: StatementCacheConfig = StatementCacheConfig(),
    debug: Boolean = false,
    probe: ProbeConfig
) extends pillars.Config:
    def toHikariConfig: HikariConfig =
        val cfg = new HikariConfig
        cfg.setDriverClassName(driverClassName)
        cfg.setJdbcUrl(url)
        cfg.setUsername(username)
        cfg.setPassword(password.value)

        val props = new Properties
        props.put("cachePrepStmts", statementCache.enabled.toString)
        props.put("prepStmtCacheSize", statementCache.size.toString)
        props.put("prepStmtCacheSqlLimit", statementCache.sqlLimit.toString)

        cfg.setDataSourceProperties(props)
        cfg.setMaximumPoolSize(poolSize)

        cfg
    end toHikariConfig
end DatabaseConfig

object DatabaseConfig:
    given Configuration         = Configuration.default.withKebabCaseMemberNames.withKebabCaseConstructorNames.withDefaults
    given Codec[DatabaseConfig] = Codec.AsObject.derivedConfigured
end DatabaseConfig

final case class StatementCacheConfig(
    enabled: Boolean = true,
    size: Size = Size(250),
    sqlLimit: Size = Size(2048)
)

object StatementCacheConfig:
    given Configuration = Configuration.default.withKebabCaseMemberNames.withKebabCaseConstructorNames.withDefaults

    given Codec[StatementCacheConfig] = Codec.AsObject.derivedConfigured
end StatementCacheConfig

private type SizeConstraint = Positive0 DescribedAs "Size must be positive or zero"
opaque type Size <: Int     = Int :| SizeConstraint

object Size extends RefinedTypeOps[Int, SizeConstraint, Size]

private type JdbcUrlConstraint =
    Match["jdbc\\:[^:]+\\:.*"] DescribedAs "Driver class name must in jdbc:: format"
opaque type JdbcUrl <: String  = String :| JdbcUrlConstraint

object JdbcUrl extends RefinedTypeOps[String, JdbcUrlConstraint, JdbcUrl]

private type DriverClassNameConstraint = Not[Blank] DescribedAs "Driver class name must not be blank"
opaque type DriverClassName <: String  = String :| DriverClassNameConstraint

object DriverClassName extends RefinedTypeOps[String, DriverClassNameConstraint, DriverClassName]

private type DatabaseNameConstraint = Not[Blank] DescribedAs "Database name must not be blank"
opaque type DatabaseName <: String  = String :| DatabaseNameConstraint

object DatabaseName extends RefinedTypeOps[String, DatabaseNameConstraint, DatabaseName]

private type DatabaseSchemaConstraint = Not[Blank] DescribedAs "Database schema must not be blank"
opaque type DatabaseSchema <: String  = String :| DatabaseSchemaConstraint

object DatabaseSchema extends RefinedTypeOps[String, DatabaseSchemaConstraint, DatabaseSchema]:
    val public: DatabaseSchema  = DatabaseSchema("public")
    val pillars: DatabaseSchema = DatabaseSchema("pillars")

private type DatabaseTableConstraint =
    (Not[Blank] & Match["""^[a-zA-Z_][0-9a-zA-Z$_]{0,63}$"""]) DescribedAs "Database table must be at most 64 characters (letter, digit, dollar sign or underscore) long and start with a letter or an underscore"
opaque type DatabaseTable <: String  = String :| DatabaseTableConstraint

object DatabaseTable extends RefinedTypeOps[String, DatabaseTableConstraint, DatabaseTable]

private type DatabaseUserConstraint = Not[Blank] DescribedAs "Database user must not be blank"
opaque type DatabaseUser <: String  = String :| DatabaseUserConstraint

object DatabaseUser extends RefinedTypeOps[String, DatabaseUserConstraint, DatabaseUser]

private type DatabasePasswordConstraint = Not[Blank] DescribedAs "Database password must not be blank"
opaque type DatabasePassword <: String  = String :| DatabasePasswordConstraint

object DatabasePassword extends RefinedTypeOps[String, DatabasePasswordConstraint, DatabasePassword]

private type PoolSizeConstraint = GreaterEqual[1] DescribedAs "Pool size must be greater or equal to 1"
opaque type PoolSize <: Int     = Int :| PoolSizeConstraint

object PoolSize extends RefinedTypeOps[Int, PoolSizeConstraint, PoolSize]

private type VersionConstraint      = Not[Blank] & Match["^(\\d+\\.\\d+\\.\\d+)$"] DescribedAs
    "Schema version must be in the form of X.Y.Z"
opaque type SchemaVersion <: String = String :| VersionConstraint

object SchemaVersion extends RefinedTypeOps[String, Not[Blank] & Match["^(\\d+\\.\\d+\\.\\d+)$"], SchemaVersion]




© 2015 - 2025 Weber Informatics LLC | Privacy Policy