Skip to content

Book management application using Hotwire, SQLite and Litestack

Notifications You must be signed in to change notification settings

diegolinhares/freebooks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

README

Welcome to Freebooks!

Spoiler: Some frontend resources were extracted from the Yataska project.

You can check the application running in this video.

Tooling

Versions

  • Ruby: 3.3.1
  • Rails: 7.2.0.beta2

Frontend

  • Hotwire: Includes gems turbo-rails and stimulus-rails, used to add Turbo and Stimulus functionality, respectively.
  • Pico: CSS framework used for web design.

Database and Storage

  • sqlite: Connector and adapter for SQLite database.
  • litestack: Used for improving SQLite, for example, full text search.

Application Features

  • Pagy: Lightweight and efficient pagination gem.
  • action-policy: Create policies for librarians.

Testing

  • Fixtures: Instead of FactoryBot, I've used Rails default fixtures. They were used to create the seeds too.
  • Rspec-rails: Testing framework for Rails.
  • Shoulda-matchers: Provides simplifications for testing Rails functionality.

Features Implemented

  • Authentication and Authorization

  • Book Management

  • Borrowing and Returning

  • Dashboard

  • API Endpoints

  • Frontend (Bonus): Implemented using Hotwire

Testing

Below is a summary of the testing efforts:

  • Models

    • Comprehensive testing of all models to validate core functionality.
  • Integration Tests (API)

    • Integration testing of API functionality to ensure proper communication.

Getting Started

To get started, ensure that you have the following prerequisites installed on your system:

  • Ruby (version 3.3.1)
  • Rails (version 7.2.0.beta2)

Setup

  1. Clone the repository to your local machine.
  git clone https://github.com/diegolinhares/freebooks
  1. Navigate to the project directory.
  cd freebooks
  1. Run the following command to set up the project, which will install dependencies, create the database, and perform necessary setup tasks.
bin/setup

Running the project

bin/dev

This will start the development server, and you can access the application at http://localhost:3000.

Seed Data

The following user credentials are available for testing:

Librarian

  • Email: [email protected]
  • Password: 12341234
  • Role: librarian
  • API Access Token: WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9

Member

  • Email: [email protected]
  • Password: 12341234
  • Role: member
  • API Access Token: fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae

Running tests

bundle exec rspec spec

This command will execute all the tests.

Solution

Overview

This project is divided into two contexts: Web and API. For both contexts, we have a "BackOffice" for members and another for librarians.

The controllers in each context handle authentication and authorization by role.

Implementation

Some actions were basic CRUD operations. I didn't see the need to create services, use cases, or orchestrators. I used the controllers themselves as orchestrators, leveraging the expressiveness of Ruby/Rails.

Book Borrowing

When a user attempts to borrow a book, I considered that, as a system with a lot of writes, it would be beneficial to use a pessimistic lock. This locks a book while a user is attempting to borrow it, helping to maintain the correct number of available books in a distributed environment. I created a specific attribute for this and compare it with the total number of books that exist.

An interesting challenge was ensuring that members can borrow a book only if it's available and cannot borrow the same book multiple times simultaneously. To solve this, I created a unique index in the database using a constraint and also added a validation at the application level:

t.index ["user_id", "book_id"], name: "unique_borrowing_index", unique: true, where: "returned_at IS NULL"

Book Search

Another challenge of this project was the search functionality for books by title, author, or genre. To address this, I used the full-text search feature with trigrams from SQLite, using the Litesearch functionality from the Litestack gem. For pagination, I combined the results with the Pagy gem.

Fixtures and Seeds

Typically, when using RSpec, you use Factory Bot to create data. However, I preferred to use fixtures and implemented a seed strategy to have the same data in both development and test environments. You can see how this strategy works in the seeds.rb file.

Database

