add project creation and listing

This commit is contained in:
João Geonizeli
2022-07-09 10:05:01 -03:00
parent c3fe6e6f1c
commit e0dd6c2307
27 changed files with 256 additions and 26 deletions

5
server/.env Normal file
View File

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

View File

@@ -1,2 +0,0 @@
PORT=5000
SECRET=aE8efkEP8+V/ibEQl8IKbw==

5
server/.env.template Normal file
View File

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

View File

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

3
server/.gitignore vendored
View File

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

View File

@@ -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: ["<rootDir>/src/setupTests.ts"],
};

View File

@@ -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",

View File

@@ -1 +1,2 @@
export { UserRoutes } from './users.controller'
export { UserRoutes } from './user.controller'
export { ProjectRoutes } from './project.controller'

View File

@@ -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<ProjectDto>(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;

View File

@@ -0,0 +1,4 @@
export type NewProjectDto = {
name: string
userId: number
}

View File

@@ -0,0 +1,3 @@
export type ProjectDto = {
name: string
}

View File

@@ -0,0 +1,4 @@
export type UpdateProjectDto = {
id: string
name: string
}

View File

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

View File

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

View File

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

View File

@@ -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[]
}

6
server/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare namespace NodeJS {
interface ProcessEnv {
DB_NAME?: string;
NODE_ENV?: 'test' | 'development' | 'production';
}
}

View File

@@ -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,10 +11,10 @@ dotenv.config();
const app = express();
app.use(express.json());
app.use(sessionMiddleware)
app.use(UserRoutes)
app.use(UserRoutes)
app.use(ProjectRoutes)
const startApp = async () => {
console.log('[redis]: connecting')

View File

@@ -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: [],
})

View File

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

View File

@@ -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'];

View File

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

View File

@@ -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<Project> {
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<Project[]> {
const query = projectRepository.createQueryBuilder();
query.where('"userId" = :userId', { userId });
return query.getMany();
}
export const ProjectService = {
create,
listAllByUserId,
};

View File

@@ -8,14 +8,12 @@ async function create(newUserDto: NewUserDto): Promise<User> {
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) {

5
server/src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
import * as dotenv from 'dotenv';
dotenv.config({
path: '.env.test'
});

View File

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