{"id":14955273,"url":"https://github.com/diegolinhares/freebooks","last_synced_at":"2026-03-16T20:37:56.710Z","repository":{"id":245711352,"uuid":"818904474","full_name":"diegolinhares/freebooks","owner":"diegolinhares","description":"Book management application using Hotwire, SQLite and Litestack","archived":false,"fork":false,"pushed_at":"2024-07-03T03:05:54.000Z","size":196,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-29T08:41:40.535Z","etag":null,"topics":["api","hotwire","litestack","rails","sqlite"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diegolinhares.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-23T08:00:08.000Z","updated_at":"2024-12-23T20:00:57.000Z","dependencies_parsed_at":"2024-09-21T22:40:47.474Z","dependency_job_id":null,"html_url":"https://github.com/diegolinhares/freebooks","commit_stats":{"total_commits":87,"total_committers":1,"mean_commits":87.0,"dds":0.0,"last_synced_commit":"b1cbdef9dc155250edb69412cd2589c04eaa68bf"},"previous_names":["diegolinhares/freebooks"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diegolinhares%2Ffreebooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diegolinhares%2Ffreebooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diegolinhares%2Ffreebooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diegolinhares%2Ffreebooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diegolinhares","download_url":"https://codeload.github.com/diegolinhares/freebooks/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237932070,"owners_count":19389560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","hotwire","litestack","rails","sqlite"],"created_at":"2024-09-24T13:10:47.927Z","updated_at":"2025-10-24T07:31:03.208Z","avatar_url":"https://github.com/diegolinhares.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# README\n\nWelcome to Freebooks!\n\n**Spoiler:** Some frontend resources were extracted from the [Yataska](https://github.com/yataska) project.\n\nYou can check the application running in [this video](https://youtu.be/Cx2iWVKnxkM).\n\n# Tooling\n\n## Versions\n- **Ruby**: 3.3.1\n- **Rails**: 7.2.0.beta2\n\n## Frontend\n- **Hotwire**: Includes gems `turbo-rails` and `stimulus-rails`, used to add Turbo and Stimulus functionality, respectively.\n- **Pico**: CSS framework used for web design.\n\n## Database and Storage\n- **sqlite**: Connector and adapter for SQLite database.\n- **litestack**: Used for improving SQLite, for example, full text search.\n\n## Application Features\n- **Pagy**: Lightweight and efficient pagination gem.\n- **action-policy**: Create policies for librarians.\n\n## Testing\n- **Fixtures**: Instead of FactoryBot, I've used Rails default fixtures. They were used to create the seeds too.\n- **Rspec-rails**: Testing framework for Rails.\n- **Shoulda-matchers**: Provides simplifications for testing Rails functionality.\n\n# Features Implemented\n\n- [x] Authentication and Authorization\n\n- [x] Book Management\n\n- [x] Borrowing and Returning\n\n- [x] Dashboard\n\n- [x] API Endpoints\n\n- [x] Frontend (Bonus): Implemented using Hotwire\n\n# Testing\n\nBelow is a summary of the testing efforts:\n\n- [x] **Models**\n  - Comprehensive testing of all models to validate core functionality.\n\n- [x] **Integration Tests (API)**\n  - Integration testing of API functionality to ensure proper communication.\n\n## Getting Started\n\nTo get started, ensure that you have the following prerequisites installed on your system:\n\n- Ruby (version 3.3.1)\n- Rails (version 7.2.0.beta2)\n\n### Setup\n\n1. Clone the repository to your local machine.\n\n```bash\n  git clone https://github.com/diegolinhares/freebooks\n```\n\n2. Navigate to the project directory.\n\n```bash\n  cd freebooks\n```\n\n3. Run the following command to set up the project, which will install dependencies, create the database, and perform necessary setup tasks.\n\n```bash\nbin/setup\n```\n\n### Running the project\n\n```bash\nbin/dev\n```\n\nThis will start the development server, and you can access the application at http://localhost:3000.\n\n### Seed Data\n\nThe following user credentials are available for testing:\n\n#### Librarian\n- **Email:** librarian@freebooks.com\n- **Password:** 12341234\n- **Role:** librarian\n- **API Access Token:** WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\n\n#### Member\n- **Email:** samuel_tarly@freebooks.com\n- **Password:** 12341234\n- **Role:** member\n- **API Access Token:** fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\n\n### Running tests\n```bash\nbundle exec rspec spec\n```\n\nThis command will execute all the tests.\n\n# Solution\n\n### Overview\n\nThis project is divided into two contexts: Web and API. For both contexts, we have a \"BackOffice\" for members and another for librarians.\n\nThe controllers in each context handle authentication and authorization by role.\n\n### Implementation\n\nSome 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.\n\n### Book Borrowing\n\nWhen 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.\n\nAn 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:\n\n```ruby\nt.index [\"user_id\", \"book_id\"], name: \"unique_borrowing_index\", unique: true, where: \"returned_at IS NULL\"\n```\n\n### Book Search\nAnother 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.\n\n### Fixtures and Seeds\nTypically, 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.\n\n# Database\n\n```mermaid\nerDiagram\n    USERS {\n        string email PK \"unique\"\n        string password_digest\n        string role\n        string api_access_token PK \"unique\"\n        datetime created_at\n        datetime updated_at\n    }\n\n    AUTHORS {\n        string name PK \"unique\"\n        datetime created_at\n        datetime updated_at\n    }\n\n    GENRES {\n        string name PK \"unique\"\n        datetime created_at\n        datetime updated_at\n    }\n\n    BOOKS {\n        string title\n        string isbn PK \"unique\"\n        int total_copies\n        int available_copies\n        int genre_id FK\n        int author_id FK\n        datetime created_at\n        datetime updated_at\n    }\n\n    BORROWINGS {\n        int user_id FK\n        int book_id FK\n        datetime borrowed_at\n        datetime due_date\n        datetime returned_at\n        datetime created_at\n        datetime updated_at\n    }\n\n    USERS ||--o{ BORROWINGS : \"has many\"\n    BOOKS ||--o{ BORROWINGS : \"has many\"\n    AUTHORS ||--o{ BOOKS : \"writes\"\n    GENRES ||--o{ BOOKS : \"categorizes\"\n```\n\n## Indexes\n\n- **Authors Table:**\n  - Unique index on `name`\n\n- **Books Table:**\n  - Index on `genre_id`\n  - Index on `author_id`\n  - Unique index on `isbn`\n\n- **Borrowings Table:**\n  - Index on `user_id`\n  - Index on `book_id`\n  - Unique index on `user_id` and `book_id` where `returned_at` is NULL\n\n- **Genres Table:**\n  - Unique index on `name`\n\n- **Users Table:**\n  - Unique index on `email`\n  - Unique index on `api_access_token`\n\n# API Endpoints\n\n## Members\n\n### Sessions\n\n`POST /api/v1/members/sessions`\n\n#### When Authenticated\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/sessions \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"samuel.tarly@example.com\",\n    \"password\": \"12341234\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Action not allowed for authenticated member\",\n  \"details\": {}\n}\n```\n\n#### When Unauthenticated (valid credentials)\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/sessions \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"samuel.tarly@example.com\",\n    \"password\": \"12341234\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"access_token\": \"newly_generated_access_token\"\n  }\n}\n```\n\n#### When Unauthenticated (invalid credentials)\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/sessions \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"bad-email\",\n    \"password\": \"bad-pass\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid email or password\",\n  \"details\": {}\n}\n```\n\n### Delete Session\n\n`DELETE /api/v1/members/sessions`\n\n#### When Authenticated\n\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/members/sessions \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\"\n}\n```\n\n#### When Unauthenticated\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/members/sessions \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Registration\n\n`POST /api/v1/members/registrations`\n\n#### When Unauthenticated\n\nTo register a new member with valid parameters:\n\n```bash\ncurl -X POST http://localhost:300/api/v1/members/registrations \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"new_member@example.com\",\n    \"password\": \"password123\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"message\": \"Member registered successfully\",\n    \"access_token\": \"newly_generated_access_token\"\n  }\n}\n```\n\nTo return an error when parameters are invalid:\n\n```bash\ncurl -X POST http://localhost:300/api/v1/members/registrations \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"invalid_email\",\n    \"password\": \"\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Failed to register member\",\n  \"details\": [\n    \"Email is invalid\",\n    \"Password can't be blank\"\n  ]\n}\n```\n\n#### When Authenticated\n\nTo prevent authenticated members from registering again:\n\n```bash\ncurl -X POST http://localhost:300/api/v1/members/registrations \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"new_member@example.com\",\n    \"password\": \"password123\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Action not allowed for authenticated member\",\n  \"details\": {}\n}\n```\n### Borrowings\n\n`GET /api/v1/members/borrowings`\n\n#### When Authenticated\n\nTo return paginated borrowings for the current member:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/members/borrowings \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"borrowings\": [\n      {\n        \"book_title\": \"Expired Book\",\n        \"status\": \"overdue\"\n      },\n      {\n        \"book_title\": \"A Game of Thrones\",\n        \"status\": \"not overdue\"\n      },\n      {\n        \"book_title\": \"Dune\",\n        \"status\": \"not overdue\"\n      }\n    ]\n  },\n  \"pagination\": {\n    \"count\": 3,\n    \"items\": 5,\n    \"next\": null,\n    \"page\": 1,\n    \"pages\": 1,\n    \"prev\": null\n  }\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/members/borrowings \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Create Borrowing\n\n`POST /api/v1/members/book_borrowings`\n\n#### When Authenticated\n\nTo create a borrowing successfully:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"message\": \"Book successfully borrowed.\"\n  },\n  \"type\": \"object\"\n}\n```\n\nTo fail to create a borrowing when no copies are available:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"No available copies to borrow.\",\n  \"details\": {}\n}\n```\n\nTo return an error when the user tries to borrow a book they have already borrowed and not returned:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Failed to borrow book\",\n  \"details\": [\"User has already borrowed this book and not returned it yet\"]\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/members/books/:book_id/borrowings \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Get Books\n\n`GET /api/v1/members/books`\n\n#### When Authenticated\n\nTo return paginated books for the current member:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/members/books \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"books\": [\n      {\n        \"title\": \"A Feast for Crows\",\n        \"author_name\": \"George R. R. Martin\",\n        \"genre_name\": \"Fantasy\"\n      },\n      {\n        \"title\": \"Dune\",\n        \"author_name\": \"Frank Herbert\",\n        \"genre_name\": \"Science Fiction\"\n      },\n      {\n        \"title\": \"A Dance with Dragons\",\n        \"author_name\": \"George R. R. Martin\",\n        \"genre_name\": \"Fantasy\"\n      },\n      {\n        \"title\": \"The Book Thief\",\n        \"author_name\": \"Markus Zusak\",\n        \"genre_name\": \"Historical Fiction\"\n      },\n      {\n        \"title\": \"Gone Girl\",\n        \"author_name\": \"Gillian Flynn\",\n        \"genre_name\": \"Thriller\"\n      }\n    ]\n  },\n  \"pagination\": {\n    \"count\": 16,\n    \"items\": 5,\n    \"next\": 2,\n    \"page\": 1,\n    \"pages\": 4,\n    \"prev\": null\n  }\n}\n```\n\nTo return paginated books for the search query \"Dune\":\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/members/books?query=Dune \\\n-H \"Authorization: Bearer fD7WoV9ZH4qii8KsvwmNKUbSVfsm79rtjwuxgKuCae\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"books\": [\n      {\n        \"title\": \"Dune\",\n        \"author_name\": \"Frank Herbert\",\n        \"genre_name\": \"Science Fiction\"\n      }\n    ]\n  },\n  \"pagination\": {\n    \"count\": 1,\n    \"items\": 5,\n    \"next\": null,\n    \"page\": 1,\n    \"pages\": 1,\n    \"prev\": null\n  }\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/members/books \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n## Librarians\n\n### Create Session\n\n`POST /api/v1/librarians/sessions`\n\n#### When Authenticated\n\nTo avoid re-authenticating the user:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/sessions \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"librarian@freebooks.com\",\n    \"password\": \"12341234\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Action not allowed for authenticated librarian\",\n  \"details\": {}\n}\n```\n\n#### When Unauthenticated\n\nTo authenticate the user when parameters are valid:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/sessions \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"librarian@freebooks.com\",\n    \"password\": \"12341234\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"access_token\": \"newly_generated_access_token\"\n  }\n}\n```\n\nTo avoid authenticating the user when parameters are invalid:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/sessions \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"bad-email\",\n    \"password\": \"bad-pass\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid email or password\",\n  \"details\": {}\n}\n```\n\n### Delete Session\n\n`DELETE /api/v1/librarians/sessions`\n\n#### When Authenticated\n\nTo sign out the current librarian and regenerate the API access token:\n\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/librarians/sessions \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\"\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/librarians/sessions \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Create Registration\n\n`POST /api/v1/librarians/registrations`\n\n#### When Unauthenticated\n\nTo register a new librarian and return access token:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/registrations \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"librarian@example.com\",\n    \"password\": \"password123\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"message\": \"Librarian registered successfully\",\n    \"access_token\": \"newly_generated_access_token\"\n  },\n  \"type\": \"object\"\n}\n```\n\nTo return errors when registration fails:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/registrations \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"\",\n    \"password\": \"password123\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Failed to register librarian\",\n  \"details\": [\"Email can't be blank\"]\n}\n```\n\n#### When Authenticated\n\nTo disallow authenticated librarian from registering:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/registrations \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"user\": {\n    \"email\": \"librarian2@example.com\",\n    \"password\": \"password123\"\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"details\": {},\n  \"message\": \"Action not allowed for authenticated librarian\"\n}\n```\n\n### Get Statistics\n\n`GET /api/v1/librarians/statistics`\n\n#### When Authenticated\n\nTo return dashboard statistics for the librarian:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/statistics \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"books\": 17,\n    \"total_borrowed_books\": 13,\n    \"books_due_today\": 0\n  },\n  \"type\": \"object\"\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/statistics \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Get Members\n\n`GET /api/v1/librarians/members`\n\n#### When Authenticated\n\nTo return paginated members with overdue books:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/members \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"members\": [\n      {\n        \"email\": \"member1@example.com\"\n      },\n      {\n        \"email\": \"member2@example.com\"\n      },\n      ...\n    ]\n  },\n  \"pagination\": {\n    \"count\": 10,\n    \"items\": 5,\n    \"next\": 2,\n    \"page\": 1,\n    \"pages\": 2,\n    \"prev\": null\n  },\n  \"type\": \"object\"\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/members \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Get Books\n\n`GET /api/v1/librarians/books`\n\n#### When Authenticated\n\nTo return paginated books for the librarian:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/books \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"books\": [\n      {\n        \"title\": \"A Feast for Crows\",\n        \"author_name\": \"George R. R. Martin\",\n        \"genre_name\": \"Fantasy\"\n      },\n      {\n        \"title\": \"Sapiens: A Brief History of Humankind\",\n        \"author_name\": \"Yuval Noah Harari\",\n        \"genre_name\": \"Non-fiction\"\n      },\n      {\n        \"title\": \"Dune\",\n        \"author_name\": \"Frank Herbert\",\n        \"genre_name\": \"Science Fiction\"\n      },\n      {\n        \"title\": \"A Dance with Dragons\",\n        \"author_name\": \"George R. R. Martin\",\n        \"genre_name\": \"Fantasy\"\n      },\n      {\n        \"title\": \"The Book Thief\",\n        \"author_name\": \"Markus Zusak\",\n        \"genre_name\": \"Historical Fiction\"\n      }\n    ]\n  },\n  \"pagination\": {\n    \"count\": 17,\n    \"items\": 5,\n    \"next\": 2,\n    \"page\": 1,\n    \"pages\": 4,\n    \"prev\": null\n  }\n}\n```\n\nTo return paginated books for the search query \"Dune\":\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/books?query=Dune \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"type\": \"object\",\n  \"data\": {\n    \"books\": [\n      {\n        \"title\": \"Dune\",\n        \"author_name\": \"Frank Herbert\",\n        \"genre_name\": \"Science Fiction\"\n      }\n    ]\n  },\n  \"pagination\": {\n    \"count\": 1,\n    \"items\": 5,\n    \"next\": null,\n    \"page\": 1,\n    \"pages\": 1,\n    \"prev\": null\n  }\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/books \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Create Book\n\n`POST /api/v1/librarians/books`\n\n#### When Authenticated\n\nTo create a book successfully:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/books \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"New Book\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"message\": \"Book created\",\n    \"book\": {\n      \"title\": \"New Book\"\n    }\n  },\n  \"type\": \"object\"\n}\n```\n\nTo fail to create a book due to validation errors:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/books \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Failed to create book\",\n  \"details\": [\"Title can't be blank\"]\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X POST http://localhost:3000/api/v1/librarians/books \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"New Book\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Update Book\n\n`PUT /api/v1/librarians/books/:id`\n\n#### When Authenticated\n\nTo update a book successfully:\n\n```bash\ncurl -X PUT http://localhost:3000/api/v1/librarians/books/1 \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"Updated Book Title\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"message\": \"Book updated\",\n    \"book\": {\n      \"title\": \"Updated Book Title\"\n    }\n  },\n  \"type\": \"object\"\n}\n```\n\nTo fail to update a book due to validation errors:\n\n```bash\ncurl -X PUT http://localhost:3000/api/v1/librarians/books/1 \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Failed to update book\",\n  \"details\": [\"Title can't be blank\"]\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X PUT http://localhost:3000/api/v1/librarians/books/1 \\\n-H \"Content-Type: application/json\" \\\n-d '{\n  \"book\": {\n    \"title\": \"Updated Book Title\",\n    \"author_id\": 1,\n    \"genre_id\": 1,\n    \"isbn\": \"978-1234567890\",\n    \"total_copies\": 10,\n    \"available_copies\": 10\n  }\n}'\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Delete Book\n\n`DELETE /api/v1/librarians/books/:id`\n\n#### When Authenticated\n\nTo delete a book successfully:\n\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/librarians/books/1 \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\"\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X DELETE http://localhost:3000/api/v1/librarians/books/1 \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Get Member Borrowings\n\n`GET /api/v1/librarians/members/:member_id/borrowings`\n\n#### When Authenticated\n\nTo return paginated borrowings for the member:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/members/1/borrowings \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"borrowings\": [\n      {\n        \"book_title\": \"Expired Book\"\n      },\n      {\n        \"book_title\": \"A Game of Thrones\"\n      },\n      {\n        \"book_title\": \"Dune\"\n      }\n    ]\n  }\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X GET http://localhost:3000/api/v1/librarians/members/1/borrowings \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n\n### Mark Book as Returned\n\n`PATCH /api/v1/librarians/borrowings/:borrowing_id/return`\n\n#### When Authenticated\n\nTo mark the book as returned:\n\n```bash\ncurl -X PATCH http://localhost:3000/api/v1/librarians/borrowings/1/return \\\n-H \"Authorization: Bearer WJDTXRjAxKoZ8WLxKKmjudLUEUMbzKP3g727QHsqY9\" \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"success\",\n  \"data\": {\n    \"message\": \"Book marked as returned.\"\n  },\n  \"type\": \"object\"\n}\n```\n\n#### When Unauthenticated\n\nTo return unauthorized status:\n\n```bash\ncurl -X PATCH http://localhost:3000/api/v1/librarians/borrowings/1/return \\\n-H \"Content-Type: application/json\"\n```\n\n**Expected Response:**\n\n```bash\n{\n  \"status\": \"error\",\n  \"message\": \"Invalid access token\",\n  \"details\": {}\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiegolinhares%2Ffreebooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiegolinhares%2Ffreebooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiegolinhares%2Ffreebooks/lists"}