diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index baec7ba2..2cf430e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,15 @@ -#TODO Configure o Dockerfile +FROM node:lts-alpine + +WORKDIR /home/node/app + +COPY package*.json ./ + +RUN npm install + +USER node + +COPY --chown=node:node . . + +EXPOSE 3000 + +CMD [ "npm", "run", "start" ] \ No newline at end of file diff --git a/README.md b/README.md index 8d48b221..cb0f6723 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,95 @@ -# Descrição do Teste para a Vaga de Desenvolvedor Jr. - -## Contextualização do Desafio - -Este teste foi desenvolvido para avaliar suas habilidades práticas em tarefas comuns do dia a dia de um desenvolvedor júnior. Através deste desafio, você terá a oportunidade de demonstrar seu conhecimento na criação de banco de dados, definição de relacionamentos entre tabelas e entidades, além de aplicar boas práticas de desenvolvimento em um ambiente Docker. O objetivo é simular uma situação real de desenvolvimento de uma aplicação simples, onde você deverá criar as estruturas necessárias e garantir que o sistema esteja funcionando corretamente por meio de testes. A conclusão bem-sucedida desta tarefa refletirá seu domínio de conceitos importantes para a vaga. - -## 1º Passo: Criação das Tabelas no `init.sql` - -Dentro do arquivo `init.sql`, crie as seguintes tabelas: - -### Tabela `user` -- **id** – Tipo: `Int`, autoincremental, chave primária (PK). -- **firstName** – Tipo: `Varchar(100)`, não nulo. -- **lastName** – Tipo: `Varchar(100)`, não nulo. -- **email** – Tipo: `Varchar(100)`, não nulo. - -### Tabela `post` -- **id** – Tipo: `Int`, autoincremental, chave primária (PK). -- **title** – Tipo: `Varchar(100)`, não nulo. -- **description** – Tipo: `Varchar(100)`, não nulo. -- **userId** – Tipo: `Int`, não nulo (chave estrangeira referenciando a tabela `user`). - ---- - -## 2º Passo: Criação das Entidades `User` e `Post` - -Dentro da pasta `src/Entity`, crie as entidades correspondentes às tabelas `User` e `Post`. - ---- - -## 3º Passo: Configurar endpoints `users` e `posts` - -Dentro de `src/index.ts`, configure dois endpoints `users` & `posts` - ---- - -## 4º Passo: Configuração do Dockerfile - -Configure o `Dockerfile` da aplicação para garantir que ela seja construída corretamente no ambiente Docker. - ---- - -## 5º Passo: Teste da Aplicação - -Execute os seguintes comandos para testar a aplicação: - -1. **Subir a aplicação utilizando Docker Compose**: - ```bash - docker compose up --build - docker exec -it /bin/sh - - ``` - - Dentro do container, execute o teste: - ```bash - npm test - ``` - -## 6º Passo: Crie um fork desse repositório e submita o código preenchido nele. -Crie um Pull Request para a brach master nos enviando o código +# Dev Test API - Node.js + +## Table of Contents + +- [Overview](#overview) +- [Requirements](#requirements) +- [Features](#features) +- [Usage](#usage) + - [Run Server](#run-server) + - [Test](#test) +- [Endpoints](#endpoints) + +## Overview + +This is a [Node.js](https://nodejs.org/en) API project built with [TypeScript](https://www.typescriptlang.org/) and [Express](https://expressjs.com/), designed to manage **users**, **posts**, and their relationships. The API interacts with a [MySQL](https://www.mysql.com/) database using [TypeORM](http://typeorm.io/) for data management. Both the API and the database are containerized using [Docker](https://www.docker.com/). + +## Requirements + +For this project, the following (essential for execution) resources were used: + - [Node.js](https://nodejs.org/) + - [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) + +## Features + +- **Create a user**: A user could be created with `firstName`, `lastName` and `email`. The email field is unique. +- **Create a post**: A post could be created with `title`, `description` and `userId`. The `userId` establishes a relationship with a user. + +## Usage + +### Run Server + +Build and start containers. + +``` +docker-compose up --build -d +``` + +The server will run at port `3000` or the specified port. + +### Test + +First, access API: + +``` +docker exec -it dev_test-api-1 /bin/sh +``` + +And then, run test: + +``` +npm test +``` + +## Endpoints +- **POST** - `/users`: Create a new user + - Data should be sent in the request body in JSON format: + ``` + { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + } + ``` + - The return will be the created user object: + ``` + { + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "id": 1 + } + ``` +- **POST** - `/posts`: Create a new post + - Data should be sent in the request body in JSON format: + ``` + { + "title": "Some title", + "description": "some description", + "user": "1" + } + ``` + - The return will be the created post object: + ``` + { + "title": "Some title", + "description": "some description", + "user": { + "id": 1, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com" + }, + "id": 1 + } + ``` diff --git a/dist/db.test.js b/dist/db.test.js new file mode 100644 index 00000000..10a505e9 --- /dev/null +++ b/dist/db.test.js @@ -0,0 +1,42 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const axios_1 = __importDefault(require("axios")); +const testUser = { + firstName: "John", + lastName: "Doe", + email: "john.doe@example.com" +}; +let userId = null; +async function testCreateUser() { + try { + const response = await axios_1.default.post('http://localhost:3000/users', testUser); + userId = response.data.id; + console.log('User created successfully:', response.data); + } + catch (error) { + console.error('Error creating user:', error); + } +} +const testPost = { + title: "Some message", + description: "Some description", + userId: null +}; +async function testCreatePost() { + testPost.userId = userId; + try { + const response = await axios_1.default.post('http://localhost:3000/posts', testPost); + console.log('Post created successfully:', response.data); + } + catch (error) { + console.error('Error creating post:', error); + } +} +async function init() { + await testCreateUser(); + await testCreatePost(); +} +init(); diff --git a/dist/entity/Post.js b/dist/entity/Post.js new file mode 100644 index 00000000..7050e62b --- /dev/null +++ b/dist/entity/Post.js @@ -0,0 +1,44 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Post = void 0; +const typeorm_1 = require("typeorm"); +const User_1 = require("./User"); +let Post = class Post { + constructor(data) { + this.title = ''; + this.description = ''; + if (data) { + Object.assign(this, data); + } + } +}; +exports.Post = Post; +__decorate([ + (0, typeorm_1.PrimaryGeneratedColumn)(), + __metadata("design:type", Number) +], Post.prototype, "id", void 0); +__decorate([ + (0, typeorm_1.Column)(), + __metadata("design:type", String) +], Post.prototype, "title", void 0); +__decorate([ + (0, typeorm_1.Column)(), + __metadata("design:type", String) +], Post.prototype, "description", void 0); +__decorate([ + (0, typeorm_1.ManyToOne)(() => User_1.User, (user) => user.posts, { onDelete: 'CASCADE' }), + __metadata("design:type", User_1.User) +], Post.prototype, "user", void 0); +exports.Post = Post = __decorate([ + (0, typeorm_1.Entity)(), + __metadata("design:paramtypes", [Object]) +], Post); diff --git a/dist/entity/User.js b/dist/entity/User.js new file mode 100644 index 00000000..106bda8a --- /dev/null +++ b/dist/entity/User.js @@ -0,0 +1,49 @@ +"use strict"; +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +var __metadata = (this && this.__metadata) || function (k, v) { + if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.User = void 0; +const typeorm_1 = require("typeorm"); +const Post_1 = require("./Post"); +let User = class User { + constructor(data) { + this.firstName = ''; + this.lastName = ''; + this.email = ''; + if (data) { + Object.assign(this, data); + } + } +}; +exports.User = User; +__decorate([ + (0, typeorm_1.PrimaryGeneratedColumn)(), + __metadata("design:type", Number) +], User.prototype, "id", void 0); +__decorate([ + (0, typeorm_1.Column)(), + __metadata("design:type", String) +], User.prototype, "firstName", void 0); +__decorate([ + (0, typeorm_1.Column)(), + __metadata("design:type", String) +], User.prototype, "lastName", void 0); +__decorate([ + (0, typeorm_1.Column)({ unique: true }), + __metadata("design:type", String) +], User.prototype, "email", void 0); +__decorate([ + (0, typeorm_1.OneToMany)(() => Post_1.Post, (post) => post.user), + __metadata("design:type", Array) +], User.prototype, "posts", void 0); +exports.User = User = __decorate([ + (0, typeorm_1.Entity)(), + __metadata("design:paramtypes", [Object]) +], User); diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 00000000..03271203 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,77 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +require("dotenv/config"); +require("reflect-metadata"); +const express_1 = __importDefault(require("express")); +const typeorm_1 = require("typeorm"); +const User_1 = require("./entity/User"); +const Post_1 = require("./entity/Post"); +const zod_1 = require("zod"); +const app = (0, express_1.default)(); +app.use(express_1.default.json()); +const AppDataSource = new typeorm_1.DataSource({ + type: "mysql", + host: process.env.DB_HOST || "localhost", + port: 3306, + username: process.env.DB_USER || "root", + password: process.env.DB_PASSWORD || "password", + database: process.env.DB_NAME || "test_db", + entities: [User_1.User, Post_1.Post], + synchronize: true, + logging: true +}); +const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); +const initializeDatabase = async () => { + await wait(20000); + try { + await AppDataSource.initialize(); + console.log("Data Source has been initialized!"); + } + catch (err) { + console.error("Error during Data Source initialization:", err); + process.exit(1); + } +}; +initializeDatabase(); +app.post('/users', async (req, res) => { + const userBodySchema = zod_1.z.object({ + firstName: zod_1.z.string(), + lastName: zod_1.z.string(), + email: zod_1.z.string().email() + }); + const { firstName, lastName, email } = userBodySchema.parse(req.body); + const userRepository = AppDataSource.getRepository(User_1.User); + const user = new User_1.User(); + user.firstName = firstName; + user.lastName = lastName; + user.email = email; + const userCreated = await userRepository.save(user); + res.status(201).send(userCreated); +}); +app.post('/posts', async (req, res) => { + const postBodySchema = zod_1.z.object({ + title: zod_1.z.string(), + description: zod_1.z.string(), + userId: zod_1.z.number() + }); + const { title, description, userId } = postBodySchema.parse(req.body); + const postRepository = AppDataSource.getRepository(Post_1.Post); + const userRepository = AppDataSource.getRepository(User_1.User); + const user = await userRepository.findOne({ where: { id: userId } }); + if (!user) { + throw new Error('User not found'); + } + const post = new Post_1.Post(); + post.title = title; + post.description = description; + post.user = user; + const postCreated = await postRepository.save(post); + res.status(201).send(postCreated); +}); +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); diff --git a/docker-compose.yml b/docker-compose.yml index 2ac8411e..f0ca3571 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: - DB_NAME=test_db depends_on: - db + stdin_open: true + tty: true db: diff --git a/init.sql b/init.sql index e2c36c6f..44b15b75 100644 --- a/init.sql +++ b/init.sql @@ -1,5 +1,16 @@ USE test_db; ---TODO Crie a tabela de user; +CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL UNIQUE +); ---TODO Crie a tabela de posts; +CREATE TABLE posts ( + id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(100) NOT NULL, + description VARCHAR(100) NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index febfdc4f..bc9e3854 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,12 @@ "name": "node-starter", "dependencies": { "axios": "^1.5.0", + "dotenv": "^16.4.5", "express": "^4.18.2", "mysql2": "^3.6.1", "reflect-metadata": "^0.1.13", - "typeorm": "^0.3.17" + "typeorm": "^0.3.17", + "zod": "^3.23.8" }, "devDependencies": { "@types/express": "^4.17.17", @@ -816,6 +818,7 @@ "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2583,6 +2586,15 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 46cdba25..d724475b 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "test": "ts-node src/db.test.ts" }, "dependencies": { + "axios": "^1.5.0", + "dotenv": "^16.4.5", "express": "^4.18.2", "mysql2": "^3.6.1", - "typeorm": "^0.3.17", "reflect-metadata": "^0.1.13", - "axios": "^1.5.0" + "typeorm": "^0.3.17", + "zod": "^3.23.8" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/src/entity/Post.ts b/src/entity/Post.ts index a1f68038..3d664b66 100644 --- a/src/entity/Post.ts +++ b/src/entity/Post.ts @@ -1,3 +1,31 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm"; +import { User } from "./User"; //TODO Crie a entidade de Post +interface PostInterface { + id?: number + title: string + description: string + user: User +} + +@Entity() +export class Post { + @PrimaryGeneratedColumn() + id?: number + + @Column() + title: string = '' + + @Column() + description: string = '' + + @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE'}) + user!: User + + constructor(data?: PostInterface) { + if (data) { + Object.assign(this, data) + } + } +} \ No newline at end of file diff --git a/src/entity/User.ts b/src/entity/User.ts index a8e22632..9ad4c793 100644 --- a/src/entity/User.ts +++ b/src/entity/User.ts @@ -1,3 +1,35 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"; +import { Post } from "./Post"; //TODO Crie a entidade de User +interface UserInterface { + id?: number + firstName: string + lastName: string + email: string + posts?: Post[] +} + +@Entity() +export class User{ + @PrimaryGeneratedColumn() + id?: number + + @Column() + firstName: string = '' + + @Column() + lastName: string = '' + + @Column({ unique: true }) + email: string = '' + + @OneToMany(() => Post, (post) => post.user) + posts?: Post[] + + constructor(data?: UserInterface) { + if (data) { + Object.assign(this, data) + } + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1af52a84..ba064042 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ +import 'dotenv/config' import 'reflect-metadata'; import express from 'express'; import { DataSource } from 'typeorm'; import { User } from './entity/User'; import { Post } from './entity/Post'; +import { z } from 'zod'; const app = express(); app.use(express.json()); @@ -16,6 +18,7 @@ const AppDataSource = new DataSource({ database: process.env.DB_NAME || "test_db", entities: [User,Post], synchronize: true, + logging: true }); const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -34,11 +37,54 @@ const initializeDatabase = async () => { initializeDatabase(); app.post('/users', async (req, res) => { -// Crie o endpoint de users + const userBodySchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string().email() + }) + + const { firstName, lastName, email } = userBodySchema.parse(req.body) + + const userRepository = AppDataSource.getRepository(User) + + const user = new User() + + user.firstName = firstName + user.lastName = lastName + user.email = email + + const userCreated = await userRepository.save(user) + + res.status(201).send(userCreated) }); app.post('/posts', async (req, res) => { -// Crie o endpoint de posts + const postBodySchema = z.object({ + title: z.string(), + description: z.string(), + userId: z.number() + }) + + const { title, description, userId } = postBodySchema.parse(req.body) + + const postRepository = AppDataSource.getRepository(Post) + const userRepository = AppDataSource.getRepository(User) + + const user = await userRepository.findOne({ where: { id: userId }}) + + if (!user) { + throw new Error('User not found') + } + + const post = new Post() + + post.title = title + post.description = description + post.user = user + + const postCreated = await postRepository.save(post) + + res.status(201).send(postCreated) }); const PORT = process.env.PORT || 3000;