erDiagram
    USERS {
        string email PK "unique"
        string password_digest
        string role
        string api_access_token PK "unique"
        datetime created_at
        datetime updated_at
    }

    AUTHORS {
        string name PK "unique"
        datetime created_at
        datetime updated_at
    }

    GENRES {
        string name PK "unique"
        datetime created_at
        datetime updated_at
    }

    BOOKS {
        string title
        string isbn PK "unique"
        int total_copies
        int available_copies
        int genre_id FK
        int author_id FK
        datetime created_at
        datetime updated_at
    }

    BORROWINGS {
        int user_id FK
        int book_id FK
        datetime borrowed_at
        datetime due_date
        datetime returned_at
        datetime created_at
        datetime updated_at
    }

    USERS ||--o{ BORROWINGS : "has many"
    BOOKS ||--o{ BORROWINGS : "has many"
    AUTHORS ||--o{ BOOKS : "writes"
    GENRES ||--o{ BOOKS : "categorizes"
Loading

Indexes

  • Authors Table:

    • Unique index on name
  • Books Table:

    • Index on genre_id
    • Index on author_id
    • Unique index on isbn
  • Borrowings Table:

    • Index on user_id
    • Index on book_id
    • Unique index on user_id and book_id where returned_at is NULL
  • Genres Table:

    • Unique index on name
  • Users Table:

    • Unique index on email
    • Unique index on api_access_token

API Endpoints

Members

Sessions

POST /api/v1/members/sessions

When Authenticated

curl -X POST http://localhost:3000/api/v1/members/sessions \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "12341234"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Action not allowed for authenticated member",
  "details": {}
}

When Unauthenticated (valid credentials)

curl -X POST http://localhost:3000/api/v1/members/sessions \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "12341234"
  }
}'

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "access_token": "newly_generated_access_token"
  }
}

When Unauthenticated (invalid credentials)

curl -X POST http://localhost:3000/api/v1/members/sessions \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "bad-email",
    "password": "bad-pass"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Invalid email or password",
  "details": {}
}

Delete Session

DELETE /api/v1/members/sessions

When Authenticated

curl -X DELETE http://localhost:3000/api/v1/members/sessions \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success"
}

When Unauthenticated

curl -X DELETE http://localhost:3000/api/v1/members/sessions \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Registration

POST /api/v1/members/registrations

When Unauthenticated

To register a new member with valid parameters:

curl -X POST http://localhost:300/api/v1/members/registrations \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "password123"
  }
}'

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "message": "Member registered successfully",
    "access_token": "newly_generated_access_token"
  }
}

To return an error when parameters are invalid:

curl -X POST http://localhost:300/api/v1/members/registrations \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "invalid_email",
    "password": ""
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Failed to register member",
  "details": [
    "Email is invalid",
    "Password can't be blank"
  ]
}

When Authenticated

To prevent authenticated members from registering again:

curl -X POST http://localhost:300/api/v1/members/registrations \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "password123"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Action not allowed for authenticated member",
  "details": {}
}

Borrowings

GET /api/v1/members/borrowings

When Authenticated

To return paginated borrowings for the current member:

curl -X GET http://localhost:3000/api/v1/members/borrowings \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "borrowings": [
      {
        "book_title": "Expired Book",
        "status": "overdue"
      },
      {
        "book_title": "A Game of Thrones",
        "status": "not overdue"
      },
      {
        "book_title": "Dune",
        "status": "not overdue"
      }
    ]
  },
  "pagination": {
    "count": 3,
    "items": 5,
    "next": null,
    "page": 1,
    "pages": 1,
    "prev": null
  }
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/members/borrowings \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Create Borrowing

POST /api/v1/members/book_borrowings

When Authenticated

To create a borrowing successfully:

curl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "data": {
    "message": "Book successfully borrowed."
  },
  "type": "object"
}

To fail to create a borrowing when no copies are available:

curl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "No available copies to borrow.",
  "details": {}
}

To return an error when the user tries to borrow a book they have already borrowed and not returned:

curl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Failed to borrow book",
  "details": ["User has already borrowed this book and not returned it yet"]
}

When Unauthenticated

To return unauthorized status:

curl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Get Books

GET /api/v1/members/books

When Authenticated

To return paginated books for the current member:

curl -X GET http://localhost:3000/api/v1/members/books \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "books": [
      {
        "title": "A Feast for Crows",
        "author_name": "George R. R. Martin",
        "genre_name": "Fantasy"
      },
      {
        "title": "Dune",
        "author_name": "Frank Herbert",
        "genre_name": "Science Fiction"
      },
      {
        "title": "A Dance with Dragons",
        "author_name": "George R. R. Martin",
        "genre_name": "Fantasy"
      },
      {
        "title": "The Book Thief",
        "author_name": "Markus Zusak",
        "genre_name": "Historical Fiction"
      },
      {
        "title": "Gone Girl",
        "author_name": "Gillian Flynn",
        "genre_name": "Thriller"
      }
    ]
  },
  "pagination": {
    "count": 16,
    "items": 5,
    "next": 2,
    "page": 1,
    "pages": 4,
    "prev": null
  }
}

To return paginated books for the search query "Dune":

curl -X GET http://localhost:3000/api/v1/members/books?query=Dune \
-H "Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "books": [
      {
        "title": "Dune",
        "author_name": "Frank Herbert",
        "genre_name": "Science Fiction"
      }
    ]
  },
  "pagination": {
    "count": 1,
    "items": 5,
    "next": null,
    "page": 1,
    "pages": 1,
    "prev": null
  }
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/members/books \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Librarians

Create Session

POST /api/v1/librarians/sessions

When Authenticated

To avoid re-authenticating the user:

curl -X POST http://localhost:3000/api/v1/librarians/sessions \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "12341234"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Action not allowed for authenticated librarian",
  "details": {}
}

When Unauthenticated

To authenticate the user when parameters are valid:

curl -X POST http://localhost:3000/api/v1/librarians/sessions \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "12341234"
  }
}'

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "access_token": "newly_generated_access_token"
  }
}

To avoid authenticating the user when parameters are invalid:

curl -X POST http://localhost:3000/api/v1/librarians/sessions \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "bad-email",
    "password": "bad-pass"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Invalid email or password",
  "details": {}
}

Delete Session

DELETE /api/v1/librarians/sessions

When Authenticated

To sign out the current librarian and regenerate the API access token:

curl -X DELETE http://localhost:3000/api/v1/librarians/sessions \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success"
}

When Unauthenticated

To return unauthorized status:

curl -X DELETE http://localhost:3000/api/v1/librarians/sessions \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Create Registration

POST /api/v1/librarians/registrations

When Unauthenticated

To register a new librarian and return access token:

curl -X POST http://localhost:3000/api/v1/librarians/registrations \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "password123"
  }
}'

Expected Response:

{
  "status": "success",
  "data": {
    "message": "Librarian registered successfully",
    "access_token": "newly_generated_access_token"
  },
  "type": "object"
}

To return errors when registration fails:

curl -X POST http://localhost:3000/api/v1/librarians/registrations \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "",
    "password": "password123"
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Failed to register librarian",
  "details": ["Email can't be blank"]
}

When Authenticated

To disallow authenticated librarian from registering:

curl -X POST http://localhost:3000/api/v1/librarians/registrations \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "user": {
    "email": "[email protected]",
    "password": "password123"
  }
}'

Expected Response:

{
  "status": "error",
  "details": {},
  "message": "Action not allowed for authenticated librarian"
}

Get Statistics

GET /api/v1/librarians/statistics

When Authenticated

To return dashboard statistics for the librarian:

curl -X GET http://localhost:3000/api/v1/librarians/statistics \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "data": {
    "books": 17,
    "total_borrowed_books": 13,
    "books_due_today": 0
  },
  "type": "object"
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/librarians/statistics \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Get Members

GET /api/v1/librarians/members

When Authenticated

To return paginated members with overdue books:

curl -X GET http://localhost:3000/api/v1/librarians/members \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "data": {
    "members": [
      {
        "email": "[email protected]"
      },
      {
        "email": "[email protected]"
      },
      ...
    ]
  },
  "pagination": {
    "count": 10,
    "items": 5,
    "next": 2,
    "page": 1,
    "pages": 2,
    "prev": null
  },
  "type": "object"
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/librarians/members \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Get Books

GET /api/v1/librarians/books

When Authenticated

To return paginated books for the librarian:

