Repository: rocketseat-education/nlw-unite-nodejs Branch: main Commit: 61445f96837f Files: 24 Total size: 21.9 KB Directory structure: gitextract__xl0pikx/ ├── .gitignore ├── README.md ├── api.http ├── package.json ├── prisma/ │ ├── migrations/ │ │ ├── 20240325193227_create_table_events/ │ │ │ └── migration.sql │ │ ├── 20240326182647_create_table_attendees/ │ │ │ └── migration.sql │ │ ├── 20240326184908_add_uniqueness_on_event_id_and_email/ │ │ │ └── migration.sql │ │ ├── 20240327140503_create_check_ins_table/ │ │ │ └── migration.sql │ │ ├── 20240327143815_add_cascades/ │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── schema.prisma │ └── seed.ts ├── src/ │ ├── error-handler.ts │ ├── lib/ │ │ └── prisma.ts │ ├── routes/ │ │ ├── _errors/ │ │ │ └── bad-request.ts │ │ ├── check-in.ts │ │ ├── create-event.ts │ │ ├── get-attendee-badge.ts │ │ ├── get-event-attendees.ts │ │ ├── get-event.ts │ │ └── register-for-event.ts │ ├── server.ts │ └── utils/ │ └── generate-slug.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules # Keep environment variables out of version control .env dist ================================================ FILE: README.md ================================================ # pass.in O pass.in é uma aplicação de **gestão de participantes em eventos presenciais**. A ferramenta permite que o organizador cadastre um evento e abra uma página pública de inscrição. Os participantes inscritos podem emitir uma credencial para check-in no dia do evento. O sistema fará um scan da credencial do participante para permitir a entrada no evento. ## Requisitos ### Requisitos funcionais - [x] O organizador deve poder cadastrar um novo evento; - [x] O organizador deve poder visualizar dados de um evento; - [x] O organizador deve poser visualizar a lista de participantes; - [x] O participante deve poder se inscrever em um evento; - [x] O participante deve poder visualizar seu crachá de inscrição; - [x] O participante deve poder realizar check-in no evento; ### Regras de negócio - [x] O participante só pode se inscrever em um evento uma única vez; - [x] O participante só pode se inscrever em eventos com vagas disponíveis; - [x] O participante só pode realizar check-in em um evento uma única vez; ### Requisitos não-funcionais - [x] O check-in no evento será realizado através de um QRCode; ## Documentação da API (Swagger) Para documentação da API, acesse o link: https://nlw-unite-nodejs.onrender.com/docs ## Banco de dados Nessa aplicação vamos utilizar banco de dados relacional (SQL). Para ambiente de desenvolvimento seguiremos com o SQLite pela facilidade do ambiente. ### Diagrama ERD Diagrama ERD do banco de dados ### Estrutura do banco (SQL) ```sql -- CreateTable CREATE TABLE "events" ( "id" TEXT NOT NULL PRIMARY KEY, "title" TEXT NOT NULL, "details" TEXT, "slug" TEXT NOT NULL, "maximum_attendees" INTEGER ); -- CreateTable CREATE TABLE "attendees" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "email" TEXT NOT NULL, "event_id" TEXT NOT NULL, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "attendees_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); -- CreateTable CREATE TABLE "check_ins" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "attendeeId" INTEGER NOT NULL, CONSTRAINT "check_ins_attendeeId_fkey" FOREIGN KEY ("attendeeId") REFERENCES "attendees" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); -- CreateIndex CREATE UNIQUE INDEX "events_slug_key" ON "events"("slug"); -- CreateIndex CREATE UNIQUE INDEX "attendees_event_id_email_key" ON "attendees"("event_id", "email"); -- CreateIndex CREATE UNIQUE INDEX "check_ins_attendeeId_key" ON "check_ins"("attendeeId"); ``` ================================================ FILE: api.http ================================================ POST http://localhost:3333/events Content-Type: application/json { "title": 123, "details": null, "maximumAttendees": 1 } ### POST http://localhost:3333/events/81a3f6db-45bf-4a6f-b10c-bcaf8837ac43/attendees Content-Type: application/json { "name": "Diego Fernandes", "email": "diego2@rocketseat.com.br" } ### GET http://localhost:3333/events/81a3f6db-45bf-4a6f-b10c-bcaf8837ac43 ### GET http://localhost:3333/attendees/3/badge ### GET http://localhost:3333/attendees/4/check-in ### GET http://localhost:3333/events/81a3f6db-45bf-4a6f-b10c-bcaf8837ac43/attendees?query=diego ================================================ FILE: package.json ================================================ { "name": "server-node", "version": "1.0.0", "description": "O pass.in é uma aplicação de **gestão de participantes em eventos presenciais**.", "main": "index.js", "scripts": { "start": "node dist/server.mjs", "build": "tsup src --format esm", "dev": "tsx watch --env-file .env src/server.ts", "db:migrate": "prisma migrate dev", "db:studio": "prisma studio" }, "keywords": [], "author": "", "license": "ISC", "prisma": { "seed": "tsx prisma/seed.ts" }, "devDependencies": { "@faker-js/faker": "8.4.1", "@types/node": "20.11.30", "prisma": "5.11.0", "tsup": "8.0.2", "tsx": "4.7.1", "typescript": "5.4.3" }, "dependencies": { "@fastify/cors": "9.0.1", "@fastify/swagger": "8.14.0", "@fastify/swagger-ui": "3.0.0", "@prisma/client": "5.11.0", "dayjs": "1.11.10", "fastify": "4.26.2", "fastify-type-provider-zod": "1.1.9", "zod": "3.22.4" } } ================================================ FILE: prisma/migrations/20240325193227_create_table_events/migration.sql ================================================ -- CreateTable CREATE TABLE "events" ( "id" TEXT NOT NULL PRIMARY KEY, "title" TEXT NOT NULL, "details" TEXT, "slug" TEXT NOT NULL, "maximum_attendees" INTEGER ); -- CreateIndex CREATE UNIQUE INDEX "events_slug_key" ON "events"("slug"); ================================================ FILE: prisma/migrations/20240326182647_create_table_attendees/migration.sql ================================================ -- CreateTable CREATE TABLE "attendees" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "email" TEXT NOT NULL, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "event_id" TEXT NOT NULL, CONSTRAINT "attendees_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); ================================================ FILE: prisma/migrations/20240326184908_add_uniqueness_on_event_id_and_email/migration.sql ================================================ /* Warnings: - A unique constraint covering the columns `[event_id,email]` on the table `attendees` will be added. If there are existing duplicate values, this will fail. */ -- CreateIndex CREATE UNIQUE INDEX "attendees_event_id_email_key" ON "attendees"("event_id", "email"); ================================================ FILE: prisma/migrations/20240327140503_create_check_ins_table/migration.sql ================================================ -- CreateTable CREATE TABLE "check_ins" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "attendee_id" INTEGER NOT NULL, CONSTRAINT "check_ins_attendee_id_fkey" FOREIGN KEY ("attendee_id") REFERENCES "attendees" ("id") ON DELETE RESTRICT ON UPDATE CASCADE ); -- CreateIndex CREATE UNIQUE INDEX "check_ins_attendee_id_key" ON "check_ins"("attendee_id"); ================================================ FILE: prisma/migrations/20240327143815_add_cascades/migration.sql ================================================ -- RedefineTables PRAGMA foreign_keys=OFF; CREATE TABLE "new_check_ins" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "attendee_id" INTEGER NOT NULL, CONSTRAINT "check_ins_attendee_id_fkey" FOREIGN KEY ("attendee_id") REFERENCES "attendees" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); INSERT INTO "new_check_ins" ("attendee_id", "created_at", "id") SELECT "attendee_id", "created_at", "id" FROM "check_ins"; DROP TABLE "check_ins"; ALTER TABLE "new_check_ins" RENAME TO "check_ins"; CREATE UNIQUE INDEX "check_ins_attendee_id_key" ON "check_ins"("attendee_id"); CREATE TABLE "new_attendees" ( "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "name" TEXT NOT NULL, "email" TEXT NOT NULL, "created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "event_id" TEXT NOT NULL, CONSTRAINT "attendees_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "events" ("id") ON DELETE CASCADE ON UPDATE CASCADE ); INSERT INTO "new_attendees" ("created_at", "email", "event_id", "id", "name") SELECT "created_at", "email", "event_id", "id", "name" FROM "attendees"; DROP TABLE "attendees"; ALTER TABLE "new_attendees" RENAME TO "attendees"; CREATE UNIQUE INDEX "attendees_event_id_email_key" ON "attendees"("event_id", "email"); PRAGMA foreign_key_check; PRAGMA foreign_keys=ON; ================================================ FILE: prisma/migrations/migration_lock.toml ================================================ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) provider = "sqlite" ================================================ FILE: prisma/schema.prisma ================================================ generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Event { id String @id @default(uuid()) title String details String? slug String @unique maximumAttendees Int? @map("maximum_attendees") attendees Attendee[] @@map("events") } model Attendee { id Int @id @default(autoincrement()) name String email String createdAt DateTime @default(now()) @map("created_at") eventId String @map("event_id") event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) checkIn CheckIn? @@unique([eventId, email]) @@map("attendees") } model CheckIn { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) @map("created_at") attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade) attendeeId Int @unique @map("attendee_id") @@map("check_ins") } ================================================ FILE: prisma/seed.ts ================================================ import { prisma } from '../src/lib/prisma' import { faker } from '@faker-js/faker' import { Prisma } from '@prisma/client' import dayjs from 'dayjs' async function seed() { const eventId = '9e9bd979-9d10-4915-b339-3786b1634f33' await prisma.event.deleteMany() await prisma.event.create({ data: { id: eventId, title: 'Unite Summit', slug: 'unite-summit', details: 'Um evento p/ devs apaixonados(as) por código!', maximumAttendees: 120, } }) const attendeesToInsert: Prisma.AttendeeUncheckedCreateInput[] = [] for (let i = 0; i <= 120; i++) { attendeesToInsert.push({ id: 10000 + i, name: faker.person.fullName(), email: faker.internet.email(), eventId, createdAt: faker.date.recent({ days: 30, refDate: dayjs().subtract(8, "days").toDate() }), checkIn: faker.helpers.arrayElement([ undefined, { create: { createdAt: faker.date.recent({ days: 7 }), } } ]) }) } await Promise.all(attendeesToInsert.map(data => { return prisma.attendee.create({ data, }) })) } seed().then(() => { console.log('Database seeded!') prisma.$disconnect() }) ================================================ FILE: src/error-handler.ts ================================================ import { FastifyInstance } from 'fastify' import { BadRequest } from './routes/_errors/bad-request' import { ZodError } from 'zod' type FastifyErrorHandler = FastifyInstance['errorHandler'] export const errorHandler: FastifyErrorHandler = (error, request, reply) => { if (error instanceof ZodError) { return reply.status(400).send({ message: 'Error during validation', errors: error.flatten().fieldErrors, }) } if (error instanceof BadRequest) { return reply.status(400).send({ message: error.message }) } return reply.status(500).send({ message: 'Internal server error!' }) } ================================================ FILE: src/lib/prisma.ts ================================================ import { PrismaClient } from "@prisma/client"; export const prisma = new PrismaClient({ log: ['query'], }) ================================================ FILE: src/routes/_errors/bad-request.ts ================================================ export class BadRequest extends Error {} ================================================ FILE: src/routes/check-in.ts ================================================ import { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import z from "zod"; import { prisma } from "../lib/prisma"; import { BadRequest } from "./_errors/bad-request"; export async function checkIn(app: FastifyInstance) { app .withTypeProvider() .get('/attendees/:attendeeId/check-in', { schema: { summary: 'Check-in an attendee', tags: ['check-ins'], params: z.object({ attendeeId: z.coerce.number().int() }), response: { 201: z.null(), } } }, async (request, reply) => { const { attendeeId } = request.params const attendeeCheckIn = await prisma.checkIn.findUnique({ where: { attendeeId, } }) if (attendeeCheckIn !== null) { throw new BadRequest('Attendee already checked in!') } await prisma.checkIn.create({ data: { attendeeId, } }) return reply.status(201).send() }) } ================================================ FILE: src/routes/create-event.ts ================================================ import { ZodTypeProvider } from "fastify-type-provider-zod" import { z } from "zod" import { generateSlug } from "../utils/generate-slug" import { prisma } from "../lib/prisma" import { FastifyInstance } from "fastify" import { BadRequest } from "./_errors/bad-request" export async function createEvent(app: FastifyInstance) { app .withTypeProvider() .post('/events', { schema: { summary: 'Create an event', tags: ['events'], body: z.object({ title: z.string().min(4), details: z.string().nullable(), maximumAttendees: z.number().int().positive().nullable(), }), response: { 201: z.object({ eventId: z.string().uuid(), }) }, }, }, async (request, reply) => { const { title, details, maximumAttendees, } = request.body const slug = generateSlug(title) const eventWithSameSlug = await prisma.event.findUnique({ where: { slug, } }) if (eventWithSameSlug !== null) { throw new BadRequest('Another event with same title already exists.') } const event = await prisma.event.create({ data: { title, details, maximumAttendees, slug, }, }) return reply.status(201).send({ eventId: event.id }) }) } ================================================ FILE: src/routes/get-attendee-badge.ts ================================================ import { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import { z } from "zod"; import { prisma } from "../lib/prisma"; import { BadRequest } from "./_errors/bad-request"; export async function getAttendeeBadge(app: FastifyInstance) { app .withTypeProvider() .get('/attendees/:attendeeId/badge', { schema: { summary: 'Get an attendee badge', tags: ['attendees'], params: z.object({ attendeeId: z.coerce.number().int(), }), response: { 200: z.object({ badge: z.object({ name: z.string(), email: z.string().email(), eventTitle: z.string(), checkInURL: z.string().url(), }) }) }, } }, async (request, reply) => { const { attendeeId } = request.params const attendee = await prisma.attendee.findUnique({ select: { name: true, email: true, event: { select: { title: true, }, }, }, where: { id: attendeeId, } }) if (attendee === null) { throw new BadRequest('Attendee not found.') } const baseURL = `${request.protocol}://${request.hostname}` const checkInURL = new URL(`/attendees/${attendeeId}/check-in`, baseURL) return reply.send({ badge: { name: attendee.name, email: attendee.email, eventTitle: attendee.event.title, checkInURL: checkInURL.toString(), } }) }) } ================================================ FILE: src/routes/get-event-attendees.ts ================================================ import { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import { z } from "zod"; import { prisma } from "../lib/prisma"; export async function getEventAttendees(app: FastifyInstance) { app .withTypeProvider() .get('/events/:eventId/attendees', { schema: { summary: 'Get event attendees', tags: ['events'], params: z.object({ eventId: z.string().uuid(), }), querystring: z.object({ query: z.string().nullish(), pageIndex: z.string().nullish().default('0').transform(Number), }), response: { 200: z.object({ attendees: z.array( z.object({ id: z.number(), name: z.string(), email: z.string().email(), createdAt: z.date(), checkedInAt: z.date().nullable(), }) ), total: z.number(), }), }, } }, async (request, reply) => { const { eventId } = request.params const { pageIndex, query } = request.query const [attendees, total] = await Promise.all([ prisma.attendee.findMany({ select: { id: true, name: true, email: true, createdAt: true, checkIn: { select: { createdAt: true, } } }, where: query ? { eventId, name: { contains: query, } } : { eventId, }, take: 10, skip: pageIndex * 10, orderBy: { createdAt: 'desc' } }), prisma.attendee.count({ where: query ? { eventId, name: { contains: query, } } : { eventId, }, }) ]) return reply.send({ attendees: attendees.map(attendee => { return { id: attendee.id, name: attendee.name, email: attendee.email, createdAt: attendee.createdAt, checkedInAt: attendee.checkIn?.createdAt ?? null, } }), total, }) }) } ================================================ FILE: src/routes/get-event.ts ================================================ import { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import { z } from "zod"; import { prisma } from "../lib/prisma"; import { BadRequest } from "./_errors/bad-request"; export async function getEvent(app: FastifyInstance) { app .withTypeProvider() .get('/events/:eventId', { schema: { summary: 'Get an event', tags: ['events'], params: z.object({ eventId: z.string().uuid(), }), response: { 200: z.object({ event: z.object({ id: z.string().uuid(), title: z.string(), slug: z.string(), details: z.string().nullable(), maximumAttendees: z.number().int().nullable(), attendeesAmount: z.number().int(), }) }), }, } }, async (request, reply) => { const { eventId } = request.params const event = await prisma.event.findUnique({ select: { id: true, title: true, slug: true, details: true, maximumAttendees: true, _count: { select: { attendees: true, } }, }, where: { id: eventId, } }) if (event === null) { throw new BadRequest('Event not found.') } return reply.send({ event: { id: event.id, title: event.title, slug: event.slug, details: event.details, maximumAttendees: event.maximumAttendees, attendeesAmount: event._count.attendees, }, }) }) } ================================================ FILE: src/routes/register-for-event.ts ================================================ import { FastifyInstance } from "fastify"; import { ZodTypeProvider } from "fastify-type-provider-zod"; import { z } from "zod"; import { prisma } from "../lib/prisma"; import { BadRequest } from "./_errors/bad-request"; export async function registerForEvent(app: FastifyInstance) { app .withTypeProvider() .post('/events/:eventId/attendees', { schema: { summary: 'Register an attendee', tags: ['attendees'], body: z.object({ name: z.string().min(4), email: z.string().email(), }), params: z.object({ eventId: z.string().uuid(), }), response: { 201: z.object({ attendeeId: z.number(), }) } } }, async (request, reply) => { const { eventId } = request.params const { name, email } = request.body const attendeeFromEmail = await prisma.attendee.findUnique({ where: { eventId_email: { email, eventId } } }) if (attendeeFromEmail !== null) { throw new BadRequest('This e-mail is already registered for this event.') } const [event, amountOfAttendeesForEvent] = await Promise.all([ prisma.event.findUnique({ where: { id: eventId, } }), prisma.attendee.count({ where: { eventId, } }) ]) if (event?.maximumAttendees && amountOfAttendeesForEvent >= event.maximumAttendees) { throw new BadRequest('The maximum number of attendees for this event has been reached.') } const attendee = await prisma.attendee.create({ data: { name, email, eventId, } }) return reply.status(201).send({ attendeeId: attendee.id }) }) } ================================================ FILE: src/server.ts ================================================ import fastify from "fastify"; import fastifySwagger from "@fastify/swagger"; import fastifySwaggerUI from "@fastify/swagger-ui"; import fastifyCors from "@fastify/cors"; import { serializerCompiler, validatorCompiler, jsonSchemaTransform, ZodTypeProvider } from 'fastify-type-provider-zod' import { createEvent } from "./routes/create-event"; import { registerForEvent } from "./routes/register-for-event"; import { getEvent } from "./routes/get-event"; import { getAttendeeBadge } from "./routes/get-attendee-badge"; import { checkIn } from "./routes/check-in"; import { getEventAttendees } from "./routes/get-event-attendees"; import { errorHandler } from "./error-handler"; export const app = fastify().withTypeProvider() app.register(fastifyCors, { origin: '*', }) app.register(fastifySwagger, { swagger: { consumes: ['application/json'], produces: ['application/json'], info: { title: 'pass.in', description: 'Especificações da API para o back-end da aplicação pass.in construída durante o NLW Unite da Rocketseat.', version: '1.0.0' }, }, transform: jsonSchemaTransform, }) app.register(fastifySwaggerUI, { routePrefix: '/docs', }) app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler); app.register(createEvent) app.register(registerForEvent) app.register(getEvent) app.register(getAttendeeBadge) app.register(checkIn) app.register(getEventAttendees) app.setErrorHandler(errorHandler) app.listen({ port: 3333, host: '0.0.0.0' }).then(() => { console.log('HTTP server running!') }) ================================================ FILE: src/utils/generate-slug.ts ================================================ export function generateSlug(text: string): string { return text .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .toLowerCase() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-"); } ================================================ FILE: tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Node 20", "_version": "20.1.0", "compilerOptions": { "lib": ["es2023"], "module": "node16", "target": "es2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "node16" }, "include": ["src"] }