This commit is contained in:
João Geonizeli
2022-07-09 14:28:36 -03:00
parent 97dac843fe
commit 82c078f5db
17 changed files with 200 additions and 50 deletions

View File

@@ -1,5 +0,0 @@
ENV=development
PORT=5000
SECRET=aE8efkEP8+V/ibEQl8IKbw==
DB_NAME=todoListDev

2
server/.gitignore vendored
View File

@@ -1,5 +1,5 @@
node_modules node_modules
dist dist
coverage coverage
.env.development .env
.env.test .env.test

View File

@@ -22,6 +22,7 @@
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^7.3.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "^28.1.4", "@types/jest": "^28.1.4",

View File

@@ -45,7 +45,7 @@ router.delete(`${apiNamespace}/:id`, async (req, res, next) => {
const projecToBeDeleted = userProjects.find(project => project.id === projectId) const projecToBeDeleted = userProjects.find(project => project.id === projectId)
if (projecToBeDeleted) { if (projecToBeDeleted) {
const success = await ProjectService.destroyProject(projecToBeDeleted) const success = await ProjectService.destroy(projecToBeDeleted)
if (success) { if (success) {
res.json({ success }) res.json({ success })

View File

@@ -0,0 +1,3 @@
export type NewTaskDto = {
description: string;
}

View File

@@ -1,3 +1,4 @@
import { faker } from "@faker-js/faker";
import { AppDataSource } from "../../infra/dataSource"; import { AppDataSource } from "../../infra/dataSource";
import { projectRepository } from "../../repository/project.repository"; import { projectRepository } from "../../repository/project.repository";
import { userRepository } from "../../repository/user.repository"; import { userRepository } from "../../repository/user.repository";
@@ -10,23 +11,23 @@ describe("Project", () => {
}); });
afterAll(async () => { afterAll(async () => {
await cleanDataSource(AppDataSource); await cleanDataSource(AppDataSource);
await AppDataSource.destroy() await AppDataSource.destroy();
}) });
describe("relations", () => { describe("relations", () => {
it("should have many projects", async () => { it("should belongs a one user", async () => {
const user = await userRepository.save({ const user = await userRepository.save({
name: "John Doe", name: "John Doe",
email: "john.doe@example.com", email: faker.internet.email(),
encryptedPassword: 'encryptedPassword' encryptedPassword: "encryptedPassword",
}) });
const project = await projectRepository.save({ const project = await projectRepository.save({
name: "My first project", name: "My first project",
user, user,
}) });
expect(project.user.id).toBe(user.id) expect(project.user.id).toBe(user.id);
}); });
}); });
}); });

View File

@@ -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);
});
});
});

View File

@@ -1,3 +1,4 @@
import { faker } from "@faker-js/faker";
import { AppDataSource } from "../../infra/dataSource"; import { AppDataSource } from "../../infra/dataSource";
import { userRepository } from "../../repository/user.repository"; import { userRepository } from "../../repository/user.repository";
import { cleanDataSource } from "../../utils/cleanDataSource"; import { cleanDataSource } from "../../utils/cleanDataSource";
@@ -11,8 +12,8 @@ describe("User", () => {
}); });
afterAll(async () => { afterAll(async () => {
await cleanDataSource(AppDataSource); await cleanDataSource(AppDataSource);
await AppDataSource.destroy() await AppDataSource.destroy();
}) });
describe("relations", () => { describe("relations", () => {
it("should have many projects", async () => { it("should have many projects", async () => {
@@ -21,13 +22,13 @@ describe("User", () => {
Object.assign(user, { Object.assign(user, {
name: "John Doe", name: "John Doe",
email: "john.doe@example.com", email: faker.internet.email(),
encryptedPassword: "encryptedPassword", encryptedPassword: "encryptedPassword",
}); });
Object.assign(user2, { Object.assign(user2, {
name: "Luis Doe", name: "Luis Doe",
email: "luis.doe@example.com", email: faker.internet.email(),
encryptedPassword: "encryptedPassword", encryptedPassword: "encryptedPassword",
}); });
@@ -40,10 +41,10 @@ describe("User", () => {
user.projects = [project1, proejct2]; user.projects = [project1, proejct2];
await userRepository.save(user); await userRepository.save(user);
await userRepository.save(user2) await userRepository.save(user2);
expect(user.projects).toHaveLength(2); expect(user.projects).toHaveLength(2);
expect(user2.projects).toBeUndefined() expect(user2.projects).toBeUndefined();
}); });
}); });
}); });