curl -X GET http://localhost:3000/api/v1/librarians/books \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "books": [
      {
        "title": "A Feast for Crows",
        "author_name": "George R. R. Martin",
        "genre_name": "Fantasy"
      },
      {
        "title": "Sapiens: A Brief History of Humankind",
        "author_name": "Yuval Noah Harari",
        "genre_name": "Non-fiction"
      },
      {
        "title": "Dune",
        "author_name": "Frank Herbert",
        "genre_name": "Science Fiction"
      },
      {
        "title": "A Dance with Dragons",
        "author_name": "George R. R. Martin",
        "genre_name": "Fantasy"
      },
      {
        "title": "The Book Thief",
        "author_name": "Markus Zusak",
        "genre_name": "Historical Fiction"
      }
    ]
  },
  "pagination": {
    "count": 17,
    "items": 5,
    "next": 2,
    "page": 1,
    "pages": 4,
    "prev": null
  }
}

To return paginated books for the search query "Dune":

curl -X GET http://localhost:3000/api/v1/librarians/books?query=Dune \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "type": "object",
  "data": {
    "books": [
      {
        "title": "Dune",
        "author_name": "Frank Herbert",
        "genre_name": "Science Fiction"
      }
    ]
  },
  "pagination": {
    "count": 1,
    "items": 5,
    "next": null,
    "page": 1,
    "pages": 1,
    "prev": null
  }
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/librarians/books \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Create Book

POST /api/v1/librarians/books

When Authenticated

To create a book successfully:

curl -X POST http://localhost:3000/api/v1/librarians/books \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "New Book",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "success",
  "data": {
    "message": "Book created",
    "book": {
      "title": "New Book"
    }
  },
  "type": "object"
}

To fail to create a book due to validation errors:

curl -X POST http://localhost:3000/api/v1/librarians/books \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Failed to create book",
  "details": ["Title can't be blank"]
}

When Unauthenticated

To return unauthorized status:

curl -X POST http://localhost:3000/api/v1/librarians/books \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "New Book",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Update Book

PUT /api/v1/librarians/books/:id

When Authenticated

To update a book successfully:

curl -X PUT http://localhost:3000/api/v1/librarians/books/1 \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "Updated Book Title",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "success",
  "data": {
    "message": "Book updated",
    "book": {
      "title": "Updated Book Title"
    }
  },
  "type": "object"
}

To fail to update a book due to validation errors:

curl -X PUT http://localhost:3000/api/v1/librarians/books/1 \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Failed to update book",
  "details": ["Title can't be blank"]
}

When Unauthenticated

To return unauthorized status:

curl -X PUT http://localhost:3000/api/v1/librarians/books/1 \
-H "Content-Type: application/json" \
-d '{
  "book": {
    "title": "Updated Book Title",
    "author_id": 1,
    "genre_id": 1,
    "isbn": "978-1234567890",
    "total_copies": 10,
    "available_copies": 10
  }
}'

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Delete Book

DELETE /api/v1/librarians/books/:id

When Authenticated

To delete a book successfully:

curl -X DELETE http://localhost:3000/api/v1/librarians/books/1 \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success"
}

When Unauthenticated

To return unauthorized status:

curl -X DELETE http://localhost:3000/api/v1/librarians/books/1 \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Get Member Borrowings

GET /api/v1/librarians/members/:member_id/borrowings

When Authenticated

To return paginated borrowings for the member:

curl -X GET http://localhost:3000/api/v1/librarians/members/1/borrowings \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "data": {
    "borrowings": [
      {
        "book_title": "Expired Book"
      },
      {
        "book_title": "A Game of Thrones"
      },
      {
        "book_title": "Dune"
      }
    ]
  }
}

When Unauthenticated

To return unauthorized status:

curl -X GET http://localhost:3000/api/v1/librarians/members/1/borrowings \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

Mark Book as Returned

PATCH /api/v1/librarians/borrowings/:borrowing_id/return

When Authenticated

To mark the book as returned:

curl -X PATCH http://localhost:3000/api/v1/librarians/borrowings/1/return \
-H "Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9" \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "success",
  "data": {
    "message": "Book marked as returned."
  },
  "type": "object"
}

When Unauthenticated

To return unauthorized status:

curl -X PATCH http://localhost:3000/api/v1/librarians/borrowings/1/return \
-H "Content-Type: application/json"

Expected Response:

{
  "status": "error",
  "message": "Invalid access token",
  "details": {}
}

About

Book management application using Hotwire, SQLite and Litestack

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published