import { Controller, Get, Route, Tags, Post, Body, Patch, Delete, Path, Response, Produces, } from "tsoa"; import { Book, BookStatus, BorrowBookRequest, CreateBookRequest, ReturnBookRequest, } from "../models/Book"; import { readMyData, writeMyData } from "../utils/fileStorage"; import { wrapResponse, ApiResponse } from "../utils/responseWrapper"; import { v4 as uuidv4 } from "uuid"; import { NotFoundError, BadRequestError } from "../utils/apiErrors"; import { BookValidator } from "../validators/bookValidator"; import { Visitor } from "../models/Visitor"; import { Employee } from "../models/Employee"; import { DateUtils } from "../utils/dateUtils"; import { bookLinkManager } from "../models/Book"; import { Link } from "../models/Link"; @Route("books") @Tags("Books") export class BookController extends Controller { private file = "books.sea"; private visitorsFile = "visitors.sea"; private employeesFile = "employees.sea"; @Get("/") public async getBooks(): Promise> { const books = await readMyData(this.file); return wrapResponse(books); } @Post("/") public async addBook( @Body() body: CreateBookRequest ): Promise> { BookValidator.validate(body); const books = await readMyData(this.file); const newBook: Book = { id: uuidv4(), ...body, status: BookStatus.AVAILABLE, }; books.push(newBook); await writeMyData(this.file, books); return wrapResponse(newBook); } @Get("/{id}") public async getBook(id: string): Promise> { const books = await readMyData(this.file); const book = books.find((b) => b.id === id); if (!book) { throw new NotFoundError(`Book with id ${id} not found`); } return wrapResponse(book); } @Patch("/{id}") public async updateBook( @Path() id: string, @Body() body: Partial ): Promise> { BookValidator.validateForUpdate(body); const books = await readMyData(this.file); const bookIndex = books.findIndex((b) => b.id === id); if (bookIndex === -1) { throw new NotFoundError(`Book with id ${id} not found`); } books[bookIndex] = { ...books[bookIndex], ...body }; await writeMyData(this.file, books); return wrapResponse(books[bookIndex]); } @Delete("/{id}") public async deleteBook( @Path() id: string ): Promise> { const books = await readMyData(this.file); const index = books.findIndex((b) => b.id === id); if (index === -1) { throw new NotFoundError(`Book with id ${id} not found`); } books.splice(index, 1); await writeMyData(this.file, books); return wrapResponse({ message: `Book with id ${id} deleted successfully`, }); } @Get("/{id}/download") @Produces("application/json") public async downloadBook( @Path() id: string ): Promise { const book = await this.getBook(id); return JSON.stringify(book.data, null, 2); } @Post("/borrow") public async borrowBook( @Body() body: BorrowBookRequest ): Promise> { const books = await readMyData(this.file); const visitors = await readMyData(this.visitorsFile); const employees = await readMyData(this.employeesFile); const visitorIndex = visitors.findIndex((v) => v.id === body.visitorId); if (visitorIndex === -1) { throw new NotFoundError(`Visitor with id ${body.visitorId} not found`); } const employee = employees.find((e) => e.id === body.employeeId); if (!employee) { throw new NotFoundError( `Employee with id ${body.employeeId} not found` ); } if (!DateUtils.isWorkingDay(body.borrowDate, employee.workDays)) { throw new BadRequestError("Library is closed on this day"); } const borrowedBooks: string[] = []; for (const bookId of body.bookIds) { const bookIndex = books.findIndex((b) => b.id === bookId); if (bookIndex === -1) { throw new NotFoundError(`Book with id ${bookId} not found`); } if (books[bookIndex].status === BookStatus.BORROWED) { throw new BadRequestError( `Book "${books[bookIndex].title}" is already borrowed` ); } books[bookIndex].status = BookStatus.BORROWED; const bookLink = bookLinkManager.toLink(bookId); visitors[visitorIndex].currentBooks.push(bookLink); borrowedBooks.push(bookId); } await writeMyData(this.file, books); await writeMyData(this.visitorsFile, visitors); return wrapResponse({ message: `Successfully borrowed ${borrowedBooks.length} book(s)`, }); } @Post("/return") public async returnBook( @Body() body: ReturnBookRequest ): Promise> { const books = await readMyData(this.file); const visitors = await readMyData(this.visitorsFile); const employees = await readMyData(this.employeesFile); const visitorIndex = visitors.findIndex((v) => v.id === body.visitorId); if (visitorIndex === -1) { throw new NotFoundError(`Visitor with id ${body.visitorId} not found`); } const employee = employees.find((e) => e.id === body.employeeId); if (!employee) { throw new NotFoundError( `Employee with id ${body.employeeId} not found` ); } if (!DateUtils.isWorkingDay(body.returnDate, employee.workDays)) { throw new BadRequestError("Library is closed on this day"); } const returnedBooks: string[] = []; for (const bookId of body.bookIds) { const bookIndex = books.findIndex((b) => b.id === bookId); if (bookIndex === -1) { throw new NotFoundError(`Book with id ${bookId} not found`); } if (books[bookIndex].status === BookStatus.AVAILABLE) { throw new BadRequestError( `Book "${books[bookIndex].title}" is not borrowed` ); } // Ищем Link объект в currentBooks const linkIndex = visitors[visitorIndex].currentBooks.findIndex( (link) => (link).id === bookId ); if (linkIndex === -1) { throw new BadRequestError( `Visitor does not have book "${books[bookIndex].title}"` ); } books[bookIndex].status = BookStatus.AVAILABLE; // Удаляем из currentBooks и добавляем в history как Link объект const bookLink = visitors[visitorIndex].currentBooks.splice( linkIndex, 1 )[0]; visitors[visitorIndex].history.push(bookLink); returnedBooks.push(bookId); } await writeMyData(this.file, books); await writeMyData(this.visitorsFile, visitors); return wrapResponse({ message: `Successfully returned ${returnedBooks.length} book(s)`, }); } }