From 1aeb2c55da4c2bc312d982eb5c39908bab9d8f11 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Fri, 2 Feb 2024 12:22:57 -0300 Subject: [PATCH 01/11] Adding SECRET_ACCESS_KEY as env vars --- .env.example | 8 +++++--- src/env/index.ts | 4 +++- src/use-cases/addImageToProjectUseCase.ts | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 9562e1c..30bd5c6 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ FRONTEND_URL="http://127.0.0.1:5173" # http://localhost:5173 STORAGE_TYPE = "local" AWS_S3_URL = "" -REGION = '', -ACCESS_KEY_ID ='', -BUCKET_NAME = "" \ No newline at end of file +REGION = '' +ACCESS_KEY_ID ='' +SECRET_ACCESS_KEY = '' +BUCKET_NAME = "" + \ No newline at end of file diff --git a/src/env/index.ts b/src/env/index.ts index b716251..349debb 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -12,6 +12,7 @@ const envSchema = z.object({ REGION: z.string().optional(), ACCESS_KEY_ID: z.string().optional(), BUCKET_NAME: z.string().optional(), + SECRET_ACCESS_KEY: z.string().optional(), }) const _env = envSchema.safeParse(process.env) @@ -27,7 +28,8 @@ if (_env.data.STORAGE_TYPE === 's3') { _env.data.AWS_S3_URL === undefined || _env.data.REGION === undefined || _env.data.ACCESS_KEY_ID === undefined || - _env.data.BUCKET_NAME === undefined + _env.data.BUCKET_NAME === undefined || + _env.data.SECRET_ACCESS_KEY === undefined ) { console.error('❌❌❌ Invalid environment variables') diff --git a/src/use-cases/addImageToProjectUseCase.ts b/src/use-cases/addImageToProjectUseCase.ts index 4376041..454cef6 100644 --- a/src/use-cases/addImageToProjectUseCase.ts +++ b/src/use-cases/addImageToProjectUseCase.ts @@ -47,6 +47,7 @@ export class AddImageToProjectUseCase { { region: env.REGION, accessKeyId: env.ACCESS_KEY_ID, + secretAccessKey: env.SECRET_ACCESS_KEY, }, ]) From d0d18e3fa1bc29c7dacafd74e18fff2aad2706f7 Mon Sep 17 00:00:00 2001 From: pedrodecf Date: Fri, 2 Feb 2024 15:50:28 -0300 Subject: [PATCH 02/11] UseCase to delete projects with controller, tests and route --- .../project/deleteProjectById.spec.ts | 68 +++++++++++++++++++ src/controller/project/deleteProjectById.ts | 25 +++++++ src/controller/project/routes.ts | 2 + .../in-memory-db/inMemoryProjectRepository.ts | 10 +++ .../prisma/prisma-project-repository.ts | 10 +++ src/repositories/project-repository.ts | 1 + src/server.ts | 2 + .../project/deleteProjectByIdUseCase.spec.ts | 62 +++++++++++++++++ .../project/deleteProjectByIdUseCase.ts | 18 +++++ 9 files changed, 198 insertions(+) create mode 100644 src/controller/project/deleteProjectById.spec.ts create mode 100644 src/controller/project/deleteProjectById.ts create mode 100644 src/use-cases/project/deleteProjectByIdUseCase.spec.ts create mode 100644 src/use-cases/project/deleteProjectByIdUseCase.ts diff --git a/src/controller/project/deleteProjectById.spec.ts b/src/controller/project/deleteProjectById.spec.ts new file mode 100644 index 0000000..a83df5b --- /dev/null +++ b/src/controller/project/deleteProjectById.spec.ts @@ -0,0 +1,68 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest" +import request from 'supertest' +import { ProjectRepository } from '../../repositories/project-repository' +import { PrismaProjectRepository } from '../../repositories/prisma/prisma-project-repository' +import { PrismaUsersRepository } from '../../repositories/prisma/prisma-users-repository' +import { UserRepository } from '../../repositories/user-repository' +import { app } from "../../app" +import { randomUUID } from "crypto" + +let projectRepository: ProjectRepository +let userRepository: UserRepository + +describe('Delete Project By ID E2E', () => { + beforeAll(async () => { + projectRepository = new PrismaProjectRepository() + userRepository = new PrismaUsersRepository() + + await app.ready() + }) + + afterAll(async () => { + await app.close() + }) + + it('should be able to delete a project by ID', async () => { + const description = 'ReactProject' + const link = 'www.google.com.br' + const tags = ['react', 'node'] + const title = 'ReactProject' + + const newUser = await userRepository.create({ + email: 'john_doe@email.com', + name: 'John', + surname: 'Doe', + password_hash: 'password', + }) + + const project = await projectRepository.create({ + description, + link, + tags, + title, + user_id: newUser.id, + }) + + const deletedProjectByIdResponse = await request(app.server).delete( + `/project/${project.id}`, + ) + + expect(deletedProjectByIdResponse.statusCode).toEqual(200) + expect(deletedProjectByIdResponse.body).toEqual({}) + }) + + it('should not be able to delete a project by ID that does not exist', async () => { + + const deletedProjectByIdResponse = await request(app.server).delete( + `/project/${randomUUID()}`, + ) + + expect(deletedProjectByIdResponse.statusCode).toEqual(404) + expect(deletedProjectByIdResponse.body).toEqual( + expect.objectContaining({ + error: 'Unable to delete project !', + }), + ) + }) + +}) \ No newline at end of file diff --git a/src/controller/project/deleteProjectById.ts b/src/controller/project/deleteProjectById.ts new file mode 100644 index 0000000..484101d --- /dev/null +++ b/src/controller/project/deleteProjectById.ts @@ -0,0 +1,25 @@ +import { FastifyReply, FastifyRequest } from "fastify" +import { PrismaProjectRepository } from "../../repositories/prisma/prisma-project-repository" +import { DeleteProjectByIdUseCase } from "../../use-cases/project/deleteProjectByIdUseCase" +import { z } from "zod" + +export async function deleteProjectById( + request: FastifyRequest, + response: FastifyReply, +) { + const projectRepository = new PrismaProjectRepository() + const deleteProjectByIdUseCase = new DeleteProjectByIdUseCase(projectRepository) + + const DeleteProjectByIdParamsSchema = z.object({ + projectId: z.string().uuid(), + }) + + const { projectId } = DeleteProjectByIdParamsSchema.parse(request.params) + + try { + await deleteProjectByIdUseCase.execute({ projectId }) + return response.status(200).send() + } catch (error) { + return response.status(404).send({ error: 'Unable to delete project !'}) + } +} \ No newline at end of file diff --git a/src/controller/project/routes.ts b/src/controller/project/routes.ts index c9d58e0..90f10ce 100644 --- a/src/controller/project/routes.ts +++ b/src/controller/project/routes.ts @@ -8,6 +8,7 @@ import path from 'path' import fastifyStatic from '@fastify/static' import { getProjectsByTags } from './getProjectsByTags' import { editProject } from './editProjectById' +import { deleteProjectById } from './deleteProjectById' export async function projectRoutes(app: FastifyInstance) { app.register(FastifyMultipart, { @@ -30,4 +31,5 @@ export async function projectRoutes(app: FastifyInstance) { app.post('/user/:userId/project', createProject) app.put('/project/:projectId/edit', editProject) + app.delete('/project/:projectId', deleteProjectById) } diff --git a/src/repositories/in-memory-db/inMemoryProjectRepository.ts b/src/repositories/in-memory-db/inMemoryProjectRepository.ts index 7cf6423..aeda533 100644 --- a/src/repositories/in-memory-db/inMemoryProjectRepository.ts +++ b/src/repositories/in-memory-db/inMemoryProjectRepository.ts @@ -55,6 +55,16 @@ export class InMemoryProjectRepository implements ProjectRepository { async addPhotoUrl(projectId: string, photoUrl: string): Promise { throw new Error('Method not implemented.') } + + async deleteProjectByID(projectId: string): Promise { + const index = this.dbProject.findIndex((project) => project.id === projectId); + + if (index !== -1) { + this.dbProject.splice(index, 1); + } + + return + } async fetchProjectByTags(tags: string[]): Promise { const projects = this.dbProject.filter((project) => diff --git a/src/repositories/prisma/prisma-project-repository.ts b/src/repositories/prisma/prisma-project-repository.ts index 3e3c1a0..24fc4c8 100644 --- a/src/repositories/prisma/prisma-project-repository.ts +++ b/src/repositories/prisma/prisma-project-repository.ts @@ -56,6 +56,16 @@ export class PrismaProjectRepository implements ProjectRepository { return project } + async deleteProjectByID(projectId: string):Promise { + await prisma.project.delete({ + where: { + id: projectId + } + }) + + return + } + async edit(data: Prisma.ProjectUncheckedCreateInput): Promise { const project = await prisma.project.update({ diff --git a/src/repositories/project-repository.ts b/src/repositories/project-repository.ts index 6074c99..6796689 100644 --- a/src/repositories/project-repository.ts +++ b/src/repositories/project-repository.ts @@ -7,4 +7,5 @@ export interface ProjectRepository { addPhotoUrl(projectId: string, photoUrl: string): Promise fetchProjectByTags(tags: string[]): Promise edit(data: Prisma.ProjectUncheckedCreateInput): Promise + deleteProjectByID(projectID: string): Promise } diff --git a/src/server.ts b/src/server.ts index 833034b..4b7ca23 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,8 @@ import { app } from './app' import { env } from './env' +console.log(process.env) + app .listen({ port: env.PORT, diff --git a/src/use-cases/project/deleteProjectByIdUseCase.spec.ts b/src/use-cases/project/deleteProjectByIdUseCase.spec.ts new file mode 100644 index 0000000..69bb540 --- /dev/null +++ b/src/use-cases/project/deleteProjectByIdUseCase.spec.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, beforeEach } from "vitest"; +import { ProjectRepository } from "../../repositories/project-repository"; +import { DeleteProjectByIdUseCase } from "./deleteProjectByIdUseCase"; +import { InMemoryProjectRepository } from "../../repositories/in-memory-db/inMemoryProjectRepository"; +import { GetProjectsByUserIdUseCase } from "./getProjectsByUserIdUseCase"; +import { UserRepository } from "../../repositories/user-repository"; +import { InMemoryUserRepository } from "../../repositories/in-memory-db/inMemoryUserRepository"; + +let userRepository: UserRepository +let projectRepository: ProjectRepository +let deleteProjectByIdUseCase: DeleteProjectByIdUseCase +let getProjectsByUserIdUseCase: GetProjectsByUserIdUseCase + +describe('Delete Project By Id Use Case', () => { + beforeEach(() => { + userRepository = new InMemoryUserRepository() + projectRepository = new InMemoryProjectRepository() + deleteProjectByIdUseCase = new DeleteProjectByIdUseCase(projectRepository) + getProjectsByUserIdUseCase = new GetProjectsByUserIdUseCase( + projectRepository, + userRepository, + ) + }) + + it('should be able delete project by ID', async () => { + const newUser = await userRepository.create({ + name: 'Matheus', + surname: 'Sanchez', + email: 'exemplo@gmail.com', + password_hash: '123456', + }) + + const firstProject = await projectRepository.create({ + title: 'React Typescript 1', + description: 'Best Project', + tags: ['react', 'node'], + link: 'https://github.com/luiseduardo3/nodets-petcanil', + user_id: newUser.id, + }) + + const secondProject = await projectRepository.create({ + title: 'React Typescript 2', + description: 'Best Project 2', + tags: ['react', 'figma'], + link: 'https://www.linkedin.com/in/pedrodecf/', + user_id: newUser.id, + }) + + const deletedProject = await deleteProjectByIdUseCase.execute({ projectId: firstProject.id }) + const { projects } = await getProjectsByUserIdUseCase.execute({ userId: newUser.id }) + + expect(deletedProject).toEqual(undefined) + expect(secondProject).toEqual( + expect.objectContaining({ + title: 'React Typescript 2', + description: 'Best Project 2', + }), + ) + + expect(projects).toHaveLength(1) + }) +}) \ No newline at end of file diff --git a/src/use-cases/project/deleteProjectByIdUseCase.ts b/src/use-cases/project/deleteProjectByIdUseCase.ts new file mode 100644 index 0000000..b8c7413 --- /dev/null +++ b/src/use-cases/project/deleteProjectByIdUseCase.ts @@ -0,0 +1,18 @@ +import { ProjectRepository } from "../../repositories/project-repository"; + +interface DeleteProjectByIdRequest { + projectId: string +} + +export class DeleteProjectByIdUseCase{ + constructor ( + private projectRepository: ProjectRepository, + ) {} + + async execute({ + projectId, + }: DeleteProjectByIdRequest): Promise{ + await this.projectRepository.deleteProjectByID(projectId) + return + } +} \ No newline at end of file From 8bf985f989c25637e3d760193841790d951ad360 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Fri, 2 Feb 2024 17:51:29 -0300 Subject: [PATCH 03/11] Fixing prod error, missing region. Add debug script --- package.json | 8 +- pnpm-lock.yaml | 179 +++++++++++++++++- src/controller/project/addImageToProject.ts | 2 +- src/tmp/uploads/.gitkeep | 0 .../{ => project}/addImageToProjectUseCase.ts | 19 +- 5 files changed, 186 insertions(+), 22 deletions(-) delete mode 100644 src/tmp/uploads/.gitkeep rename src/use-cases/{ => project}/addImageToProjectUseCase.ts (81%) diff --git a/package.json b/package.json index 82eba19..02df37b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "test:e2e": "vitest run --dir src/controller", "test:e2e:coverage": "vitest run --dir src/controller --coverage", "test:e2e:watch": "vitest --dir src/controller", - "test:coverage": "vitest run --coverage" + "test:coverage": "vitest run --coverage", + "debug": "ts-node-dev --respawn --transpile-only src/server.ts --debug" }, "keywords": [], "author": "", @@ -25,16 +26,17 @@ "devDependencies": { "@rocketseat/eslint-config": "^2.1.0", "@types/node": "^20.11.6", + "@types/pump": "^1.1.3", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.2.1", "eslint": "8.52.0", "npm-run-all": "^4.1.5", "prisma": "5.8.1", "supertest": "^6.3.4", + "ts-node-dev": "^2.0.0", "tsx": "^4.7.0", "typescript": "^5.3.3", - "vitest": "^1.2.1", - "@types/pump": "^1.1.3" + "vitest": "^1.2.1" }, "dependencies": { "@aws-sdk/client-s3": "^3.502.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fc09b5d..1e52066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ devDependencies: supertest: specifier: ^6.3.4 version: 6.3.4 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.6)(typescript@5.3.3) tsx: specifier: ^4.7.0 version: 4.7.0 @@ -762,6 +765,13 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@esbuild/aix-ppc64@0.19.12: resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} engines: {node: '>=12'} @@ -1135,6 +1145,13 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@lukeed/ms@2.0.2: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} @@ -1790,6 +1807,22 @@ packages: tslib: 2.6.2 dev: false + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/bcryptjs@2.4.6: resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} dev: false @@ -1898,6 +1931,14 @@ packages: '@types/node': 20.11.6 dev: false + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: true + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: true + /@types/superagent@8.1.3: resolution: {integrity: sha512-R/CfN6w2XsixLb1Ii8INfn+BT9sGPvw74OavfkW4SwY+jeUcAwLZv2+bXLJkndnimxjEBm0RPHgcjW9pLCa8cw==} dependencies: @@ -2210,12 +2251,15 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: false /archy@1.0.0: resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} dev: false + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -2378,13 +2422,11 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - dev: false /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} dev: false - /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} dev: false @@ -2408,6 +2450,10 @@ packages: dependencies: fill-range: 7.0.1 + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /buffer-writer@2.0.0: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} @@ -2502,7 +2548,6 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: false /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2563,6 +2608,10 @@ packages: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} dev: true + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /cross-spawn@6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -2664,6 +2713,11 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2689,6 +2743,12 @@ packages: engines: {node: '>=12'} dev: false + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: false @@ -3765,7 +3825,6 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 - dev: false /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -4160,6 +4219,10 @@ packages: semver: 7.5.4 dev: true + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + /memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -4251,6 +4314,12 @@ packages: engines: {node: '>=16 || 14 >=14.17'} dev: false + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + /mlly@1.5.0: resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} dependencies: @@ -4317,7 +4386,6 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: false /npm-run-all@4.1.5: resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} @@ -4849,7 +4917,6 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: false /real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} @@ -4936,6 +5003,13 @@ packages: resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} dev: false + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -5118,6 +5192,13 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -5285,6 +5366,11 @@ packages: engines: {node: '>=12'} dev: true + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -5449,12 +5535,70 @@ packages: /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - dev: false /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: false + /ts-node-dev@2.0.0(@types/node@20.11.6)(typescript@5.3.3): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.5.3 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.8 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.11.6)(typescript@5.3.3) + tsconfig: 7.0.0 + typescript: 5.3.3 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: true + + /ts-node@10.9.2(@types/node@20.11.6)(typescript@5.3.3): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.11.6 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.3.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + /tsconfck@3.0.1(typescript@5.3.3): resolution: {integrity: sha512-7ppiBlF3UEddCLeI1JRx5m2Ryq+xk4JrZuq4EuYXykipebaq1dV0Fhgr1hb7CkmHt32QSgOZlcqVLEtHBG4/mg==} engines: {node: ^18 || >=20} @@ -5477,6 +5621,15 @@ packages: strip-bom: 3.0.0 dev: true + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} @@ -5633,6 +5786,10 @@ packages: hasBin: true dev: false + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /v8-to-istanbul@9.2.0: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} @@ -5886,7 +6043,6 @@ packages: /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - dev: false /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -5896,6 +6052,11 @@ packages: engines: {node: '>= 14'} dev: false + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/controller/project/addImageToProject.ts b/src/controller/project/addImageToProject.ts index 622af32..fdac8e9 100644 --- a/src/controller/project/addImageToProject.ts +++ b/src/controller/project/addImageToProject.ts @@ -1,7 +1,7 @@ import { FastifyReply, FastifyRequest } from 'fastify' import { z } from 'zod' import { PrismaProjectRepository } from '../../repositories/prisma/prisma-project-repository' -import { AddImageToProjectUseCase } from '../../use-cases/addImageToProjectUseCase' +import { AddImageToProjectUseCase } from '../../use-cases/project/addImageToProjectUseCase' import { ResourceNotFoundError } from '../../use-cases/errors/ResourceNotFoundError' import { AwsS3Error } from '../../use-cases/errors/AwsS3Error' diff --git a/src/tmp/uploads/.gitkeep b/src/tmp/uploads/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/use-cases/addImageToProjectUseCase.ts b/src/use-cases/project/addImageToProjectUseCase.ts similarity index 81% rename from src/use-cases/addImageToProjectUseCase.ts rename to src/use-cases/project/addImageToProjectUseCase.ts index 454cef6..03ea92a 100644 --- a/src/use-cases/addImageToProjectUseCase.ts +++ b/src/use-cases/project/addImageToProjectUseCase.ts @@ -1,15 +1,15 @@ import { Project } from '@prisma/client' -import { ProjectRepository } from '../repositories/project-repository' -import { ResourceNotFoundError } from './errors/ResourceNotFoundError' +import { ProjectRepository } from '../../repositories/project-repository' +import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' import { MultipartFile } from '@fastify/multipart' import { randomUUID } from 'node:crypto' -import { env } from '../env' +import { env } from '../../env' import path from 'node:path' import fs from 'node:fs' -import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3' +import { PutObjectCommand, S3Client, S3ClientConfig } from '@aws-sdk/client-s3' import pump from 'pump' -import { AwsS3Error } from './errors/AwsS3Error' +import { AwsS3Error } from '../errors/AwsS3Error' interface AddImageToProjectUseCaseRequest { projectId: string @@ -43,13 +43,14 @@ export class AddImageToProjectUseCase { await pump(photo.file, writeSteam) photoUrl = `${uploadPath}/${newFileName}` } else { - const s3bucket = new S3Client([ - { - region: env.REGION, + const s3bucket = new S3Client({ + region: env.REGION, + + credentials: { accessKeyId: env.ACCESS_KEY_ID, secretAccessKey: env.SECRET_ACCESS_KEY, }, - ]) + } as S3ClientConfig) const putObjectCommand = new PutObjectCommand({ Bucket: env.BUCKET_NAME, From 600b13e6f86bb100969d54b46275840646ca6abe Mon Sep 17 00:00:00 2001 From: pedrodecf Date: Fri, 2 Feb 2024 18:19:13 -0300 Subject: [PATCH 04/11] Remove unnecessary test --- src/server.ts | 2 -- src/use-cases/project/deleteProjectByIdUseCase.spec.ts | 7 ------- 2 files changed, 9 deletions(-) diff --git a/src/server.ts b/src/server.ts index 4b7ca23..833034b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,8 +1,6 @@ import { app } from './app' import { env } from './env' -console.log(process.env) - app .listen({ port: env.PORT, diff --git a/src/use-cases/project/deleteProjectByIdUseCase.spec.ts b/src/use-cases/project/deleteProjectByIdUseCase.spec.ts index 69bb540..1fa0cbf 100644 --- a/src/use-cases/project/deleteProjectByIdUseCase.spec.ts +++ b/src/use-cases/project/deleteProjectByIdUseCase.spec.ts @@ -50,13 +50,6 @@ describe('Delete Project By Id Use Case', () => { const { projects } = await getProjectsByUserIdUseCase.execute({ userId: newUser.id }) expect(deletedProject).toEqual(undefined) - expect(secondProject).toEqual( - expect.objectContaining({ - title: 'React Typescript 2', - description: 'Best Project 2', - }), - ) - expect(projects).toHaveLength(1) }) }) \ No newline at end of file From 53df3894163b437d028c94000340debe67f405f9 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Fri, 2 Feb 2024 18:41:55 -0300 Subject: [PATCH 05/11] adding migrations --- prisma/migrations/20240202214129_add_country_user/migration.sql | 2 ++ prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 prisma/migrations/20240202214129_add_country_user/migration.sql diff --git a/prisma/migrations/20240202214129_add_country_user/migration.sql b/prisma/migrations/20240202214129_add_country_user/migration.sql new file mode 100644 index 0000000..86a285f --- /dev/null +++ b/prisma/migrations/20240202214129_add_country_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "country" TEXT NOT NULL DEFAULT 'brasil'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0359edb..7915d14 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,7 @@ model User { is_google Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime @updatedAt + country String @default("brasil") projects Project[] } From 4d19bb9848db5f0b4f1f22b6333cfc0648763ec0 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Fri, 2 Feb 2024 18:51:02 -0300 Subject: [PATCH 06/11] Refactory to test country, removing comments and constructor that we don't need now :) --- src/controller/user/getUserByEmail.spec.ts | 1 + src/controller/user/getUserById.spec.ts | 1 + .../in-memory-db/inMemoryUserRepository.ts | 31 +++---------------- .../user/getUserByEmailUseCase.spec.ts | 1 + src/use-cases/user/getUserByIdUseCase.spec.ts | 5 ++- 5 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/controller/user/getUserByEmail.spec.ts b/src/controller/user/getUserByEmail.spec.ts index c656f63..ed85f05 100644 --- a/src/controller/user/getUserByEmail.spec.ts +++ b/src/controller/user/getUserByEmail.spec.ts @@ -36,6 +36,7 @@ describe('Get User By email E2E', () => { name, surname, password_hash: expect.any(String), + country: 'brasil', }), ) diff --git a/src/controller/user/getUserById.spec.ts b/src/controller/user/getUserById.spec.ts index 6eb0731..7f6f21b 100644 --- a/src/controller/user/getUserById.spec.ts +++ b/src/controller/user/getUserById.spec.ts @@ -31,6 +31,7 @@ describe('Get User By Id E2E', () => { expect(getUserByIdResponse.body.user).toEqual( expect.objectContaining({ id, + country: 'brasil', }), ) }) diff --git a/src/repositories/in-memory-db/inMemoryUserRepository.ts b/src/repositories/in-memory-db/inMemoryUserRepository.ts index 4558073..84991fe 100644 --- a/src/repositories/in-memory-db/inMemoryUserRepository.ts +++ b/src/repositories/in-memory-db/inMemoryUserRepository.ts @@ -5,31 +5,8 @@ import { randomUUID } from 'crypto' export class InMemoryUserRepository implements UserRepository { private db: User[] = [] - - // This is a way to test our controllers without necessartralyy add the - // db repository; Once the program starts, one user is added to User[] and - // you can get http://localhost:3333/user/9600de4f-8d18-4e69-ba7a-ed7fa210618d - // to check the routes; + constructor() {} - // this constructor will be delete later; - constructor(){ - - const email = 'johndoe2@email.com' - const name = 'John' - const surname = 'Doe' - const password_hash = 'password_hash' - const id = '9600de4f-8d18-4e69-ba7a-ed7fa210618d' - - this.create({ - id, - name, - surname, - email, - password_hash, - }) - - } - async findByEmail(email: string): Promise { const User = this.db.find((User) => User.email === email) @@ -58,9 +35,10 @@ export class InMemoryUserRepository implements UserRepository { surname, email, password_hash, + country, }: Prisma.UserCreateInput) { const user: User = { - id: (id == undefined) ? randomUUID() : id, + id: id == undefined ? randomUUID() : id, name, surname, @@ -69,11 +47,10 @@ export class InMemoryUserRepository implements UserRepository { created_at: new Date(), updated_at: new Date(), + country: country || 'brasil', } this.db.push(user) - return user } - } diff --git a/src/use-cases/user/getUserByEmailUseCase.spec.ts b/src/use-cases/user/getUserByEmailUseCase.spec.ts index 60f778b..ce5c437 100644 --- a/src/use-cases/user/getUserByEmailUseCase.spec.ts +++ b/src/use-cases/user/getUserByEmailUseCase.spec.ts @@ -35,6 +35,7 @@ describe('Get User By Email Use Case', () => { expect(user.name).toEqual(name) expect(user.surname).toEqual(surname) expect(user.email).toEqual(email) + expect(user.country).toEqual('brasil') }) it('should not be able to get user that does not exists', async () => { diff --git a/src/use-cases/user/getUserByIdUseCase.spec.ts b/src/use-cases/user/getUserByIdUseCase.spec.ts index 54bc9e2..3a7e9c0 100644 --- a/src/use-cases/user/getUserByIdUseCase.spec.ts +++ b/src/use-cases/user/getUserByIdUseCase.spec.ts @@ -8,7 +8,6 @@ let userRepository: InMemoryUserRepository let getUserByIdUseCase: GetUserByIdUseCase - describe('Get User By Id Use Case', () => { beforeEach(() => { userRepository = new InMemoryUserRepository() @@ -24,7 +23,7 @@ describe('Get User By Id Use Case', () => { const newUser = await userRepository.create({ email, name, - surname, + surname, password_hash: await hash(password, 6), }) @@ -34,7 +33,7 @@ describe('Get User By Id Use Case', () => { expect(user.name).toEqual(name) expect(user.surname).toEqual(surname) expect(user.email).toEqual(email) - + expect(user.country).toEqual('brasil') }) it('should not be able to get user that does not exists', async () => { From 2fcf61e675ced69ec0ab6ea8710c890b772f9030 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Sat, 3 Feb 2024 12:48:23 -0300 Subject: [PATCH 07/11] Fixing prisma types --- .../prisma/prisma-project-repository.ts | 52 ++++++++++++------- .../prisma-project-with-user-data-type.ts | 13 +++++ src/repositories/project-repository.ts | 5 +- 3 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 src/repositories/prisma/prisma-project-with-user-data-type.ts diff --git a/src/repositories/prisma/prisma-project-repository.ts b/src/repositories/prisma/prisma-project-repository.ts index 24fc4c8..e8fcb9f 100644 --- a/src/repositories/prisma/prisma-project-repository.ts +++ b/src/repositories/prisma/prisma-project-repository.ts @@ -1,6 +1,7 @@ import { Prisma, Project } from '@prisma/client' import { ProjectRepository } from '../project-repository' import { prisma } from '../../lib/prisma' +import { ProjectWithUserData } from './prisma-project-with-user-data-type' export class PrismaProjectRepository implements ProjectRepository { async create(data: Prisma.ProjectUncheckedCreateInput): Promise { @@ -21,17 +22,6 @@ export class PrismaProjectRepository implements ProjectRepository { return projects } - async fetchProjectById(projectId: string): Promise { - const project = await prisma.project.findUnique({ - where: { - id: projectId, - }, - }) - - return project - } - - async addPhotoUrl(projectId: string, photoUrl: string): Promise { const project = await prisma.project.update({ where: { @@ -45,27 +35,53 @@ export class PrismaProjectRepository implements ProjectRepository { return project } - async fetchProjectByTags(tags: string[]): Promise { + async fetchProjectByTags(tags: string[]): Promise { const project = await prisma.project.findMany({ where: { tags: { hasEvery: tags }, - + }, + include: { + user: { + select: { + avatar_url: true, + name: true, + surname: true, + }, + }, }, }) return project } - async deleteProjectByID(projectId: string):Promise { - await prisma.project.delete({ + async fetchProjectById( + projectId: string, + ): Promise { + const project = await prisma.project.findUnique({ where: { - id: projectId - } + id: projectId, + }, + include: { + user: { + select: { + avatar_url: true, + name: true, + surname: true, + }, + }, + }, }) - return + return project } + async deleteProjectByID(projectId: string): Promise { + await prisma.project.delete({ + where: { + id: projectId, + }, + }) + } async edit(data: Prisma.ProjectUncheckedCreateInput): Promise { const project = await prisma.project.update({ diff --git a/src/repositories/prisma/prisma-project-with-user-data-type.ts b/src/repositories/prisma/prisma-project-with-user-data-type.ts new file mode 100644 index 0000000..6e5f295 --- /dev/null +++ b/src/repositories/prisma/prisma-project-with-user-data-type.ts @@ -0,0 +1,13 @@ +import { Prisma } from '@prisma/client' + +const userAvatarUrlAndNameData = Prisma.validator()({ + select: { avatar_url: true, name: true, surname: true }, +}) + +const projectsWithUserData = Prisma.validator()({ + include: { user: userAvatarUrlAndNameData }, +}) + +export type ProjectWithUserData = Prisma.ProjectGetPayload< + typeof projectsWithUserData +> diff --git a/src/repositories/project-repository.ts b/src/repositories/project-repository.ts index 6796689..5f6ac90 100644 --- a/src/repositories/project-repository.ts +++ b/src/repositories/project-repository.ts @@ -1,11 +1,12 @@ import { Prisma, Project } from '@prisma/client' +import { ProjectWithUserData } from './prisma/prisma-project-with-user-data-type' export interface ProjectRepository { create(data: Prisma.ProjectUncheckedCreateInput): Promise fetchProjectsByUserId(userId: string): Promise - fetchProjectById(projectId: string): Promise + fetchProjectById(projectId: string): Promise addPhotoUrl(projectId: string, photoUrl: string): Promise - fetchProjectByTags(tags: string[]): Promise + fetchProjectByTags(tags: string[]): Promise edit(data: Prisma.ProjectUncheckedCreateInput): Promise deleteProjectByID(projectID: string): Promise } From 09f521b39beff48288684c3c2e3dddcecc5c4540 Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Sat, 3 Feb 2024 14:20:59 -0300 Subject: [PATCH 08/11] Checking if the user data is coming. --- src/controller/project/getProjectById.spec.ts | 10 +- .../project/getProjectsByTags.spec.ts | 18 +++- .../in-memory-db/inMemoryProjectRepository.ts | 72 ++++++++++++-- .../in-memory-db/inMemoryUserRepository.ts | 33 +------ .../project/editProjectUseCase.spec.ts | 15 ++- .../project/getProjectsByIdUseCase.spec.ts | 12 ++- .../project/getProjectsByIdUseCase.ts | 5 +- .../project/getProjetsByTagsUseCase.spec.ts | 97 ++++++++++++++----- .../project/getProjetsByTagsUseCase.ts | 6 +- 9 files changed, 185 insertions(+), 83 deletions(-) diff --git a/src/controller/project/getProjectById.spec.ts b/src/controller/project/getProjectById.spec.ts index 1191e05..e00a447 100644 --- a/src/controller/project/getProjectById.spec.ts +++ b/src/controller/project/getProjectById.spec.ts @@ -50,11 +50,11 @@ describe('Get Projets By ID E2E', () => { expect(getProjectByIdResponse.statusCode).toEqual(200) expect(getProjectByIdResponse.body.project).toEqual( - expect.objectContaining({ title }), - ) - - expect(getProjectByIdResponse.body.project).toEqual( - expect.objectContaining({ tags }), + expect.objectContaining({ + title, + user: { name: 'John', surname: 'Doe', avatar_url: null }, + tags, + }), ) }) diff --git a/src/controller/project/getProjectsByTags.spec.ts b/src/controller/project/getProjectsByTags.spec.ts index 93fc851..1565eaf 100644 --- a/src/controller/project/getProjectsByTags.spec.ts +++ b/src/controller/project/getProjectsByTags.spec.ts @@ -60,13 +60,22 @@ describe('Get Projets By Tags E2E', () => { .post(`/projects/tags`) .send({ tags }) + console.log('getProjectsByTagsResponse') + console.log(getProjectsByTagsResponse.body.projects[0]) + expect(getProjectsByTagsResponse.statusCode).toEqual(200) expect(getProjectsByTagsResponse.body.projects).toHaveLength(2) expect(getProjectsByTagsResponse.body.projects[0]).toEqual( - expect.objectContaining({ title: 'Project 01' }), + expect.objectContaining({ + title: 'Project 01', + user: { name: 'John', surname: 'Doe', avatar_url: null }, + }), ) expect(getProjectsByTagsResponse.body.projects[1]).toEqual( - expect.objectContaining({ title: 'Project 02' }), + expect.objectContaining({ + title: 'Project 02', + user: { name: 'John', surname: 'Doe', avatar_url: null }, + }), ) }) @@ -94,7 +103,10 @@ describe('Get Projets By Tags E2E', () => { expect(getProjectsByTagsResponse.statusCode).toEqual(200) expect(getProjectsByTagsResponse.body.projects).toHaveLength(1) expect(getProjectsByTagsResponse.body.projects[0]).toEqual( - expect.objectContaining({ title: 'Project 03' }), + expect.objectContaining({ + title: 'Project 03', + user: { name: 'John', surname: 'Doe', avatar_url: null }, + }), ) }) }) diff --git a/src/repositories/in-memory-db/inMemoryProjectRepository.ts b/src/repositories/in-memory-db/inMemoryProjectRepository.ts index aeda533..8a18573 100644 --- a/src/repositories/in-memory-db/inMemoryProjectRepository.ts +++ b/src/repositories/in-memory-db/inMemoryProjectRepository.ts @@ -2,9 +2,12 @@ import { randomUUID } from 'crypto' import { Prisma, Project } from '@prisma/client' import { ProjectRepository } from '../project-repository' +import { ProjectWithUserData } from '../prisma/prisma-project-with-user-data-type' +import { InMemoryUserRepository } from './inMemoryUserRepository' export class InMemoryProjectRepository implements ProjectRepository { public dbProject: Project[] = [] + public dbUser: InMemoryUserRepository = new InMemoryUserRepository() constructor() {} @@ -18,6 +21,7 @@ export class InMemoryProjectRepository implements ProjectRepository { user_id: data.user_id, created_at: new Date(), updated_at: new Date(), + photo_url: null, } this.dbProject.push(project) @@ -43,34 +47,82 @@ export class InMemoryProjectRepository implements ProjectRepository { return projects } - async fetchProjectById(projectId: string): Promise { + async fetchProjectById( + projectId: string, + ): Promise { const project = this.dbProject.find((project) => project.id === projectId) + if (!project) { return null } - return project - } + const { + created_at, + description, + id, + link, + photo_url, + tags, + title, + updated_at, + user_id, + } = project + const foundUser = this.dbUser.db.find((user) => user.id === project.user_id) + + if (!foundUser) { + return null + } + + return { + created_at, + description, + id, + link, + photo_url, + tags, + title, + updated_at, + user_id, + user: { + name: foundUser?.name, + surname: foundUser?.surname, + avatar_url: foundUser?.avatar_url, + }, + } + } async addPhotoUrl(projectId: string, photoUrl: string): Promise { throw new Error('Method not implemented.') } async deleteProjectByID(projectId: string): Promise { - const index = this.dbProject.findIndex((project) => project.id === projectId); + const index = this.dbProject.findIndex( + (project) => project.id === projectId, + ) if (index !== -1) { - this.dbProject.splice(index, 1); + this.dbProject.splice(index, 1) } - - return } - - async fetchProjectByTags(tags: string[]): Promise { + + async fetchProjectByTags(tags: string[]): Promise { const projects = this.dbProject.filter((project) => project.tags.some((tag) => tags.includes(tag)), ) - return projects + const projectPromises = projects.map(async (project) => { + const user = await this.dbUser.findById(project.user_id) + + return { + ...project, + user: { + name: user.name, + surname: user.surname, + avatar_url: user.avatar_url, + }, + } + }) + + return Promise.all(projectPromises) } } diff --git a/src/repositories/in-memory-db/inMemoryUserRepository.ts b/src/repositories/in-memory-db/inMemoryUserRepository.ts index 4558073..b8c6ce9 100644 --- a/src/repositories/in-memory-db/inMemoryUserRepository.ts +++ b/src/repositories/in-memory-db/inMemoryUserRepository.ts @@ -3,33 +3,10 @@ import { UserRepository } from '../user-repository' import { randomUUID } from 'crypto' export class InMemoryUserRepository implements UserRepository { - private db: User[] = [] + public db: User[] = [] - - // This is a way to test our controllers without necessartralyy add the - // db repository; Once the program starts, one user is added to User[] and - // you can get http://localhost:3333/user/9600de4f-8d18-4e69-ba7a-ed7fa210618d - // to check the routes; + constructor() {} - // this constructor will be delete later; - constructor(){ - - const email = 'johndoe2@email.com' - const name = 'John' - const surname = 'Doe' - const password_hash = 'password_hash' - const id = '9600de4f-8d18-4e69-ba7a-ed7fa210618d' - - this.create({ - id, - name, - surname, - email, - password_hash, - }) - - } - async findByEmail(email: string): Promise { const User = this.db.find((User) => User.email === email) @@ -50,8 +27,6 @@ export class InMemoryUserRepository implements UserRepository { return User } - // create in a in-memory database is just used to help us on unit tests; - // that's why is not in our interface :) async create({ id, name, @@ -60,7 +35,7 @@ export class InMemoryUserRepository implements UserRepository { password_hash, }: Prisma.UserCreateInput) { const user: User = { - id: (id == undefined) ? randomUUID() : id, + id: id === undefined ? randomUUID() : id, name, surname, @@ -69,11 +44,11 @@ export class InMemoryUserRepository implements UserRepository { created_at: new Date(), updated_at: new Date(), + avatar_url: null, } this.db.push(user) return user } - } diff --git a/src/use-cases/project/editProjectUseCase.spec.ts b/src/use-cases/project/editProjectUseCase.spec.ts index 384b17f..f2181cd 100644 --- a/src/use-cases/project/editProjectUseCase.spec.ts +++ b/src/use-cases/project/editProjectUseCase.spec.ts @@ -1,17 +1,24 @@ import { expect, describe, it, beforeEach } from 'vitest' import { InMemoryProjectRepository } from '../../repositories/in-memory-db/inMemoryProjectRepository' -import { ProjectRepository } from '../../repositories/project-repository' import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' import { EditProjectUseCase } from './editProjectUseCase' +import { User } from '@prisma/client' -let projectRepository: ProjectRepository +let projectRepository: InMemoryProjectRepository let editProjectUseCase: EditProjectUseCase +let newUser: User describe('Edit Project By Id Use Case', () => { - beforeEach(() => { + beforeEach(async () => { projectRepository = new InMemoryProjectRepository() editProjectUseCase = new EditProjectUseCase(projectRepository) + newUser = await projectRepository.dbUser.create({ + name: 'John', + surname: 'Doe', + email: 'johndoe@email.com', + password_hash: '123456', + }) }) it('should be able edit one project by ID', async () => { @@ -20,7 +27,7 @@ describe('Edit Project By Id Use Case', () => { description: 'Best Project', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) const { project: projectEdited } = await editProjectUseCase.execute({ diff --git a/src/use-cases/project/getProjectsByIdUseCase.spec.ts b/src/use-cases/project/getProjectsByIdUseCase.spec.ts index f3eb8dc..2cdd923 100644 --- a/src/use-cases/project/getProjectsByIdUseCase.spec.ts +++ b/src/use-cases/project/getProjectsByIdUseCase.spec.ts @@ -1,11 +1,10 @@ import { expect, describe, it, beforeEach } from 'vitest' import { InMemoryProjectRepository } from '../../repositories/in-memory-db/inMemoryProjectRepository' -import { ProjectRepository } from '../../repositories/project-repository' import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' import { GetProjectsByIdUseCase } from './getProjectsByIdUseCase' -let projectRepository: ProjectRepository +let projectRepository: InMemoryProjectRepository let getProjectByIdUseCase: GetProjectsByIdUseCase describe('Get Project By Id Use Case', () => { @@ -15,12 +14,18 @@ describe('Get Project By Id Use Case', () => { }) it('should be able get project by ID', async () => { + const newUser = await projectRepository.dbUser.create({ + name: 'John', + surname: 'Doe', + email: 'johndoe@email.com', + password_hash: '123456', + }) const newProject = await projectRepository.create({ title: 'React Typescript 1', description: 'Best Project', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) const { project } = await getProjectByIdUseCase.execute({ @@ -32,6 +37,7 @@ describe('Get Project By Id Use Case', () => { title: 'React Typescript 1', id: newProject.id, tags: ['react', 'node'], + user: { name: 'John', surname: 'Doe', avatar_url: null }, }), ) }) diff --git a/src/use-cases/project/getProjectsByIdUseCase.ts b/src/use-cases/project/getProjectsByIdUseCase.ts index be6719d..7606819 100644 --- a/src/use-cases/project/getProjectsByIdUseCase.ts +++ b/src/use-cases/project/getProjectsByIdUseCase.ts @@ -1,15 +1,14 @@ -import { Project } from '@prisma/client' - import { ProjectRepository } from '../../repositories/project-repository' import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' +import { ProjectWithUserData } from '../../repositories/prisma/prisma-project-with-user-data-type' interface GetProjectByIdRequest { projectId: string } interface GetProjectByIdResponse { - project: Project + project: ProjectWithUserData } export class GetProjectsByIdUseCase { diff --git a/src/use-cases/project/getProjetsByTagsUseCase.spec.ts b/src/use-cases/project/getProjetsByTagsUseCase.spec.ts index cd5447d..95396b6 100644 --- a/src/use-cases/project/getProjetsByTagsUseCase.spec.ts +++ b/src/use-cases/project/getProjetsByTagsUseCase.spec.ts @@ -2,16 +2,23 @@ import { expect, describe, it, beforeEach } from 'vitest' import { InMemoryProjectRepository } from '../../repositories/in-memory-db/inMemoryProjectRepository' -import { ProjectRepository } from '../../repositories/project-repository' import { GetProjectsByTagsUseCase } from './getProjetsByTagsUseCase' +import { User } from '@prisma/client' -let projectRepository: ProjectRepository +let projectRepository: InMemoryProjectRepository let getProjectsByTagsUseCase: GetProjectsByTagsUseCase +let newUser: User describe('Get Project By Tags', () => { - beforeEach(() => { + beforeEach(async () => { projectRepository = new InMemoryProjectRepository() getProjectsByTagsUseCase = new GetProjectsByTagsUseCase(projectRepository) + newUser = await projectRepository.dbUser.create({ + name: 'John', + surname: 'Doe', + email: 'johndoe@email.com', + password_hash: '123456', + }) }) it('should be able get projects that include a tag', async () => { @@ -20,7 +27,7 @@ describe('Get Project By Tags', () => { description: 'Best Project', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) await projectRepository.create({ @@ -28,7 +35,7 @@ describe('Get Project By Tags', () => { description: 'Best Project 2', tags: ['react', 'node', 'typescript'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) const { projects } = await getProjectsByTagsUseCase.execute({ @@ -37,11 +44,15 @@ describe('Get Project By Tags', () => { expect(projects).toHaveLength(1) expect(projects[0]).toEqual( - expect.objectContaining({ title: 'React Typescript 2' }), - ) - - expect(projects[0]).toEqual( - expect.objectContaining({ tags: ['react', 'node', 'typescript'] }), + expect.objectContaining({ + title: 'React Typescript 2', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + tags: ['react', 'node', 'typescript'], + }), ) }) @@ -51,7 +62,7 @@ describe('Get Project By Tags', () => { description: 'Best Project', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) await projectRepository.create({ @@ -59,7 +70,7 @@ describe('Get Project By Tags', () => { description: 'Best Project 2', tags: ['react', 'node', 'typescript'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) await projectRepository.create({ @@ -67,7 +78,7 @@ describe('Get Project By Tags', () => { description: 'Best Project 2', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) const { projects } = await getProjectsByTagsUseCase.execute({ @@ -76,15 +87,36 @@ describe('Get Project By Tags', () => { expect(projects).toHaveLength(3) expect(projects[0]).toEqual( - expect.objectContaining({ title: 'React Typescript 1' }), + expect.objectContaining({ + title: 'React Typescript 1', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) expect(projects[1]).toEqual( - expect.objectContaining({ title: 'React Typescript 2' }), + expect.objectContaining({ + title: 'React Typescript 2', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) expect(projects[2]).toEqual( - expect.objectContaining({ title: 'React Typescript 3' }), + expect.objectContaining({ + title: 'React Typescript 3', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) }) @@ -94,7 +126,7 @@ describe('Get Project By Tags', () => { description: 'Best Project', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) await projectRepository.create({ @@ -102,7 +134,7 @@ describe('Get Project By Tags', () => { description: 'Best Project 2', tags: ['react', 'node', 'typescript'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) await projectRepository.create({ @@ -110,7 +142,7 @@ describe('Get Project By Tags', () => { description: 'Best Project 2', tags: ['react', 'node'], link: 'https://github.com/luiseduardo3/nodets-petcanil', - user_id: 'user_id', + user_id: newUser.id, }) const { projects } = await getProjectsByTagsUseCase.execute({ @@ -119,15 +151,36 @@ describe('Get Project By Tags', () => { expect(projects).toHaveLength(3) expect(projects[0]).toEqual( - expect.objectContaining({ title: 'React Typescript 1' }), + expect.objectContaining({ + title: 'React Typescript 1', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) expect(projects[1]).toEqual( - expect.objectContaining({ title: 'React Typescript 2' }), + expect.objectContaining({ + title: 'React Typescript 2', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) expect(projects[2]).toEqual( - expect.objectContaining({ title: 'React Typescript 3' }), + expect.objectContaining({ + title: 'React Typescript 3', + user: { + name: newUser.name, + surname: newUser.surname, + avatar_url: null, + }, + }), ) }) diff --git a/src/use-cases/project/getProjetsByTagsUseCase.ts b/src/use-cases/project/getProjetsByTagsUseCase.ts index 85b4d7e..ff816b8 100644 --- a/src/use-cases/project/getProjetsByTagsUseCase.ts +++ b/src/use-cases/project/getProjetsByTagsUseCase.ts @@ -1,15 +1,13 @@ -import { Project } from '@prisma/client' - import { ProjectRepository } from '../../repositories/project-repository' -import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' +import { ProjectWithUserData } from '../../repositories/prisma/prisma-project-with-user-data-type' interface GetProjectsByTagsRequest { projectTags: string[] } interface GetProjectsByTagsResponse { - projects: Project[] + projects: ProjectWithUserData[] } export class GetProjectsByTagsUseCase { From ec128be6e4ca4a9d1e587688d4f62aabeb16532b Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Sat, 3 Feb 2024 16:14:02 -0300 Subject: [PATCH 09/11] edit user use case and unit tests --- .../in-memory-db/inMemoryUserRepository.ts | 21 +++++++- src/repositories/user-repository.ts | 8 +++ src/use-cases/user/editUserUseCase.spec.ts | 54 +++++++++++++++++++ src/use-cases/user/editUserUseCase.ts | 43 +++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/use-cases/user/editUserUseCase.spec.ts create mode 100644 src/use-cases/user/editUserUseCase.ts diff --git a/src/repositories/in-memory-db/inMemoryUserRepository.ts b/src/repositories/in-memory-db/inMemoryUserRepository.ts index 0aa5088..74ddd1d 100644 --- a/src/repositories/in-memory-db/inMemoryUserRepository.ts +++ b/src/repositories/in-memory-db/inMemoryUserRepository.ts @@ -1,10 +1,9 @@ import { Prisma, User } from '@prisma/client' -import { UserRepository } from '../user-repository' +import { UserRepository, editUserRequestPrisma } from '../user-repository' import { randomUUID } from 'crypto' export class InMemoryUserRepository implements UserRepository { public db: User[] = [] - constructor() {} async findByEmail(email: string): Promise { @@ -52,4 +51,22 @@ export class InMemoryUserRepository implements UserRepository { this.db.push(user) return user } + + async edit({ + name, + surname, + country, + userId, + }: editUserRequestPrisma): Promise { + const indexToUpdate = this.db.findIndex((user) => user.id === userId) + + this.db[indexToUpdate] = { + ...this.db[indexToUpdate], + name, + surname, + country, + } + + return this.db[indexToUpdate] + } } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 2a53acb..1f878ae 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -1,7 +1,15 @@ import { Prisma, User } from '@prisma/client' +export interface editUserRequestPrisma { + name: string + surname: string + country: string + userId: string +} + export interface UserRepository { create(data: Prisma.UserCreateInput): Promise findByEmail(email: string): Promise findById(id: string): Promise + edit({ name, surname, country, userId }: editUserRequestPrisma): Promise } diff --git a/src/use-cases/user/editUserUseCase.spec.ts b/src/use-cases/user/editUserUseCase.spec.ts new file mode 100644 index 0000000..7f4bb2d --- /dev/null +++ b/src/use-cases/user/editUserUseCase.spec.ts @@ -0,0 +1,54 @@ +import { expect, describe, it, beforeEach } from 'vitest' + +import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' +import { InMemoryUserRepository } from '../../repositories/in-memory-db/inMemoryUserRepository' +import { EditUserUseCase } from './editUserUseCase' + +let userRepository: InMemoryUserRepository + +let editUserUseCase: EditUserUseCase + +describe('Edit Project By Id Use Case', () => { + beforeEach(async () => { + userRepository = new InMemoryUserRepository() + editUserUseCase = new EditUserUseCase(userRepository) + }) + + it('should be able edit one user by ID', async () => { + const userToBeEdited = await userRepository.create({ + name: 'John', + surname: 'Doe', + email: 'johndoe@email.com', + password_hash: '123456', + }) + + const { user } = await editUserUseCase.execute({ + name: 'newCoolName', + surname: 'newSurCoolName', + country: 'differentCountry', + userId: userToBeEdited.id, + }) + + expect(user).toEqual( + expect.objectContaining({ + name: 'newCoolName', + surname: 'newSurCoolName', + country: 'differentCountry', + email: 'johndoe@email.com', + password_hash: '123456', + id: userToBeEdited.id, + }), + ) + }) + + it('should not be able to edit a user that does not exist', async () => { + await expect(() => + editUserUseCase.execute({ + name: 'newCoolName', + surname: 'newSurCoolName', + country: 'differentCountry', + userId: 'not-exist-id', + }), + ).rejects.toBeInstanceOf(ResourceNotFoundError) + }) +}) diff --git a/src/use-cases/user/editUserUseCase.ts b/src/use-cases/user/editUserUseCase.ts new file mode 100644 index 0000000..9a753ec --- /dev/null +++ b/src/use-cases/user/editUserUseCase.ts @@ -0,0 +1,43 @@ +import { User } from '@prisma/client' + +import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' +import { UserRepository } from '../../repositories/user-repository' + +interface EditUserUseCaseRequest { + name: string + surname: string + country: string + userId: string +} + +interface EditUserUseCaseResponse { + user: User +} + +export class EditUserUseCase { + constructor(private userRepository: UserRepository) {} + + async execute({ + name, + surname, + country, + userId, + }: EditUserUseCaseRequest): Promise { + const userToBeUpdated = await this.userRepository.findById(userId) + + if (!userToBeUpdated) { + throw new ResourceNotFoundError() + } + + const user = await this.userRepository.edit({ + name, + surname, + country, + userId, + }) + + return { + user, + } + } +} From 29f8c234b7c459b32211812bf503dcb6160a32ad Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Sat, 3 Feb 2024 16:41:55 -0300 Subject: [PATCH 10/11] edit user route and tests e2e --- src/controller/user/editUserById.spec.ts | 71 +++++++++++++++++++ src/controller/user/editUserById.ts | 41 +++++++++++ src/controller/user/routes.ts | 2 + .../prisma/prisma-users-repository.ts | 16 ++++- 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/controller/user/editUserById.spec.ts create mode 100644 src/controller/user/editUserById.ts diff --git a/src/controller/user/editUserById.spec.ts b/src/controller/user/editUserById.spec.ts new file mode 100644 index 0000000..ed632f1 --- /dev/null +++ b/src/controller/user/editUserById.spec.ts @@ -0,0 +1,71 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import request from 'supertest' +import { app } from '../../app' +import { randomUUID } from 'crypto' +import { PrismaUsersRepository } from '../../repositories/prisma/prisma-users-repository' +import { UserRepository } from '../../repositories/user-repository' + +let userRepository: UserRepository + +describe('edit User E2E', () => { + beforeAll(async () => { + userRepository = new PrismaUsersRepository() + await app.ready() + }) + + afterAll(async () => { + await app.close() + }) + + it('should be able to edit a user', async () => { + const email = 'john_doe@email.com' + const name = 'John' + const surname = 'Doe' + const password_hash = 'password_hash' + + const newUser = await userRepository.create({ + email, + name, + surname, + password_hash, + }) + + const editUserResponse = await request(app.server) + .put(`/user/${newUser.id}/edit`) + .send({ + name: 'newName', + surname: 'surname', + country: 'country', + }) + + expect(editUserResponse.statusCode).toEqual(200) + expect(editUserResponse.body.user).toEqual( + expect.objectContaining({ + name: 'newName', + surname: 'surname', + country: 'country', + id: newUser.id, + email, + password_hash, + }), + ) + }) + + it('should not be able to edit a user that does not exist', async () => { + const editUserResponse = await request(app.server) + .put(`/user/${randomUUID()}/edit`) + .send({ + name: 'newName', + surname: 'surname', + country: 'country', + }) + + expect(editUserResponse.statusCode).toEqual(404) + + expect(editUserResponse.body).toEqual( + expect.objectContaining({ + error: 'User was not Found !', + }), + ) + }) +}) diff --git a/src/controller/user/editUserById.ts b/src/controller/user/editUserById.ts new file mode 100644 index 0000000..f6d6ec4 --- /dev/null +++ b/src/controller/user/editUserById.ts @@ -0,0 +1,41 @@ +import { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' +import { ResourceNotFoundError } from '../../use-cases/errors/ResourceNotFoundError' +import { PrismaUsersRepository } from '../../repositories/prisma/prisma-users-repository' +import { EditUserUseCase } from '../../use-cases/user/editUserUseCase' + +export async function editUserById( + request: FastifyRequest, + response: FastifyReply, +) { + const editUserBodySchema = z.object({ + name: z.string(), + surname: z.string(), + country: z.string(), + }) + + const editUserParamsSchema = z.object({ + userId: z.string().uuid(), + }) + + const { name, surname, country } = editUserBodySchema.parse(request.body) + const { userId } = editUserParamsSchema.parse(request.params) + + const userRepository = new PrismaUsersRepository() + const editUserUseCase = new EditUserUseCase(userRepository) + + try { + const { user } = await editUserUseCase.execute({ + name, + surname, + country, + userId, + }) + + return response.status(200).send({ user }) + } catch (error) { + if (error instanceof ResourceNotFoundError) { + return response.status(404).send({ error: 'User was not Found !' }) + } + } +} diff --git a/src/controller/user/routes.ts b/src/controller/user/routes.ts index 941a5ed..2ae9568 100644 --- a/src/controller/user/routes.ts +++ b/src/controller/user/routes.ts @@ -2,9 +2,11 @@ import { FastifyInstance } from 'fastify' import { getUserById } from './getUserById' import { getUserByEmail } from './getUserByEmail' import { registerUser } from './registerUser' +import { editUserById } from './editUserById' export async function userRoutes(app: FastifyInstance) { app.post('/user', registerUser) app.get('/user/:id', getUserById) app.get('/user', getUserByEmail) + app.put('/user/:userId/edit', editUserById) } diff --git a/src/repositories/prisma/prisma-users-repository.ts b/src/repositories/prisma/prisma-users-repository.ts index e43197d..3872a6e 100644 --- a/src/repositories/prisma/prisma-users-repository.ts +++ b/src/repositories/prisma/prisma-users-repository.ts @@ -1,6 +1,6 @@ import { Prisma, User } from '@prisma/client' import { prisma } from '../../lib/prisma' -import { UserRepository } from '../user-repository' +import { UserRepository, editUserRequestPrisma } from '../user-repository' export class PrismaUsersRepository implements UserRepository { async findByEmail(email: string): Promise { @@ -30,4 +30,18 @@ export class PrismaUsersRepository implements UserRepository { return user } + + async edit({ + name, + surname, + country, + userId, + }: editUserRequestPrisma): Promise { + const user = await prisma.user.update({ + where: { id: userId }, + data: { name, surname, country }, + }) + + return user + } } From 085066ccf5fb7aa0863b248c7795d395b1429d1e Mon Sep 17 00:00:00 2001 From: Matheus Sanchez Date: Sat, 3 Feb 2024 17:04:10 -0300 Subject: [PATCH 11/11] Handle upload files local and S3 aws --- src/controller/user/addImageToUser.ts | 36 +++++++++ src/controller/user/routes.ts | 10 +++ .../in-memory-db/inMemoryUserRepository.ts | 4 + .../prisma/prisma-users-repository.ts | 13 ++++ src/repositories/user-repository.ts | 1 + src/use-cases/user/addImageToUserUseCase.ts | 74 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 src/controller/user/addImageToUser.ts create mode 100644 src/use-cases/user/addImageToUserUseCase.ts diff --git a/src/controller/user/addImageToUser.ts b/src/controller/user/addImageToUser.ts new file mode 100644 index 0000000..2121206 --- /dev/null +++ b/src/controller/user/addImageToUser.ts @@ -0,0 +1,36 @@ +import { FastifyReply, FastifyRequest } from 'fastify' +import { z } from 'zod' +import { ResourceNotFoundError } from '../../use-cases/errors/ResourceNotFoundError' +import { AwsS3Error } from '../../use-cases/errors/AwsS3Error' +import { PrismaUsersRepository } from '../../repositories/prisma/prisma-users-repository' +import { AddImageToUserUseCase } from '../../use-cases/user/addImageToUserUseCase' + +export async function addImageUser( + request: FastifyRequest, + response: FastifyReply, +) { + const userRepository = new PrismaUsersRepository() + const addImageToUserUseCase = new AddImageToUserUseCase(userRepository) + const addImageUserParamsSchema = z.object({ + userId: z.string().uuid(), + }) + + const { userId } = addImageUserParamsSchema.parse(request.params) + const photo = await request.file() + + if (photo === undefined) { + return response.status(400).send({ error: 'Fail load a photo!' }) + } + + try { + const { user } = await addImageToUserUseCase.execute({ userId, photo }) + return response.status(200).send({ user }) + } catch (error) { + if (error instanceof ResourceNotFoundError) { + return response.status(400).send({ error: 'User was not found !' }) + } else if (error instanceof AwsS3Error) { + return response.status(400).send({ error: error.message }) + } + throw error + } +} diff --git a/src/controller/user/routes.ts b/src/controller/user/routes.ts index 941a5ed..210da29 100644 --- a/src/controller/user/routes.ts +++ b/src/controller/user/routes.ts @@ -2,9 +2,19 @@ import { FastifyInstance } from 'fastify' import { getUserById } from './getUserById' import { getUserByEmail } from './getUserByEmail' import { registerUser } from './registerUser' +import { addImageUser } from './addImageToUser' +import FastifyMultipart from '@fastify/multipart' export async function userRoutes(app: FastifyInstance) { + app.register(FastifyMultipart, { + limits: { + files: 1, + fileSize: 1000000, // the max file size in bytes + }, + }) app.post('/user', registerUser) app.get('/user/:id', getUserById) app.get('/user', getUserByEmail) + + app.post('/user/:userId/photo', addImageUser) } diff --git a/src/repositories/in-memory-db/inMemoryUserRepository.ts b/src/repositories/in-memory-db/inMemoryUserRepository.ts index 0aa5088..8a735f9 100644 --- a/src/repositories/in-memory-db/inMemoryUserRepository.ts +++ b/src/repositories/in-memory-db/inMemoryUserRepository.ts @@ -52,4 +52,8 @@ export class InMemoryUserRepository implements UserRepository { this.db.push(user) return user } + + async addPhotoUrl(projectId: string, photoUrl: string): Promise { + throw new Error('Method not implemented.') + } } diff --git a/src/repositories/prisma/prisma-users-repository.ts b/src/repositories/prisma/prisma-users-repository.ts index e43197d..468e366 100644 --- a/src/repositories/prisma/prisma-users-repository.ts +++ b/src/repositories/prisma/prisma-users-repository.ts @@ -30,4 +30,17 @@ export class PrismaUsersRepository implements UserRepository { return user } + + async addPhotoUrl(userId: string, photoUrl: string): Promise { + const user = await prisma.user.update({ + where: { + id: userId, + }, + data: { + avatar_url: photoUrl, + }, + }) + + return user + } } diff --git a/src/repositories/user-repository.ts b/src/repositories/user-repository.ts index 2a53acb..e6419b2 100644 --- a/src/repositories/user-repository.ts +++ b/src/repositories/user-repository.ts @@ -4,4 +4,5 @@ export interface UserRepository { create(data: Prisma.UserCreateInput): Promise findByEmail(email: string): Promise findById(id: string): Promise + addPhotoUrl(userId: string, photoUrl: string): Promise } diff --git a/src/use-cases/user/addImageToUserUseCase.ts b/src/use-cases/user/addImageToUserUseCase.ts new file mode 100644 index 0000000..213238e --- /dev/null +++ b/src/use-cases/user/addImageToUserUseCase.ts @@ -0,0 +1,74 @@ +import { User } from '@prisma/client' + +import { ResourceNotFoundError } from '../errors/ResourceNotFoundError' +import { MultipartFile } from '@fastify/multipart' +import { randomUUID } from 'node:crypto' +import { env } from '../../env' +import path from 'node:path' +import fs from 'node:fs' +import { PutObjectCommand, S3Client, S3ClientConfig } from '@aws-sdk/client-s3' +import pump from 'pump' +import { AwsS3Error } from '../errors/AwsS3Error' +import { UserRepository } from '../../repositories/user-repository' + +interface AddImageToUserUseCaseRequest { + userId: string + photo: MultipartFile +} + +interface AddImageToUserUseCaseResponse { + user: User +} + +export class AddImageToUserUseCase { + constructor(private userRepository: UserRepository) {} + + async execute({ + userId, + photo, + }: AddImageToUserUseCaseRequest): Promise { + const userToBeUpdated = await this.userRepository.findById(userId) + + if (!userToBeUpdated) { + throw new ResourceNotFoundError() + } + + const newFileName = randomUUID() + photo.filename.replace(/\s/g, '') + let photoUrl = '' + + if (env.STORAGE_TYPE === 'local') { + const uploadPath = path.resolve(__dirname, '..', 'tmp', 'uploads') + const writeSteam = fs.createWriteStream(`${uploadPath}/${newFileName}`) + await pump(photo.file, writeSteam) + photoUrl = `${uploadPath}/${newFileName}` + } else { + const s3bucket = new S3Client({ + region: env.REGION, + + credentials: { + accessKeyId: env.ACCESS_KEY_ID, + secretAccessKey: env.SECRET_ACCESS_KEY, + }, + } as S3ClientConfig) + + const putObjectCommand = new PutObjectCommand({ + Bucket: env.BUCKET_NAME, + Key: newFileName, + Body: await photo.toBuffer(), + ContentType: photo.mimetype, + ACL: 'public-read', + }) + + const publishs3Result = await s3bucket.send(putObjectCommand) + + if (publishs3Result.$metadata.httpStatusCode !== 200) { + throw new AwsS3Error() + } + photoUrl = env.AWS_S3_URL + newFileName + } + + const user = await this.userRepository.addPhotoUrl(userId, photoUrl) + + return { user } + } +}