From 358ec586464a18c243d5319117903000c45b06e9 Mon Sep 17 00:00:00 2001 From: Maxime Date: Tue, 6 Jun 2023 10:36:16 +0200 Subject: [PATCH 01/16] feat : base implementation --- src/main/g8/backend/build.sbt | 10 ++++- src/main/g8/backend/src/main/scala/Main.scala | 13 ++++-- src/main/g8/backend/src/main/scala/Todo.scala | 7 +++ .../src/main/scala/TodoController.scala | 44 +++++++++++++++++++ .../backend/src/main/scala/TodoService.scala | 23 ++++++++++ 5 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 src/main/g8/backend/src/main/scala/Todo.scala create mode 100644 src/main/g8/backend/src/main/scala/TodoController.scala create mode 100644 src/main/g8/backend/src/main/scala/TodoService.scala diff --git a/src/main/g8/backend/build.sbt b/src/main/g8/backend/build.sbt index da76952..d326608 100644 --- a/src/main/g8/backend/build.sbt +++ b/src/main/g8/backend/build.sbt @@ -1,9 +1,15 @@ // give the user a nice default project! ThisBuild / organization := "com.do" -ThisBuild / scalaVersion := "2.12.8" +ThisBuild / scalaVersion := "2.12.16" lazy val root = (project in file(".")). settings( name := "Fullstack Scaffhold", - mainClass := Some("com.do.Main") + mainClass := Some("com.do.Main"), + libraryDependencies ++= Seq( + "dev.zio" %% "zio" % "2.0.14", + "dev.zio" %% "zio-http" % "3.0.0-RC1", + "dev.zio" %% "zio-sql" % "0.1.2", + "dev.zio" %% "zio-sql-postgres" % "0.1.2" + ) ) diff --git a/src/main/g8/backend/src/main/scala/Main.scala b/src/main/g8/backend/src/main/scala/Main.scala index 54ccbec..c0f0ed4 100644 --- a/src/main/g8/backend/src/main/scala/Main.scala +++ b/src/main/g8/backend/src/main/scala/Main.scala @@ -1,5 +1,10 @@ -object Main { - def main(args: Array[String]): Unit = { - println("Hello, world") - } +package todo + +import zio._ + +import zio.http._ + +object TodoApp extends ZIOAppDefault { + // Run it like any simple app + override val run = Server.serve(TodoController.routes).provide(Server.default) } \ No newline at end of file diff --git a/src/main/g8/backend/src/main/scala/Todo.scala b/src/main/g8/backend/src/main/scala/Todo.scala new file mode 100644 index 0000000..10b836b --- /dev/null +++ b/src/main/g8/backend/src/main/scala/Todo.scala @@ -0,0 +1,7 @@ +package todo + +final case class Todo( + id: Int, + title: String, + completed: Boolean +) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala new file mode 100644 index 0000000..1cf8038 --- /dev/null +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -0,0 +1,44 @@ +package todo + +import zio._ +import zio.http._ +import zio.json._ + +object TodoController { + + val BasePath = !! / "todos" + + implicit val todoEncoder: JsonEncoder[Todo] = DeriveJsonEncoder.gen[Todo] + implicit val todosEncoder: JsonEncoder[List[Todo]] = DeriveJsonEncoder.gen[List[Todo]] + + val routes: HttpApp[Any, Nothing] = Http.collect[Request] { + case Method.GET -> BasePath => { + Response.text("TODO: get all todos") + //TodoService.getTodos().map(_.toJson).map(Response.text(_)) + } + case Method.GET -> BasePath / id => { + if (id.forall(_.isDigit)) { + Response.text("TODO: get a todo by id") + } else { + Response.fromHttpError(HttpError.BadRequest()) + } + } + case Method.POST -> BasePath => { + Response.text("TODO: create a todo") + } + case Method.PUT -> BasePath / id => { + if (id.forall(_.isDigit)) { + Response.text("TODO: update a todo by id") + } else { + Response.fromHttpError(HttpError.BadRequest()) + } + } + case Method.DELETE -> BasePath / id => { + if (id.forall(_.isDigit)) { + Response.text("TODO: delete a todo by id") + } else { + Response.fromHttpError(HttpError.BadRequest()) + } + } + } +} \ No newline at end of file diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala new file mode 100644 index 0000000..9474536 --- /dev/null +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -0,0 +1,23 @@ +package todo + +import zio._ + +object TodoService { + + private var todos: List[Todo] = List( + Todo(1, "Faire les courses", completed = false), + Todo(2, "Terminer le projet", completed = false), + Todo(3, "Apprendre Scala", completed = true) + ) + + def getTodos(): Task[List[Todo]] = ??? + + def getTodoById(id: Int): Task[Option[Todo]] = ??? + + def createTodo(): Task[Todo] = ??? + + def updateTodo( todoId : Int, todo : Todo): Task[Todo] = ??? + + def deleteTodoById(id: Int): Task[Unit] = ??? + +} From dbd514a353b11ce4f3e4cfecb141c89c1b0a9539 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 7 Jun 2023 10:26:43 +0200 Subject: [PATCH 02/16] feat: add server port configuration Signed-off-by: esteban baron --- src/main/g8/backend/src/main/scala/Main.scala | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/Main.scala b/src/main/g8/backend/src/main/scala/Main.scala index c0f0ed4..c2edc52 100644 --- a/src/main/g8/backend/src/main/scala/Main.scala +++ b/src/main/g8/backend/src/main/scala/Main.scala @@ -1,10 +1,20 @@ package todo import zio._ - import zio.http._ object TodoApp extends ZIOAppDefault { - // Run it like any simple app - override val run = Server.serve(TodoController.routes).provide(Server.default) -} \ No newline at end of file + val port: Int = + sys.env + .get("PORT") + .filter(_.nonEmpty) + .map(_.toInt) + .getOrElse(8080) + + private val config = Server.Config.default.port(port) + private val configLayer = ZLayer.succeed(config) + + printf("Server listening on http://0.0.0.0:%d\n", port) + override val run = + Server.serve(TodoController.routes).provide(configLayer, Server.live) +} From ef3c0d346d3bd81a603505582093d78c780eb5dc Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 7 Jun 2023 13:28:11 +0200 Subject: [PATCH 03/16] wip: add mongodb connection and start service implementation Signed-off-by: esteban baron --- src/main/g8/backend/build.sbt | 27 ++++++++------- .../src/main/scala/MongoDBClient.scala | 8 +++++ .../src/main/scala/MongoDBConfig.scala | 3 ++ .../src/main/scala/TodoController.scala | 20 ++++++++--- .../backend/src/main/scala/TodoService.scala | 34 +++++++++++++------ 5 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 src/main/g8/backend/src/main/scala/MongoDBClient.scala create mode 100644 src/main/g8/backend/src/main/scala/MongoDBConfig.scala diff --git a/src/main/g8/backend/build.sbt b/src/main/g8/backend/build.sbt index d326608..deae2d9 100644 --- a/src/main/g8/backend/build.sbt +++ b/src/main/g8/backend/build.sbt @@ -1,15 +1,18 @@ // give the user a nice default project! ThisBuild / organization := "com.do" -ThisBuild / scalaVersion := "2.12.16" +ThisBuild / scalaVersion := "2.13.10" -lazy val root = (project in file(".")). - settings( - name := "Fullstack Scaffhold", - mainClass := Some("com.do.Main"), - libraryDependencies ++= Seq( - "dev.zio" %% "zio" % "2.0.14", - "dev.zio" %% "zio-http" % "3.0.0-RC1", - "dev.zio" %% "zio-sql" % "0.1.2", - "dev.zio" %% "zio-sql-postgres" % "0.1.2" - ) - ) +lazy val root = (project in file(".")).settings( + name := "Fullstack Scaffhold", + mainClass := Some("com.do.Main"), + libraryDependencies ++= Seq( + "dev.zio" %% "zio" % "2.0.14", + "dev.zio" %% "zio-http" % "3.0.0-RC1", + "dev.zio" %% "zio-sql" % "0.1.2", + "dev.zio" %% "zio-sql-postgres" % "0.1.2", + "dev.zio" %% "zio-streams" % "1.0.12", + "dev.zio" %% "zio-interop-cats" % "3.1.1.0" + ), + libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "4.4.0", + libraryDependencies += "org.mongodb.scala" %% "mongo-scala-bson" % "4.4.0" +) diff --git a/src/main/g8/backend/src/main/scala/MongoDBClient.scala b/src/main/g8/backend/src/main/scala/MongoDBClient.scala new file mode 100644 index 0000000..af562aa --- /dev/null +++ b/src/main/g8/backend/src/main/scala/MongoDBClient.scala @@ -0,0 +1,8 @@ +package todo + +import org.mongodb.scala._ + +object MongoDBClient { + def createClient(config: MongoDBConfig): MongoClient = + MongoClient(config.uri) +} diff --git a/src/main/g8/backend/src/main/scala/MongoDBConfig.scala b/src/main/g8/backend/src/main/scala/MongoDBConfig.scala new file mode 100644 index 0000000..589f244 --- /dev/null +++ b/src/main/g8/backend/src/main/scala/MongoDBConfig.scala @@ -0,0 +1,3 @@ +package todo + +case class MongoDBConfig(uri: String, database: String) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index 1cf8038..c3c8467 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -9,16 +9,26 @@ object TodoController { val BasePath = !! / "todos" implicit val todoEncoder: JsonEncoder[Todo] = DeriveJsonEncoder.gen[Todo] - implicit val todosEncoder: JsonEncoder[List[Todo]] = DeriveJsonEncoder.gen[List[Todo]] + implicit val todosEncoder: JsonEncoder[List[Todo]] = + DeriveJsonEncoder.gen[List[Todo]] val routes: HttpApp[Any, Nothing] = Http.collect[Request] { case Method.GET -> BasePath => { Response.text("TODO: get all todos") - //TodoService.getTodos().map(_.toJson).map(Response.text(_)) - } + // TodoService.getTodos().map(_.toJson).map(Response.text(_)) + } case Method.GET -> BasePath / id => { if (id.forall(_.isDigit)) { - Response.text("TODO: get a todo by id") + // Response.text("TODO: get a todo by id") + TodoService + .getTodoById(id.toInt) + .map(_.toJson) + .map(Response.text(_)) + .orElse( + Response.fromHttpError( + HttpError.NotFound(s"Todo with ID $id not found") + ) + ) } else { Response.fromHttpError(HttpError.BadRequest()) } @@ -41,4 +51,4 @@ object TodoController { } } } -} \ No newline at end of file +} diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index 9474536..8afe562 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -1,23 +1,35 @@ package todo +import zio.Task import zio._ -object TodoService { +import org.mongodb.scala._ +import org.mongodb.scala.model.Filters._ +import org.mongodb.scala.model.Updates._ +import java.util.concurrent.FutureTask - private var todos: List[Todo] = List( - Todo(1, "Faire les courses", completed = false), - Todo(2, "Terminer le projet", completed = false), - Todo(3, "Apprendre Scala", completed = true) +object TodoService { + private val todosCollection: MongoCollection[Todo] = { + val client = MongoDBClient.createClient( + MongoDBConfig("mongodb://root:root@localhost:27017", "todoapp") ) + val database = client.getDatabase("todoapp") + + database.getCollection[Todo]("todos") + } - def getTodos(): Task[List[Todo]] = ??? + def getTodos(): Task[List[Todo]] = ??? +// ZIO.fromFuture(_ => todosCollection.find().toFuture()) - def getTodoById(id: Int): Task[Option[Todo]] = ??? + def getTodoById(id: Int): Task[Option[Todo]] = + ZIO.fromFuture(_ => todosCollection.find(equal("id", id)).headOption()) - def createTodo(): Task[Todo] = ??? + def createTodo(): Task[Todo] = + ??? - def updateTodo( todoId : Int, todo : Todo): Task[Todo] = ??? + def updateTodo(todoId: Int, todo: Todo): Task[Todo] = + ??? - def deleteTodoById(id: Int): Task[Unit] = ??? - + def deleteTodoById(id: Int): Task[Unit] = + ??? } From d8a41cb715cafe8504fe9419943d3c00f35105e9 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 14 Jun 2023 11:41:07 +0200 Subject: [PATCH 04/16] feat: update GET routes (fetch data from database are not working yet) Signed-off-by: esteban baron --- src/main/g8/backend/.scalafmt.conf | 2 + src/main/g8/backend/data.json | 12 ++++ src/main/g8/backend/docker-compose.yaml | 17 +++++ src/main/g8/backend/project/metals.sbt | 6 ++ .../g8/backend/project/project/metals.sbt | 6 ++ src/main/g8/backend/src/main/scala/Main.scala | 2 +- .../src/main/scala/TodoController.scala | 71 +++++++++++-------- .../backend/src/main/scala/TodoService.scala | 6 +- 8 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 src/main/g8/backend/.scalafmt.conf create mode 100644 src/main/g8/backend/data.json create mode 100644 src/main/g8/backend/docker-compose.yaml create mode 100644 src/main/g8/backend/project/metals.sbt create mode 100644 src/main/g8/backend/project/project/metals.sbt diff --git a/src/main/g8/backend/.scalafmt.conf b/src/main/g8/backend/.scalafmt.conf new file mode 100644 index 0000000..755b21e --- /dev/null +++ b/src/main/g8/backend/.scalafmt.conf @@ -0,0 +1,2 @@ +version = "3.7.3" +runner.dialect = scala213 \ No newline at end of file diff --git a/src/main/g8/backend/data.json b/src/main/g8/backend/data.json new file mode 100644 index 0000000..c0c2d3e --- /dev/null +++ b/src/main/g8/backend/data.json @@ -0,0 +1,12 @@ +[ + { + "id": 1, + "title": "Faire du sale", + "completed": false + }, + { + "id": 2, + "title": "Manger du poulet", + "completed": false + } +] diff --git a/src/main/g8/backend/docker-compose.yaml b/src/main/g8/backend/docker-compose.yaml new file mode 100644 index 0000000..586b4e0 --- /dev/null +++ b/src/main/g8/backend/docker-compose.yaml @@ -0,0 +1,17 @@ +# Use root/example as user/password credentials +version: '3.1' + +services: + mongo: + image: mongo + ports: + - 27018:27017 + environment: + - MONGO_INITDB_ROOT_USERNAME=root + - MONGO_INITDB_ROOT_PASSWORD=root + - MONGO_INITDB_DATABASE=todo_app + volumes: + - mongodb_data_todo_app:/data/db + +volumes: + mongodb_data_todo_app: diff --git a/src/main/g8/backend/project/metals.sbt b/src/main/g8/backend/project/metals.sbt new file mode 100644 index 0000000..05fd2b3 --- /dev/null +++ b/src/main/g8/backend/project/metals.sbt @@ -0,0 +1,6 @@ +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.6") + diff --git a/src/main/g8/backend/project/project/metals.sbt b/src/main/g8/backend/project/project/metals.sbt new file mode 100644 index 0000000..05fd2b3 --- /dev/null +++ b/src/main/g8/backend/project/project/metals.sbt @@ -0,0 +1,6 @@ +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.6") + diff --git a/src/main/g8/backend/src/main/scala/Main.scala b/src/main/g8/backend/src/main/scala/Main.scala index c2edc52..1a60181 100644 --- a/src/main/g8/backend/src/main/scala/Main.scala +++ b/src/main/g8/backend/src/main/scala/Main.scala @@ -14,7 +14,7 @@ object TodoApp extends ZIOAppDefault { private val config = Server.Config.default.port(port) private val configLayer = ZLayer.succeed(config) - printf("Server listening on http://0.0.0.0:%d\n", port) + printf("Server listening on http://localhost:%d\n", port) override val run = Server.serve(TodoController.routes).provide(configLayer, Server.live) } diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index c3c8467..ba924f6 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -12,43 +12,56 @@ object TodoController { implicit val todosEncoder: JsonEncoder[List[Todo]] = DeriveJsonEncoder.gen[List[Todo]] - val routes: HttpApp[Any, Nothing] = Http.collect[Request] { - case Method.GET -> BasePath => { - Response.text("TODO: get all todos") - // TodoService.getTodos().map(_.toJson).map(Response.text(_)) - } - case Method.GET -> BasePath / id => { - if (id.forall(_.isDigit)) { - // Response.text("TODO: get a todo by id") + val routes: Http[Any, Nothing, Request, Response] = + Http.collectZIO[Request] { + case Method.GET -> BasePath => { TodoService - .getTodoById(id.toInt) + .getTodos() .map(_.toJson) .map(Response.text(_)) .orElse( - Response.fromHttpError( - HttpError.NotFound(s"Todo with ID $id not found") + ZIO.succeed( + Response.fromHttpError( + HttpError.NotFound("No todos found") + ) ) ) - } else { - Response.fromHttpError(HttpError.BadRequest()) } - } - case Method.POST -> BasePath => { - Response.text("TODO: create a todo") - } - case Method.PUT -> BasePath / id => { - if (id.forall(_.isDigit)) { - Response.text("TODO: update a todo by id") - } else { - Response.fromHttpError(HttpError.BadRequest()) + case Method.GET -> BasePath / id => { + if (id.forall(_.isDigit)) { + TodoService + .getTodoById(id.toInt) + .map(_.toJson) + .map(Response.text(_)) + .orElse( + ZIO.succeed( + Response.fromHttpError( + HttpError.NotFound(s"Todo with ID $id not found") + ) + ) + ) + } else { + ZIO.succeed( + Response.fromHttpError(HttpError.BadRequest("Invalid ID format")) + ) + } } - } - case Method.DELETE -> BasePath / id => { - if (id.forall(_.isDigit)) { - Response.text("TODO: delete a todo by id") - } else { - Response.fromHttpError(HttpError.BadRequest()) + case Method.POST -> BasePath => { + ZIO.succeed(Response.text("TODO: create a todo")) + } + case Method.PUT -> BasePath / id => { + if (id.forall(_.isDigit)) { + ZIO.succeed(Response.text("TODO: update a todo by id")) + } else { + ZIO.succeed(Response.fromHttpError(HttpError.BadRequest())) + } + } + case Method.DELETE -> BasePath / id => { + if (id.forall(_.isDigit)) { + ZIO.succeed(Response.text("TODO: delete a todo by id")) + } else { + ZIO.succeed(Response.fromHttpError(HttpError.BadRequest())) + } } } - } } diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index 8afe562..d19814c 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -11,15 +11,15 @@ import java.util.concurrent.FutureTask object TodoService { private val todosCollection: MongoCollection[Todo] = { val client = MongoDBClient.createClient( - MongoDBConfig("mongodb://root:root@localhost:27017", "todoapp") + MongoDBConfig("mongodb://root:root@localhost:27018", "todoapp") ) val database = client.getDatabase("todoapp") database.getCollection[Todo]("todos") } - def getTodos(): Task[List[Todo]] = ??? -// ZIO.fromFuture(_ => todosCollection.find().toFuture()) + def getTodos(): Task[Seq[Todo]] = + ZIO.fromFuture(_ => todosCollection.find().toFuture()).debug("getTodos") def getTodoById(id: Int): Task[Option[Todo]] = ZIO.fromFuture(_ => todosCollection.find(equal("id", id)).headOption()) From c9946bab07719ccf1e9dbd2437e1c84a3a22b313 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 14 Jun 2023 13:13:55 +0200 Subject: [PATCH 05/16] fix: connection with database & route get all todos Signed-off-by: esteban baron --- src/main/g8/backend/build.sbt | 8 ++--- src/main/g8/backend/src/main/scala/DB.scala | 30 +++++++++++++++++++ .../backend/src/main/scala/TodoService.scala | 15 +++------- 3 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/main/g8/backend/src/main/scala/DB.scala diff --git a/src/main/g8/backend/build.sbt b/src/main/g8/backend/build.sbt index deae2d9..79c9122 100644 --- a/src/main/g8/backend/build.sbt +++ b/src/main/g8/backend/build.sbt @@ -11,8 +11,8 @@ lazy val root = (project in file(".")).settings( "dev.zio" %% "zio-sql" % "0.1.2", "dev.zio" %% "zio-sql-postgres" % "0.1.2", "dev.zio" %% "zio-streams" % "1.0.12", - "dev.zio" %% "zio-interop-cats" % "3.1.1.0" - ), - libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "4.4.0", - libraryDependencies += "org.mongodb.scala" %% "mongo-scala-bson" % "4.4.0" + "dev.zio" %% "zio-interop-cats" % "3.1.1.0", + "org.mongodb.scala" %% "mongo-scala-driver" % "4.9.1", + "org.mongodb.scala" %% "mongo-scala-bson" % "4.9.1" + ) ) diff --git a/src/main/g8/backend/src/main/scala/DB.scala b/src/main/g8/backend/src/main/scala/DB.scala new file mode 100644 index 0000000..97fcb96 --- /dev/null +++ b/src/main/g8/backend/src/main/scala/DB.scala @@ -0,0 +1,30 @@ +package todo + +import org.bson.codecs.configuration.CodecRegistry +import org.bson.codecs.configuration.CodecRegistries.{ + fromProviders, + fromRegistries +} +import org.mongodb.scala.{MongoClient, MongoCollection} +import org.mongodb.scala.MongoClient.DEFAULT_CODEC_REGISTRY +import org.mongodb.scala.bson.codecs.Macros._ + +object DB { + // Définissez votre URL de connexion MongoDB + private val connectionString: String = + "mongodb://root:root@localhost:27018" + + private val customCodecs: CodecRegistry = + fromProviders(classOf[Todo]) + + private val codecRegistry: CodecRegistry = + fromRegistries(customCodecs, DEFAULT_CODEC_REGISTRY) + + private val database = + MongoClient(connectionString) + .getDatabase("todoapp") + .withCodecRegistry(codecRegistry) + + val todosCollection: MongoCollection[Todo] = + database.getCollection[Todo]("todos") +} diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index d19814c..de275cc 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -3,23 +3,16 @@ package todo import zio.Task import zio._ -import org.mongodb.scala._ import org.mongodb.scala.model.Filters._ import org.mongodb.scala.model.Updates._ -import java.util.concurrent.FutureTask -object TodoService { - private val todosCollection: MongoCollection[Todo] = { - val client = MongoDBClient.createClient( - MongoDBConfig("mongodb://root:root@localhost:27018", "todoapp") - ) - val database = client.getDatabase("todoapp") +import org.mongodb.scala.MongoCollection - database.getCollection[Todo]("todos") - } +object TodoService { + private val todosCollection: MongoCollection[Todo] = DB.todosCollection def getTodos(): Task[Seq[Todo]] = - ZIO.fromFuture(_ => todosCollection.find().toFuture()).debug("getTodos") + ZIO.fromFuture(_ => todosCollection.find().toFuture()) def getTodoById(id: Int): Task[Option[Todo]] = ZIO.fromFuture(_ => todosCollection.find(equal("id", id)).headOption()) From 24d6b9190c37cf527c186890ec4a90bd01df4cbd Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 14 Jun 2023 13:17:26 +0200 Subject: [PATCH 06/16] feat: add database url configuration in env variable Signed-off-by: esteban baron --- src/main/g8/backend/src/main/scala/DB.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/DB.scala b/src/main/g8/backend/src/main/scala/DB.scala index 97fcb96..7f5eed6 100644 --- a/src/main/g8/backend/src/main/scala/DB.scala +++ b/src/main/g8/backend/src/main/scala/DB.scala @@ -10,9 +10,9 @@ import org.mongodb.scala.MongoClient.DEFAULT_CODEC_REGISTRY import org.mongodb.scala.bson.codecs.Macros._ object DB { - // Définissez votre URL de connexion MongoDB - private val connectionString: String = - "mongodb://root:root@localhost:27018" + private val databaseURL: String = + Option(System.getenv("MONGO_URL")) + .getOrElse("mongodb://username:password@localhost:27017") private val customCodecs: CodecRegistry = fromProviders(classOf[Todo]) @@ -21,7 +21,7 @@ object DB { fromRegistries(customCodecs, DEFAULT_CODEC_REGISTRY) private val database = - MongoClient(connectionString) + MongoClient(databaseURL) .getDatabase("todoapp") .withCodecRegistry(codecRegistry) From 875e30c022aef6ff0e258a6defc4c992dd528d5c Mon Sep 17 00:00:00 2001 From: esteban baron Date: Wed, 14 Jun 2023 13:25:22 +0200 Subject: [PATCH 07/16] feat: implement delete route Signed-off-by: esteban baron --- .../g8/backend/src/main/scala/TodoController.scala | 11 ++++++++++- src/main/g8/backend/src/main/scala/TodoService.scala | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index ba924f6..14d837b 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -58,7 +58,16 @@ object TodoController { } case Method.DELETE -> BasePath / id => { if (id.forall(_.isDigit)) { - ZIO.succeed(Response.text("TODO: delete a todo by id")) + TodoService + .deleteTodoById(id.toInt) + .map(_ => Response.text(s"Task $id Deleted")) + .orElse( + ZIO.succeed( + Response.fromHttpError( + HttpError.NotFound(s"Todo with ID $id not found") + ) + ) + ) } else { ZIO.succeed(Response.fromHttpError(HttpError.BadRequest())) } diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index de275cc..9166436 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -24,5 +24,8 @@ object TodoService { ??? def deleteTodoById(id: Int): Task[Unit] = - ??? + ZIO + .fromFuture(_ => todosCollection.deleteOne(equal("id", id)).toFuture()) + .unit + } From 5e37833a2bba3f759b6239b17a8f4a8ea7ee891c Mon Sep 17 00:00:00 2001 From: esteban baron Date: Thu, 15 Jun 2023 16:16:17 +0200 Subject: [PATCH 08/16] feat: impl post route with query params Signed-off-by: esteban baron --- src/main/g8/backend/src/main/scala/DB.scala | 2 +- src/main/g8/backend/src/main/scala/Todo.scala | 9 ++++++++- .../src/main/scala/TodoController.scala | 20 +++++++++++++------ .../backend/src/main/scala/TodoService.scala | 11 ++++++++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/DB.scala b/src/main/g8/backend/src/main/scala/DB.scala index 7f5eed6..166bf47 100644 --- a/src/main/g8/backend/src/main/scala/DB.scala +++ b/src/main/g8/backend/src/main/scala/DB.scala @@ -12,7 +12,7 @@ import org.mongodb.scala.bson.codecs.Macros._ object DB { private val databaseURL: String = Option(System.getenv("MONGO_URL")) - .getOrElse("mongodb://username:password@localhost:27017") + .getOrElse("mongodb://root:root@localhost:27018") private val customCodecs: CodecRegistry = fromProviders(classOf[Todo]) diff --git a/src/main/g8/backend/src/main/scala/Todo.scala b/src/main/g8/backend/src/main/scala/Todo.scala index 10b836b..e42ebd6 100644 --- a/src/main/g8/backend/src/main/scala/Todo.scala +++ b/src/main/g8/backend/src/main/scala/Todo.scala @@ -1,7 +1,14 @@ package todo -final case class Todo( +import zio.json._ + +case class Todo( id: Int, title: String, completed: Boolean ) + +object Todo { + implicit val todoEncoder: JsonEncoder[Todo] = DeriveJsonEncoder.gen[Todo] + implicit val todoDecoder: JsonDecoder[Todo] = DeriveJsonDecoder.gen[Todo] +} diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index 14d837b..7027370 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -8,10 +8,6 @@ object TodoController { val BasePath = !! / "todos" - implicit val todoEncoder: JsonEncoder[Todo] = DeriveJsonEncoder.gen[Todo] - implicit val todosEncoder: JsonEncoder[List[Todo]] = - DeriveJsonEncoder.gen[List[Todo]] - val routes: Http[Any, Nothing, Request, Response] = Http.collectZIO[Request] { case Method.GET -> BasePath => { @@ -46,8 +42,20 @@ object TodoController { ) } } - case Method.POST -> BasePath => { - ZIO.succeed(Response.text("TODO: create a todo")) + case req @ Method.POST -> BasePath => { + (for { + queryParams <- ZIO + .fromOption(Option(req.url.queryParams)) + .orElseFail(HttpError.BadRequest("Missing query parameters")) + title <- ZIO + .fromOption(queryParams.get("title").collect(_.head)) + .orElseFail(HttpError.BadRequest("Missing 'title' parameter")) + createdTodo <- TodoService.createTodo(title) + } yield createdTodo) + .fold( + error => Response.fromHttpError(HttpError.InternalServerError()), + todo => Response.text(todo.toJson) + ) } case Method.PUT -> BasePath / id => { if (id.forall(_.isDigit)) { diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index 9166436..f48d445 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -17,8 +17,15 @@ object TodoService { def getTodoById(id: Int): Task[Option[Todo]] = ZIO.fromFuture(_ => todosCollection.find(equal("id", id)).headOption()) - def createTodo(): Task[Todo] = - ??? + def createTodo(title: String): Task[Todo] = + for { + todos <- getTodos() + newId = todos.map(_.id).max + 1 + newTodo = Todo(newId, title, false) + _ <- ZIO + .fromFuture(_ => todosCollection.insertOne(newTodo).toFuture()) + .unit + } yield newTodo def updateTodo(todoId: Int, todo: Todo): Task[Todo] = ??? From 48822200ff55abca4bb95afe606b147f8fcb318d Mon Sep 17 00:00:00 2001 From: esteban baron Date: Thu, 15 Jun 2023 16:25:11 +0200 Subject: [PATCH 09/16] feat: impl put route (allow us to update completed field of a task) Signed-off-by: esteban baron --- .../src/main/scala/TodoController.scala | 12 +++++++++- .../backend/src/main/scala/TodoService.scala | 24 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index 7027370..8c2bee7 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -59,7 +59,17 @@ object TodoController { } case Method.PUT -> BasePath / id => { if (id.forall(_.isDigit)) { - ZIO.succeed(Response.text("TODO: update a todo by id")) + TodoService + .updateTodo(id.toInt) + .map(_.toJson) + .map(Response.text(_)) + .orElse( + ZIO.succeed( + Response.fromHttpError( + HttpError.NotFound(s"Todo with ID $id not found") + ) + ) + ) } else { ZIO.succeed(Response.fromHttpError(HttpError.BadRequest())) } diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index f48d445..628ee72 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -27,8 +27,28 @@ object TodoService { .unit } yield newTodo - def updateTodo(todoId: Int, todo: Todo): Task[Todo] = - ??? + def updateTodo(todoId: Int): Task[Todo] = + for { + taskToChange <- getTodoById(todoId) + updatedTodo <- taskToChange match { + case Some(todo) => + val updated = todo.copy(completed = !todo.completed) + ZIO + .fromFuture(_ => + todosCollection + .updateOne( + equal("id", todoId), + set("completed", updated.completed) + ) + .toFuture() + ) + .map(_ => updated) + case None => + ZIO.fail( + new NoSuchElementException(s"Todo with ID $todoId not found") + ) + } + } yield updatedTodo def deleteTodoById(id: Int): Task[Unit] = ZIO From 05b1008d1f25dbd54d12de4c7ddf88c8c7e0fdcb Mon Sep 17 00:00:00 2001 From: esteban baron Date: Thu, 15 Jun 2023 16:51:33 +0200 Subject: [PATCH 10/16] feat: add README.md with routes documentation Signed-off-by: esteban baron --- src/main/g8/backend/README.md | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/main/g8/backend/README.md diff --git a/src/main/g8/backend/README.md b/src/main/g8/backend/README.md new file mode 100644 index 0000000..a82371d --- /dev/null +++ b/src/main/g8/backend/README.md @@ -0,0 +1,81 @@ +# Scalajs backend app + +## Routes + +### GET /todos + +Returns a list of todos tasks + +_Response example:_ + +```json +[ + { + "id": 1, + "title": "Todo 1", + "completed": false + }, + { + "id": 2, + "title": "Todo 2", + "completed": false + }, + { + "id": 3, + "title": "Todo 3", + "completed": false + } +] +``` + +### GET /todos/:id + +Returns a todo task + +_Response example:_ + +```json +{ + "id": 1, + "title": "Todo 1", + "completed": false +} +``` + +### POST /todos?title=string + +Creates a todo task + +_Response example:_ + +```json +{ + "id": 1, + "title": "Todo 1", + "completed": false +} +``` + +### PUT /todos/:id + +Updates a todo task by changing the completed status + +_Response example:_ + +```json +{ + "id": 1, + "title": "Todo 1", + "completed": true +} +``` + +### DELETE /todos/:id + +Deletes a todo task + +_Response example:_ a message confirming the deletion + +``` +"Task 1 Deleted" +``` From 468ae5ee3b3156ca6d69edebefcad78ea3ef4dc9 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Thu, 15 Jun 2023 16:59:02 +0200 Subject: [PATCH 11/16] feat: update DELETE route return nothing if the task we want to delete doesn't exist Signed-off-by: esteban baron --- src/main/g8/backend/README.md | 2 +- src/main/g8/backend/src/main/scala/TodoController.scala | 2 +- src/main/g8/backend/src/main/scala/TodoService.scala | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/g8/backend/README.md b/src/main/g8/backend/README.md index a82371d..2ddf214 100644 --- a/src/main/g8/backend/README.md +++ b/src/main/g8/backend/README.md @@ -77,5 +77,5 @@ Deletes a todo task _Response example:_ a message confirming the deletion ``` -"Task 1 Deleted" +Task 1 has been deleted ``` diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index 8c2bee7..b6c2fe3 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -78,7 +78,7 @@ object TodoController { if (id.forall(_.isDigit)) { TodoService .deleteTodoById(id.toInt) - .map(_ => Response.text(s"Task $id Deleted")) + .map(_ => Response.text(s"Task $id has been deleted")) .orElse( ZIO.succeed( Response.fromHttpError( diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index 628ee72..fd6df28 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -53,6 +53,11 @@ object TodoService { def deleteTodoById(id: Int): Task[Unit] = ZIO .fromFuture(_ => todosCollection.deleteOne(equal("id", id)).toFuture()) - .unit - + .flatMap { result => + if (result.wasAcknowledged() && result.getDeletedCount == 0) { + ZIO.fail(new Exception("Todo not found")) + } else { + ZIO.unit + } + } } From 56b26a3d3df750c211aaecc4c268c4f7bfed6f70 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Fri, 16 Jun 2023 13:53:27 +0200 Subject: [PATCH 12/16] feat: add a route to update the title of a todo & update route to modify the completed field Signed-off-by: esteban baron --- .../src/main/scala/TodoController.scala | 19 ++++++++++++++-- .../backend/src/main/scala/TodoService.scala | 22 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index b6c2fe3..db14604 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -57,10 +57,25 @@ object TodoController { todo => Response.text(todo.toJson) ) } - case Method.PUT -> BasePath / id => { + case req @ Method.PUT -> BasePath / id => { + (for { + queryParams <- ZIO + .fromOption(Option(req.url.queryParams)) + .orElseFail(HttpError.BadRequest("Missing query parameters")) + title <- ZIO + .fromOption(queryParams.get("title").collect(_.head)) + .orElseFail(HttpError.BadRequest("Missing 'title' parameter")) + updatedTodo <- TodoService.updateTodoTitleField(id.toInt, title) + } yield updatedTodo) + .fold( + error => Response.fromHttpError(HttpError.InternalServerError()), + todo => Response.text(todo.toJson) + ) + } + case Method.PUT -> BasePath / id / "completed" => { if (id.forall(_.isDigit)) { TodoService - .updateTodo(id.toInt) + .updateTodoCompletedField(id.toInt) .map(_.toJson) .map(Response.text(_)) .orElse( diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index fd6df28..903b21d 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -27,7 +27,27 @@ object TodoService { .unit } yield newTodo - def updateTodo(todoId: Int): Task[Todo] = + def updateTodoTitleField(todoId: Int, title: String): Task[Todo] = + for { + taskToChange <- getTodoById(todoId) + updatedTodo <- taskToChange match { + case Some(todo) => + val updated = todo.copy(title = title) + ZIO + .fromFuture(_ => + todosCollection + .updateOne(equal("id", todoId), set("title", updated.title)) + .toFuture() + ) + .map(_ => updated) + case None => + ZIO.fail( + new NoSuchElementException(s"Todo with ID $todoId not found") + ) + } + } yield updatedTodo + + def updateTodoCompletedField(todoId: Int): Task[Todo] = for { taskToChange <- getTodoById(todoId) updatedTodo <- taskToChange match { From b7c1ee126bb7bb8dffcaed3d99cdf45182081f6f Mon Sep 17 00:00:00 2001 From: esteban baron Date: Fri, 16 Jun 2023 13:57:28 +0200 Subject: [PATCH 13/16] feat: add new routes in README.md Signed-off-by: esteban baron --- src/main/g8/backend/README.md | 20 ++++++++++++++++--- .../src/main/scala/TodoController.scala | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/main/g8/backend/README.md b/src/main/g8/backend/README.md index 2ddf214..28c5b5e 100644 --- a/src/main/g8/backend/README.md +++ b/src/main/g8/backend/README.md @@ -56,7 +56,21 @@ _Response example:_ } ``` -### PUT /todos/:id +### PUT /todos/:id?title=string + +Updates a todo task by changing the title field + +_Response example:_ + +```json +{ + "id": 1, + "title": "Todo 1", + "completed": false +} +``` + +### POST /todos/:id/completed Updates a todo task by changing the completed status @@ -74,8 +88,8 @@ _Response example:_ Deletes a todo task -_Response example:_ a message confirming the deletion +_Response example:_ -``` +```text Task 1 has been deleted ``` diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index db14604..bfcf145 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -72,7 +72,7 @@ object TodoController { todo => Response.text(todo.toJson) ) } - case Method.PUT -> BasePath / id / "completed" => { + case Method.POST -> BasePath / id / "completed" => { if (id.forall(_.isDigit)) { TodoService .updateTodoCompletedField(id.toInt) From fc20f96c326bce5249b6377691a6404f3df9e4b9 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Fri, 16 Jun 2023 14:16:52 +0200 Subject: [PATCH 14/16] feat: add route to delete all completed tasks Signed-off-by: esteban baron --- .../g8/backend/src/main/scala/TodoController.scala | 12 ++++++++++++ .../g8/backend/src/main/scala/TodoService.scala | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/g8/backend/src/main/scala/TodoController.scala b/src/main/g8/backend/src/main/scala/TodoController.scala index bfcf145..57cf796 100644 --- a/src/main/g8/backend/src/main/scala/TodoController.scala +++ b/src/main/g8/backend/src/main/scala/TodoController.scala @@ -89,6 +89,18 @@ object TodoController { ZIO.succeed(Response.fromHttpError(HttpError.BadRequest())) } } + case Method.DELETE -> BasePath / "completed" => { + TodoService + .deleteCompletedTodo() + .map(_ => Response.text("All completed tasks have been deleted")) + .orElse( + ZIO.succeed( + Response.fromHttpError( + HttpError.InternalServerError("Error deleting completed tasks") + ) + ) + ) + } case Method.DELETE -> BasePath / id => { if (id.forall(_.isDigit)) { TodoService diff --git a/src/main/g8/backend/src/main/scala/TodoService.scala b/src/main/g8/backend/src/main/scala/TodoService.scala index 903b21d..8f5fbbe 100644 --- a/src/main/g8/backend/src/main/scala/TodoService.scala +++ b/src/main/g8/backend/src/main/scala/TodoService.scala @@ -80,4 +80,17 @@ object TodoService { ZIO.unit } } + + def deleteCompletedTodo(): Task[Unit] = + ZIO + .fromFuture(_ => + todosCollection.deleteMany(equal("completed", true)).toFuture() + ) + .flatMap { result => + if (result.wasAcknowledged() && result.getDeletedCount == 0) { + ZIO.fail(new Exception("No todo deleted")) + } else { + ZIO.unit + } + } } From 592513b137be180df2ce60b08e89060bc38b0ca8 Mon Sep 17 00:00:00 2001 From: esteban baron Date: Fri, 16 Jun 2023 14:17:11 +0200 Subject: [PATCH 15/16] feat: remove useless config files Signed-off-by: esteban baron --- src/main/g8/backend/src/main/scala/MongoDBClient.scala | 8 -------- src/main/g8/backend/src/main/scala/MongoDBConfig.scala | 3 --- 2 files changed, 11 deletions(-) delete mode 100644 src/main/g8/backend/src/main/scala/MongoDBClient.scala delete mode 100644 src/main/g8/backend/src/main/scala/MongoDBConfig.scala diff --git a/src/main/g8/backend/src/main/scala/MongoDBClient.scala b/src/main/g8/backend/src/main/scala/MongoDBClient.scala deleted file mode 100644 index af562aa..0000000 --- a/src/main/g8/backend/src/main/scala/MongoDBClient.scala +++ /dev/null @@ -1,8 +0,0 @@ -package todo - -import org.mongodb.scala._ - -object MongoDBClient { - def createClient(config: MongoDBConfig): MongoClient = - MongoClient(config.uri) -} diff --git a/src/main/g8/backend/src/main/scala/MongoDBConfig.scala b/src/main/g8/backend/src/main/scala/MongoDBConfig.scala deleted file mode 100644 index 589f244..0000000 --- a/src/main/g8/backend/src/main/scala/MongoDBConfig.scala +++ /dev/null @@ -1,3 +0,0 @@ -package todo - -case class MongoDBConfig(uri: String, database: String) From 10223ec2f8faa7ac098b8d34d27cd0b27d54e35e Mon Sep 17 00:00:00 2001 From: esteban baron Date: Fri, 16 Jun 2023 14:18:23 +0200 Subject: [PATCH 16/16] feat: update README.md with new route documentation Signed-off-by: esteban baron --- src/main/g8/backend/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/g8/backend/README.md b/src/main/g8/backend/README.md index 28c5b5e..ecd0bf0 100644 --- a/src/main/g8/backend/README.md +++ b/src/main/g8/backend/README.md @@ -93,3 +93,13 @@ _Response example:_ ```text Task 1 has been deleted ``` + +### DELETE /todos/completed + +Deletes all completed todo tasks + +_Response example:_ + +```text +All completed tasks have been deleted +```