From e0dd6c2307296905767542e2533e989a202f8d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Geonizeli?= Date: Sat, 9 Jul 2022 10:05:01 -0300 Subject: [PATCH] add project creation and listing --- server/.env | 5 +++ server/.env.example | 2 - server/.env.template | 5 +++ server/.env.test.template | 5 +++ server/.gitignore | 3 +- server/jest.config.js | 7 +-- server/package.json | 4 +- server/src/controller/index.ts | 3 +- server/src/controller/project.controller.ts | 40 +++++++++++++++++ ...users.controller.ts => user.controller.ts} | 0 server/src/dto/newProject.dto.ts | 4 ++ server/src/dto/poject.dto.ts | 3 ++ server/src/dto/updateProject.dto.ts | 4 ++ .../entity/__test__/project.entity.spec.ts | 28 ++++++++++++ .../src/entity/__test__/user.entity.spec.ts | 45 +++++++++++++++++++ server/src/entity/project.entity.ts | 18 ++++++++ server/src/entity/user.entity.ts | 7 ++- server/src/env.d.ts | 6 +++ server/src/index.ts | 8 ++-- server/src/infra/dataSource.ts | 12 +++-- .../__test__/unprotectedRoutes.spec.ts | 4 +- server/src/middleware/session.middleware.ts | 4 +- server/src/repository/project.repository.ts | 4 ++ server/src/service/project.service.ts | 34 ++++++++++++++ server/src/service/user.service.ts | 4 +- server/src/setupTests.ts | 5 +++ server/src/utils/cleanDataSource.ts | 18 ++++++++ 27 files changed, 256 insertions(+), 26 deletions(-) create mode 100644 server/.env delete mode 100644 server/.env.example create mode 100644 server/.env.template create mode 100644 server/.env.test.template create mode 100644 server/src/controller/project.controller.ts rename server/src/controller/{users.controller.ts => user.controller.ts} (100%) create mode 100644 server/src/dto/newProject.dto.ts create mode 100644 server/src/dto/poject.dto.ts create mode 100644 server/src/dto/updateProject.dto.ts create mode 100644 server/src/entity/__test__/project.entity.spec.ts create mode 100644 server/src/entity/__test__/user.entity.spec.ts create mode 100644 server/src/entity/project.entity.ts create mode 100644 server/src/env.d.ts create mode 100644 server/src/repository/project.repository.ts create mode 100644 server/src/service/project.service.ts create mode 100644 server/src/setupTests.ts create mode 100644 server/src/utils/cleanDataSource.ts diff --git a/server/.env b/server/.env new file mode 100644 index 0000000..7ec0442 --- /dev/null +++ b/server/.env @@ -0,0 +1,5 @@ +ENV=development +PORT=5000 +SECRET=aE8efkEP8+V/ibEQl8IKbw== + +DB_NAME=todoListDev \ No newline at end of file diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 11c0ff9..0000000 --- a/server/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -PORT=5000 -SECRET=aE8efkEP8+V/ibEQl8IKbw== \ No newline at end of file diff --git a/server/.env.template b/server/.env.template new file mode 100644 index 0000000..7ec0442 --- /dev/null +++ b/server/.env.template @@ -0,0 +1,5 @@ +ENV=development +PORT=5000 +SECRET=aE8efkEP8+V/ibEQl8IKbw== + +DB_NAME=todoListDev \ No newline at end of file diff --git a/server/.env.test.template b/server/.env.test.template new file mode 100644 index 0000000..21f4293 --- /dev/null +++ b/server/.env.test.template @@ -0,0 +1,5 @@ +ENV=test +PORT=5000 +SECRET=aE8efkEP8+V/ibEQl8IKbw== + +DB_NAME=todoListTest \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index 9e5db7c..66d3bf4 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,4 +1,5 @@ node_modules dist coverage -.env \ No newline at end of file +.env.development +.env.test \ No newline at end of file diff --git a/server/jest.config.js b/server/jest.config.js index 7b87312..22cdd67 100644 --- a/server/jest.config.js +++ b/server/jest.config.js @@ -4,10 +4,11 @@ */ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', + preset: "ts-jest", + testEnvironment: "node", clearMocks: true, collectCoverage: true, coverageDirectory: "coverage", coverageProvider: "v8", -} + setupFilesAfterEnv: ["/src/setupTests.ts"], +}; diff --git a/server/package.json b/server/package.json index 98ca2c3..ebba413 100644 --- a/server/package.json +++ b/server/package.json @@ -6,8 +6,8 @@ "scripts": { "build": "npx tsc", "start": "node dist/index.js", - "dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"", - "test": "jest" + "dev": "rm -rf ./dist && concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"", + "test": "NODE_ENV=test jest" }, "dependencies": { "bcrypt": "^5.0.1", diff --git a/server/src/controller/index.ts b/server/src/controller/index.ts index 95d3712..5aae651 100644 --- a/server/src/controller/index.ts +++ b/server/src/controller/index.ts @@ -1 +1,2 @@ -export { UserRoutes } from './users.controller' +export { UserRoutes } from './user.controller' +export { ProjectRoutes } from './project.controller' \ No newline at end of file diff --git a/server/src/controller/project.controller.ts b/server/src/controller/project.controller.ts new file mode 100644 index 0000000..d03abf0 --- /dev/null +++ b/server/src/controller/project.controller.ts @@ -0,0 +1,40 @@ +import { Router } from "express"; +import { ProjectDto } from "../dto/poject.dto"; +import { ProjectService } from "../service/project.service"; + +const router = Router(); + +const BASE_PATH = "/projects"; + +export const getAllPath = BASE_PATH; +router.get(getAllPath, async (req, res) => { + const projects = await ProjectService.listAllByUserId(req.userId) + + const response: ProjectDto[] = projects.map(project => ({ + name: project.name + })) + + res.json(response) +}); + +export const createPath = BASE_PATH; +router.post(createPath, (req, res) => { + const { name } = req.body; + + ProjectService.create({ + name, + userId: req.userId + }).then(project => { + const respose: ProjectDto = { + name: project.name + } + + res.json(respose); + }).catch(err => { + res.status(422).json({ + error: err.message + }); + }) +}); + +export const ProjectRoutes = router; diff --git a/server/src/controller/users.controller.ts b/server/src/controller/user.controller.ts similarity index 100% rename from server/src/controller/users.controller.ts rename to server/src/controller/user.controller.ts diff --git a/server/src/dto/newProject.dto.ts b/server/src/dto/newProject.dto.ts new file mode 100644 index 0000000..00d077a --- /dev/null +++ b/server/src/dto/newProject.dto.ts @@ -0,0 +1,4 @@ +export type NewProjectDto = { + name: string + userId: number +} \ No newline at end of file diff --git a/server/src/dto/poject.dto.ts b/server/src/dto/poject.dto.ts new file mode 100644 index 0000000..7e6ee1f --- /dev/null +++ b/server/src/dto/poject.dto.ts @@ -0,0 +1,3 @@ +export type ProjectDto = { + name: string +} \ No newline at end of file diff --git a/server/src/dto/updateProject.dto.ts b/server/src/dto/updateProject.dto.ts new file mode 100644 index 0000000..4dad3bd --- /dev/null +++ b/server/src/dto/updateProject.dto.ts @@ -0,0 +1,4 @@ +export type UpdateProjectDto = { + id: string + name: string +} \ No newline at end of file diff --git a/server/src/entity/__test__/project.entity.spec.ts b/server/src/entity/__test__/project.entity.spec.ts new file mode 100644 index 0000000..a8ddb2e --- /dev/null +++ b/server/src/entity/__test__/project.entity.spec.ts @@ -0,0 +1,28 @@ +import { AppDataSource } from "../../infra/dataSource"; +import { projectRepository } from "../../repository/project.repository"; +import { userRepository } from "../../repository/user.repository"; +import { cleanDataSource } from "../../utils/cleanDataSource"; + +describe("Project", () => { + beforeAll(async () => { + await AppDataSource.initialize(); + await cleanDataSource(AppDataSource, ["project", "user"]); + }); + + describe("relations", () => { + it("should have many projects", async () => { + const user = await userRepository.save({ + name: "John Doe", + email: "john.doe@example.com", + encryptedPassword: 'encryptedPassword' + }) + + const project = await projectRepository.save({ + name: "My first project", + user, + }) + + expect(project.user.id).toBe(user.id) + }); + }); +}); diff --git a/server/src/entity/__test__/user.entity.spec.ts b/server/src/entity/__test__/user.entity.spec.ts new file mode 100644 index 0000000..cefe1f3 --- /dev/null +++ b/server/src/entity/__test__/user.entity.spec.ts @@ -0,0 +1,45 @@ +import { AppDataSource } from "../../infra/dataSource"; +import { userRepository } from "../../repository/user.repository"; +import { cleanDataSource } from "../../utils/cleanDataSource"; +import { Project } from "../project.entity"; +import { User } from "../user.entity"; + +describe("User", () => { + beforeAll(async () => { + await AppDataSource.initialize(); + await cleanDataSource(AppDataSource, ["project", "user"]); + }); + + describe("relations", () => { + it("should have many projects", async () => { + const user = new User(); + const user2 = new User(); + + Object.assign(user, { + name: "John Doe", + email: "john.doe@example.com", + encryptedPassword: "encryptedPassword", + }); + + Object.assign(user2, { + name: "Luis Doe", + email: "luis.doe@example.com", + encryptedPassword: "encryptedPassword", + }); + + const project1 = new Project(); + const proejct2 = new Project(); + + project1.name = "My first project"; + proejct2.name = "My favorite project"; + + user.projects = [project1, proejct2]; + + await userRepository.save(user); + await userRepository.save(user2) + + expect(user.projects).toHaveLength(2); + expect(user2.projects).toBeUndefined() + }); + }); +}); diff --git a/server/src/entity/project.entity.ts b/server/src/entity/project.entity.ts new file mode 100644 index 0000000..c3c8640 --- /dev/null +++ b/server/src/entity/project.entity.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty } from "class-validator" +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from "typeorm" +import { User } from "./user.entity" + +@Entity() +export class Project { + @PrimaryGeneratedColumn() + id: number + + @Column() + @IsNotEmpty() + name: string + + @ManyToOne((_type) => User, (user) => user.projects) + @JoinColumn() + @IsNotEmpty() + user: User +} diff --git a/server/src/entity/user.entity.ts b/server/src/entity/user.entity.ts index 5cba178..2efd175 100644 --- a/server/src/entity/user.entity.ts +++ b/server/src/entity/user.entity.ts @@ -1,9 +1,9 @@ import { IsEmail, IsNotEmpty } from "class-validator" -import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, AfterLoad } from "typeorm" +import { Project } from "./project.entity" @Entity() export class User { - @PrimaryGeneratedColumn() id: number @@ -14,4 +14,7 @@ export class User { @Column() encryptedPassword: string + + @OneToMany((_type) => Project, (item) => item.user) + projects?: Project[] } diff --git a/server/src/env.d.ts b/server/src/env.d.ts new file mode 100644 index 0000000..f190d4e --- /dev/null +++ b/server/src/env.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + interface ProcessEnv { + DB_NAME?: string; + NODE_ENV?: 'test' | 'development' | 'production'; + } +} \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 37f072c..9709e3d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,7 +2,7 @@ import "reflect-metadata" import * as dotenv from 'dotenv'; import * as express from 'express'; import { AppDataSource } from "./infra/dataSource"; -import { UserRoutes } from "./controller"; +import { ProjectRoutes, UserRoutes } from "./controller"; import { RedisConnection } from "./infra/redis"; import { sessionMiddleware } from "./middleware/session.middleware"; @@ -11,12 +11,12 @@ dotenv.config(); const app = express(); app.use(express.json()); - app.use(sessionMiddleware) + app.use(UserRoutes) +app.use(ProjectRoutes) - -const startApp = async () => { +const startApp = async () => { console.log('[redis]: connecting') await RedisConnection.connect() console.log('[redis]: connected') diff --git a/server/src/infra/dataSource.ts b/server/src/infra/dataSource.ts index 2dda45c..b5b37ed 100644 --- a/server/src/infra/dataSource.ts +++ b/server/src/infra/dataSource.ts @@ -1,16 +1,20 @@ -import "reflect-metadata" -import { DataSource } from "typeorm" +import * as dotenv from 'dotenv'; +import "reflect-metadata"; +import { DataSource } from "typeorm"; +import { Project } from "../entity/project.entity"; import { User } from "../entity/user.entity"; +dotenv.config(); + export const AppDataSource = new DataSource({ type: "postgres", host: "localhost", port: 5432, username: "joao", - database: "todoListDev", + database: process.env.DB_NAME, synchronize: true, logging: false, - entities: [User], + entities: [User, Project], migrations: [], subscribers: [], }) diff --git a/server/src/middleware/__test__/unprotectedRoutes.spec.ts b/server/src/middleware/__test__/unprotectedRoutes.spec.ts index 3eb5fa0..47a1d95 100644 --- a/server/src/middleware/__test__/unprotectedRoutes.spec.ts +++ b/server/src/middleware/__test__/unprotectedRoutes.spec.ts @@ -1,8 +1,8 @@ -import { signInPath } from '../../controller/users.controller'; +import { signInPath, createPath } from '../../controller/users.controller'; import { UNPROTECTED_ROUTES } from '../session.middleware' describe('Unprotected Routes', () => { it('check content', () => { - expect(UNPROTECTED_ROUTES.sort()).toEqual([signInPath].sort()); + expect(UNPROTECTED_ROUTES.sort()).toEqual([createPath, signInPath].sort()); }) }) \ No newline at end of file diff --git a/server/src/middleware/session.middleware.ts b/server/src/middleware/session.middleware.ts index 189e82f..4e39006 100644 --- a/server/src/middleware/session.middleware.ts +++ b/server/src/middleware/session.middleware.ts @@ -1,9 +1,9 @@ import { Handler, Request, Response } from 'express'; import { verify } from 'jsonwebtoken'; -import { signInPath } from '../controller/users.controller'; +import { signInPath, createPath } from '../controller/user.controller'; import { AuthService } from '../service/auth.service'; -export const UNPROTECTED_ROUTES = [signInPath]; +export const UNPROTECTED_ROUTES = [signInPath, createPath]; export const sessionMiddleware: Handler = (req: Request, res: Response, next) => { const token = req.headers['x-access-token']; diff --git a/server/src/repository/project.repository.ts b/server/src/repository/project.repository.ts new file mode 100644 index 0000000..0e2e37e --- /dev/null +++ b/server/src/repository/project.repository.ts @@ -0,0 +1,4 @@ +import { Project } from "../entity/project.entity"; +import { AppDataSource } from "../infra/dataSource"; + +export const projectRepository = AppDataSource.getRepository(Project) diff --git a/server/src/service/project.service.ts b/server/src/service/project.service.ts new file mode 100644 index 0000000..cbd0f42 --- /dev/null +++ b/server/src/service/project.service.ts @@ -0,0 +1,34 @@ +import { validate } from "class-validator"; +import { NewProjectDto } from "../dto/newProject.dto"; +import { Project } from "../entity/project.entity"; +import { User } from "../entity/user.entity"; +import { projectRepository } from "../repository/project.repository"; +import { UserService } from "./user.service"; + +async function create(newProject: NewProjectDto): Promise { + const project = new Project(); + const user = await UserService.findUserById(newProject.userId); + + project.name = newProject.name; + project.user = user + + const errors = await validate(project); + + if (errors.length) { + throw new Error("Invalid project data"); + } + + return projectRepository.save(project); +} + +async function listAllByUserId(userId: User["id"]): Promise { + const query = projectRepository.createQueryBuilder(); + query.where('"userId" = :userId', { userId }); + + return query.getMany(); +} + +export const ProjectService = { + create, + listAllByUserId, +}; diff --git a/server/src/service/user.service.ts b/server/src/service/user.service.ts index 8810c28..deb267e 100644 --- a/server/src/service/user.service.ts +++ b/server/src/service/user.service.ts @@ -8,14 +8,12 @@ async function create(newUserDto: NewUserDto): Promise { const user = new User() user.email = newUserDto.email - const errors = await validate(user); - if (errors.length > 0) { + if (errors.length) { throw new Error("Invalid user data") } - const result = await findByEmail(user.email) if (result) { diff --git a/server/src/setupTests.ts b/server/src/setupTests.ts new file mode 100644 index 0000000..a80fc24 --- /dev/null +++ b/server/src/setupTests.ts @@ -0,0 +1,5 @@ +import * as dotenv from 'dotenv'; + +dotenv.config({ + path: '.env.test' +}); diff --git a/server/src/utils/cleanDataSource.ts b/server/src/utils/cleanDataSource.ts new file mode 100644 index 0000000..ca7763d --- /dev/null +++ b/server/src/utils/cleanDataSource.ts @@ -0,0 +1,18 @@ +import { DataSource } from "typeorm"; + +export const cleanDataSource = async ( + dataSource: DataSource, + entityNames: string[] +) => { + if (process.env.NODE_ENV !== "test") { + throw new Error( + `You tried to run a cleanDataSource into ${process.env.NODE_ENV} enviroment` + ); + } + + await Promise.all( + entityNames.map((tableName) => { + return dataSource.query(`DELETE FROM "${tableName}";`); + }) + ); +};