Skip to content

Commit

Permalink
feat: add simple books API with fasthttp & postgres (#51)
Browse files Browse the repository at this point in the history
* adds simple books api

Signed-off-by: DarkhanShakhan <[email protected]>

* adds keploy testing

Signed-off-by: DarkhanShakhan <[email protected]>

* Update app.go for v2

* updated for v2

Signed-off-by: Animesh pathak <[email protected]>

---------

Signed-off-by: DarkhanShakhan <[email protected]>
Signed-off-by: Animesh pathak <[email protected]>
Co-authored-by: Animesh Pathak <[email protected]>
Co-authored-by: Animesh pathak <[email protected]>
  • Loading branch information
3 people authored Feb 7, 2024
1 parent 73502cf commit ac2142e
Show file tree
Hide file tree
Showing 16 changed files with 1,026 additions and 0 deletions.
13 changes: 13 additions & 0 deletions fasthttp-postgres/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
version: '3.7'
services:
postgres:
image: postgres:10.5
restart: always
environment:
- POSTGRES_DB=books
- POSTGRES_USER=books_user
- POSTGRES_PASSWORD=books_password
ports:
- '5433:5432'
volumes:
- ./migrations:/docker-entrypoint-initdb.d
16 changes: 16 additions & 0 deletions fasthttp-postgres/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module fasthttp-postgres

go 1.20

require (
github.com/fasthttp/router v1.4.18
github.com/lib/pq v1.10.0
github.com/valyala/fasthttp v1.45.0
)

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/klauspost/compress v1.16.3 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
)
14 changes: 14 additions & 0 deletions fasthttp-postgres/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/fasthttp/router v1.4.18 h1:elMnlFq527oZd8MHsuUpO6uLDup1exv8rXPfIjClDHk=
github.com/fasthttp/router v1.4.18/go.mod h1:ZmC20Mn0VgCBbUWFDmnYzFbQYRfdGeKgpkBy0+JioKA=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
40 changes: 40 additions & 0 deletions fasthttp-postgres/internal/app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app

import (
"database/sql"
"fasthttp-postgres/internal/handlers"
"fasthttp-postgres/internal/repository"

"log"

"github.com/fasthttp/router"
_ "github.com/lib/pq"
"github.com/valyala/fasthttp"
)

func InitApp() {

uri := "postgresql://books_user:books_password@localhost:5433/books?sslmode=disable"
db, err := sql.Open("postgres", uri)
if err != nil {
log.Fatal("Error connecting to database")
}
repo := repository.NewRepository(db)
ctrl := handlers.NewHandler(repo)
router := router.New()

router.GET("/authors", ctrl.GetAllAuthors)
router.GET("/books", ctrl.GetAllBooks)
router.GET("/books/{id}", ctrl.GetBookById)
router.GET("/authors/{id}", ctrl.GetBooksByAuthorId)
router.POST("/books", ctrl.CreateBook)
router.POST("/authors", ctrl.CreateAuthor)
server := &fasthttp.Server{
Handler: router.Handler,
Name: "Server",
}
log.Println("Starting server: http://localhost:8080")
if err := server.ListenAndServe(":8080"); err != nil {
log.Fatalf("Error starting server: %s\n", err)
}
}
14 changes: 14 additions & 0 deletions fasthttp-postgres/internal/entity/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package entity

type Author struct {
ID uint `json:"id" db:"id"`
FirstName string `json:"first_name" db:"first_name"`
LastName string `json:"last_name" db:"last_name"`
}

type Book struct {
ID uint `json:"id"`
Title string `json:"title"`
Year int `json:"year"`
Author Author `json:"author"`
}
140 changes: 140 additions & 0 deletions fasthttp-postgres/internal/handlers/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package handlers

import (
"context"
"encoding/json"
"fasthttp-postgres/internal/entity"
"net/http"
"strconv"

"github.com/valyala/fasthttp"
)

type Handler struct {
repository Repository
}

type Repository interface {
GetAllAuthors(context.Context) ([]entity.Author, error)
GetAllBooks(context.Context) ([]entity.Book, error)
GetBookById(context.Context, int) ([]entity.Book, error)
GetBooksByAuthorId(context.Context, int) ([]entity.Book, error)
CreateBook(context.Context, entity.Book) error
CreateAuthor(context.Context, entity.Author) error
}

func NewHandler(repository Repository) *Handler {
return &Handler{
repository: repository,
}
}

func (h *Handler) GetAllAuthors(ctx *fasthttp.RequestCtx) {
authors, err := h.repository.GetAllAuthors(ctx)
if err != nil {
sendError(ctx, err, 500)
return
}
sendData(ctx, authors)
}

func (h *Handler) GetAllBooks(ctx *fasthttp.RequestCtx) {
books, err := h.repository.GetAllBooks(ctx)
if err != nil {
sendError(ctx, err, 500)
return
}
sendData(ctx, books)
}

func (h *Handler) GetBookById(ctx *fasthttp.RequestCtx) {
bookId := ctx.UserValue("id").(string)
id, err := strconv.Atoi(bookId)
if err != nil {
sendError(ctx, nil, http.StatusNotFound)
return
}
books, err := h.repository.GetBookById(ctx, id)
if err != nil {
sendError(ctx, nil, http.StatusNotFound)
return
}
sendData(ctx, books[0])
}

