Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add task ownership and restrictions #7

Merged
merged 7 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/auth/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Task } from 'src/tasks/task.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
Expand All @@ -10,4 +11,7 @@ export class User {

@Column()
password: string;

@OneToMany((_type) => Task, (task) => task.user, { eager: true })
tasks: Task[];
}
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();
8 changes: 7 additions & 1 deletion src/tasks/task.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';
import { User } from 'src/auth/user.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { TaskStatus } from './task-status.enum';

@Entity()
Expand All @@ -14,4 +16,8 @@ export class Task {

@Column()
status: TaskStatus;

@ManyToOne((_type) => User, (user) => user.tasks, { eager: false })
@Exclude({ toPlainOnly: true })
user: User;
}
27 changes: 18 additions & 9 deletions src/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GetUser } from 'src/auth/get-user.decorator';
import { User } from 'src/auth/user.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { GetTasksFilterDto } from './dto/get-tasks-filter.dto';
import { UpdateTaskStatusDto } from './dto/update-task-status.dto';
Expand All @@ -22,31 +24,38 @@ export class TasksController {
constructor(private tasksService: TasksService) {}

@Get()
getTasks(@Query() filterDto: GetTasksFilterDto): Promise<Task[]> {
return this.tasksService.getTasks(filterDto);
getTasks(
@Query() filterDto: GetTasksFilterDto,
@GetUser() user: User,
): Promise<Task[]> {
return this.tasksService.getTasks(filterDto, user);
}

@Get('/:id')
getTaskById(@Param('id') id: string): Promise<Task> {
return this.tasksService.getTaskById(id);
getTaskById(@Param('id') id: string, @GetUser() user: User): Promise<Task> {
return this.tasksService.getTaskById(id, user);
}

@Post()
createTask(@Body() createTaskDto: CreateTaskDto): Promise<Task> {
return this.tasksService.createTask(createTaskDto);
createTask(
@Body() createTaskDto: CreateTaskDto,
@GetUser() user: User,
): Promise<Task> {
return this.tasksService.createTask(createTaskDto, user);
}

@Delete('/:id')
deleteTask(@Param('id') id: string): Promise<void> {
return this.tasksService.deleteTask(id);
deleteTask(@Param('id') id: string, @GetUser() user: User): Promise<void> {
return this.tasksService.deleteTask(id, user);
}

@Patch('/:id/status')
updateTaskStatus(
@Param('id') id: string,
@Body() updateTaskStatusDto: UpdateTaskStatusDto,
@GetUser() user: User,
): Promise<Task> {
const { status } = updateTaskStatusDto;
return this.tasksService.updateTaskStatus(id, status);
return this.tasksService.updateTaskStatus(id, status, user);
}
}
10 changes: 7 additions & 3 deletions src/tasks/tasks.repository.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { User } from 'src/auth/user.entity';
import { EntityRepository, Repository } from 'typeorm';
import { CreateTaskDto } from './dto/create-task.dto';
import { GetTasksFilterDto } from './dto/get-tasks-filter.dto';
Expand All @@ -6,16 +7,18 @@ import { Task } from './task.entity';

@EntityRepository(Task)
export class TasksRepository extends Repository<Task> {
async getTasks(filterDto: GetTasksFilterDto): Promise<Task[]> {
async getTasks(filterDto: GetTasksFilterDto, user: User): Promise<Task[]> {
const { status, search } = filterDto;
const query = this.createQueryBuilder('task');
query.where({ user });

if (status) {
query.andWhere('task.status = :status', { status });
}

if (search) {
query.andWhere(
'LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search)',
'(LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search))',
{ search: `%${search}%` },
);
}
Expand All @@ -24,12 +27,13 @@ export class TasksRepository extends Repository<Task> {
return tasks;
}

async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
async createTask(createTaskDto: CreateTaskDto, user: User): Promise<Task> {
const { title, description } = createTaskDto;
const task = this.create({
title,
description,
status: TaskStatus.OPEN,
user,
});

await this.save(task);
Expand Down
25 changes: 15 additions & 10 deletions src/tasks/tasks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GetTasksFilterDto } from './dto/get-tasks-filter.dto';
import { TasksRepository } from './tasks.repository';
import { InjectRepository } from '@nestjs/typeorm';
import { Task } from './task.entity';
import { User } from 'src/auth/user.entity';

@Injectable()
export class TasksService {
Expand All @@ -13,32 +14,36 @@ export class TasksService {
private tasksRepository: TasksRepository,
) {}

getTasks(filterDto: GetTasksFilterDto): Promise<Task[]> {
return this.tasksRepository.getTasks(filterDto);
getTasks(filterDto: GetTasksFilterDto, user: User): Promise<Task[]> {
return this.tasksRepository.getTasks(filterDto, user);
}

async getTaskById(id: string): Promise<Task> {
const foundTask = await this.tasksRepository.findOne(id);
async getTaskById(id: string, user: User): Promise<Task> {
const foundTask = await this.tasksRepository.findOne({ id, user });

if (!foundTask) {
throw new NotFoundException(`Task with ID ${id} is not found.`);
}
return foundTask;
}

createTask(createTaskDto: CreateTaskDto): Promise<Task> {
return this.tasksRepository.createTask(createTaskDto);
createTask(createTaskDto: CreateTaskDto, user: User): Promise<Task> {
return this.tasksRepository.createTask(createTaskDto, user);
}

async deleteTask(id: string): Promise<void> {
const result = await this.tasksRepository.delete(id);
async deleteTask(id: string, user: User): Promise<void> {
const result = await this.tasksRepository.delete({ id, user });
if (result.affected === 0) {
throw new NotFoundException(`Task with ID "${id}" is not found.`);
}
}

async updateTaskStatus(id: string, status: TaskStatus): Promise<Task> {
const task = await this.getTaskById(id);
async updateTaskStatus(
id: string,
status: TaskStatus,
user: User,
): Promise<Task> {
const task = await this.getTaskById(id, user);
task.status = status;
await this.tasksRepository.save(task);
return task;
Expand Down
15 changes: 15 additions & 0 deletions src/transform.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
NestInterceptor,
ExecutionContext,
Injectable,
CallHandler,
} from '@nestjs/common';
import { classToPlain } from 'class-transformer';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<any>) {
return next.handle().pipe(map((data) => classToPlain(data)));
}
}