From f54805e95159d227ba95da0ad5dc19ef99773fb9 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Fri, 15 Nov 2024 16:11:06 -0300 Subject: [PATCH 01/14] create `init.sql` file --- init.sql | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/init.sql b/init.sql index e2c36c6f..a0fa8936 100644 --- a/init.sql +++ b/init.sql @@ -1,5 +1,18 @@ USE test_db; ---TODO Crie a tabela de user; +CREATE TABLE IF NOT EXISTS `user` ( + id int AUTO_INCREMENT NOT NULL, + 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 NOT NULL, + title varchar(100) NOT NULL, + description varchar(100) NOT NULL, + userId int NOT NULL, + PRIMARY KEY (id), + FOREIGN KEY (userId) REFERENCES `user`(id) +); \ No newline at end of file From 8bc66971d996e121427630c193753f53ad85fc5f Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Fri, 15 Nov 2024 16:11:32 -0300 Subject: [PATCH 02/14] create `Dockerfile` --- Dockerfile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Dockerfile b/Dockerfile index baec7ba2..5eacd6f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1 +1,14 @@ #TODO Configure o Dockerfile +FROM node:20-alpine AS build + +WORKDIR /usr/app + +COPY package.json package-lock.json* ./ + +RUN npm install + +COPY . . + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] \ No newline at end of file From a5ecfbdf86ed2eb6d04bdee38b66615a1a1dfa21 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Fri, 15 Nov 2024 16:11:59 -0300 Subject: [PATCH 03/14] create `User` entity file --- src/entity/User.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/entity/User.ts b/src/entity/User.ts index a8e22632..406ac08f 100644 --- a/src/entity/User.ts +++ b/src/entity/User.ts @@ -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() +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[] +} \ No newline at end of file From 9c3e44076a3549d4261613fe5ade6dce3cce2137 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Fri, 15 Nov 2024 16:12:10 -0300 Subject: [PATCH 04/14] create `Post` entity file --- src/entity/Post.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/entity/Post.ts b/src/entity/Post.ts index a1f68038..d7b2adca 100644 --- a/src/entity/Post.ts +++ b/src/entity/Post.ts @@ -1,3 +1,17 @@ -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"; +import { User } from "./User"; -//TODO Crie a entidade de Post +@Entity() +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) + user: User +} \ No newline at end of file From d01f3f7fc399c510a3b011fb74b97bc4ed534742 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:29:55 -0300 Subject: [PATCH 05/14] create `/users` and `/posts` routes --- src/routes/postRoutes.ts | 11 +++++++++++ src/routes/userRoutes.ts | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/routes/postRoutes.ts create mode 100644 src/routes/userRoutes.ts diff --git a/src/routes/postRoutes.ts b/src/routes/postRoutes.ts new file mode 100644 index 00000000..01dda21c --- /dev/null +++ b/src/routes/postRoutes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { Request, Response } from "express" +import { PostController } from "../controllers/PostController"; + +const postRoutes = Router() +const postController = new PostController(); + +postRoutes.post("/", (req: Request, res: Response) => + postController.createPost(req, res)) + +export default postRoutes; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts new file mode 100644 index 00000000..40f6cf6d --- /dev/null +++ b/src/routes/userRoutes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { Request, Response } from "express" +import { UserController } from "../controllers/UserController"; + +const userRoutes = Router(); +const userController = new UserController(); + +userRoutes.post("/", (req: Request, res: Response) => + userController.createUser(req, res)) + +export default userRoutes; \ No newline at end of file From 80195ca0a4fdfbb915ffc27e980592e2b8daf335 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:30:32 -0300 Subject: [PATCH 06/14] create controllers for `Posts` and `Users` --- src/controllers/PostController.ts | 18 ++++++++++++++++++ src/controllers/UserController.ts | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/controllers/PostController.ts create mode 100644 src/controllers/UserController.ts diff --git a/src/controllers/PostController.ts b/src/controllers/PostController.ts new file mode 100644 index 00000000..fe9f5d45 --- /dev/null +++ b/src/controllers/PostController.ts @@ -0,0 +1,18 @@ +import { PostService } from "../services/PostService"; +import { Request, Response } from "express" + +export class PostController { + private postService = new PostService() + + async createPost(req: Request, res: Response) { + const { title, description, userId } = req.body; + + try { + const createPost = await this.postService.createPost(title, description, userId) + return res.status(201).json(createPost) + } catch (error) { + console.error("Error while creating post: ", error) + return res.status(500).json({ message: "Internal Server Error"}) + } + } +} \ No newline at end of file diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts new file mode 100644 index 00000000..09db8ab0 --- /dev/null +++ b/src/controllers/UserController.ts @@ -0,0 +1,18 @@ +import { UserService } from "../services/UserService"; +import { Request, Response } from "express" + +export class UserController { + private userService = new UserService() + + async createUser(req: Request, res: Response) { + const { firstName, lastName, email } = req.body; + + try { + const createUser = await this.userService.createUser(firstName, lastName, email) + return res.status(201).json(createUser) + } catch(error) { + console.error("Error while creating user: ", error) + return res.status(500).json({ message: "Internal Server Error"}) + } + } +} \ No newline at end of file From 05bb1a95dbaab33b968537f41faa5a50326566c6 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:32:01 -0300 Subject: [PATCH 07/14] create services for `User` and `Posts` --- src/services/PostService.ts | 13 +++++++++++++ src/services/UserService.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/services/PostService.ts create mode 100644 src/services/UserService.ts diff --git a/src/services/PostService.ts b/src/services/PostService.ts new file mode 100644 index 00000000..6729fb67 --- /dev/null +++ b/src/services/PostService.ts @@ -0,0 +1,13 @@ +import { Post } from "../entity/Post"; +import { User } from "../entity/User"; +import database from "../infra/db"; + +export class PostService { + private PostRepository = database.AppDataSource.getRepository(Post) + + async createPost(title: string, description: string, userId: number) : Promise { + const post = this.PostRepository.create({ title, description, userId }) + + return await this.PostRepository.save(post); + } +} \ No newline at end of file diff --git a/src/services/UserService.ts b/src/services/UserService.ts new file mode 100644 index 00000000..5a36faef --- /dev/null +++ b/src/services/UserService.ts @@ -0,0 +1,12 @@ +import { User } from "../entity/User"; +import database from "../infra/db"; + +export class UserService { + private userRepository = database.AppDataSource.getRepository(User) + + async createUser(firstName: string, lastName: string, email: string) : Promise { + const user = this.userRepository.create({ firstName, lastName, email}) + + return await this.userRepository.save(user); + } +} \ No newline at end of file From c02188ed3bb9779708998720afec2c27dcc4c8bc Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:32:39 -0300 Subject: [PATCH 08/14] create `/infra` folder to handle database initialization --- src/infra/db.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/infra/db.ts diff --git a/src/infra/db.ts b/src/infra/db.ts new file mode 100644 index 00000000..309cee37 --- /dev/null +++ b/src/infra/db.ts @@ -0,0 +1,35 @@ +import { DataSource } from 'typeorm'; +import { User } from '../entity/User'; +import { Post } from '../entity/Post'; + +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], + logging: true, + synchronize: true, +}); + +const wait = (ms: number) => 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); + } +}; + +const database = { + AppDataSource, + initializeDatabase, +} + +export default database; \ No newline at end of file From cb0f3c8c59d6e7eceaf8aec429324e3fe8667da2 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:33:17 -0300 Subject: [PATCH 09/14] use routes and adjust server start --- src/index.ts | 53 +++++++++++++++++---------------------------------- tsconfig.json | 1 + 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1af52a84..da91a17e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,47 +1,30 @@ import 'reflect-metadata'; import express from 'express'; -import { DataSource } from 'typeorm'; -import { User } from './entity/User'; -import { Post } from './entity/Post'; +import database from './infra/db'; +import userRoutes from './routes/userRoutes'; +import postRoutes from './routes/postRoutes'; 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 startServer = async () => { + try { + await database.initializeDatabase(); + console.log('Database initialized'); -const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const PORT = process.env.PORT || 3000; -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); + app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); + }); + } catch (error) { + console.error('Error initializing the database:', error); process.exit(1); - } -}; - -initializeDatabase(); + }; +} -app.post('/users', async (req, res) => { -// Crie o endpoint de users -}); +startServer(); -app.post('/posts', async (req, res) => { -// Crie o endpoint de posts -}); +app.use("/users", userRoutes) +app.use("/posts", postRoutes) -const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); -}); diff --git a/tsconfig.json b/tsconfig.json index cb1c9da8..64afdc7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "esModuleInterop": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, + "strictPropertyInitialization": false, "skipLibCheck": true }, "include": ["src/**/*", "src/db.test.ts"], From 06ba92d85d42ada7e1468ed4c507213a443231ac Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:34:06 -0300 Subject: [PATCH 10/14] adjust `Post` entity adding `userId` column --- src/entity/Post.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/entity/Post.ts b/src/entity/Post.ts index d7b2adca..3322bd57 100644 --- a/src/entity/Post.ts +++ b/src/entity/Post.ts @@ -12,6 +12,9 @@ export class Post { @Column({ type: "varchar", length: 100 }) description: string - @ManyToOne(() => User, (user) => user.posts) + @ManyToOne(() => User, (user) => user.posts, { nullable: false }) user: User + + @Column() + userId: number } \ No newline at end of file From dc434cc6eac3f1de2909f66660dc14ed5610a5fb Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sat, 16 Nov 2024 14:34:23 -0300 Subject: [PATCH 11/14] create `.gitignore` file --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore 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 From 38e2312f681bc711a0ff8bc7ec47acb7b017ff59 Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sun, 17 Nov 2024 16:51:12 -0300 Subject: [PATCH 12/14] add `Zod` to validate body schema --- package-lock.json | 12 +++++++++++- package.json | 5 +++-- src/middlewares/validationMiddleware.ts | 20 ++++++++++++++++++++ src/schemas/postSchema.ts | 7 +++++++ src/schemas/userSchema.ts | 7 +++++++ 5 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 src/middlewares/validationMiddleware.ts create mode 100644 src/schemas/postSchema.ts create mode 100644 src/schemas/userSchema.ts diff --git a/package-lock.json b/package-lock.json index febfdc4f..768d64f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "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", @@ -2583,6 +2584,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..3164ccf0 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,12 @@ "test": "ts-node src/db.test.ts" }, "dependencies": { + "axios": "^1.5.0", "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/middlewares/validationMiddleware.ts b/src/middlewares/validationMiddleware.ts new file mode 100644 index 00000000..4ab6acbf --- /dev/null +++ b/src/middlewares/validationMiddleware.ts @@ -0,0 +1,20 @@ +import { z, ZodError, ZodIssue } from "zod"; +import { Request, Response, NextFunction } from "express"; + +export function validateFields(schema: z.ZodObject) { + return (req: Request, res: Response, next: NextFunction) => { + try { + schema.parse(req.body) + next() + } catch (error) { + if (error instanceof ZodError) { + const errorformatted = error.flatten((issue: ZodIssue) => ({ + message: issue.message, + })); + res.status(400).json({ error: "Invalid data", details: errorformatted.fieldErrors}) + } else { + res.status(500).json(({ message: "Internal Server Error"})) + } + } + } +} \ No newline at end of file diff --git a/src/schemas/postSchema.ts b/src/schemas/postSchema.ts new file mode 100644 index 00000000..11a3b751 --- /dev/null +++ b/src/schemas/postSchema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const postSchemaValidation = z.object({ + title: z.string().min(1).max(100), + description: z.string().min(1).max(100), + userId: z.number().int().positive().min(1), +}) \ No newline at end of file diff --git a/src/schemas/userSchema.ts b/src/schemas/userSchema.ts new file mode 100644 index 00000000..cd9d6391 --- /dev/null +++ b/src/schemas/userSchema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const userSchemaValidation = z.object({ + firstName: z.string().min(1).max(100), + lastName: z.string().min(1).max(100), + email: z.string().email(), +}) \ No newline at end of file From 9bd483c32e9f63c1f4e5636b2e15ec0663b59bcc Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sun, 17 Nov 2024 16:51:41 -0300 Subject: [PATCH 13/14] add validation middleware to routes --- src/routes/postRoutes.ts | 8 ++++++-- src/routes/userRoutes.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/routes/postRoutes.ts b/src/routes/postRoutes.ts index 01dda21c..cad904b1 100644 --- a/src/routes/postRoutes.ts +++ b/src/routes/postRoutes.ts @@ -1,11 +1,15 @@ import { Router } from "express"; import { Request, Response } from "express" import { PostController } from "../controllers/PostController"; +import { validateFields } from "../middlewares/validationMiddleware"; +import { postSchemaValidation } from "../schemas/postSchema"; const postRoutes = Router() const postController = new PostController(); -postRoutes.post("/", (req: Request, res: Response) => - postController.createPost(req, res)) +postRoutes.post("/", + validateFields(postSchemaValidation), (req: Request, res: Response) => + postController.createPost(req, res) +) export default postRoutes; \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 40f6cf6d..f93f6b57 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,11 +1,15 @@ import { Router } from "express"; import { Request, Response } from "express" import { UserController } from "../controllers/UserController"; +import { validateFields } from "../middlewares/validationMiddleware"; +import { userSchemaValidation } from "../schemas/userSchema"; const userRoutes = Router(); const userController = new UserController(); -userRoutes.post("/", (req: Request, res: Response) => - userController.createUser(req, res)) +userRoutes.post("/", + validateFields(userSchemaValidation), (req: Request, res: Response) => + userController.createUser(req, res) +) export default userRoutes; \ No newline at end of file From df09e35eefb0749a84639e71c1794db2a0eb2bfc Mon Sep 17 00:00:00 2001 From: Evandro Costa Date: Sun, 17 Nov 2024 16:52:16 -0300 Subject: [PATCH 14/14] add health check and dependency to docker compose file --- docker-compose.yml | 9 ++++++++- src/index.ts | 15 +++++++-------- src/infra/db.ts | 1 - 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2ac8411e..35a82757 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,8 @@ services: - DB_PASSWORD=password - DB_NAME=test_db depends_on: - - db + db: + condition: service_healthy db: @@ -24,6 +25,12 @@ services: volumes: - mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"] + interval: 5s + timeout: 30s + retries: 5 + start_period: 0s volumes: mysql-data: diff --git a/src/index.ts b/src/index.ts index da91a17e..9389b0d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,24 +7,23 @@ import postRoutes from './routes/postRoutes'; const app = express(); app.use(express.json()); -const startServer = async () => { +const waitForDatabase = async () => { try { await database.initializeDatabase(); console.log('Database initialized'); - - const PORT = process.env.PORT || 3000; - - app.listen(PORT, () => { - console.log(`Server is running on port ${PORT}`); - }); } catch (error) { console.error('Error initializing the database:', error); process.exit(1); }; } -startServer(); +waitForDatabase(); app.use("/users", userRoutes) app.use("/posts", postRoutes) +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`Server is running on port ${PORT}`); +}); \ No newline at end of file diff --git a/src/infra/db.ts b/src/infra/db.ts index 309cee37..69079619 100644 --- a/src/infra/db.ts +++ b/src/infra/db.ts @@ -17,7 +17,6 @@ const AppDataSource = new DataSource({ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const initializeDatabase = async () => { - await wait(20000); try { await AppDataSource.initialize(); console.log("Data Source has been initialized!");