Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolução do desafio backend junior - Kaio Felipe Nink Cardoso #107

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bb58fc6
chore(database): define 'user' and 'post' tables into init.sql
kaio-nink Nov 16, 2024
271f68e
feat: define User and Post entities
kaio-nink Nov 17, 2024
6951e0a
refactor: move AppDataSource to a separate file
kaio-nink Nov 17, 2024
0443bcc
feat: get enviroment variable or a default value
kaio-nink Nov 17, 2024
3f1f645
feat: add user repository with 'create' and 'get' (by id) methods
kaio-nink Nov 17, 2024
75baa3d
feat: add post repository with 'create' method
kaio-nink Nov 17, 2024
aa078b2
Merge pull request #1 from kaio-nink/feat/database-setup
kaio-nink Nov 17, 2024
79943dd
feat(user-repository): add getByEmail method
kaio-nink Nov 17, 2024
20bea37
Merge pull request #2 from kaio-nink/feat/get-user-by-email
kaio-nink Nov 17, 2024
7419da9
feat(utils): add utility function validating if a string is empty
kaio-nink Nov 17, 2024
26fe478
feat(utils): add utility function validating if a string is an email
kaio-nink Nov 17, 2024
c09e1b6
feat: add userDTO
kaio-nink Nov 17, 2024
16d5b80
feat: add user service with with 'createUser' method
kaio-nink Nov 17, 2024
6052c03
feat: throw BadRequestError if an userDTO is invalid
kaio-nink Nov 17, 2024
66649c6
feat(user-service): throw BadRequestError if an email is already in u…
kaio-nink Nov 17, 2024
46d6ce8
Merge pull request #3 from kaio-nink/feat/add-user-service
kaio-nink Nov 17, 2024
d73dc27
feat: add PostDTO with required fields validation
kaio-nink Nov 17, 2024
ff5ae04
feat: add PostService with 'createPost' method
kaio-nink Nov 17, 2024
6db8106
feat(post-service): throw NotFoundError if the specified user is not …
kaio-nink Nov 17, 2024
b4c8a5d
Merge pull request #4 from kaio-nink/feat/add-post-service
kaio-nink Nov 17, 2024
e341ee3
feat: return an UserResponseDTO on creating an user
kaio-nink Nov 17, 2024
680efd9
feat: add UserController with 'store' method
kaio-nink Nov 17, 2024
ff876ac
feat: add a factory to make UserController
kaio-nink Nov 17, 2024
36741d8
feat: setup 'POST /users' endpoint
kaio-nink Nov 17, 2024
93e24db
feat: return a PostResponseDTO on creating a post
kaio-nink Nov 17, 2024
4e16108
feat: add PostController with 'store' method
kaio-nink Nov 17, 2024
326cf32
feat: add a factory to make PostController
kaio-nink Nov 17, 2024
f980b5a
feat: setup 'POST /posts' endpoint
kaio-nink Nov 17, 2024
a0b85fa
fix(controllers): return the correct status code in case of BadResque…
kaio-nink Nov 17, 2024
12917c4
Merge pull request #5 from kaio-nink/feat/add-user-and-post-endpoints
kaio-nink Nov 17, 2024
e27ca74
chore: setup the Dockerfile
kaio-nink Nov 17, 2024
f07decd
Merge pull request #6 from kaio-nink/chore/dockerfile-setup
kaio-nink Nov 17, 2024
5937484
Merge pull request #7 from kaio-nink/develop
kaio-nink Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
#TODO Configure o Dockerfile
FROM node:20

WORKDIR /app
COPY package*.json ./

RUN npm install
COPY . .

RUN npm run build
EXPOSE 3000

CMD ["npm", "run", "dev"]
17 changes: 15 additions & 2 deletions init.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
USE test_db;

--TODO Crie a tabela de user;
CREATE TABLE IF NOT EXISTS user (
id INT AUTO_INCREMENT,
firstName VARCHAR(100) NOT NULL,
lastName VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);

--TODO Crie a tabela de posts;
CREATE TABLE IF NOT EXISTS post (
id INT AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
description VARCHAR(100) NOT NULL,
userId INT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_user_post FOREIGN KEY (userId) REFERENCES user (id)
);
1 change: 1 addition & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getEnv = (name: string, defaultValue: string = '') => process.env[name] || defaultValue
24 changes: 24 additions & 0 deletions src/controllers/post-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request, Response } from "express";
import { PostDTO } from '../dtos/post-dto';
import { PostService } from '../services/post-service';
import { BadRequestError } from '../utils/errors/bad-request-error';
import { NotFoundError } from '../utils/errors/not-found-error';

