Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Make skunkier #162

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: CI
on:
push:
branches: ["main"]
branches: ["*"]
tags: ["v*"]
pull_request:
branches: ["*"]
Expand All @@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-java@v3
with:
distribution: 'temurin'
distribution: 'temurin'
java-version: '17'
cache: 'sbt'

Expand All @@ -37,19 +37,19 @@ jobs:
run: sbt --client 'compile; Test/compile;'

- name: Fast tests
run: sbt --client fastTests
- name: Integration tests
run: sbt --client fastTests

- name: Integration tests
run: sbt --client integrationTests

- name: Frontend tests
- name: Frontend tests
run: sbt --client playwrightTests

- name: Build Docker container
run: sbt --client 'app/Docker/publishLocal; versionDump'

- name: Deploy to Fly.io
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ frontend-build/node_modules
node_modules
.idea
postgres-data
.smithy.lsp.log

.vscode/settings.json

.env
34 changes: 16 additions & 18 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@ import smithy4s.codegen.Smithy4sCodegenPlugin
Global / onChangedBuildSource := ReloadOnSourceChanges

val Versions = new {
val http4s = "0.23.19"
val http4s = "0.23.23"

val Scala = "3.2.2"
val Scala = "3.3.1"

val scribe = "3.11.5"
val scribe = "3.12.2"

val http4sDom = "0.2.7"
val http4sDom = "0.2.10"

val Flyway = "9.15.2"
val Flyway = "9.22.3"

val Postgres = "42.5.4"
val Postgres = "42.6.0"

val TestContainers = "0.40.9"

val Weaver = "0.8.1"

val Playwright = "0.0.5"

val Laminar = "15.0.0-M7"
val Laminar = "16.0.0"

val waypoint = "6.0.0-M5"
val waypoint = "7.0.0"

val scalacss = "1.0.0"

val circe = "0.14.5"
val circe = "0.14.6"

val doobie = "1.0.0-RC2"
val skunk = "0.6.1"

val macroTaskExecutor = "1.1.1"

Expand Down Expand Up @@ -106,9 +106,7 @@ lazy val backend = projectMatrix
"com.outr" %% "scribe" % Versions.scribe,
"com.outr" %% "scribe-cats" % Versions.scribe,
"com.outr" %% "scribe-slf4j" % Versions.scribe,
"org.tpolecat" %% "doobie-core" % Versions.doobie,
"org.tpolecat" %% "doobie-postgres" % Versions.doobie,
"org.tpolecat" %% "doobie-hikari" % Versions.doobie
"org.tpolecat" %% "skunk-core" % Versions.skunk
),
Compile / doc / sources := Seq.empty
)
Expand Down Expand Up @@ -295,11 +293,11 @@ import sbtwelcome.*

