diff --git a/src/routes/books.ts b/src/routes/books.ts index dabc5c5..9ca9a51 100644 --- a/src/routes/books.ts +++ b/src/routes/books.ts @@ -1,5 +1,7 @@ import { Router } from "express" import { BookService } from "../services/book-service.ts" +import { createBookSchema } from "../schema/book-schema.ts" +import { z } from "zod" const router = Router() const bookService = new BookService() @@ -22,51 +24,20 @@ router.get("/:id", (req, res) => { }) router.post("/", (req, res) => { - const { title, author, genre, description, pages, imageURL } = req.body + try { + const validatedData = createBookSchema.parse(req.body) - if (!title) { - res.status(400).json({ - error: "Preencha todos os campos!" - }) - return - } - - if (!genre) { - res.status(400).json({ - error: "O gênero do livro é obrigatório." - }) - return - } - - if (!description) { - res.status(400).json({ - error: "A descrição do livro é obrigatório." - }) - return - } + const newBook = bookService.create(validatedData); - if (!pages) { - res.status(400).json({ - error: "O número de páginas do livro é obrigatório." - }) - return + res.status(201).json(newBook) + } catch (error) { + if (error instanceof z.ZodError) { + res.status(400).json({ + error: error.issues.map((err) => err.message).join("; ") + }) + return + } } - - if (!imageURL) { - res.status(400).json({ - error: "A imagem do livro é obrigatória." - }) - return - } - - const newBook = bookService.create({ - title, - author, - genre, - description, - imageURL - }) - res.status(201).json(newBook) }) router.put("/:id", (req, res) => { diff --git a/src/schema/book-schema.ts b/src/schema/book-schema.ts new file mode 100644 index 0000000..b0b0df7 --- /dev/null +++ b/src/schema/book-schema.ts @@ -0,0 +1,35 @@ +import { z } from "zod" + +export const createBookSchema = z.object({ + title: z.string({ + message: "O título é obrigatório" + }) + .min(1, "O título não pode estar vazio"), + author: z.string({ + message: "O autor é obrigatório" + }) + .min(1, "O autor não pode estar vazio"), + description: z.string({ + message: "A descrição é obrigatória" + }) + .max(500, "A descrição deve ter no máximo 500 caracteres") + .min(1, "A descrição não pode estar vazia"), + genre: z.string({ + message: "O gênero é obrigatório" + }) + .transform((str) => str.split(",").map(g => g.trim()).filter(g => g.length > 0)), + pages: z.number({ + message: "O número de páginas é obrigatório" + }) + .positive("O número de páginas deve ser maior que zero") + .int("O número de páginas deve ser um número inteiro"), + imageURL: z.string({ + message: "A URL da imagem é obrigatória" + }) + .url("A URL da imagem não é inválida"), +}) + +export const updateBookSchema = createBookSchema.partial() + +export type CreateBookInput = z.infer +export type UpdateBookInput = z.infer \ No newline at end of file diff --git a/src/schema/borrow-schema.ts b/src/schema/borrow-schema.ts new file mode 100644 index 0000000..ee80b1b --- /dev/null +++ b/src/schema/borrow-schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const createBorrowSchema = z.object({ + bookId: z.string({ + error: "O ID do livro é obrigatório" + }), + studentName: z.string({ + error: "O nome do estudante é obrigatório" + }) +}) + +export type CreateBorrowInput = z.infer \ No newline at end of file diff --git a/src/services/book-service.ts b/src/services/book-service.ts index 3e27105..9d14527 100644 --- a/src/services/book-service.ts +++ b/src/services/book-service.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto" import { DatabaseRepository, IBook } from "../utils/db.ts" +import { CreateBookInput } from "../schema/book-schema.ts" export class BookService { public getAll(genre?: string): IBook[] { @@ -17,17 +18,17 @@ export class BookService { return databaseState.books.find(book => String(book.id) === id) || null } - public create(bookData: Partial): IBook { + public create(bookData: CreateBookInput): IBook { const databaseState = DatabaseRepository.read() const newBook: IBook = { id: randomUUID(), - title: bookData.title!, - author: bookData.author!, - description: bookData.description!, - genre: bookData.genre!, - pages: bookData.pages!, - imageURL: bookData.imageURL!, + title: bookData.title, + author: bookData.author, + description: bookData.description || "", + genre: bookData.genre, + pages: bookData.pages, + imageURL: bookData.imageURL || "", available: true } diff --git a/src/swagger.ts b/src/swagger.ts index bfd9285..9072b65 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -1,4 +1,4 @@ -import swaggerJsdoc from "swagger-jsdoc" +import swaggerJsdoc from "swagger-jsdoc"; const options: swaggerJsdoc.Options = { definition: { @@ -14,96 +14,104 @@ const options: swaggerJsdoc.Options = { description: "Servidor Local", }, ], + tags: [ + { name: "Livros", description: "Gerenciamento do acervo de livros" }, + { name: "Empréstimos", description: "Controle de saídas e devoluções de livros" }, + ], paths: { "/books": { get: { - summary: "Lista todos os livros. Aceita filtro por gênero via query param", + tags: ["Livros"], + summary: "Lista todos os livros", + description: "Retorna todos os livros do acervo. Aceita filtro por gênero via query param.", parameters: [ { in: "query", name: "genre", - schema: { - type: "string" - }, + schema: { type: "string" }, description: "Filtrar livros por gênero", }, ], responses: { 200: { - description: "Lista de livros retornada com sucesso" + description: "Lista de livros retornada com sucesso", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Book" } + } + } + } }, }, }, post: { + tags: ["Livros"], summary: "Cadastra um novo livro", requestBody: { required: true, content: { "application/json": { - schema: { - type: "object", - required: ["title", "author", "genre", "description", "imageURL"], - properties: { - title: { type: "string" }, - author: { type: "string" }, - genre: { type: "string" }, - description: { type: "string" }, - imageURL: { type: "string" }, - }, - }, + schema: { $ref: "#/components/schemas/BookInput" }, }, }, }, responses: { 201: { - description: "Livro cadastrado com sucesso" - }, - 400: { - description: "Campos obrigatórios faltando" - }, + description: "Livro cadastrado com sucesso", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Book" } + } + } + }, + 400: { description: "Campos obrigatórios faltando ou inválidos" }, }, }, }, "/books/{id}": { get: { + tags: ["Livros"], summary: "Retorna um livro pelo ID", parameters: [ { in: "path", name: "id", required: true, - schema: { type: "integer" }, - description: "ID do livro", + schema: { type: "string" }, + description: "ID único do livro", }, ], responses: { - 200: { description: "Livro encontrado com sucesso" }, + 200: { + description: "Livro encontrado com sucesso", + content: { + "application/json": { + schema: { $ref: "#/components/schemas/Book" } + } + } + }, 404: { description: "Livro não encontrado" }, }, }, put: { + tags: ["Livros"], summary: "Atualiza os dados de um livro", parameters: [ { in: "path", name: "id", required: true, - schema: { type: "integer" }, - description: "ID do livro", + schema: { type: "string" }, + description: "ID único do livro", }, ], requestBody: { required: true, content: { "application/json": { - schema: { - type: "object", - properties: { - title: { type: "string" }, - author: { type: "string" }, - genre: { type: "string" }, - }, - }, + schema: { $ref: "#/components/schemas/BookInput" }, }, }, }, @@ -113,13 +121,14 @@ const options: swaggerJsdoc.Options = { }, }, delete: { + tags: ["Livros"], summary: "Remove um livro do acervo", parameters: [ { in: "path", name: "id", required: true, - schema: { type: "integer" }, + schema: { type: "string" }, description: "ID do livro a ser deletado", }, ], @@ -131,66 +140,105 @@ const options: swaggerJsdoc.Options = { }, "/borrows": { post: { + tags: ["Empréstimos"], summary: "Registra um novo empréstimo", requestBody: { required: true, content: { "application/json": { - schema: { - type: "object", - required: ["bookId", "studentName"], - properties: { - bookId: { type: "integer" }, - studentName: { type: "string" }, - loanDate: { type: "string" }, - }, - }, + schema: { $ref: "#/components/schemas/BorrowInput" }, }, }, }, responses: { - 201: { - description: "Empréstimo registrado com sucesso" - }, - 400: { - description: "Livro indisponível ou dados malformados" - }, - 404: { - description: "Livro correspondente não encontrado" - }, + 201: { description: "Empréstimo registrado com sucesso" }, + 400: { description: "Livro indisponível ou dados malformados" }, + 404: { description: "Livro correspondente não encontrado" }, }, }, get: { + tags: ["Empréstimos"], summary: "Lista todos os empréstimos ativos", responses: { 200: { - description: "Lista de empréstimos ativos obtida com sucesso" + description: "Lista de empréstimos ativos obtida com sucesso", + content: { + "application/json": { + schema: { + type: "array", + items: { $ref: "#/components/schemas/Borrow" } + } + } + } }, }, }, }, "/borrows/{id}/return": { patch: { + tags: ["Empréstimos"], summary: "Marca um empréstimo como devolvido", parameters: [ { in: "path", name: "id", required: true, - schema: { type: "integer" }, - description: "ID do empréstimo a ser devolvido", + schema: { type: "string" }, + description: "ID do empréstimo a ser retornado", }, ], responses: { - 200: { - description: "Livro devolvido e acervo atualizado com sucesso" - }, - 400: { - description: "Empréstimo já devolvido anteriormente" - }, - 404: { - description: "Empréstimo não encontrado" - }, + 200: { description: "Livro devolvido e acervo atualizado com sucesso" }, + 400: { description: "Empréstimo já devolvido anteriormente" }, + 404: { description: "Empréstimo não encontrado" }, + }, + }, + }, + }, + + // COMPONENTES PARA DEFINIÇÃO DE SCHEMAS DE DADOS REUTILIZÁVEIS + components: { + schemas: { + BookInput: { + type: "object", + required: ["title", "author", "genre", "description", "pages", "imageURL"], + properties: { + title: { type: "string", example: "O Senhor dos Anéis" }, + author: { type: "string", example: "J.R.R. Tolkien" }, + genre: { type: "string", example: "Fantasia" }, + description: { type: "string", example: "Uma grande jornada pela Terra Média." }, + pages: { type: "integer", example: 1178 }, + imageURL: { type: "string", example: "http://exemplo.com/imagem.jpg" }, + }, + }, + Book: { + type: "object", + properties: { + id: { type: "string", example: 1 }, + title: { type: "string", example: "O Senhor dos Anéis" }, + author: { type: "string", example: "J.R.R. Tolkien" }, + genre: { type: "string", example: "Fantasia" }, + description: { type: "string", example: "Uma grande jornada pela Terra Média." }, + pages: { type: "integer", example: 1178 }, + imageURL: { type: "string", example: "http://exemplo.com/imagem.jpg" }, + }, + }, + BorrowInput: { + type: "object", + required: ["bookId", "studentName"], + properties: { + bookId: { type: "integer", example: 1 }, + studentName: { type: "string", example: "João Silva" }, + }, + }, + Borrow: { + type: "object", + properties: { + id: { type: "string", example: 123 }, + bookId: { type: "string", example: 1 }, + studentName: { type: "string", example: "João Silva" }, + borrowDate: { type: "string", format: "date-time", example: "2026-02-20" }, + returned: { type: "boolean", example: false } }, }, }, @@ -199,4 +247,4 @@ const options: swaggerJsdoc.Options = { apis: [], }; -export const swaggerSpec = swaggerJsdoc(options) \ No newline at end of file +export const swaggerSpec = swaggerJsdoc(options); \ No newline at end of file