Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/implement backend with mongodb connection #3

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/g8/backend/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = "3.7.3"
runner.dialect = scala213
105 changes: 105 additions & 0 deletions src/main/g8/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# 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?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

_Response example:_

```json
{
"id": 1,
"title": "Todo 1",
"completed": true
}
```

### DELETE /todos/:id

Deletes a todo task

_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
```
19 changes: 14 additions & 5 deletions src/main/g8/backend/build.sbt
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// give the user a nice default project!
ThisBuild / organization := "com.do"
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / scalaVersion := "2.13.10"

lazy val root = (project in file(".")).
settings(
name := "Fullstack Scaffhold",
mainClass := Some("com.do.Main")
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",
"org.mongodb.scala" %% "mongo-scala-driver" % "4.9.1",
"org.mongodb.scala" %% "mongo-scala-bson" % "4.9.1"
)
)
12 changes: 12 additions & 0 deletions src/main/g8/backend/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"id": 1,
"title": "Faire du sale",
"completed": false
},
{
"id": 2,
"title": "Manger du poulet",
"completed": false
}
]
17 changes: 17 additions & 0 deletions src/main/g8/backend/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -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:
6 changes: 6 additions & 0 deletions src/main/g8/backend/project/metals.sbt
Original file line number Diff line number Diff line change
@@ -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")

6 changes: 6 additions & 0 deletions src/main/g8/backend/project/project/metals.sbt
Original file line number Diff line number Diff line change
@@ -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")

30 changes: 30 additions & 0 deletions src/main/g8/backend/src/main/scala/DB.scala
Original file line number Diff line number Diff line change
@@ -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 {
private val databaseURL: String =
Option(System.getenv("MONGO_URL"))
.getOrElse("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(databaseURL)
.getDatabase("todoapp")
.withCodecRegistry(codecRegistry)

val todosCollection: MongoCollection[Todo] =
database.getCollection[Todo]("todos")
}
25 changes: 20 additions & 5 deletions src/main/g8/backend/src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
object Main {
def main(args: Array[String]): Unit = {
println("Hello, world")
}
}
package todo

import zio._
import zio.http._

object TodoApp extends ZIOAppDefault {
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://localhost:%d\n", port)
override val run =
Server.serve(TodoController.routes).provide(configLayer, Server.live)
}
14 changes: 14 additions & 0 deletions src/main/g8/backend/src/main/scala/Todo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package 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]
}
121 changes: 121 additions & 0 deletions src/main/g8/backend/src/main/scala/TodoController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package todo

import zio._
import zio.http._
import zio.json._

object TodoController {

val BasePath = !! / "todos"

val routes: Http[Any, Nothing, Request, Response] =
Http.collectZIO[Request] {
case Method.GET -> BasePath => {
TodoService
.getTodos()
.map(_.toJson)
.map(Response.text(_))
.orElse(
ZIO.succeed(
Response.fromHttpError(
HttpError.NotFound("No todos found")
)
)
)
}
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 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 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.POST -> BasePath / id / "completed" => {
if (id.forall(_.isDigit)) {
TodoService
.updateTodoCompletedField(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()))
}
}
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
.deleteTodoById(id.toInt)
.map(_ => Response.text(s"Task $id has been deleted"))
.orElse(
ZIO.succeed(
Response.fromHttpError(
HttpError.NotFound(s"Todo with ID $id not found")
)
)
)
} else {
ZIO.succeed(Response.fromHttpError(HttpError.BadRequest()))
}
}
}
}
Loading