diff --git a/server/.env b/server/.env deleted file mode 100644 index 7ec0442..0000000 --- a/server/.env +++ /dev/null @@ -1,5 +0,0 @@ -ENV=development -PORT=5000 -SECRET=aE8efkEP8+V/ibEQl8IKbw== - -DB_NAME=todoListDev \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index 66d3bf4..d545b9d 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,5 +1,5 @@ node_modules dist coverage -.env.development +.env .env.test \ No newline at end of file diff --git a/server/package.json b/server/package.json index 93bd0fc..2fc2b69 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@faker-js/faker": "^7.3.0", "@types/bcrypt": "^5.0.0", "@types/express": "^4.17.13", "@types/jest": "^28.1.4", diff --git a/server/src/controller/project.controller.ts b/server/src/controller/project.controller.ts index a8c785b..beb7964 100644 --- a/server/src/controller/project.controller.ts +++ b/server/src/controller/project.controller.ts @@ -45,7 +45,7 @@ router.delete(`${apiNamespace}/:id`, async (req, res, next) => { const projecToBeDeleted = userProjects.find(project => project.id === projectId) if (projecToBeDeleted) { - const success = await ProjectService.destroyProject(projecToBeDeleted) + const success = await ProjectService.destroy(projecToBeDeleted) if (success) { res.json({ success }) diff --git a/server/src/dto/newTaskDto.ts b/server/src/dto/newTaskDto.ts new file mode 100644 index 0000000..ba084c6 --- /dev/null +++ b/server/src/dto/newTaskDto.ts @@ -0,0 +1,3 @@ +export type NewTaskDto = { + description: 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 index 3ddc9fd..82f714a 100644 --- a/server/src/entity/__test__/project.entity.spec.ts +++ b/server/src/entity/__test__/project.entity.spec.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import { AppDataSource } from "../../infra/dataSource"; import { projectRepository } from "../../repository/project.repository"; import { userRepository } from "../../repository/user.repository"; @@ -10,23 +11,23 @@ describe("Project", () => { }); afterAll(async () => { await cleanDataSource(AppDataSource); - await AppDataSource.destroy() - }) + await AppDataSource.destroy(); + }); describe("relations", () => { - it("should have many projects", async () => { + it("should belongs a one user", async () => { const user = await userRepository.save({ name: "John Doe", - email: "john.doe@example.com", - encryptedPassword: 'encryptedPassword' - }) + email: faker.internet.email(), + encryptedPassword: "encryptedPassword", + }); const project = await projectRepository.save({ name: "My first project", user, - }) + }); - expect(project.user.id).toBe(user.id) + expect(project.user.id).toBe(user.id); }); }); }); diff --git a/server/src/entity/__test__/task.entity.spec.ts b/server/src/entity/__test__/task.entity.spec.ts new file mode 100644 index 0000000..27f750a --- /dev/null +++ b/server/src/entity/__test__/task.entity.spec.ts @@ -0,0 +1,63 @@ +import { faker } from "@faker-js/faker"; +import { AppDataSource } from "../../infra/dataSource"; +import { projectRepository } from "../../repository/project.repository"; +import { taskRepository } from "../../repository/task.repository"; +import { userRepository } from "../../repository/user.repository"; +import { cleanDataSource } from "../../utils/cleanDataSource"; + +describe("Task", () => { + beforeAll(async () => { + await AppDataSource.initialize(); + await cleanDataSource(AppDataSource); + }); + afterAll(async () => { + await cleanDataSource(AppDataSource); + await AppDataSource.destroy(); + }); + + describe("relations", () => { + it("should belongs a one project", async () => { + const user = await userRepository.save({ + email: faker.internet.email(), + encryptedPassword: "encryptedPassword", + }); + + const project = await projectRepository.save({ + name: "My first project", + user, + }); + + const task = await taskRepository.save({ + description: "My first task", + project, + createdAt: new Date(), + }); + + expect(task.project.id).toBe(project.id); + }); + }); + + describe("date fields", () => { + it("should return date fields as Date objects", async () => { + const user = await userRepository.save({ + email: faker.internet.email(), + encryptedPassword: "encryptedPassword", + }); + + const project = await projectRepository.save({ + name: "My secound project", + user, + }); + + const task = await taskRepository.save({ + description: "My secound task", + project, + createdAt: new Date(), + finishedAt: new Date(), + }); + + expect(task.createdAt).toBeInstanceOf(Date); + expect(task.finishedAt).toBeInstanceOf(Date); + }); + }); +}); diff --git a/server/src/entity/__test__/user.entity.spec.ts b/server/src/entity/__test__/user.entity.spec.ts index ee4ca83..c936c6b 100644 --- a/server/src/entity/__test__/user.entity.spec.ts +++ b/server/src/entity/__test__/user.entity.spec.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import { AppDataSource } from "../../infra/dataSource"; import { userRepository } from "../../repository/user.repository"; import { cleanDataSource } from "../../utils/cleanDataSource"; @@ -11,8 +12,8 @@ describe("User", () => { }); afterAll(async () => { await cleanDataSource(AppDataSource); - await AppDataSource.destroy() - }) + await AppDataSource.destroy(); + }); describe("relations", () => { it("should have many projects", async () => { @@ -21,13 +22,13 @@ describe("User", () => { Object.assign(user, { name: "John Doe", - email: "john.doe@example.com", + email: faker.internet.email(), encryptedPassword: "encryptedPassword", }); Object.assign(user2, { name: "Luis Doe", - email: "luis.doe@example.com", + email: faker.internet.email(), encryptedPassword: "encryptedPassword", }); @@ -40,10 +41,10 @@ describe("User", () => { user.projects = [project1, proejct2]; await userRepository.save(user); - await userRepository.save(user2) + await userRepository.save(user2); expect(user.projects).toHaveLength(2); - expect(user2.projects).toBeUndefined() + expect(user2.projects).toBeUndefined(); }); }); }); diff --git a/server/src/entity/project.entity.ts b/server/src/entity/project.entity.ts index c3c8640..adcab87 100644 --- a/server/src/entity/project.entity.ts +++ b/server/src/entity/project.entity.ts @@ -1,18 +1,24 @@ -import { IsNotEmpty } from "class-validator" -import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from "typeorm" -import { User } from "./user.entity" +import { IsNotEmpty } from "class-validator"; +import { + Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn +} from "typeorm"; +import { Task } from "./task.entity"; +import { User } from "./user.entity"; @Entity() export class Project { - @PrimaryGeneratedColumn() - id: number + @PrimaryGeneratedColumn() + id: number; - @Column() - @IsNotEmpty() - name: string + @Column() + @IsNotEmpty() + name: string; - @ManyToOne((_type) => User, (user) => user.projects) - @JoinColumn() - @IsNotEmpty() - user: User + @ManyToOne((_type) => User, (user) => user.projects) + @JoinColumn() + @IsNotEmpty() + user: User; + + @OneToMany((_type) => Task, (item) => item.project) + tasks?: Task[]; } diff --git a/server/src/entity/task.entity.ts b/server/src/entity/task.entity.ts new file mode 100644 index 0000000..283d304 --- /dev/null +++ b/server/src/entity/task.entity.ts @@ -0,0 +1,31 @@ +import { IsNotEmpty } from "class-validator"; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn +} from "typeorm"; +import { Project } from "./project.entity"; + +@Entity() +export class Task { + @PrimaryGeneratedColumn() + id: number; + + @Column() + @IsNotEmpty() + description: string; + + @Column({ type: "timestamptz" }) + @IsNotEmpty() + createdAt: Date; + + @Column({ type: "timestamptz", nullable: true }) + finishedAt?: Date; + + @ManyToOne((_type) => Project, (project) => project.tasks) + @JoinColumn() + @IsNotEmpty() + project: Project; +} diff --git a/server/src/entity/user.entity.ts b/server/src/entity/user.entity.ts index 2efd175..29a4d6e 100644 --- a/server/src/entity/user.entity.ts +++ b/server/src/entity/user.entity.ts @@ -1,20 +1,22 @@ -import { IsEmail, IsNotEmpty } from "class-validator" -import { Entity, PrimaryGeneratedColumn, Column, OneToMany, AfterLoad } from "typeorm" -import { Project } from "./project.entity" +import { IsEmail, IsNotEmpty } from "class-validator"; +import { + Column, Entity, OneToMany, PrimaryGeneratedColumn +} from "typeorm"; +import { Project } from "./project.entity"; @Entity() export class User { - @PrimaryGeneratedColumn() - id: number + @PrimaryGeneratedColumn() + id: number; - @Column({ unique: true }) - @IsEmail() - @IsNotEmpty() - email: string + @Column({ unique: true }) + @IsEmail() + @IsNotEmpty() + email: string; - @Column() - encryptedPassword: string + @Column() + encryptedPassword: string; - @OneToMany((_type) => Project, (item) => item.user) - projects?: Project[] + @OneToMany((_type) => Project, (item) => item.user) + projects?: Project[]; } diff --git a/server/src/infra/dataSource.ts b/server/src/infra/dataSource.ts index b5b37ed..5f8a866 100644 --- a/server/src/infra/dataSource.ts +++ b/server/src/infra/dataSource.ts @@ -2,6 +2,7 @@ import * as dotenv from 'dotenv'; import "reflect-metadata"; import { DataSource } from "typeorm"; import { Project } from "../entity/project.entity"; +import { Task } from '../entity/task.entity'; import { User } from "../entity/user.entity"; dotenv.config(); @@ -14,7 +15,7 @@ export const AppDataSource = new DataSource({ database: process.env.DB_NAME, synchronize: true, logging: false, - entities: [User, Project], + entities: [User, Project, Task], migrations: [], subscribers: [], }) diff --git a/server/src/repository/task.repository.ts b/server/src/repository/task.repository.ts new file mode 100644 index 0000000..be43458 --- /dev/null +++ b/server/src/repository/task.repository.ts @@ -0,0 +1,4 @@ +import { Task } from "../entity/task.entity"; +import { AppDataSource } from "../infra/dataSource"; + +export const taskRepository = AppDataSource.getRepository(Task) diff --git a/server/src/service/project.service.ts b/server/src/service/project.service.ts index 6ba4f45..fec5161 100644 --- a/server/src/service/project.service.ts +++ b/server/src/service/project.service.ts @@ -28,7 +28,7 @@ async function listAllByUserId(userId: User["id"]): Promise { return query.getMany(); } -async function destroyProject(project: Project): Promise{ +async function destroy(project: Project): Promise{ const query = projectRepository.createQueryBuilder(); query.where('"id" = :projectId', { projectId: project.id }); @@ -40,5 +40,5 @@ async function destroyProject(project: Project): Promise{ export const ProjectService = { create, listAllByUserId, - destroyProject + destroy }; diff --git a/server/src/service/task.service.ts b/server/src/service/task.service.ts new file mode 100644 index 0000000..6b0427a --- /dev/null +++ b/server/src/service/task.service.ts @@ -0,0 +1,37 @@ +import { NewTaskDto } from "../dto/newTaskDto"; +import { Project } from "../entity/project.entity"; +import { Task } from "../entity/task.entity"; +import { taskRepository } from "../repository/task.repository"; + +async function finish(task: Task) { + if (task.finishedAt) { + throw new Error("Task already finished"); + } + + task.finishedAt = new Date(); + + return taskRepository.save(task); +} + +async function destroy(task: Task) { + if (task.finishedAt) { + throw new Error("You can't destroy a finished task"); + } + + return taskRepository.delete(task); +} + +async function create(newTaskDto: NewTaskDto, project: Project) { + const task = new Task(); + + task.description = newTaskDto.description; + task.project = project; + + return taskRepository.save(task); +} + +export const TaskService = { + finish, + destroy, + create, +}; diff --git a/server/src/utils/cleanDataSource.ts b/server/src/utils/cleanDataSource.ts index ee28839..daaa5ac 100644 --- a/server/src/utils/cleanDataSource.ts +++ b/server/src/utils/cleanDataSource.ts @@ -2,7 +2,7 @@ import { DataSource } from "typeorm"; export const cleanDataSource = async ( dataSource: DataSource, - entityNames: string[] = ["project", "user"] + entityNames: string[] = ["task", "project", "user"] ) => { if (process.env.NODE_ENV !== "test") { throw new Error( diff --git a/server/yarn.lock b/server/yarn.lock index 389afde..7fdf90a 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -291,6 +291,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@faker-js/faker@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-7.3.0.tgz#a508df35ded585c4e071cb5d9d7c89623c837fae" + integrity sha512-1W0PZezq2rxlAssoWemi9gFRD8IQxvf0FPL5Km3TOmGHFG7ib0TbFBJ0yC7D/1NsxunjNTK6WjUXV8ao/mKZ5w== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"