diff --git a/correction/prisma/schema.prisma b/correction/prisma/schema.prisma new file mode 100644 index 0000000000000000000000000000000000000000..a6a46a78f1a0265af9fb2447d439015556ee9ad8 --- /dev/null +++ b/correction/prisma/schema.prisma @@ -0,0 +1,33 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Author { + id Int @id @default(autoincrement()) + firstname String + lastname String + books Book[] +} + +model Book { + id Int @id @default(autoincrement()) + title String + publication_year Int? + author Author @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + tags Tag[] +} + +model Tag { + id Int @id @default(autoincrement()) + name String @unique + books Book[] +} diff --git a/correction/src/db.ts b/correction/src/db.ts new file mode 100644 index 0000000000000000000000000000000000000000..39b43888511ca03a3b7115a6c32f300d30af0992 --- /dev/null +++ b/correction/src/db.ts @@ -0,0 +1,6 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +prisma.$connect(); + +export { prisma }; diff --git a/correction/src/error.ts b/correction/src/error.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b3313810834536908bee2c105efb5964ade4c4c --- /dev/null +++ b/correction/src/error.ts @@ -0,0 +1,8 @@ +export class HttpError extends Error { + status?: number; // optionnel, afin de rester compatible avec le type Error standard + + constructor(message: string, status: number) { + super(message); + this.status = status; + } +} diff --git a/correction/src/index.ts b/correction/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..a032f4602aa8823265c67731cc527ae9c206a7ce --- /dev/null +++ b/correction/src/index.ts @@ -0,0 +1,46 @@ +import express, { Request, Response, NextFunction } from 'express'; +import { HttpError } from './error'; +import { StructError } from 'superstruct'; + +import * as author from './requestHandlers/author'; +import * as book from './requestHandlers/book'; +import * as tag from './requestHandlers/tag'; + +const app = express(); +const port = 3000; + +app.use(express.json()); + +app.get('/authors', author.get_all); +app.get('/authors/:author_id', author.get_one); +app.post('/authors', author.create_one); +app.patch('/authors/:author_id', author.update_one); +app.delete('/authors/:author_id', author.delete_one); + +app.get('/books', book.get_all); +app.get('/books/:book_id', book.get_one); +app.get('/authors/:author_id/books', book.get_all_of_author); +app.post('/authors/:author_id/books', book.create_one_of_author); +app.patch('/books/:book_id', book.update_one); +app.delete('/books/:book_id', book.delete_one); + +app.get('/tags', tag.get_all); +app.get('/tags/:tag_id', tag.get_one); +app.get('/books/:book_id/tags', tag.get_all_of_book); +app.post('/tags', tag.create_one); +app.patch('/tags/:tag_id', tag.update_one); +app.delete('/tags/:tag_id', tag.delete_one); +app.post('/books/:book_id/tags/:tag_id', tag.add_one_to_book); +app.delete('/books/:book_id/tags/:tag_id', tag.remove_one_from_book); + +app.use((err: HttpError, req: Request, res: Response, next: NextFunction) => { + if (err instanceof StructError) { + err.status = 400; + err.message = `Bad value for field ${err.key}`; + } + return res.status(err.status ?? 500).send(err.message); +}); + +app.listen(port, () => { + console.log(`App listening on port ${port}`); +}); diff --git a/correction/src/requestHandlers/author.ts b/correction/src/requestHandlers/author.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ec348eb86c6905c72e25395d11d94baeac02cfc --- /dev/null +++ b/correction/src/requestHandlers/author.ts @@ -0,0 +1,82 @@ +import { prisma } from '../db'; +import { Prisma } from '@prisma/client'; +import type { Request, Response } from 'express'; +import { HttpError } from '../error'; +import { assert } from 'superstruct'; +import { AuthorCreationData, AuthorUpdateData, AuthorGetAllQuery } from '../validation/author'; + +export async function get_all(req: Request, res: Response) { + assert(req.query, AuthorGetAllQuery); + const { lastname, hasBooks, include, skip, take } = req.query; + const filter: Prisma.AuthorWhereInput = {}; + if (lastname) { + filter.lastname = { contains: String(lastname) }; + } + if (hasBooks === 'true') { + filter.books = { some: {} }; + } + const assoc: Prisma.AuthorInclude = {}; + if (include === 'books') { + assoc.books = { select: { id: true, title: true }, orderBy: { title: 'asc' } }; + } + const authors = await prisma.author.findMany({ + where: filter, + include: assoc, + orderBy: { lastname: 'asc' }, + skip: skip ? Number(skip) : undefined, + take: take ? Number(take) : undefined + }); + const authorCount = await prisma.author.count({ where: filter }); + res.header('X-Total-Count', String(authorCount)); + res.json(authors); +}; + +export async function get_one(req: Request, res: Response) { + const author = await prisma.author.findUnique({ + where: { + id: Number(req.params.author_id) + } + }); + if (!author) { + throw new HttpError('Author not found', 404); + } + res.json(author); +}; + +export async function create_one(req: Request, res: Response) { + assert(req.body, AuthorCreationData); + const author = await prisma.author.create({ + data: req.body + }); + res.status(201).json(author); +}; + +export async function update_one(req: Request, res: Response) { + assert(req.body, AuthorUpdateData); + try { + const author = await prisma.author.update({ + where: { + id: Number(req.params.author_id) + }, + data: req.body + }); + res.json(author); + } + catch (err) { + throw new HttpError('Author not found', 404); + } +}; + +export async function delete_one(req: Request, res: Response) { + try { + await prisma.author.delete({ + where: { + id: Number(req.params.author_id) + } + }); + res.status(204).send(); + } + catch (err) { + throw new HttpError('Author not found', 404); + } +}; diff --git a/correction/src/requestHandlers/book.ts b/correction/src/requestHandlers/book.ts new file mode 100644 index 0000000000000000000000000000000000000000..53db40cae68cef5e0fa9ce6ad5dc6f8945f16d90 --- /dev/null +++ b/correction/src/requestHandlers/book.ts @@ -0,0 +1,107 @@ +import { prisma } from '../db'; +import { Prisma } from '@prisma/client'; +import type { Request, Response } from 'express'; +import { HttpError } from '../error'; +import { assert } from 'superstruct'; +import { BookCreationData, BookUpdateData } from '../validation/book'; + +export async function get_all(req: Request, res: Response) { + const { title, include, skip, take } = req.query; + const filter: Prisma.BookWhereInput = {}; + if (title) { + filter.title = { contains: String(title) }; + } + const assoc: Prisma.BookInclude = {}; + if (include === 'author') { + assoc.author = { select: { id: true, firstname: true, lastname: true } }; + } + const books = await prisma.book.findMany({ + where: filter, + include: assoc, + orderBy: { title: 'asc' }, + skip: skip ? Number(skip) : undefined, + take: take ? Number(take) : undefined + }); + const bookCount = await prisma.book.count({ where: filter }); + res.header('X-Total-Count', String(bookCount)); + res.json(books); +}; + +export async function get_one(req: Request, res: Response) { + const book = await prisma.book.findUnique({ + where: { + id: Number(req.params.book_id) + } + }); + if (!book) { + throw new HttpError('Book not found', 404); + } + res.json(book); +}; + +export async function get_all_of_author(req: Request, res: Response) { + const { title } = req.query; + const filter: Prisma.BookWhereInput = {}; + if (title) { + filter.title = { contains: String(title) }; + } + const author = await prisma.author.findUnique({ + where: { + id: Number(req.params.author_id), + }, + include: { + books: { + where: filter, + } + } + }); + if (!author) { + throw new HttpError('Author not found', 404); + } + res.json(author.books); +}; + +export async function create_one_of_author(req: Request, res: Response) { + assert(req.body, BookCreationData); + const book = await prisma.book.create({ + data: { + ...req.body, + author: { + connect: { + id: Number(req.params.author_id) + } + } + } + }); + res.status(201).json(book); +}; + +export async function update_one(req: Request, res: Response) { + assert(req.body, BookUpdateData); + try { + const book = await prisma.book.update({ + where: { + id: Number(req.params.book_id) + }, + data: req.body + }); + res.json(book); + } + catch (err) { + throw new HttpError('Book not found', 404); + } +}; + +export async function delete_one(req: Request, res: Response) { + try { + await prisma.book.delete({ + where: { + id: Number(req.params.book_id) + } + }); + res.status(204).send(); + } + catch (err) { + throw new HttpError('Book not found', 404); + } +}; diff --git a/correction/src/requestHandlers/tag.ts b/correction/src/requestHandlers/tag.ts new file mode 100644 index 0000000000000000000000000000000000000000..300abeac1dad3eba8312ae25ce726cb9bb12aed1 --- /dev/null +++ b/correction/src/requestHandlers/tag.ts @@ -0,0 +1,107 @@ +import { prisma } from '../db'; +import type { Request, Response } from 'express'; +import { HttpError } from '../error'; +import { assert } from 'superstruct'; +import { TagCreationData, TagUpdateData } from '../validation/tag'; + +export async function get_all(req: Request, res: Response) { + const tags = await prisma.tag.findMany(); + res.json(tags); +}; + +export async function get_one(req: Request, res: Response) { + const tag = await prisma.tag.findUnique({ + where: { + id: Number(req.params.tag_id) + } + }); + if (!tag) { + throw new HttpError('Tag not found', 404); + } + res.json(tag); +}; + +export async function get_all_of_book(req: Request, res: Response) { + const book = await prisma.book.findUnique({ + where: { + id: Number(req.params.book_id) + }, + include: { + tags: true + } + }); + if (!book) { + throw new HttpError('Book not found', 404); + } + res.json(book.tags); +}; + +export async function create_one(req: Request, res: Response) { + assert(req.body, TagCreationData); + const tag = await prisma.tag.create({ + data: req.body + }); + res.status(201).json(tag); +}; + +export async function update_one(req: Request, res: Response) { + assert(req.body, TagUpdateData); + try { + const tag = await prisma.tag.update({ + where: { + id: Number(req.params.tag_id) + }, + data: req.body + }); + res.json(tag); + } + catch (err) { + throw new HttpError('Tag not found', 404); + } +}; + +export async function delete_one(req: Request, res: Response) { + try { + await prisma.tag.delete({ + where: { + id: Number(req.params.tag_id) + } + }); + res.status(204).send(); + } + catch (err) { + throw new HttpError('Tag not found', 404); + } +}; + +export async function add_one_to_book(req: Request, res: Response) { + await prisma.tag.update({ + where: { + id: Number(req.params.tag_id) + }, + data: { + books: { + connect: { + id: Number(req.params.book_id) + } + } + } + }); + res.status(204).send(); +}; + +export async function remove_one_from_book(req: Request, res: Response) { + await prisma.tag.update({ + where: { + id: Number(req.params.tag_id) + }, + data: { + books: { + disconnect: { + id: Number(req.params.book_id) + } + } + } + }); + res.status(204).send(); +}; diff --git a/correction/src/validation/author.ts b/correction/src/validation/author.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c1d758feeebb12a031b4774c3df3fc303862715 --- /dev/null +++ b/correction/src/validation/author.ts @@ -0,0 +1,20 @@ +import { object, string, optional, size, refine } from 'superstruct'; +import { isInt } from 'validator'; + +export const AuthorCreationData = object({ + firstname: size(string(), 1, 50), + lastname: size(string(), 1, 50), +}); + +export const AuthorUpdateData = object({ + firstname: optional(size(string(), 1, 50)), + lastname: optional(size(string(), 1, 50)), +}); + +export const AuthorGetAllQuery = object({ + lastname: optional(string()), + hasBooks: optional(refine(string(), 'true', (value) => value === 'true')), + include: optional(refine(string(), 'include', (value) => value === 'books')), + skip: optional(refine(string(), 'int', (value) => isInt(value))), + take: optional(refine(string(), 'int', (value) => isInt(value))), +}); diff --git a/correction/src/validation/book.ts b/correction/src/validation/book.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b46a63ac7069e86d3e490a40a68cb2b2c12c7c0 --- /dev/null +++ b/correction/src/validation/book.ts @@ -0,0 +1,11 @@ +import { object, string, integer, optional, size } from 'superstruct'; + +export const BookCreationData = object({ + title: size(string(), 1, 50), + publication_year: optional(integer()) +}); + +export const BookUpdateData = object({ + title: optional(size(string(), 1, 50)), + publication_year: optional(integer()) +}); diff --git a/correction/src/validation/tag.ts b/correction/src/validation/tag.ts new file mode 100644 index 0000000000000000000000000000000000000000..4077e2ea7c156b987b09b781ab99b5f202bd70d9 --- /dev/null +++ b/correction/src/validation/tag.ts @@ -0,0 +1,9 @@ +import { object, string, optional, size } from 'superstruct'; + +export const TagCreationData = object({ + name: size(string(), 1, 50) +}); + +export const TagUpdateData = object({ + name: optional(size(string(), 1, 50)) +});