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
### 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"]
}