From e08d3d5eff7c5338277b35b13bb842815cf40562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sat, 7 May 2022 20:13:05 +0200 Subject: [PATCH 01/24] common separate module --- .gitignore | 3 +- .scalafmt.conf | 6 ++- build.sbt | 38 ++++++++++--------- .../scala/taskforce/common/ErrorHandler.scala | 0 .../scala/taskforce/common/ErrorMessage.scala | 0 .../common/NewTypeDoobieMetaInstance.scala | 0 .../main/scala/taskforce/common/Sqlizer.scala | 0 .../main/scala/taskforce/common/errors.scala | 1 + .../main/scala/taskforce/common/package.scala | 0 project/Dependencies.scala | 1 + project/metals.sbt | 3 +- project/project/metals.sbt | 3 +- .../scala/taskforce/task/TaskService.scala | 4 +- 13 files changed, 32 insertions(+), 27 deletions(-) rename {src => common/src}/main/scala/taskforce/common/ErrorHandler.scala (100%) rename {src => common/src}/main/scala/taskforce/common/ErrorMessage.scala (100%) rename {src => common/src}/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala (100%) rename {src => common/src}/main/scala/taskforce/common/Sqlizer.scala (100%) rename {src => common/src}/main/scala/taskforce/common/errors.scala (99%) rename {src => common/src}/main/scala/taskforce/common/package.scala (100%) diff --git a/.gitignore b/.gitignore index f09078b..34e32a0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target/ project/target/ .bsp/sbt.json **/metals.sbt -*.worksheet.sc \ No newline at end of file +*.worksheet.sc +.idea \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index 0956a69..3267390 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,4 @@ -version = "3.4.0" -runner.dialect = scala213 \ No newline at end of file +version = 3.5.2 +runner.dialect = scala213 +align.preset = more +maxColumn = 120 \ No newline at end of file diff --git a/build.sbt b/build.sbt index 968f866..2e6fcf9 100644 --- a/build.sbt +++ b/build.sbt @@ -5,6 +5,7 @@ ThisBuild / githubWorkflowPublishTargetBranches := Seq() ThisBuild / organization := "com.pfl" ThisBuild / organizationName := "pfl" ThisBuild / scalaVersion := "2.13.8" +//ThisBuild / scalacOptions += "-P:semanticdb:synthetics:on" ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / versionScheme := Some("early-semver") @@ -26,32 +27,31 @@ lazy val root = (project in file(".")) publish / skip := true, Docker / packageName := "taskforce", dockerCommands := dockerCommands.value.flatMap { - case cmd @ Cmd("FROM", _) => - List(cmd, Cmd("RUN", "apk update && apk add bash")) - case other => List(other) + case cmd @ Cmd("FROM", _) => List(cmd, Cmd("RUN", "apk update && apk add bash")) + case other => List(other) }, dockerExposedPorts ++= Seq(9090), dockerBaseImage := "openjdk:8-jre-alpine", dockerUpdateLatest := true, - semanticdbEnabled := true, // enable SemanticDB + semanticdbEnabled := true, // enable SemanticDB semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x libraryDependencies ++= Seq( - catsEffect, + // catsEffect, circe, circeDerivation, circeExtras, circeFs2, circeParser, circeRefined, - doobie, + // doobie, doobieHikari, doobiePostgres, doobieRefined, doobieQuill, flyway, - http4sCirce, + // http4sCirce, http4sClient, - http4sDsl, + // http4sDsl, http4sServer, jwtCirce, logback, @@ -68,20 +68,24 @@ lazy val root = (project in file(".")) // quill, scalaCheckEffect, scalaCheckEffectMunit, - simulacrum, + // simulacrum, slf4j, log4cats ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), addCompilerPlugin(betterMonadicFor), scalacOptions ++= Seq( - "-deprecation", - "-encoding", - "UTF-8", - "-language:higherKinds", - "-language:postfixOps", + "-deprecation", + "-encoding", "UTF-8", + "-language:higherKinds", + "-language:postfixOps", "-feature", "-Xlint:unused", - "-Ymacro-annotations" - ) - ) + "-Ymacro-annotations" + ) + ).dependsOn(common) + +lazy val common = (project in file("taskforce/common")).settings( + libraryDependencies ++= Seq(cats,http4sDsl,circe,http4sCirce,doobie,simulacrum), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq( "-Ymacro-annotations" )) \ No newline at end of file diff --git a/src/main/scala/taskforce/common/ErrorHandler.scala b/common/src/main/scala/taskforce/common/ErrorHandler.scala similarity index 100% rename from src/main/scala/taskforce/common/ErrorHandler.scala rename to common/src/main/scala/taskforce/common/ErrorHandler.scala diff --git a/src/main/scala/taskforce/common/ErrorMessage.scala b/common/src/main/scala/taskforce/common/ErrorMessage.scala similarity index 100% rename from src/main/scala/taskforce/common/ErrorMessage.scala rename to common/src/main/scala/taskforce/common/ErrorMessage.scala diff --git a/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala b/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala similarity index 100% rename from src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala rename to common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala diff --git a/src/main/scala/taskforce/common/Sqlizer.scala b/common/src/main/scala/taskforce/common/Sqlizer.scala similarity index 100% rename from src/main/scala/taskforce/common/Sqlizer.scala rename to common/src/main/scala/taskforce/common/Sqlizer.scala diff --git a/src/main/scala/taskforce/common/errors.scala b/common/src/main/scala/taskforce/common/errors.scala similarity index 99% rename from src/main/scala/taskforce/common/errors.scala rename to common/src/main/scala/taskforce/common/errors.scala index 5329aeb..bdedb85 100644 --- a/src/main/scala/taskforce/common/errors.scala +++ b/common/src/main/scala/taskforce/common/errors.scala @@ -8,6 +8,7 @@ object errors { case class NotAuthor(userId: UUID) extends NoStackTrace case object BadRequest extends NoStackTrace case class NotFound(resourceId: String) extends NoStackTrace + case class InvalidQueryParam(s: String) extends NoStackTrace } diff --git a/src/main/scala/taskforce/common/package.scala b/common/src/main/scala/taskforce/common/package.scala similarity index 100% rename from src/main/scala/taskforce/common/package.scala rename to common/src/main/scala/taskforce/common/package.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 8a5cdbe..b18d29b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -32,6 +32,7 @@ object Dependencies { def refinedLib(artifact: String): ModuleID = "eu.timepit" %% artifact % V.refined def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v % "it,test" + val cats = "org.typelevel" %% "cats-core" val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEff val circe = circeLib("circe-generic") val circeDerivation = "io.circe" %% "circe-derivation" % V.circeDerivation diff --git a/project/metals.sbt b/project/metals.sbt index 183a0eb..49a03af 100644 --- a/project/metals.sbt +++ b/project/metals.sbt @@ -1,6 +1,5 @@ // DO NOT EDIT! This file is auto-generated. - // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.13") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") diff --git a/project/project/metals.sbt b/project/project/metals.sbt index 183a0eb..49a03af 100644 --- a/project/project/metals.sbt +++ b/project/project/metals.sbt @@ -1,6 +1,5 @@ // DO NOT EDIT! This file is auto-generated. - // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.13") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") diff --git a/src/main/scala/taskforce/task/TaskService.scala b/src/main/scala/taskforce/task/TaskService.scala index e09a1f3..541b088 100644 --- a/src/main/scala/taskforce/task/TaskService.scala +++ b/src/main/scala/taskforce/task/TaskService.scala @@ -77,9 +77,7 @@ final class TaskService[F[_]: Sync]( allUserTasksWithoutOld = allUserTasks.filterNot(_.id == oldTask.id) _ <- taskPeriodIsValid(task, allUserTasksWithoutOld) updatedTask <- taskRepo.update(oldTask.id, task) - } yield updatedTask.leftWiden[TaskError]).recover { case WrongPeriodError => - WrongPeriodError.asLeft[Task] - } + } yield updatedTask.leftWiden[TaskError]).recover { case WrongPeriodError => WrongPeriodError.asLeft[Task] } def delete(projectId: ProjectId, taskId: TaskId, caller: UserId) = for { From 448eae68a70430b5e651262a1bff2b0879a2db2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sat, 7 May 2022 20:28:08 +0200 Subject: [PATCH 02/24] authentication module --- .../TaskForceAuthMiddleware.scala | 0 .../scala/taskforce/authentication/User.scala | 0 .../authentication/UserRepository.scala | 0 .../authentication/instances/Circe.scala | 19 ++++++++++ .../taskforce/authentication/package.scala | 0 .../authentication/TestUserRepository.scala | 0 build.sbt | 36 +++++++++++-------- 7 files changed, 41 insertions(+), 14 deletions(-) rename {src => auth/src}/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala (100%) rename {src => auth/src}/main/scala/taskforce/authentication/User.scala (100%) rename {src => auth/src}/main/scala/taskforce/authentication/UserRepository.scala (100%) create mode 100644 auth/src/main/scala/taskforce/authentication/instances/Circe.scala rename {src => auth/src}/main/scala/taskforce/authentication/package.scala (100%) rename {src => auth/src}/test/scala/taskforce/authentication/TestUserRepository.scala (100%) diff --git a/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala b/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala similarity index 100% rename from src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala rename to auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala diff --git a/src/main/scala/taskforce/authentication/User.scala b/auth/src/main/scala/taskforce/authentication/User.scala similarity index 100% rename from src/main/scala/taskforce/authentication/User.scala rename to auth/src/main/scala/taskforce/authentication/User.scala diff --git a/src/main/scala/taskforce/authentication/UserRepository.scala b/auth/src/main/scala/taskforce/authentication/UserRepository.scala similarity index 100% rename from src/main/scala/taskforce/authentication/UserRepository.scala rename to auth/src/main/scala/taskforce/authentication/UserRepository.scala diff --git a/auth/src/main/scala/taskforce/authentication/instances/Circe.scala b/auth/src/main/scala/taskforce/authentication/instances/Circe.scala new file mode 100644 index 0000000..78f8efe --- /dev/null +++ b/auth/src/main/scala/taskforce/authentication/instances/Circe.scala @@ -0,0 +1,19 @@ +package taskforce.authentication.instances + +import io.circe.generic.semiauto._ +import io.circe.{Decoder, Encoder} +import java.util.UUID +import taskforce.authentication.{User, UserId} +trait Circe { + + implicit val userIdDecoder: Decoder[UserId] = + Decoder[UUID].map(UserId.apply) + implicit val userIdEncoder: Encoder[UserId] = + Encoder[UUID].contramap(_.value) + + implicit val userDecoder: Decoder[User] = + deriveDecoder[User] + implicit val userEncoder: Encoder[User] = + deriveEncoder[User] + +} diff --git a/src/main/scala/taskforce/authentication/package.scala b/auth/src/main/scala/taskforce/authentication/package.scala similarity index 100% rename from src/main/scala/taskforce/authentication/package.scala rename to auth/src/main/scala/taskforce/authentication/package.scala diff --git a/src/test/scala/taskforce/authentication/TestUserRepository.scala b/auth/src/test/scala/taskforce/authentication/TestUserRepository.scala similarity index 100% rename from src/test/scala/taskforce/authentication/TestUserRepository.scala rename to auth/src/test/scala/taskforce/authentication/TestUserRepository.scala diff --git a/build.sbt b/build.sbt index 2e6fcf9..5bfd87e 100644 --- a/build.sbt +++ b/build.sbt @@ -55,8 +55,6 @@ lazy val root = (project in file(".")) http4sServer, jwtCirce, logback, - monixNewType, - monixNewTypeCirce, mUnit, mUnitCE, mUnitScalacheck, @@ -64,8 +62,7 @@ lazy val root = (project in file(".")) pureConfigCE, pureConfigRefined, refined, - refinedCats, - // quill, + // quill, scalaCheckEffect, scalaCheckEffectMunit, // simulacrum, @@ -75,17 +72,28 @@ lazy val root = (project in file(".")) addCompilerPlugin(kindProjector), addCompilerPlugin(betterMonadicFor), scalacOptions ++= Seq( - "-deprecation", - "-encoding", "UTF-8", - "-language:higherKinds", - "-language:postfixOps", + "-deprecation", + "-encoding", + "UTF-8", + "-language:higherKinds", + "-language:postfixOps", "-feature", "-Xlint:unused", - "-Ymacro-annotations" - ) - ).dependsOn(common) + "-Ymacro-annotations" + ) + ) + .dependsOn(authentication) -lazy val common = (project in file("taskforce/common")).settings( - libraryDependencies ++= Seq(cats,http4sDsl,circe,http4sCirce,doobie,simulacrum), +lazy val common = (project in file("common")).settings( + libraryDependencies ++= Seq(cats, http4sDsl, circe, http4sCirce, doobie, simulacrum), addCompilerPlugin(kindProjector), - scalacOptions ++= Seq( "-Ymacro-annotations" )) \ No newline at end of file + scalacOptions ++= Seq("-Ymacro-annotations") +) + +lazy val authentication = (project in file("auth")) + .settings( + libraryDependencies ++= Seq(circeParser, http4sServer, jwtCirce,doobiePostgres), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) + .dependsOn(common) From c58aa23e6ca72048a9c3ccf121e70e7a35cc5fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 8 May 2022 13:44:14 +0200 Subject: [PATCH 03/24] some cleaning after rebase --- build.sbt | 66 +++++++++++++++++++++++--------------- project/Dependencies.scala | 4 +-- 2 files changed, 43 insertions(+), 27 deletions(-) diff --git a/build.sbt b/build.sbt index 5bfd87e..587ffac 100644 --- a/build.sbt +++ b/build.sbt @@ -2,11 +2,10 @@ import Dependencies.Libraries._ import com.typesafe.sbt.packager.docker.Cmd ThisBuild / githubWorkflowPublishTargetBranches := Seq() -ThisBuild / organization := "com.pfl" -ThisBuild / organizationName := "pfl" -ThisBuild / scalaVersion := "2.13.8" -//ThisBuild / scalacOptions += "-P:semanticdb:synthetics:on" -ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / organization := "com.pfl" +ThisBuild / organizationName := "pfl" +ThisBuild / scalaVersion := "2.13.8" +ThisBuild / version := "0.1.0-SNAPSHOT" ThisBuild / versionScheme := Some("early-semver") IntegrationTest / parallelExecution := false @@ -18,40 +17,40 @@ lazy val root = (project in file(".")) .enablePlugins(AshScriptPlugin) .configs(IntegrationTest.extend(Test)) .settings( - name := "taskforce", - flywayUrl := "jdbc:postgresql://localhost:54340/task", - flywayUser := "vder", + name := "taskforce", + flywayUrl := "jdbc:postgresql://localhost:54340/task", + flywayUser := "vder", flywayPassword := "password", Defaults.itSettings, - publish := {}, - publish / skip := true, + publish := {}, + publish / skip := true, Docker / packageName := "taskforce", dockerCommands := dockerCommands.value.flatMap { case cmd @ Cmd("FROM", _) => List(cmd, Cmd("RUN", "apk update && apk add bash")) case other => List(other) }, dockerExposedPorts ++= Seq(9090), - dockerBaseImage := "openjdk:8-jre-alpine", + dockerBaseImage := "openjdk:8-jre-alpine", dockerUpdateLatest := true, - semanticdbEnabled := true, // enable SemanticDB - semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x + semanticdbEnabled := true, // enable SemanticDB + semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x libraryDependencies ++= Seq( - // catsEffect, + // catsEffect, circe, circeDerivation, circeExtras, circeFs2, circeParser, circeRefined, - // doobie, + // doobie, doobieHikari, doobiePostgres, doobieRefined, doobieQuill, flyway, - // http4sCirce, + // http4sCirce, http4sClient, - // http4sDsl, + // http4sDsl, http4sServer, jwtCirce, logback, @@ -62,10 +61,11 @@ lazy val root = (project in file(".")) pureConfigCE, pureConfigRefined, refined, - // quill, + refinedCats, + // quill, scalaCheckEffect, scalaCheckEffectMunit, - // simulacrum, + // simulacrum, slf4j, log4cats ).map(_.exclude("org.slf4j", "*")), @@ -84,15 +84,31 @@ lazy val root = (project in file(".")) ) .dependsOn(authentication) -lazy val common = (project in file("common")).settings( - libraryDependencies ++= Seq(cats, http4sDsl, circe, http4sCirce, doobie, simulacrum), - addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") -) +lazy val common = (project in file("common")) + .settings( + libraryDependencies ++= Seq( + cats, + circe, + doobie, + doobieQuill, + http4sCirce, + http4sDsl, + monixNewType, + monixNewTypeCirce, + simulacrum + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) lazy val authentication = (project in file("auth")) .settings( - libraryDependencies ++= Seq(circeParser, http4sServer, jwtCirce,doobiePostgres), + libraryDependencies ++= Seq( + circeParser, + doobiePostgres, + http4sServer, + jwtCirce + ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), scalacOptions ++= Seq("-Ymacro-annotations") ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b18d29b..98b487c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,7 +4,7 @@ object Dependencies { object V { val Logback = "1.2.11" val betterMonadicFor = "0.3.1" - val cats = "2.6.0" + val cats = "2.7.0" val catsEff = "3.3.11" val circe = "0.14.0" val circeDerivation = "0.13.0-M5" @@ -32,7 +32,7 @@ object Dependencies { def refinedLib(artifact: String): ModuleID = "eu.timepit" %% artifact % V.refined def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v % "it,test" - val cats = "org.typelevel" %% "cats-core" + val cats = "org.typelevel" %% "cats-core" % V.cats val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEff val circe = circeLib("circe-generic") val circeDerivation = "io.circe" %% "circe-derivation" % V.circeDerivation From d514ffa4677d53f2ccb5d39a7968592b25971537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 8 May 2022 16:02:25 +0200 Subject: [PATCH 04/24] removing dependencies from task in projects feature & and some minor cleaning --- .../taskforce/project/ProjectRepository.scala | 65 ++++++++----------- .../taskforce/project/instances/Doobie.scala | 26 +++++++- .../taskforce/task/instances/Doobie.scala | 10 ++- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/main/scala/taskforce/project/ProjectRepository.scala b/src/main/scala/taskforce/project/ProjectRepository.scala index c7c0c87..3d904ee 100644 --- a/src/main/scala/taskforce/project/ProjectRepository.scala +++ b/src/main/scala/taskforce/project/ProjectRepository.scala @@ -4,22 +4,14 @@ import cats.effect.Sync import cats.effect.kernel.MonadCancel import cats.syntax.all._ import doobie.implicits._ -import org.polyvariant.doobiequill._ import doobie.util.transactor.Transactor -import eu.timepit.refined.api.Refined -import eu.timepit.refined.types.string -import java.time.Duration -import java.time.LocalDateTime +import io.getquill.{NamingStrategy, PluralizedTableNames, SnakeCase} +import org.polyvariant.doobiequill._ import org.postgresql.util.PSQLException import taskforce.authentication.UserId -import taskforce.task.Task -import taskforce.task.TaskDuration -import taskforce.task.instances.{Doobie => DoobieTaskInstances} -import io.getquill.NamingStrategy -import io.getquill.SnakeCase -import io.getquill.PluralizedTableNames -import java.time.temporal.ChronoUnit import taskforce.common._ +import java.time.{Duration, LocalDateTime} +import java.time.temporal.ChronoUnit trait ProjectRepository[F[_]] { def create(newProject: ProjectName, userId: UserId): F[Either[DuplicateProjectNameError, Project]] @@ -37,38 +29,23 @@ final class LiveProjectRepository[F[_]: MonadCancel[*[_], Throwable]]( xa: Transactor[F] ) extends ProjectRepository[F] with instances.Doobie - with DoobieTaskInstances with NewTypeQuillInstances { - val ctx = new DoobieContext.Postgres(NamingStrategy(PluralizedTableNames, SnakeCase)) + private val ctx = new DoobieContext.Postgres(NamingStrategy(PluralizedTableNames, SnakeCase)) import ctx._ - val projectQuery = quote { + private val projectQuery = quote { query[Project] } - - val taskQuery = quote { - querySchema[Task]("tasks", _.created -> "started") - } - - val newProjectId = ProjectId(0L) - - implicit val decodeNonEmptyString = MappedEncoding[String, string.NonEmptyString](Refined.unsafeApply(_)) - implicit val encodeNonEmptyString = MappedEncoding[string.NonEmptyString, String](_.value) - implicit val taskDurationNumeric = fakeNumeric[TaskDuration] - - def mapDatabaseErr(newProject: ProjectName): PartialFunction[Throwable, Either[DuplicateProjectNameError, Project]] = { - case x: PSQLException - if x.getMessage.contains( - "unique constraint" - ) => - DuplicateProjectNameError(newProject).asLeft[Project] + private val taskQuery = quote { + querySchema[TaskTime]("tasks") } + private val newProjectId = ProjectId(0L) override def totalTime(projectId: ProjectId): F[TotalTime] = - run(taskQuery.filter(t => t.projectId == lift(projectId) && t.deleted.isEmpty).map(_.duration).sum) + run(taskQuery.filter(t => t.projectId == lift(projectId) && t.deleted.isEmpty).map(_.time).sum) .transact(xa) - .map(t => TotalTime(t.getOrElse(TaskDuration(Duration.ZERO)).value)) + .map(t => t.getOrElse(TotalTime(Duration.ZERO))) override def find(id: ProjectId): F[Option[Project]] = run(query[Project].filter(_.id == lift(id))).transact(xa).map(_.headOption) @@ -84,11 +61,11 @@ final class LiveProjectRepository[F[_]: MonadCancel[*[_], Throwable]]( val created = CreationDate(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)) run( projectQuery - .insert(lift(Project(newProjectId, newProject, author, created, None))) + .insertValue(lift(Project(newProjectId, newProject, author, created, None))) .returningGenerated(_.id) ) .transact(xa) - .map { case id => + .map { id => Project(id, newProject, author, created, None) } .map(_.asRight[DuplicateProjectNameError]) @@ -100,7 +77,7 @@ final class LiveProjectRepository[F[_]: MonadCancel[*[_], Throwable]]( val result = for { x <- run(projectQuery.filter(p => p.id == lift(id) && p.deleted.isEmpty).update(_.deleted -> lift(deleted))) y <- run(taskQuery.filter(t => t.projectId == lift(id) && t.deleted.isEmpty).update(_.deleted -> lift(deleted))) - } yield (x + y) + } yield x + y result.transact(xa).map(_.toInt) } @@ -121,9 +98,21 @@ final class LiveProjectRepository[F[_]: MonadCancel[*[_], Throwable]]( } + private def mapDatabaseErr( + newProject: ProjectName + ): PartialFunction[Throwable, Either[DuplicateProjectNameError, Project]] = { + case x: PSQLException + if x.getMessage.contains( + "unique constraint" + ) => + DuplicateProjectNameError(newProject).asLeft[Project] + } + + case class TaskTime(projectId: ProjectId, time: TotalTime, deleted: Option[DeletionDate]) + } object LiveProjectRepository { - def make[F[_]: Sync](xa: Transactor[F]) = + def make[F[_]: Sync](xa: Transactor[F]): F[LiveProjectRepository[F]] = Sync[F].delay { new LiveProjectRepository[F](xa) } } diff --git a/src/main/scala/taskforce/project/instances/Doobie.scala b/src/main/scala/taskforce/project/instances/Doobie.scala index b4738aa..6618f5a 100644 --- a/src/main/scala/taskforce/project/instances/Doobie.scala +++ b/src/main/scala/taskforce/project/instances/Doobie.scala @@ -1,9 +1,33 @@ package taskforce.project.instances +import doobie.Meta +import eu.timepit.refined.api.Refined +import eu.timepit.refined.types.string +import io.getquill.MappedEncoding +import taskforce.project.TotalTime + +import java.time.Duration + trait Doobie { - def fakeNumeric[T] = new Numeric[T] { + implicit val totalTimeMeta: Meta[TotalTime] = + Meta[Long].imap(x => TotalTime(Duration.ofMinutes(x)))(x => + x.value.toMinutes + ) + + implicit val decodeTotalTime: MappedEncoding[Long, TotalTime] = + MappedEncoding[Long, TotalTime](long => + TotalTime(Duration.ofMinutes(long)) + ) + implicit val encodeTotalTime: MappedEncoding[TotalTime, Long] = + MappedEncoding[TotalTime, Long](_.value.toMinutes) + + implicit val decodeNonEmptyString: MappedEncoding[String, string.NonEmptyString] = MappedEncoding[String, string.NonEmptyString](Refined.unsafeApply) + implicit val encodeNonEmptyString: MappedEncoding[string.NonEmptyString, String] = MappedEncoding[string.NonEmptyString, String](_.value) + implicit val taskDurationNumeric: Numeric[TotalTime] = fakeNumeric[TotalTime] + + def fakeNumeric[T]: Numeric[T] = new Numeric[T] { override def compare(x: T, y: T): Int = ??? diff --git a/src/main/scala/taskforce/task/instances/Doobie.scala b/src/main/scala/taskforce/task/instances/Doobie.scala index 99a349d..9923d3b 100644 --- a/src/main/scala/taskforce/task/instances/Doobie.scala +++ b/src/main/scala/taskforce/task/instances/Doobie.scala @@ -6,20 +6,18 @@ import java.time.Duration import io.getquill.MappedEncoding import taskforce.common.NewTypeQuillInstances -trait Doobie - extends taskforce.project.instances.Doobie - with NewTypeQuillInstances { +trait Doobie extends NewTypeQuillInstances { implicit val taskDurationMeta: Meta[TaskDuration] = Meta[Long].imap(x => TaskDuration(Duration.ofMinutes(x)))(x => - x.value.toMinutes() + x.value.toMinutes ) - implicit val decodeTaskDuration = + implicit val decodeTaskDuration: MappedEncoding[Long, TaskDuration] = MappedEncoding[Long, TaskDuration](long => TaskDuration(Duration.ofMinutes(long)) ) - implicit val encodeTimeDuration = + implicit val encodeTimeDuration: MappedEncoding[TaskDuration, Long] = MappedEncoding[TaskDuration, Long](_.value.toMinutes) } From c5e30506e230f0f0c79f76db392b39052b7748cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 8 May 2022 20:36:15 +0200 Subject: [PATCH 05/24] project feature separation --- .gitignore | 6 ++- build.sbt | 43 ++++++++++++--- .../test/scala/taskforce/http4stest.scala | 2 +- project/Dependencies.scala | 4 +- .../scala/taskforce/project/Project.scala | 7 +-- .../taskforce/project/ProjectError.scala | 0 .../taskforce/project/ProjectRepository.scala | 0 .../taskforce/project/ProjectRoutes.scala | 2 +- .../taskforce/project/ProjectService.scala | 0 .../taskforce/project/instances/Doobie.scala | 0 .../taskforce/project/instances/Http4s.scala | 4 ++ .../scala/taskforce/project/package.scala | 3 +- .../project/ProjectRoutesSuite.scala | 6 ++- .../project/TestProjectRepository.scala | 0 .../scala/taskforce/project/arbitraries.scala | 16 ++++++ .../scala/taskforce/project/generators.scala | 54 +++++++++++++++++++ .../taskforce/filter/FilterRoutesSuite.scala | 4 +- .../taskforce/task/TaskRoutesSuite.scala | 2 +- 18 files changed, 131 insertions(+), 22 deletions(-) rename {src => common/src}/test/scala/taskforce/http4stest.scala (98%) rename {src => projectsFeature/src}/main/scala/taskforce/project/Project.scala (99%) rename {src => projectsFeature/src}/main/scala/taskforce/project/ProjectError.scala (100%) rename {src => projectsFeature/src}/main/scala/taskforce/project/ProjectRepository.scala (100%) rename {src => projectsFeature/src}/main/scala/taskforce/project/ProjectRoutes.scala (100%) rename {src => projectsFeature/src}/main/scala/taskforce/project/ProjectService.scala (100%) rename {src => projectsFeature/src}/main/scala/taskforce/project/instances/Doobie.scala (100%) rename {src => projectsFeature/src}/main/scala/taskforce/project/instances/Http4s.scala (99%) rename {src => projectsFeature/src}/main/scala/taskforce/project/package.scala (99%) rename {src => projectsFeature/src}/test/scala/taskforce/project/ProjectRoutesSuite.scala (98%) rename {src => projectsFeature/src}/test/scala/taskforce/project/TestProjectRepository.scala (100%) create mode 100644 projectsFeature/src/test/scala/taskforce/project/arbitraries.scala create mode 100644 projectsFeature/src/test/scala/taskforce/project/generators.scala diff --git a/.gitignore b/.gitignore index 34e32a0..8bff4de 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ target/ .history/ project/target/ .bsp/sbt.json -**/metals.sbt +metals.sbt *.worksheet.sc -.idea \ No newline at end of file +.idea +*/metals.sbt +**/metals.sbt \ No newline at end of file diff --git a/build.sbt b/build.sbt index 587ffac..4d887b5 100644 --- a/build.sbt +++ b/build.sbt @@ -41,12 +41,12 @@ lazy val root = (project in file(".")) circeExtras, circeFs2, circeParser, - circeRefined, + // circeRefined, // doobie, doobieHikari, doobiePostgres, doobieRefined, - doobieQuill, + // doobieQuill, flyway, // http4sCirce, http4sClient, @@ -54,7 +54,6 @@ lazy val root = (project in file(".")) http4sServer, jwtCirce, logback, - mUnit, mUnitCE, mUnitScalacheck, pureConfig, @@ -82,10 +81,12 @@ lazy val root = (project in file(".")) "-Ymacro-annotations" ) ) - .dependsOn(authentication) + .dependsOn(projects,common % "test->test") + .aggregate(projects) lazy val common = (project in file("common")) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( cats, circe, @@ -95,14 +96,22 @@ lazy val common = (project in file("common")) http4sDsl, monixNewType, monixNewTypeCirce, - simulacrum + mUnit, + mUnitCE, + mUnitScalacheck, + simulacrum, + scalaCheckEffect, + scalaCheckEffectMunit ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") - ) + ).configs(IntegrationTest.extend(Test)) lazy val authentication = (project in file("auth")) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( circeParser, doobiePostgres, @@ -111,5 +120,25 @@ lazy val authentication = (project in file("auth")) ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), scalacOptions ++= Seq("-Ymacro-annotations") - ) + ).configs(IntegrationTest.extend(Test)) .dependsOn(common) + +lazy val projects = (project in file("projectsFeature")) + .settings( + libraryDependencies ++= Seq( + circeDerivation, + circeExtras, + circeFs2, + circeParser, + circeRefined, + doobieHikari, + doobiePostgres, + doobieRefined, + http4sClient, + http4sCirce, + refined, + refinedCats).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) + .dependsOn(authentication % "compile->compile;test->test",common % "test->test") \ No newline at end of file diff --git a/src/test/scala/taskforce/http4stest.scala b/common/src/test/scala/taskforce/http4stest.scala similarity index 98% rename from src/test/scala/taskforce/http4stest.scala rename to common/src/test/scala/taskforce/http4stest.scala index 1df289c..67cf8fe 100644 --- a/src/test/scala/taskforce/http4stest.scala +++ b/common/src/test/scala/taskforce/http4stest.scala @@ -1,4 +1,4 @@ -package suite +package taskforce import cats.effect.IO import io.circe._ diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 98b487c..da178e5 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -28,9 +28,9 @@ object Dependencies { def circeLib(artifact: String): ModuleID = "io.circe" %% artifact % V.circe def doobieLib(artifact: String): ModuleID = "org.tpolecat" %% artifact % V.doobie def http4sLib(artifact: String): ModuleID = "org.http4s" %% artifact % V.http4s - def mUnitLib(artifact: String): ModuleID = "org.scalameta" %% artifact % V.munit % "it,test" + def mUnitLib(artifact: String): ModuleID = "org.scalameta" %% artifact % V.munit % Test def refinedLib(artifact: String): ModuleID = "eu.timepit" %% artifact % V.refined - def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v % "it,test" + def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v % Test val cats = "org.typelevel" %% "cats-core" % V.cats val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEff diff --git a/src/main/scala/taskforce/project/Project.scala b/projectsFeature/src/main/scala/taskforce/project/Project.scala similarity index 99% rename from src/main/scala/taskforce/project/Project.scala rename to projectsFeature/src/main/scala/taskforce/project/Project.scala index 92a2de0..0ea81c5 100644 --- a/src/main/scala/taskforce/project/Project.scala +++ b/projectsFeature/src/main/scala/taskforce/project/Project.scala @@ -1,10 +1,12 @@ package taskforce.project + import taskforce.authentication.UserId -import io.circe.generic.JsonCodec -import io.circe.refined._ + import taskforce.common.CreationDate import taskforce.common.DeletionDate +import io.circe.refined._ +import io.circe.generic.JsonCodec @@ -15,4 +17,3 @@ import taskforce.common.DeletionDate created: CreationDate, deleted: Option[DeletionDate] ) - diff --git a/src/main/scala/taskforce/project/ProjectError.scala b/projectsFeature/src/main/scala/taskforce/project/ProjectError.scala similarity index 100% rename from src/main/scala/taskforce/project/ProjectError.scala rename to projectsFeature/src/main/scala/taskforce/project/ProjectError.scala diff --git a/src/main/scala/taskforce/project/ProjectRepository.scala b/projectsFeature/src/main/scala/taskforce/project/ProjectRepository.scala similarity index 100% rename from src/main/scala/taskforce/project/ProjectRepository.scala rename to projectsFeature/src/main/scala/taskforce/project/ProjectRepository.scala diff --git a/src/main/scala/taskforce/project/ProjectRoutes.scala b/projectsFeature/src/main/scala/taskforce/project/ProjectRoutes.scala similarity index 100% rename from src/main/scala/taskforce/project/ProjectRoutes.scala rename to projectsFeature/src/main/scala/taskforce/project/ProjectRoutes.scala index a52e378..ca949e6 100644 --- a/src/main/scala/taskforce/project/ProjectRoutes.scala +++ b/projectsFeature/src/main/scala/taskforce/project/ProjectRoutes.scala @@ -2,13 +2,13 @@ package taskforce.project import cats.effect.Sync import cats.implicits._ -import io.circe.refined._ import org.http4s.{AuthedRequest, AuthedRoutes, Response} import org.http4s.circe._ import org.http4s.dsl.Http4sDsl import org.http4s.server.{AuthMiddleware, Router} import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, ErrorHandler, errors => commonErrors} +import io.circe.refined._ final class ProjectRoutes[F[_]: Sync: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], diff --git a/src/main/scala/taskforce/project/ProjectService.scala b/projectsFeature/src/main/scala/taskforce/project/ProjectService.scala similarity index 100% rename from src/main/scala/taskforce/project/ProjectService.scala rename to projectsFeature/src/main/scala/taskforce/project/ProjectService.scala diff --git a/src/main/scala/taskforce/project/instances/Doobie.scala b/projectsFeature/src/main/scala/taskforce/project/instances/Doobie.scala similarity index 100% rename from src/main/scala/taskforce/project/instances/Doobie.scala rename to projectsFeature/src/main/scala/taskforce/project/instances/Doobie.scala diff --git a/src/main/scala/taskforce/project/instances/Http4s.scala b/projectsFeature/src/main/scala/taskforce/project/instances/Http4s.scala similarity index 99% rename from src/main/scala/taskforce/project/instances/Http4s.scala rename to projectsFeature/src/main/scala/taskforce/project/instances/Http4s.scala index 0a484fc..a4e7999 100644 --- a/src/main/scala/taskforce/project/instances/Http4s.scala +++ b/projectsFeature/src/main/scala/taskforce/project/instances/Http4s.scala @@ -4,6 +4,10 @@ import org.http4s.EntityEncoder import org.http4s.circe._ import taskforce.project.{Project, TotalTime} + + + + trait Http4s[F[_]] { implicit val totalTimeEntityEncoder: EntityEncoder[F, TotalTime] = jsonEncoderOf[F, TotalTime] diff --git a/src/main/scala/taskforce/project/package.scala b/projectsFeature/src/main/scala/taskforce/project/package.scala similarity index 99% rename from src/main/scala/taskforce/project/package.scala rename to projectsFeature/src/main/scala/taskforce/project/package.scala index 7c32636..734234d 100644 --- a/src/main/scala/taskforce/project/package.scala +++ b/projectsFeature/src/main/scala/taskforce/project/package.scala @@ -2,10 +2,11 @@ package taskforce import monix.newtypes.integrations.DerivedCirceCodec import monix.newtypes.NewtypeWrapped -import eu.timepit.refined.types.string.NonEmptyString import java.time.Duration import taskforce.common.NewTypeDoobieMeta import taskforce.common.NewTypeQuillInstances +import eu.timepit.refined.types.string.NonEmptyString + diff --git a/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/projectsFeature/src/test/scala/taskforce/project/ProjectRoutesSuite.scala similarity index 98% rename from src/test/scala/taskforce/project/ProjectRoutesSuite.scala rename to projectsFeature/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index 7b80575..37475d4 100644 --- a/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/projectsFeature/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -1,5 +1,6 @@ package taskforce.project + import cats.data.Kleisli import cats.effect.IO import cats.implicits._ @@ -13,14 +14,15 @@ import org.http4s.client.dsl.io._ import org.http4s.implicits._ import org.http4s.server.AuthMiddleware import org.scalacheck.effect.PropF -import suite.HttpTestSuite -import taskforce.arbitraries._ +import taskforce.HttpTestSuite import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.project.ProjectName class ProjectRoutesSuite extends HttpTestSuite { + import arbitraries._ + implicit def decodeNewProduct: EntityDecoder[IO, ProjectName] = jsonOf implicit def encodeNewProduct: EntityEncoder[IO, ProjectName] = jsonEncoderOf diff --git a/src/test/scala/taskforce/project/TestProjectRepository.scala b/projectsFeature/src/test/scala/taskforce/project/TestProjectRepository.scala similarity index 100% rename from src/test/scala/taskforce/project/TestProjectRepository.scala rename to projectsFeature/src/test/scala/taskforce/project/TestProjectRepository.scala diff --git a/projectsFeature/src/test/scala/taskforce/project/arbitraries.scala b/projectsFeature/src/test/scala/taskforce/project/arbitraries.scala new file mode 100644 index 0000000..8c3b64c --- /dev/null +++ b/projectsFeature/src/test/scala/taskforce/project/arbitraries.scala @@ -0,0 +1,16 @@ +package taskforce.project + +import org.scalacheck.Arbitrary +import generators._ + + +object arbitraries { + + implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) + implicit def arbProjectIdGen = Arbitrary(projectIdGen) + implicit def arbUserIdGen = Arbitrary(userIdGen) + implicit def arbNewProjectGen = Arbitrary(newProjectGen) + implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) + implicit def arbProjectGen = Arbitrary(projectGen) + +} diff --git a/projectsFeature/src/test/scala/taskforce/project/generators.scala b/projectsFeature/src/test/scala/taskforce/project/generators.scala new file mode 100644 index 0000000..ed97f31 --- /dev/null +++ b/projectsFeature/src/test/scala/taskforce/project/generators.scala @@ -0,0 +1,54 @@ +package taskforce.project + +import eu.timepit.refined.api.Refined +import eu.timepit.refined.types.string.NonEmptyString +import java.time.format.DateTimeFormatter +import java.time.{LocalDate, LocalDateTime} +import org.scalacheck.Gen +import taskforce.authentication.UserId +import taskforce.common.CreationDate +import taskforce.common.DeletionDate +object generators { + + val nonEmptyStringGen: Gen[String] = + Gen + .chooseNum(21, 40) + .flatMap { n => + Gen.buildableOfN[String, Char](n, Gen.alphaChar) + } + + val projectIdGen: Gen[ProjectId] = + Gen.chooseNum[Long](0, 10000).map(ProjectId.apply) + + val userIdGen: Gen[UserId] = + Gen.uuid.map(UserId.apply) + + val newProjectGen: Gen[ProjectName] = + nonEmptyStringGen + .map[NonEmptyString](Refined.unsafeApply) + .map(ProjectName.apply) + + def creationDateTimeGen: Gen[CreationDate] = + localDateTimeGen.map(CreationDate.apply) + + def deletionDateTimeGen: Gen[DeletionDate] = + localDateTimeGen.map(DeletionDate.apply) + + def localDateTimeGen: Gen[LocalDateTime] = + for { + minutes <- Gen.chooseNum(0, 1000000000) + } yield LocalDate + .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) + .atStartOfDay() + .plusMinutes(minutes.toLong) + + val projectGen: Gen[Project] = + for { + projectId <- projectIdGen + name <- newProjectGen + userId <- userIdGen + created <- localDateTimeGen + } yield Project(projectId, name, userId, CreationDate(created), None) + +} + \ No newline at end of file diff --git a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala b/src/test/scala/taskforce/filter/FilterRoutesSuite.scala index d1de56c..e41787f 100644 --- a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala +++ b/src/test/scala/taskforce/filter/FilterRoutesSuite.scala @@ -12,7 +12,7 @@ import org.http4s.client.dsl.io._ import org.http4s.implicits._ import org.http4s.server.AuthMiddleware import org.scalacheck.effect.PropF -import suite.HttpTestSuite +import taskforce.HttpTestSuite import taskforce.arbitraries._ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} @@ -31,7 +31,7 @@ class FilterRoutesSuite extends HttpTestSuite with instances.Circe { implicit def unsafeLogger = Slf4jLogger.getLogger[IO] def authMiddleware: AuthMiddleware[IO, UserId] = - AuthMiddleware(Kleisli.pure(UserId(UUID.randomUUID()))) + AuthMiddleware(Kleisli.pure(UserId(UUID.randomUUID()))) def authMiddleware(userId: UserId): AuthMiddleware[IO, UserId] = AuthMiddleware(Kleisli.pure(userId)) diff --git a/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/src/test/scala/taskforce/task/TaskRoutesSuite.scala index 7a2c27a..7f2bcf9 100644 --- a/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -12,7 +12,7 @@ import org.http4s.client.dsl.io._ import org.http4s.implicits._ import org.http4s.server.AuthMiddleware import org.scalacheck.effect.PropF -import suite.HttpTestSuite +import taskforce.HttpTestSuite import taskforce.arbitraries._ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} From d1632036a7b478b8b91b0b6db227461541d323a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 8 May 2022 21:35:36 +0200 Subject: [PATCH 06/24] separation of the task features --- build.sbt | 62 +++++++++---- project/metals.sbt | 1 + project/project/metals.sbt | 1 + .../taskforce/task/TaskRepositorySuite.scala | 4 +- src/test/scala/taskforce/arbitraries.scala | 6 -- .../taskforce/filter/FilterRoutesSuite.scala | 2 +- src/test/scala/taskforce/generators.scala | 64 +------------ .../src}/main/scala/taskforce/task/Task.scala | 1 - .../main/scala/taskforce/task/TaskError.scala | 0 .../scala/taskforce/task/TaskRepository.scala | 1 - .../scala/taskforce/task/TaskRoutes.scala | 1 - .../scala/taskforce/task/TaskService.scala | 1 - .../taskforce/task/instances/Doobie.scala | 0 .../main/scala/taskforce/task/package.scala | 8 ++ .../taskforce/task/TaskRoutesSuite.scala | 3 +- .../taskforce/task/TestTaskRepository.scala | 1 - .../scala/taskforce/task/arbitraries.scala | 17 ++++ .../scala/taskforce/task/generators.scala | 89 +++++++++++++++++++ 18 files changed, 170 insertions(+), 92 deletions(-) rename {src => tasksFeature/src}/main/scala/taskforce/task/Task.scala (96%) rename {src => tasksFeature/src}/main/scala/taskforce/task/TaskError.scala (100%) rename {src => tasksFeature/src}/main/scala/taskforce/task/TaskRepository.scala (99%) rename {src => tasksFeature/src}/main/scala/taskforce/task/TaskRoutes.scala (98%) rename {src => tasksFeature/src}/main/scala/taskforce/task/TaskService.scala (98%) rename {src => tasksFeature/src}/main/scala/taskforce/task/instances/Doobie.scala (100%) rename {src => tasksFeature/src}/main/scala/taskforce/task/package.scala (88%) rename {src => tasksFeature/src}/test/scala/taskforce/task/TaskRoutesSuite.scala (98%) rename {src => tasksFeature/src}/test/scala/taskforce/task/TestTaskRepository.scala (95%) create mode 100644 tasksFeature/src/test/scala/taskforce/task/arbitraries.scala create mode 100644 tasksFeature/src/test/scala/taskforce/task/generators.scala diff --git a/build.sbt b/build.sbt index 4d887b5..3d64006 100644 --- a/build.sbt +++ b/build.sbt @@ -5,8 +5,7 @@ ThisBuild / githubWorkflowPublishTargetBranches := Seq() ThisBuild / organization := "com.pfl" ThisBuild / organizationName := "pfl" ThisBuild / scalaVersion := "2.13.8" -ThisBuild / version := "0.1.0-SNAPSHOT" -ThisBuild / versionScheme := Some("early-semver") +ThisBuild / version := "0.1.0-SNAPSHOT" IntegrationTest / parallelExecution := false @@ -41,12 +40,12 @@ lazy val root = (project in file(".")) circeExtras, circeFs2, circeParser, - // circeRefined, + // circeRefined, // doobie, doobieHikari, doobiePostgres, doobieRefined, - // doobieQuill, + // doobieQuill, flyway, // http4sCirce, http4sClient, @@ -81,12 +80,18 @@ lazy val root = (project in file(".")) "-Ymacro-annotations" ) ) - .dependsOn(projects,common % "test->test") - .aggregate(projects) + .dependsOn( + common % "test->test", + projects % "compile->compile;test->test", + tasks % "compile->compile;test->test" + ) + .aggregate( + projects, + tasks + ) lazy val common = (project in file("common")) .settings( - Defaults.itSettings, libraryDependencies ++= Seq( cats, circe, @@ -103,15 +108,12 @@ lazy val common = (project in file("common")) scalaCheckEffect, scalaCheckEffectMunit ).map(_.exclude("org.slf4j", "*")), - addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") - ).configs(IntegrationTest.extend(Test)) + ) lazy val authentication = (project in file("auth")) .settings( - Defaults.itSettings, libraryDependencies ++= Seq( circeParser, doobiePostgres, @@ -120,7 +122,7 @@ lazy val authentication = (project in file("auth")) ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), scalacOptions ++= Seq("-Ymacro-annotations") - ).configs(IntegrationTest.extend(Test)) + ) .dependsOn(common) lazy val projects = (project in file("projectsFeature")) @@ -137,8 +139,36 @@ lazy val projects = (project in file("projectsFeature")) http4sClient, http4sCirce, refined, - refinedCats).map(_.exclude("org.slf4j", "*")), - addCompilerPlugin(kindProjector), + refinedCats + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), scalacOptions ++= Seq("-Ymacro-annotations") - ) - .dependsOn(authentication % "compile->compile;test->test",common % "test->test") \ No newline at end of file + ) + .dependsOn( + authentication % "compile->compile;test->test", + common % "test->test" + ) + +lazy val tasks = (project in file("tasksFeature")) + .settings( + libraryDependencies ++= Seq( + circeDerivation, + circeExtras, + circeFs2, + circeParser, + circeRefined, + doobieHikari, + doobiePostgres, + doobieRefined, + http4sClient, + http4sCirce, + refined, + refinedCats + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) + .dependsOn( + authentication % "compile->compile;test->test", + common % "test->test" + ) diff --git a/project/metals.sbt b/project/metals.sbt index 49a03af..a13ea3b 100644 --- a/project/metals.sbt +++ b/project/metals.sbt @@ -1,4 +1,5 @@ // DO NOT EDIT! This file is auto-generated. + // This file enables sbt-bloop to create bloop config files. addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") diff --git a/project/project/metals.sbt b/project/project/metals.sbt index 49a03af..a13ea3b 100644 --- a/project/project/metals.sbt +++ b/project/project/metals.sbt @@ -1,4 +1,5 @@ // DO NOT EDIT! This file is auto-generated. + // This file enables sbt-bloop to create bloop config files. addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") diff --git a/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/src/it/scala/taskforce/task/TaskRepositorySuite.scala index 1f6ddbf..16e989a 100644 --- a/src/it/scala/taskforce/task/TaskRepositorySuite.scala +++ b/src/it/scala/taskforce/task/TaskRepositorySuite.scala @@ -2,8 +2,8 @@ package taskforce.task import cats.effect.IO import org.scalacheck.effect.PropF -import taskforce.arbitraries._ -import taskforce.project.ProjectId +import taskforce.task.arbitraries._ +import taskforce.task.ProjectId import taskforce.BasicRepositorySuite class TaskRepositorySuite extends BasicRepositorySuite { diff --git a/src/test/scala/taskforce/arbitraries.scala b/src/test/scala/taskforce/arbitraries.scala index 9833c67..1467710 100644 --- a/src/test/scala/taskforce/arbitraries.scala +++ b/src/test/scala/taskforce/arbitraries.scala @@ -6,15 +6,9 @@ import taskforce.generators._ object arbitraries { implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) - implicit def arbProjectIdGen = Arbitrary(projectIdGen) implicit def arbUserIdGen = Arbitrary(userIdGen) - implicit def arbTaskIdGen = Arbitrary(taskIdGen) - implicit def arbTaskDurationGen = Arbitrary(taskDurationGen) implicit def arbNewProjectGen = Arbitrary(newProjectGen) implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) - implicit def arbProjectGen = Arbitrary(projectGen) - implicit def arbTaskGen = Arbitrary(taskGen) - implicit def arbNewTaskGen = Arbitrary(newTaskGen) implicit def arbOperatorGen = Arbitrary(operatorGen) implicit def arbStatusGen = Arbitrary(statusGen) implicit def arbInGen = Arbitrary(inGen) diff --git a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala b/src/test/scala/taskforce/filter/FilterRoutesSuite.scala index e41787f..128a9d0 100644 --- a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala +++ b/src/test/scala/taskforce/filter/FilterRoutesSuite.scala @@ -31,7 +31,7 @@ class FilterRoutesSuite extends HttpTestSuite with instances.Circe { implicit def unsafeLogger = Slf4jLogger.getLogger[IO] def authMiddleware: AuthMiddleware[IO, UserId] = - AuthMiddleware(Kleisli.pure(UserId(UUID.randomUUID()))) + AuthMiddleware(Kleisli.pure(UserId(UUID.randomUUID()))) def authMiddleware(userId: UserId): AuthMiddleware[IO, UserId] = AuthMiddleware(Kleisli.pure(userId)) diff --git a/src/test/scala/taskforce/generators.scala b/src/test/scala/taskforce/generators.scala index dccedfb..2e2aba2 100644 --- a/src/test/scala/taskforce/generators.scala +++ b/src/test/scala/taskforce/generators.scala @@ -1,18 +1,17 @@ package taskforce -import cats.syntax.option._ -import eu.timepit.refined._ -import eu.timepit.refined.collection._ + import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Positive import eu.timepit.refined.types.string.NonEmptyString import java.time.format.DateTimeFormatter -import java.time.{Duration, LocalDate, LocalDateTime} +import java.time.{LocalDate, LocalDateTime} import org.scalacheck.Gen import taskforce.authentication.UserId import taskforce.filter._ import taskforce.project._ -import taskforce.task._ +import taskforce.task.generators._ +import taskforce.project.generators._ import taskforce.common.CreationDate import taskforce.common.DeletionDate object generators { @@ -24,18 +23,10 @@ object generators { Gen.buildableOfN[String, Char](n, Gen.alphaChar) } - val projectIdGen: Gen[ProjectId] = - Gen.chooseNum[Long](0, 10000).map(ProjectId.apply) val userIdGen: Gen[UserId] = Gen.uuid.map(UserId.apply) - val taskIdGen: Gen[TaskId] = - Gen.uuid.map(TaskId.apply) - - val taskDurationGen: Gen[TaskDuration] = - Gen.chooseNum[Long](10, 1000).map(x => TaskDuration(Duration.ofMinutes(x))) - val newProjectGen: Gen[ProjectName] = nonEmptyStringGen .map[NonEmptyString](Refined.unsafeApply) @@ -55,53 +46,6 @@ object generators { .atStartOfDay() .plusMinutes(minutes.toLong) - val projectGen: Gen[Project] = - for { - projectId <- projectIdGen - name <- newProjectGen - userId <- userIdGen - created <- localDateTimeGen - } yield Project(projectId, name, userId, CreationDate(created), None) - - val taskVolumeGen: Gen[TaskVolume] = - Gen - .posNum[Int] - .map(Refined.unsafeApply[Int, Positive]) - .map(TaskVolume.apply) - - val taskCommentGen: Gen[Option[TaskComment]] = - Gen.alphaStr - .map(refineV[NonEmpty](_).toOption) - .map(_.map(TaskComment.apply)) - - val taskGen: Gen[Task] = - for { - id <- taskIdGen - projectId <- projectIdGen - author <- userIdGen - created <- localDateTimeGen - duration <- taskDurationGen - volume <- taskVolumeGen - comment <- taskCommentGen - } yield Task( - id, - projectId, - author, - CreationDate(created), - duration, - volume.some, - None, - comment - ) - - val newTaskGen: Gen[NewTask] = - for { - created <- creationDateTimeGen - duration <- taskDurationGen - volume <- taskVolumeGen - comment <- taskCommentGen - } yield NewTask(created.some, duration, volume.some, comment) - val operatorGen: Gen[Operator] = Gen.oneOf(List(Eq, Gt, Gteq, Lteq, Lt)) val statusGen: Gen[Status] = Gen.oneOf(List(Active, Deactive, All)) diff --git a/src/main/scala/taskforce/task/Task.scala b/tasksFeature/src/main/scala/taskforce/task/Task.scala similarity index 96% rename from src/main/scala/taskforce/task/Task.scala rename to tasksFeature/src/main/scala/taskforce/task/Task.scala index 2f8ea48..e5b390f 100644 --- a/src/main/scala/taskforce/task/Task.scala +++ b/tasksFeature/src/main/scala/taskforce/task/Task.scala @@ -2,7 +2,6 @@ package taskforce.task import java.time.LocalDateTime import java.util.UUID -import taskforce.project.ProjectId import taskforce.authentication.UserId import taskforce.common._ import io.circe.refined._ diff --git a/src/main/scala/taskforce/task/TaskError.scala b/tasksFeature/src/main/scala/taskforce/task/TaskError.scala similarity index 100% rename from src/main/scala/taskforce/task/TaskError.scala rename to tasksFeature/src/main/scala/taskforce/task/TaskError.scala diff --git a/src/main/scala/taskforce/task/TaskRepository.scala b/tasksFeature/src/main/scala/taskforce/task/TaskRepository.scala similarity index 99% rename from src/main/scala/taskforce/task/TaskRepository.scala rename to tasksFeature/src/main/scala/taskforce/task/TaskRepository.scala index d160fb2..eb1b327 100644 --- a/src/main/scala/taskforce/task/TaskRepository.scala +++ b/tasksFeature/src/main/scala/taskforce/task/TaskRepository.scala @@ -8,7 +8,6 @@ import org.polyvariant.doobiequill.DoobieContext import doobie.util.transactor.Transactor import org.postgresql.util.PSQLException import taskforce.authentication.UserId -import taskforce.project.ProjectId import fs2.Stream import eu.timepit.refined.numeric import eu.timepit.refined.api.Refined diff --git a/src/main/scala/taskforce/task/TaskRoutes.scala b/tasksFeature/src/main/scala/taskforce/task/TaskRoutes.scala similarity index 98% rename from src/main/scala/taskforce/task/TaskRoutes.scala rename to tasksFeature/src/main/scala/taskforce/task/TaskRoutes.scala index 5933137..267a864 100644 --- a/src/main/scala/taskforce/task/TaskRoutes.scala +++ b/tasksFeature/src/main/scala/taskforce/task/TaskRoutes.scala @@ -10,7 +10,6 @@ import org.http4s.server.{AuthMiddleware, Router} import org.http4s.{AuthedRequest, AuthedRoutes} import taskforce.common.{ErrorMessage, ErrorHandler, errors => commonErrors} import taskforce.authentication.UserId -import taskforce.project.ProjectId import org.http4s.Response final class TaskRoutes[F[_]: Sync: JsonDecoder]( diff --git a/src/main/scala/taskforce/task/TaskService.scala b/tasksFeature/src/main/scala/taskforce/task/TaskService.scala similarity index 98% rename from src/main/scala/taskforce/task/TaskService.scala rename to tasksFeature/src/main/scala/taskforce/task/TaskService.scala index 541b088..82abe3d 100644 --- a/src/main/scala/taskforce/task/TaskService.scala +++ b/tasksFeature/src/main/scala/taskforce/task/TaskService.scala @@ -4,7 +4,6 @@ import cats.effect.Sync import cats.implicits._ import taskforce.common.{errors => commonErrors} import taskforce.authentication.UserId -import taskforce.project.ProjectId import java.time.LocalDateTime final class TaskService[F[_]: Sync]( diff --git a/src/main/scala/taskforce/task/instances/Doobie.scala b/tasksFeature/src/main/scala/taskforce/task/instances/Doobie.scala similarity index 100% rename from src/main/scala/taskforce/task/instances/Doobie.scala rename to tasksFeature/src/main/scala/taskforce/task/instances/Doobie.scala diff --git a/src/main/scala/taskforce/task/package.scala b/tasksFeature/src/main/scala/taskforce/task/package.scala similarity index 88% rename from src/main/scala/taskforce/task/package.scala rename to tasksFeature/src/main/scala/taskforce/task/package.scala index 83501af..bb6e631 100644 --- a/src/main/scala/taskforce/task/package.scala +++ b/tasksFeature/src/main/scala/taskforce/task/package.scala @@ -11,6 +11,14 @@ import io.circe.{Encoder => JsonEncoder,Decoder => JsonDecoder} import eu.timepit.refined.types.string.NonEmptyString package object task { + + type ProjectId = ProjectId.Type + object ProjectId + extends NewtypeWrapped[Long] + with DerivedCirceCodec + with NewTypeDoobieMeta + with NewTypeQuillInstances + type TaskId = TaskId.Type object TaskId extends NewtypeWrapped[UUID] diff --git a/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/tasksFeature/src/test/scala/taskforce/task/TaskRoutesSuite.scala similarity index 98% rename from src/test/scala/taskforce/task/TaskRoutesSuite.scala rename to tasksFeature/src/test/scala/taskforce/task/TaskRoutesSuite.scala index 7f2bcf9..a023e77 100644 --- a/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/tasksFeature/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -13,10 +13,9 @@ import org.http4s.implicits._ import org.http4s.server.AuthMiddleware import org.scalacheck.effect.PropF import taskforce.HttpTestSuite -import taskforce.arbitraries._ +import arbitraries._ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} -import taskforce.project.ProjectId import taskforce.common.CreationDate class TasksRoutesSuite extends HttpTestSuite { diff --git a/src/test/scala/taskforce/task/TestTaskRepository.scala b/tasksFeature/src/test/scala/taskforce/task/TestTaskRepository.scala similarity index 95% rename from src/test/scala/taskforce/task/TestTaskRepository.scala rename to tasksFeature/src/test/scala/taskforce/task/TestTaskRepository.scala index 3bcb294..c4a8bbc 100644 --- a/src/test/scala/taskforce/task/TestTaskRepository.scala +++ b/tasksFeature/src/test/scala/taskforce/task/TestTaskRepository.scala @@ -4,7 +4,6 @@ import cats.effect.IO import cats.implicits._ import fs2.Stream import taskforce.authentication.UserId -import taskforce.project.ProjectId final case class TestTaskRepository(tasks: List[Task]) extends TaskRepository[IO] { override def create(task: Task) = task.asRight[DuplicateTaskNameError].pure[IO] diff --git a/tasksFeature/src/test/scala/taskforce/task/arbitraries.scala b/tasksFeature/src/test/scala/taskforce/task/arbitraries.scala new file mode 100644 index 0000000..5bdb1fb --- /dev/null +++ b/tasksFeature/src/test/scala/taskforce/task/arbitraries.scala @@ -0,0 +1,17 @@ +package taskforce.task + +import org.scalacheck.Arbitrary +import generators._ + +object arbitraries { + + implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) + implicit def arbProjectIdGen = Arbitrary(projectIdGen) + implicit def arbUserIdGen = Arbitrary(userIdGen) + implicit def arbTaskIdGen = Arbitrary(taskIdGen) + implicit def arbTaskDurationGen = Arbitrary(taskDurationGen) + implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) + implicit def arbTaskGen = Arbitrary(taskGen) + implicit def arbNewTaskGen = Arbitrary(newTaskGen) + +} diff --git a/tasksFeature/src/test/scala/taskforce/task/generators.scala b/tasksFeature/src/test/scala/taskforce/task/generators.scala new file mode 100644 index 0000000..b78d045 --- /dev/null +++ b/tasksFeature/src/test/scala/taskforce/task/generators.scala @@ -0,0 +1,89 @@ +package taskforce.task + +import cats.syntax.option._ +import eu.timepit.refined._ +import eu.timepit.refined.collection._ +import eu.timepit.refined.api.Refined +import eu.timepit.refined.numeric.Positive +import java.time.format.DateTimeFormatter +import java.time.{Duration, LocalDate, LocalDateTime} +import org.scalacheck.Gen +import taskforce.authentication.UserId +import taskforce.common.CreationDate +import taskforce.common.DeletionDate + +object generators { + + val nonEmptyStringGen: Gen[String] = + Gen + .chooseNum(21, 40) + .flatMap { n => + Gen.buildableOfN[String, Char](n, Gen.alphaChar) + } + + val projectIdGen: Gen[ProjectId] = + Gen.chooseNum[Long](0, 10000).map(ProjectId.apply) + + val userIdGen: Gen[UserId] = + Gen.uuid.map(UserId.apply) + + val taskIdGen: Gen[TaskId] = + Gen.uuid.map(TaskId.apply) + + val taskDurationGen: Gen[TaskDuration] = + Gen.chooseNum[Long](10, 1000).map(x => TaskDuration(Duration.ofMinutes(x))) + + def creationDateTimeGen: Gen[CreationDate] = + localDateTimeGen.map(CreationDate.apply) + + def deletionDateTimeGen: Gen[DeletionDate] = + localDateTimeGen.map(DeletionDate.apply) + + def localDateTimeGen: Gen[LocalDateTime] = + for { + minutes <- Gen.chooseNum(0, 1000000000) + } yield LocalDate + .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) + .atStartOfDay() + .plusMinutes(minutes.toLong) + + val taskVolumeGen: Gen[TaskVolume] = + Gen + .posNum[Int] + .map(Refined.unsafeApply[Int, Positive]) + .map(TaskVolume.apply) + + val taskCommentGen: Gen[Option[TaskComment]] = + Gen.alphaStr + .map(refineV[NonEmpty](_).toOption) + .map(_.map(TaskComment.apply)) + + val taskGen: Gen[Task] = + for { + id <- taskIdGen + projectId <- projectIdGen + author <- userIdGen + created <- localDateTimeGen + duration <- taskDurationGen + volume <- taskVolumeGen + comment <- taskCommentGen + } yield Task( + id, + projectId, + author, + CreationDate(created), + duration, + volume.some, + None, + comment + ) + + val newTaskGen: Gen[NewTask] = + for { + created <- creationDateTimeGen + duration <- taskDurationGen + volume <- taskVolumeGen + comment <- taskCommentGen + } yield NewTask(created.some, duration, volume.some, comment) + + } \ No newline at end of file From 27dbc40fd088078156363425c23190b626075760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 8 May 2022 22:25:00 +0200 Subject: [PATCH 07/24] filters move to seperate module --- build.sbt | 48 +++++++------------ .../main/scala/taskforce/filter/Filter.scala | 0 .../taskforce/filter/FilterRepository.scala | 0 .../scala/taskforce/filter/FilterRoutes.scala | 0 .../taskforce/filter/FilterService.scala | 0 .../scala/taskforce/filter/QueryParams.scala | 0 .../taskforce/filter/instances/Circe.scala | 0 .../taskforce/filter/instances/Doobie.scala | 0 .../taskforce/filter/instances/Http4s.scala | 0 .../taskforce/filter/instances/helpers.scala | 0 .../taskforce/filter/FilterRoutesSuite.scala | 8 ++-- .../filter/TestFilterRepository.scala | 0 .../scala/taskforce/filter}/arbitraries.scala | 7 +-- .../scala/taskforce/filter}/generators.scala | 25 ++-------- .../filter/FilterRepositorySuite.scala | 2 +- .../project/ProjectRepositorySuite.scala | 2 +- 16 files changed, 29 insertions(+), 63 deletions(-) rename {src => filtersFeature/src}/main/scala/taskforce/filter/Filter.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/FilterRepository.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/FilterRoutes.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/FilterService.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/QueryParams.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/instances/Circe.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/instances/Doobie.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/instances/Http4s.scala (100%) rename {src => filtersFeature/src}/main/scala/taskforce/filter/instances/helpers.scala (100%) rename {src => filtersFeature/src}/test/scala/taskforce/filter/FilterRoutesSuite.scala (99%) rename {src => filtersFeature/src}/test/scala/taskforce/filter/TestFilterRepository.scala (100%) rename {src/test/scala/taskforce => filtersFeature/src/test/scala/taskforce/filter}/arbitraries.scala (79%) rename {src/test/scala/taskforce => filtersFeature/src/test/scala/taskforce/filter}/generators.scala (78%) diff --git a/build.sbt b/build.sbt index 3d64006..f15b9a8 100644 --- a/build.sbt +++ b/build.sbt @@ -34,38 +34,12 @@ lazy val root = (project in file(".")) semanticdbEnabled := true, // enable SemanticDB semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x libraryDependencies ++= Seq( - // catsEffect, - circe, - circeDerivation, - circeExtras, - circeFs2, - circeParser, - // circeRefined, - // doobie, - doobieHikari, - doobiePostgres, - doobieRefined, - // doobieQuill, flyway, - // http4sCirce, - http4sClient, - // http4sDsl, - http4sServer, - jwtCirce, logback, - mUnitCE, - mUnitScalacheck, pureConfig, pureConfigCE, pureConfigRefined, - refined, - refinedCats, - // quill, - scalaCheckEffect, - scalaCheckEffectMunit, - // simulacrum, - slf4j, - log4cats + ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), addCompilerPlugin(betterMonadicFor), @@ -81,11 +55,11 @@ lazy val root = (project in file(".")) ) ) .dependsOn( - common % "test->test", - projects % "compile->compile;test->test", - tasks % "compile->compile;test->test" + common % "test->test", + filters % "compile->compile;test->test" ) .aggregate( + filters, projects, tasks ) @@ -172,3 +146,17 @@ lazy val tasks = (project in file("tasksFeature")) authentication % "compile->compile;test->test", common % "test->test" ) + +lazy val filters = (project in file("filtersFeature")) + .settings( + libraryDependencies ++= Seq( + log4cats, + slf4j + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) + .dependsOn( + tasks % "compile->compile;test->test", + projects % "compile->compile;test->test" + ) diff --git a/src/main/scala/taskforce/filter/Filter.scala b/filtersFeature/src/main/scala/taskforce/filter/Filter.scala similarity index 100% rename from src/main/scala/taskforce/filter/Filter.scala rename to filtersFeature/src/main/scala/taskforce/filter/Filter.scala diff --git a/src/main/scala/taskforce/filter/FilterRepository.scala b/filtersFeature/src/main/scala/taskforce/filter/FilterRepository.scala similarity index 100% rename from src/main/scala/taskforce/filter/FilterRepository.scala rename to filtersFeature/src/main/scala/taskforce/filter/FilterRepository.scala diff --git a/src/main/scala/taskforce/filter/FilterRoutes.scala b/filtersFeature/src/main/scala/taskforce/filter/FilterRoutes.scala similarity index 100% rename from src/main/scala/taskforce/filter/FilterRoutes.scala rename to filtersFeature/src/main/scala/taskforce/filter/FilterRoutes.scala diff --git a/src/main/scala/taskforce/filter/FilterService.scala b/filtersFeature/src/main/scala/taskforce/filter/FilterService.scala similarity index 100% rename from src/main/scala/taskforce/filter/FilterService.scala rename to filtersFeature/src/main/scala/taskforce/filter/FilterService.scala diff --git a/src/main/scala/taskforce/filter/QueryParams.scala b/filtersFeature/src/main/scala/taskforce/filter/QueryParams.scala similarity index 100% rename from src/main/scala/taskforce/filter/QueryParams.scala rename to filtersFeature/src/main/scala/taskforce/filter/QueryParams.scala diff --git a/src/main/scala/taskforce/filter/instances/Circe.scala b/filtersFeature/src/main/scala/taskforce/filter/instances/Circe.scala similarity index 100% rename from src/main/scala/taskforce/filter/instances/Circe.scala rename to filtersFeature/src/main/scala/taskforce/filter/instances/Circe.scala diff --git a/src/main/scala/taskforce/filter/instances/Doobie.scala b/filtersFeature/src/main/scala/taskforce/filter/instances/Doobie.scala similarity index 100% rename from src/main/scala/taskforce/filter/instances/Doobie.scala rename to filtersFeature/src/main/scala/taskforce/filter/instances/Doobie.scala diff --git a/src/main/scala/taskforce/filter/instances/Http4s.scala b/filtersFeature/src/main/scala/taskforce/filter/instances/Http4s.scala similarity index 100% rename from src/main/scala/taskforce/filter/instances/Http4s.scala rename to filtersFeature/src/main/scala/taskforce/filter/instances/Http4s.scala diff --git a/src/main/scala/taskforce/filter/instances/helpers.scala b/filtersFeature/src/main/scala/taskforce/filter/instances/helpers.scala similarity index 100% rename from src/main/scala/taskforce/filter/instances/helpers.scala rename to filtersFeature/src/main/scala/taskforce/filter/instances/helpers.scala diff --git a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala b/filtersFeature/src/test/scala/taskforce/filter/FilterRoutesSuite.scala similarity index 99% rename from src/test/scala/taskforce/filter/FilterRoutesSuite.scala rename to filtersFeature/src/test/scala/taskforce/filter/FilterRoutesSuite.scala index 128a9d0..91499d3 100644 --- a/src/test/scala/taskforce/filter/FilterRoutesSuite.scala +++ b/filtersFeature/src/test/scala/taskforce/filter/FilterRoutesSuite.scala @@ -1,23 +1,23 @@ package taskforce.filter +import arbitraries._ import cats.data.Kleisli import cats.effect.IO import cats.implicits._ import fs2.Stream import java.util.UUID -import org.http4s.Method._ import org.http4s._ import org.http4s.circe._ import org.http4s.client.dsl.io._ import org.http4s.implicits._ +import org.http4s.Method._ import org.http4s.server.AuthMiddleware import org.scalacheck.effect.PropF -import taskforce.HttpTestSuite -import taskforce.arbitraries._ +import org.typelevel.log4cats.slf4j.Slf4jLogger import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.common.errors._ -import org.typelevel.log4cats.slf4j.Slf4jLogger +import taskforce.HttpTestSuite class FilterRoutesSuite extends HttpTestSuite with instances.Circe { diff --git a/src/test/scala/taskforce/filter/TestFilterRepository.scala b/filtersFeature/src/test/scala/taskforce/filter/TestFilterRepository.scala similarity index 100% rename from src/test/scala/taskforce/filter/TestFilterRepository.scala rename to filtersFeature/src/test/scala/taskforce/filter/TestFilterRepository.scala diff --git a/src/test/scala/taskforce/arbitraries.scala b/filtersFeature/src/test/scala/taskforce/filter/arbitraries.scala similarity index 79% rename from src/test/scala/taskforce/arbitraries.scala rename to filtersFeature/src/test/scala/taskforce/filter/arbitraries.scala index 1467710..ae5a8a9 100644 --- a/src/test/scala/taskforce/arbitraries.scala +++ b/filtersFeature/src/test/scala/taskforce/filter/arbitraries.scala @@ -1,18 +1,15 @@ -package taskforce +package taskforce.filter import org.scalacheck.Arbitrary -import taskforce.generators._ +import generators._ object arbitraries { implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) - implicit def arbUserIdGen = Arbitrary(userIdGen) - implicit def arbNewProjectGen = Arbitrary(newProjectGen) implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) implicit def arbOperatorGen = Arbitrary(operatorGen) implicit def arbStatusGen = Arbitrary(statusGen) implicit def arbInGen = Arbitrary(inGen) - implicit def arbTaskCreatedGen = Arbitrary(taskCreatedGen) implicit def arbStateGen = Arbitrary(stateGen) implicit def arbConditionsGen = Arbitrary(conditionsGen) implicit def arbFilterIdGen = Arbitrary(filterIdGen) diff --git a/src/test/scala/taskforce/generators.scala b/filtersFeature/src/test/scala/taskforce/filter/generators.scala similarity index 78% rename from src/test/scala/taskforce/generators.scala rename to filtersFeature/src/test/scala/taskforce/filter/generators.scala index 2e2aba2..97af167 100644 --- a/src/test/scala/taskforce/generators.scala +++ b/filtersFeature/src/test/scala/taskforce/filter/generators.scala @@ -1,4 +1,4 @@ -package taskforce +package taskforce.filter import eu.timepit.refined.api.Refined @@ -6,14 +6,11 @@ import eu.timepit.refined.numeric.Positive import eu.timepit.refined.types.string.NonEmptyString import java.time.format.DateTimeFormatter import java.time.{LocalDate, LocalDateTime} + import org.scalacheck.Gen -import taskforce.authentication.UserId -import taskforce.filter._ -import taskforce.project._ import taskforce.task.generators._ import taskforce.project.generators._ -import taskforce.common.CreationDate -import taskforce.common.DeletionDate + object generators { val nonEmptyStringGen: Gen[String] = @@ -23,21 +20,6 @@ object generators { Gen.buildableOfN[String, Char](n, Gen.alphaChar) } - - val userIdGen: Gen[UserId] = - Gen.uuid.map(UserId.apply) - - val newProjectGen: Gen[ProjectName] = - nonEmptyStringGen - .map[NonEmptyString](Refined.unsafeApply) - .map(ProjectName.apply) - - def creationDateTimeGen: Gen[CreationDate] = - localDateTimeGen.map(CreationDate.apply) - - def deletionDateTimeGen: Gen[DeletionDate] = - localDateTimeGen.map(DeletionDate.apply) - def localDateTimeGen: Gen[LocalDateTime] = for { minutes <- Gen.chooseNum(0, 1000000000) @@ -45,7 +27,6 @@ object generators { .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) .atStartOfDay() .plusMinutes(minutes.toLong) - val operatorGen: Gen[Operator] = Gen.oneOf(List(Eq, Gt, Gteq, Lteq, Lt)) val statusGen: Gen[Status] = Gen.oneOf(List(Active, Deactive, All)) diff --git a/src/it/scala/taskforce/filter/FilterRepositorySuite.scala b/src/it/scala/taskforce/filter/FilterRepositorySuite.scala index 366f643..3282479 100644 --- a/src/it/scala/taskforce/filter/FilterRepositorySuite.scala +++ b/src/it/scala/taskforce/filter/FilterRepositorySuite.scala @@ -8,7 +8,7 @@ import eu.timepit.refined.collection._ import eu.timepit.refined.numeric.Positive import java.time.LocalDateTime import org.scalacheck.effect.PropF -import taskforce.arbitraries._ +import arbitraries._ import taskforce.BasicRepositorySuite class FilterRepositorySuite extends BasicRepositorySuite { diff --git a/src/it/scala/taskforce/project/ProjectRepositorySuite.scala b/src/it/scala/taskforce/project/ProjectRepositorySuite.scala index afc49e1..615ec9d 100644 --- a/src/it/scala/taskforce/project/ProjectRepositorySuite.scala +++ b/src/it/scala/taskforce/project/ProjectRepositorySuite.scala @@ -3,7 +3,7 @@ package taskforce.project import cats.effect.IO import cats.implicits._ import org.scalacheck.effect.PropF -import taskforce.arbitraries._ +import taskforce.project.arbitraries._ import taskforce.BasicRepositorySuite class ProjectRepositorySuite extends BasicRepositorySuite { From ab15500182d14210ca9df533ea299e2e21ae45ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Mon, 9 May 2022 11:11:08 +0200 Subject: [PATCH 08/24] strats moved to separate module --- build.sbt | 30 ++++++++++++++++++- .../main/scala/taskforce/stats/Stats.scala | 0 .../taskforce/stats/StatsRepository.scala | 0 .../scala/taskforce/stats/StatsRoutes.scala | 0 .../scala/taskforce/stats/StatsService.scala | 0 .../taskforce/stats/instances/Circe.scala | 0 .../taskforce/stats/instances/Doobie.scala | 2 +- 7 files changed, 30 insertions(+), 2 deletions(-) rename {src => statsFeature/src}/main/scala/taskforce/stats/Stats.scala (100%) rename {src => statsFeature/src}/main/scala/taskforce/stats/StatsRepository.scala (100%) rename {src => statsFeature/src}/main/scala/taskforce/stats/StatsRoutes.scala (100%) rename {src => statsFeature/src}/main/scala/taskforce/stats/StatsService.scala (100%) rename {src => statsFeature/src}/main/scala/taskforce/stats/instances/Circe.scala (100%) rename {src => statsFeature/src}/main/scala/taskforce/stats/instances/Doobie.scala (87%) diff --git a/build.sbt b/build.sbt index f15b9a8..b1b7d2d 100644 --- a/build.sbt +++ b/build.sbt @@ -56,11 +56,13 @@ lazy val root = (project in file(".")) ) .dependsOn( common % "test->test", - filters % "compile->compile;test->test" + filters % "compile->compile;test->test", + stats % "compile->compile;test->test" ) .aggregate( filters, projects, + stats, tasks ) @@ -160,3 +162,29 @@ lazy val filters = (project in file("filtersFeature")) tasks % "compile->compile;test->test", projects % "compile->compile;test->test" ) + + + lazy val stats = (project in file("statsFeature")) + .settings( + libraryDependencies ++= Seq( + circeDerivation, + circeExtras, + circeFs2, + circeParser, + circeRefined, + doobieHikari, + doobiePostgres, + doobieRefined, + http4sClient, + http4sCirce, + refined, + refinedCats, + log4cats + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") + ) + .dependsOn( + authentication % "compile->compile;test->test", + common % "test->test" + ) \ No newline at end of file diff --git a/src/main/scala/taskforce/stats/Stats.scala b/statsFeature/src/main/scala/taskforce/stats/Stats.scala similarity index 100% rename from src/main/scala/taskforce/stats/Stats.scala rename to statsFeature/src/main/scala/taskforce/stats/Stats.scala diff --git a/src/main/scala/taskforce/stats/StatsRepository.scala b/statsFeature/src/main/scala/taskforce/stats/StatsRepository.scala similarity index 100% rename from src/main/scala/taskforce/stats/StatsRepository.scala rename to statsFeature/src/main/scala/taskforce/stats/StatsRepository.scala diff --git a/src/main/scala/taskforce/stats/StatsRoutes.scala b/statsFeature/src/main/scala/taskforce/stats/StatsRoutes.scala similarity index 100% rename from src/main/scala/taskforce/stats/StatsRoutes.scala rename to statsFeature/src/main/scala/taskforce/stats/StatsRoutes.scala diff --git a/src/main/scala/taskforce/stats/StatsService.scala b/statsFeature/src/main/scala/taskforce/stats/StatsService.scala similarity index 100% rename from src/main/scala/taskforce/stats/StatsService.scala rename to statsFeature/src/main/scala/taskforce/stats/StatsService.scala diff --git a/src/main/scala/taskforce/stats/instances/Circe.scala b/statsFeature/src/main/scala/taskforce/stats/instances/Circe.scala similarity index 100% rename from src/main/scala/taskforce/stats/instances/Circe.scala rename to statsFeature/src/main/scala/taskforce/stats/instances/Circe.scala diff --git a/src/main/scala/taskforce/stats/instances/Doobie.scala b/statsFeature/src/main/scala/taskforce/stats/instances/Doobie.scala similarity index 87% rename from src/main/scala/taskforce/stats/instances/Doobie.scala rename to statsFeature/src/main/scala/taskforce/stats/instances/Doobie.scala index fbcf477..f5794da 100644 --- a/src/main/scala/taskforce/stats/instances/Doobie.scala +++ b/statsFeature/src/main/scala/taskforce/stats/instances/Doobie.scala @@ -8,7 +8,7 @@ import taskforce.common.Sqlizer import taskforce.stats.StatsQuery import taskforce.common.NewTypeDoobieMeta -trait Doobie extends taskforce.task.instances.Doobie with NewTypeDoobieMeta { +trait Doobie extends NewTypeDoobieMeta { implicit val statsQuerySqlizer: Sqlizer[StatsQuery] = new Sqlizer[StatsQuery] { def toFragment(sq: StatsQuery) = From 2951c5ca94bb75174137ca28e4bc51487bfc924e Mon Sep 17 00:00:00 2001 From: vder Date: Sun, 22 May 2022 12:28:27 +0200 Subject: [PATCH 09/24] Update README.md --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 705ac16..4287c42 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,46 @@ # Taskforce App +## Requirements + +The task is to write a REST API for the time logging system. +### Functional requirements: +1. The system can be used by logged in users. Users are assumed to be system authenticated with a JWT that includes a user ID in UUID format. No registration or login mechanism is required, a only authentication. +2. The system allows each user to create a new project. Project consists of the: + * Unique identifier of the project in text form, given by user. + * Timestamp of project creation for accounting purposes. +3. The system enables the author of the project to change the project ID. +4. The system allows each user to log the time spent on project (hereinafter referred to as a task). The task consists of: + * Project start time stamp. + * The duration of the task. + * Optional: volume expressed as a natural number. + * Optional: comment. +5. The given user cannot save two tasks which overlap in time according to the attributes of these tasks. +6. The system allows the author of the task to delete it, but it must be a "soft delete" timestamped with removal. + +7. The system allows the author of the task to change all of the above-mentioned task attributes while validating from point 5. This should be done as deleting a task (as in point 6) and creating a new one in its place. +8. The system allows the author of the project to delete it, but it must be a "soft delete" timestamped with removal. All existing deleted tasks the project is considered deleted with the same timestamp. +9. The system allows you to return information about a given project together with the associated ones tasks and the total duration of the project. +10. The system allows you to list projects together with the associated tasks from using conjugation of the following filters (each is optional): + * list of identifiers, + * from - creation timestamp, + * to - creation timestamp, + * removed / not removed, + +and sort using at most one of the following criteria: + * Creation timestamp - descending / ascending, + * update timestamp - the latest added task (in case of an empty project - the creation date should be taken) - descending / ascending, + +and simple pagination based on size and page number. + +11. The system allows you to display the following statistics (they only count for the statistics unremoved projects and tasks): + * Total number of tasks. + * Average duration of the task. + * Average volume of the job (for jobs with the specified volume). + * Volume weighted average task duration (for tasks from given volume). + +Access to statistics should be parameterized with a list of identifiers users whose tasks should be included and the from-to dates in the form year-month. + + ## Usage ```shell @@ -26,4 +67,4 @@ * "http://{host}:{port}/api/v1/filters" /GET /POST * "http://{host}:{port}/api/v1/filters/{filterId}" /GET * "http://{host}:{port}/api/v1/filters/{filterId}/data" /GET -* "http://{host}:{port}/api/v1/stats" /GET \ No newline at end of file +* "http://{host}:{port}/api/v1/stats" /GET From d2d2aae057957d8fc72e7f442e7f5192cd98777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 22 May 2022 20:19:16 +0200 Subject: [PATCH 10/24] integration tests split between modules everything works but require some cleaning --- build.sbt | 41 ++- common/src/it/resources/application.conf | 7 + .../db/migration/V1__table_users.sql | 1 + .../db/migration/V2__table_users_data.sql | 8 + .../db/migration/V3__table_projects.sql | 7 + .../db/migration/V4__table_tasks.sql | 10 + .../db/migration/V5__table_filters.sql | 10 + .../db/migration/V6__sample_data.sql | 255 ++++++++++++++++++ common/src/it/resources/logback.xml | 16 ++ .../taskforce/BasicRepositorySuite.scala | 5 +- .../taskforce/config/DatabaseConfig.scala | 0 .../scala/taskforce/config/HostConfig.scala | 0 .../filter/FilterRepositorySuite.scala | 0 project/Dependencies.scala | 4 +- .../project/ProjectRepositorySuite.scala | 2 + .../taskforce/task/TaskRepositorySuite.scala | 6 +- 16 files changed, 357 insertions(+), 15 deletions(-) create mode 100644 common/src/it/resources/application.conf create mode 100644 common/src/it/resources/db/migration/V1__table_users.sql create mode 100644 common/src/it/resources/db/migration/V2__table_users_data.sql create mode 100644 common/src/it/resources/db/migration/V3__table_projects.sql create mode 100644 common/src/it/resources/db/migration/V4__table_tasks.sql create mode 100644 common/src/it/resources/db/migration/V5__table_filters.sql create mode 100644 common/src/it/resources/db/migration/V6__sample_data.sql create mode 100644 common/src/it/resources/logback.xml rename {src => common/src}/it/scala/taskforce/BasicRepositorySuite.scala (89%) rename {src => common/src}/main/scala/taskforce/config/DatabaseConfig.scala (100%) rename {src => common/src}/main/scala/taskforce/config/HostConfig.scala (100%) rename {src => filtersFeature/src}/it/scala/taskforce/filter/FilterRepositorySuite.scala (100%) rename {src => projectsFeature/src}/it/scala/taskforce/project/ProjectRepositorySuite.scala (96%) rename {src => tasksFeature/src}/it/scala/taskforce/task/TaskRepositorySuite.scala (94%) diff --git a/build.sbt b/build.sbt index b1b7d2d..dec3aad 100644 --- a/build.sbt +++ b/build.sbt @@ -7,13 +7,14 @@ ThisBuild / organizationName := "pfl" ThisBuild / scalaVersion := "2.13.8" ThisBuild / version := "0.1.0-SNAPSHOT" -IntegrationTest / parallelExecution := false +IntegrationTest / parallelExecution in Global := false lazy val root = (project in file(".")) .enablePlugins(JavaAppPackaging) .enablePlugins(DockerPlugin) .enablePlugins(FlywayPlugin) .enablePlugins(AshScriptPlugin) + .enablePlugins(RevolverPlugin) .configs(IntegrationTest.extend(Test)) .settings( name := "taskforce", @@ -35,11 +36,7 @@ lazy val root = (project in file(".")) semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x libraryDependencies ++= Seq( flyway, - logback, - pureConfig, - pureConfigCE, - pureConfigRefined, - + logback, ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), addCompilerPlugin(betterMonadicFor), @@ -67,28 +64,40 @@ lazy val root = (project in file(".")) ) lazy val common = (project in file("common")) + .disablePlugins(RevolverPlugin) + .configs(IntegrationTest extend Test) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( cats, circe, doobie, doobieQuill, + flyway, http4sCirce, http4sDsl, + log4cats, + logback, monixNewType, monixNewTypeCirce, mUnit, mUnitCE, mUnitScalacheck, + pureConfig, + pureConfigCE, + pureConfigRefined, simulacrum, scalaCheckEffect, - scalaCheckEffectMunit + scalaCheckEffectMunit, + slf4j ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), + scalacOptions ++= Seq("-Ymacro-annotations") ) lazy val authentication = (project in file("auth")) + .disablePlugins(RevolverPlugin) .settings( libraryDependencies ++= Seq( circeParser, @@ -102,7 +111,10 @@ lazy val authentication = (project in file("auth")) .dependsOn(common) lazy val projects = (project in file("projectsFeature")) + .disablePlugins(RevolverPlugin) + .configs((IntegrationTest extend Test)) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( circeDerivation, circeExtras, @@ -122,11 +134,15 @@ lazy val projects = (project in file("projectsFeature")) ) .dependsOn( authentication % "compile->compile;test->test", - common % "test->test" + common % "test->test;it->it;test->it" ) + lazy val tasks = (project in file("tasksFeature")) + .disablePlugins(RevolverPlugin) + .configs((IntegrationTest extend Test)) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( circeDerivation, circeExtras, @@ -138,6 +154,9 @@ lazy val tasks = (project in file("tasksFeature")) doobieRefined, http4sClient, http4sCirce, + mUnit, + mUnitCE, + mUnitScalacheck, refined, refinedCats ).map(_.exclude("org.slf4j", "*")), @@ -146,11 +165,14 @@ lazy val tasks = (project in file("tasksFeature")) ) .dependsOn( authentication % "compile->compile;test->test", - common % "test->test" + common % "test->test;it->it;compile->compile;test->it" ) lazy val filters = (project in file("filtersFeature")) + .disablePlugins(RevolverPlugin) + .configs((IntegrationTest extend Test)) .settings( + Defaults.itSettings, libraryDependencies ++= Seq( log4cats, slf4j @@ -165,6 +187,7 @@ lazy val filters = (project in file("filtersFeature")) lazy val stats = (project in file("statsFeature")) + .disablePlugins(RevolverPlugin) .settings( libraryDependencies ++= Seq( circeDerivation, diff --git a/common/src/it/resources/application.conf b/common/src/it/resources/application.conf new file mode 100644 index 0000000..82f1da1 --- /dev/null +++ b/common/src/it/resources/application.conf @@ -0,0 +1,7 @@ + +database { + driver = "org.postgresql.Driver" + url = "jdbc:postgresql://localhost:54340/task_test" + user = "vder" + pass = "password" +} \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V1__table_users.sql b/common/src/it/resources/db/migration/V1__table_users.sql new file mode 100644 index 0000000..ff445c7 --- /dev/null +++ b/common/src/it/resources/db/migration/V1__table_users.sql @@ -0,0 +1 @@ +create table if not exists users(id UUID PRIMARY KEY); \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V2__table_users_data.sql b/common/src/it/resources/db/migration/V2__table_users_data.sql new file mode 100644 index 0000000..5cec935 --- /dev/null +++ b/common/src/it/resources/db/migration/V2__table_users_data.sql @@ -0,0 +1,8 @@ +insert into users(id) +values('5260ca29-a70b-494e-a3d6-55374a3b0a04'); +insert into users(id) +values('a57b662a-a386-11eb-bcbc-0242ac130002'); +insert into users(id) +values('aa7e1d66-a386-11eb-bcbc-0242ac130002'); +insert into users(id) +values('b0b8d040-a386-11eb-bcbc-0242ac130002'); \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V3__table_projects.sql b/common/src/it/resources/db/migration/V3__table_projects.sql new file mode 100644 index 0000000..fcdb5d5 --- /dev/null +++ b/common/src/it/resources/db/migration/V3__table_projects.sql @@ -0,0 +1,7 @@ +create table if not exists projects( + id BIGSERIAL PRIMARY KEY, + name TEXT UNIQUE, + author UUID references users(id), + created timestamp without time zone, + deleted timestamp without time zone +); \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V4__table_tasks.sql b/common/src/it/resources/db/migration/V4__table_tasks.sql new file mode 100644 index 0000000..89719d7 --- /dev/null +++ b/common/src/it/resources/db/migration/V4__table_tasks.sql @@ -0,0 +1,10 @@ +create table if not exists tasks( + id UUID PRIMARY KEY, + project_id int NOT NULL REFERENCES projects(id), + author UUID references users(id), + started timestamp without time zone, + duration Int, + volume int, + deleted timestamp without time zone, + comment text +); \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V5__table_filters.sql b/common/src/it/resources/db/migration/V5__table_filters.sql new file mode 100644 index 0000000..f3da431 --- /dev/null +++ b/common/src/it/resources/db/migration/V5__table_filters.sql @@ -0,0 +1,10 @@ +create table if not exists filters( + id SERIAL PRIMARY KEY, + filter_id UUID, + criteria_type varchar(20), + field varchar(20), + operator varchar(20), + date_value timestamp without time zone, + status_value varchar(20), + list_value text [] +); \ No newline at end of file diff --git a/common/src/it/resources/db/migration/V6__sample_data.sql b/common/src/it/resources/db/migration/V6__sample_data.sql new file mode 100644 index 0000000..6b30e8b --- /dev/null +++ b/common/src/it/resources/db/migration/V6__sample_data.sql @@ -0,0 +1,255 @@ +delete from tasks; +delete from projects; +insert into projects(name, author, created) +values( + 'project 1', + '5260ca29-a70b-494e-a3d6-55374a3b0a04', + '2021-05-09 13:38:17.730944' + ); +insert into projects(name, author, created) +values( + 'project 2', + '5260ca29-a70b-494e-a3d6-55374a3b0a04', + '2021-05-09 13:38:17.730944' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + '54b28d5e-3b33-46a6-a02c-4ac8159a7bcd', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0001-04-17 01:35:32', + 10, + 16, + null, + 'task11' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'ba386e57-c609-4703-9e0b-18428b41c84f', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0002-08-19 09:53:30', + 1000, + 51, + null, + 'task12' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'e274078c-21d8-44c2-b832-05e484828c6e', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0003-07-02 00:00:01', + 1000, + 5, + null, + 'task13' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'd2e3a5eb-29dc-4a6c-aef5-372ecf55592a', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0004-07-28 23:27:20', + 1000, + 11, + null, + 'task14' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + '18231054-b3e8-4d57-bfb1-6c3b6b9923d0', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0005-07-04 23:23:39', + 1000, + 27, + '0005-07-04 23:23:39', + 'task15' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'bd9274f8-c00f-4838-8f64-8e42f17cbbc3', + 1, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0006-07-02 00:00:00', + 10, + 16, + '0006-07-02 00:00:00', + 'task16' + ); +---- +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + '721a73c4-08ad-4cba-96d8-6d295cd60ecb', + 2, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0201-07-02 00:00:01', + 1000, + 52, + null, + 'task21' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'b22d9602-c8e1-47b7-a2d7-7e01ef933fef', + 2, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '0202-03-10 01:46:40', + 995, + 84, + null, + 'task22' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + '8ea8ea18-93bc-4ca7-b213-661d91f9f339', + 2, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '203-11-24 11:40:56', + 452, + 6, + null, + 'task23' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'bf274431-73fa-40ef-b2bc-03bdd5caa851', + 2, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '204-07-02 00:00:01', + 409, + 22, + null, + 'task24' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + '9d586225-6667-48ac-8076-6460cd64e35e', + 2, + 'aa7e1d66-a386-11eb-bcbc-0242ac130002', + '205-07-02 00:00:01', + 877, + 97, + '205-07-02 00:00:01', + 'task25' + ); +insert into tasks( + id, + project_id, + author, + started, + duration, + volume, + deleted, + comment + ) +values( + 'a43be0db-1e32-410e-abd6-c903c5459b18', + 2, + '5260ca29-a70b-494e-a3d6-55374a3b0a04', + '206-11-22 02:02:20', + 1000, + 98, + '206-11-22 02:02:20', + 'task26' + ); \ No newline at end of file diff --git a/common/src/it/resources/logback.xml b/common/src/it/resources/logback.xml new file mode 100644 index 0000000..8ea8412 --- /dev/null +++ b/common/src/it/resources/logback.xml @@ -0,0 +1,16 @@ + + + + true + + [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n + + + + + + diff --git a/src/it/scala/taskforce/BasicRepositorySuite.scala b/common/src/it/scala/taskforce/BasicRepositorySuite.scala similarity index 89% rename from src/it/scala/taskforce/BasicRepositorySuite.scala rename to common/src/it/scala/taskforce/BasicRepositorySuite.scala index 2d15b71..fb93e99 100644 --- a/src/it/scala/taskforce/BasicRepositorySuite.scala +++ b/common/src/it/scala/taskforce/BasicRepositorySuite.scala @@ -8,7 +8,6 @@ import java.util.UUID import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.flywaydb.core.Flyway import taskforce.config.DatabaseConfig -import taskforce.authentication.UserId import cats.effect.kernel.Sync import org.typelevel.log4cats.slf4j.Slf4jLogger @@ -18,7 +17,7 @@ trait BasicRepositorySuite extends CatsEffectSuite with ScalaCheckEffectSuite { var db: DatabaseConfig = null var flyway: Flyway = null var xa: transactor.Transactor.Aux[IO, Unit] = null - val userID = UserId(UUID.fromString("5260ca29-a70b-494e-a3d6-55374a3b0a04")) + val userIdUUID = UUID.fromString("5260ca29-a70b-494e-a3d6-55374a3b0a04") override def scalaCheckTestParameters = super.scalaCheckTestParameters.withMinSuccessfulTests(1) @@ -26,7 +25,7 @@ trait BasicRepositorySuite extends CatsEffectSuite with ScalaCheckEffectSuite { override def beforeAll(): Unit = { db = ConfigSource.default - .at("database_test") + .at("database") .load[DatabaseConfig] match { case Left(errors) => diff --git a/src/main/scala/taskforce/config/DatabaseConfig.scala b/common/src/main/scala/taskforce/config/DatabaseConfig.scala similarity index 100% rename from src/main/scala/taskforce/config/DatabaseConfig.scala rename to common/src/main/scala/taskforce/config/DatabaseConfig.scala diff --git a/src/main/scala/taskforce/config/HostConfig.scala b/common/src/main/scala/taskforce/config/HostConfig.scala similarity index 100% rename from src/main/scala/taskforce/config/HostConfig.scala rename to common/src/main/scala/taskforce/config/HostConfig.scala diff --git a/src/it/scala/taskforce/filter/FilterRepositorySuite.scala b/filtersFeature/src/it/scala/taskforce/filter/FilterRepositorySuite.scala similarity index 100% rename from src/it/scala/taskforce/filter/FilterRepositorySuite.scala rename to filtersFeature/src/it/scala/taskforce/filter/FilterRepositorySuite.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala index da178e5..7e7538e 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -28,9 +28,9 @@ object Dependencies { def circeLib(artifact: String): ModuleID = "io.circe" %% artifact % V.circe def doobieLib(artifact: String): ModuleID = "org.tpolecat" %% artifact % V.doobie def http4sLib(artifact: String): ModuleID = "org.http4s" %% artifact % V.http4s - def mUnitLib(artifact: String): ModuleID = "org.scalameta" %% artifact % V.munit % Test + def mUnitLib(artifact: String): ModuleID = "org.scalameta" %% artifact % V.munit def refinedLib(artifact: String): ModuleID = "eu.timepit" %% artifact % V.refined - def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v % Test + def typeLevelLibTest(artifact: String, v: String): ModuleID = "org.typelevel" %% artifact % v val cats = "org.typelevel" %% "cats-core" % V.cats val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEff diff --git a/src/it/scala/taskforce/project/ProjectRepositorySuite.scala b/projectsFeature/src/it/scala/taskforce/project/ProjectRepositorySuite.scala similarity index 96% rename from src/it/scala/taskforce/project/ProjectRepositorySuite.scala rename to projectsFeature/src/it/scala/taskforce/project/ProjectRepositorySuite.scala index 615ec9d..477be12 100644 --- a/src/it/scala/taskforce/project/ProjectRepositorySuite.scala +++ b/projectsFeature/src/it/scala/taskforce/project/ProjectRepositorySuite.scala @@ -5,10 +5,12 @@ import cats.implicits._ import org.scalacheck.effect.PropF import taskforce.project.arbitraries._ import taskforce.BasicRepositorySuite +import taskforce.authentication.UserId class ProjectRepositorySuite extends BasicRepositorySuite { var projectRepo: IO[ProjectRepository[IO]] = null + val userID = UserId(userIdUUID) override def beforeAll(): Unit = { super.beforeAll() diff --git a/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/tasksFeature/src/it/scala/taskforce/task/TaskRepositorySuite.scala similarity index 94% rename from src/it/scala/taskforce/task/TaskRepositorySuite.scala rename to tasksFeature/src/it/scala/taskforce/task/TaskRepositorySuite.scala index 16e989a..c6d1517 100644 --- a/src/it/scala/taskforce/task/TaskRepositorySuite.scala +++ b/tasksFeature/src/it/scala/taskforce/task/TaskRepositorySuite.scala @@ -2,20 +2,24 @@ package taskforce.task import cats.effect.IO import org.scalacheck.effect.PropF -import taskforce.task.arbitraries._ import taskforce.task.ProjectId import taskforce.BasicRepositorySuite +import taskforce.authentication.UserId +import taskforce.task.arbitraries._ class TaskRepositorySuite extends BasicRepositorySuite { var taskRepo: IO[TaskRepository[IO]] = null + val userID = UserId(userIdUUID) + override def beforeAll(): Unit = { super.beforeAll() taskRepo = LiveTaskRepository.make[IO](xa) } + test("task creation") { PropF.forAllF { (t: Task) => for { From 1ec4b0472eac9d5da88a3cf697f8b147ea108c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Sun, 22 May 2022 21:35:52 +0200 Subject: [PATCH 11/24] moving files between folders + build.sbt changes --- build.sbt | 124 ++++++------------ .../TaskForceAuthMiddleware.scala | 0 .../scala/taskforce/authentication/User.scala | 0 .../authentication/UserRepository.scala | 0 .../authentication/instances/Circe.scala | 0 .../taskforce/authentication/package.scala | 0 .../authentication/TestUserRepository.scala | 0 .../common}/src/it/resources/application.conf | 0 .../db/migration/V1__table_users.sql | 0 .../db/migration/V2__table_users_data.sql | 0 .../db/migration/V3__table_projects.sql | 0 .../db/migration/V4__table_tasks.sql | 0 .../db/migration/V5__table_filters.sql | 0 .../db/migration/V6__sample_data.sql | 0 .../common}/src/it/resources/logback.xml | 0 .../taskforce/BasicRepositorySuite.scala | 0 .../scala/taskforce/common/ErrorHandler.scala | 0 .../scala/taskforce/common/ErrorMessage.scala | 0 .../common/NewTypeDoobieMetaInstance.scala | 0 .../main/scala/taskforce/common/Sqlizer.scala | 0 .../main/scala/taskforce/common/errors.scala | 0 .../main/scala/taskforce/common/package.scala | 0 .../taskforce/config/DatabaseConfig.scala | 0 .../scala/taskforce/config/HostConfig.scala | 0 .../src/test/scala/taskforce/http4stest.scala | 0 .../filter/FilterRepositorySuite.scala | 0 .../main/scala/taskforce/filter/Filter.scala | 0 .../taskforce/filter/FilterRepository.scala | 0 .../scala/taskforce/filter/FilterRoutes.scala | 0 .../taskforce/filter/FilterService.scala | 0 .../scala/taskforce/filter/QueryParams.scala | 0 .../taskforce/filter/instances/Circe.scala | 0 .../taskforce/filter/instances/Doobie.scala | 0 .../taskforce/filter/instances/Http4s.scala | 0 .../taskforce/filter/instances/helpers.scala | 0 .../taskforce/filter/FilterRoutesSuite.scala | 0 .../filter/TestFilterRepository.scala | 0 .../scala/taskforce/filter/arbitraries.scala | 0 .../scala/taskforce/filter/generators.scala | 0 .../project/ProjectRepositorySuite.scala | 0 .../scala/taskforce/project/Project.scala | 0 .../taskforce/project/ProjectError.scala | 0 .../taskforce/project/ProjectRepository.scala | 0 .../taskforce/project/ProjectRoutes.scala | 0 .../taskforce/project/ProjectService.scala | 0 .../taskforce/project/instances/Doobie.scala | 0 .../taskforce/project/instances/Http4s.scala | 0 .../scala/taskforce/project/package.scala | 0 .../project/ProjectRoutesSuite.scala | 0 .../project/TestProjectRepository.scala | 0 .../scala/taskforce/project/arbitraries.scala | 0 .../scala/taskforce/project/generators.scala | 0 .../main/scala/taskforce/stats/Stats.scala | 0 .../taskforce/stats/StatsRepository.scala | 0 .../scala/taskforce/stats/StatsRoutes.scala | 0 .../scala/taskforce/stats/StatsService.scala | 0 .../taskforce/stats/instances/Circe.scala | 0 .../taskforce/stats/instances/Doobie.scala | 0 .../taskforce/task/TaskRepositorySuite.scala | 0 .../src/main/scala/taskforce/task/Task.scala | 0 .../main/scala/taskforce/task/TaskError.scala | 0 .../scala/taskforce/task/TaskRepository.scala | 0 .../scala/taskforce/task/TaskRoutes.scala | 0 .../scala/taskforce/task/TaskService.scala | 0 .../taskforce/task/instances/Doobie.scala | 0 .../main/scala/taskforce/task/package.scala | 0 .../taskforce/task/TaskRoutesSuite.scala | 0 .../taskforce/task/TestTaskRepository.scala | 0 .../scala/taskforce/task/arbitraries.scala | 0 .../scala/taskforce/task/generators.scala | 0 70 files changed, 41 insertions(+), 83 deletions(-) rename {auth => modules/auth}/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala (100%) rename {auth => modules/auth}/src/main/scala/taskforce/authentication/User.scala (100%) rename {auth => modules/auth}/src/main/scala/taskforce/authentication/UserRepository.scala (100%) rename {auth => modules/auth}/src/main/scala/taskforce/authentication/instances/Circe.scala (100%) rename {auth => modules/auth}/src/main/scala/taskforce/authentication/package.scala (100%) rename {auth => modules/auth}/src/test/scala/taskforce/authentication/TestUserRepository.scala (100%) rename {common => modules/common}/src/it/resources/application.conf (100%) rename {common => modules/common}/src/it/resources/db/migration/V1__table_users.sql (100%) rename {common => modules/common}/src/it/resources/db/migration/V2__table_users_data.sql (100%) rename {common => modules/common}/src/it/resources/db/migration/V3__table_projects.sql (100%) rename {common => modules/common}/src/it/resources/db/migration/V4__table_tasks.sql (100%) rename {common => modules/common}/src/it/resources/db/migration/V5__table_filters.sql (100%) rename {common => modules/common}/src/it/resources/db/migration/V6__sample_data.sql (100%) rename {common => modules/common}/src/it/resources/logback.xml (100%) rename {common => modules/common}/src/it/scala/taskforce/BasicRepositorySuite.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/ErrorHandler.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/ErrorMessage.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/Sqlizer.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/errors.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/common/package.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/config/DatabaseConfig.scala (100%) rename {common => modules/common}/src/main/scala/taskforce/config/HostConfig.scala (100%) rename {common => modules/common}/src/test/scala/taskforce/http4stest.scala (100%) rename {filtersFeature => modules/filters}/src/it/scala/taskforce/filter/FilterRepositorySuite.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/Filter.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/FilterRepository.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/FilterRoutes.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/FilterService.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/QueryParams.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/instances/Circe.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/instances/Doobie.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/instances/Http4s.scala (100%) rename {filtersFeature => modules/filters}/src/main/scala/taskforce/filter/instances/helpers.scala (100%) rename {filtersFeature => modules/filters}/src/test/scala/taskforce/filter/FilterRoutesSuite.scala (100%) rename {filtersFeature => modules/filters}/src/test/scala/taskforce/filter/TestFilterRepository.scala (100%) rename {filtersFeature => modules/filters}/src/test/scala/taskforce/filter/arbitraries.scala (100%) rename {filtersFeature => modules/filters}/src/test/scala/taskforce/filter/generators.scala (100%) rename {projectsFeature => modules/projects}/src/it/scala/taskforce/project/ProjectRepositorySuite.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/Project.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/ProjectError.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/ProjectRepository.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/ProjectRoutes.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/ProjectService.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/instances/Doobie.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/instances/Http4s.scala (100%) rename {projectsFeature => modules/projects}/src/main/scala/taskforce/project/package.scala (100%) rename {projectsFeature => modules/projects}/src/test/scala/taskforce/project/ProjectRoutesSuite.scala (100%) rename {projectsFeature => modules/projects}/src/test/scala/taskforce/project/TestProjectRepository.scala (100%) rename {projectsFeature => modules/projects}/src/test/scala/taskforce/project/arbitraries.scala (100%) rename {projectsFeature => modules/projects}/src/test/scala/taskforce/project/generators.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/Stats.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/StatsRepository.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/StatsRoutes.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/StatsService.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/instances/Circe.scala (100%) rename {statsFeature => modules/stats}/src/main/scala/taskforce/stats/instances/Doobie.scala (100%) rename {tasksFeature => modules/tasks}/src/it/scala/taskforce/task/TaskRepositorySuite.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/Task.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/TaskError.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/TaskRepository.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/TaskRoutes.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/TaskService.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/instances/Doobie.scala (100%) rename {tasksFeature => modules/tasks}/src/main/scala/taskforce/task/package.scala (100%) rename {tasksFeature => modules/tasks}/src/test/scala/taskforce/task/TaskRoutesSuite.scala (100%) rename {tasksFeature => modules/tasks}/src/test/scala/taskforce/task/TestTaskRepository.scala (100%) rename {tasksFeature => modules/tasks}/src/test/scala/taskforce/task/arbitraries.scala (100%) rename {tasksFeature => modules/tasks}/src/test/scala/taskforce/task/generators.scala (100%) diff --git a/build.sbt b/build.sbt index dec3aad..262d1d9 100644 --- a/build.sbt +++ b/build.sbt @@ -34,10 +34,6 @@ lazy val root = (project in file(".")) dockerUpdateLatest := true, semanticdbEnabled := true, // enable SemanticDB semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x - libraryDependencies ++= Seq( - flyway, - logback, - ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), addCompilerPlugin(betterMonadicFor), scalacOptions ++= Seq( @@ -52,9 +48,9 @@ lazy val root = (project in file(".")) ) ) .dependsOn( - common % "test->test", + common % "test->test", filters % "compile->compile;test->test", - stats % "compile->compile;test->test" + stats % "compile->compile;test->test" ) .aggregate( filters, @@ -63,7 +59,7 @@ lazy val root = (project in file(".")) tasks ) -lazy val common = (project in file("common")) +lazy val common = (project in file("modules/common")) .disablePlugins(RevolverPlugin) .configs(IntegrationTest extend Test) .settings( @@ -71,13 +67,12 @@ lazy val common = (project in file("common")) libraryDependencies ++= Seq( cats, circe, - doobie, doobieQuill, flyway, http4sCirce, http4sDsl, log4cats, - logback, + logback, monixNewType, monixNewTypeCirce, mUnit, @@ -86,17 +81,16 @@ lazy val common = (project in file("common")) pureConfig, pureConfigCE, pureConfigRefined, - simulacrum, scalaCheckEffect, scalaCheckEffectMunit, + simulacrum, slf4j ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") ) -lazy val authentication = (project in file("auth")) +lazy val authentication = (project in file("modules/auth")) .disablePlugins(RevolverPlugin) .settings( libraryDependencies ++= Seq( @@ -110,73 +104,29 @@ lazy val authentication = (project in file("auth")) ) .dependsOn(common) -lazy val projects = (project in file("projectsFeature")) +lazy val projects = (project in file("modules/projects")) .disablePlugins(RevolverPlugin) .configs((IntegrationTest extend Test)) - .settings( - Defaults.itSettings, - libraryDependencies ++= Seq( - circeDerivation, - circeExtras, - circeFs2, - circeParser, - circeRefined, - doobieHikari, - doobiePostgres, - doobieRefined, - http4sClient, - http4sCirce, - refined, - refinedCats - ).map(_.exclude("org.slf4j", "*")), - addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") - ) + .settings(Defaults.itSettings,sharedSettings) .dependsOn( authentication % "compile->compile;test->test", common % "test->test;it->it;test->it" ) - -lazy val tasks = (project in file("tasksFeature")) +lazy val tasks = (project in file("modules/tasks")) .disablePlugins(RevolverPlugin) .configs((IntegrationTest extend Test)) - .settings( - Defaults.itSettings, - libraryDependencies ++= Seq( - circeDerivation, - circeExtras, - circeFs2, - circeParser, - circeRefined, - doobieHikari, - doobiePostgres, - doobieRefined, - http4sClient, - http4sCirce, - mUnit, - mUnitCE, - mUnitScalacheck, - refined, - refinedCats - ).map(_.exclude("org.slf4j", "*")), - addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") - ) + .settings(Defaults.itSettings,sharedSettings) .dependsOn( authentication % "compile->compile;test->test", common % "test->test;it->it;compile->compile;test->it" ) -lazy val filters = (project in file("filtersFeature")) +lazy val filters = (project in file("modules/filters")) .disablePlugins(RevolverPlugin) .configs((IntegrationTest extend Test)) .settings( Defaults.itSettings, - libraryDependencies ++= Seq( - log4cats, - slf4j - ).map(_.exclude("org.slf4j", "*")), addCompilerPlugin(kindProjector), scalacOptions ++= Seq("-Ymacro-annotations") ) @@ -185,29 +135,37 @@ lazy val filters = (project in file("filtersFeature")) projects % "compile->compile;test->test" ) - - lazy val stats = (project in file("statsFeature")) +lazy val stats = (project in file("modules/stats")) .disablePlugins(RevolverPlugin) - .settings( - libraryDependencies ++= Seq( - circeDerivation, - circeExtras, - circeFs2, - circeParser, - circeRefined, - doobieHikari, - doobiePostgres, - doobieRefined, - http4sClient, - http4sCirce, - refined, - refinedCats, - log4cats - ).map(_.exclude("org.slf4j", "*")), - addCompilerPlugin(kindProjector), - scalacOptions ++= Seq("-Ymacro-annotations") - ) + .settings(Defaults.itSettings,sharedSettings) .dependsOn( authentication % "compile->compile;test->test", common % "test->test" - ) \ No newline at end of file + ) + +lazy val sharedSettings = Seq( + libraryDependencies ++= Seq( + circeDerivation, + circeExtras, + circeFs2, + circeParser, + circeRefined, + doobieHikari, + doobiePostgres, + doobieRefined, + http4sClient, + refined, + refinedCats + ).map(_.exclude("org.slf4j", "*")), + addCompilerPlugin(kindProjector), + scalacOptions ++= Seq( + "-deprecation", + "-encoding", + "UTF-8", + "-language:higherKinds", + "-language:postfixOps", + "-feature", + "-Xlint:unused", + "-Ymacro-annotations" + ) +) diff --git a/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala b/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala similarity index 100% rename from auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala rename to modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala diff --git a/auth/src/main/scala/taskforce/authentication/User.scala b/modules/auth/src/main/scala/taskforce/authentication/User.scala similarity index 100% rename from auth/src/main/scala/taskforce/authentication/User.scala rename to modules/auth/src/main/scala/taskforce/authentication/User.scala diff --git a/auth/src/main/scala/taskforce/authentication/UserRepository.scala b/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala similarity index 100% rename from auth/src/main/scala/taskforce/authentication/UserRepository.scala rename to modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala diff --git a/auth/src/main/scala/taskforce/authentication/instances/Circe.scala b/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala similarity index 100% rename from auth/src/main/scala/taskforce/authentication/instances/Circe.scala rename to modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala diff --git a/auth/src/main/scala/taskforce/authentication/package.scala b/modules/auth/src/main/scala/taskforce/authentication/package.scala similarity index 100% rename from auth/src/main/scala/taskforce/authentication/package.scala rename to modules/auth/src/main/scala/taskforce/authentication/package.scala diff --git a/auth/src/test/scala/taskforce/authentication/TestUserRepository.scala b/modules/auth/src/test/scala/taskforce/authentication/TestUserRepository.scala similarity index 100% rename from auth/src/test/scala/taskforce/authentication/TestUserRepository.scala rename to modules/auth/src/test/scala/taskforce/authentication/TestUserRepository.scala diff --git a/common/src/it/resources/application.conf b/modules/common/src/it/resources/application.conf similarity index 100% rename from common/src/it/resources/application.conf rename to modules/common/src/it/resources/application.conf diff --git a/common/src/it/resources/db/migration/V1__table_users.sql b/modules/common/src/it/resources/db/migration/V1__table_users.sql similarity index 100% rename from common/src/it/resources/db/migration/V1__table_users.sql rename to modules/common/src/it/resources/db/migration/V1__table_users.sql diff --git a/common/src/it/resources/db/migration/V2__table_users_data.sql b/modules/common/src/it/resources/db/migration/V2__table_users_data.sql similarity index 100% rename from common/src/it/resources/db/migration/V2__table_users_data.sql rename to modules/common/src/it/resources/db/migration/V2__table_users_data.sql diff --git a/common/src/it/resources/db/migration/V3__table_projects.sql b/modules/common/src/it/resources/db/migration/V3__table_projects.sql similarity index 100% rename from common/src/it/resources/db/migration/V3__table_projects.sql rename to modules/common/src/it/resources/db/migration/V3__table_projects.sql diff --git a/common/src/it/resources/db/migration/V4__table_tasks.sql b/modules/common/src/it/resources/db/migration/V4__table_tasks.sql similarity index 100% rename from common/src/it/resources/db/migration/V4__table_tasks.sql rename to modules/common/src/it/resources/db/migration/V4__table_tasks.sql diff --git a/common/src/it/resources/db/migration/V5__table_filters.sql b/modules/common/src/it/resources/db/migration/V5__table_filters.sql similarity index 100% rename from common/src/it/resources/db/migration/V5__table_filters.sql rename to modules/common/src/it/resources/db/migration/V5__table_filters.sql diff --git a/common/src/it/resources/db/migration/V6__sample_data.sql b/modules/common/src/it/resources/db/migration/V6__sample_data.sql similarity index 100% rename from common/src/it/resources/db/migration/V6__sample_data.sql rename to modules/common/src/it/resources/db/migration/V6__sample_data.sql diff --git a/common/src/it/resources/logback.xml b/modules/common/src/it/resources/logback.xml similarity index 100% rename from common/src/it/resources/logback.xml rename to modules/common/src/it/resources/logback.xml diff --git a/common/src/it/scala/taskforce/BasicRepositorySuite.scala b/modules/common/src/it/scala/taskforce/BasicRepositorySuite.scala similarity index 100% rename from common/src/it/scala/taskforce/BasicRepositorySuite.scala rename to modules/common/src/it/scala/taskforce/BasicRepositorySuite.scala diff --git a/common/src/main/scala/taskforce/common/ErrorHandler.scala b/modules/common/src/main/scala/taskforce/common/ErrorHandler.scala similarity index 100% rename from common/src/main/scala/taskforce/common/ErrorHandler.scala rename to modules/common/src/main/scala/taskforce/common/ErrorHandler.scala diff --git a/common/src/main/scala/taskforce/common/ErrorMessage.scala b/modules/common/src/main/scala/taskforce/common/ErrorMessage.scala similarity index 100% rename from common/src/main/scala/taskforce/common/ErrorMessage.scala rename to modules/common/src/main/scala/taskforce/common/ErrorMessage.scala diff --git a/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala b/modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala similarity index 100% rename from common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala rename to modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala diff --git a/common/src/main/scala/taskforce/common/Sqlizer.scala b/modules/common/src/main/scala/taskforce/common/Sqlizer.scala similarity index 100% rename from common/src/main/scala/taskforce/common/Sqlizer.scala rename to modules/common/src/main/scala/taskforce/common/Sqlizer.scala diff --git a/common/src/main/scala/taskforce/common/errors.scala b/modules/common/src/main/scala/taskforce/common/errors.scala similarity index 100% rename from common/src/main/scala/taskforce/common/errors.scala rename to modules/common/src/main/scala/taskforce/common/errors.scala diff --git a/common/src/main/scala/taskforce/common/package.scala b/modules/common/src/main/scala/taskforce/common/package.scala similarity index 100% rename from common/src/main/scala/taskforce/common/package.scala rename to modules/common/src/main/scala/taskforce/common/package.scala diff --git a/common/src/main/scala/taskforce/config/DatabaseConfig.scala b/modules/common/src/main/scala/taskforce/config/DatabaseConfig.scala similarity index 100% rename from common/src/main/scala/taskforce/config/DatabaseConfig.scala rename to modules/common/src/main/scala/taskforce/config/DatabaseConfig.scala diff --git a/common/src/main/scala/taskforce/config/HostConfig.scala b/modules/common/src/main/scala/taskforce/config/HostConfig.scala similarity index 100% rename from common/src/main/scala/taskforce/config/HostConfig.scala rename to modules/common/src/main/scala/taskforce/config/HostConfig.scala diff --git a/common/src/test/scala/taskforce/http4stest.scala b/modules/common/src/test/scala/taskforce/http4stest.scala similarity index 100% rename from common/src/test/scala/taskforce/http4stest.scala rename to modules/common/src/test/scala/taskforce/http4stest.scala diff --git a/filtersFeature/src/it/scala/taskforce/filter/FilterRepositorySuite.scala b/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala similarity index 100% rename from filtersFeature/src/it/scala/taskforce/filter/FilterRepositorySuite.scala rename to modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/Filter.scala b/modules/filters/src/main/scala/taskforce/filter/Filter.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/Filter.scala rename to modules/filters/src/main/scala/taskforce/filter/Filter.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/FilterRepository.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/FilterRepository.scala rename to modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/FilterRoutes.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/FilterRoutes.scala rename to modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/FilterService.scala b/modules/filters/src/main/scala/taskforce/filter/FilterService.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/FilterService.scala rename to modules/filters/src/main/scala/taskforce/filter/FilterService.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/QueryParams.scala b/modules/filters/src/main/scala/taskforce/filter/QueryParams.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/QueryParams.scala rename to modules/filters/src/main/scala/taskforce/filter/QueryParams.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/instances/Circe.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/instances/Circe.scala rename to modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/instances/Doobie.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/instances/Doobie.scala rename to modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/instances/Http4s.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Http4s.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/instances/Http4s.scala rename to modules/filters/src/main/scala/taskforce/filter/instances/Http4s.scala diff --git a/filtersFeature/src/main/scala/taskforce/filter/instances/helpers.scala b/modules/filters/src/main/scala/taskforce/filter/instances/helpers.scala similarity index 100% rename from filtersFeature/src/main/scala/taskforce/filter/instances/helpers.scala rename to modules/filters/src/main/scala/taskforce/filter/instances/helpers.scala diff --git a/filtersFeature/src/test/scala/taskforce/filter/FilterRoutesSuite.scala b/modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala similarity index 100% rename from filtersFeature/src/test/scala/taskforce/filter/FilterRoutesSuite.scala rename to modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala diff --git a/filtersFeature/src/test/scala/taskforce/filter/TestFilterRepository.scala b/modules/filters/src/test/scala/taskforce/filter/TestFilterRepository.scala similarity index 100% rename from filtersFeature/src/test/scala/taskforce/filter/TestFilterRepository.scala rename to modules/filters/src/test/scala/taskforce/filter/TestFilterRepository.scala diff --git a/filtersFeature/src/test/scala/taskforce/filter/arbitraries.scala b/modules/filters/src/test/scala/taskforce/filter/arbitraries.scala similarity index 100% rename from filtersFeature/src/test/scala/taskforce/filter/arbitraries.scala rename to modules/filters/src/test/scala/taskforce/filter/arbitraries.scala diff --git a/filtersFeature/src/test/scala/taskforce/filter/generators.scala b/modules/filters/src/test/scala/taskforce/filter/generators.scala similarity index 100% rename from filtersFeature/src/test/scala/taskforce/filter/generators.scala rename to modules/filters/src/test/scala/taskforce/filter/generators.scala diff --git a/projectsFeature/src/it/scala/taskforce/project/ProjectRepositorySuite.scala b/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala similarity index 100% rename from projectsFeature/src/it/scala/taskforce/project/ProjectRepositorySuite.scala rename to modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/Project.scala b/modules/projects/src/main/scala/taskforce/project/Project.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/Project.scala rename to modules/projects/src/main/scala/taskforce/project/Project.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/ProjectError.scala b/modules/projects/src/main/scala/taskforce/project/ProjectError.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/ProjectError.scala rename to modules/projects/src/main/scala/taskforce/project/ProjectError.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/ProjectRepository.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/ProjectRepository.scala rename to modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/ProjectRoutes.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/ProjectRoutes.scala rename to modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/ProjectService.scala b/modules/projects/src/main/scala/taskforce/project/ProjectService.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/ProjectService.scala rename to modules/projects/src/main/scala/taskforce/project/ProjectService.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/instances/Doobie.scala b/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/instances/Doobie.scala rename to modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/instances/Http4s.scala b/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/instances/Http4s.scala rename to modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala diff --git a/projectsFeature/src/main/scala/taskforce/project/package.scala b/modules/projects/src/main/scala/taskforce/project/package.scala similarity index 100% rename from projectsFeature/src/main/scala/taskforce/project/package.scala rename to modules/projects/src/main/scala/taskforce/project/package.scala diff --git a/projectsFeature/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala similarity index 100% rename from projectsFeature/src/test/scala/taskforce/project/ProjectRoutesSuite.scala rename to modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala diff --git a/projectsFeature/src/test/scala/taskforce/project/TestProjectRepository.scala b/modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala similarity index 100% rename from projectsFeature/src/test/scala/taskforce/project/TestProjectRepository.scala rename to modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala diff --git a/projectsFeature/src/test/scala/taskforce/project/arbitraries.scala b/modules/projects/src/test/scala/taskforce/project/arbitraries.scala similarity index 100% rename from projectsFeature/src/test/scala/taskforce/project/arbitraries.scala rename to modules/projects/src/test/scala/taskforce/project/arbitraries.scala diff --git a/projectsFeature/src/test/scala/taskforce/project/generators.scala b/modules/projects/src/test/scala/taskforce/project/generators.scala similarity index 100% rename from projectsFeature/src/test/scala/taskforce/project/generators.scala rename to modules/projects/src/test/scala/taskforce/project/generators.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/Stats.scala b/modules/stats/src/main/scala/taskforce/stats/Stats.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/Stats.scala rename to modules/stats/src/main/scala/taskforce/stats/Stats.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/StatsRepository.scala b/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/StatsRepository.scala rename to modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/StatsRoutes.scala b/modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/StatsRoutes.scala rename to modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/StatsService.scala b/modules/stats/src/main/scala/taskforce/stats/StatsService.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/StatsService.scala rename to modules/stats/src/main/scala/taskforce/stats/StatsService.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/instances/Circe.scala b/modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/instances/Circe.scala rename to modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala diff --git a/statsFeature/src/main/scala/taskforce/stats/instances/Doobie.scala b/modules/stats/src/main/scala/taskforce/stats/instances/Doobie.scala similarity index 100% rename from statsFeature/src/main/scala/taskforce/stats/instances/Doobie.scala rename to modules/stats/src/main/scala/taskforce/stats/instances/Doobie.scala diff --git a/tasksFeature/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala similarity index 100% rename from tasksFeature/src/it/scala/taskforce/task/TaskRepositorySuite.scala rename to modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/Task.scala b/modules/tasks/src/main/scala/taskforce/task/Task.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/Task.scala rename to modules/tasks/src/main/scala/taskforce/task/Task.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/TaskError.scala b/modules/tasks/src/main/scala/taskforce/task/TaskError.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/TaskError.scala rename to modules/tasks/src/main/scala/taskforce/task/TaskError.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/TaskRepository.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/TaskRepository.scala rename to modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/TaskRoutes.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/TaskRoutes.scala rename to modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/TaskService.scala b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/TaskService.scala rename to modules/tasks/src/main/scala/taskforce/task/TaskService.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/instances/Doobie.scala b/modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/instances/Doobie.scala rename to modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala diff --git a/tasksFeature/src/main/scala/taskforce/task/package.scala b/modules/tasks/src/main/scala/taskforce/task/package.scala similarity index 100% rename from tasksFeature/src/main/scala/taskforce/task/package.scala rename to modules/tasks/src/main/scala/taskforce/task/package.scala diff --git a/tasksFeature/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala similarity index 100% rename from tasksFeature/src/test/scala/taskforce/task/TaskRoutesSuite.scala rename to modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala diff --git a/tasksFeature/src/test/scala/taskforce/task/TestTaskRepository.scala b/modules/tasks/src/test/scala/taskforce/task/TestTaskRepository.scala similarity index 100% rename from tasksFeature/src/test/scala/taskforce/task/TestTaskRepository.scala rename to modules/tasks/src/test/scala/taskforce/task/TestTaskRepository.scala diff --git a/tasksFeature/src/test/scala/taskforce/task/arbitraries.scala b/modules/tasks/src/test/scala/taskforce/task/arbitraries.scala similarity index 100% rename from tasksFeature/src/test/scala/taskforce/task/arbitraries.scala rename to modules/tasks/src/test/scala/taskforce/task/arbitraries.scala diff --git a/tasksFeature/src/test/scala/taskforce/task/generators.scala b/modules/tasks/src/test/scala/taskforce/task/generators.scala similarity index 100% rename from tasksFeature/src/test/scala/taskforce/task/generators.scala rename to modules/tasks/src/test/scala/taskforce/task/generators.scala From 26f7bce7591ff5cee3bc2a0be123826ccad04954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Mon, 25 Jul 2022 20:10:01 +0200 Subject: [PATCH 12/24] fixing projects module --- .../scala/taskforce/project/Project.scala | 5 +--- .../taskforce/project/instances/Circe.scala | 15 +++++++++++ .../taskforce/project/instances/Doobie.scala | 4 ++- .../taskforce/project/instances/Http4s.scala | 5 +--- .../scala/taskforce/project/package.scala | 27 +++---------------- .../project/ProjectRoutesSuite.scala | 3 ++- project/metals.sbt | 2 +- project/project/metals.sbt | 2 +- 8 files changed, 28 insertions(+), 35 deletions(-) create mode 100644 modules/projects/src/main/scala/taskforce/project/instances/Circe.scala diff --git a/modules/projects/src/main/scala/taskforce/project/Project.scala b/modules/projects/src/main/scala/taskforce/project/Project.scala index 0ea81c5..fb2a8cc 100644 --- a/modules/projects/src/main/scala/taskforce/project/Project.scala +++ b/modules/projects/src/main/scala/taskforce/project/Project.scala @@ -5,12 +5,9 @@ import taskforce.authentication.UserId import taskforce.common.CreationDate import taskforce.common.DeletionDate -import io.circe.refined._ -import io.circe.generic.JsonCodec - -@JsonCodec final case class Project( +final case class Project( id: ProjectId, name: ProjectName, author: UserId, diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala b/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala new file mode 100644 index 0000000..49639f1 --- /dev/null +++ b/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala @@ -0,0 +1,15 @@ +package taskforce.project.instances + +import io.circe._, io.circe.generic.semiauto._ +import taskforce.project.Project + +import io.circe.refined._ +import monix.newtypes.integrations.DerivedCirceCodec + + +trait Circe extends DerivedCirceCodec{ + + implicit val projectIdDecoder: Decoder[Project] = deriveDecoder + implicit val projectIdEncoder: Encoder[Project] = deriveEncoder + +} diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala b/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala index 6618f5a..ed7c1e8 100644 --- a/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala +++ b/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala @@ -7,9 +7,11 @@ import io.getquill.MappedEncoding import taskforce.project.TotalTime import java.time.Duration +import taskforce.common.NewTypeDoobieMeta +import taskforce.common.NewTypeQuillInstances -trait Doobie { +trait Doobie extends NewTypeDoobieMeta with NewTypeQuillInstances{ implicit val totalTimeMeta: Meta[TotalTime] = Meta[Long].imap(x => TotalTime(Duration.ofMinutes(x)))(x => diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala b/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala index a4e7999..e7efc0f 100644 --- a/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala +++ b/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala @@ -5,10 +5,7 @@ import org.http4s.circe._ import taskforce.project.{Project, TotalTime} - - - -trait Http4s[F[_]] { +trait Http4s[F[_]] extends Circe { implicit val totalTimeEntityEncoder: EntityEncoder[F, TotalTime] = jsonEncoderOf[F, TotalTime] implicit val projectEntityEncoder: EntityEncoder[F, Project] = jsonEncoderOf[F, Project] diff --git a/modules/projects/src/main/scala/taskforce/project/package.scala b/modules/projects/src/main/scala/taskforce/project/package.scala index 734234d..1ea85ae 100644 --- a/modules/projects/src/main/scala/taskforce/project/package.scala +++ b/modules/projects/src/main/scala/taskforce/project/package.scala @@ -1,37 +1,18 @@ package taskforce -import monix.newtypes.integrations.DerivedCirceCodec +//import monix.newtypes.integrations.DerivedCirceCodec import monix.newtypes.NewtypeWrapped import java.time.Duration -import taskforce.common.NewTypeDoobieMeta -import taskforce.common.NewTypeQuillInstances import eu.timepit.refined.types.string.NonEmptyString - - - - package object project { type ProjectId = ProjectId.Type - object ProjectId - extends NewtypeWrapped[Long] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + object ProjectId extends NewtypeWrapped[Long] type ProjectName = ProjectName.Type - object ProjectName - extends NewtypeWrapped[NonEmptyString] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances - + object ProjectName extends NewtypeWrapped[NonEmptyString] type TotalTime = TotalTime.Type - object TotalTime - extends NewtypeWrapped[Duration] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + object TotalTime extends NewtypeWrapped[Duration] } diff --git a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index 37475d4..ff717bf 100644 --- a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -18,8 +18,9 @@ import taskforce.HttpTestSuite import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.project.ProjectName +import taskforce.project.instances.Circe -class ProjectRoutesSuite extends HttpTestSuite { +class ProjectRoutesSuite extends HttpTestSuite with Circe { import arbitraries._ diff --git a/project/metals.sbt b/project/metals.sbt index a13ea3b..ef0d4c5 100644 --- a/project/metals.sbt +++ b/project/metals.sbt @@ -2,5 +2,5 @@ // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.2") diff --git a/project/project/metals.sbt b/project/project/metals.sbt index a13ea3b..ef0d4c5 100644 --- a/project/project/metals.sbt +++ b/project/project/metals.sbt @@ -2,5 +2,5 @@ // This file enables sbt-bloop to create bloop config files. -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.2") From 5603b08751b425e73c3dea10711316c202342597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Mon, 25 Jul 2022 20:38:05 +0200 Subject: [PATCH 13/24] authentication stats tasks projects fixed --- .../TaskForceAuthMiddleware.scala | 3 +- .../scala/taskforce/authentication/User.scala | 3 +- .../authentication/instances/Circe.scala | 14 ++++---- .../taskforce/authentication/package.scala | 6 ++-- .../taskforce/stats/instances/Circe.scala | 3 +- .../src/main/scala/taskforce/task/Task.scala | 6 ++-- .../scala/taskforce/task/TaskRoutes.scala | 2 +- .../taskforce/task/instances/Circe.scala | 29 ++++++++++++++++ .../taskforce/task/instances/Doobie.scala | 15 ++++----- .../taskforce/task/instances/Http4s.scala | 13 ++++++++ .../main/scala/taskforce/task/package.scala | 33 ++++--------------- .../taskforce/task/TaskRoutesSuite.scala | 5 +-- 12 files changed, 74 insertions(+), 58 deletions(-) create mode 100644 modules/tasks/src/main/scala/taskforce/task/instances/Circe.scala create mode 100644 modules/tasks/src/main/scala/taskforce/task/instances/Http4s.scala diff --git a/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala b/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala index 22edb94..0a8b7d0 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala @@ -9,8 +9,9 @@ import org.http4s.server.AuthMiddleware import org.http4s.{AuthScheme, AuthedRoutes, Credentials, Request} import pdi.jwt.{JwtAlgorithm, JwtCirce} import cats.MonadThrow +import taskforce.authentication.instances.Circe -object TaskForceAuthMiddleware { +object TaskForceAuthMiddleware extends Circe { def apply[F[_]: MonadThrow]( userRepo: UserRepository[F], diff --git a/modules/auth/src/main/scala/taskforce/authentication/User.scala b/modules/auth/src/main/scala/taskforce/authentication/User.scala index 637bdec..0476289 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/User.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/User.scala @@ -1,5 +1,4 @@ package taskforce.authentication -import io.circe.generic.JsonCodec -@JsonCodec final case class User(id: UserId) +final case class User(id: UserId) diff --git a/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala b/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala index 78f8efe..697cb8e 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala @@ -2,14 +2,14 @@ package taskforce.authentication.instances import io.circe.generic.semiauto._ import io.circe.{Decoder, Encoder} -import java.util.UUID -import taskforce.authentication.{User, UserId} -trait Circe { +import taskforce.authentication.User +import monix.newtypes.integrations.DerivedCirceCodec +trait Circe extends DerivedCirceCodec{ - implicit val userIdDecoder: Decoder[UserId] = - Decoder[UUID].map(UserId.apply) - implicit val userIdEncoder: Encoder[UserId] = - Encoder[UUID].contramap(_.value) + // implicit val userIdDecoder: Decoder[UserId] = + // Decoder[UUID].map(UserId.apply) + // implicit val userIdEncoder: Encoder[UserId] = + // Encoder[UUID].contramap(_.value) implicit val userDecoder: Decoder[User] = deriveDecoder[User] diff --git a/modules/auth/src/main/scala/taskforce/authentication/package.scala b/modules/auth/src/main/scala/taskforce/authentication/package.scala index dca2d57..175e505 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/package.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/package.scala @@ -2,11 +2,9 @@ package taskforce import java.util.UUID import monix.newtypes._ -import monix.newtypes.integrations.DerivedCirceCodec -import taskforce.common.NewTypeDoobieMeta -import taskforce.common.NewTypeQuillInstances + package object authentication { type UserId = UserId.Type - object UserId extends NewtypeWrapped[UUID] with DerivedCirceCodec with NewTypeDoobieMeta with NewTypeQuillInstances + object UserId extends NewtypeWrapped[UUID] } diff --git a/modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala b/modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala index a0389c4..f6f088a 100644 --- a/modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala +++ b/modules/stats/src/main/scala/taskforce/stats/instances/Circe.scala @@ -6,8 +6,9 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter import taskforce.authentication.UserId import taskforce.stats.{StatsResponse, StatsQuery} +import monix.newtypes.integrations.DerivedCirceCodec -trait Circe { +trait Circe extends DerivedCirceCodec { private val toDateFmt = DateTimeFormatter.ofPattern("yyyy.MM.dd") private val fromDateFmt = DateTimeFormatter.ofPattern("yyyy.MM") diff --git a/modules/tasks/src/main/scala/taskforce/task/Task.scala b/modules/tasks/src/main/scala/taskforce/task/Task.scala index e5b390f..fa87560 100644 --- a/modules/tasks/src/main/scala/taskforce/task/Task.scala +++ b/modules/tasks/src/main/scala/taskforce/task/Task.scala @@ -4,19 +4,17 @@ import java.time.LocalDateTime import java.util.UUID import taskforce.authentication.UserId import taskforce.common._ -import io.circe.refined._ -import io.circe.generic.JsonCodec import taskforce.task.TaskVolume import taskforce.task.TaskComment -@JsonCodec final case class NewTask( +final case class NewTask( created: Option[CreationDate] = None, duration: TaskDuration, volume: Option[TaskVolume], comment: Option[TaskComment] ) -@JsonCodec final case class Task( +final case class Task( id: TaskId, projectId: ProjectId, author: UserId, diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala index 267a864..bbc772c 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala @@ -15,7 +15,7 @@ import org.http4s.Response final class TaskRoutes[F[_]: Sync: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], taskService: TaskService[F] -) { +) extends instances.Http4s[F] { private[this] val prefixPath = "/api/v1/projects" diff --git a/modules/tasks/src/main/scala/taskforce/task/instances/Circe.scala b/modules/tasks/src/main/scala/taskforce/task/instances/Circe.scala new file mode 100644 index 0000000..6b8a92f --- /dev/null +++ b/modules/tasks/src/main/scala/taskforce/task/instances/Circe.scala @@ -0,0 +1,29 @@ +package taskforce.task.instances + +import io.circe._, io.circe.generic.semiauto._ +import io.circe.refined._ +import monix.newtypes.integrations.DerivedCirceCodec +import taskforce.task.Task +import taskforce.task.NewTask +import taskforce.task.TaskDuration +import java.time.Duration + +trait Circe extends DerivedCirceCodec { + + implicit val taskDecoder: Decoder[Task] = + deriveDecoder + implicit val taskEncoder: Encoder[Task] = + deriveEncoder + + implicit val newTaskDecoder: Decoder[NewTask] = + deriveDecoder + implicit val newTaskEncoder: Encoder[NewTask] = + deriveEncoder + + implicit val taskDurationEncoder: Encoder[TaskDuration] = + Encoder[Long].contramap(_.value.toMinutes()) + + implicit val taskDurationDecoder: Decoder[TaskDuration] = + Decoder[Long].map(x => TaskDuration(Duration.ofMinutes(x))) + +} diff --git a/modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala b/modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala index 9923d3b..430e853 100644 --- a/modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala +++ b/modules/tasks/src/main/scala/taskforce/task/instances/Doobie.scala @@ -5,18 +5,15 @@ import taskforce.task.TaskDuration import java.time.Duration import io.getquill.MappedEncoding import taskforce.common.NewTypeQuillInstances +import taskforce.common.NewTypeDoobieMeta + +trait Doobie extends NewTypeDoobieMeta with NewTypeQuillInstances { -trait Doobie extends NewTypeQuillInstances { - implicit val taskDurationMeta: Meta[TaskDuration] = - Meta[Long].imap(x => TaskDuration(Duration.ofMinutes(x)))(x => - x.value.toMinutes - ) + Meta[Long].imap(x => TaskDuration(Duration.ofMinutes(x)))(x => x.value.toMinutes) - implicit val decodeTaskDuration: MappedEncoding[Long, TaskDuration] = - MappedEncoding[Long, TaskDuration](long => - TaskDuration(Duration.ofMinutes(long)) - ) + implicit val decodeTaskDuration: MappedEncoding[Long, TaskDuration] = + MappedEncoding[Long, TaskDuration](long => TaskDuration(Duration.ofMinutes(long))) implicit val encodeTimeDuration: MappedEncoding[TaskDuration, Long] = MappedEncoding[TaskDuration, Long](_.value.toMinutes) diff --git a/modules/tasks/src/main/scala/taskforce/task/instances/Http4s.scala b/modules/tasks/src/main/scala/taskforce/task/instances/Http4s.scala new file mode 100644 index 0000000..11008ba --- /dev/null +++ b/modules/tasks/src/main/scala/taskforce/task/instances/Http4s.scala @@ -0,0 +1,13 @@ +package taskforce.task.instances + +import org.http4s.EntityEncoder +import org.http4s.circe._ +import taskforce.task.Task +import taskforce.task.NewTask + +trait Http4s[F[_]] extends Circe { + + implicit val taskEntityEncoder: EntityEncoder[F, Task] = jsonEncoderOf[F, Task] + implicit val newTaskEntityEncoder: EntityEncoder[F, NewTask] = jsonEncoderOf[F, NewTask] + +} diff --git a/modules/tasks/src/main/scala/taskforce/task/package.scala b/modules/tasks/src/main/scala/taskforce/task/package.scala index bb6e631..2c873bb 100644 --- a/modules/tasks/src/main/scala/taskforce/task/package.scala +++ b/modules/tasks/src/main/scala/taskforce/task/package.scala @@ -1,13 +1,10 @@ package taskforce -import monix.newtypes.integrations.DerivedCirceCodec + import monix.newtypes.NewtypeWrapped -import taskforce.common.NewTypeDoobieMeta -import taskforce.common.NewTypeQuillInstances import eu.timepit.refined.types.numeric.PosInt import java.util.UUID import java.time.Duration -import io.circe.{Encoder => JsonEncoder,Decoder => JsonDecoder} import eu.timepit.refined.types.string.NonEmptyString package object task { @@ -15,43 +12,25 @@ package object task { type ProjectId = ProjectId.Type object ProjectId extends NewtypeWrapped[Long] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + type TaskId = TaskId.Type object TaskId extends NewtypeWrapped[UUID] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + type TaskDuration = TaskDuration.Type object TaskDuration extends NewtypeWrapped[Duration] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + type TaskVolume = TaskVolume.Type object TaskVolume extends NewtypeWrapped[PosInt] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances - + type TaskComment = TaskComment.Type object TaskComment extends NewtypeWrapped[NonEmptyString] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances - - - implicit val taskDurationEncoder: JsonEncoder[TaskDuration] = - JsonEncoder[Long].contramap(_.value.toMinutes()) - - implicit val taskDurationDecoder: JsonDecoder[TaskDuration] = - JsonDecoder[Long].map(x => TaskDuration(Duration.ofMinutes(x))) + } diff --git a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala index a023e77..738c721 100644 --- a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -17,10 +17,11 @@ import arbitraries._ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.common.CreationDate +import taskforce.task.instances.Circe -class TasksRoutesSuite extends HttpTestSuite { +class TasksRoutesSuite extends HttpTestSuite with Circe{ - implicit def encodeNewProduct: EntityEncoder[IO, NewTask] = jsonEncoderOf + implicit def encodeNewTask: EntityEncoder[IO, NewTask] = jsonEncoderOf def authMiddleware: AuthMiddleware[IO, UserId] = AuthMiddleware(Kleisli.pure(UserId(UUID.randomUUID()))) From e5a91857167eb83ac89e0c616c772686d4a7270f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Mon, 25 Jul 2022 21:19:56 +0200 Subject: [PATCH 14/24] formatting + common project fix --- .../TaskForceAuthMiddleware.scala | 6 ++--- .../scala/taskforce/authentication/User.scala | 1 - .../authentication/instances/Circe.scala | 7 +----- .../taskforce/authentication/package.scala | 3 +-- .../common/NewTypeDoobieMetaInstance.scala | 2 +- .../main/scala/taskforce/common/errors.scala | 4 ++-- .../main/scala/taskforce/common/package.scala | 10 +-------- .../main/scala/taskforce/filter/Filter.scala | 17 +++++++------- .../taskforce/filter/FilterRepository.scala | 12 +++++----- .../taskforce/filter/instances/Circe.scala | 2 +- .../project/ProjectRepositorySuite.scala | 2 +- .../scala/taskforce/project/Project.scala | 2 -- .../taskforce/project/ProjectRoutes.scala | 2 +- .../taskforce/project/instances/Circe.scala | 7 +++--- .../taskforce/project/instances/Doobie.scala | 19 +++++++--------- .../taskforce/project/instances/Http4s.scala | 3 +-- .../project/ProjectRoutesSuite.scala | 1 - .../scala/taskforce/project/arbitraries.scala | 1 - .../scala/taskforce/project/generators.scala | 7 +++--- .../taskforce/task/TaskRepositorySuite.scala | 1 - .../scala/taskforce/task/TaskRepository.scala | 4 +--- .../scala/taskforce/task/TaskService.scala | 10 ++++----- .../main/scala/taskforce/task/package.scala | 22 +++++-------------- .../taskforce/task/TaskRoutesSuite.scala | 2 +- .../scala/taskforce/task/generators.scala | 20 ++++++++--------- 25 files changed, 63 insertions(+), 104 deletions(-) diff --git a/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala b/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala index 0a8b7d0..771ad52 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/TaskForceAuthMiddleware.scala @@ -13,7 +13,7 @@ import taskforce.authentication.instances.Circe object TaskForceAuthMiddleware extends Circe { - def apply[F[_]: MonadThrow]( + def apply[F[_]: MonadThrow]( userRepo: UserRepository[F], secret: String ): AuthMiddleware[F, UserId] = { @@ -29,11 +29,11 @@ object TaskForceAuthMiddleware extends Circe { encodedString <- request.headers .get[Authorization] - .fold(s"missing Authorization header: ${request}".asLeft[String]){ + .fold(s"missing Authorization header: ${request}".asLeft[String]) { case Authorization(Credentials.Token(AuthScheme.Bearer, t)) => t.asRight[String] case header => s"invalid header type $header".asLeft[String] - } + } jwtClaim <- JwtCirce .decode(encodedString, secret, Seq(JwtAlgorithm.HS256)) diff --git a/modules/auth/src/main/scala/taskforce/authentication/User.scala b/modules/auth/src/main/scala/taskforce/authentication/User.scala index 0476289..19ed175 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/User.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/User.scala @@ -1,4 +1,3 @@ package taskforce.authentication - final case class User(id: UserId) diff --git a/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala b/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala index 697cb8e..21c6f31 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/instances/Circe.scala @@ -4,12 +4,7 @@ import io.circe.generic.semiauto._ import io.circe.{Decoder, Encoder} import taskforce.authentication.User import monix.newtypes.integrations.DerivedCirceCodec -trait Circe extends DerivedCirceCodec{ - - // implicit val userIdDecoder: Decoder[UserId] = - // Decoder[UUID].map(UserId.apply) - // implicit val userIdEncoder: Encoder[UserId] = - // Encoder[UUID].contramap(_.value) +trait Circe extends DerivedCirceCodec { implicit val userDecoder: Decoder[User] = deriveDecoder[User] diff --git a/modules/auth/src/main/scala/taskforce/authentication/package.scala b/modules/auth/src/main/scala/taskforce/authentication/package.scala index 175e505..c270a29 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/package.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/package.scala @@ -3,8 +3,7 @@ package taskforce import java.util.UUID import monix.newtypes._ - package object authentication { type UserId = UserId.Type - object UserId extends NewtypeWrapped[UUID] + object UserId extends NewtypeWrapped[UUID] } diff --git a/modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala b/modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala index 59054fb..0f7b55f 100644 --- a/modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala +++ b/modules/common/src/main/scala/taskforce/common/NewTypeDoobieMetaInstance.scala @@ -8,7 +8,7 @@ import doobie.util.Get import cats.Show import io.getquill.MappedEncoding -trait NewTypeDoobieMeta extends NewTypeDoobiePut with NewTypeDoobieGet +trait NewTypeDoobieMeta extends NewTypeDoobiePut with NewTypeDoobieGet trait NewTypeDoobiePut { implicit def putInstance[T, S](implicit diff --git a/modules/common/src/main/scala/taskforce/common/errors.scala b/modules/common/src/main/scala/taskforce/common/errors.scala index bdedb85..811162b 100644 --- a/modules/common/src/main/scala/taskforce/common/errors.scala +++ b/modules/common/src/main/scala/taskforce/common/errors.scala @@ -5,8 +5,8 @@ import java.util.UUID object errors { - case class NotAuthor(userId: UUID) extends NoStackTrace - case object BadRequest extends NoStackTrace + case class NotAuthor(userId: UUID) extends NoStackTrace + case object BadRequest extends NoStackTrace case class NotFound(resourceId: String) extends NoStackTrace case class InvalidQueryParam(s: String) extends NoStackTrace diff --git a/modules/common/src/main/scala/taskforce/common/package.scala b/modules/common/src/main/scala/taskforce/common/package.scala index 7037e49..85b9d9b 100644 --- a/modules/common/src/main/scala/taskforce/common/package.scala +++ b/modules/common/src/main/scala/taskforce/common/package.scala @@ -1,24 +1,16 @@ package taskforce -import monix.newtypes.integrations.DerivedCirceCodec import monix.newtypes.NewtypeWrapped import java.time.LocalDateTime package object common { - - type CreationDate = CreationDate.Type object CreationDate extends NewtypeWrapped[LocalDateTime] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances + type DeletionDate = DeletionDate.Type object DeletionDate extends NewtypeWrapped[LocalDateTime] - with DerivedCirceCodec - with NewTypeDoobieMeta - with NewTypeQuillInstances } diff --git a/modules/filters/src/main/scala/taskforce/filter/Filter.scala b/modules/filters/src/main/scala/taskforce/filter/Filter.scala index 78b6c18..387df65 100644 --- a/modules/filters/src/main/scala/taskforce/filter/Filter.scala +++ b/modules/filters/src/main/scala/taskforce/filter/Filter.scala @@ -8,24 +8,23 @@ import taskforce.task.Task sealed trait Operator -final case object Eq extends Operator -final case object Lt extends Operator -final case object Gt extends Operator +final case object Eq extends Operator +final case object Lt extends Operator +final case object Gt extends Operator final case object Lteq extends Operator final case object Gteq extends Operator sealed trait Status -final case object Active extends Status +final case object Active extends Status final case object Deactive extends Status -final case object All extends Status +final case object All extends Status sealed trait Criteria extends Product with Serializable -final case class In(names: List[NonEmptyString]) extends Criteria -final case class TaskCreatedDate(op: Operator, date: LocalDateTime) - extends Criteria -final case class State(status: Status) extends Criteria +final case class In(names: List[NonEmptyString]) extends Criteria +final case class TaskCreatedDate(op: Operator, date: LocalDateTime) extends Criteria +final case class State(status: Status) extends Criteria final case class FilterId(value: UUID) extends AnyVal final case class NewFilter(conditions: List[Criteria]) diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala index 488b660..ee490a5 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala @@ -20,7 +20,6 @@ import eu.timepit.refined.cats._ import org.typelevel.log4cats.Logger import cats.Show - trait FilterRepository[F[_]] { def create(filter: Filter): F[Filter] def delete(id: FilterId): F[Int] @@ -36,11 +35,10 @@ trait FilterRepository[F[_]] { final class LiveFilterRepository[F[_]: MonadCancel[*[_], Throwable]: Logger]( xa: Transactor[F] ) extends FilterRepository[F] - with instances.Doobie - { + with instances.Doobie { implicit val showInstance: Show[LocalDateTime] = - Show.fromToString[LocalDateTime] + Show.fromToString[LocalDateTime] private val tuple2Condition: PartialFunction[ ( @@ -91,14 +89,14 @@ final class LiveFilterRepository[F[_]: MonadCancel[*[_], Throwable]: Logger]( fragments.whereAnd(filter.conditions.map(_.toFragment): _*) val orderClause = sortByOption.fold(Fragment.empty)(_.toFragment) val limitClause = page.toFragment - val sqlQuery = sql.getData ++ whereClause ++ orderClause ++ limitClause + val sqlQuery = sql.getData ++ whereClause ++ orderClause ++ limitClause Stream.eval[F, Unit](Logger[F].info("test")) >> sqlQuery .query[(Project, Option[Task])] - //.query[CreationDate] + // .query[CreationDate] .stream - // .as( null : FilterResultRow) + // .as( null : FilterResultRow) .transact(xa) .map(x => FilterResultRow.fromTuple(x)) } diff --git a/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala index 4feabb2..13e29f7 100644 --- a/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala +++ b/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala @@ -8,7 +8,7 @@ import java.time.LocalDateTime import java.util.UUID import taskforce.filter._ -trait Circe { +trait Circe { implicit lazy val encodeCriteria: Encoder[Criteria] = Encoder.instance { case in @ In(_) => in.asJson diff --git a/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala b/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala index 477be12..268d1e9 100644 --- a/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala +++ b/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala @@ -10,7 +10,7 @@ import taskforce.authentication.UserId class ProjectRepositorySuite extends BasicRepositorySuite { var projectRepo: IO[ProjectRepository[IO]] = null - val userID = UserId(userIdUUID) + val userID = UserId(userIdUUID) override def beforeAll(): Unit = { super.beforeAll() diff --git a/modules/projects/src/main/scala/taskforce/project/Project.scala b/modules/projects/src/main/scala/taskforce/project/Project.scala index fb2a8cc..c9e97e2 100644 --- a/modules/projects/src/main/scala/taskforce/project/Project.scala +++ b/modules/projects/src/main/scala/taskforce/project/Project.scala @@ -1,12 +1,10 @@ package taskforce.project - import taskforce.authentication.UserId import taskforce.common.CreationDate import taskforce.common.DeletionDate - final case class Project( id: ProjectId, name: ProjectName, diff --git a/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala index ca949e6..a9f89b9 100644 --- a/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala +++ b/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala @@ -27,7 +27,7 @@ final class ProjectRoutes[F[_]: Sync: JsonDecoder]( ) } - def newProjectFromReq(authReq: AuthedRequest[F, UserId]):F[ProjectName] = + def newProjectFromReq(authReq: AuthedRequest[F, UserId]): F[ProjectName] = authReq.req .asJsonDecode[ProjectName] .adaptError(_ => commonErrors.BadRequest) diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala b/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala index 49639f1..e14e058 100644 --- a/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala +++ b/modules/projects/src/main/scala/taskforce/project/instances/Circe.scala @@ -6,10 +6,9 @@ import taskforce.project.Project import io.circe.refined._ import monix.newtypes.integrations.DerivedCirceCodec +trait Circe extends DerivedCirceCodec { -trait Circe extends DerivedCirceCodec{ - - implicit val projectIdDecoder: Decoder[Project] = deriveDecoder - implicit val projectIdEncoder: Encoder[Project] = deriveEncoder + implicit val projectIdDecoder: Decoder[Project] = deriveDecoder + implicit val projectIdEncoder: Encoder[Project] = deriveEncoder } diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala b/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala index ed7c1e8..21df672 100644 --- a/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala +++ b/modules/projects/src/main/scala/taskforce/project/instances/Doobie.scala @@ -10,24 +10,21 @@ import java.time.Duration import taskforce.common.NewTypeDoobieMeta import taskforce.common.NewTypeQuillInstances - -trait Doobie extends NewTypeDoobieMeta with NewTypeQuillInstances{ +trait Doobie extends NewTypeDoobieMeta with NewTypeQuillInstances { implicit val totalTimeMeta: Meta[TotalTime] = - Meta[Long].imap(x => TotalTime(Duration.ofMinutes(x)))(x => - x.value.toMinutes - ) + Meta[Long].imap(x => TotalTime(Duration.ofMinutes(x)))(x => x.value.toMinutes) implicit val decodeTotalTime: MappedEncoding[Long, TotalTime] = - MappedEncoding[Long, TotalTime](long => - TotalTime(Duration.ofMinutes(long)) - ) + MappedEncoding[Long, TotalTime](long => TotalTime(Duration.ofMinutes(long))) implicit val encodeTotalTime: MappedEncoding[TotalTime, Long] = MappedEncoding[TotalTime, Long](_.value.toMinutes) - implicit val decodeNonEmptyString: MappedEncoding[String, string.NonEmptyString] = MappedEncoding[String, string.NonEmptyString](Refined.unsafeApply) - implicit val encodeNonEmptyString: MappedEncoding[string.NonEmptyString, String] = MappedEncoding[string.NonEmptyString, String](_.value) - implicit val taskDurationNumeric: Numeric[TotalTime] = fakeNumeric[TotalTime] + implicit val decodeNonEmptyString: MappedEncoding[String, string.NonEmptyString] = + MappedEncoding[String, string.NonEmptyString](Refined.unsafeApply) + implicit val encodeNonEmptyString: MappedEncoding[string.NonEmptyString, String] = + MappedEncoding[string.NonEmptyString, String](_.value) + implicit val taskDurationNumeric: Numeric[TotalTime] = fakeNumeric[TotalTime] def fakeNumeric[T]: Numeric[T] = new Numeric[T] { diff --git a/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala b/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala index e7efc0f..1704534 100644 --- a/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala +++ b/modules/projects/src/main/scala/taskforce/project/instances/Http4s.scala @@ -4,8 +4,7 @@ import org.http4s.EntityEncoder import org.http4s.circe._ import taskforce.project.{Project, TotalTime} - -trait Http4s[F[_]] extends Circe { +trait Http4s[F[_]] extends Circe { implicit val totalTimeEntityEncoder: EntityEncoder[F, TotalTime] = jsonEncoderOf[F, TotalTime] implicit val projectEntityEncoder: EntityEncoder[F, Project] = jsonEncoderOf[F, Project] diff --git a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index ff717bf..95119c1 100644 --- a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -1,6 +1,5 @@ package taskforce.project - import cats.data.Kleisli import cats.effect.IO import cats.implicits._ diff --git a/modules/projects/src/test/scala/taskforce/project/arbitraries.scala b/modules/projects/src/test/scala/taskforce/project/arbitraries.scala index 8c3b64c..71a9e7e 100644 --- a/modules/projects/src/test/scala/taskforce/project/arbitraries.scala +++ b/modules/projects/src/test/scala/taskforce/project/arbitraries.scala @@ -3,7 +3,6 @@ package taskforce.project import org.scalacheck.Arbitrary import generators._ - object arbitraries { implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) diff --git a/modules/projects/src/test/scala/taskforce/project/generators.scala b/modules/projects/src/test/scala/taskforce/project/generators.scala index ed97f31..bd7ed4b 100644 --- a/modules/projects/src/test/scala/taskforce/project/generators.scala +++ b/modules/projects/src/test/scala/taskforce/project/generators.scala @@ -45,10 +45,9 @@ object generators { val projectGen: Gen[Project] = for { projectId <- projectIdGen - name <- newProjectGen - userId <- userIdGen - created <- localDateTimeGen + name <- newProjectGen + userId <- userIdGen + created <- localDateTimeGen } yield Project(projectId, name, userId, CreationDate(created), None) } - \ No newline at end of file diff --git a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala index c6d1517..c62235f 100644 --- a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala +++ b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala @@ -19,7 +19,6 @@ class TaskRepositorySuite extends BasicRepositorySuite { } - test("task creation") { PropF.forAllF { (t: Task) => for { diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala index eb1b327..0420277 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala @@ -85,9 +85,7 @@ final class LiveTaskRepository[F[_]: MonadCancel[*[_], Throwable]]( override def find(projectId: ProjectId, taskId: TaskId): F[Option[Task]] = run( - taskQuery.filter(t => - t.projectId == lift(projectId) && t.id == lift(taskId) - ) + taskQuery.filter(t => t.projectId == lift(projectId) && t.id == lift(taskId)) ) .transact(xa) .map(_.headOption) diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala index 82abe3d..144baa1 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala @@ -59,8 +59,8 @@ final class TaskService[F[_]: Sync]( def create(task: Task): F[Either[TaskError, Task]] = (for { allUserTasks <- Sync[F].delay(taskRepo.listByUser(task.author)) - _ <- taskPeriodIsValid(task, allUserTasks) - result <- taskRepo.create(task) + _ <- taskPeriodIsValid(task, allUserTasks) + result <- taskRepo.create(task) } yield result.leftWiden[TaskError]).recover { case WrongPeriodError => WrongPeriodError.asLeft[Task] } @@ -71,16 +71,16 @@ final class TaskService[F[_]: Sync]( caller: UserId ): F[Either[TaskError, Task]] = (for { - oldTask <- getTaskIfAuthor(task.projectId, taskId, caller) + oldTask <- getTaskIfAuthor(task.projectId, taskId, caller) allUserTasks <- Sync[F].delay(taskRepo.listByUser(task.author)) allUserTasksWithoutOld = allUserTasks.filterNot(_.id == oldTask.id) - _ <- taskPeriodIsValid(task, allUserTasksWithoutOld) + _ <- taskPeriodIsValid(task, allUserTasksWithoutOld) updatedTask <- taskRepo.update(oldTask.id, task) } yield updatedTask.leftWiden[TaskError]).recover { case WrongPeriodError => WrongPeriodError.asLeft[Task] } def delete(projectId: ProjectId, taskId: TaskId, caller: UserId) = for { - task <- getTaskIfAuthor(projectId, taskId, caller) + task <- getTaskIfAuthor(projectId, taskId, caller) result <- taskRepo.delete(task.id) } yield result diff --git a/modules/tasks/src/main/scala/taskforce/task/package.scala b/modules/tasks/src/main/scala/taskforce/task/package.scala index 2c873bb..f23cb02 100644 --- a/modules/tasks/src/main/scala/taskforce/task/package.scala +++ b/modules/tasks/src/main/scala/taskforce/task/package.scala @@ -1,6 +1,5 @@ package taskforce - import monix.newtypes.NewtypeWrapped import eu.timepit.refined.types.numeric.PosInt import java.util.UUID @@ -10,27 +9,18 @@ import eu.timepit.refined.types.string.NonEmptyString package object task { type ProjectId = ProjectId.Type - object ProjectId - extends NewtypeWrapped[Long] - + object ProjectId extends NewtypeWrapped[Long] type TaskId = TaskId.Type - object TaskId - extends NewtypeWrapped[UUID] - + object TaskId extends NewtypeWrapped[UUID] type TaskDuration = TaskDuration.Type - object TaskDuration - extends NewtypeWrapped[Duration] - + object TaskDuration extends NewtypeWrapped[Duration] type TaskVolume = TaskVolume.Type - object TaskVolume - extends NewtypeWrapped[PosInt] - + object TaskVolume extends NewtypeWrapped[PosInt] + type TaskComment = TaskComment.Type - object TaskComment - extends NewtypeWrapped[NonEmptyString] - + object TaskComment extends NewtypeWrapped[NonEmptyString] } diff --git a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala index 738c721..808d390 100644 --- a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -19,7 +19,7 @@ import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.common.CreationDate import taskforce.task.instances.Circe -class TasksRoutesSuite extends HttpTestSuite with Circe{ +class TasksRoutesSuite extends HttpTestSuite with Circe { implicit def encodeNewTask: EntityEncoder[IO, NewTask] = jsonEncoderOf diff --git a/modules/tasks/src/test/scala/taskforce/task/generators.scala b/modules/tasks/src/test/scala/taskforce/task/generators.scala index b78d045..5666779 100644 --- a/modules/tasks/src/test/scala/taskforce/task/generators.scala +++ b/modules/tasks/src/test/scala/taskforce/task/generators.scala @@ -60,13 +60,13 @@ object generators { val taskGen: Gen[Task] = for { - id <- taskIdGen + id <- taskIdGen projectId <- projectIdGen - author <- userIdGen - created <- localDateTimeGen - duration <- taskDurationGen - volume <- taskVolumeGen - comment <- taskCommentGen + author <- userIdGen + created <- localDateTimeGen + duration <- taskDurationGen + volume <- taskVolumeGen + comment <- taskCommentGen } yield Task( id, projectId, @@ -80,10 +80,10 @@ object generators { val newTaskGen: Gen[NewTask] = for { - created <- creationDateTimeGen + created <- creationDateTimeGen duration <- taskDurationGen - volume <- taskVolumeGen - comment <- taskCommentGen + volume <- taskVolumeGen + comment <- taskCommentGen } yield NewTask(created.some, duration, volume.some, comment) - } \ No newline at end of file +} From 4f96c4e0d761c3341d620d265a4f88d44bd5b758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Tue, 26 Jul 2022 18:54:21 +0200 Subject: [PATCH 15/24] auth module --- .../authentication/UserRepository.scala | 16 ++++++---------- src/main/scala/taskforce/infrastructure/Db.scala | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala b/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala index 92fbf72..ce053e9 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala @@ -1,21 +1,21 @@ package taskforce.authentication -import cats.effect.Sync + import doobie.implicits._ import doobie.postgres.implicits._ import doobie.util.transactor.Transactor -import cats.effect.kernel.MonadCancel import taskforce.common.NewTypeDoobieMeta +import cats.effect.kernel.MonadCancelThrow + trait UserRepository[F[_]] { def find(userId: UserId): F[Option[User]] def create(user: User): F[Int] } -final class LiveUserRepository[F[_]: MonadCancel[*[_], Throwable]]( - xa: Transactor[F] -) extends UserRepository[F] - with NewTypeDoobieMeta { +object UserRepository { + def make[F[_] : MonadCancelThrow](xa: Transactor[F]) = + new UserRepository[F] with NewTypeDoobieMeta { override def create(user: User): F[Int] = sql.insert(user).update.run.transact(xa) @@ -34,8 +34,4 @@ final class LiveUserRepository[F[_]: MonadCancel[*[_], Throwable]]( } } - -object LiveUserRepository { - def make[F[_]: Sync](xa: Transactor[F]) = - Sync[F].delay { new LiveUserRepository[F](xa) } } diff --git a/src/main/scala/taskforce/infrastructure/Db.scala b/src/main/scala/taskforce/infrastructure/Db.scala index a12b080..8df25e9 100644 --- a/src/main/scala/taskforce/infrastructure/Db.scala +++ b/src/main/scala/taskforce/infrastructure/Db.scala @@ -3,7 +3,7 @@ package taskforce.infrastructure import cats.effect.Sync import cats.implicits._ import doobie.util.transactor.Transactor -import taskforce.authentication.{UserRepository, LiveUserRepository} +import taskforce.authentication.UserRepository import taskforce.filter.{FilterRepository, LiveFilterRepository} import taskforce.project.{ProjectRepository, LiveProjectRepository} import taskforce.stats.{StatsRepository, LiveStatsRepository} @@ -25,6 +25,6 @@ object Db { projectDb <- LiveProjectRepository.make[F](xa) statsDb <- LiveStatsRepository.make[F](xa) taskDb <- LiveTaskRepository.make[F](xa) - userdDb <- LiveUserRepository.make[F](xa) + userdDb <- UserRepository.make[F](xa).pure[F] } yield Db(filterDb, projectDb, statsDb, taskDb, userdDb) } From 88e4b0dbec932d4a3a5dc993993b87c88e617eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Tue, 26 Jul 2022 19:12:09 +0200 Subject: [PATCH 16/24] filters module --- .../authentication/UserRepository.scala | 2 +- .../main/scala/taskforce/filter/Filter.scala | 2 +- .../taskforce/filter/FilterRepository.scala | 247 +++++++++--------- .../scala/taskforce/filter/FilterRoutes.scala | 8 +- .../taskforce/filter/FilterService.scala | 9 +- .../scala/taskforce/infrastructure/Db.scala | 4 +- .../taskforce/infrastructure/Server.scala | 4 +- 7 files changed, 135 insertions(+), 141 deletions(-) diff --git a/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala b/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala index ce053e9..4b98e32 100644 --- a/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala +++ b/modules/auth/src/main/scala/taskforce/authentication/UserRepository.scala @@ -27,7 +27,7 @@ object UserRepository { .option .transact(xa) - object sql { + private object sql { def insert(user: User) = sql"insert into users(id) values(${user.id})" def find(userId: UserId) = sql"select id from users where id =${userId.value}" diff --git a/modules/filters/src/main/scala/taskforce/filter/Filter.scala b/modules/filters/src/main/scala/taskforce/filter/Filter.scala index 387df65..926b6a5 100644 --- a/modules/filters/src/main/scala/taskforce/filter/Filter.scala +++ b/modules/filters/src/main/scala/taskforce/filter/Filter.scala @@ -26,7 +26,7 @@ final case class In(names: List[NonEmptyString]) extends Crit final case class TaskCreatedDate(op: Operator, date: LocalDateTime) extends Criteria final case class State(status: Status) extends Criteria -final case class FilterId(value: UUID) extends AnyVal +final case class FilterId(value: UUID) final case class NewFilter(conditions: List[Criteria]) final case class Filter(id: FilterId, conditions: List[Criteria]) diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala index ee490a5..6367337 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala @@ -1,6 +1,6 @@ package taskforce.filter -import cats.effect.Sync + import cats.implicits._ import doobie._ import doobie.implicits._ @@ -15,7 +15,7 @@ import java.util.UUID import taskforce.common.Sqlizer.ops._ import taskforce.project.Project import taskforce.task.Task -import cats.effect.kernel.MonadCancel +import cats.effect.kernel.MonadCancelThrow import eu.timepit.refined.cats._ import org.typelevel.log4cats.Logger import cats.Show @@ -32,125 +32,124 @@ trait FilterRepository[F[_]] { def list: Stream[F, Filter] } -final class LiveFilterRepository[F[_]: MonadCancel[*[_], Throwable]: Logger]( - xa: Transactor[F] -) extends FilterRepository[F] - with instances.Doobie { - - implicit val showInstance: Show[LocalDateTime] = - Show.fromToString[LocalDateTime] - - private val tuple2Condition: PartialFunction[ - ( - String, - Option[Operator], - Option[LocalDateTime], - Option[Status], - Option[List[NonEmptyString]] - ), - Criteria - ] = { - case ("in", _, _, _, Some(list)) => - In(list) - case ("cond", Some(op), Some(date), _, _) => - TaskCreatedDate(op, date) - case ("state", _, _, Some(status), _) => State(status) - } - - override def list: Stream[F, Filter] = - sql.getAll - .query[ - ( - UUID, - ( - String, - Option[Operator], - Option[LocalDateTime], - Option[Status], - Option[List[NonEmptyString]] - ) - ) - ] - .stream - .transact(xa) - .map { case (k, v) => (k, tuple2Condition(v)) } - .groupAdjacentBy { case (uuid, _) => uuid } - .map { case (uuid, criteriaChunk) => - Filter(FilterId(uuid), criteriaChunk.map(_._2).toList) - } +object FilterRepository { + def make[F[_]: MonadCancelThrow: Logger](xa: Transactor[F]) = + new FilterRepository[F] with instances.Doobie { - override def execute( - filter: Filter, - sortByOption: Option[SortBy], - page: Page - ): Stream[F, FilterResultRow] = { - - val whereClause = - fragments.whereAnd(filter.conditions.map(_.toFragment): _*) - val orderClause = sortByOption.fold(Fragment.empty)(_.toFragment) - val limitClause = page.toFragment - val sqlQuery = sql.getData ++ whereClause ++ orderClause ++ limitClause - - Stream.eval[F, Unit](Logger[F].info("test")) >> - sqlQuery - .query[(Project, Option[Task])] - // .query[CreationDate] - .stream - // .as( null : FilterResultRow) - .transact(xa) - .map(x => FilterResultRow.fromTuple(x)) - } - - def createCriterias(filterId: FilterId)(criteria: Criteria) = - criteria match { - case in @ In(_) => - sql.createInCritaria(filterId, in).update.run - case date @ TaskCreatedDate(_, _) => - sql.createDateCritaria(filterId, date).update.run - case state @ State(_) => - sql.createStateCritaria(filterId, state).update.run - } - override def create(filter: Filter): F[Filter] = { - filter.conditions - .traverse(createCriterias(filter.id)) - .transact(xa) - .as(filter) - } - - override def delete(id: FilterId): F[Int] = - sql.delete(id).update.run.transact(xa) - - override def find(id: FilterId): F[Option[Filter]] = - sql - .get(id) - .query[ + implicit val showInstance: Show[LocalDateTime] = + Show.fromToString[LocalDateTime] + + private val tuple2Condition: PartialFunction[ ( String, Option[Operator], Option[LocalDateTime], Option[Status], Option[List[NonEmptyString]] - ) - ] - .to[List] - .map( - _.collect { - case ("in", _, _, _, Some(list)) => - In(list) - case ("cond", Some(op), Some(date), _, _) => - TaskCreatedDate(op, date) - case ("state", _, _, Some(status), _) => State(status) + ), + Criteria + ] = { + case ("in", _, _, _, Some(list)) => + In(list) + case ("cond", Some(op), Some(date), _, _) => + TaskCreatedDate(op, date) + case ("state", _, _, Some(status), _) => State(status) + } + + override def list: Stream[F, Filter] = + sql.getAll + .query[ + ( + UUID, + ( + String, + Option[Operator], + Option[LocalDateTime], + Option[Status], + Option[List[NonEmptyString]] + ) + ) + ] + .stream + .transact(xa) + .map { case (k, v) => (k, tuple2Condition(v)) } + .groupAdjacentBy { case (uuid, _) => uuid } + .map { case (uuid, criteriaChunk) => + Filter(FilterId(uuid), criteriaChunk.map(_._2).toList) + } + + override def execute( + filter: Filter, + sortByOption: Option[SortBy], + page: Page + ): Stream[F, FilterResultRow] = { + + val whereClause = + fragments.whereAnd(filter.conditions.map(_.toFragment): _*) + val orderClause = sortByOption.fold(Fragment.empty)(_.toFragment) + val limitClause = page.toFragment + val sqlQuery = sql.getData ++ whereClause ++ orderClause ++ limitClause + + Stream.eval[F, Unit](Logger[F].info("test")) >> + sqlQuery + .query[(Project, Option[Task])] + // .query[CreationDate] + .stream + // .as( null : FilterResultRow) + .transact(xa) + .map(x => FilterResultRow.fromTuple(x)) + } + + def createCriterias(filterId: FilterId)(criteria: Criteria) = + criteria match { + case in @ In(_) => + sql.createInCritaria(filterId, in).update.run + case date @ TaskCreatedDate(_, _) => + sql.createDateCritaria(filterId, date).update.run + case state @ State(_) => + sql.createStateCritaria(filterId, state).update.run } - ) - .transact(xa) - .map { - case Nil => None - case l => Filter(id, l).some + override def create(filter: Filter): F[Filter] = { + filter.conditions + .traverse(createCriterias(filter.id)) + .transact(xa) + .as(filter) } - object sql { + override def delete(id: FilterId): F[Int] = + sql.delete(id).update.run.transact(xa) - val getData = fr"""select p.id, + override def find(id: FilterId): F[Option[Filter]] = + sql + .get(id) + .query[ + ( + String, + Option[Operator], + Option[LocalDateTime], + Option[Status], + Option[List[NonEmptyString]] + ) + ] + .to[List] + .map( + _.collect { + case ("in", _, _, _, Some(list)) => + In(list) + case ("cond", Some(op), Some(date), _, _) => + TaskCreatedDate(op, date) + case ("state", _, _, Some(status), _) => State(status) + } + ) + .transact(xa) + .map { + case Nil => None + case l => Filter(id, l).some + } + + private object sql { + + val getData = fr"""select p.id, | p.name, | p.author, | p.created, @@ -166,7 +165,7 @@ final class LiveFilterRepository[F[_]: MonadCancel[*[_], Throwable]: Logger]( | from projects p left join tasks t | on t.project_id = p.id""".stripMargin - val getAll = sql"""select filter_id, + val getAll = sql"""select filter_id, | criteria_type, | operator, | date_value, @@ -174,34 +173,30 @@ final class LiveFilterRepository[F[_]: MonadCancel[*[_], Throwable]: Logger]( | list_value | from filters order by filter_id""".stripMargin - def createStateCritaria(id: FilterId, state: State) = - sql"""insert into filters(filter_id,criteria_type,status_value) + def createStateCritaria(id: FilterId, state: State) = + sql"""insert into filters(filter_id,criteria_type,status_value) | values (${id}, | 'state', | ${state.status})""".stripMargin - def createInCritaria(id: FilterId, in: In) = - sql"""insert into filters(filter_id,criteria_type,list_value) + def createInCritaria(id: FilterId, in: In) = + sql"""insert into filters(filter_id,criteria_type,list_value) | values (${id}, | 'in', | ${in.names.map(_.value)} | )""".stripMargin - def createDateCritaria(id: FilterId, date: TaskCreatedDate) = - sql"""insert into filters(filter_id,criteria_type,operator,date_value) + def createDateCritaria(id: FilterId, date: TaskCreatedDate) = + sql"""insert into filters(filter_id,criteria_type,operator,date_value) | values (${id}, | 'cond', | ${date.op}, | ${date.date})""".stripMargin - def delete(id: FilterId) = sql"delete from filters where filter_id = $id" - def get(id: FilterId) = - sql"""select criteria_type,operator,date_value,status_value,list_value + def delete(id: FilterId) = sql"delete from filters where filter_id = $id" + def get(id: FilterId) = + sql"""select criteria_type,operator,date_value,status_value,list_value | from filters | where filter_id = ${id}""".stripMargin - } -} - -object LiveFilterRepository { - def make[F[_]: Sync: Logger](xa: Transactor[F]) = - Sync[F].delay { new LiveFilterRepository[F](xa) } + } + } } diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala index 73af0ee..3528c76 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala @@ -1,6 +1,5 @@ package taskforce.filter -import cats.effect.Sync import cats.implicits._ import io.circe.syntax._ import org.http4s.AuthedRoutes @@ -10,9 +9,10 @@ import org.http4s.dsl.Http4sDsl import org.http4s.server.{AuthMiddleware, Router} import taskforce.authentication.UserId import taskforce.common.{ErrorHandler, errors => commonErrors} +import cats.MonadThrow final class FilterRoutes[ - F[_]: Sync: JsonDecoder + F[_]: MonadThrow: JsonDecoder ]( authMiddleware: AuthMiddleware[F, UserId], filterService: FilterService[F] @@ -65,8 +65,8 @@ final class FilterRoutes[ } object FilterRoutes { - def make[F[_]: Sync: JsonDecoder]( + def make[F[_]: MonadThrow: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], filterService: FilterService[F] - ) = Sync[F].delay { new FilterRoutes(authMiddleware, filterService) } + ) = new FilterRoutes(authMiddleware, filterService) } diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterService.scala b/modules/filters/src/main/scala/taskforce/filter/FilterService.scala index bddeec0..25b9d65 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterService.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterService.scala @@ -1,11 +1,11 @@ package taskforce.filter -import cats.effect.Sync import cats.implicits._ import fs2.Stream import java.util.UUID import taskforce.common.{errors => commonErrors} -final class FilterService[F[_]: Sync]( +import cats.MonadThrow +final class FilterService[F[_]: MonadThrow]( filterRepo: FilterRepository[F] ) { @@ -30,8 +30,7 @@ final class FilterService[F[_]: Sync]( } object FilterService { - def make[F[_]: Sync](filterRepo: FilterRepository[F]) = - Sync[F].delay( + def make[F[_]: MonadThrow](filterRepo: FilterRepository[F]) = new FilterService[F](filterRepo) - ) + } diff --git a/src/main/scala/taskforce/infrastructure/Db.scala b/src/main/scala/taskforce/infrastructure/Db.scala index 8df25e9..7282f0f 100644 --- a/src/main/scala/taskforce/infrastructure/Db.scala +++ b/src/main/scala/taskforce/infrastructure/Db.scala @@ -4,7 +4,7 @@ import cats.effect.Sync import cats.implicits._ import doobie.util.transactor.Transactor import taskforce.authentication.UserRepository -import taskforce.filter.{FilterRepository, LiveFilterRepository} +import taskforce.filter.FilterRepository import taskforce.project.{ProjectRepository, LiveProjectRepository} import taskforce.stats.{StatsRepository, LiveStatsRepository} import taskforce.task.{TaskRepository, LiveTaskRepository} @@ -21,7 +21,7 @@ final case class Db[F[_]]( object Db { def make[F[_]: Sync: Logger](xa: Transactor[F]) = for { - filterDb <- LiveFilterRepository.make[F](xa) + filterDb <- FilterRepository.make[F](xa).pure[F] projectDb <- LiveProjectRepository.make[F](xa) statsDb <- LiveStatsRepository.make[F](xa) taskDb <- LiveTaskRepository.make[F](xa) diff --git a/src/main/scala/taskforce/infrastructure/Server.scala b/src/main/scala/taskforce/infrastructure/Server.scala index b547370..8c3223f 100644 --- a/src/main/scala/taskforce/infrastructure/Server.scala +++ b/src/main/scala/taskforce/infrastructure/Server.scala @@ -28,9 +28,9 @@ final class Server[F[_]: Logger: Async] private ( projectService <- ProjectService.make(db.projectRepo) taskService <- TaskService.make(db.taskRepo) statsService <- StatsService.make(db.statsRepo) - filterService <- FilterService.make(db.filterRepo) + filterService <- FilterService.make(db.filterRepo).pure[F] projectRoutes <- ProjectRoutes.make(authMiddleware, projectService) - filterRoutes <- FilterRoutes.make(authMiddleware, filterService) + filterRoutes <- FilterRoutes.make(authMiddleware, filterService).pure[F] statsRoutes <- StatsRoutes.make(authMiddleware, statsService) taskRoutes <- TaskRoutes.make(authMiddleware, taskService) errHandler = LiveHttpErrorHandler[F] From ac4835bff1ed17a27a51253c4d32033c25a530d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Tue, 26 Jul 2022 19:20:35 +0200 Subject: [PATCH 17/24] project module --- .../taskforce/project/ProjectRepository.scala | 165 +++++++++--------- .../taskforce/project/ProjectRoutes.scala | 11 +- .../taskforce/project/ProjectService.scala | 8 +- .../scala/taskforce/infrastructure/Db.scala | 4 +- .../taskforce/infrastructure/Server.scala | 4 +- 5 files changed, 93 insertions(+), 99 deletions(-) diff --git a/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala index 3d904ee..a7fc592 100644 --- a/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala +++ b/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala @@ -1,7 +1,6 @@ package taskforce.project -import cats.effect.Sync -import cats.effect.kernel.MonadCancel +import cats.effect.kernel.MonadCancelThrow import cats.syntax.all._ import doobie.implicits._ import doobie.util.transactor.Transactor @@ -25,94 +24,90 @@ trait ProjectRepository[F[_]] { def totalTime(projectId: ProjectId): F[TotalTime] } -final class LiveProjectRepository[F[_]: MonadCancel[*[_], Throwable]]( - xa: Transactor[F] -) extends ProjectRepository[F] - with instances.Doobie - with NewTypeQuillInstances { - - private val ctx = new DoobieContext.Postgres(NamingStrategy(PluralizedTableNames, SnakeCase)) - import ctx._ - - private val projectQuery = quote { - query[Project] - } - private val taskQuery = quote { - querySchema[TaskTime]("tasks") - } - private val newProjectId = ProjectId(0L) - - override def totalTime(projectId: ProjectId): F[TotalTime] = - run(taskQuery.filter(t => t.projectId == lift(projectId) && t.deleted.isEmpty).map(_.time).sum) - .transact(xa) - .map(t => t.getOrElse(TotalTime(Duration.ZERO))) - - override def find(id: ProjectId): F[Option[Project]] = - run(query[Project].filter(_.id == lift(id))).transact(xa).map(_.headOption) - - override def list: F[List[Project]] = - run(projectQuery) - .transact(xa) - - override def create( - newProject: ProjectName, - author: UserId - ): F[Either[DuplicateProjectNameError, Project]] = { - val created = CreationDate(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)) - run( - projectQuery - .insertValue(lift(Project(newProjectId, newProject, author, created, None))) - .returningGenerated(_.id) - ) - .transact(xa) - .map { id => - Project(id, newProject, author, created, None) +object ProjectRepository { + def make[F[_]: MonadCancelThrow](xa: Transactor[F]): ProjectRepository[F] = + new ProjectRepository[F] with instances.Doobie with NewTypeQuillInstances { + + private val ctx = new DoobieContext.Postgres(NamingStrategy(PluralizedTableNames, SnakeCase)) + import ctx._ + + private val projectQuery = quote { + query[Project] + } + private val taskQuery = quote { + querySchema[TaskTime]("tasks") + } + private val newProjectId = ProjectId(0L) + + override def totalTime(projectId: ProjectId): F[TotalTime] = + run(taskQuery.filter(t => t.projectId == lift(projectId) && t.deleted.isEmpty).map(_.time).sum) + .transact(xa) + .map(t => t.getOrElse(TotalTime(Duration.ZERO))) + + override def find(id: ProjectId): F[Option[Project]] = + run(query[Project].filter(_.id == lift(id))).transact(xa).map(_.headOption) + + override def list: F[List[Project]] = + run(projectQuery) + .transact(xa) + + override def create( + newProject: ProjectName, + author: UserId + ): F[Either[DuplicateProjectNameError, Project]] = { + val created = CreationDate(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)) + run( + projectQuery + .insertValue(lift(Project(newProjectId, newProject, author, created, None))) + .returningGenerated(_.id) + ) + .transact(xa) + .map { id => + Project(id, newProject, author, created, None) + } + .map(_.asRight[DuplicateProjectNameError]) + .recover(mapDatabaseErr(newProject)) } - .map(_.asRight[DuplicateProjectNameError]) - .recover(mapDatabaseErr(newProject)) - } - override def delete(id: ProjectId): F[Int] = { - val deleted = DeletionDate(LocalDateTime.now()).some - val result = for { - x <- run(projectQuery.filter(p => p.id == lift(id) && p.deleted.isEmpty).update(_.deleted -> lift(deleted))) - y <- run(taskQuery.filter(t => t.projectId == lift(id) && t.deleted.isEmpty).update(_.deleted -> lift(deleted))) - } yield x + y + override def delete(id: ProjectId): F[Int] = { + val deleted = DeletionDate(LocalDateTime.now()).some + val result = for { + x <- run(projectQuery.filter(p => p.id == lift(id) && p.deleted.isEmpty).update(_.deleted -> lift(deleted))) + y <- run( + taskQuery.filter(t => t.projectId == lift(id) && t.deleted.isEmpty).update(_.deleted -> lift(deleted)) + ) + } yield x + y - result.transact(xa).map(_.toInt) - } + result.transact(xa).map(_.toInt) + } - override def update( - id: ProjectId, - newProject: ProjectName - ): F[Either[DuplicateProjectNameError, Project]] = { - run( - projectQuery - .filter(_.id == lift(id)) - .update(_.name -> lift(newProject)) - .returning(p => p) - ) - .transact(xa) - .map(_.asRight[DuplicateProjectNameError]) - .recover(mapDatabaseErr(newProject)) - - } - - private def mapDatabaseErr( - newProject: ProjectName - ): PartialFunction[Throwable, Either[DuplicateProjectNameError, Project]] = { - case x: PSQLException - if x.getMessage.contains( - "unique constraint" - ) => - DuplicateProjectNameError(newProject).asLeft[Project] - } + override def update( + id: ProjectId, + newProject: ProjectName + ): F[Either[DuplicateProjectNameError, Project]] = { + run( + projectQuery + .filter(_.id == lift(id)) + .update(_.name -> lift(newProject)) + .returning(p => p) + ) + .transact(xa) + .map(_.asRight[DuplicateProjectNameError]) + .recover(mapDatabaseErr(newProject)) + + } - case class TaskTime(projectId: ProjectId, time: TotalTime, deleted: Option[DeletionDate]) + private def mapDatabaseErr( + newProject: ProjectName + ): PartialFunction[Throwable, Either[DuplicateProjectNameError, Project]] = { + case x: PSQLException + if x.getMessage.contains( + "unique constraint" + ) => + DuplicateProjectNameError(newProject).asLeft[Project] + } -} + case class TaskTime(projectId: ProjectId, time: TotalTime, deleted: Option[DeletionDate]) -object LiveProjectRepository { - def make[F[_]: Sync](xa: Transactor[F]): F[LiveProjectRepository[F]] = - Sync[F].delay { new LiveProjectRepository[F](xa) } + } } diff --git a/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala index a9f89b9..a8c28a2 100644 --- a/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala +++ b/modules/projects/src/main/scala/taskforce/project/ProjectRoutes.scala @@ -1,6 +1,5 @@ package taskforce.project -import cats.effect.Sync import cats.implicits._ import org.http4s.{AuthedRequest, AuthedRoutes, Response} import org.http4s.circe._ @@ -9,8 +8,10 @@ import org.http4s.server.{AuthMiddleware, Router} import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, ErrorHandler, errors => commonErrors} import io.circe.refined._ +import cats.MonadThrow +import org.http4s.HttpRoutes -final class ProjectRoutes[F[_]: Sync: JsonDecoder]( +final class ProjectRoutes[F[_]: MonadThrow: JsonDecoder] private ( authMiddleware: AuthMiddleware[F, UserId], projectService: ProjectService[F] ) extends instances.Http4s[F] { @@ -70,15 +71,15 @@ final class ProjectRoutes[F[_]: Sync: JsonDecoder]( } } - def routes(errHandler: ErrorHandler[F, Throwable]) = + def routes(errHandler: ErrorHandler[F, Throwable]): HttpRoutes[F] = Router( prefixPath -> errHandler.basicHandle(authMiddleware(httpRoutes)) ) } object ProjectRoutes { - def make[F[_]: Sync: JsonDecoder]( + def make[F[_]: MonadThrow: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], projectService: ProjectService[F] - ) = Sync[F].delay { new ProjectRoutes(authMiddleware, projectService) } + ): ProjectRoutes[F] = new ProjectRoutes(authMiddleware, projectService) } diff --git a/modules/projects/src/main/scala/taskforce/project/ProjectService.scala b/modules/projects/src/main/scala/taskforce/project/ProjectService.scala index f421151..b631490 100644 --- a/modules/projects/src/main/scala/taskforce/project/ProjectService.scala +++ b/modules/projects/src/main/scala/taskforce/project/ProjectService.scala @@ -6,7 +6,7 @@ import taskforce.authentication.UserId import taskforce.common.errors._ import cats.MonadThrow -final class ProjectService[F[_]: Sync]( +final class ProjectService[F[_]: MonadThrow] private ( projectRepo: ProjectRepository[F] ) { @@ -46,8 +46,6 @@ final class ProjectService[F[_]: Sync]( } object ProjectService { - def make[F[_]: Sync](projectRepo: ProjectRepository[F]) = - Sync[F].delay( - new ProjectService[F](projectRepo) - ) + def make[F[_]: Sync](projectRepo: ProjectRepository[F]): ProjectService[F] = new ProjectService[F](projectRepo) + } diff --git a/src/main/scala/taskforce/infrastructure/Db.scala b/src/main/scala/taskforce/infrastructure/Db.scala index 7282f0f..c710534 100644 --- a/src/main/scala/taskforce/infrastructure/Db.scala +++ b/src/main/scala/taskforce/infrastructure/Db.scala @@ -5,7 +5,7 @@ import cats.implicits._ import doobie.util.transactor.Transactor import taskforce.authentication.UserRepository import taskforce.filter.FilterRepository -import taskforce.project.{ProjectRepository, LiveProjectRepository} +import taskforce.project.ProjectRepository import taskforce.stats.{StatsRepository, LiveStatsRepository} import taskforce.task.{TaskRepository, LiveTaskRepository} import org.typelevel.log4cats.Logger @@ -22,7 +22,7 @@ object Db { def make[F[_]: Sync: Logger](xa: Transactor[F]) = for { filterDb <- FilterRepository.make[F](xa).pure[F] - projectDb <- LiveProjectRepository.make[F](xa) + projectDb <- ProjectRepository.make[F](xa).pure[F] statsDb <- LiveStatsRepository.make[F](xa) taskDb <- LiveTaskRepository.make[F](xa) userdDb <- UserRepository.make[F](xa).pure[F] diff --git a/src/main/scala/taskforce/infrastructure/Server.scala b/src/main/scala/taskforce/infrastructure/Server.scala index 8c3223f..9cc96c1 100644 --- a/src/main/scala/taskforce/infrastructure/Server.scala +++ b/src/main/scala/taskforce/infrastructure/Server.scala @@ -25,11 +25,11 @@ final class Server[F[_]: Logger: Async] private ( def run /*(implicit T: Temporal[F])*/ = for { basicRoutes <- BasicRoutes.make(authMiddleware) - projectService <- ProjectService.make(db.projectRepo) + projectService <- ProjectService.make(db.projectRepo).pure[F] taskService <- TaskService.make(db.taskRepo) statsService <- StatsService.make(db.statsRepo) filterService <- FilterService.make(db.filterRepo).pure[F] - projectRoutes <- ProjectRoutes.make(authMiddleware, projectService) + projectRoutes <- ProjectRoutes.make(authMiddleware, projectService).pure[F] filterRoutes <- FilterRoutes.make(authMiddleware, filterService).pure[F] statsRoutes <- StatsRoutes.make(authMiddleware, statsService) taskRoutes <- TaskRoutes.make(authMiddleware, taskService) From b05fcc227d21f7212924fb79dda4d82d5138cb7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Tue, 26 Jul 2022 19:26:38 +0200 Subject: [PATCH 18/24] stats module + fixed tests in projects module --- .../project/ProjectRoutesSuite.scala | 2 +- .../taskforce/stats/StatsRepository.scala | 34 ++++++++----------- .../scala/taskforce/infrastructure/Db.scala | 4 +-- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index 95119c1..9dd89d1 100644 --- a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -45,7 +45,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { DuplicateProjectNameError(newProject).asLeft[Project].pure[IO] } val routes = - new ProjectRoutes[IO](authMiddleware(p1.author), new ProjectService[IO](projectRepo)).routes(errHandler) + ProjectRoutes.make[IO](authMiddleware(p1.author), ProjectService.make[IO](projectRepo)).routes(errHandler) PUT(p2.name, Uri.unsafeFromString(s"api/v1/projects/${p1.id.value}")).pure[IO].flatMap { req => assertHttp(routes, req)( Status.Conflict, diff --git a/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala b/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala index af97e5d..c8d84e8 100644 --- a/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala +++ b/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala @@ -1,6 +1,5 @@ package taskforce.stats -import cats.effect.Sync import doobie.implicits._ import doobie.postgres.implicits._ import doobie.refined.implicits._ @@ -11,21 +10,20 @@ trait StatsRepository[F[_]] { def get(query: StatsQuery): F[StatsResponse] } -final class LiveStatsRepository[F[_]: MonadCancelThrow]( - xa: Transactor[F] -) extends StatsRepository[F] - with instances.Doobie { +object StatsRepository { + def make[F[_]: MonadCancelThrow](xa: Transactor[F]) = + new StatsRepository[F] with instances.Doobie { - override def get(query: StatsQuery): F[StatsResponse] = - sql - .getStats(query) - .query[StatsResponse] - .unique - .transact(xa) + override def get(query: StatsQuery): F[StatsResponse] = + sql + .getStats(query) + .query[StatsResponse] + .unique + .transact(xa) - object sql { - def getStats(query: StatsQuery) = { - fr"""select count(*) cnt, + object sql { + def getStats(query: StatsQuery) = { + fr"""select count(*) cnt, | avg(duration) avg_duration, | avg(volume) avg_volume, | sum(duration* volume)/sum(volume) avg_volume_duration @@ -33,11 +31,7 @@ final class LiveStatsRepository[F[_]: MonadCancelThrow]( |where t.deleted is null | and t.started between coalesce(${query.from},t.started) and coalesce(${query.to},t.started) |""".stripMargin ++ query.toFragment + } + } } - } -} - -object LiveStatsRepository { - def make[F[_]: Sync](xa: Transactor[F]) = - Sync[F].delay { new LiveStatsRepository[F](xa) } } diff --git a/src/main/scala/taskforce/infrastructure/Db.scala b/src/main/scala/taskforce/infrastructure/Db.scala index c710534..f26a217 100644 --- a/src/main/scala/taskforce/infrastructure/Db.scala +++ b/src/main/scala/taskforce/infrastructure/Db.scala @@ -6,7 +6,7 @@ import doobie.util.transactor.Transactor import taskforce.authentication.UserRepository import taskforce.filter.FilterRepository import taskforce.project.ProjectRepository -import taskforce.stats.{StatsRepository, LiveStatsRepository} +import taskforce.stats.StatsRepository import taskforce.task.{TaskRepository, LiveTaskRepository} import org.typelevel.log4cats.Logger @@ -23,7 +23,7 @@ object Db { for { filterDb <- FilterRepository.make[F](xa).pure[F] projectDb <- ProjectRepository.make[F](xa).pure[F] - statsDb <- LiveStatsRepository.make[F](xa) + statsDb <- StatsRepository.make[F](xa).pure[F] taskDb <- LiveTaskRepository.make[F](xa) userdDb <- UserRepository.make[F](xa).pure[F] } yield Db(filterDb, projectDb, statsDb, taskDb, userdDb) From a85f10731b65fcf0cbe1901f27c93061041ce80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Tue, 26 Jul 2022 20:05:48 +0200 Subject: [PATCH 19/24] Stats tasks + fixing tests --- .../filter/FilterRepositorySuite.scala | 2 +- .../scala/taskforce/filter/FilterRoutes.scala | 6 ++-- .../taskforce/filter/FilterService.scala | 8 ++--- .../taskforce/filter/FilterRoutesSuite.scala | 6 ++-- .../project/ProjectRepositorySuite.scala | 2 +- .../project/ProjectRoutesSuite.scala | 8 ++--- .../taskforce/stats/StatsRepository.scala | 2 +- .../scala/taskforce/stats/StatsRoutes.scala | 10 +++--- .../scala/taskforce/stats/StatsService.scala | 12 +++---- .../taskforce/task/TaskRepositorySuite.scala | 3 +- .../scala/taskforce/task/TaskRepository.scala | 15 ++++---- .../scala/taskforce/task/TaskRoutes.scala | 8 ++--- .../scala/taskforce/task/TaskService.scala | 11 +++--- .../taskforce/task/TaskRoutesSuite.scala | 36 +++++++++---------- src/main/scala/taskforce/Main.scala | 2 +- .../infrastructure/BasicRoutes.scala | 8 ++--- .../scala/taskforce/infrastructure/Db.scala | 4 +-- .../taskforce/infrastructure/Server.scala | 18 +++++----- 18 files changed, 74 insertions(+), 87 deletions(-) diff --git a/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala b/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala index 3282479..e8b339c 100644 --- a/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala +++ b/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala @@ -17,7 +17,7 @@ class FilterRepositorySuite extends BasicRepositorySuite { override def beforeAll(): Unit = { super.beforeAll() - filterRepo = LiveFilterRepository.make[IO](xa) + filterRepo = FilterRepository.make[IO](xa).pure[IO] } test("filter by project Name") { diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala index 3528c76..72a5750 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterRoutes.scala @@ -11,9 +11,7 @@ import taskforce.authentication.UserId import taskforce.common.{ErrorHandler, errors => commonErrors} import cats.MonadThrow -final class FilterRoutes[ - F[_]: MonadThrow: JsonDecoder -]( +final class FilterRoutes[F[_]: MonadThrow: JsonDecoder] private ( authMiddleware: AuthMiddleware[F, UserId], filterService: FilterService[F] ) extends instances.Circe @@ -68,5 +66,5 @@ object FilterRoutes { def make[F[_]: MonadThrow: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], filterService: FilterService[F] - ) = new FilterRoutes(authMiddleware, filterService) + ) = new FilterRoutes(authMiddleware, filterService) } diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterService.scala b/modules/filters/src/main/scala/taskforce/filter/FilterService.scala index 25b9d65..0027ecc 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterService.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterService.scala @@ -5,9 +5,7 @@ import fs2.Stream import java.util.UUID import taskforce.common.{errors => commonErrors} import cats.MonadThrow -final class FilterService[F[_]: MonadThrow]( - filterRepo: FilterRepository[F] -) { +final class FilterService[F[_]: MonadThrow] private (filterRepo: FilterRepository[F]) { def create(newFilter: NewFilter) = filterRepo @@ -31,6 +29,6 @@ final class FilterService[F[_]: MonadThrow]( object FilterService { def make[F[_]: MonadThrow](filterRepo: FilterRepository[F]) = - new FilterService[F](filterRepo) - + new FilterService[F](filterRepo) + } diff --git a/modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala b/modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala index 91499d3..f13f3ca 100644 --- a/modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala +++ b/modules/filters/src/test/scala/taskforce/filter/FilterRoutesSuite.scala @@ -46,7 +46,7 @@ class FilterRoutesSuite extends HttpTestSuite with instances.Circe { test("create filter") { PropF.forAllF { (f: NewFilter, fId: FilterId) => val filterRepo = new TestFilterRepository(List(Filter(fId, f.conditions)), List()) - val routes = new FilterRoutes[IO](authMiddleware, new FilterService(filterRepo)).routes(errHandler) + val routes = FilterRoutes.make[IO](authMiddleware, FilterService.make(filterRepo)).routes(errHandler) POST(f, uri).pure[IO].flatMap { _ => assertHttpStatus(routes, POST(f, uri))( Status.Created @@ -57,7 +57,7 @@ class FilterRoutesSuite extends HttpTestSuite with instances.Circe { test("get filter that does not exist") { PropF.forAllF { (f: NewFilter, fId: FilterId, fId2: FilterId) => val filterRepo = new TestFilterRepository(List(Filter(fId2, f.conditions)), List()) - val routes = new FilterRoutes[IO](authMiddleware, new FilterService(filterRepo)).routes(errHandler) + val routes = FilterRoutes.make[IO](authMiddleware, FilterService.make(filterRepo)).routes(errHandler) GET(f, Uri.unsafeFromString(s"api/v1/filters/${fId.value}?sortBy=-creaed")).pure[IO].flatMap { req => assertHttp(routes, req)( Status.NotFound, @@ -82,7 +82,7 @@ class FilterRoutesSuite extends HttpTestSuite with instances.Circe { } } - val routes = new FilterRoutes[IO](authMiddleware, new FilterService(filterRepo)).routes(errHandler) + val routes = FilterRoutes.make[IO](authMiddleware, FilterService.make(filterRepo)).routes(errHandler) GET(f, Uri.unsafeFromString(s"api/v1/filters/${fId.value}/data?${queryParams}")).pure[IO].flatMap { req => assertHttp(routes, req)( Status.Ok, diff --git a/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala b/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala index 268d1e9..894145e 100644 --- a/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala +++ b/modules/projects/src/it/scala/taskforce/project/ProjectRepositorySuite.scala @@ -14,7 +14,7 @@ class ProjectRepositorySuite extends BasicRepositorySuite { override def beforeAll(): Unit = { super.beforeAll() - projectRepo = LiveProjectRepository.make[IO](xa) + projectRepo = ProjectRepository.make[IO](xa).pure[IO] } diff --git a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index 9dd89d1..56f69b2 100644 --- a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -65,7 +65,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { DuplicateProjectNameError(newProject).asLeft[Project].pure[IO] } - val routes = new ProjectRoutes[IO](authMiddleware(u), new ProjectService[IO](projectRepo)).routes(errHandler) + val routes = ProjectRoutes.make[IO](authMiddleware(u), ProjectService.make[IO](projectRepo)).routes(errHandler) POST(p1.name, uri).pure[IO].flatMap { req => assertHttp(routes, req)( Status.Conflict, @@ -78,7 +78,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { test("returns project with given ID") { PropF.forAllF { (p: Project, list: List[Project]) => val projectRepo = new TestProjectRepository(p :: list, currentTime) - val routes = new ProjectRoutes[IO](authMiddleware, new ProjectService[IO](projectRepo)).routes(errHandler) + val routes = ProjectRoutes.make[IO](authMiddleware, ProjectService.make[IO](projectRepo)).routes(errHandler) GET(Uri.unsafeFromString(s"api/v1/projects/${p.id.value}")).pure[IO].flatMap { req => assertHttp(routes, req)(Status.Ok, p) @@ -89,7 +89,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { test("cannot delete others project") { PropF.forAllF { (p: Project, u: UserId) => val projectRepo = new TestProjectRepository(List(p), currentTime) - val routes = new ProjectRoutes[IO](authMiddleware(u), new ProjectService[IO](projectRepo)).routes(errHandler) + val routes = ProjectRoutes.make[IO](authMiddleware(u), ProjectService.make[IO](projectRepo)).routes(errHandler) DELETE(Uri.unsafeFromString(s"api/v1/projects/${p.id.value}")).pure[IO].flatMap { req => assertHttp(routes, req)(Status.Forbidden, ErrorMessage("BASIC-003", "User is not an owner of the resource")) @@ -99,7 +99,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { test("provide valid response where project is not found") { PropF.forAllF { (p: ProjectId, u: UserId) => val projectRepo = new TestProjectRepository(List(), currentTime) - val routes = new ProjectRoutes[IO](authMiddleware(u), new ProjectService[IO](projectRepo)).routes(errHandler) + val routes = ProjectRoutes.make[IO](authMiddleware(u), ProjectService.make[IO](projectRepo)).routes(errHandler) DELETE(Uri.unsafeFromString(s"api/v1/projects/${p.value}")).pure[IO].flatMap { req => assertHttp(routes, req)( diff --git a/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala b/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala index c8d84e8..559aaa8 100644 --- a/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala +++ b/modules/stats/src/main/scala/taskforce/stats/StatsRepository.scala @@ -21,7 +21,7 @@ object StatsRepository { .unique .transact(xa) - object sql { + private object sql { def getStats(query: StatsQuery) = { fr"""select count(*) cnt, | avg(duration) avg_duration, diff --git a/modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala b/modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala index 0dc02b5..ebf3357 100644 --- a/modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala +++ b/modules/stats/src/main/scala/taskforce/stats/StatsRoutes.scala @@ -1,6 +1,5 @@ package taskforce.stats -import cats.effect.Sync import cats.implicits._ import org.http4s.AuthedRoutes import org.http4s.circe.CirceEntityEncoder._ @@ -10,10 +9,9 @@ import org.http4s.server.{AuthMiddleware, Router} import org.typelevel.log4cats.Logger import taskforce.authentication.UserId import taskforce.common.{ErrorHandler, errors => commonErrors} +import cats.MonadThrow -final class StatsRoutes[ - F[_]: Sync: JsonDecoder: Logger -]( +final class StatsRoutes[F[_]: MonadThrow: JsonDecoder: Logger] private ( authMiddleware: AuthMiddleware[F, UserId], statsService: StatsService[F] ) extends instances.Circe { @@ -47,8 +45,8 @@ final class StatsRoutes[ } object StatsRoutes { - def make[F[_]: Sync: Logger: JsonDecoder]( + def make[F[_]: MonadThrow: Logger: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], statsService: StatsService[F] - ) = Sync[F].delay { new StatsRoutes(authMiddleware, statsService) } + ) = new StatsRoutes(authMiddleware, statsService) } diff --git a/modules/stats/src/main/scala/taskforce/stats/StatsService.scala b/modules/stats/src/main/scala/taskforce/stats/StatsService.scala index 9c1ff68..70098f1 100644 --- a/modules/stats/src/main/scala/taskforce/stats/StatsService.scala +++ b/modules/stats/src/main/scala/taskforce/stats/StatsService.scala @@ -1,16 +1,12 @@ package taskforce.stats -import cats.effect.Sync -final class StatsService[F[_]]( - statsRepo: StatsRepository[F] -) { +final class StatsService[F[_]] private (statsRepo: StatsRepository[F]) { def getStats(query: StatsQuery) = statsRepo.get(query) } object StatsService { - def make[F[_]: Sync](statsRepo: StatsRepository[F]) = - Sync[F].delay( - new StatsService[F](statsRepo) - ) + def make[F[_]](statsRepo: StatsRepository[F]) = + new StatsService[F](statsRepo) + } diff --git a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala index c62235f..17b399d 100644 --- a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala +++ b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala @@ -6,6 +6,7 @@ import taskforce.task.ProjectId import taskforce.BasicRepositorySuite import taskforce.authentication.UserId import taskforce.task.arbitraries._ +import cats.implicits._ class TaskRepositorySuite extends BasicRepositorySuite { @@ -15,7 +16,7 @@ class TaskRepositorySuite extends BasicRepositorySuite { override def beforeAll(): Unit = { super.beforeAll() - taskRepo = LiveTaskRepository.make[IO](xa) + taskRepo = TaskRepository.make[IO](xa).pure[IO] } diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala index 0420277..7d7d1af 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala @@ -1,7 +1,6 @@ package taskforce.task -import cats.effect.Sync -import cats.effect.kernel.MonadCancel +import cats.effect.kernel.MonadCancelThrow import cats.syntax.all._ import doobie.implicits._ import org.polyvariant.doobiequill.DoobieContext @@ -27,9 +26,11 @@ trait TaskRepository[F[_]] { def update(id: TaskId, task: Task): F[Either[DuplicateTaskNameError, Task]] } -final class LiveTaskRepository[F[_]: MonadCancel[*[_], Throwable]]( - xa: Transactor[F] -) extends TaskRepository[F] + + +object TaskRepository { + def make[F[_]: MonadCancelThrow](xa: Transactor[F]) = + new TaskRepository[F] with instances.Doobie { val ctx = @@ -110,8 +111,4 @@ final class LiveTaskRepository[F[_]: MonadCancel[*[_], Throwable]]( .transact(xa) } - -object LiveTaskRepository { - def make[F[_]: Sync](xa: Transactor[F]) = - Sync[F].delay { new LiveTaskRepository[F](xa) } } diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala index bbc772c..11b1e0f 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskRoutes.scala @@ -1,6 +1,5 @@ package taskforce.task -import cats.effect.Sync import cats.implicits._ import io.circe.syntax._ import org.http4s.circe.CirceEntityEncoder._ @@ -11,8 +10,9 @@ import org.http4s.{AuthedRequest, AuthedRoutes} import taskforce.common.{ErrorMessage, ErrorHandler, errors => commonErrors} import taskforce.authentication.UserId import org.http4s.Response +import cats.MonadThrow -final class TaskRoutes[F[_]: Sync: JsonDecoder]( +final class TaskRoutes[F[_]: MonadThrow: JsonDecoder] private ( authMiddleware: AuthMiddleware[F, UserId], taskService: TaskService[F] ) extends instances.Http4s[F] { @@ -89,8 +89,8 @@ final class TaskRoutes[F[_]: Sync: JsonDecoder]( } object TaskRoutes { - def make[F[_]: Sync: JsonDecoder]( + def make[F[_]: MonadThrow: JsonDecoder]( authMiddleware: AuthMiddleware[F, UserId], taskService: TaskService[F] - ) = Sync[F].delay { new TaskRoutes(authMiddleware, taskService) } + ) = new TaskRoutes(authMiddleware, taskService) } diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala index 144baa1..0405211 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala @@ -6,7 +6,7 @@ import taskforce.common.{errors => commonErrors} import taskforce.authentication.UserId import java.time.LocalDateTime -final class TaskService[F[_]: Sync]( +final class TaskService[F[_]: Sync] private ( taskRepo: TaskRepository[F] ) { @@ -58,7 +58,7 @@ final class TaskService[F[_]: Sync]( def create(task: Task): F[Either[TaskError, Task]] = (for { - allUserTasks <- Sync[F].delay(taskRepo.listByUser(task.author)) + allUserTasks <- taskRepo.listByUser(task.author).pure[F] _ <- taskPeriodIsValid(task, allUserTasks) result <- taskRepo.create(task) } yield result.leftWiden[TaskError]).recover { case WrongPeriodError => @@ -72,7 +72,7 @@ final class TaskService[F[_]: Sync]( ): F[Either[TaskError, Task]] = (for { oldTask <- getTaskIfAuthor(task.projectId, taskId, caller) - allUserTasks <- Sync[F].delay(taskRepo.listByUser(task.author)) + allUserTasks <- taskRepo.listByUser(task.author).pure[F] allUserTasksWithoutOld = allUserTasks.filterNot(_.id == oldTask.id) _ <- taskPeriodIsValid(task, allUserTasksWithoutOld) updatedTask <- taskRepo.update(oldTask.id, task) @@ -88,7 +88,6 @@ final class TaskService[F[_]: Sync]( object TaskService { def make[F[_]: Sync](taskRepo: TaskRepository[F]) = - Sync[F].delay( - new TaskService[F](taskRepo) - ) + new TaskService[F](taskRepo) + } diff --git a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala index 808d390..b0963b0 100644 --- a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -38,9 +38,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("cannot delete not other's task") { PropF.forAllF { (t: Task, u: UserId) => val taskRepo = new TestTaskRepository(List(t)) - val routes = - new TaskRoutes[IO](authMiddleware(u), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(u), TaskService.make(taskRepo)) + .routes(errHandler) DELETE( Uri.unsafeFromString( @@ -58,9 +58,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("cannot add overlapping tasks #1") { PropF.forAllF { (t: Task) => val taskRepo = new TestTaskRepository(List(t)) - val routes = - new TaskRoutes[IO](authMiddleware(t.author), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(t.author), TaskService.make(taskRepo)) + .routes(errHandler) val newTask = NewTask(t.created.some, t.duration, None, None) POST( @@ -81,9 +81,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("cannot add overlapping tasks #2") { PropF.forAllF { (t: Task) => val taskRepo = new TestTaskRepository(List(t)) - val routes = - new TaskRoutes[IO](authMiddleware(t.author), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(t.author), TaskService.make(taskRepo)) + .routes(errHandler) val newTask = NewTask( CreationDate( @@ -112,9 +112,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("cannot add overlapping tasks #3") { PropF.forAllF { (t: Task) => val taskRepo = new TestTaskRepository(List(t)) - val routes = - new TaskRoutes[IO](authMiddleware(t.author), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(t.author), TaskService.make(taskRepo)) + .routes(errHandler) val newTask = NewTask( CreationDate(t.created.value.minus(Duration.ofMinutes(10))).some, @@ -141,9 +141,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("different users can add overlapping tasks") { PropF.forAllF { (t: Task, u: UserId) => val taskRepo = new TestTaskRepository(List(t)) - val routes = - new TaskRoutes[IO](authMiddleware(u), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(u), TaskService.make(taskRepo)) + .routes(errHandler) val newTask = NewTask(t.created.some, t.duration, None, None) POST( @@ -158,9 +158,9 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { test("for unknown ids notFouns err is served") { PropF.forAllF { (taskId: TaskId, projectId: ProjectId, u: UserId) => val taskRepo = new TestTaskRepository(List()) - val routes = - new TaskRoutes[IO](authMiddleware(u), new TaskService(taskRepo)) - .routes(errHandler) + val routes = TaskRoutes + .make[IO](authMiddleware(u), TaskService.make(taskRepo)) + .routes(errHandler) GET( Uri.unsafeFromString( diff --git a/src/main/scala/taskforce/Main.scala b/src/main/scala/taskforce/Main.scala index 74aba30..d057208 100644 --- a/src/main/scala/taskforce/Main.scala +++ b/src/main/scala/taskforce/Main.scala @@ -47,7 +47,7 @@ object Main extends IOApp { hostConfig.port.value, authMiddleware, db - ) + ).pure[IO] exitCode <- server.run } yield exitCode } diff --git a/src/main/scala/taskforce/infrastructure/BasicRoutes.scala b/src/main/scala/taskforce/infrastructure/BasicRoutes.scala index 82e9da3..0122981 100644 --- a/src/main/scala/taskforce/infrastructure/BasicRoutes.scala +++ b/src/main/scala/taskforce/infrastructure/BasicRoutes.scala @@ -1,14 +1,14 @@ package taskforce.infrastructure -import cats.effect.Sync import cats.implicits._ import org.http4s.AuthedRoutes import org.http4s.dsl.Http4sDsl import org.http4s.server.{AuthMiddleware, Router} import taskforce.authentication.UserId import org.http4s.HttpRoutes +import cats.effect.kernel.MonadCancelThrow -final class BasicRoutes[F[_]: Sync]( +final class BasicRoutes[F[_]: MonadCancelThrow] private ( authMiddleware: AuthMiddleware[F, UserId] ) { @@ -41,8 +41,8 @@ final class BasicRoutes[F[_]: Sync]( } object BasicRoutes { - def make[F[_]: Sync]( + def make[F[_]: MonadCancelThrow]( authMiddleware: AuthMiddleware[F, UserId] ) = - Sync[F].delay { new BasicRoutes(authMiddleware) } + new BasicRoutes(authMiddleware) } diff --git a/src/main/scala/taskforce/infrastructure/Db.scala b/src/main/scala/taskforce/infrastructure/Db.scala index f26a217..3a828f8 100644 --- a/src/main/scala/taskforce/infrastructure/Db.scala +++ b/src/main/scala/taskforce/infrastructure/Db.scala @@ -7,7 +7,7 @@ import taskforce.authentication.UserRepository import taskforce.filter.FilterRepository import taskforce.project.ProjectRepository import taskforce.stats.StatsRepository -import taskforce.task.{TaskRepository, LiveTaskRepository} +import taskforce.task.TaskRepository import org.typelevel.log4cats.Logger final case class Db[F[_]]( @@ -24,7 +24,7 @@ object Db { filterDb <- FilterRepository.make[F](xa).pure[F] projectDb <- ProjectRepository.make[F](xa).pure[F] statsDb <- StatsRepository.make[F](xa).pure[F] - taskDb <- LiveTaskRepository.make[F](xa) + taskDb <- TaskRepository.make[F](xa).pure[F] userdDb <- UserRepository.make[F](xa).pure[F] } yield Db(filterDb, projectDb, statsDb, taskDb, userdDb) } diff --git a/src/main/scala/taskforce/infrastructure/Server.scala b/src/main/scala/taskforce/infrastructure/Server.scala index 9cc96c1..3efe8c6 100644 --- a/src/main/scala/taskforce/infrastructure/Server.scala +++ b/src/main/scala/taskforce/infrastructure/Server.scala @@ -1,7 +1,7 @@ package taskforce.infrastructure import cats.effect.kernel.Async -import cats.effect.{ExitCode, Sync} +import cats.effect.ExitCode import cats.implicits._ import org.http4s.blaze.server.BlazeServerBuilder import org.http4s.implicits._ @@ -22,17 +22,17 @@ final class Server[F[_]: Logger: Async] private ( db: Db[F] ) { - def run /*(implicit T: Temporal[F])*/ = + def run = for { - basicRoutes <- BasicRoutes.make(authMiddleware) + basicRoutes <- BasicRoutes.make(authMiddleware).pure[F] projectService <- ProjectService.make(db.projectRepo).pure[F] - taskService <- TaskService.make(db.taskRepo) - statsService <- StatsService.make(db.statsRepo) + taskService <- TaskService.make(db.taskRepo).pure[F] + statsService <- StatsService.make(db.statsRepo).pure[F] filterService <- FilterService.make(db.filterRepo).pure[F] projectRoutes <- ProjectRoutes.make(authMiddleware, projectService).pure[F] filterRoutes <- FilterRoutes.make(authMiddleware, filterService).pure[F] - statsRoutes <- StatsRoutes.make(authMiddleware, statsService) - taskRoutes <- TaskRoutes.make(authMiddleware, taskService) + statsRoutes <- StatsRoutes.make(authMiddleware, statsService).pure[F] + taskRoutes <- TaskRoutes.make(authMiddleware, taskService).pure[F] errHandler = LiveHttpErrorHandler[F] routes = basicRoutes.routes <+> projectRoutes.routes(errHandler) <+> @@ -57,6 +57,6 @@ object Server { port: Int, authMiddleware: AuthMiddleware[F, UserId], db: Db[F] - ): F[Server[F]] = - Sync[F].delay(new Server(port, authMiddleware, db)) + ): Server[F] = + new Server(port, authMiddleware, db) } From 88654e925c182423f303bd377462ab4c4c5cfa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Fa=C5=82drowicz?= Date: Thu, 28 Jul 2022 22:26:42 +0200 Subject: [PATCH 20/24] date changes --- .../db/migration/V3__table_projects.sql | 4 ++-- .../resources/db/migration/V4__table_tasks.sql | 4 ++-- .../resources/db/migration/V5__table_filters.sql | 2 +- .../main/scala/taskforce/common/package.scala | 6 +++--- .../taskforce/filter/FilterRepositorySuite.scala | 8 ++++---- .../src/main/scala/taskforce/filter/Filter.scala | 16 ++++++++-------- .../taskforce/filter/FilterRepository.scala | 15 ++++++++------- .../scala/taskforce/filter/instances/Circe.scala | 4 ++-- .../taskforce/filter/instances/Doobie.scala | 2 +- .../scala/taskforce/filter/arbitraries.scala | 2 +- .../test/scala/taskforce/filter/generators.scala | 10 ++++++---- .../taskforce/project/ProjectRepository.scala | 7 ++++--- .../taskforce/project/ProjectRoutesSuite.scala | 4 ++-- .../project/TestProjectRepository.scala | 4 ++-- .../scala/taskforce/project/arbitraries.scala | 2 +- .../scala/taskforce/project/generators.scala | 14 ++++++++------ .../taskforce/task/TaskRepositorySuite.scala | 3 +++ .../src/main/scala/taskforce/task/Task.scala | 5 +++-- .../scala/taskforce/task/TaskRepository.scala | 6 +++--- .../main/scala/taskforce/task/TaskService.scala | 6 +++--- .../scala/taskforce/task/TaskRoutesSuite.scala | 5 +++-- .../test/scala/taskforce/task/arbitraries.scala | 2 +- .../test/scala/taskforce/task/generators.scala | 16 +++++++++------- .../db/migration/V3__table_projects.sql | 4 ++-- .../resources/db/migration/V5__table_filters.sql | 2 +- 25 files changed, 83 insertions(+), 70 deletions(-) diff --git a/modules/common/src/it/resources/db/migration/V3__table_projects.sql b/modules/common/src/it/resources/db/migration/V3__table_projects.sql index fcdb5d5..f182209 100644 --- a/modules/common/src/it/resources/db/migration/V3__table_projects.sql +++ b/modules/common/src/it/resources/db/migration/V3__table_projects.sql @@ -2,6 +2,6 @@ create table if not exists projects( id BIGSERIAL PRIMARY KEY, name TEXT UNIQUE, author UUID references users(id), - created timestamp without time zone, - deleted timestamp without time zone + created timestamp with time zone, + deleted timestamp with time zone ); \ No newline at end of file diff --git a/modules/common/src/it/resources/db/migration/V4__table_tasks.sql b/modules/common/src/it/resources/db/migration/V4__table_tasks.sql index 89719d7..a80a89c 100644 --- a/modules/common/src/it/resources/db/migration/V4__table_tasks.sql +++ b/modules/common/src/it/resources/db/migration/V4__table_tasks.sql @@ -2,9 +2,9 @@ create table if not exists tasks( id UUID PRIMARY KEY, project_id int NOT NULL REFERENCES projects(id), author UUID references users(id), - started timestamp without time zone, + started timestamp with time zone, duration Int, volume int, - deleted timestamp without time zone, + deleted timestamp with time zone, comment text ); \ No newline at end of file diff --git a/modules/common/src/it/resources/db/migration/V5__table_filters.sql b/modules/common/src/it/resources/db/migration/V5__table_filters.sql index f3da431..195597d 100644 --- a/modules/common/src/it/resources/db/migration/V5__table_filters.sql +++ b/modules/common/src/it/resources/db/migration/V5__table_filters.sql @@ -4,7 +4,7 @@ create table if not exists filters( criteria_type varchar(20), field varchar(20), operator varchar(20), - date_value timestamp without time zone, + date_value timestamp with time zone, status_value varchar(20), list_value text [] ); \ No newline at end of file diff --git a/modules/common/src/main/scala/taskforce/common/package.scala b/modules/common/src/main/scala/taskforce/common/package.scala index 85b9d9b..f6f5660 100644 --- a/modules/common/src/main/scala/taskforce/common/package.scala +++ b/modules/common/src/main/scala/taskforce/common/package.scala @@ -1,16 +1,16 @@ package taskforce import monix.newtypes.NewtypeWrapped -import java.time.LocalDateTime +import java.time.Instant package object common { type CreationDate = CreationDate.Type object CreationDate - extends NewtypeWrapped[LocalDateTime] + extends NewtypeWrapped[Instant] type DeletionDate = DeletionDate.Type object DeletionDate - extends NewtypeWrapped[LocalDateTime] + extends NewtypeWrapped[Instant] } diff --git a/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala b/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala index e8b339c..1629536 100644 --- a/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala +++ b/modules/filters/src/it/scala/taskforce/filter/FilterRepositorySuite.scala @@ -6,10 +6,10 @@ import eu.timepit.refined.api.Refined import eu.timepit.refined.auto._ import eu.timepit.refined.collection._ import eu.timepit.refined.numeric.Positive -import java.time.LocalDateTime import org.scalacheck.effect.PropF import arbitraries._ import taskforce.BasicRepositorySuite +import java.time.Instant class FilterRepositorySuite extends BasicRepositorySuite { @@ -37,7 +37,7 @@ class FilterRepositorySuite extends BasicRepositorySuite { ) .compile .toList - } yield assertEquals(rows.filter(_.projectName == "project 1").size, pageSize.value.value.min(6)) + } yield assertEquals(rows.count(_.projectName == "project 1"), pageSize.value.value.min(6)) } } @@ -172,8 +172,8 @@ class FilterRepositorySuite extends BasicRepositorySuite { for { fRepo <- filterRepo filter = Filter(f, List(State(All))) - fromDate = LocalDateTime.parse("0004-12-03T10:15:30") - toDate = LocalDateTime.parse("0004-12-03T10:15:30") + fromDate = Instant.parse("0004-12-03T10:15:30Z") + toDate = Instant.parse("0004-12-03T10:15:30Z") page = Page( PageNo(Refined.unsafeApply[Int, Positive](1)), PageSize(Refined.unsafeApply[Int, Positive](40)) diff --git a/modules/filters/src/main/scala/taskforce/filter/Filter.scala b/modules/filters/src/main/scala/taskforce/filter/Filter.scala index 926b6a5..2d5dd28 100644 --- a/modules/filters/src/main/scala/taskforce/filter/Filter.scala +++ b/modules/filters/src/main/scala/taskforce/filter/Filter.scala @@ -1,10 +1,10 @@ package taskforce.filter import eu.timepit.refined.types.string.NonEmptyString -import java.time.LocalDateTime import java.util.UUID import taskforce.project.Project import taskforce.task.Task +import java.time.Instant sealed trait Operator @@ -22,9 +22,9 @@ final case object All extends Status sealed trait Criteria extends Product with Serializable -final case class In(names: List[NonEmptyString]) extends Criteria -final case class TaskCreatedDate(op: Operator, date: LocalDateTime) extends Criteria -final case class State(status: Status) extends Criteria +final case class In(names: List[NonEmptyString]) extends Criteria +final case class TaskCreatedDate(op: Operator, date: Instant) extends Criteria +final case class State(status: Status) extends Criteria final case class FilterId(value: UUID) final case class NewFilter(conditions: List[Criteria]) @@ -33,12 +33,12 @@ final case class Filter(id: FilterId, conditions: List[Criteria]) case class FilterResultRow( projectId: Long, projectName: String, - projectCreated: LocalDateTime, - projectDeleted: Option[LocalDateTime], + projectCreated: Instant, + projectDeleted: Option[Instant], projectAuthor: UUID, taskComment: Option[String], - taskCreated: Option[LocalDateTime], - taskDeleted: Option[LocalDateTime], + taskCreated: Option[Instant], + taskDeleted: Option[Instant], duration: Option[Long] ) diff --git a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala index 6367337..0ab37ba 100644 --- a/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala +++ b/modules/filters/src/main/scala/taskforce/filter/FilterRepository.scala @@ -10,7 +10,6 @@ import doobie.util.fragments import doobie.util.transactor.Transactor import eu.timepit.refined.types.string._ import fs2.Stream -import java.time.LocalDateTime import java.util.UUID import taskforce.common.Sqlizer.ops._ import taskforce.project.Project @@ -19,6 +18,8 @@ import cats.effect.kernel.MonadCancelThrow import eu.timepit.refined.cats._ import org.typelevel.log4cats.Logger import cats.Show +import java.time.Instant + trait FilterRepository[F[_]] { def create(filter: Filter): F[Filter] @@ -36,14 +37,14 @@ object FilterRepository { def make[F[_]: MonadCancelThrow: Logger](xa: Transactor[F]) = new FilterRepository[F] with instances.Doobie { - implicit val showInstance: Show[LocalDateTime] = - Show.fromToString[LocalDateTime] + implicit val showInstance: Show[Instant] = + Show.fromToString[Instant] private val tuple2Condition: PartialFunction[ ( String, Option[Operator], - Option[LocalDateTime], + Option[Instant], Option[Status], Option[List[NonEmptyString]] ), @@ -64,7 +65,7 @@ object FilterRepository { ( String, Option[Operator], - Option[LocalDateTime], + Option[Instant], Option[Status], Option[List[NonEmptyString]] ) @@ -90,7 +91,7 @@ object FilterRepository { val limitClause = page.toFragment val sqlQuery = sql.getData ++ whereClause ++ orderClause ++ limitClause - Stream.eval[F, Unit](Logger[F].info("test")) >> + Stream.eval[F, Unit](Logger[F].info(s"test: $sqlQuery")) >> sqlQuery .query[(Project, Option[Task])] // .query[CreationDate] @@ -126,7 +127,7 @@ object FilterRepository { ( String, Option[Operator], - Option[LocalDateTime], + Option[Instant], Option[Status], Option[List[NonEmptyString]] ) diff --git a/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala index 13e29f7..ff2d527 100644 --- a/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala +++ b/modules/filters/src/main/scala/taskforce/filter/instances/Circe.scala @@ -4,9 +4,9 @@ import cats.syntax.functor._ import io.circe.refined._ import io.circe.syntax._ import io.circe.{Decoder, Encoder} -import java.time.LocalDateTime import java.util.UUID import taskforce.filter._ +import java.time.Instant trait Circe { @@ -43,7 +43,7 @@ trait Circe { } implicit lazy val decodeTaskCreatedDate: Decoder[TaskCreatedDate] = - Decoder.forProduct2[TaskCreatedDate, Operator, LocalDateTime]( + Decoder.forProduct2[TaskCreatedDate, Operator, Instant]( "op", "date" )(TaskCreatedDate.apply) diff --git a/modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala b/modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala index 822093c..f497fbe 100644 --- a/modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala +++ b/modules/filters/src/main/scala/taskforce/filter/instances/Doobie.scala @@ -20,7 +20,7 @@ import taskforce.common.NewTypeDoobieMeta -trait Doobie extends taskforce.task.instances.Doobie with NewTypeDoobieMeta { +trait Doobie extends taskforce.task.instances.Doobie with NewTypeDoobieMeta with doobie.util.meta.LegacyInstantMetaInstance { implicit val putNonEmptyList: Put[List[NonEmptyString]] = Put[List[String]].contramap(_.map(_.value)) diff --git a/modules/filters/src/test/scala/taskforce/filter/arbitraries.scala b/modules/filters/src/test/scala/taskforce/filter/arbitraries.scala index ae5a8a9..bdb3aee 100644 --- a/modules/filters/src/test/scala/taskforce/filter/arbitraries.scala +++ b/modules/filters/src/test/scala/taskforce/filter/arbitraries.scala @@ -6,7 +6,7 @@ import generators._ object arbitraries { implicit def arbNonEmptyStringGen = Arbitrary(nonEmptyStringGen) - implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) + implicit def arbInstantGen = Arbitrary(instantGen) implicit def arbOperatorGen = Arbitrary(operatorGen) implicit def arbStatusGen = Arbitrary(statusGen) implicit def arbInGen = Arbitrary(inGen) diff --git a/modules/filters/src/test/scala/taskforce/filter/generators.scala b/modules/filters/src/test/scala/taskforce/filter/generators.scala index 97af167..f7f360c 100644 --- a/modules/filters/src/test/scala/taskforce/filter/generators.scala +++ b/modules/filters/src/test/scala/taskforce/filter/generators.scala @@ -5,11 +5,13 @@ import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Positive import eu.timepit.refined.types.string.NonEmptyString import java.time.format.DateTimeFormatter -import java.time.{LocalDate, LocalDateTime} +import java.time.LocalDate import org.scalacheck.Gen import taskforce.task.generators._ import taskforce.project.generators._ +import java.time.ZoneOffset +import java.time.Instant object generators { @@ -20,13 +22,13 @@ object generators { Gen.buildableOfN[String, Char](n, Gen.alphaChar) } - def localDateTimeGen: Gen[LocalDateTime] = + def instantGen: Gen[Instant] = for { minutes <- Gen.chooseNum(0, 1000000000) } yield LocalDate .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) .atStartOfDay() - .plusMinutes(minutes.toLong) + .plusMinutes(minutes.toLong).toInstant(ZoneOffset.UTC) val operatorGen: Gen[Operator] = Gen.oneOf(List(Eq, Gt, Gteq, Lteq, Lt)) val statusGen: Gen[Status] = Gen.oneOf(List(Active, Deactive, All)) @@ -38,7 +40,7 @@ object generators { val taskCreatedGen: Gen[TaskCreatedDate] = for { op <- operatorGen - date <- localDateTimeGen + date <- instantGen } yield TaskCreatedDate(op, date) val stateGen = statusGen.map(State.apply) diff --git a/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala b/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala index a7fc592..5c2a1fe 100644 --- a/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala +++ b/modules/projects/src/main/scala/taskforce/project/ProjectRepository.scala @@ -9,8 +9,9 @@ import org.polyvariant.doobiequill._ import org.postgresql.util.PSQLException import taskforce.authentication.UserId import taskforce.common._ -import java.time.{Duration, LocalDateTime} +import java.time.Duration import java.time.temporal.ChronoUnit +import java.time.Instant trait ProjectRepository[F[_]] { def create(newProject: ProjectName, userId: UserId): F[Either[DuplicateProjectNameError, Project]] @@ -55,7 +56,7 @@ object ProjectRepository { newProject: ProjectName, author: UserId ): F[Either[DuplicateProjectNameError, Project]] = { - val created = CreationDate(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS)) + val created = CreationDate(Instant.now().truncatedTo(ChronoUnit.SECONDS)) run( projectQuery .insertValue(lift(Project(newProjectId, newProject, author, created, None))) @@ -70,7 +71,7 @@ object ProjectRepository { } override def delete(id: ProjectId): F[Int] = { - val deleted = DeletionDate(LocalDateTime.now()).some + val deleted = DeletionDate(Instant.now()).some val result = for { x <- run(projectQuery.filter(p => p.id == lift(id) && p.deleted.isEmpty).update(_.deleted -> lift(deleted))) y <- run( diff --git a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala index 56f69b2..6d131ff 100644 --- a/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala +++ b/modules/projects/src/test/scala/taskforce/project/ProjectRoutesSuite.scala @@ -4,7 +4,6 @@ import cats.data.Kleisli import cats.effect.IO import cats.implicits._ import io.circe.refined._ -import java.time.LocalDateTime import java.util.UUID import org.http4s.Method._ import org.http4s._ @@ -18,6 +17,7 @@ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.project.ProjectName import taskforce.project.instances.Circe +import java.time.Instant class ProjectRoutesSuite extends HttpTestSuite with Circe { @@ -34,7 +34,7 @@ class ProjectRoutesSuite extends HttpTestSuite with Circe { def authMiddleware(userId: UserId): AuthMiddleware[IO, UserId] = AuthMiddleware(Kleisli.pure(userId)) - val currentTime = LocalDateTime.now() + val currentTime = Instant.now() val uri = uri"api/v1/projects" diff --git a/modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala b/modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala index 53ca4c3..8d58a2e 100644 --- a/modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala +++ b/modules/projects/src/test/scala/taskforce/project/TestProjectRepository.scala @@ -6,9 +6,9 @@ import java.time.Duration import java.util.UUID import taskforce.authentication.UserId import taskforce.common.CreationDate -import java.time.LocalDateTime +import java.time.Instant -case class TestProjectRepository(projects: List[Project], currentTime: LocalDateTime) extends ProjectRepository[IO] { +case class TestProjectRepository(projects: List[Project], currentTime: Instant) extends ProjectRepository[IO] { override def totalTime(projectId: ProjectId): IO[TotalTime] = TotalTime(Duration.ZERO).pure[IO] diff --git a/modules/projects/src/test/scala/taskforce/project/arbitraries.scala b/modules/projects/src/test/scala/taskforce/project/arbitraries.scala index 71a9e7e..613cbeb 100644 --- a/modules/projects/src/test/scala/taskforce/project/arbitraries.scala +++ b/modules/projects/src/test/scala/taskforce/project/arbitraries.scala @@ -9,7 +9,7 @@ object arbitraries { implicit def arbProjectIdGen = Arbitrary(projectIdGen) implicit def arbUserIdGen = Arbitrary(userIdGen) implicit def arbNewProjectGen = Arbitrary(newProjectGen) - implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) + implicit def arbInstantGen = Arbitrary(instantGen) implicit def arbProjectGen = Arbitrary(projectGen) } diff --git a/modules/projects/src/test/scala/taskforce/project/generators.scala b/modules/projects/src/test/scala/taskforce/project/generators.scala index bd7ed4b..70daab1 100644 --- a/modules/projects/src/test/scala/taskforce/project/generators.scala +++ b/modules/projects/src/test/scala/taskforce/project/generators.scala @@ -3,11 +3,13 @@ package taskforce.project import eu.timepit.refined.api.Refined import eu.timepit.refined.types.string.NonEmptyString import java.time.format.DateTimeFormatter -import java.time.{LocalDate, LocalDateTime} import org.scalacheck.Gen import taskforce.authentication.UserId import taskforce.common.CreationDate import taskforce.common.DeletionDate +import java.time.ZoneOffset +import java.time.Instant +import java.time.LocalDate object generators { val nonEmptyStringGen: Gen[String] = @@ -29,25 +31,25 @@ object generators { .map(ProjectName.apply) def creationDateTimeGen: Gen[CreationDate] = - localDateTimeGen.map(CreationDate.apply) + instantGen.map(CreationDate.apply) def deletionDateTimeGen: Gen[DeletionDate] = - localDateTimeGen.map(DeletionDate.apply) + instantGen.map(DeletionDate.apply) - def localDateTimeGen: Gen[LocalDateTime] = + def instantGen: Gen[Instant] = for { minutes <- Gen.chooseNum(0, 1000000000) } yield LocalDate .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) .atStartOfDay() - .plusMinutes(minutes.toLong) + .plusMinutes(minutes.toLong).toInstant(ZoneOffset.UTC) val projectGen: Gen[Project] = for { projectId <- projectIdGen name <- newProjectGen userId <- userIdGen - created <- localDateTimeGen + created <- instantGen } yield Project(projectId, name, userId, CreationDate(created), None) } diff --git a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala index 17b399d..e6d1b87 100644 --- a/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala +++ b/modules/tasks/src/it/scala/taskforce/task/TaskRepositorySuite.scala @@ -7,6 +7,7 @@ import taskforce.BasicRepositorySuite import taskforce.authentication.UserId import taskforce.task.arbitraries._ import cats.implicits._ +import org.typelevel.log4cats.Logger class TaskRepositorySuite extends BasicRepositorySuite { @@ -24,8 +25,10 @@ class TaskRepositorySuite extends BasicRepositorySuite { PropF.forAllF { (t: Task) => for { repo <- taskRepo + _ <- Logger[IO].info("Before All PFL") allBefore <- repo.list(ProjectId(1)).compile.toList task = t.copy(projectId = ProjectId(1), author = userID) + _ <- Logger[IO].info("Task created") taskResult <- repo.create(task) allAfter <- repo.list(ProjectId(1)).compile.toList } yield assertEquals( diff --git a/modules/tasks/src/main/scala/taskforce/task/Task.scala b/modules/tasks/src/main/scala/taskforce/task/Task.scala index fa87560..d8cafab 100644 --- a/modules/tasks/src/main/scala/taskforce/task/Task.scala +++ b/modules/tasks/src/main/scala/taskforce/task/Task.scala @@ -1,11 +1,12 @@ package taskforce.task -import java.time.LocalDateTime + import java.util.UUID import taskforce.authentication.UserId import taskforce.common._ import taskforce.task.TaskVolume import taskforce.task.TaskComment +import java.time.Instant final case class NewTask( created: Option[CreationDate] = None, @@ -36,7 +37,7 @@ object Task { TaskId(UUID.randomUUID()), projectId, userId, - newTask.created.fold(CreationDate(LocalDateTime.now()))(identity), + newTask.created.fold(CreationDate(Instant.now()))(identity), newTask.duration, newTask.volume, None, diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala index 7d7d1af..1e8baba 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskRepository.scala @@ -11,7 +11,7 @@ import fs2.Stream import eu.timepit.refined.numeric import eu.timepit.refined.api.Refined import eu.timepit.refined.types.string -import java.time.LocalDateTime +import java.time.Instant import io.getquill.NamingStrategy import io.getquill.PluralizedTableNames import io.getquill.SnakeCase @@ -69,7 +69,7 @@ object TaskRepository { _ <- run( taskQuery .filter(p => p.id == lift(id) && p.deleted.isEmpty) - .update(_.deleted -> lift(DeletionDate(LocalDateTime.now()).some)) + .update(_.deleted -> lift(DeletionDate(Instant.now()).some)) ) _ <- run(taskQuery.insert(lift(task))) } yield () @@ -101,7 +101,7 @@ object TaskRepository { run( taskQuery .filter(p => p.id == lift(id) && p.deleted.isEmpty) - .update(_.deleted -> lift(DeletionDate(LocalDateTime.now()).some)) + .update(_.deleted -> lift(DeletionDate(Instant.now()).some)) ) .transact(xa) .map(_.toInt) diff --git a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala index 0405211..3648f0e 100644 --- a/modules/tasks/src/main/scala/taskforce/task/TaskService.scala +++ b/modules/tasks/src/main/scala/taskforce/task/TaskService.scala @@ -4,7 +4,7 @@ import cats.effect.Sync import cats.implicits._ import taskforce.common.{errors => commonErrors} import taskforce.authentication.UserId -import java.time.LocalDateTime +import java.time.Instant final class TaskService[F[_]: Sync] private ( taskRepo: TaskRepository[F] @@ -14,9 +14,9 @@ final class TaskService[F[_]: Sync] private ( newTask: Task, userTasks: fs2.Stream[F, Task] ): F[Boolean] = { - val taskEnd: LocalDateTime = + val taskEnd: Instant = newTask.created.value.plus(newTask.duration.value) - val taskStart: LocalDateTime = newTask.created.value + val taskStart: Instant = newTask.created.value for { isValid <- userTasks diff --git a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala index b0963b0..6802345 100644 --- a/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala +++ b/modules/tasks/src/test/scala/taskforce/task/TaskRoutesSuite.scala @@ -3,7 +3,7 @@ package taskforce.task import cats.data.Kleisli import cats.effect.IO import cats.implicits._ -import java.time.{Duration, LocalDateTime} +import java.time.{Duration} import java.util.UUID import org.http4s.Method._ import org.http4s._ @@ -18,6 +18,7 @@ import taskforce.authentication.UserId import taskforce.common.{ErrorMessage, LiveHttpErrorHandler} import taskforce.common.CreationDate import taskforce.task.instances.Circe +import java.time.Instant class TasksRoutesSuite extends HttpTestSuite with Circe { @@ -31,7 +32,7 @@ class TasksRoutesSuite extends HttpTestSuite with Circe { val errHandler = LiveHttpErrorHandler[IO] - val currentTime = LocalDateTime.now() + val currentTime = Instant.now() val uri = uri"api/v1/projects/" diff --git a/modules/tasks/src/test/scala/taskforce/task/arbitraries.scala b/modules/tasks/src/test/scala/taskforce/task/arbitraries.scala index 5bdb1fb..60bc601 100644 --- a/modules/tasks/src/test/scala/taskforce/task/arbitraries.scala +++ b/modules/tasks/src/test/scala/taskforce/task/arbitraries.scala @@ -10,7 +10,7 @@ object arbitraries { implicit def arbUserIdGen = Arbitrary(userIdGen) implicit def arbTaskIdGen = Arbitrary(taskIdGen) implicit def arbTaskDurationGen = Arbitrary(taskDurationGen) - implicit def arbLocalDateTimeGen = Arbitrary(localDateTimeGen) + implicit def arbInstantGen = Arbitrary(instantGen) implicit def arbTaskGen = Arbitrary(taskGen) implicit def arbNewTaskGen = Arbitrary(newTaskGen) diff --git a/modules/tasks/src/test/scala/taskforce/task/generators.scala b/modules/tasks/src/test/scala/taskforce/task/generators.scala index 5666779..842d680 100644 --- a/modules/tasks/src/test/scala/taskforce/task/generators.scala +++ b/modules/tasks/src/test/scala/taskforce/task/generators.scala @@ -6,11 +6,13 @@ import eu.timepit.refined.collection._ import eu.timepit.refined.api.Refined import eu.timepit.refined.numeric.Positive import java.time.format.DateTimeFormatter -import java.time.{Duration, LocalDate, LocalDateTime} +import java.time.{Duration, LocalDate} import org.scalacheck.Gen import taskforce.authentication.UserId import taskforce.common.CreationDate import taskforce.common.DeletionDate +import java.time.Instant +import java.time.ZoneOffset object generators { @@ -34,18 +36,18 @@ object generators { Gen.chooseNum[Long](10, 1000).map(x => TaskDuration(Duration.ofMinutes(x))) def creationDateTimeGen: Gen[CreationDate] = - localDateTimeGen.map(CreationDate.apply) + instantGen.map(CreationDate.apply) def deletionDateTimeGen: Gen[DeletionDate] = - localDateTimeGen.map(DeletionDate.apply) + instantGen.map(DeletionDate.apply) - def localDateTimeGen: Gen[LocalDateTime] = + def instantGen: Gen[Instant] = for { minutes <- Gen.chooseNum(0, 1000000000) } yield LocalDate .parse("2000.01.01", DateTimeFormatter.ofPattern("yyyy.MM.dd")) .atStartOfDay() - .plusMinutes(minutes.toLong) + .plusMinutes(minutes.toLong).toInstant(ZoneOffset.UTC) val taskVolumeGen: Gen[TaskVolume] = Gen @@ -63,7 +65,7 @@ object generators { id <- taskIdGen projectId <- projectIdGen author <- userIdGen - created <- localDateTimeGen + created <- creationDateTimeGen duration <- taskDurationGen volume <- taskVolumeGen comment <- taskCommentGen @@ -71,7 +73,7 @@ object generators { id, projectId, author, - CreationDate(created), + created, duration, volume.some, None, diff --git a/src/main/resources/db/migration/V3__table_projects.sql b/src/main/resources/db/migration/V3__table_projects.sql index fcdb5d5..f182209 100644 --- a/src/main/resources/db/migration/V3__table_projects.sql +++ b/src/main/resources/db/migration/V3__table_projects.sql @@ -2,6 +2,6 @@ create table if not exists projects( id BIGSERIAL PRIMARY KEY, name TEXT UNIQUE, author UUID references users(id), - created timestamp without time zone, - deleted timestamp without time zone + created timestamp with time zone, + deleted timestamp with time zone ); \ No newline at end of file diff --git a/src/main/resources/db/migration/V5__table_filters.sql b/src/main/resources/db/migration/V5__table_filters.sql index f3da431..195597d 100644 --- a/src/main/resources/db/migration/V5__table_filters.sql +++ b/src/main/resources/db/migration/V5__table_filters.sql @@ -4,7 +4,7 @@ create table if not exists filters( criteria_type varchar(20), field varchar(20), operator varchar(20), - date_value timestamp without time zone, + date_value timestamp with time zone, status_value varchar(20), list_value text [] ); \ No newline at end of file From 75b73b09328089e8758bbb01735564f10f9fe7d0 Mon Sep 17 00:00:00 2001 From: vder Date: Sat, 13 Aug 2022 13:02:57 +0200 Subject: [PATCH 21/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4287c42..4fb0ae5 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ and simple pagination based on size and page number. * Average volume of the job (for jobs with the specified volume). * Volume weighted average task duration (for tasks from given volume). -Access to statistics should be parameterized with a list of identifiers users whose tasks should be included and the from-to dates in the form year-month. +Access to statistics should be parameterized with a list of identifiers users whose tasks should be included and the from-to dates. ## Usage From 82bda35ce4b78abee4643c2fe080944065bac7b5 Mon Sep 17 00:00:00 2001 From: vder Date: Sat, 13 Aug 2022 13:06:02 +0200 Subject: [PATCH 22/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fb0ae5..b568d24 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The task is to write a REST API for the time logging system. * Timestamp of project creation for accounting purposes. 3. The system enables the author of the project to change the project ID. 4. The system allows each user to log the time spent on project (hereinafter referred to as a task). The task consists of: - * Project start time stamp. + * Project start timestamp. * The duration of the task. * Optional: volume expressed as a natural number. * Optional: comment. From 774af103d90f4ce233a5cce8aac2129294d82a8d Mon Sep 17 00:00:00 2001 From: vder Date: Sun, 14 Aug 2022 16:02:12 +0200 Subject: [PATCH 23/24] Update README.md --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index b568d24..f0d4641 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ and simple pagination based on size and page number. Access to statistics should be parameterized with a list of identifiers users whose tasks should be included and the from-to dates. +## Tech stack +* scala 2.13 +* cats & cats effect 3 +* pure config +* monix new types +* refined +* postgress +* flyway +* doobie & quill +* munit & scalacheck +* http4s & tapir ## Usage From 5a50660612a9c732a8b5c2623e31d083cac564f8 Mon Sep 17 00:00:00 2001 From: vder Date: Wed, 24 Aug 2022 14:27:16 +0200 Subject: [PATCH 24/24] cert --- "Certificate-Piotr Fa\305\202drowicz.png" | Bin 0 -> 785074 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 "Certificate-Piotr Fa\305\202drowicz.png" diff --git "a/Certificate-Piotr Fa\305\202drowicz.png" "b/Certificate-Piotr Fa\305\202drowicz.png" new file mode 100644 index 0000000000000000000000000000000000000000..8849a44f276212721f5eb61cfde8f8c798e40a38 GIT binary patch literal 785074 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°s*&Qgn5j6T23i(XqwhYF~nC2-NTr zH3WO=@xIFgg2nfXm`wGng}^xz`=qZP__4jboVgk(e3ag58JXJ`)3G{4nwL*uIAx-> zk~0?K%>8=)2yjApsFCP{j!L-JSTxY2tC@NqV+WjVUB+;7dAX+WHIxCg=k9fd94Q1L zuJj=6-^!&VK_Ihfa`uj^emu@fZGV6DloJJOgn)lGRV*q4)gVqT1{D?tbX8&ne^&Xb z=c===eT?Ji#7aX8mgdAO*OXxIx}U9B!{eKn-zRVowP>K^n6zJEY&TT zJsJu*UqhHphZ|4uJhPKuLl3}?u!=*Yp|9IX$eJ0&5k@Ek!jS4lITv)X-JT*3rs zgX(I(vr1>;FDS7=isirw!pSS8m{Zl7H(<+CuG7^lw@mFNqgO@MGkIAh&7Zw0hA2Y0 z`sR}ppEE;?d~tnbp;DA>dblB`jbRAI-*wca-4Dc%kC3F|?3r(_g#(RaS7W8- z8Uxa&u;D|zhk&Sdmb+f?(gM-OUY}psiu77m9$9!Y^QaRz_U+^%^u78EfVLwgRVep4 zLhs8v4B?LuMfe+#mU@PpuZ@qn{V8xUqBM}=8e6v1a^&W-q*mcrUW_svQID{CsSs40 zOFtnwKyk5C{4eeJ?C}78rdn)38y}zY;H{?c39{;M!xd&K4io#oG2@KU70$5<4AD~T z4?DDU`;G~&@fJ?qEwI|OlNajVH8UDw%C4f}ETtul?tU-7ZJm|_uwCIQDH^&@AR~ke z&4SuZ(JcMOXnwybgRbMbl?4o^dJ7?~x<~XtqIdzQ-+~CnmtZ0!tNKJGI7O+~h34rE z)eCjg?^_dS^g-~6Rcp;pA~2Hzvt$w>y_+uMC#*|e$;&+B6}M{cl*7m5%6>0Nqq6a$ z1Cm{L`Yw}fKA1Ee@NsF!v}>Q&0j^-r&LQFciYDgrCw8dlEH6{sjOAju@#ez2!aJw(L%&aiIxrD3ty!Bi4;=b@ zV>9RowiniqWUrwz(Ydzy$D7F@h`XjMr(yEz1BIQ_0>Kj}J=DdLcnT6178VMP78v_I zx^s9*MWqv*oe?DTA=MLh@;F}~QTWo-gNjg6X6tTnu;Oq8YTXZA%RAN8_gP)=5?2@- z{<8an0SG~PUSW6%1z2SHooEyJee?<{aBq49e`0%qkXR*!4qmLiHF_{~$63pRoK@H7 z(=+?^^BDvp3w-o{B^tx3%1e_MZ`9Z&|s$LZ_R_9SjKk&Ax9s{=d( z=4E*^gR+xi@c};7GGhvVK z<=+3MHiu+M*eipoM$Q+y<;zLHL8n;Z&i9r?14SQp*_c=0&fw7rpfTvxO@>!2oj;Oo zMG*Fc{5;9d`{Q|D!fWn`IxNpjmeS=`of9}BV3ubx4@$u?Tzh)xlOimGU(*p5Fc3J9 z-Ww3ujTfoCkOnXb=T<{$UiAo|0DX=s{2Wfl-~H|#G(S;^%NxBUfA}`;zQt#MOY>$4?l+SY>L7x*i4|<|&kY}3 zRsx3NqbR^_QoYFQ=VS2jf8U7Co8xx5l(r-7hVKe#6|ymcKbH6DUD7>a?0Z2e^8=}S zQbIop#&4U(i^PkQ0&6wZVvni2*8$&IW-BA)nE`1e|3A{`NH&vmVfv4^j#E)vU#Vjr zo}B+DEr2Xtypx*h=1rrCSGub9pf)=1M?|85^!0h>1oYb%yl75PG?kfe)D(!BW3@G| zMUdEsU?*C%)Xx_A5Wv?umtQc3BaN4y)>0#SI3ybBP;U8P!$}0>?#1#u+f3vW&k|O~ zAe{7I+PUmx-u;Y%cp5vHE}My&-U4syoZ1`KGI-?`%S-MVRs zDSfXQ-WZ)pa5{)(c#2qD7RZT*LG?unTt=7(S>vfw_Q0Ol>Z&JNQsrN5Z7~LDigjt$ zVeRkNsNSg^`RZ~>-xv4mpkBDbXB}$0sd|&5mB!~xisuHGsRtB@@v)gug|p5PV1FI# z$et!5>L*&j#I@jcudicFeo;eEk*-Pl)U=ViabHq6i z!xV6uewoZnm%o>bj(-S3+iW?-=fc!=m)yH1e}(lqz~P@WN0yi2-~m&xT2XLd-wUIe zDa~GD=&S=P2t!eACumnjM_T*#RZ(JnhQ!yoT1&Qnvft@;Gy8e%{^4Zyt13})O@32r zTcVaWz?f`}ns4z4&Y(Y%*SrFou_T_`N;rdW7`9o0!pPzP@)+dNtjbc{`Z6tA(vVm1 zZB|h7BG)}@Q$ORE+B=u#&v!qc(xKIS@omlCOFVJ7ZY&9%-{GU3kK#^S5zYt)>6F_e z?KU)q#X0R(TYYQT?Rgs+XPVnJEUSCA44XRL0&-t=u8cgrt}%J^giOrIj=e4H_8*+i z-qiE4XjX6ref8nQg^WHsCk<8X9qA-EhUq^Jzfv#pcOL*}2W!~z;1k+?+k>|+#g%&w z;v6F*oM3mO_nwKE7z?=*mzk@Ubl^9+ZB0VTff+N6|El4$S&ex4fA~>7V~i;3I+@YY zCo43!&r*Tq{qf5rPUmZ6es;x=U27V1ak$*C3*wG9iukW4Aq?|$eD9+%%weVJFxT=0 zU~{<`HVp)+3b+Y=gs2JtG5uK;jQNNDP+%lE>;Hj~nD;$Shcd%ct3@6l?W8$-kT)Ec zK{?(xT3T_CkucYH6aU?0Mbdi_3OQfOW?Vk-BL%k&mqVMJe3w@ZwW!u{@Jr4JMT9+6 zCz)e^vf9__taTi`WX59cGUu)AUA3(Ibi64*|ID`&okrAE?@e9rOMel1*_#Zu851zO z@b8TBj}*N#PRb28qryuuDp~W6Sl|rHk_l-DmhyyYiML?FYK;&9rFmH!`9qltD?{5 zqKlKP1P++L&ps@O8~$*s8{O?@6TwXHd4k8&Oq9VT7Y-JLJQ5ElJ4>SngFVsJm%%j97qoTJmq+ADiJtt~x;dvQoW@$+@|>g#QG>*G7@ghH-pYUwR!^ z?C*Sbw>!o?5^VLDfwLBo*V4>fczkfPqo#J_OXdO2tKH1TckoYdCYGvqljk9o! zBz+SJ7aEQIWaf)O^(#?|-3RA&$!g7gYuYnWBUzbz3LiJkMI|Ha$i40f^!Uni`Ef9; zWeXBjJ?$pAdz^6=BCS;1B$=%^^gJA0)%aQfGZ&4^2Zt+8&KNOH@Q$MeMn6eUz)1*Z zs6!6#poWV@^-JK25FfBm=5PiQ``pfu*(`8;NtqiB_EMb|U})&>GA#tP-N9#)_otBz zs!=NY|M{gM@DlPrdoivfIGYIC7<3!PSA$Z zhvoR|!c}uSxSb`EuGR}nX43J}XzJA6i`}F>0q)K|ZBsnl_qid*qN|20tu_lBC7dT^ zzLW>m?MF~Cke4+jdygqk@yu~`p$CbUr(l$omi(d%W@h%`IGkY3 z!4x`_SH0B+bsqCx9(io@W~)hF*P2n=@h2vJ)KQpIVLX9d$3FlY5f+sCubHM#1HX$U z5bv_oWj4uJV|}slXPD*!EfR6uXe#J|^5C^X)RW7JZ+=}l9Y;%F_aXPwrqIOZr$3g-qf=92|Oi(Y;l01Ah}49IK&9zNj%gHLggG(JZ-^#CWJpn7)uOw>}CB0+Q|4|w2$ zVGJ58eckee2>*ZChfrb`nF^OI2U5*r#lW@QZ4PY>RD#93jT2^*bGSB?D38Um`_dSQ zqrFQ=MfiB&wy885|2@rCH|bWvgFRm^=nahz2@;n4v?pnS22>}s z4Iul8x)NH#yk&Q5y&^)~sycuC{5fq^{EGc<>6Qk5|JC+Vm`vaWRKJaW!7W9R&SJ!7 z-pkmg*$PVa!NBe?L@uREm@f9n4Eu5VUL}C34RE`N^U07cIpNP{+g$_eKHNZClo z68DHjgH{%`9l_;YcqBj=(v%j>j3UoGtB|&VsBfq}qnAX$!%o}R)e7(}+6lY_7o<6h zBAV`C+!(XSLzrx&(RD(S`dGnI1WhU#pJQQUu_(NQaP%#rjbPdB93cBbB7jtD+Cty1 zFm>sPBykT(akg}hTEsBL%G-MD1x*s?`KKzI{kB~GshZq~Dw#~$z#c{zjDF1q23Y-$i;h>Bf zqI97NVT)QB)Ip5{ac;Onol!Y zj$k2Z`nizUg$0jVfYUo;BMw(bvbyR8Qn-Bx-K&aCZF3FlZ}NbX$VbAJcpP3boA+7& ze|q(wN_~qimWM=^Ej&I>-0uiU7howxN0GhXH+fgaH6^%;=cxQmHS;p~8IAz!>`g6fj{)(8|^fCO2homcK;JcdRG9qkhCY-e%;ZZ4+b z!Qr4zO|+qAVf3s#>LBMz8c!>p#$>g8GnHWLn8C2+P4`CYU+$n>oP7Hm%TMz+F#fZK&2gr2`tHd=XQT(dICw74CglG-9H4^}{_6Shx%h|m=l$F!+ja@= zZWf%r6gjPWwgjXkI0Ada2={BwjE(+7MJ?9p_JCH*#y!z75@_)s;t(UF^ZrPcYzr?8 z(` zz|`U&=k91R7Gh}7%eYjE621(PVF5c zCVWSt9Ew&jUYERbe0AQJ@VR;t4yLvX5 zD&ZGu{s8FUg&>7af8l(d2nZm?5F)FLq6Zg+n5&E}D8YHWJ8QJ+f#0dZJ6MUwNPi2- zNW?ULJ|mKLiD!4Ipb&FUly4s-ooozmsQYY7_p3pnBouLzy9_`BqHj~jmdmt77%X4`ziO42_asW=33F+ zQ(F^;B*Gc~1b@J5Yv64@E1wa|D9eVwtr{ki@hCj0PVX-B9+e`*2`qwV{^K7@rvMLH zIK%CUBD?~=V#HG`OSu$^t3CPi$34GTUYiR zxW@0vKFfhqvk4e9E&(>eSrN2i zFCl2B$fuVDrLD+ffY(>po8e=m?4CBrGBN7OOXtH7KLL?kHkO+ zMI)6?gYG2BcZjy^YMV1MFAPo_4N@=|f8oD~(ri(RrVNhUIUhHQU&P`Pczx_vNOBXL zw8wn~v8kAR9|gz#kppj0MBnyDw18Z!J_?o$JWh~Fn^HY5ukgf1&EhfGuSn1?;V1aX zTem(I%PGzdk)b5`H9-Ff4YVQjPa*E|Wp|`ImMrucZ|1969nI(mGY(1S8dd&|w{mZ8 zP>PYV=c!oVE@_$=!Da$Pbijr*ntlsHAW$T&!86vyluM7*biAJqrihlN*PY>6!v|?# z>=3SmqS>lxVbJ}ETs#~wWcZw7#m@OIP(?f+U+UI=9z-s7z5|f*!`Fv-C7(XX#~SID ze6dMBURurZ2;0BZn2vpa7WH$N>d#SG(1?w$awe4ozgc40_Z@kM#N<6Duz6;IUfWHSd%miO>S-e&cu-cnEbCQw?b;7|`E22W?TF z{C4c?SL1mh@a*aE9Q3v&_Jevli_;m%lz77EMPA z>Xt{eaR8)PfV-YldP}%#Z=wMyJ94}od`u>e|b(x`xgKw~8@*7_RhN4?bB9ci{ebin1Lfw(}In(TPYLZ~IFbF&mXF#U+xF1ac$0ryEMC#sKZ~R}a9Tq??~!}HVwx%0 z33A!rIIs;+so1>SbpYg{_21p&n$=Tj{`~{)`-it1d#`r>W&M9W;NFhaz(d`=O8Y58 zOWg=TT<z!l7bjcIefRabB6IFxF3(Xz?*DR3e#P@zc)BT`*c}x+<1PqQM zU)dP|*4E4=7cyS+h4{690vC=5R+;JCPwB)Aed@Hveq(|5=QfCRmuj@mX``$nNKrhr5JB9Mf>J8n4g))B8{zG5^38j1%=xRxRDW zEuKW@;DGTM$)4_|yjmp=W(1;M8+fC{%fb&Sia8!pjBVz9uEpr8oA(AMl4`~cc}rP_ zZWk3Gx0C2&bI9eA*z7(|&1EkyCOz>katu)T zzf^Wy0{l}m`RJ^2u!ntTPmlr=57FDJ-NsM9N)jdmfCBM!ulW-e?drz%Q;PjNCY(Z! zL<@umX_VCh;!<}XfPmn?yXI!+V}BLa<;?`}BVj9dYrF%zL!iE>RY`IRh9 zsVIrnT44G~@A7X3E{>~HjJN|ZagAYlZi3C=U%!9BKSlcemvVdmL3>u2EaZXCR}k=J z#}u7HQ=YY&b8#HuBIO8BC1t#L?R2~9%hjukH&%ML=D!kj7TA;00d@GP=FUE=3^L%9 z^%*?|GRCE+w&*&ht;Wf=sjmk8kSb->3wvRJliXi18Sj~OK&JOB9vy{H7j}ee z?rh)suR9l?hOuX+hN`~e&VLw{zukRG={ax4?*dG>?eXh=5YV0Yj@2`hw~Z)Y7J>wC zP#{xrTX1fzU`y|1NS{DD^=J;0=@o4yz!`^TDixI54c;HK*npcJ=tXq9Y8GFxhJfvy zsCi|jbcu?ZQH@2wNB5P=1w?QgPUXu}ovjFRMpIuy^7v0INkxG{dJe(3>#^bped%7w z*H8vcWS|wAyCuZol<(=dZ{LH`fYYpm&j|fH75g9aS+Cy*A+!I9?@90#{Priqlr%87N?ZIptMLCXzH4?y(;<~4YF&`f zGuzG!)_wcq^IcKIlAhofdGcqjo@^cwW}AaE$}wacL{$1VI?KN z(FLU9|F1Z@V;1Z(fG{J(E)SXqDuEc$zvS_Ly(5{cWW0pTaU*~pIZDsZv1kSN-0@=h zpS03bBnUa%s8A2su0Nm&fHhb-=Y&&~olQZpNVRJXj=IQsjkGAY(0aQ{L`Klc{6D42 zW$W6Z_WHbSe4Q!j6zuxla2@U>0}2?$;PGSeNJ*ovt>>?C}k7!_|0%rDDBtmm8 z9->h>TOur}5ni8q@XU!ckYgSBhF)B5m%d_#o6GO3(Lsp@lQ!&LC{zphMJ2VM0fOVc zooU6jYPQC%IbR`r3BNX+?x8i*%V$^+4Nf8y!LiOfw!-y)koV?MO=aJ@XjM^4SqMnk zpwg94_9aM34@p$OsEB}oAYBv`q?aB-nnF|%govOh2oX?v2uR;0BCXOy+R&qP0Rluw zLP$vSR;)U|bAIQYJMKI8o%8N{tU`zG=E%`V^WxtW4(@`HOC zq^p*1s%rqUrSCt;mY9j9Nhfo=8~1njp*X@HQ@hACYV4ZR00X!joArqBhFrnwoS8M5 zemG^4F)aw02#QkkaG)ub01%~s_rvY*0SFK;B|fEY)=!cK|M;}&q?V7{us#(KyipH`4T)#L?b8QI6K zbFmi;T7Aw5va_r@vR1CJq~3feGf%5@Uw9WObvXx*srA*GWtj{*YI}Y%_200b-pUUZ zvfN_Qjp-nrfY*H&O^3$=$XhRib6WuVECiLrWYVzEkdsrJWR-vIHQ9>#fGqiqiVP$& zH-PE`5P=wYLgxU|?3W{LBJN?-@g!pwaJ~E6!1u!Q%~Tjs#E|tR-=_dB?LR1nSscq= z#|ubyauk}jD-DW;we-8cs3(TEXEfN0L3{U*$iV(SFvyNhQS1$UM&LYKE4$P zL%iMP^-7e~nIMB)DUsEXPQV|VB+C}PkYu7@^gDtvo^&CA9Qf(YzcbvrEfB~d&{;_; zT92Rqq7=8+7aaZw*ia8?J z$~e?<(qw`o2(qTTw<&~l(jQ)HdJsp4sU_w+2swU=XA%nk>f(U8i?k?(#QYp7J%c6< zcDlNi&tjZX6Gbz5FR>RPKK8LE?PEI5cIBScOa@^(8dAbyv^;BfsJL1JN!R}g#RIi9W04I6~1 z1zkqpG{s{P7>i?Yk29%>uv(Jd;6b(cfq3TJ76jy^tJuTHo_Y%>y{nrK-?{k)Yw2fo z!5QbmMhyi!BYfFy#51Y1Kj=Gez=GFR8AFgO5+h@Yn>egf-AmtaAJEs$eW&DZ2MDWR zLQq;vbGe2pn_m|hq}|)e^K9+h=RWs~*-^UD$59GnFi8DEwtm7MGX4C=N%4!ZrucN% z8U5t^$!`D4vZ+L2qzmJI9{r~Rn`_t~z+zgzbRj*OFzB7XG;(uY#94>+oKNh(;I+2i`LMnyvh8yX<(mu(IqutX@ zu&MT@0_O8eJO@X{!Z+U+&8?cjISh3f~9Nx3mYZ ze|VPmk`82#FN8GR=r}<_5uY=Ec_w*Y&3qTljKzps1m9GFqzX?LxtR5by1iW%6kkYQz4OoRuOXCx(m$b)EfdI{q=@NVjnV0GF0l zoaglLKp(*`>y~R>GyXA=BQ(~;)8mBtKNMKjST|hSn~7`MIJ-~uwt{zJ5K@>R)u%QO zSwGwZ$7;*wUNzVU1%ehVVcR>ge}KXp!F8Ra53Q-M%etY*5tl|(cMc_1gGgN9x2+*Z zEq6Z+$P?vmvGzn0jHQ^oISwoSx!RoC#RgC~%+%yW%k;iHfP4ovsy8XgSD5z+HuYz* zR|j=t0j5}&{;K;0tK#y2ew|2~yBy%@lTXtU?97d5n`ixBQKiYS@bwc5RV0YoLi|O5 z08mpc0tM9uw!CJ~!6^$CzAW8@!8`n~x|V;ZjVL>+UBi*_wiv zvy7J4*{!Cf`Qdk>ADod*dI_?dzK8#-SZgO|?cuVfCaN`7wLm+rZClRlGy5BlbE2MY z2g9n}B>40ajHW;*b7U$P(}mj^-gxQUx^ zi1E`cj8pk(o!z=hD2U$7{X2!yWOn4}v*dGqzk{&5N-W^}T_gjJAW$!C*mzlF+}GNi zlWw419vumlCP)ITsE7)-2^IO!|6or2&$xa6Irf+(AtK?Y&fmQECIzd!&jc-72g>s_7pbj%V&mVTVrj8e1+(~x?pq&i;ii^ zBw}<%-LP-`ZRzWy^mbWXH$hc?;fLPks_|!o=np2nTjji-({oYbL>Ia)@rF~t<^5Mw zGQME47@1oxz5}DKy+ii$MG~iLctIM=Gt`@vtxb*B$+bgecvTbQcN5azbe0=qq>~hb zY!H|3QXW{q)_=YTK+F?t8QFhkbmk~Twv8D#RcclqZs%eit8$60e=}-;NDuHYAOQIU z*jL7rYqQ#ak#qiMtghYs*=LSWtY?_=I0nu?VP#v};@`46-2z=!)4T*|U6WxV6+tLW8kM=>RkHFU&jA zg;d*rCdi4VXcTb${#O`%x6&=mAP2|qG{Jg7{avjsZ-*5J)rPr4#w1r+^56F0tqe56 zSqmG}IFmNT@Ud4i`wt@4GoN3o&@#Jp#89rhH0HC^?YO>>gNcwrEq?91X~D`$!*slU zFHn?xLceW$LZX62&$cmZmw%O3>N?;g%TIBv|9dLEl|_)_g~yT7m&3tn+3xm?=Fe!> zr>z4`+`kEdMT&qtNVr2{r1|DNy^5@?QBm;L5RI>uN#W1iohRHBTL8r!uy&vOfKxxS z61AE=xIaft(i2ePZdPXIkO3NUb9qJ|=d;l!>7K^($zUd))X*V?1+!E;nh6| z^rsSN7^^hQZ{C}q{d{fW)BYT~XUw__4)TnCaYP@~E2!)JqvPvJwkk5Xa+P<2k+s4M zsluU`Tp|xxnMej)gP_diX_jJ{2a_Ixerd=TDnidM>p-fXfT)hs{t7@id2-iutBic62|?5CdDYQ{O)*S2dO3 z2oQLB)DVLJ;)V_2Xp8^Gs;bF+Gx^EJ_RE6%!-MH5EA@m?y%gb~j3T5^e?M*a6-kkSy8>QnymF4=f?lamV+OS=8 z=av{(G-3|)ilkKj?hChyI9s$J*kpCTeuD5Us@Qthn<}ng9!Qh^s4rG={k67%@V4Av z6e}=VAABoVgYF>O)%}C9Z^gh>rSh|2__iiF{lyJLpZW(G>%V{0G3iHy-=7XS*Z@M_ zM?-$I@qSKbTb&3u2b@27);AU#AfsggDo51oqP4n>t9Txy-~ZG1Ps@n2AoB7b&kU)xCUP?;c*C0CJ%rS zA?!7c?>rncuznDb-q@RGB$$77AeAa2B=y=3k zKmAboNc52>39t74wDt5Kzx;S6^X86(mrbf?GOz#e{ciK$F8pG2+IY*(=p)xo-#st2 zBD~7(r=_H<=#JO@{+>?e6BqQ@FZF`Dm&58dgOS{I{=&-uV(Oiq+bO7spF$LGcCGPHa5jZJg(+#t=nZaFx8EY;E_6l3_d zI?vGPym!VPS1VkN5xLF5B_W;BF;+q02AbJ21_t*-JWf1@Gm+`9HQV3d0*uM*y#W!A zdME*`G_~~$zlGRBgBBm_8LgYQ-B-(pD>$dcw<<$sj^njTO)-wrOpB;_R=dhMR<7aA z^h{C9QBezVFgR7uh(P7#Dh5TT(*Y-tk!J$LA}M0%{sTzJT682!q&fQA*zDUzuH4OE zf}%Su`-fgRvD_Z%e#|lTN;FG5(;W30=w0IajAi*c;&~V3XKWfWRJT?yMFbcRL9OsL zrpR_{sJC+}ih$@|9eXY3Q+)utx53yjuC_i`aZ`EF{r2y@hQu}Zoue}pmN{3n>oUjb zXzjG{?yxISBSB#nkhx!I8X9{R4j?$?553pK!*!-R`})M!0`BZV$lITBF()r8ABhsZ z!jqIP`mE5bKIm;#Hb4#rG@O1dp5oDSNNNt0R!Yo%;r2pzxr}YIlZd)U6=+B=?r2DN zlN#Ja@HklT+}NVFkO_CH#%xmcktpKH5~+1tox5Tb(;pk&`mKOq^&1|l`*^JPw&Tm$ z$j*aJg zPV@`XWlQt~snsf#UrVII{T6prJfO51e0z+0K6#V!+@lOZVe^h$R>&OuI`SN>I66bCO=Ez{ z_qP+k(WmI9|rS$BV&L2_9Uz6P5e#mJL*`9-9f3;lb)?c}Vt(lImj~icJ5ao^%gn13t z6YClKr>g{WlZ_it8Ap-<*81B}r4=|9y)I8-zHF;uEcm`p;F&hV*T!#><9@j#yf#t@nHM+k%2O~=dJ84se&X+ocsn9hnn zvsDIyH9Uywt0mH6!O#3<-;g&Vw)(=P8RTh*#B<^XP-N+Km`&mc`SjxpKv?prTaJ0z zBkdwmesjQ;uw$`QspvD_T^6I9TUQySm(7Oz5B+Uk7AD?BlKQ=3a7G$#qa?y%u75Sx z^f%e)FC2@PdPW|*oHYS`3s{oMFm+9^c{l7m^kFgmTU)>7sA$8dqbQ6THbR!*QNQ-F z!+mL+OP?0!2Fvo2dKj)V<$3Q`=V~bB;%_;>Qsk9I_C=Z*+S&RVR7y*9Cr+uF&3eF6 z3}F@lwyDrgcQN(HaG;*&<5C<6_My*jwTHT+2=W~#svP{_`vl&tq>9octATc)7TM_-FW!y}h@R=iPRk#AZW5v!{D%0k>>JDtG13isFv7EOT;2El3la5zUy z+TbKzqgk>M?8wsAEtrDd<9sJVr|`XNy?qo-WxYVMdy$#c0)1!!0|VFm7F#srn~coC z``NA5=T%Sym!!(JzQ}LcThet}HQ*vNJrzj!*fmI7U3H! z-!Ngfvu`to58{#E?F^bQJCt?y&W8*ehWpM&i&R}BkMGsriM}hXBWnqAlFMWkgtBSq z09qj{NGCEY&v5A@8yOVfp)$wqW(#&uP$NdTx^zkadT~eHT7%R0%aLf;afJH6XaUp^ z84a*zO}tjmbTO6XfY)m-MiFeZbF`d`=_tCiUssMPvzitL0Z7Nv?8l5cns<{v>Mi)QQnt%;^d;gu!?BS2-chxA6tr~K0GfxX;q9zkj0YaTQ%@nGV&EK z3}5frA?Co<+m39PwQvjm@449{#wqflsu6qn)DUyRagvn+reEMNdFsE?Ao;vl)!HKv zAS57Sq;z~gC)4=G)Jn-q$w@kl8DXNJ>~i{1G~(@@dPcW?A5i&D#~`-}in0`yJ?fxS zdi9FVDQ2njegk)x|5_Woe8tLhEeceUfJF!=cijNaBLA@=Q}FrQI zEfHO8rl9O8rml>+csLDpC^qYjJ>bX9}5B5&@Cv)8-k?>KQ zgV7vV^~W3j$UBa>sF_mu{asr46SnBOL&z~!eL8Kr!wHvtifo_)R6{>-LT)#I$k5W% zvAL%pxg6wR=XZa1FJSgNp+3YQZ1Z>7+6OEy=^@@+xwfPb{e|0qAb2)8zX{jBwSxsI zBay0-Zv5u=y^+z8?ST@y)W^%UdD^gyBDEW=&5>iXqjY9=3Yz$BSRd5UU| z3j%&PiiLsSEp89T@_296ls*12I6+w#CTj|dc$n^3wfe_e!-3Ng5*MyRKoW1^6>4?c zXtEXUh^Fub{Ue9zHh9r3jo$^@+grr$5gOK0%IECx7punn0{{=EVJ+%7Y{9Ypi|*Dw z@$g{mMg65}{=56XmD~v-Ycry>#FZXNsOnuTVyv|k2MzMQxW&h2g<*3qPm&vMw2cB3@DR8<; z%OOKP%(GW^7g3W}Ylo@>{RQ7G+(U_fzw%_wPviG-D8zYEWk&DW4Jn15G3eObTX z>HH5I;^vBJ-7?X0gk7+?i|T{pWi;(HgC01TyQMHZ;-b{jHa;h3k7o^ZPEVQ0Zo-== zuield2tcnz&)=6Ha*{5+!ENt3Q*;PD`#yeSLVAuH!wo>M8}}<-4{taR3=S-P=24uM z%jt@JY)s&CNrGWvhmDdKHbd&)SX^P`w|TeSIl0+bkNjft!>G^Ztj4v8+WdS&)Babc zX!tCc-Oo*J26wJ&OSU>g(;4^F4s}A<2GItP89Z3`m8BH@(*~?l;F#?%)gm3ZcKXO}b2BG+G4nsYDB=9>1N_oL`9 zI-^VsdtSy;&x~K}=-yC!5k^vFhuyD0&oHCPBQelRoa- z_M7C{=SHnx`p&>)O7-l~DM|w_ihj z2eiMzpNA!koZkL*e%itBd7Q1TYF9`i&v9iw1HZ940CVC+m2*iy zZ@MVm%nodnRy`kjol)Y~ZvoHNq}U!8wgoI+k~^w8fdcT1-UjxBeu|bgW_@t2gYdY@ zFeS4(R0K)dqmC-$$J+Z^hc~)w)R320bNO8FQ&`;}=pYhd z{h=wq6D_3CQEn>>oT>ol%=>Zc zTF1#RaQSr7o6YX>#bX6bjXM*YWWFvHL(st$I^N)OhfVI^Y6ah(5G0R~=nZ zEfSv6Rp(loR@4;DMuk=&_2u^S_>&d`XBer_1i#qpswg8kBNy^mt*rA8_YVhQ z&CQBeBMt9d3j9hG@tke*9CT2v<(jhQ~In^5ER7()^S)yI;9 zwfWhYs~<=?PC*0K?rG#A&x4DLed5QwGv9uau@naU1`oCd>slpyZ`j zGjPFwb(`m*V(kp>e6lBVLi5~Z3KFd%y zu*L}b8qpW^%Vb*~eKnjKhrMGz_&&SldZEm%+x~D9wxi^#5>uu?huf^Bu0L1T2TiHE z(pZI6tN;3bs_AXa)7};sv3jUj3d(Gt6T>}qylYynaz@uj4?6J9N{61`ysS@uE?7&3 z$r>#2v6m)XSB6{)POPtY$YwF`j{eNa9F7oIOmfAe%^{Ga7l zCY*~=nD4s7g*8}!Ux;yop8!vFpL^kUv~tT!4-e)&^=d&cEOci4JPqZOR^D??PL@`? z+A7xvm?M86ClZ1{dKEyaXQQ@&H@vd?r(6D)yF;aI^2yNk(t$FG3V6)&cU9vnCRzKd zO(dngFd-9A_dy=?iOqq5T{W)Gd5%TW817@ZT=bXKUV8;#$Cq2?tJ+n9izrXYdd){* zF>FFM_eG{4l}6}(h(8X|HbDGij*hH&W~y3H<{jb^VD%Fp!#sNg>MtkB%TDwGE(1HZ z+Lp~4v{&nTf#0OSs(qX(tiahaDfN%IDSkm7W-yOHt*<}j&V~dZuk3zLYKY_&vuf9d zWknaPa#!iv6GDLMGfUsyVz7Q@$xdy~<@o3+QrK1rsTLKSUS?=^pZ%oXHJmJsE#a@A zvQS(nNGYygyIAw*2>Exn-K3WxMcVDc(eaQs5)AsDfh{|VMk^*>VOSw|7kh)z8^AG5 zP&-v8rKBZ_AE z;l&=RL+kDjTYAlu=E_s2jGxF?ODUiAkf=DhPbIj3;+Lwwn|e2T!glf@Qf0@%2 zqpjA7n!7R#<40z4r7PFJlQU2bivA3b1nr#+@uGg>9Sw|;n5hnPNB{;go5g!!&(W35X_ei0*;?g0gMwH~t4GmCo z!ICpEtT~ya=bZet|Y?XQ8tPou8Q_3Q(`p+qX!QhgD+?6c$O)K zmZWZclGl11)ssYLy?bNky`BXfDkke0&E}5pa`PS(lMddgl+v_y?PSVYgJLx^uM5*J zKFYcll`CJ7I#t+({M_g>Wf(AYTJfAEks_81tr*5}uh)SM;AvK{<;%YZ;kML&I0&lK zhhOSc2B)f>M3AT7iAh}s0R?X#zxRWP;)MOrSH~DAm92FKi0f9G9m8K$DpElY`scsB zV=%>nLVnfi$9ew&l&YvfX?N?doyyyD0Yt8E9upec`}x-KD|;wGx9w-2;SfkTJ1nfSL5YgS=Ruvr5VPBdg`{Xmw-=3;Jo)-H40-9`ByR_C3 z1DR>8h7jcJ`|fmt5M>x_GjZ+y>$Ug$c%h0i=_dEw4E;Vk%McYNnT%5dR+c#u-NGB( z9%;3UIcZ2Fac_tRR2abSrBNndS8x{#aw4VlszWDw9Rj+Z!p`$iem!vw-w7YnFCVMg zkW2OfdeTgAdkX^mouT&;us zIa<^GKMXd!Q-OQ?4~W})zT1jBub{lq^Q&yG7NcH~$IO#d;u|%K4E|}BmhNJt>EDK+ z``=**&KHLO#5vXX*h>EXnSuu%Wr~5J;1M4nJaQ2HMVqrOM7F-l{bvSZloHB_3uDs*s0#Wc*3g<{WKyVO}3BoWz9j&TCt*fDu z)|Q+JUWyoKxXS*~!$qNX4|CY{cKk@^35s3uwdG2`fqTnp^N+K%!WJWoAa}-%7I!cL zuABJnIIsM~$V;Vr><|Q3aL8-$hy1v`54ioRS&^}BN=7p!;$Yd??bHh0Yl`xyinA=( z(IxJfIO07$p|ttyV#8yulpsU}msL(1> z6E#QXdS0zN8c(#{SxGB+Fao3x5_9Z6wO&8*YQ*>xD#&l(rgThu<4bP&hgF@$w^CkT zPo``nq_^c+N6(3Qmclj`EbET0@Jr2<*M=g&E?2Zo)DI}wU%y>J87U_o0nO5xV34wP zxXz|S#f1GH+6#?l=^kg~A}jv%kUhf~Xt7R&98nezSnNC0Hl5p{SJK#YdkT(J%(;3X zMY^y>@Hxk9IlG$O27TD>A`R@J;?TE#^!vzty8_HDH&fIcUF%x0+SO9G7H~t=Ma6e7 z4~XI>mPi`vwqp4M%9fZIjDLD}J71}#V`<|&y4ZSind}50pN#vF zx5B&q`MzNtzqxZ}pV3vOuGt$z2|Hyk2LArGJ$!AKW&v`e(z{~Z;bX|UK^oob%Q=}P zGwbU#?xxESR9g1J$41tp8S$S8{csc$ftw4#qjpd5BvPr{!V5ia)V*ktwMQjI zyQwBAtv%`pxyeH4G>K~^nrhX|Cm|Cr>s_GV?aaLd!=|+tL@VmsfJ+B}&{^B`U-rj4PoC~SX2kfOr9Pyz{C=3xm*ddd z09(Jn1Z*8eHxNCbf*FZYMakD55=}1%Jp?Vw1`UgQQRw+msJYyKvdtTt61rb4@LFMt zJl>F&{7F zzL$adTF7PsH~05=>;Q*d7jmX7b~*)u|6NieBo~b&D^b2WPUG{^20#ojR$-9SMZsegfl# z2{ZIHh!9Zw%$PcJj$4DG+pYB%DV)xONP}J%#U94OVWd2`nI5>A0*i(MKmo-Cb=*9M zxf2o}mV8D**?%CO8w3q_sEA`ehci*!hl*|W9j~NhYCPb4J$#K34i3TuNGRV3=Q452 zJSy6uA`6p7cF1f~0r3h59+TOoPX8m<(5QICg4FG%+B2O1Vi4(Ou%7AXA^Z9dDBCRO zBB-a5=T}ADC+Fbj{W|NmgYFmnU+8}6N7%wl;>j?>xchq?jx4hW_;0AI2<*}KjI;M_ zypIliys6s5%EY9ZDP*>FWCxGkXk*sY7hN0H_e@fk&w1gR;;Dp9;97t+4XDp;rmQKY#3EWa$6Gi>p)QQg%!3!tmB#g6vCW;7wiWU za>xUk9+R+FjnXln)WTI4s>KTBYp;$pE8g10)%@Iw+;gqD;#}|cbwNaIz^N{czPUOc zmpt6G290Q2y^TQxU9#snk+GEHJ9iV^kLPq3GtTIP8I{ou3Qm(AD;CXY!GH0L zM$yJmj1vp&Dn3*pV9Bo+o^~}&x}r8dfk`0=G{4+^7uaWte`Y(yZ&2HB?iNZqSWuj- zr!*v*@UVh9-kZmc!NYmg$9I!Cp2fowL3huqEy}j!W`O>#enG~8qf0lc8Vd78Z%c3_ebifPSLw}8 z^#Mr7wqTNczT>2?FeS+>^(WH`6^Q@Cf3~;DC)ma<)LH!&TKejMHLgO`TpBa&3<_fU zJh^kDU?JpY$)5C7?*fQODCn)c1~33_>DOJqeLQW^%75N;F3U0gJpV3M!ag zgrtid97P-#XqMfl8FOSNHy>_T0Q5v?KWxLdn55^~!=ctFpW|Ju0p%)0j~8GW{5J7v zkW4>F9HOw~M@Ild=wX;N{b&F_6tB+XbA+>eH5;;u>Q9||G_No{C@tsW!#*>dgkdB# zWab3#)aW#>Ku5`8EplPRldx~M-j{o&3!e<7x`f-(Ugtc;7X%K!X&Jxs+lh6kL-`tJ zUmUrQGLY!7^BZJ+!v)%jmx!VgBkgqs(7KIMkTleyY~o zcY>_+n=xM{%RvaDkBRF;F6=Z_I#pz^^i8` z-iIyDk6z!G#e}$58W5=v3F#YBi#D1GTB@+oudVm$GA`@R_NPk)EZzjS?RF;#j$?yw zaqWrG$d5;JcB7LnS`aBAME1_rtWe-1(!~R|XO3E+=VZh=Wa#Fl{ioJ9%4s&i9f)V| z88Ly|*Z6$3a4~7Ipy!r4OYin_tT)CbP9#le4UDesX(wGaQT;$Idh4sa;Q_Mkhm-fN z7GOjrS5|=s(X?3=d%PfNWnPFn5iTkBDlfzrO6@7aP(T~C=bIv+W<36?Ud!ja54 zoJ8jv2dtk9B(iCKbiAI?*MVRN8E;Nx%XSUR^>8K#Z4KDP9gQ~N9@w*-35155uMstz zq{tTJ8UBdyrZ9NhX2Y>PFB|^%-`EBQV|4aSnv*ZSKNWHrx-g@`9%IaGS(W$qMq(B` zW5E|!(er!S0}>W|^x&edSMq9|THA6Oo^lc|#R!#q=={Nfy~$9rze6wX$-;*`cNdh6 zVC&wh>I2|9QeC{2rTTavK$&i$M(oeJ4&i&|bNg6-Mux$vb{)XkA`id#QTFRd{2w8$ z``j)9z?Z7>jnD6Uat1f7My~T&ePfLJQxN87F#tg9-iO9EHfj?)B#=uoPU~kuR@uKs z@$0;yykWpT&knWSBTCm|=U+HYPEWu<9Hn3xnGVZ9(bZDQv@;nE2wb?Z|G-^BB`r*F zZ>@oJyL%$Kn~j02@GYZ$B90VOIXgd!3?y7dZ*r4W%?62Kk!exQj*u4VTpGbFM8N81 z#`(UyqAUJ8u$Q}R+Ui^3;q=fyysLX2T%gx)*@;jb0VLn~r+mJcKltv#4m+}ihAz$fr@pEpXLH#vffq;F#krh{=F(lX1S3fBTr91y} ze(#POXTOw)#qKy^kTUvO6b52iZL;1`G~a4xwqA$x&-vZXU8hCh(shR95-`sFzPqC0 zJxGch4yo(e5D$Yz?HEtVA%Zxu7)LwgQD{mcM6qtuy}3P_ewI>^@r9>&_!wo^M`%Ilp?vw-uHA$pWsB zMl4^}T4<5=>pA#Fl+$bJICXL|FtMvQ3RWMe*8e*0r1Me@oG#Y-=iZs*Q;;b`4KH?% z(jsbl#RP*{_ZPG4x$o@oY)t^we83`w#&jl1f z&0}eXhrHJp{O**KC}K(wzY=X_esk{R5h;ZM4CHH#*0O1Me<&De%Kj2CY>rA4(|+4j z3__aLGcn4{^1P{P;Xf(9?ejXypJ5TmfkGmq>(wY(}~4XhAA z4KSP6hlTvtUSXHw&i<~%<*>Lj9>s(Gp?T~`rI+}0te~d+%T~FIl3z#qEte@`2ud4x zwr3d;)Wz8Nf}PWkz$69(uqrR$JG6Xmgzkg&_j`9t4}4C(s8jrQ&BL8USWaCZb0K^j zb~9#W=`qQ@51jxRT4?wz_g#b}z*NkrgC$TsXeC$kZ(1qu z1&#kdYlVXZI${zpc!WonC*g?DR$cxt350G8jyE6x=K zpN_!2*BY>QK8xj#mqW=>s_1XAhB&ghWJPLuQkE8H=_yT29GGEiLYpWq4szx*@qf2k z4#Vme-C9o3fh(Bkq`WEq?Yc8jU1#c!It|uq4XLt3+Lc(|w9-+%A59waVde`3CNA4^ zwQ43{I@7)W9Ve180koop8tR5YV>T-c8n_Tyq&aZT3-til`s+6@dodu{)K}W3kXW^C zRMYoir}z76=$-3g1FrHxfoPE4xYc!0n!{{VZPVj~Jp&x>@aZzQiS^2=H!e8c;%b+| z&g4Aj6q$P+bX2)=dBJ1*p- z?CC=tAsNziSr8Sbd=$KULr}$)9>NR=flyF?-&9J1p`5K>!No<()JM6{u;s7SsPB0Q z4m}3MShNr423r#S`wt<)A4q}`-$=5bQ>&Q~kE!dY*U|_Ts{%#ZVzS;cP87zxE4q%- z9JI4BN3vNWGB+4)F!`c6^VhyZ;0UB)e$O<*Bo!Gy^PRc@2>?_n0T_x-(s1HP5HUa2 z_4r~`tx?goQAWcrYwdZ{A#sLTqBCZ7&voC^+v%Xq|7U+_D2PUU`&l3jna3!Nx&f*J zIc8FTc=UY({~cM9&GY-+E+>q2_^u_K+Mb1}_Q|BIp$G#(XyAu90i+XfOEwJh;f?_3 zPNb>e6g^mJ0GeO}R?Nsq+;Elb%72cv=`ygzmzH!=SiDi{yDwv4PvJ zBCbAi`cbjp+^!D|yh9*94N5W54k1sF*>;W;DnJvnhGl!ECvfB}WdA_`QLclowV!#U zwIj>(rLM8R8tDVP`{X0;m)eI;9gK#n6jqHB7P_*ZGr=a(jGdAL4CJ=SKDlb=esIBe zgFA(nK0nYP8w5Q%dixyTJb9i4nUpvHI^7>!+}ham6_l30u68*bRkGxklNNFwsHt8% zP;!=NA}%Wwtl~&H05bk+{(3e_TUvDnHKTGoXL0!Ss{qJSRk67LqI3S4x_Fy(Ar9lk z3gk;taKE(1DT={>Y`y_HFE$Gs>TT2_w>`(9FM*t|G_|7W zD(bdjf|77!s@`}kr?0mq?JA!tXWxxt6#8cOk#$ad@kuXQtJI%e$8=t8*5ezPn}wB! zIT?uiai2>!cfc3wfN)>$x;{$Rcb0v{FK2L%z1IstD(q$LbUf*`TROV3`c>s=;*QFx znB#9abE#^v<{jaG;-US@IMK=cEn*|EI-OD1n&C(>Ni*BL{Wc4eYr+Im6!aJ_F*{p&f3 zPC(3z6DAS&5|~v}(9VWu)bOHWnp|=f-PHY(zumiUyEM4DF2kSwlr7~d1=}vms+y&A{D*s{q zmZ?He)a(}HeuJsj#Bc0JlV@J#G;R78M-|;a+##C)<$z)t-!Fzj%SEdS+MK1Xn)Vb+ zYCQE}aAQ}u_SW!wrjDYJLkH$dUxb1G=Nwba=Ek5u7G!+%!@eRt-{pJ*AGEB3h+ zuKm&#(Zz^a;Xz^X{mFycw%ADC!E9P@tDNr`?Y{gBfW@Cq28egh0q4 zDR{3nXNt#Tka0KVEFg=&BotRtQ&Ch_jiN^n+`9+_Z9rBfuMMTXO>E1_$GI)HwOd|@ zHDAAx$^H4R!pOY+$J>$vt_@nq8D-cM+;91ecTt9xj^4=paiz+|ot?A#=Wq_ULG4aO*0f(|P zQ&5w;>^WI`@`r_;oUk*ft-I36B|w+rHEYmO?F3%!k|_vXiqaAnWvS9zzh-DnhZ#P#&(>|+j)rQFz|-0sqULgoNx;;&57y%!MT4S zIbqQ@#}q-D0Q?GDhp~;p;mK|xW&mPYiig?4V#?ftLFP(PEzAtUvLc4NUDBKRY{;OW zD{!S2)CqvRSH`DFp(i4Fxu+i9nZ+{pyKUv!Quba1y9(s_xy)R9VlBTU=s8D%8Pcq^ zJj;9JKG>W8AMCw(RMXemFMd3<(jEt@w2naPNUH)OWhO~23Mx|(0g)k21qB%+Q^=sD ziii*qWe`ZJ2*?-_nMopoB4f%JrXh z*Ej6FpZ)CVc|M=d13N48dZ3N`!AUE6$8XQpt56Z6uPRmR9rGO*f?XZE&X8G74h7&W zN~jHN!S<&<_9;%hNL1P8G7nOry}s2J+|eY~6wOTV;Ek{!!8u*16S(&qf6-Z+OFvRM z*B@WK`^RE?4HlbqPB+)v{^#)7g>-*Zm*xZ{h)KPfg7p&*{yEl||& zzxmNwk7Km{YS!A;P+%i1|%yAwZk4ZgmH5>UCGGUfGW_*5{I}kfH=(nD0?I`O z)iF%2Kl^n1nI1>eK!Yl;#xKS257oaNqgk>%ysAA=v$=xGZ&E(bcgiyzi{4`qN8Yl< zy^e@SHACPgfv@%`$`wCsL65iScBi97U9WCyTnEQ6&$(40g*+tcEyvwtVFr@+=K>hC zImVQ=80X&e*3A@xkZY2UqLyJbAC}NhP7FP$sSSv#XUGQGg`$N**^$Jr+`M2NPxvoT zc37XC%`XN&L|B#6U@KA`<85>sq@cl~3glz-0D zKql9KmPI7Z$QG=&3boSiGm?7u*r1q1_t?$mg(oo|Zan~WC=dt z|8O5;kob1VKjAe1vGlbcI*40{x6G`=W68&e6pM=2RH1S3l(#PlAzqj{JuFl z*Zn}Zd-S#Jn@gVqp^j~Ol@3Q(em3_$PVE}Y*_Leh_1!g&WA8UjHX62C(NXXCnc#jd zdr4w~aUQ9Fw*BesO~)btqWlF*sfe7Nk&SfiToTykKlLvhFKw7gLzJee>Daumbv&+v zSCj6|8{cqN1usV}ciG0!w!3KKV4IRDohn*pYmRnb&gSIk`@0gBALgUM87aOn9H^qT0!T4n z@KOMwt}|c%{}%-q+#9~g$bVUON+WuW@=K~<= zK6tCk=Kv3O`dfd%>LskPdIJKh3apn=o#4_QHzO{E%$gHxK}W-(zkxqM--qAv?;b+I zz-ulI1dK2X;iK954Dh+dIPEl!Z2$t+=Ni z5S&rvH58riE&1*Y)M0E_{JpM&ho~G|GGjjZ4Y)ezlW-Vi*Ew{`cG4v^jlUr#Tmtei zD}$yEaQU+J%b_~agB_U1Kf(Zy@B0U}vSVj)F90`KI9FJe2d1=t_FA!na&01hXKBA9 znM60e)Ir7rX7>3PaaL2{n$tob3UUF2?<@!~ZI{3s-i?HX1LpMKIm(7$?)D14GzOe% zC%~z06ae_-^UI!{0I}$_`wJB%T(|d~mJgm>N|7}8*z69>q5wgay6?+M29K-jj0kK| zzxD}j5Sg;u+nSDNC%X_z@`t-yua0>leFn_&6cm`zs2*hr%cdt0;rB}qm|Upyh{)~ zusCtF_k)+MicjB>TT*r$YUL!jw-0|x6)nCF!tjS4g)EBMbcF79>Py3PgxtQ2dGy8R zEnXGSNUzXK8xRY15)Ls}xKyep7pB`SquW6=6}M(z68AO?tTOo0y+-a-!s)L=zJL7r z^0%+QN%rzz<8p7Q=K}yKlCl3J ze`RCd4(M3w$mrryKh3%I!$YCU(Y5ydg%Zje=3If^VFdW zyCcZVziv44khtmgz-nS7S&(gB*83s*hlDBi&d~NtG>t>@eb*~J{Mzc@FMTObvI8`` z;4M#YcAwr_`J|!zZ;oxA^W>%0!#|w~K?4bwZa|teg-S@a2?_Up1qEZGGJ+0#Fr2bGVO)HkN(CM?C_-MvCj z*2*h~my9+NzWL0>!8jZhso1t?>*ix#BB%+U|KYR{JDQ=%vXEN7$4S7d8XwPmp==Fk zkSvaM)<=8hsOs00)OFStaGpRX6E28%lc$hd&#>O~gl(`2yY&;fhW)gNKSF!L_7Z*P z;&hnLsvhT&jyZ+DQp)#Z*q?SS9cN4|!Q{L12K_RjxR;?(iUlrT9>2**Ci9j}By>X* zt8vS*8?fg`Dw?oGk}2BXVzf`(i|zTYP~d&cOT(ul5}Tqg4@W&%SMaB~{Wq1^S*ip9 zT=Kztm|ga#=s=frtw{90(HyA#pJ@*GJi*slJDG*vP*h#5V``H=Cf*hC(JQBK@ci<% zBOOJoNW?pab0nK;o!=HYJ-+icVqk6q+0XPi*mi``f$f=G*BgCd*!N)p5*dH;c>={W zI6M-w$j@qSGp~p*1(2$bziw(;05^`wFDIW`zi^+PU_A##CBF$&-09d&fS%juU0n|c z!?@f_yC{3rsPS#3ej}s4?^hiwfAH(g9H9#;n6kn^qDxFh*LusV>h8_vg2NHqI|;vkhl7Mz1q)$a@MD4 zRswr$P=8*|`%uH#)%tKDEQa61y^V>1&Tpxt#|00(>~X;k>Q?&723_63hC!qqpT1>V zH@T@_WEKss&)mDDX5qt}3u~GP*@R|8zeKD&SsjA@f^fIXNoW7b_v%J>W9ofgu%5u{ zB^P^z*;|Bc*)^VUue?*?kl%{u#Eb}`I{(xU47d}qsWcMRhsD_E z>_=ZLL7*~ZK1Y4IO6q* zgpWhCK(BD2y#>1)xntHPs?01dJ)X=Peo)GtZ4K_RK14{n`CEf}9)SIhbR|6IM@cQa zH@?>1OF1s>by1$yfUSPNc)Q?GFjc;Jwrx0&RGJ#0g6>z{xbk>fl5?8?ARLhqRw7g~ z)*U-?9Ns9D=EOpDqU*UMTyl+qqgGRk92BDau5T37P>?yIW^vaD_oSplCg_x6Ev6m^ zPqIbcT_V0??{VA+_-!Lru{iR>4K>O8lFF4Adyphk1s8Jmh2bv*WwHTUJB=9avYFrP z0u0K7MQyO2(!E`C6~FbJl4*N!g{g_VIk zygRPndHcR@kzy~lU`PQx)}z01vpTo9&QUUZ0PcXKLGdXT?PQ-fgdnsPeW?h+(Dja< zas_9j?~SfBZp@ipq9Be&jGhK{)bS;$s(jL$54!@ch7~9$Q6@#pXMtI>U8K1HyU--FMyb1Qf`%njXb)(^+?IfHSrX{ z!`KZo|LysvrjR4#K0^*|!qxZsNf3e*mo|rM4llIa-WK@v-7B*tNzOPO(lNWkg+Qky z5?Y!$aMx+`8xWh;vI`D>AQ418@wngv}+P)3spI_%ezfI@N}Rdl77*j&`!eV-Fe z5qN_nZ{=8unjWCtH3N1@j>5|4r&PCdG(k2Pn(6=I(2Chk7PBvAe-_{#mbMJv8r}Se zrC6!bUw#U-b)J+g#p!cP%abq*u+V;lo_@CyfS#P%g>XgFmFp9Urds53KNY+|3MhEr z&A*BAeS}X&v)dT6Ty->5zLaLV?KnC`Dgk8@d80dJv>S37GPji&qq5nL&!CSEw$MI# zSERK-PaK28quEtVnvpa#vCwDdCGGJLjQe5Q_WEMBgy2k43e{Ge0G-6=I6;r~?B=@x z&8@s`T3QoJRo93PEI5DU=IU5n;$1)!9%8=VPlx0#Z{!1Oq5h+c_CNH8)^O_T_=edR zDNgp5yjL!ihu_VW%_J*UOO)}t1bYw@OgR^ndq<~@Qa`+kww2fH<$f`oWe|4uLeSt; zw`d8eSO`8!>0PuzD~WA1sikBlXDPL&Zh%pSEj^0wcq_phWCIUe~mX0YBzK_uf$oVs}PGK{rzQr3)jja9e1>U_i64I zhZUeA`r{<%%FLU_70cE;FHTVzl1y3^H-_qpyO$Lqx&Geh8@RB5^TXE8ExcV#3?bb+ z(i;@U+hml2sR4^(jvl!(DxE<0?@9gJYIaD2Osf}YF~NW)VUeGo3pt2O>VFT`mRx*) z;J;gU11yN*L%Z7PL!WHjz4vFfbwYsyL2^-Qmw8Eh?{`)JGFzw51~gfiKPRe0Y8}{r zE|VMnpx;_=`Xt&r#v3CnaKM1FXCm+({x9-jqp)ngwT7LQhFxyl(8Gq+IhH|8&LN7* zde7SBP6(EzN#2YHsqz3CnwV=(3`}A-IYkGmQIul}U{(<(Y)&Ez}Fq z*UPp~=Px%xWFgBxY}G+wXZFNRZ8$yfI#KI===;>*7=A@0aUq4QH@#k}mT27P=1`F% z{4v3N3zg(Bdq+H!GW5XGM))>AX$UHssR{A!tmEMAR=3T4x;=~bSMi{urLotkL(+GR z&rFr?wu$$!`}*%W!NgY$5HY?^_r0Zu1CmTkxAY?v=x3nt*uAM@&txaGpDDbwQ(IXeoR?T-cWnyt8fvOrVHMh-5}z zzyEFCM_QtXQOXzA+TgFg+R}It+w0U)nnXbOk8QBn9sK4rCEd^!e+Epv4;hP;f8Nu~ z8mkVFYv8K&(nh2Ay15?aR?L%%+n%O(mG)z%O=sZc=aLY5N z`Q%@SEq;RS1}2QFJv!<@Fbn|Ct#?~vCclXQWb2>$IA)JRs`|iLE`1Y7Ipwx_<(Hj- z^QY&*ZeNr)R9Fy^tL!N6Y~OymIadhivsg1P>NvF@t^{?(f0Xzt1{p{^G^Xy@ug!(j zuTp6=2Z=a*oQqHM^0u7227Q9ffGj$fYZXnG@Jeere&n6p%V!)ac1At`r19v&0+WfP z-~E1uPl_~*|3z_8=6e)>&sPOh?|f=aM}hVeZ;pjw%w~()E^WNG-T#Z>UYDey@QP9H z1t;UapdyknH>`GLVi%a^&WZW}Rcr^9REjrKaqrBco~`=!rV7w zNQ|&=$_!#Ws#aN$uT{bIZ=hEY{0kXwh=Bg7oAb?ppI4FH9Ia~GM5yHr36*3O8HOzU zu<3Fps?~Y-P}oO*g1r43?)sSPo?sZIJ$GrKZfx2Fy7!jGKpr8i zx3uU}#MT^=xRui&zcVi-F3E?Tc3g+cpH`?Yt1E=6wB4*+L-w0F!!R2vY^^ zuAs~6qdsiZaf8mj2=17*qBYeD3jh^`gT*%Fko4mWzxH`~{D08Z2X-3prG=N8to0ZW zRc_2I#CM^=o2N=C_G$ozE;GHIc@-ONs5&R9Wos~wU~9|ceE~hQzX5e*DD%@ z$Dfn*Z-eeR!%Lq*3+Aml>r}HtTB(-j4lq=6@^QJ(nA(GP2`t7~LJ=og{Ep zgI!cjOP4;XCa-DTC(S9 z3A-5mh{B{`oHG;vRQfK2xc33wL+{GdHX8$eLpm=?^`=4?O%jw`+*&Y~PgFQrVA~h3 z6>Y9pcC!E@b+3vR3@(n*RbO2m-MrLx1WGaCHgCTQ z610!shve3nb+oYNt{&HF>qg2`*)j z!*WH1u)Hus(4~%!m5-EicS8%EF3oG+ev_|e60!!fHjzs75=0y!|NPKzdhApZ`M1J1$Mx`(=*fC`ln z{vOD^0H^0a za;C>VcD}O}mAgLX?E~)KLa>(2D$YTowHoP4rBtf)FIF-(kTZ4bR#OEQTx@An`cd8w zF)@^nM***MJ8kc~@<+UC0ZCl5J}nQToNUf9AJ`n6`^2piNohXu<2B7bb!mr)iqZ&$ zp-1Z%wf5L#;(TlX)03Q2ZM{X~_J+FPjmzA>1ad+*-1TDb+3?d~>v#O+wL`wC8o>W;t8jAlDP z@U zKni#(Zr&@fy2i?#z{d?N#SUDGwo4fSsOhitr0nloB zb>4H{0y18zLEKDYqrox^HN)+PfB&7)P;x~{w~jun+AO&LD?}hUzC`ISlKQLO=?bF- ze(#*XRzB({pAK;>~ych?s*o&DA^O<9~r!W!zZ$b_qLa^a7Bd z%yE`C9iz<-cOL$bNmaU0V;}pyQ0|b+PA+0A8ay6DfOywK<+}~E{P2FU7v;yBaVd@F zgel{Q_RoRtbEx6xjX0n6+)Z#|?wN3)e-V{?P6fMgx>E2?iGuBcvT|}aw zEO-Nk~M3N8+X73tmAxU(4S;T2rX9A&}f!u3GZPw)-S ztCmfO>CuJNplZc=p547JPWu8v^xChG3vCy46|DRylP(}taV#4XFsI1!W8j+pu&5QtCc~) z1N&=KXZbfiLR&7cj_(*yDlE9!DWVXWQlBi`%4h2VS_nww0DfsKD{K_*$wLFx*ohx&(``Y7+*a?@=CVx=nd^^5pyK3mdaB0iRbud z{kj0Z)xUwqW>Xj2ObcIh9eh_5(c+I&5!@u1ix9$VNAI9)jX4#Gjvx0QO1k?`$Qc}f zG;4p3Y&W!B{9(TWQ+`!Q@&GKvWRgk_@_$ZGgup@~hdum&GG?Wc-VJ2&U|C1HMBs)9 z@X9OHgIr>$d>iWprJ4U;jYsP2)7-s zOw>2nzXwqF0ESpu>Q2|FzrVfQ~PTAyL}$;rSKlf{0O z>PH^nPFaGY_N3)}sOoe(9UOQz?%s)t<|#cx(&;MY*a}+ITSneN`#j;C>??s=q;A$x z6NLTv`^@8HI!A8%)5vTE*aBML?_aUM%0IE%H+?~uDP;-y)Yr&)=fKh;9dQ-bfRiBD z|5-}xjtyJ-)H6r&Qh>ftQ(RsWSgx)cDh1Fa7yyi5z;T}G^|j4o$fDg74k)0h+T=P# zDxMum-^-vCzsht1C@;W<2b4b?Qr_p0&Hd4qm;frNqF*H}0f9gjqpX~1vpXmq20~}zJ#Fq& zC38Wo{3*sPQA24270n^Vx);5$)w1F4;_}X?+tP8jf6?Hp>MSx%2VIQ{X~>g6`)rft z@BFbYsXC1Q*MD&0{@~)NtZV+RW=v`Wj0Goli4*=?p5%YaxI6^YM-fA(ch1h_;FEO< zqcUcNnL?|xL|R4C3l%7Pc4au&PF*|d)e9S8k?h#F;|Vt}fhsLNq^gmnwp_$W2!@jW zOJKvzRk1+PkgE}Kj_9@K87Re)c+dz)cWEpk%OgHsy%rmHJX3fkIQ%9Vpfm}9vUL7Q zBF;2eHkBBCn05FyqM{}A+~$gz#u0UJjLdYcAyUjNvPgylxe7q=%65c_U)+4VPQCSx zOIXdpc5j+=A7IjX#F}N{7&MZDDdVOK2%6^!0DGC+<@7+Jjt{s!51u~&InR={7wqdv zs~j}p&%6a#W@|nF7jN2+wdIlKYVA;?x7F|K1t2h&YJHxHq9K?fM8$Q36Q}wz)>^;MF?p`H?K7XH}79>BUs*SN{(aiFWIzdWqt8(CR`Bz(OJdGl&!WMsk@<)*R@k& z7Z<(KY8|;4F!@Kodwn`paYn=APg=Wff69#{IIaKOC<^H+mJJPu!kK&Wd#jjZ| zPL{?UP}&ID@B4=4F%=z?^gf*lgE25Ez1TIzj68Nx~pG&*7~($)F*M<7I$pT zvx(n(?Jv4lKZ~{b)cLd3YPiFXwW156TE6gHpE~`$ovYr4 zNF#bdZScI2ZXCg4y0~KHzPn7efGd^cuV7drjA5E!SjI&t%ZE#X!nq$HEj$5>SKiMT zWH1dzdc5+5d#lD=ZsotJW61;Jl#N}%8aR82ll2{Q^P|0new&U&_F9_ZUZbZ)pNqbu z57zP{l#ds;JyJgwtl4bo@X^KnX~7kn{f%UNDn|8j7K^!0V_u+)C|$a_<19~y?^6@Ea(v| zs&v~o8Z}p-F@*P+BIYHcZe%M1>c+_D9j0aN8bv*`ou82n^Sy~w30mqUwtAtIz>7T6ii?#FD@b{PZl!^{_UgrnSK?L9?B)yx7^!dSQ}z`TVbjV~ zp&I0dmI(v_W$Y=hs|oICKPy1D=Me@dUg4v$iH*|}MfsLjBTPjl86N%w{9v!9YB1>k zrNJCi02Ccle+OlpH>MHOgjzTu+)4j^bV*_$AxU3oc|pA6{-C}F|lE?mJMR&pT$ z!)gapSM8|3DHMEVsr;>m?EBANl3CFovQ36^)M3BuM8B34tSb#I#>`kl{0*>+pUyk8 ztx(s-v)y^=buUvYCJWc`3(aY5K8o%LG0U6TX~8WX2z4YS~m9 zpcyu_sWBFS9u;XB)Xs?+%+$1rQoP%{w~LHS!sFl2`? zBiGTb+3Inne~Wg-#TzT1SlYohVo^T_Xjv(%`PIsW=KX2Qr;oCaf7x;2vb66(t+BcE zs$WZd%&wNxN8pj`8x3{yQb(9p;nAbZ_V)Y}dyTh>-aJYh_K|h~|DU7vVekx|0C{FF5ZY3?t zC_YBMY~MZ?f5Lx1@A?7VGBj;m5%PAjX(a=BFqA|j#AiU0PbD@%OD6ZH@;yc%o71xf zV93t>JYkQKVY%+sg5>J_Cs|EP&;)7Yx)jY}zs`&%G8T2vcjX_0^9#lOYjA&`x%v`e zJsV!dSW2z(l5e>>_cnWcZ9Y;BD)GApz6ci4Ecw^xo3YE6<_F4zN>lk6==6wnBIb2D z&otk(nIvt@&r{qZ#W1*UJXw# z-DGDZ1zFdq&3onZy+aUNM&FmvLqY8$vXvP$D=JjtjNeH~5)F+_mriP0D!-YwiuC_E zjo+4EM_JfFeaYbZu-;wN-OOQxuQK*iV2de4*<@4uafPq$43DE&XaEitxAep)-c;Tb z=d_N(_2&CSv?za#p2*cP8r9t7m&WN>9Lx;CtzHzqUzvLJIj89+Zq*65{nmartANp( zfj|=yg6%!@fST20C(YUr#7En**&g$7_?0Ty0Kl{r)VK72z!mF*)74oNkqp& zZ1ll}v3fN|dBe((}q57vCmvewtohY)c?ln>K}oHLJFr^1F4AM&%>BFPnEhQtdB0k81ruiE{O#m*{o5)UoqRYa~Yb_+1$p6U;B# zLQ~har(0uREWIxpuMG-}Ck#R1;T?TMb4>5Yb}iA}^GW4vyW9GWaSsiAzM_W**e&<+ z#03GRiuTzY8{cVFPqGfIZH^bsenBr5R+oBuKF^P!Ie8<6evT01V@A#7J*g8%*7nwQ zje8~&+~0>YhSoaFd0GTVN?s;snDaK3X@Q*|Y|R^YY~F*YKUB)?Pn{`X2wdo1@{Ata z#P;^k;CoYMc56HP+mO%14E_bFh*>w;0iE z$qZe6)1>Z>fSYxi`T0^LetL%Wi4h3?0EfHIU9X+9? zSF;TXmH;+8?oVf#nE%!(I!u}rtXx@{Lh$$#zaitApm4LDU`dGF{#aSf!&0V5v4Vt) z%I8Dz7@0)FxS%=9Xp}>&Xjn;Gk|^ReU~7`0YF}Kun$7T|o3W;e^l@ET|{A zI0pI=THG0O=Fv2ri;>*fv8jzabpCMqv%5EiCsmNxMLaCq*t23T=cslcoc61&@401% zBf63HH{gD6```ibV;x*^&AEfR@7~9>^(NP5*`<%Qt#ikqdQoV<7clNeL`(xpBuXy9wu*K{u?E~pJT}2sHg&5AM$Qo_yQEYRc9Gn}rHfoB z&2Ob2D5!9_=(`C6buZzpHd%>h0*#{tezp&lLR^BbXPU0`6-jIIcjC-P3!V5ga_|P> zlM!hCvP39<%r{)1l#zTGG-$`_TE}ep;8?-&<>N8=bP_)wjlV5Q#0WW9-t`ZN&DES9 zOOz5qR>ttRvnq=xUiIhL3VSbg%Xw-V>Tr2BLA!>FV z(=>uho)WRTORLB4uNXH5Z%iB;vnEqCe=0=-`8il>}FL2TrnXnw@1}~Q)v$>VSBh!_Q`9=C4WgX31U>lE+0Z>N!}jiPDU@K99tsT(UXTu%cJOyDS9F@vA~V(A8<%GYM< zmZ>BYNgNRqS5x3!6a|-pUA;1%eRRhU_{AWqt?IISh)_ZpmiB`bK0Ju3*iG%~G?zM` z)Wk$h_jU&tmtak2!?%|vELC@CYg$r+9TS^|EfAs#1f40HHYH)z{pViw^q!2QQ=Re(8HVXUj`$AHrw*EJk?oeF+D2^-=6-awY>=PfSoeI!)-kI2e@ z{rvx0(?t|R1s=2;k=hD`q^f>O?=B%G-CfN{V;Hq>aH#j?&;*4|utXo4ZG~TLh)lH# zKcHS;V%n+li6Y8KC}d!F$6}D^N?$CrAAmD@`{?f8ZzPO8t{h5l$?YN?QS<@zxngPK zr z?3vq%TZq^XI}=G-Xr+{8`d!wgq@(!AI!NC^U7kaM19Nou$msg{X;W%+U5IdZKzpHk zIrLar*FaY0Q)I};=zM zcc^di?SPjh1m;zCd%GJ2Nd|||E`M&^=JTKz)~XMtiwlx#II|8Bo$X#zb+!iVjn#LD zB+t~`G++Z?h$OCf^8kMkwM3<48yUHad0lPhYNgLAL5b78SPc{KGzIJb>8^~T-&=5{ z#4Lw(-U2N`EG`O1V>gA85W@8GJe%pleA7X5Bjgjl&cC|wf4LP*Q6O3+dV>%@L0pHg zT+#5Pf$LUKBWrF(-^JYTt#L2RM5aS~pUB!Uh$j1>%+o5GR!mWuNY9iMbK|!m9i15& zMQC26w|UP{hPxtYYxoa=$I_Vo@7C=e_n~qO{Tve8T@4~bI<3&ItOa;2RooH03kF?j zIo_ll@l*ZsDMZ=ORsHHKFsuwQq)AwS56St;TSACey0aav_;!&`|6AiiAKt?YoholV zhU|RVH571{bTmdpB-cSl*TL)c$Dc4cU7gV&USp~2GpuzAG-uXNSkQF3#=`Y$6f1vB zshfePA*XMNfBdN3&!=%;^ZH6)j<_1 zCtc?&AIyznhZPgh3g(lwm6$JDTrISOW_-m>GY;u%+f@w?RKio~TX#*be=If>9@Pj* zxv}4Q%c&Eztev0cPnV}g9k5vZQU2?BwfyzxgI`+-z9eRNyLaB|J4>A&S!bWF$;~hh zN#xkFcAXCt&fI2f=+pP|`HXP6S4y#K8zCZ>cPOeib%~La)}vNjB{Alf_+;k?=`GfxzjlRx*8jF(U7la6 zA7Q+a5k9r?yxvl8Zq?IpLTyD8cj@*y`PZs0rV~5}(f2hX=}(LEB9f zQ$H(=y|^iB*F}_X^z^r|k+m`Gb@E0Pd9cAPqFx_zs!!u*;}HK_vl&wv`KuwoFVx%; ztioqYeE-#nf0iW{Y$iZu8K&gx=ChIJ1Wqs39$jBXP;% z4oZz>rh~08_NPr=s&m%VtZtPz*w)VN8Uj+vOrS>v_g5}x;Hdq5&BaxyVZrVa9|-nt zU6B)Q*8rn8N1ZUu8sAoR!<{PW8$OWc^?KTz-ljHxM&lKeLoXJU^+8PFOUcyh@a0&M zUx@bd=klha@g{3w>}6GZHL+l20KW$NPP02TL)e=StAWlOhj*8PxOAz1CfhSu$gv6! zh^J4fwJ=)Z59F=i(V31Aw<3aYLU-Jt&gxtBGd`l~JLQ0TeK1TtvNg-{*5}CaEAuWy z$<3JV6l@`M)mEpw6fQHoW>E+^5^%8sUjj?65i|Bjas==mwld90*|tE`RDNL@#Lukj z$=FP*UA9_4Zwhl0b_j$SFgOsdtnsY1YMU~SOt<<~C&T=WRtVGz>fibHG)%N}rSuS% zp-bcWOfN^|7uUG>MuolY!!z|UzQ=mIV+j?L@$*VKCW|zqw1D3 zT7E3bCweVtQF)CRAEeY3BRa<3JjyZ6r`g*GbNuw8FUNq$4Hp(2anxwKKwNyAdk3!- zBFw3>BW7%@Sbnzl1r*DOlh&sXS0MW6&Fwn>Xqz*uyWI*zPeX|G5v(hV++L$rE$Nsw zN^zFYc-RpJbj-`-wN3=>_6O1auz*q2*gkbH`OJK`-$83Vu&018eDDa-7|dWgV%}XS zG%Sz%N82b#%crzOE^LHtT!rY1G(htHQi?@N+OM92qxv7|R@yq=pz>1u)Q!MaN!wNJ zv-*!A+di^PbUySW&0WP$vPSZeIEcdIYRup_5(<3vGy)*DP zt!Z%>0zbHWDauM3;roZnS+=Z0RIuSwk6nhmDbxCm{Da=Vte%)5-y}-3jz$(^WE08( zLIlqxj@4kEpB*vecgG2M(?-|LtA(K8_ps#3PlA_`I3hwah1k_8T8yAdnZY|UVal20 z3l>)$@a+qex18o=#MLI!B?v)3qpI)$kYgiYSNK0@JOAC)Fr{3i^4F8Ikd&a_P24rIjw~*C$AC& zmN(=z7^^h#E0LgPPRvGbAXqvva7iRmO{Ys$YWh&GF*`rCydYx${FSv|o6X4=`i(-N zRU#zYIpAA34K@V=N*sDUA6t-7%|TJuy0Q`dnQ9NL1@E0{b82Hhakv?op%DWowh?f! zs?i!jubjOqdCFC7dj$^Os)1zwR~iU)u?GD?>=eve(qj!#mhFOTefAT3nvm=QDQz)M zc>;>9A5*?MO!WyKxh~+Jb>L2q*jWKfqANb0m~S)U7vUb_Bp}*P8kR6#bVZ|!6*VrG zMFrwtS*t7$R8~Dqil=7&vi;I}cQ8HaEj!pmX+67CQN0?-TA89ynA4C417*Fa*?3yfpOogAe3BDu@nXwW?vdN zT9gF)crI)3L}hr)jVK*rS0{|^%w;d05Dm`=j3dz;o*q@1T9N-WLxU}&#V`d!*}R+V zh@wmR1%hA#aby#i@JvChiH|E?iV?C`bGXx6AeWatm4?@kXnO@ALRUXAUo13Rd_``T zrGUBBec_&4=+rgT`v7`uyUz3hY|Ck@sgr>SYOj6$IXE=mcZHGfKj|2Mm?p)@&l;7X zjNLJk{zIlQA=L{<4HoSjFTcru=bb9a^&S5<(BBmv;9~RyuWr1wl{&efu=Ko=gz!J; zQ%EGZ7)g09#$owQPuOw_aLM!Jr$sPKz>`dXzkF^(zj~2lRZk= zF<%gpmJ8a5I+J;!k2ch6KQ(bYvA)T`JU+kvu9nQ_8|Rz zS(ri_-s9?}dX}n}MefMH{NR$Xa`m_k8UmFeY+??L{8RwiAav)s3Cm54UBpp8?o(NB z{96Nmsmge{l38=(az4y;U*s9DXeM9P%f6x-PEVCv&0uWfowzka zzUHb9;|t>tM}BrC=2hs8OFrm`&)1FqyuFf(Tk)$3R#pb@=KVQ}K&qH+X>2P?#-ppr zqo2ZibOh4YJp$_ETirVM6Rb5t7 zUowaNHd8Z~V@?_c7r~$bEt|@~Km#=GqJ- zj++!pZk7ZZHCx791+FB(#$Y=b*zt*<#pxRkq`GKWGcH{A@9$W4(exsL6n&cUt075) znrO9s?r+ogm}p^afL@vOD!5t-4Rv)xNu(<5!docpY2DA}&rK2X(F=BSFz{Xica9bi ze&LottU~bk>C%fyGac^m>i(^DPW^AeB@Gz{pa&?fI*2Lf)ErLVhZ6K@lvXtHQotLK z-kg=yM5Zv;n!U#RSF`_ABR}XnWALXIz`uWt&&b18y8*q0U?p6PG86 z{s^Uz^;=NPFkrq#=3?Z(Ew#rNc$oH(B!o8LW$IEO&K5nP3CE*)uMB;gpJnlI@wVT3 z&1n#1&G8DmoNu+kJd~<@p$jiyC{`k`0g4ZOn*;P%PEPP;a}PE4=8I9j;q40=AEYd$ zBBCJy(W@g(Z{FlkGuhPX3J33dm)%&?>#K!z^YDYYc{&+g4^z%N_P<5pu`X;}e|cxH z3imYZ5G?s=WBV^mJNZM9O8d8Kz)27X?p_bC<~gP}EPwXes@%et3;yH$veO>_wXV-s zc7cVYZPA>|cWWh=YbxMBl19`v;%64w%l?RxCo>BV{m#_CL4X_)5{h8tuPlaZg!GYQ z=HwwKhgUw6P0RO+59rjXiy93Kd`I?_Wfkjnk+2^UllPQfCyN%ZkZoio!uXG0uRr3m-?5Sh>Npw0brO~RyBKPKGA%R21((FIj{;m zDdtv{5a?f*%o~%$^L@bw0t4`r;fmf$;?s-dkMXlxRQMt;Hj{oD?h7Df>QWkZ#VC&T zN!>|HfY1FBK?{p{2+ucVxF^_8W^BbXhLk;a$~R<=E7inGiNByZkPt$BDrk?N`YEuL z%5eubv3*!h4`GP15ggq5B z*%bDA{lWIpKsgo5j~P0Q6F%XxO4|krI^od;cTApb2iHswe;0W#fOxitkSOOJ=O>CZ zcIX&(Kosfwk3Sl$&5%&W0J6hEf5=kHioMmK?}`onfB+?2?i$dYULFBR(g|*p;61V{ zMj^t$qD#<84M+hJ^W93kIW*D|B{wBWf5)z}92CQgEU*5p!z3l)rKcL({xa%BKNz(f z;Ve67xC)@ghE#|z_Sb;>LL``ua?9|^-&@4@W^bR7EDJSANx zxuWSxq_Vm9RPm#_WN20NeejTiLOG~g$b*K^y#evfJO#cW`TT*%eKA3TFF`sz2GuM4 z%m1s`%P+a>08;Pw?L-RCv$_0aByp~$!$nqnfH&(*R|Ckv-{(_=vrlX~ zRI_^!nVG-pVK>r~Q>t#}x3y>A>(r<@o-%z`yla^0Yqp8gVtHzHzOA=LNay}M+pBtu z0}Q31@z$crpsTdNf672(few&m6+_BoS)zv~1?q|h#dI-eOwAORj|Jpc&_wbJ?XEnnW6IE3#SqJNeAABk6 zc?p>RnuYJ2#D4SVrCBubNJN|GNZ9Jl?mGbug|>scqGAZGd8s>m!e+G}JLnw5u;qSbP9FC14b>J_*YlL`!SAgcyaweMobRRiLvW^=`IhoHge(0Np(`u z85+4`ZleiD0iyI%`q^>ponhNb3Qjq8>SW^tR)lsz3-al9(rudg0DXvU8E~=2|EgY1 zXQO2XCYxNiJTh!mg(G}hW5>p;upLsJY4StuHe>T0nfTIEOg(bWRq zu|4Y#ZPpM9p9O95V$Wu`Q#xyDEgVDQc1vV%ue`uht<7~GXu-XYGBX*g*SM zUW>Qz_1$Vu;F-F12-Sw&~LJ@qKw zA~>*5OJ`*Yj|8FH3jnCbqIln2JUQP+xj_%EtIqd82xgQ{%H_qC$N5&#t4%F~^B3qA zf#mI3wNwjV^Q)Ixg1I(`cPd;b0Jo&e-vM(Fc+S%ia&F^!Ug8-csWlF%aKdozgP()k z%*|EnZZ?+3ehlSyhBteQI-een6pIl8OVV|qhj%?@T1;b>A^M7F5JgSfaYcq7VbJGh zL&9D7GPckg1iB4WTp4nEc*KOVHy>JqQF{1oA|{QC^BsGEU$=QA$HgTw&ldofr~6%x za6)DmNrOamqW#a4nwOaelOmr!%rE`UrTM|n>Fsy!npeav9Rfs^0>Z@lVSLR*mso?M zj@D~|;e83G%j7w9<&2yb(}1(9x0RW$hs}xX0Chn{izd?$=a z@D8WMsxYPZ