diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index f1af85d..9340477 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -11,43 +11,50 @@ import org.mongodb.scala.MongoClient import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.typed.Cluster +import akka.cluster.typed.Join object Main extends ZIOAppDefault: override def hook = SLF4J.slf4j(LogLevel.Debug) - lazy val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = + val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = ZLayer.fromZIO(ZIO.runtime[AppEnv]) - lazy val mongoClientLayer: RLayer[ZEnv, MongoClient] = + val mongoClientLayer: RLayer[ZEnv, MongoClient] = MongoConfig.fromEnv >>> MongoClient.layer - lazy val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = + val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = (mongoClientLayer >+> MongoProofConfig.fromEnv) >>> MongoProofRepository.layer - lazy val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = + val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer - lazy val httpAppLayer: ZLayer[AppEnv, ReadError[String], HttpApplication] = - AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + val actorSystemLayer: TaskLayer[ActorSystem[_]] = + (for + system <- Task.attempt(ActorSystem(Behaviors.empty, "mdrpdb")) + _ <- Task.attempt { + val cluster = Cluster(system) + cluster.manager ! Join(cluster.selfMember.address) + } + yield system).toLayer - lazy val actorSystemLayer: TaskLayer[ActorSystem[_]] = - Task.attempt(ActorSystem(Behaviors.empty, "MDR PDB")).toLayer - - lazy val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = + val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = actorSystemLayer >>> ProofCommandBus.layer - lazy val appEnvLayer - : RLayer[ZEnv, UsersRepository & ProofRepository & ProofCommandBus] = + val appEnvLayer: RLayer[ZEnv, CustomAppEnv] = MockUsersRepository.layer >+> proofRepositoryLayer >+> proofCommandBusLayer - lazy val serverLayer: RLayer[ZEnv, HttpServer] = - appEnvLayer >+> blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer + val httpAppLayer: ZLayer[ZEnv, ReadError[String], HttpApplication] = + AppConfig.fromEnv >>> HttpApplicationLive.layer + + val serverLayer: RLayer[ZEnv, HttpServer] = + blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] .provideCustom(serverLayer) - _ <- server.serve().provideCustom(appEnvLayer) + _ <- server.serve().provideCustom(appEnvLayer >+> securityLayer) } yield () diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index f1af85d..9340477 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -11,43 +11,50 @@ import org.mongodb.scala.MongoClient import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.typed.Cluster +import akka.cluster.typed.Join object Main extends ZIOAppDefault: override def hook = SLF4J.slf4j(LogLevel.Debug) - lazy val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = + val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = ZLayer.fromZIO(ZIO.runtime[AppEnv]) - lazy val mongoClientLayer: RLayer[ZEnv, MongoClient] = + val mongoClientLayer: RLayer[ZEnv, MongoClient] = MongoConfig.fromEnv >>> MongoClient.layer - lazy val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = + val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = (mongoClientLayer >+> MongoProofConfig.fromEnv) >>> MongoProofRepository.layer - lazy val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = + val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer - lazy val httpAppLayer: ZLayer[AppEnv, ReadError[String], HttpApplication] = - AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + val actorSystemLayer: TaskLayer[ActorSystem[_]] = + (for + system <- Task.attempt(ActorSystem(Behaviors.empty, "mdrpdb")) + _ <- Task.attempt { + val cluster = Cluster(system) + cluster.manager ! Join(cluster.selfMember.address) + } + yield system).toLayer - lazy val actorSystemLayer: TaskLayer[ActorSystem[_]] = - Task.attempt(ActorSystem(Behaviors.empty, "MDR PDB")).toLayer - - lazy val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = + val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = actorSystemLayer >>> ProofCommandBus.layer - lazy val appEnvLayer - : RLayer[ZEnv, UsersRepository & ProofRepository & ProofCommandBus] = + val appEnvLayer: RLayer[ZEnv, CustomAppEnv] = MockUsersRepository.layer >+> proofRepositoryLayer >+> proofCommandBusLayer - lazy val serverLayer: RLayer[ZEnv, HttpServer] = - appEnvLayer >+> blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer + val httpAppLayer: ZLayer[ZEnv, ReadError[String], HttpApplication] = + AppConfig.fromEnv >>> HttpApplicationLive.layer + + val serverLayer: RLayer[ZEnv, HttpServer] = + blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] .provideCustom(serverLayer) - _ <- server.serve().provideCustom(appEnvLayer) + _ <- server.serve().provideCustom(appEnvLayer >+> securityLayer) } yield () diff --git a/server/src/main/scala/mdr/pdb/server/api/Routes.scala b/server/src/main/scala/mdr/pdb/server/api/Routes.scala index 4550a2b..944276c 100644 --- a/server/src/main/scala/mdr/pdb/server/api/Routes.scala +++ b/server/src/main/scala/mdr/pdb/server/api/Routes.scala @@ -11,6 +11,7 @@ import mdr.pdb.users.query.api.UsersApi import mdr.pdb.proof.query.api.ProofQueryApi import mdr.pdb.proof.command.api.ProofCommandApi +import org.http4s.HttpRoutes object Routes extends CustomTapir: import fiftyforms.tapir.InternalServerError @@ -26,5 +27,7 @@ ProofCommandApi.submitCommand.widen[AppEnv] ) - val routes: AuthedRoutes[AppAuth, AppTask] = - Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + //val routes: AuthedRoutes[AppAuth, AppTask] = + // Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + val routes: HttpRoutes[AppTask] = + Router("pdb/api" -> from(serverEndpoints).toRoutes) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index f1af85d..9340477 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -11,43 +11,50 @@ import org.mongodb.scala.MongoClient import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.typed.Cluster +import akka.cluster.typed.Join object Main extends ZIOAppDefault: override def hook = SLF4J.slf4j(LogLevel.Debug) - lazy val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = + val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = ZLayer.fromZIO(ZIO.runtime[AppEnv]) - lazy val mongoClientLayer: RLayer[ZEnv, MongoClient] = + val mongoClientLayer: RLayer[ZEnv, MongoClient] = MongoConfig.fromEnv >>> MongoClient.layer - lazy val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = + val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = (mongoClientLayer >+> MongoProofConfig.fromEnv) >>> MongoProofRepository.layer - lazy val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = + val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer - lazy val httpAppLayer: ZLayer[AppEnv, ReadError[String], HttpApplication] = - AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + val actorSystemLayer: TaskLayer[ActorSystem[_]] = + (for + system <- Task.attempt(ActorSystem(Behaviors.empty, "mdrpdb")) + _ <- Task.attempt { + val cluster = Cluster(system) + cluster.manager ! Join(cluster.selfMember.address) + } + yield system).toLayer - lazy val actorSystemLayer: TaskLayer[ActorSystem[_]] = - Task.attempt(ActorSystem(Behaviors.empty, "MDR PDB")).toLayer - - lazy val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = + val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = actorSystemLayer >>> ProofCommandBus.layer - lazy val appEnvLayer - : RLayer[ZEnv, UsersRepository & ProofRepository & ProofCommandBus] = + val appEnvLayer: RLayer[ZEnv, CustomAppEnv] = MockUsersRepository.layer >+> proofRepositoryLayer >+> proofCommandBusLayer - lazy val serverLayer: RLayer[ZEnv, HttpServer] = - appEnvLayer >+> blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer + val httpAppLayer: ZLayer[ZEnv, ReadError[String], HttpApplication] = + AppConfig.fromEnv >>> HttpApplicationLive.layer + + val serverLayer: RLayer[ZEnv, HttpServer] = + blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] .provideCustom(serverLayer) - _ <- server.serve().provideCustom(appEnvLayer) + _ <- server.serve().provideCustom(appEnvLayer >+> securityLayer) } yield () diff --git a/server/src/main/scala/mdr/pdb/server/api/Routes.scala b/server/src/main/scala/mdr/pdb/server/api/Routes.scala index 4550a2b..944276c 100644 --- a/server/src/main/scala/mdr/pdb/server/api/Routes.scala +++ b/server/src/main/scala/mdr/pdb/server/api/Routes.scala @@ -11,6 +11,7 @@ import mdr.pdb.users.query.api.UsersApi import mdr.pdb.proof.query.api.ProofQueryApi import mdr.pdb.proof.command.api.ProofCommandApi +import org.http4s.HttpRoutes object Routes extends CustomTapir: import fiftyforms.tapir.InternalServerError @@ -26,5 +27,7 @@ ProofCommandApi.submitCommand.widen[AppEnv] ) - val routes: AuthedRoutes[AppAuth, AppTask] = - Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + //val routes: AuthedRoutes[AppAuth, AppTask] = + // Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + val routes: HttpRoutes[AppTask] = + Router("pdb/api" -> from(serverEndpoints).toRoutes) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala index 7ff459f..e9ca65a 100644 --- a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -15,9 +15,10 @@ config: BlazeServerConfig, httpApp: HttpApplication ) extends HttpServer: - override def serve(): URIO[AppEnv, ExitCode] = + override def serve(): URIO[AppEnv & HttpSecurity, ExitCode] = for - routes <- httpApp.routes() + security <- ZIO.service[HttpSecurity] + routes <- httpApp.routes(security) server <- BlazeServerBuilder[AppTask] .bindHttp(config.port, config.host) .withHttpApp(routes.orNotFound) diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index f1af85d..9340477 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -11,43 +11,50 @@ import org.mongodb.scala.MongoClient import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.typed.Cluster +import akka.cluster.typed.Join object Main extends ZIOAppDefault: override def hook = SLF4J.slf4j(LogLevel.Debug) - lazy val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = + val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = ZLayer.fromZIO(ZIO.runtime[AppEnv]) - lazy val mongoClientLayer: RLayer[ZEnv, MongoClient] = + val mongoClientLayer: RLayer[ZEnv, MongoClient] = MongoConfig.fromEnv >>> MongoClient.layer - lazy val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = + val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = (mongoClientLayer >+> MongoProofConfig.fromEnv) >>> MongoProofRepository.layer - lazy val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = + val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer - lazy val httpAppLayer: ZLayer[AppEnv, ReadError[String], HttpApplication] = - AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + val actorSystemLayer: TaskLayer[ActorSystem[_]] = + (for + system <- Task.attempt(ActorSystem(Behaviors.empty, "mdrpdb")) + _ <- Task.attempt { + val cluster = Cluster(system) + cluster.manager ! Join(cluster.selfMember.address) + } + yield system).toLayer - lazy val actorSystemLayer: TaskLayer[ActorSystem[_]] = - Task.attempt(ActorSystem(Behaviors.empty, "MDR PDB")).toLayer - - lazy val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = + val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = actorSystemLayer >>> ProofCommandBus.layer - lazy val appEnvLayer - : RLayer[ZEnv, UsersRepository & ProofRepository & ProofCommandBus] = + val appEnvLayer: RLayer[ZEnv, CustomAppEnv] = MockUsersRepository.layer >+> proofRepositoryLayer >+> proofCommandBusLayer - lazy val serverLayer: RLayer[ZEnv, HttpServer] = - appEnvLayer >+> blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer + val httpAppLayer: ZLayer[ZEnv, ReadError[String], HttpApplication] = + AppConfig.fromEnv >>> HttpApplicationLive.layer + + val serverLayer: RLayer[ZEnv, HttpServer] = + blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] .provideCustom(serverLayer) - _ <- server.serve().provideCustom(appEnvLayer) + _ <- server.serve().provideCustom(appEnvLayer >+> securityLayer) } yield () diff --git a/server/src/main/scala/mdr/pdb/server/api/Routes.scala b/server/src/main/scala/mdr/pdb/server/api/Routes.scala index 4550a2b..944276c 100644 --- a/server/src/main/scala/mdr/pdb/server/api/Routes.scala +++ b/server/src/main/scala/mdr/pdb/server/api/Routes.scala @@ -11,6 +11,7 @@ import mdr.pdb.users.query.api.UsersApi import mdr.pdb.proof.query.api.ProofQueryApi import mdr.pdb.proof.command.api.ProofCommandApi +import org.http4s.HttpRoutes object Routes extends CustomTapir: import fiftyforms.tapir.InternalServerError @@ -26,5 +27,7 @@ ProofCommandApi.submitCommand.widen[AppEnv] ) - val routes: AuthedRoutes[AppAuth, AppTask] = - Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + //val routes: AuthedRoutes[AppAuth, AppTask] = + // Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + val routes: HttpRoutes[AppTask] = + Router("pdb/api" -> from(serverEndpoints).toRoutes) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala index 7ff459f..e9ca65a 100644 --- a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -15,9 +15,10 @@ config: BlazeServerConfig, httpApp: HttpApplication ) extends HttpServer: - override def serve(): URIO[AppEnv, ExitCode] = + override def serve(): URIO[AppEnv & HttpSecurity, ExitCode] = for - routes <- httpApp.routes() + security <- ZIO.service[HttpSecurity] + routes <- httpApp.routes(security) server <- BlazeServerBuilder[AppTask] .bindHttp(config.port, config.host) .withHttpApp(routes.orNotFound) diff --git a/server/src/main/scala/mdr/pdb/server/package.scala b/server/src/main/scala/mdr/pdb/server/package.scala index b7f1a3a..a6997ad 100644 --- a/server/src/main/scala/mdr/pdb/server/package.scala +++ b/server/src/main/scala/mdr/pdb/server/package.scala @@ -6,6 +6,7 @@ import mdr.pdb.proof.query.repo.ProofRepository import mdr.pdb.proof.command.entity.ProofCommandBus -type AppEnv = ZEnv & UsersRepository & ProofRepository & ProofCommandBus +type CustomAppEnv = UsersRepository & ProofRepository & ProofCommandBus +type AppEnv = ZEnv & CustomAppEnv type AppTask = RIO[AppEnv, *] type AppAuth = List[CommonProfile] diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala index ecb5f22..a95f9ef 100644 --- a/app/src/main/scala/mdr/pdb/app/Main.scala +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -18,16 +18,13 @@ import sttp.client3.* import fiftyforms.tapir.{CustomTapir, BaseUri} import mdr.pdb.users.query.client.UsersRepositoryLive +import mdr.pdb.proof.command.client.ProofCommandApiLive @js.native @JSImport("stylesheets/main.css", JSImport.Namespace) object Css extends js.Any @js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native @JSImport("params/pdb-params.json", JSImport.Default) object pdbParams extends js.Object @@ -53,7 +50,7 @@ AppConfig.layer.project(c => BaseUri(uri"${c.baseUrl}api/")) override val layer: ZLayer[ZIOAppArgs, Any, Environment] = - ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer + ZEnv.live >+> AppConfig.layer >+> baseUriLayer >+> sttpLayer >+> Routes.router >+> UsersRepositoryLive.layer >+> ApiLive.layer >+> ProofCommandApiLive.layer >+> state.AppStateLive.layer >+> LaminarAppLive.layer override def run = for diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala index f9d9c6a..ed4ada7 100644 --- a/app/src/main/scala/mdr/pdb/app/actions.scala +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -7,6 +7,7 @@ sealed trait Action case object CheckOnlineState extends Action + case object FetchDirectory extends Action case class FetchUserDetails(osc: OsobniCislo) extends Action case class FetchParameters(osc: OsobniCislo) extends Action @@ -18,4 +19,10 @@ page: (UserInfo, Parameter, ParameterCriterion) => Page ) extends Action case class FetchAvailableFiles(osc: OsobniCislo) extends Action + +case class SubmitProofCommand( + command: mdr.pdb.proof.command.Command, + next: Page +) extends Action + case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala index eff2276..e3c174a 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -9,6 +9,8 @@ import fiftyforms.services.files.File import mdr.pdb.parameters.* import mdr.pdb.users.query.* +import mdr.pdb.proof.command.CreateProof +import mdr.pdb.proof.command.Authorized object UpravDukaz: @@ -60,6 +62,18 @@ NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) case UpravDukazForm.AvailableFilesRequested => FetchAvailableFiles(s._1.personalNumber) + case UpravDukazForm.Submitted(files) => + SubmitProofCommand( + CreateProof( + "test", + s._1.personalNumber, + s._2.id, + s._3.id, + files.map(_.url), + Authorized(None) + ), + Page.DetailKriteria(s._1, s._2, s._3) + ) } ) ), diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 15222e1..25775b2 100644 --- a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -7,13 +7,13 @@ import org.scalajs.dom import com.raquo.laminar.nodes.ReactiveHtmlElement import fiftyforms.services.files.components.tailwind.FilePicker -import mdr.pdb.parameters.command.* import fiftyforms.services.files.File object UpravDukazForm: sealed trait Event case object Cancelled extends Event case object AvailableFilesRequested extends Event + case class Submitted(files: List[File]) extends Event def apply(availableFilesStream: EventStream[List[File]])( updates: Observer[Event] ): HtmlElement = @@ -35,6 +35,9 @@ tpe := "submit", cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", disabled <-- files.signal.map(_.isEmpty), + composeEvents(onClick.preventDefault)( + _.sample(files.signal).map(Submitted(_)) + ) --> updates, "Autorizovat" ) ) diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala index 8ea0261..3801f19 100644 --- a/app/src/main/scala/mdr/pdb/app/state/AppState.scala +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -20,6 +20,7 @@ import scala.annotation.unused import com.raquo.airstream.ownership.Subscription import mdr.pdb.users.query.client.UsersRepository +import mdr.pdb.proof.command.client.ProofCommandApi trait AppState extends components.AppPage.AppState @@ -37,7 +38,7 @@ object AppStateLive: def layer: URLayer[ - ZEnv & AppConfig & Api & UsersRepository & Router[Page], + ZEnv & AppConfig & Api & UsersRepository & ProofCommandApi & Router[Page], AppState ] = { (ZLayer.fromZIO(ZIO.runtime[ZEnv]) ++ ZIOOwner.layer) >>> ( @@ -45,13 +46,19 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv], owner: Owner ) => - AppStateLive(appConfig, api, usersRepository, router, runtime)(using - owner - ) + AppStateLive( + appConfig, + api, + usersRepository, + proofCommandApi, + router, + runtime + )(using owner) ).toLayer[AppState] } @@ -59,6 +66,7 @@ appConfig: AppConfig, api: Api, usersRepository: UsersRepository, + proofCommandApi: ProofCommandApi, router: Router[Page], runtime: Runtime[ZEnv] )(using @@ -82,17 +90,6 @@ private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] private val isOnline = Var(true) - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map(JSON.stringify(_).fromJson[UserInfo]) - .collect { case Right(u) => - u - } - .toList - private val mockParameters: List[Parameter] = pdbParams .asInstanceOf[js.Dictionary[js.Object]] @@ -118,40 +115,35 @@ yield () case FetchDirectory => for - users <- usersRepository.list() + users <- usersRepository.matching(AllUsers) _ <- Task.attempt(pushUsers(users)) yield () case FetchUserDetails(osc) => - Task.attempt { - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for o <- user do + pushDetails(o) + router.replaceState(Page.Detail(o)) case FetchParameters(osc) => Task.attempt(pushParameters(mockParameters)) case FetchParameter(osc, paramId) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) case FetchParameterCriterion(osc, paramId, critId, page) => - Task.attempt { - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - } - case NavigateTo(page) => Task.attempt { router.pushState(page) } + for user <- usersRepository.matching(UserWithOsobniCislo(osc)) + yield for + o <- user + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) case FetchAvailableFiles(osc) => Task.attempt { pushFiles( @@ -160,6 +152,12 @@ ) ) } + case SubmitProofCommand(cmd, next) => + for + _ <- proofCommandApi.submitCommand(cmd) + _ <- Task.attempt(router.pushState(next)) + yield () + case NavigateTo(page) => Task.attempt { router.pushState(page) } actions.events.foreach(action => runtime.unsafeRunAsync(handler(action))) diff --git a/build.sbt b/build.sbt index 227e292..13b2d5f 100644 --- a/build.sbt +++ b/build.sbt @@ -81,8 +81,8 @@ lazy val `akka-persistence-support` = project .in(file("fiftyforms/akka-persistence")) .settings( - IWDeps.akka.libs.persistence, - libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V, + libraryDependencies += IWDeps.akka.modules.persistence.cross(CrossVersion.for3Use2_13), + libraryDependencies += "com.typesafe.akka" %% "akka-cluster-sharding-typed" % IWDeps.akka.V cross(CrossVersion.for3Use2_13), IWDeps.akka.profiles.eventsourcedJdbcProjection ) @@ -138,6 +138,8 @@ IWDeps.logbackClassic, IWDeps.http4sPac4J, IWDeps.pac4jOIDC, + libraryDependencies += "mysql" % "mysql-connector-java" % "8.0.28", + // libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % IWDeps.akka.V, Docker / mappings ++= directory((app / viteBuild).value).map { case (f, p) => f -> s"/opt/docker/${p}" }, @@ -151,6 +153,7 @@ "APP_PATH" -> "/opt/docker/vite" ), reStart / envVars := Map( + "DB_HOST" -> "localhost", "APP_PATH" -> "../app/target/vite", "SECURITY_URLBASE" -> "http://localhost:8080", "SECURITY_DISCOVERYURI" -> "https://login.cmi.cz/auth/realms/MDRTest/.well-known/openid-configuration", diff --git a/core/src/main/scala/mdr/pdb/CborSerializable.scala b/core/src/main/scala/mdr/pdb/CborSerializable.scala new file mode 100644 index 0000000..aed0a8e --- /dev/null +++ b/core/src/main/scala/mdr/pdb/CborSerializable.scala @@ -0,0 +1,4 @@ +package mdr.pdb + +// Marker trait for Jackson serializer +trait CborSerializable diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e8b0422 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.1" +services: + mongo: + image: mongo:5.0.6 + ports: + - "27017:27017" + mysql: + image: mysql:5.7 + command: + - "--default-authentication-plugin=mysql_native_password" + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_unicode_ci" + environment: + MYSQL_ROOT_PASSWORD: mdrpdb + MYSQL_USER: mdrpdb + MYSQL_PASSWORD: mdrpdb + MYSQL_DATABASE: mdrpdb + ports: + - "3306:3306" diff --git a/domain/proof/command/client/src/ProofCommandApi.scala b/domain/proof/command/client/src/ProofCommandApi.scala new file mode 100644 index 0000000..d93fd76 --- /dev/null +++ b/domain/proof/command/client/src/ProofCommandApi.scala @@ -0,0 +1,26 @@ +package mdr.pdb.proof.command +package client + +import endpoints.Endpoints + +import zio.* +import fiftyforms.tapir.CustomTapir +import fiftyforms.tapir.BaseUri + +trait ProofCommandApi: + def submitCommand(command: Command): Task[Unit] + +object ProofCommandApi: + def submitCommand(command: Command): RIO[ProofCommandApi, Unit] = + ZIO.serviceWithZIO(_.submitCommand(command)) + +object ProofCommandApiLive: + val layer: URLayer[BaseUri & CustomTapir.Backend, ProofCommandApi] = + (ProofCommandApiLive(using _, _)).toLayer + +class ProofCommandApiLive(using baseUri: BaseUri, backend: CustomTapir.Backend) + extends ProofCommandApi + with CustomTapir: + private val submitCommandClient = makeClient(Endpoints.submitCommand) + override def submitCommand(command: Command): Task[Unit] = + ZIO.fromFuture(_ => submitCommandClient(command)) diff --git a/domain/proof/command/entity/src/ProofCommandBus.scala b/domain/proof/command/entity/src/ProofCommandBus.scala index 3167e02..ea009ab 100644 --- a/domain/proof/command/entity/src/ProofCommandBus.scala +++ b/domain/proof/command/entity/src/ProofCommandBus.scala @@ -21,7 +21,7 @@ for timeout <- Task.attempt( Timeout.create( - system.settings.config.getDuration("proof-bus.timeout") + system.settings.config.getDuration("domain.proof.bus.timeout") ) ) // TODO: init only once diff --git a/domain/proof/command/model/src/Command.scala b/domain/proof/command/model/src/Command.scala index 1bff3cd..1f35f67 100644 --- a/domain/proof/command/model/src/Command.scala +++ b/domain/proof/command/model/src/Command.scala @@ -5,7 +5,7 @@ import java.time.LocalDate import java.time.Instant -sealed trait Command: +sealed trait Command extends CborSerializable: def id: Proof.Id sealed trait AuthorizeOption diff --git a/domain/proof/shared/model/src/Event.scala b/domain/proof/shared/model/src/Event.scala index 1818110..c4efe55 100644 --- a/domain/proof/shared/model/src/Event.scala +++ b/domain/proof/shared/model/src/Event.scala @@ -3,12 +3,12 @@ import java.time.Instant -case class ProofEvent(event: Event, meta: WW) +case class ProofEvent(event: Event, meta: WW) extends CborSerializable object ProofEvent: val Tag = "proof" -sealed trait Event: +sealed trait Event extends CborSerializable: def id: Proof.Id case class ProofCreated( diff --git a/domain/users/query/api/src/UsersApi.scala b/domain/users/query/api/src/UsersApi.scala index f019bc7..4e5b427 100644 --- a/domain/users/query/api/src/UsersApi.scala +++ b/domain/users/query/api/src/UsersApi.scala @@ -9,6 +9,8 @@ object UsersApi extends CustomTapir: val list: ZServerEndpoint[UsersRepository, Any] = - Endpoints.list.zServerLogic(_ => - UsersRepository.list.mapError(InternalServerError.fromThrowable) + Endpoints.matching.zServerLogic(criteria => + UsersRepository + .matching(criteria) + .mapError(InternalServerError.fromThrowable) ) diff --git a/domain/users/query/client/src/UsersRepository.scala b/domain/users/query/client/src/UsersRepository.scala index eed4d38..534ac65 100644 --- a/domain/users/query/client/src/UsersRepository.scala +++ b/domain/users/query/client/src/UsersRepository.scala @@ -8,11 +8,11 @@ import fiftyforms.tapir.BaseUri trait UsersRepository: - def list(): Task[List[UserInfo]] + def matching(criteria: Criteria): Task[List[UserInfo]] object UsersRepository: - def list(): RIO[UsersRepository, List[UserInfo]] = - ZIO.serviceWithZIO(_.list()) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) object UsersRepositoryLive: @@ -22,6 +22,6 @@ class UsersRepositoryLive(using baseUri: BaseUri, backend: CustomTapir.Backend) extends UsersRepository with CustomTapir: - private val listClient = makeClient(Endpoints.list) - override def list(): Task[List[UserInfo]] = - ZIO.fromFuture(_ => listClient(())) + private val matchingClient = makeClient(Endpoints.matching) + override def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.fromFuture(_ => matchingClient(criteria)) diff --git a/domain/users/query/codecs/src/Codecs.scala b/domain/users/query/codecs/src/Codecs.scala index d30f318..7f70f0b 100644 --- a/domain/users/query/codecs/src/Codecs.scala +++ b/domain/users/query/codecs/src/Codecs.scala @@ -7,12 +7,14 @@ trait Codecs extends JsonCodecs with TapirCodecs trait JsonCodecs extends mdr.pdb.codecs.JsonCodecs: + given JsonCodec[Criteria] = DeriveJsonCodec.gen given JsonCodec[UserContract] = DeriveJsonCodec.gen given JsonCodec[UserFunction] = DeriveJsonCodec.gen given JsonCodec[UserInfo] = DeriveJsonCodec.gen given JsonCodec[UserProfile] = DeriveJsonCodec.gen trait TapirCodecs extends mdr.pdb.codecs.TapirCodecs: + given Schema[Criteria] = Schema.derived given Schema[UserContract] = Schema.derived given Schema[UserFunction] = Schema.derived given Schema[UserInfo] = Schema.derived diff --git a/domain/users/query/endpoints/src/Endpoints.scala b/domain/users/query/endpoints/src/Endpoints.scala index 16bd210..36dee94 100644 --- a/domain/users/query/endpoints/src/Endpoints.scala +++ b/domain/users/query/endpoints/src/Endpoints.scala @@ -10,8 +10,10 @@ with CustomTapir with Codecs: - val list: Endpoint[Unit, Unit, ServerError, List[UserInfo], Any] = + val matching: Endpoint[Unit, Criteria, ServerError, List[UserInfo], Any] = endpoint .in("users") + .post + .in(jsonBody[Criteria]) .out(jsonBody[List[UserInfo]]) .errorOut(jsonBody[ServerError]) diff --git a/domain/users/query/model/src/model.scala b/domain/users/query/model/src/model.scala index 664ae6f..a06ad66 100644 --- a/domain/users/query/model/src/model.scala +++ b/domain/users/query/model/src/model.scala @@ -3,6 +3,10 @@ import java.time.LocalDate +sealed trait Criteria +case object AllUsers extends Criteria +case class UserWithOsobniCislo(osobniCislo: OsobniCislo) extends Criteria + case class UserContract( rel: String, startDate: LocalDate, diff --git a/domain/users/query/repo/src/UsersRepository.scala b/domain/users/query/repo/src/UsersRepository.scala index 6ae948e..eb3f801 100644 --- a/domain/users/query/repo/src/UsersRepository.scala +++ b/domain/users/query/repo/src/UsersRepository.scala @@ -4,14 +4,20 @@ import zio.* -trait UsersRepository: - def list: Task[List[UserInfo]] - object UsersRepository: - def list: RIO[UsersRepository, List[UserInfo]] = ZIO.serviceWithZIO(_.list) + def matching(criteria: Criteria): RIO[UsersRepository, List[UserInfo]] = + ZIO.serviceWithZIO(_.matching(criteria)) + +trait UsersRepository: + def matching(criteria: Criteria): Task[List[UserInfo]] case class MockUsersRepository(users: List[UserInfo]) extends UsersRepository: - def list: Task[List[UserInfo]] = ZIO.succeed(users) + def matching(criteria: Criteria): Task[List[UserInfo]] = + ZIO.succeed( + criteria match + case AllUsers => users + case UserWithOsobniCislo(osc) => users.filter(_.personalNumber == osc) + ) object MockUsersRepository: val layer: TaskLayer[UsersRepository] = diff --git a/initdb.d/jdbc-journal.sql b/initdb.d/jdbc-journal.sql new file mode 100644 index 0000000..5c57be2 --- /dev/null +++ b/initdb.d/jdbc-journal.sql @@ -0,0 +1,38 @@ +CREATE TABLE IF NOT EXISTS event_journal( + ordering SERIAL, + deleted BOOLEAN DEFAULT false NOT NULL, + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + writer TEXT NOT NULL, + write_timestamp BIGINT NOT NULL, + adapter_manifest TEXT NOT NULL, + event_payload BLOB NOT NULL, + event_ser_id INTEGER NOT NULL, + event_ser_manifest TEXT NOT NULL, + meta_payload BLOB, + meta_ser_id INTEGER,meta_ser_manifest TEXT, + PRIMARY KEY(persistence_id,sequence_number) +); + +CREATE UNIQUE INDEX event_journal_ordering_idx ON event_journal(ordering); + +CREATE TABLE IF NOT EXISTS event_tag ( + event_id BIGINT UNSIGNED NOT NULL, + tag VARCHAR(255) NOT NULL, + PRIMARY KEY(event_id, tag), + FOREIGN KEY (event_id) + REFERENCES event_journal(ordering) + ON DELETE CASCADE + ); + +CREATE TABLE IF NOT EXISTS snapshot ( + persistence_id VARCHAR(255) NOT NULL, + sequence_number BIGINT NOT NULL, + created BIGINT NOT NULL, + snapshot_ser_id INTEGER NOT NULL, + snapshot_ser_manifest TEXT NOT NULL, + snapshot_payload BLOB NOT NULL, + meta_ser_id INTEGER, + meta_ser_manifest TEXT, + meta_payload BLOB, + PRIMARY KEY (persistence_id, sequence_number)); diff --git a/server/src/main/resources/application.conf b/server/src/main/resources/application.conf new file mode 100644 index 0000000..d0d4948 --- /dev/null +++ b/server/src/main/resources/application.conf @@ -0,0 +1,71 @@ +akka.actor.provider = "cluster" +akka.actor.allow-java-serialization = on + +//akka { +// actor { +// serializers { +// jackson-cbor = "akka.serialization.jackson.JacksonCborSerializer" +// } +// +// serialization-bindings { +// "mdr.pdb.CborSerializable" = jackson-cbor +// } +// } +//} + +domain { + proof { + bus { + timeout = 1m + } + } +} + +# Journal settings +include "general.conf" + +akka { + persistence { + journal { + plugin = "jdbc-journal" + // Enable the line below to automatically start the journal when the actorsystem is started + auto-start-journals = ["jdbc-journal"] + } + snapshot-store { + plugin = "jdbc-snapshot-store" + // Enable the line below to automatically start the snapshot-store when the actorsystem is started + auto-start-snapshot-stores = ["jdbc-snapshot-store"] + } + } +} + +jdbc-journal { + slick = ${slick} +} + +# the akka-persistence-snapshot-store in use +jdbc-snapshot-store { + slick = ${slick} +} + +# the akka-persistence-query provider in use +jdbc-read-journal { + slick = ${slick} +} + +docker.host = mysql + +slick { + profile = "slick.jdbc.MySQLProfile$" + db { + host = ${docker.host} + host = ${?DB_HOST} + url = "jdbc:mysql://"${slick.db.host}":3306/mdrpdb?cachePrepStmts=true&cacheCallableStmts=true&cacheServerConfiguration=true&useLocalSessionState=true&elideSetAutoCommits=true&alwaysSendSetIsolation=false&enableQueryTimeouts=false&connectionAttributes=none&verifyServerCertificate=false&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&useLegacyDatetimeCode=false&serverTimezone=UTC&rewriteBatchedStatements=true" + user = "mdrpdb" + password = "mdrpdb" + driver = "com.mysql.cj.jdbc.Driver" + numThreads = 5 + maxConnections = 5 + minConnections = 1 + } +} diff --git a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala index 4040e9d..8a601ae 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpApplication.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpApplication.scala @@ -18,28 +18,26 @@ import org.pac4j.core.profile.CommonProfile trait HttpApplication { - def routes(): UIO[HttpRoutes[AppTask]] + def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] } object HttpApplicationLive { - val layer: URLayer[AppConfig & HttpSecurity, HttpApplication] = - (HttpApplicationLive(_, _)).toLayer[HttpApplication] + val layer: URLayer[AppConfig, HttpApplication] = + (HttpApplicationLive(_)).toLayer[HttpApplication] } -case class HttpApplicationLive( - config: AppConfig, - security: HttpSecurity -) extends HttpApplication: +case class HttpApplicationLive(config: AppConfig) extends HttpApplication: import dsl.* val staticR = static.Routes(config) val apiR = api.Routes - def httpApp(appPath: String): HttpRoutes[AppTask] = + def httpApp(appPath: String, security: HttpSecurity): HttpRoutes[AppTask] = Router( security.route, - "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + // "/mdr" -> security.secure(apiR.routes <+> staticR.routes) + "/mdr" -> (apiR.routes <+> staticR.routes) ) - override def routes(): UIO[HttpRoutes[AppTask]] = - ZIO.succeed(httpApp(config.appPath)) + override def routes(security: HttpSecurity): UIO[HttpRoutes[AppTask]] = + ZIO.succeed(httpApp(config.appPath, security)) diff --git a/server/src/main/scala/mdr/pdb/server/HttpServer.scala b/server/src/main/scala/mdr/pdb/server/HttpServer.scala index e4e484c..d76dca5 100644 --- a/server/src/main/scala/mdr/pdb/server/HttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/HttpServer.scala @@ -3,4 +3,4 @@ import zio.* trait HttpServer: - def serve(): URIO[AppEnv, ExitCode] + def serve(): URIO[AppEnv & HttpSecurity, ExitCode] diff --git a/server/src/main/scala/mdr/pdb/server/Main.scala b/server/src/main/scala/mdr/pdb/server/Main.scala index f1af85d..9340477 100644 --- a/server/src/main/scala/mdr/pdb/server/Main.scala +++ b/server/src/main/scala/mdr/pdb/server/Main.scala @@ -11,43 +11,50 @@ import org.mongodb.scala.MongoClient import akka.actor.typed.ActorSystem import akka.actor.typed.scaladsl.Behaviors +import akka.cluster.typed.Cluster +import akka.cluster.typed.Join object Main extends ZIOAppDefault: override def hook = SLF4J.slf4j(LogLevel.Debug) - lazy val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = + val runtimeLayer: URLayer[AppEnv, Runtime[AppEnv]] = ZLayer.fromZIO(ZIO.runtime[AppEnv]) - lazy val mongoClientLayer: RLayer[ZEnv, MongoClient] = + val mongoClientLayer: RLayer[ZEnv, MongoClient] = MongoConfig.fromEnv >>> MongoClient.layer - lazy val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = + val proofRepositoryLayer: RLayer[ZEnv, ProofRepository] = (mongoClientLayer >+> MongoProofConfig.fromEnv) >>> MongoProofRepository.layer - lazy val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = + val securityLayer: ZLayer[AppEnv, ReadError[String], HttpSecurity] = security.Pac4jSecurityConfig.fromEnv ++ runtimeLayer >>> security.Pac4jHttpSecurity.layer - lazy val httpAppLayer: ZLayer[AppEnv, ReadError[String], HttpApplication] = - AppConfig.fromEnv ++ securityLayer >>> HttpApplicationLive.layer + val actorSystemLayer: TaskLayer[ActorSystem[_]] = + (for + system <- Task.attempt(ActorSystem(Behaviors.empty, "mdrpdb")) + _ <- Task.attempt { + val cluster = Cluster(system) + cluster.manager ! Join(cluster.selfMember.address) + } + yield system).toLayer - lazy val actorSystemLayer: TaskLayer[ActorSystem[_]] = - Task.attempt(ActorSystem(Behaviors.empty, "MDR PDB")).toLayer - - lazy val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = + val proofCommandBusLayer: RLayer[ZEnv, ProofCommandBus] = actorSystemLayer >>> ProofCommandBus.layer - lazy val appEnvLayer - : RLayer[ZEnv, UsersRepository & ProofRepository & ProofCommandBus] = + val appEnvLayer: RLayer[ZEnv, CustomAppEnv] = MockUsersRepository.layer >+> proofRepositoryLayer >+> proofCommandBusLayer - lazy val serverLayer: RLayer[ZEnv, HttpServer] = - appEnvLayer >+> blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer + val httpAppLayer: ZLayer[ZEnv, ReadError[String], HttpApplication] = + AppConfig.fromEnv >>> HttpApplicationLive.layer + + val serverLayer: RLayer[ZEnv, HttpServer] = + blaze.BlazeServerConfig.fromEnv >+> httpAppLayer >>> blaze.BlazeHttpServer.layer override def run = for { server <- ZIO .service[HttpServer] .provideCustom(serverLayer) - _ <- server.serve().provideCustom(appEnvLayer) + _ <- server.serve().provideCustom(appEnvLayer >+> securityLayer) } yield () diff --git a/server/src/main/scala/mdr/pdb/server/api/Routes.scala b/server/src/main/scala/mdr/pdb/server/api/Routes.scala index 4550a2b..944276c 100644 --- a/server/src/main/scala/mdr/pdb/server/api/Routes.scala +++ b/server/src/main/scala/mdr/pdb/server/api/Routes.scala @@ -11,6 +11,7 @@ import mdr.pdb.users.query.api.UsersApi import mdr.pdb.proof.query.api.ProofQueryApi import mdr.pdb.proof.command.api.ProofCommandApi +import org.http4s.HttpRoutes object Routes extends CustomTapir: import fiftyforms.tapir.InternalServerError @@ -26,5 +27,7 @@ ProofCommandApi.submitCommand.widen[AppEnv] ) - val routes: AuthedRoutes[AppAuth, AppTask] = - Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + //val routes: AuthedRoutes[AppAuth, AppTask] = + // Router("pdb/api" -> from(serverEndpoints).toRoutes).local(_.req) + val routes: HttpRoutes[AppTask] = + Router("pdb/api" -> from(serverEndpoints).toRoutes) diff --git a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala index 7ff459f..e9ca65a 100644 --- a/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala +++ b/server/src/main/scala/mdr/pdb/server/blaze/BlazeHttpServer.scala @@ -15,9 +15,10 @@ config: BlazeServerConfig, httpApp: HttpApplication ) extends HttpServer: - override def serve(): URIO[AppEnv, ExitCode] = + override def serve(): URIO[AppEnv & HttpSecurity, ExitCode] = for - routes <- httpApp.routes() + security <- ZIO.service[HttpSecurity] + routes <- httpApp.routes(security) server <- BlazeServerBuilder[AppTask] .bindHttp(config.port, config.host) .withHttpApp(routes.orNotFound) diff --git a/server/src/main/scala/mdr/pdb/server/package.scala b/server/src/main/scala/mdr/pdb/server/package.scala index b7f1a3a..a6997ad 100644 --- a/server/src/main/scala/mdr/pdb/server/package.scala +++ b/server/src/main/scala/mdr/pdb/server/package.scala @@ -6,6 +6,7 @@ import mdr.pdb.proof.query.repo.ProofRepository import mdr.pdb.proof.command.entity.ProofCommandBus -type AppEnv = ZEnv & UsersRepository & ProofRepository & ProofCommandBus +type CustomAppEnv = UsersRepository & ProofRepository & ProofCommandBus +type AppEnv = ZEnv & CustomAppEnv type AppTask = RIO[AppEnv, *] type AppAuth = List[CommonProfile] diff --git a/server/src/main/scala/mdr/pdb/server/static/Routes.scala b/server/src/main/scala/mdr/pdb/server/static/Routes.scala index f0d30e4..3af3a23 100644 --- a/server/src/main/scala/mdr/pdb/server/static/Routes.scala +++ b/server/src/main/scala/mdr/pdb/server/static/Routes.scala @@ -2,6 +2,7 @@ package static import org.http4s.AuthedRoutes +import org.http4s.HttpRoutes class Routes(config: AppConfig): import CustomTapir.* @@ -14,5 +15,7 @@ filesGetServerEndpoint("pdb")(config.appPath) ) - val routes: AuthedRoutes[AppAuth, AppTask] = - CustomTapir.from(serverEndpoints).toRoutes.local(_.req) + //val routes: AuthedRoutes[AppAuth, AppTask] = + // CustomTapir.from(serverEndpoints).toRoutes.local(_.req) + val routes: HttpRoutes[AppTask] = + CustomTapir.from(serverEndpoints).toRoutes