View File

@@ -1,18 +1,24 @@
import { IsNotEmpty } from "class-validator" import { IsNotEmpty } from "class-validator";
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from "typeorm" import {
import { User } from "./user.entity" Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn
} from "typeorm";
import { Task } from "./task.entity";
import { User } from "./user.entity";
@Entity() @Entity()
export class Project { export class Project {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number id: number;
@Column() @Column()
@IsNotEmpty() @IsNotEmpty()
name: string name: string;
@ManyToOne((_type) => User, (user) => user.projects) @ManyToOne((_type) => User, (user) => user.projects)
@JoinColumn() @JoinColumn()
@IsNotEmpty() @IsNotEmpty()
user: User user: User;
@OneToMany((_type) => Task, (item) => item.project)
tasks?: Task[];
} }

View File

@@ -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;
}

View File

@@ -1,20 +1,22 @@
import { IsEmail, IsNotEmpty } from "class-validator" import { IsEmail, IsNotEmpty } from "class-validator";
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, AfterLoad } from "typeorm" import {
import { Project } from "./project.entity" Column, Entity, OneToMany, PrimaryGeneratedColumn
} from "typeorm";
import { Project } from "./project.entity";
@Entity() @Entity()
export class User { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number id: number;
@Column({ unique: true }) @Column({ unique: true })
@IsEmail() @IsEmail()
@IsNotEmpty() @IsNotEmpty()
email: string email: string;
@Column() @Column()
encryptedPassword: string encryptedPassword: string;
@OneToMany((_type) => Project, (item) => item.user) @OneToMany((_type) => Project, (item) => item.user)
projects?: Project[] projects?: Project[];
} }

View File

@@ -2,6 +2,7 @@ import * as dotenv from 'dotenv';
import "reflect-metadata"; import "reflect-metadata";
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { Project } from "../entity/project.entity"; import { Project } from "../entity/project.entity";
import { Task } from '../entity/task.entity';
import { User } from "../entity/user.entity"; import { User } from "../entity/user.entity";
dotenv.config(); dotenv.config();
@@ -14,7 +15,7 @@ export const AppDataSource = new DataSource({
database: process.env.DB_NAME, database: process.env.DB_NAME,
synchronize: true, synchronize: true,
logging: false, logging: false,
entities: [User, Project], entities: [User, Project, Task],
migrations: [], migrations: [],
subscribers: [], subscribers: [],
}) })

View File

@@ -0,0 +1,4 @@
import { Task } from "../entity/task.entity";
import { AppDataSource } from "../infra/dataSource";
export const taskRepository = AppDataSource.getRepository(Task)

View File

@@ -28,7 +28,7 @@ async function listAllByUserId(userId: User["id"]): Promise<Project[]> {
return query.getMany(); return query.getMany();
} }
async function destroyProject(project: Project): Promise<boolean>{ async function destroy(project: Project): Promise<boolean>{
const query = projectRepository.createQueryBuilder(); const query = projectRepository.createQueryBuilder();
query.where('"id" = :projectId', { projectId: project.id }); query.where('"id" = :projectId', { projectId: project.id });
@@ -40,5 +40,5 @@ async function destroyProject(project: Project): Promise<boolean>{
export const ProjectService = { export const ProjectService = {
create, create,
listAllByUserId, listAllByUserId,
destroyProject destroy
}; };

View File

@@ -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,
};

View File

@@ -2,7 +2,7 @@ import { DataSource } from "typeorm";
export const cleanDataSource = async ( export const cleanDataSource = async (
dataSource: DataSource, dataSource: DataSource,
entityNames: string[] = ["project", "user"] entityNames: string[] = ["task", "project", "user"]
) => { ) => {
if (process.env.NODE_ENV !== "test") { if (process.env.NODE_ENV !== "test") {
throw new Error( throw new Error(

View File

@@ -291,6 +291,11 @@
dependencies: dependencies:
"@jridgewell/trace-mapping" "0.3.9" "@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": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"