Skip to content

Commit

Permalink
Add Book API
Browse files Browse the repository at this point in the history
  • Loading branch information
assayire committed Mar 21, 2024
1 parent 18bbd17 commit 17861d5
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 52 deletions.
56 changes: 17 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,35 @@
# SnorriScalason

ScalaBridge Project - mentored by Zainab Ali & Noel Welsh
> ScalaBridge Project - mentored by Zainab Ali & Noel Welsh
## Frontend

### Running
### Mar 21, 2024

To run the frontend you need to compile the code in sbt and then do some Javascript setup (which requires you have `npm` installed on your machine.)

First, the Scala side of things. From within `sbt` run

```scala
frontend / fastOptJS
```

Now the Javascript side. From a terminal, change into the `frontend` directory

```sh
cd frontend
```

Install Javascript dependencies

```sh
npm install
```

Start the web server running the frontend

```sh
npm start
```

Visit [localhost:1234](http://localhost:1234/) and you should see the frontend.


### Developing

Code is in `src/main/{css,html,scala}`
- API to add book
- Added basic scaffold for add book implementation. But the request is being dropped with a 500 ISE. Most likely because the request body is not being seen/parsed by `Snorri.addBookRoute` definition.
- TODOs
- It's time to add a service layer where you can do business logic and validations
- Add table that holds the price of the book, and other details
- API to rate a book (1/5 stars)
- Find a way to import csv/json file with books into the DB (seed data)
- FUTURE: Need help from Zainab using FS2 to stream books (for `getAllBooks`)

### Mar 7, 2024


- Got the sbt-DB tasks working
- Got the Flyway migration working
- Started working on using Skunk
- Implemented find book api using Skunk (naively)

### Feb 22, 2024

- We attempted to setup a database with a table to store book. We decided to run this all as tasks from sbt using
- Docker to create the database
- Flyway to populate the database

- Errors to resolve:
- snorri": -c: line 1: unexpected EOF while looking for matching `"'
- how do we specify flyway files location to Flyway.configure
- [x] Errors to resolve:
- [x] snorri": -c: line 1: unexpected EOF while looking for matching `"'
- [x] how do we specify flyway files location to Flyway.configure

- We spent a lot of time doing devops style work. Not very much programming.

Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion backend/src/main/scala/snorri/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import krop.all.given
object Server {
private val app: Application =
Snorri.getBookByIdRoute
.orElse(Snorri.getAllBooksRoute)
.orElse(Snorri.addBookRoute)
.orElse(Snorri.getAllBooksRoute) // FUTURE: Need help from Zainab using FS2 to stream books
.orElse(Snorri.echoRoute)
.orElse(Application.notFound)

Expand Down
13 changes: 11 additions & 2 deletions backend/src/main/scala/snorri/Snorri.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package snorri
import krop.all.*
import snorri.models.Book
import snorri.repositories.BooksDb
import snorri.models.AddBookInput

object Snorri {
val echoRoute =
Expand All @@ -17,10 +18,18 @@ object Snorri {
Response.ok(Entity.jsonOf[Iterable[Book]])
).handle(() => BooksDb.all.values)

val getBookByIdRoute = {
val getBookByIdRoute =
Route(
Request.get(Path / "books" :? Query("id", Param.string)),
Response.ok(Entity.jsonOf[Book]).orNotFound
).handle(BooksDb.find(_))
}

val addBookRoute =
Route(
Request.put(Path / "addBook").withEntity(Entity.jsonOf[AddBookInput]),
// Request.put(Path / "addBook").withEntity(Entity.text),
Response.status(Status.Created, Entity.unit)
).handle(b => {
BooksDb.addBook(b)
})
}
13 changes: 13 additions & 0 deletions backend/src/main/scala/snorri/models/Book.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ import skunk.*
import skunk.codec.all.*
import skunk.Decoder

case class AddBookInput(
isbn: String,
name: String,
author: String,
pages: Int,
publishedYear: Int
)

object AddBookInput {
implicit val circeEncoder: CirceEncoder[AddBookInput] = deriveEncoder[AddBookInput]
implicit val circeDecoder: CirceDecoder[AddBookInput] = deriveDecoder[AddBookInput]
}

case class Book(
id: Int,
isbn: String,
Expand Down
55 changes: 45 additions & 10 deletions backend/src/main/scala/snorri/repositories/BooksDb.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import cats.effect.IO
import cats.effect.unsafe.implicits.global
import io.circe.parser.decode
import natchez.Trace.Implicits.noop
import skunk.codec.all.varchar
import skunk.codec.all.{varchar, int4, text}
import skunk.implicits.sql
import skunk.{Query, Session}
import snorri.models.Book
import snorri.models.*
import snorri.utils.use

import scala.io.{BufferedSource, Source}
import cats.effect.kernel.Resource
import skunk.Command
import skunk.data.Completion.Insert

object BooksDb:
val all: Map[String, Book] = loadBooks()
Expand All @@ -23,14 +26,7 @@ object BooksDb:

// TODO: Using Session just to get it up and running.
// Will have to use something like connection pool or transaction manager
Session
.single[IO](
host = "localhost",
port = 5432,
database = "snorri",
user = "postgres",
password = Some("password")
)
dbSession()
.use { s =>
for {
q <- s.prepare(query)
Expand All @@ -48,6 +44,35 @@ object BooksDb:
}
.unsafeRunSync()

private val enc = (text *: text *: text *: int4 *: int4).values.to[AddBookInput]

private val insertBookCmd: Command[AddBookInput] =
sql"""
insert into snorri.books (isbn, name, author, pages, published_year) values $enc
""".command

def addBook(b: AddBookInput): Unit = {
dbSession()
.use { session =>
session
.prepare(insertBookCmd)
.flatMap(_.execute(b))
.attempt
.map {
case Left(ex) =>
ex.fillInStackTrace().printStackTrace()
throw ex
case Right(Insert(1)) =>
println(s"Inserted book: $b")
()
case Right(other) =>
println(s"addBook did not insert 1 record. $other")
throw new Exception(s"AddBook: $other")
}
}
.unsafeRunSync()
}

private def loadBooks(): Map[String, Book] =
decode[List[Book]](retrieveBooks()) match {
case Right(books) =>
Expand All @@ -64,3 +89,13 @@ object BooksDb:
val stream = getClass.getResourceAsStream("/book.json")
Source.fromInputStream(stream)
}

private def dbSession(): Resource[IO, Session[IO]] =
Session
.single[IO](
host = "localhost",
port = 5432,
database = "snorri",
user = "postgres",
password = Some("password")
)
37 changes: 37 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Frontend

### Running

To run the frontend you need to compile the code in sbt and then do some Javascript setup (which requires you have `npm` installed on your machine.)

First, the Scala side of things. From within `sbt` run

```scala
frontend / fastOptJS
```

Now the Javascript side. From a terminal, change into the `frontend` directory

```sh
cd frontend
```

Install Javascript dependencies

```sh
npm install
```

Start the web server running the frontend

```sh
npm start
```

Visit [localhost:1234](http://localhost:1234/) and you should see the frontend.


### Developing

Code is in `src/main/{css,html,scala}`

0 comments on commit 17861d5

Please sign in to comment.