export class PostController {
constructor(private postService: PostService) {}

async store(req: Request, res: Response) {
try {
const { title, description, userId } = req.body;
const postDTO = new PostDTO(title, description, userId)

const createdPost = await this.postService.createPost(postDTO);
res.status(201).json(createdPost)
} catch (error) {
if (error instanceof BadRequestError || error instanceof NotFoundError) return res.status(error.statusCode).send({ message: error.message })

console.error("Unexpected error:", error);
res.status(500).send({ message: "Internal Server Error" })
}
}
}
23 changes: 23 additions & 0 deletions src/controllers/user-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Request, Response } from "express";
import { UserDTO } from '../dtos/user-dto';
import { UserService } from '../services/user-service';
import { BadRequestError } from '../utils/errors/bad-request-error';

export class UserController {
constructor(private userService: UserService) {}

async store(req: Request, res: Response) {
try {
const { firstName, lastName, email } = req.body;
const userDTO = new UserDTO(firstName, lastName, email)

const createdUser = await this.userService.createUser(userDTO);
res.status(201).json(createdUser)
} catch (error) {
if (error instanceof BadRequestError) return res.status(error.statusCode).send({ message: error.message })

console.error("Unexpected error:", error);
res.status(500).send({ message: "Internal Server Error" })
}
}
}
17 changes: 17 additions & 0 deletions src/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { DataSource } from 'typeorm';
import { User } from './entity/User';
import { Post } from './entity/Post';
import { getEnv } from './config/env';

const AppDataSource = new DataSource({
type: 'mysql',
host: getEnv('DB_HOST', 'localhost'),
port: parseInt(getEnv('DB_PORT', '3306')),
username: getEnv('DB_USER', 'root'),
password: getEnv('DB_PASSWORD', 'password'),
database: getEnv('DB_NAME', 'test_db'),
entities: [User, Post],
synchronize: true,
});

export default AppDataSource;
22 changes: 22 additions & 0 deletions src/dtos/post-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BadRequestError } from '../utils/errors/bad-request-error';
import { isEmpty } from '../utils/string-util';

export class PostDTO {

constructor(
public title: string,
public description: string,
public userId: number
) {
this.validate(title, description, userId);

this.title = title;
this.description = description;
}

private validate(title: string, description: string, userId: number): void {
if (isEmpty(title)) throw new BadRequestError('Title is required')
if (isEmpty(description)) throw new BadRequestError('Description is required')
if (isNaN(userId)) throw new BadRequestError('User is required to create a post')
}
}
15 changes: 15 additions & 0 deletions src/dtos/post-response-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Post } from '../entity/Post';

export class PostResponseDTO {
id: number;
title: string;
description: string;
userId: number;

constructor(post: Post) {
this.id = post.id,
this.title = post.title,
this.description = post.description,
this.userId = post.user.id
}
}
23 changes: 23 additions & 0 deletions src/dtos/user-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BadRequestError } from '../utils/errors/bad-request-error';
import { isEmail, isEmpty } from '../utils/string-util';

export class UserDTO {

constructor(
public firstName: string,
public lastName: string,
public email: string
) {
this.validate(firstName, lastName, email);

this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}

private validate(firstName: string, lastName: string, email: string): void {
if (isEmpty(firstName)) throw new BadRequestError('First name is required')
if (isEmpty(lastName)) throw new BadRequestError('Last name is required')
if (isEmpty(email) || !isEmail(this.email)) throw new BadRequestError('Invalid email format')
}
}
15 changes: 15 additions & 0 deletions src/dtos/user-response-dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { User } from '../entity/User';

export class UserResponseDTO {
id: number;
firstName: string;
lastName: string;
email: string;

constructor(user: User) {
this.id = user.id,
this.firstName = user.firstName,
this.lastName = user.lastName,
this.email = user.email
}
}
19 changes: 17 additions & 2 deletions src/entity/Post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from "typeorm";
import { User } from './User';

//TODO Crie a entidade de Post
@Entity('post')
export class Post {
@PrimaryGeneratedColumn()
id!: number;

@Column({ type: 'varchar', length: 100 })
title!: string;

@Column({ type: 'varchar', length: 100 })
description!: string;

@ManyToOne(() => User, (user) => user.posts, { nullable: false, onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'fk_user_post' })
user!: User;
}
21 changes: 19 additions & 2 deletions src/entity/User.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Post } from './Post';

