add project creation and listing
This commit is contained in:
5
server/.env
Normal file
5
server/.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ENV=development
|
||||||
|
PORT=5000
|
||||||
|
SECRET=aE8efkEP8+V/ibEQl8IKbw==
|
||||||
|
|
||||||
|
DB_NAME=todoListDev
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
PORT=5000
|
|
||||||
SECRET=aE8efkEP8+V/ibEQl8IKbw==
|
|
||||||
5
server/.env.template
Normal file
5
server/.env.template
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ENV=development
|
||||||
|
PORT=5000
|
||||||
|
SECRET=aE8efkEP8+V/ibEQl8IKbw==
|
||||||
|
|
||||||
|
DB_NAME=todoListDev
|
||||||
5
server/.env.test.template
Normal file
5
server/.env.test.template
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ENV=test
|
||||||
|
PORT=5000
|
||||||
|
SECRET=aE8efkEP8+V/ibEQl8IKbw==
|
||||||
|
|
||||||
|
DB_NAME=todoListTest
|
||||||
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
.env
|
.env.development
|
||||||
|
.env.test
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
preset: "ts-jest",
|
||||||
testEnvironment: 'node',
|
testEnvironment: "node",
|
||||||
clearMocks: true,
|
clearMocks: true,
|
||||||
collectCoverage: true,
|
collectCoverage: true,
|
||||||
coverageDirectory: "coverage",
|
coverageDirectory: "coverage",
|
||||||
coverageProvider: "v8",
|
coverageProvider: "v8",
|
||||||
}
|
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tsc",
|
"build": "npx tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
|
"dev": "rm -rf ./dist && concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
|
||||||
"test": "jest"
|
"test": "NODE_ENV=test jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { UserRoutes } from './users.controller'
|
export { UserRoutes } from './user.controller'
|
||||||
|
export { ProjectRoutes } from './project.controller'
|
||||||
40
server/src/controller/project.controller.ts
Normal file
40
server/src/controller/project.controller.ts
Normal 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;
|
||||||
4
server/src/dto/newProject.dto.ts
Normal file
4
server/src/dto/newProject.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type NewProjectDto = {
|
||||||
|
name: string
|
||||||
|
userId: number
|
||||||
|
}
|
||||||
3
server/src/dto/poject.dto.ts
Normal file
3
server/src/dto/poject.dto.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type ProjectDto = {
|
||||||
|
name: string
|
||||||
|
}
|
||||||
4
server/src/dto/updateProject.dto.ts
Normal file
4
server/src/dto/updateProject.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type UpdateProjectDto = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
28
server/src/entity/__test__/project.entity.spec.ts
Normal file
28
server/src/entity/__test__/project.entity.spec.ts
Normal 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)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
45
server/src/entity/__test__/user.entity.spec.ts
Normal file
45
server/src/entity/__test__/user.entity.spec.ts
Normal 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()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
18
server/src/entity/project.entity.ts
Normal file
18
server/src/entity/project.entity.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IsEmail, IsNotEmpty } from "class-validator"
|
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()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number
|
id: number
|
||||||
|
|
||||||
@@ -14,4 +14,7 @@ export class User {
|
|||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
encryptedPassword: string
|
encryptedPassword: string
|
||||||
|
|
||||||
|
@OneToMany((_type) => Project, (item) => item.user)
|
||||||
|
projects?: Project[]
|
||||||
}
|
}
|
||||||
|
|||||||
6
server/src/env.d.ts
vendored
Normal file
6
server/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
DB_NAME?: string;
|
||||||
|
NODE_ENV?: 'test' | 'development' | 'production';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import "reflect-metadata"
|
|||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import { AppDataSource } from "./infra/dataSource";
|
import { AppDataSource } from "./infra/dataSource";
|
||||||
import { UserRoutes } from "./controller";
|
import { ProjectRoutes, UserRoutes } from "./controller";
|
||||||
import { RedisConnection } from "./infra/redis";
|
import { RedisConnection } from "./infra/redis";
|
||||||
import { sessionMiddleware } from "./middleware/session.middleware";
|
import { sessionMiddleware } from "./middleware/session.middleware";
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@ dotenv.config();
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.use(sessionMiddleware)
|
app.use(sessionMiddleware)
|
||||||
|
|
||||||
app.use(UserRoutes)
|
app.use(UserRoutes)
|
||||||
|
app.use(ProjectRoutes)
|
||||||
|
|
||||||
|
const startApp = async () => {
|
||||||
const startApp = async () => {
|
|
||||||
console.log('[redis]: connecting')
|
console.log('[redis]: connecting')
|
||||||
await RedisConnection.connect()
|
await RedisConnection.connect()
|
||||||
console.log('[redis]: connected')
|
console.log('[redis]: connected')
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import "reflect-metadata"
|
import * as dotenv from 'dotenv';
|
||||||
import { DataSource } from "typeorm"
|
import "reflect-metadata";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { Project } from "../entity/project.entity";
|
||||||
import { User } from "../entity/user.entity";
|
import { User } from "../entity/user.entity";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
port: 5432,
|
port: 5432,
|
||||||
username: "joao",
|
username: "joao",
|
||||||
database: "todoListDev",
|
database: process.env.DB_NAME,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [User],
|
entities: [User, Project],
|
||||||
migrations: [],
|
migrations: [],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { signInPath } from '../../controller/users.controller';
|
import { signInPath, createPath } from '../../controller/users.controller';
|
||||||
import { UNPROTECTED_ROUTES } from '../session.middleware'
|
import { UNPROTECTED_ROUTES } from '../session.middleware'
|
||||||
|
|
||||||
describe('Unprotected Routes', () => {
|
describe('Unprotected Routes', () => {
|
||||||
it('check content', () => {
|
it('check content', () => {
|
||||||
expect(UNPROTECTED_ROUTES.sort()).toEqual([signInPath].sort());
|
expect(UNPROTECTED_ROUTES.sort()).toEqual([createPath, signInPath].sort());
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Handler, Request, Response } from 'express';
|
import { Handler, Request, Response } from 'express';
|
||||||
import { verify } from 'jsonwebtoken';
|
import { verify } from 'jsonwebtoken';
|
||||||
import { signInPath } from '../controller/users.controller';
|
import { signInPath, createPath } from '../controller/user.controller';
|
||||||
import { AuthService } from '../service/auth.service';
|
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) => {
|
export const sessionMiddleware: Handler = (req: Request, res: Response, next) => {
|
||||||
const token = req.headers['x-access-token'];
|
const token = req.headers['x-access-token'];
|
||||||
|
|||||||
4
server/src/repository/project.repository.ts
Normal file
4
server/src/repository/project.repository.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Project } from "../entity/project.entity";
|
||||||
|
import { AppDataSource } from "../infra/dataSource";
|
||||||
|
|
||||||
|
export const projectRepository = AppDataSource.getRepository(Project)
|
||||||
34
server/src/service/project.service.ts
Normal file
34
server/src/service/project.service.ts
Normal 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,
|
||||||
|
};
|
||||||
@@ -8,14 +8,12 @@ async function create(newUserDto: NewUserDto): Promise<User> {
|
|||||||
const user = new User()
|
const user = new User()
|
||||||
user.email = newUserDto.email
|
user.email = newUserDto.email
|
||||||
|
|
||||||
|
|
||||||
const errors = await validate(user);
|
const errors = await validate(user);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length) {
|
||||||
throw new Error("Invalid user data")
|
throw new Error("Invalid user data")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const result = await findByEmail(user.email)
|
const result = await findByEmail(user.email)
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
5
server/src/setupTests.ts
Normal file
5
server/src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: '.env.test'
|
||||||
|
});
|
||||||
18
server/src/utils/cleanDataSource.ts
Normal file
18
server/src/utils/cleanDataSource.ts
Normal 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}";`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user