func (h *Handler) GetBooksByAuthorId(ctx *fasthttp.RequestCtx) {
authorId := ctx.UserValue("id").(string)
id, err := strconv.Atoi(authorId)
if err != nil {
sendError(ctx, nil, http.StatusNotFound)
return
}
books, err := h.repository.GetBooksByAuthorId(ctx, id)
if err != nil {
sendError(ctx, nil, http.StatusNotFound)
return
}
sendData(ctx, books)
}

func (h *Handler) CreateBook(ctx *fasthttp.RequestCtx) {
var req createBookRequest
err := json.Unmarshal(ctx.Request.Body(), &req)
if err != nil {
sendError(ctx, err, http.StatusBadRequest)
return
}
if err := h.repository.CreateBook(ctx, req.convert()); err != nil {
sendError(ctx, err, http.StatusInternalServerError)
return
}
ctx.SetStatusCode(http.StatusCreated)
}

func (h *Handler) CreateAuthor(ctx *fasthttp.RequestCtx) {
var author entity.Author
err := json.Unmarshal(ctx.Request.Body(), &author)
if err != nil {
sendError(ctx, err, http.StatusBadRequest)
return
}
if err := h.repository.CreateAuthor(ctx, author); err != nil {
sendError(ctx, err, http.StatusInternalServerError)
return
}
ctx.SetStatusCode(http.StatusCreated)
}

type createBookRequest struct {
Title string `json:"title"`
Year int `json:"year"`
AuthorID uint `json:"author_id"`
}

func (r createBookRequest) convert() entity.Book {
return entity.Book{
Title: r.Title,
Year: r.Year,
Author: entity.Author{
ID: r.AuthorID,
},
}
}

func sendData(ctx *fasthttp.RequestCtx, data interface{}) {
ctx.SetContentType("application/json")
v, err := json.Marshal(data)
if err != nil {
sendError(ctx, err, 500)
return
}
ctx.Response.SetBody(v)
}

func sendError(ctx *fasthttp.RequestCtx, err error, code int) {
var msg string
if err != nil {
msg = err.Error()
}
ctx.Error(msg, code)
}
43 changes: 43 additions & 0 deletions fasthttp-postgres/internal/repository/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package repository

import "fasthttp-postgres/internal/entity"

type model struct {
ID uint `db:"id"`
Title string `db:"title"`
Year int `db:"year"`
AuthorID uint `db:"author_id"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
}

func convert(b entity.Book) model {
return model{
Title: b.Title,
Year: b.Year,
AuthorID: b.Author.ID,
}
}

func (m model) convert() entity.Book {
return entity.Book{
ID: m.ID,
Title: m.Title,
Year: m.Year,
Author: entity.Author{
ID: m.AuthorID,
FirstName: m.FirstName,
LastName: m.LastName,
},
}
}

type models []model

func (mm models) convert() []entity.Book {
out := make([]entity.Book, 0, len(mm))
for _, m := range mm {
out = append(out, m.convert())
}
return out
}
95 changes: 95 additions & 0 deletions fasthttp-postgres/internal/repository/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package repository

import (
"context"
"database/sql"
"fasthttp-postgres/internal/entity"
"fmt"
)

type Repository struct {
db *sql.DB
}

func NewRepository(db *sql.DB) *Repository {
return &Repository{
db: db,
}
}

func (r *Repository) GetAllAuthors(ctx context.Context) ([]entity.Author, error) {
var mm []entity.Author
rows, err := r.db.QueryContext(ctx, getAllAuthors)
if err != nil {
return nil, err
}
var m entity.Author
for rows.Next() {
if err = rows.Scan(&m.ID, &m.FirstName, &m.LastName); err != nil {
return nil, err
}
mm = append(mm, m)
}
return mm, nil
}

func (r *Repository) GetAllBooks(ctx context.Context) ([]entity.Book, error) {
var mm models
rows, err := r.db.QueryContext(ctx, getAllBooks)
if err != nil {
return nil, err
}
var m model
for rows.Next() {
if err = rows.Scan(&m.ID, &m.Title, &m.Year, &m.AuthorID, &m.FirstName, &m.LastName); err != nil {
return nil, err
}
mm = append(mm, m)
}
return mm.convert(), nil
}

func (r *Repository) GetBookById(ctx context.Context, id int) ([]entity.Book, error) {
var mm models
rows, err := r.db.QueryContext(ctx, getBookById, id)
if err != nil {
fmt.Println("1")
return nil, err
}
var m model
for rows.Next() {
if err = rows.Scan(&m.ID, &m.Title, &m.Year, &m.AuthorID, &m.FirstName, &m.LastName); err != nil {
fmt.Println("3")
return nil, err
}
mm = append(mm, m)
}
return mm.convert(), nil
}

func (r *Repository) GetBooksByAuthorId(ctx context.Context, id int) ([]entity.Book, error) {
var mm models
rows, err := r.db.QueryContext(ctx, getBooksByAuthorId, id)
if err != nil {
return nil, err
}
var m model
for rows.Next() {
if err = rows.Scan(&m.ID, &m.Title, &m.Year, &m.AuthorID, &m.FirstName, &m.LastName); err != nil {
return nil, err
}
mm = append(mm, m)
}
return mm.convert(), nil
}

func (r *Repository) CreateBook(ctx context.Context, b entity.Book) error {
m := convert(b)
_, err := r.db.ExecContext(ctx, createBookStmt, m.Title, m.Year, m.AuthorID)
return err
}

func (r *Repository) CreateAuthor(ctx context.Context, a entity.Author) error {
_, err := r.db.ExecContext(ctx, createAuthorStmt, a.FirstName, a.LastName)
return err
}
Loading

0 comments on commit ac2142e

Please sign in to comment.