//TODO Crie a entidade de User
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id!: number;

@Column({ type: 'varchar', length: 100 })
firstName!: string;

@Column({ type: 'varchar', length: 100 })
lastName!: string;

@Column({ type: 'varchar', length: 100 })
email!: string;

@OneToMany(() => Post, (post) => post.user)
posts!: Post[];
}
13 changes: 13 additions & 0 deletions src/factories/post-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PostController } from '../controllers/post-controller';
import { PostRepository } from '../repositories/post-repository';
import { UserRepository } from '../repositories/user-repository';
import { PostService } from '../services/post-service';

const makePostController = (): PostController => {
const userRepository = new UserRepository();
const postRepository = new PostRepository();
const postService = new PostService(userRepository, postRepository);
return new PostController(postService);
};

export default makePostController;
11 changes: 11 additions & 0 deletions src/factories/user-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UserController } from '../controllers/user-controller';
import { UserRepository } from '../repositories/user-repository';
import { UserService } from '../services/user-service';

const makeUserController = (): UserController => {
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
return new UserController(userService);
};

export default makeUserController;
30 changes: 9 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import 'reflect-metadata';
import express from 'express';
import { DataSource } from 'typeorm';
import { User } from './entity/User';
import { Post } from './entity/Post';
import 'reflect-metadata';
import AppDataSource from './data-source';
import makeUserController from './factories/user-factory';
import makePostController from './factories/post-factory';

const app = express();
app.use(express.json());

const AppDataSource = new 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,Post],
synchronize: true,
});

const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const initializeDatabase = async () => {
Expand All @@ -33,13 +22,12 @@ const initializeDatabase = async () => {

initializeDatabase();

app.post('/users', async (req, res) => {
// Crie o endpoint de users
});
const userController = makeUserController()
const postController = makePostController();

app.post('/posts', async (req, res) => {
// Crie o endpoint de posts
});
app.post('/users', async (req, res) => userController.store(req, res));

app.post('/posts', async (req, res) => postController.store(req, res));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
Expand Down
11 changes: 11 additions & 0 deletions src/repositories/post-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import AppDataSource from '../data-source';
import { Post } from '../entity/Post';

export class PostRepository {
private repo = AppDataSource.getRepository(Post);

async create(data: Partial<Post>): Promise<Post> {
const createdPost = this.repo.create(data)
return this.repo.save(createdPost);
}
}
21 changes: 21 additions & 0 deletions src/repositories/user-repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import AppDataSource from '../data-source';
import { User } from '../entity/User';

export class UserRepository {
private repo = AppDataSource.getRepository(User);

async create(data: Partial<User>): Promise<User> {
const createdUser = this.repo.create(data)
return this.repo.save(createdUser);
}

async get(id: number): Promise<User | null> {
const user = this.repo.findOne({ where: { id } });
return user;
}

async getByEmail(email: string): Promise<User | null> {
const user = this.repo.findOne({ where: { email } });
return user;
}
}
18 changes: 18 additions & 0 deletions src/services/post-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PostDTO } from '../dtos/post-dto';
import { PostResponseDTO } from '../dtos/post-response-dto';
import { PostRepository } from '../repositories/post-repository';
import { UserRepository } from '../repositories/user-repository';
import { NotFoundError } from '../utils/errors/not-found-error';

export class PostService {
constructor(private userRepository: UserRepository, private postRepository: PostRepository) {}

async createPost(data: PostDTO) {
const { userId, ...post } = data;
const user = await this.userRepository.get(data.userId);
if (!user) throw new NotFoundError(`Couldn't find the specified user.`);

const createdPost = await this.postRepository.create({ ...post, user })
return new PostResponseDTO(createdPost);
}
}
16 changes: 16 additions & 0 deletions src/services/user-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { UserDTO } from '../dtos/user-dto';
import { UserResponseDTO } from '../dtos/user-response-dto';
import { UserRepository } from '../repositories/user-repository';
import { BadRequestError } from '../utils/errors/bad-request-error';

export class UserService {
constructor(private userRepository: UserRepository) {}

async createUser(user: UserDTO) {
const existingUser = await this.userRepository.getByEmail(user.email);
if (existingUser) throw new BadRequestError('This email is already in use.');

const createdUser = await this.userRepository.create(user);
return new UserResponseDTO(createdUser);
}
}
7 changes: 7 additions & 0 deletions src/utils/errors/bad-request-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class BadRequestError extends Error {
public readonly statusCode = 400;

constructor(message: string) {
super(message)
}
}
Loading