logo :=
s"""
| ##### ###### # # ##### # ## ##### ######
| # # ## ## # # # # # # #
| # ##### # ## # # # # # # # #####
| # # # # ##### # ###### # #
| # # # # # # # # # #
| ##### ###### # # ##### # ## ##### ######
| # # ## ## # # # # # # #
| # ##### # ## # # # # # # # #####
| # # # # ##### # ###### # #
| # # # # # # # # # #
| # ###### # # # ###### # # # ######
|
|Version: ${version.value}
Expand Down
6 changes: 4 additions & 2 deletions modules/app/src/main/scala/bootstrap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ def bootstrap(

pgCredentials = cloudDb.getOrElse(PgCredentials.from(env))

db <- DoobieDatabase.hikari(pgCredentials)
pool <- SkunkDatabase.sessionPool(
pgCredentials
)

services = Services.build(
logger,
db
Database.fromSessionPool(pool)
)

routes <- Routes.build(
Expand Down
17 changes: 17 additions & 0 deletions modules/backend/src/main/scala/database.codecs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package hellosmithy4s

import skunk.Codec
import skunk.codec.all.*
import smithy4s.Newtype
import hellosmithy4s.spec.*

object codecs:
extension [T](c: Codec[T])
def as(obj: Newtype[T]): Codec[obj.Type] =
c.imap(obj.apply(_))(_.value)

val userId: Codec[Key] = varchar(50).as(Key)
val value: Codec[Value] = int4.as(Value)


end codecs
28 changes: 0 additions & 28 deletions modules/backend/src/main/scala/database.doobie.scala

This file was deleted.

95 changes: 36 additions & 59 deletions modules/backend/src/main/scala/database.operations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,50 @@ import cats.effect.IO
import spec.*

import cats.*, cats.data.*, cats.implicits.*
import doobie.*, doobie.implicits.*
import cats.effect._
import skunk._
import skunk.implicits._
import skunk.codec.all._
import java.time.OffsetDateTime
import natchez.Trace.Implicits.noop
import smithy4s.Newtype

sealed trait SqlOp[I, O]
// sealed trait SqlOp[I, O]

sealed abstract class SqlQuery[I, O](val input: I, val out: Query0[O])
extends SqlOp[I, O]
sealed abstract class SqlUpdate[I](val input: I, val out: Update0)
extends SqlOp[I, Int]
// sealed abstract class SqlQuery[I, O](val input: I, val out: Query0[O])
// extends SqlOp[I, O]
// sealed abstract class SqlUpdate[I](val input: I, val out: Update0)
// extends SqlOp[I, Int]

private def ntGet[T: Get](nt: Newtype[T]): Get[nt.Type] =
Get[T].map(nt.apply)
// private def ntGet[T: Get](nt: Newtype[T]): Get[nt.Type] =
// Get[T].map(nt.apply)

private def ntPut[T: Put](nt: Newtype[T]): Put[nt.Type] =
Put[T].contramap[nt.Type](_.value)
// private def ntPut[T: Put](nt: Newtype[T]): Put[nt.Type] =
// Put[T].contramap[nt.Type](_.value)

given Get[Key] = ntGet(Key)
given Get[Value] = ntGet(Value)
// given Get[Key] = ntGet(Key)
// given Get[Value] = ntGet(Value)

given Put[Key] = ntPut(Key)
given Put[Value] = ntPut(Value)
// given Put[Key] = ntPut(Key)
// given Put[Value] = ntPut(Value)

object operations:

case class Get(key: Key)
extends SqlQuery(
key,
sql"select value from examples where key = $key"
.query[Int]
.map(Value.apply)
)

case object GetAll
extends SqlQuery(
(),
sql"select key, value from examples order by key"
.query[(Key, Value)]
.map(Pair.apply)
)

case class Create(key: Key, value: Option[Value])
extends SqlUpdate(
key,
sql"insert into examples (key, value) values ($key, ${value.map(_.value).getOrElse(0)})".update
)

case class Delete(key: Key)
extends SqlUpdate(
key,
sql"delete from examples where key = $key".update
)

case class Inc(key: Key)
extends SqlUpdate(
key,
sql"update examples set value = value + 1 where key = $key".update
)

case class Dec(key: Key)
extends SqlUpdate(
key,
sql"update examples set value = value - 1 where key = $key".update
)

case class Update(key: Key, value: Value)
extends SqlUpdate(
key,
sql"update examples set value = ${value.value} where key = $key".update
)
val getAll: Query[Void, Pair] = sql"select key, value from examples order by key".query(codecs.userId ~ codecs.value).map(Pair.apply)

val getKey: Query[Key, Value] = sql"select value from examples where key = ${codecs.userId}".query[Value](codecs.value)

val create: Command[(Key, Value)] = sql"insert into examples (key, value) values (${codecs.userId}, ${codecs.value})".command

val update: Query[Pair, Pair] = sql"update examples set value = ${codecs.value} where key = ${codecs.userId} RETURNING value, key"
.query(codecs.value ~ codecs.userId)
.contramap[Pair](p => p.value ~ p.key)
.map{case(v ~ k) => Pair(k, v)}

val delete: Command[Key] = sql"delete from examples where key = ${codecs.userId}".command

val incrementValue: Query[Key, Pair] = sql"update examples set value = value + 1 where key = ${codecs.userId} returning key, value".query(codecs.userId ~ codecs.value).map(Pair.apply)

val decrementValue: Query[Key, Pair] = sql"update examples set value = value - 1 where key = ${codecs.userId} returning key, value".query(codecs.userId ~ codecs.value).map(Pair.apply)

end operations
69 changes: 64 additions & 5 deletions modules/backend/src/main/scala/database.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,71 @@
package hellosmithy4s

import cats.effect.*
import hellosmithy4s.spec.*
import skunk.*
import fs2.Stream
import skunk.data.Completion
import skunk.net.message.CommandComplete

trait Database:
def stream[I, O](query: SqlOp[I, O]): fs2.Stream[IO, O]
def getKey(key: Key): IO[Option[Value]]
def create(key: Key, value: Option[Value]): IO[Unit]
def getAll(): IO[Stream[IO, Pair]]
def delete(key: Key): IO[Int]
def inc(key: Key): IO[Option[Pair]]
def dec(key: Key): IO[Option[Pair]]
def update(key: Key, value: Value): IO[Option[Pair]]

def vector[I, O](query: SqlOp[I, O]): IO[Vector[O]] =
stream(query).compile.toVector
object Database:

def option[I, O](query: SqlOp[I, O]): IO[Option[O]] =
vector(query).map(_.headOption)
def fromSessionPool(pool: SkunkSessionPool): Database =
new Database:
override def update(key: Key, value: Value): IO[Option[Pair]] = pool.use{s =>
s.prepare(operations.update).flatMap(
_.option(Pair(key, value))
)
}

override def dec(key: Key): IO[Option[Pair]] = pool.use{
s => s.prepare(operations.decrementValue).flatMap(
_.option(key)
)
}

override def inc(key: Key): IO[Option[Pair]] = pool.use{
s => s.prepare(operations.incrementValue).flatMap(
_.option(key)
)
}

override def delete(key: Key): IO[Int] = pool.use{
s => s.prepare(operations.delete).flatMap{ps =>
ps.execute(key).map{
case Completion.Delete(n) => n
case _ => throw KeyNotFound()
}
}
}

override def getAll() = pool.use{
_.prepare(operations.getAll).map{
_.stream(Void, 1024)
}
}

override def create(key: Key, value: Option[Value]): IO[Unit] = pool.use{
session =>
session.prepare(operations.create).flatMap{
_.execute(key, value.getOrElse(Value(0))).flatTap(IO.println).void
}
}

override def getKey(key:Key): IO[Option[Value]] = pool.use{
s => s.prepare(operations.getKey).flatMap(
_.option(key)
)
}

end new
end fromSessionPool
end Database
22 changes: 22 additions & 0 deletions modules/backend/src/main/scala/database.skunk.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package hellosmithy4s

import cats.*, cats.effect.*, cats.implicits.*
import skunk._
import skunk.implicits._
import skunk.codec.all._
import natchez.Trace.Implicits.noop
import cats.effect.IO

type SkunkSessionPool = Resource[IO, Session[IO]]

object SkunkDatabase:
def sessionPool(creds: PgCredentials) : Resource[IO, SkunkSessionPool] =
Session.pooled(
host = creds.host,
port = creds.port,
user = creds.user,
database = creds.database,
password = creds.password,
max = 32
)
end SkunkDatabase
Loading