diff --git a/src/common/helpers/pagination.helper.ts b/src/common/helpers/pagination.helper.ts new file mode 100644 index 0000000..df195a4 --- /dev/null +++ b/src/common/helpers/pagination.helper.ts @@ -0,0 +1,29 @@ +import { IPageFormat } from "../interfaces/IPagination"; + +export function buildPageFormat( + page: number, + limit: number, + totalItems: number, + baseUrl: string, +): IPageFormat { + const totalPages = Math.ceil(totalItems / limit); + + const prevPage = + page > 1 + ? `${baseUrl}?page=${page - 1}&limit=${limit}` + : false; + + const nextPage = + page < totalPages + ? `${baseUrl}?page=${page + 1}&limit=${limit}` + : false; + + return { + page, + limit, + totalItems, + totalPages, + prevPage, + nextPage, + }; +} \ No newline at end of file diff --git a/src/common/interfaces/paginated-result.interface.ts b/src/common/interfaces/paginated-result.interface.ts new file mode 100644 index 0000000..b9247fb --- /dev/null +++ b/src/common/interfaces/paginated-result.interface.ts @@ -0,0 +1,6 @@ +import { IPageFormat } from "./IPagination"; + +export interface PaginatedResult { + data: T[]; + meta: IPageFormat; +} \ No newline at end of file diff --git a/src/modules/task-manager/controllers/project.controller.ts b/src/modules/task-manager/controllers/project.controller.ts index 3fae2a2..55c95bd 100644 --- a/src/modules/task-manager/controllers/project.controller.ts +++ b/src/modules/task-manager/controllers/project.controller.ts @@ -1,8 +1,10 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, Query, Req } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { Request } from 'express'; import { ProjectService } from '../providers/project.service'; import { CreateProjectDto } from '../dto/project/create-project.dto'; import { UpdateProjectDto } from '../dto/project/update-project.dto'; +import { PaginationDto } from '../../../common/DTO/pagination.dto'; import { TMProject } from '../entities/project.entity'; @ApiTags('Task Manager - Projects') @@ -13,10 +15,15 @@ export class ProjectController { @Get() @ApiOperation({ summary: 'Get all projects, optionally filtered by workspace' }) @ApiQuery({ name: 'workspaceId', required: false, description: 'Filter projects by workspace ID' }) - @ApiResponse({ status: 200, description: 'List of projects', type: [TMProject] }) - findAll(@Query('workspaceId') workspaceId?: string): Promise { - if (workspaceId) return this.projectService.findByWorkspace(workspaceId); - return this.projectService.findAll(); + @ApiResponse({ status: 200, description: 'Paginated list of projects' }) + findAll( + @Query() pagination: PaginationDto, + @Query('workspaceId') workspaceId: string, + @Req() req: Request, + ) { + const baseUrl = `${req.protocol}://${req.get('host')}${req.path}`; + if (workspaceId) return this.projectService.findByWorkspace(workspaceId, pagination, baseUrl); + return this.projectService.findAll(pagination, baseUrl); } @Get(':id') @@ -50,7 +57,7 @@ export class ProjectController { @Delete(':id') @ApiOperation({ summary: 'Delete a project' }) @ApiParam({ name: 'id', description: 'Project ID' }) - @ApiResponse({ status: 200, description: 'Project deleted' }) + @ApiResponse({ status: 200, description: 'Project deleted', type: TMProject }) @ApiResponse({ status: 404, description: 'Project not found' }) remove(@Param('id') id: string): Promise { return this.projectService.remove(id); diff --git a/src/modules/task-manager/providers/project.service.ts b/src/modules/task-manager/providers/project.service.ts index 752aede..771916c 100644 --- a/src/modules/task-manager/providers/project.service.ts +++ b/src/modules/task-manager/providers/project.service.ts @@ -1,12 +1,15 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { In, Repository } from "typeorm"; -import { ProjectRepository } from "../repositories/project.repository"; -import { TMProject } from "../entities/project.entity"; -import { CreateProjectDto } from "../dto/project/create-project.dto"; -import { UpdateProjectDto } from "../dto/project/update-project.dto"; -import { User } from "../../users/entities/user.entity"; -import { WorkspaceRepository } from "../repositories/workspace.repository"; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { ProjectRepository } from '../repositories/project.repository'; +import { WorkspaceRepository } from '../repositories/workspace.repository'; +import { TMProject } from '../entities/project.entity'; +import { CreateProjectDto } from '../dto/project/create-project.dto'; +import { UpdateProjectDto } from '../dto/project/update-project.dto'; +import { PaginationDto } from '../../../common/DTO/pagination.dto'; +import { PaginatedResult } from '../../../common/interfaces/paginated-result.interface'; +import { buildPageFormat } from '../../../common/helpers/pagination.helper'; +import { User } from '../../users/entities/user.entity'; @Injectable() export class ProjectService { @@ -17,12 +20,34 @@ export class ProjectService { private readonly userRepository: Repository, ) {} - findAll(): Promise { - return this.projectRepository.findAll(); + async findAll(pagination: PaginationDto, baseUrl: string): Promise> { + const page = pagination.page ?? 1; + const limit = pagination.limit ?? 10; + const skip = (page - 1) * limit; + + const [data, totalItems] = await this.projectRepository.findAll(skip, limit); + + return { + data, + meta: buildPageFormat(page, limit, totalItems, baseUrl), + }; } - findByWorkspace(workspaceId: string): Promise { - return this.projectRepository.findByWorkspace(workspaceId); + async findByWorkspace( + workspaceId: string, + pagination: PaginationDto, + baseUrl: string, + ): Promise> { + const page = pagination.page ?? 1; + const limit = pagination.limit ?? 10; + const skip = (page - 1) * limit; + + const [data, totalItems] = await this.projectRepository.findByWorkspace(workspaceId, skip, limit); + + return { + data, + meta: buildPageFormat(page, limit, totalItems, baseUrl), + }; } async findOne(id: string): Promise { @@ -46,7 +71,9 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((id) => !workspaceUserIds.includes(id)); if (invalidUsers.length) { - throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + throw new BadRequestException( + `Users [${invalidUsers.join(', ')}] are not members of this workspace`, + ); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } @@ -72,7 +99,9 @@ export class ProjectService { const workspaceUserIds = workspace.users.map((u) => u.id); const invalidUsers = userIds.filter((uid) => !workspaceUserIds.includes(uid)); if (invalidUsers.length) { - throw new BadRequestException(`Users [${invalidUsers.join(", ")}] are not members of this workspace`); + throw new BadRequestException( + `Users [${invalidUsers.join(', ')}] are not members of this workspace`, + ); } project.users = await this.userRepository.findBy({ id: In(userIds) }); } else { @@ -86,7 +115,6 @@ export class ProjectService { async remove(id: string): Promise { const project = await this.findOne(id); await this.projectRepository.delete(id); - return project; } -} +} \ No newline at end of file diff --git a/src/modules/task-manager/repositories/project.repository.ts b/src/modules/task-manager/repositories/project.repository.ts index 0a4a1c4..6b0535a 100644 --- a/src/modules/task-manager/repositories/project.repository.ts +++ b/src/modules/task-manager/repositories/project.repository.ts @@ -1,7 +1,7 @@ -import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; -import { TMProject } from "../entities/project.entity"; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { TMProject } from '../entities/project.entity'; @Injectable() export class ProjectRepository { @@ -10,23 +10,27 @@ export class ProjectRepository { private readonly repository: Repository, ) {} - findAll() { - return this.repository.find({ - relations: ["workspace", "taskPhases", "users"], + findAll(skip: number, take: number) { + return this.repository.findAndCount({ + relations: ['workspace', 'taskPhases', 'users'], + skip, + take, }); } - findByWorkspace(workspaceId: string) { - return this.repository.find({ + findByWorkspace(workspaceId: string, skip: number, take: number) { + return this.repository.findAndCount({ where: { workspaceId }, - relations: ["taskPhases", "users"], + relations: ['taskPhases', 'users'], + skip, + take, }); } findOneById(id: string) { return this.repository.findOne({ where: { id }, - relations: ["workspace", "taskPhases", "users"], + relations: ['workspace', 'taskPhases', 'users'], }); } @@ -45,4 +49,4 @@ export class ProjectRepository { delete(id: string) { return this.repository.delete(id); } -} +} \ No newline at end of file