feat: added cursor-based pagination to checklistitems

This commit is contained in:
2026-07-04 10:14:33 +03:30
parent c22a41d29b
commit 06ec73be92
6 changed files with 142 additions and 55 deletions
+18
View File
@@ -0,0 +1,18 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsInt, IsISO8601, IsOptional, Max, Min } from "class-validator";
export class CursorPaginationDto {
@IsOptional()
@IsISO8601()
@ApiPropertyOptional({ type: "string", description: "Cursor (createdAt of last seen item)", example: "2026-07-01T10:00:00.000Z" })
cursor?: string;
@IsOptional()
@IsInt()
@Min(1)
@Max(50)
@Type(() => Number)
@ApiPropertyOptional({ type: "number", required: false, default: 10 })
limit?: number;
}
@@ -0,0 +1,25 @@
import { CursorPaginatedResult } from "../interfaces/cursor-paginated-result.interface";
export function buildCursorPageFormat<T extends { createdAt: Date }>(
data: T[],
limit: number,
previousCursor?: Date,
): CursorPaginatedResult<T> {
const hasMore = data.length > limit;
if (hasMore) data.pop();
const nextCursor = hasMore ? data[data.length - 1].createdAt.toISOString() : null;
const prevCursor = previousCursor ? previousCursor.toISOString() : null;
return {
data,
meta: {
nextCursor,
prevCursor,
hasMore,
limit,
},
};
}
@@ -0,0 +1,9 @@
export interface CursorPaginatedResult<T> {
data: T[];
meta: {
nextCursor: string | null;
prevCursor: string | null;
hasMore: boolean;
limit: number;
};
}
@@ -1,56 +1,59 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { CheckListItemService } from '../providers/check-list-item.service';
import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto';
import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto';
import { TMCheckListItem } from '../entities/check-list-item.entity';
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from "@nestjs/swagger";
import { CheckListItemService } from "../providers/check-list-item.service";
import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto";
import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto";
import { CursorPaginationDto } from "../../../common/DTO/cursor-pagination.dto";
import { TMCheckListItem } from "../entities/check-list-item.entity";
@ApiTags('Task Manager - Check List Items')
@Controller('task-manager/check-list-items')
@ApiTags("Task Manager - Check List Items")
@Controller("task-manager/check-list-items")
export class CheckListItemController {
constructor(private readonly checkListItemService: CheckListItemService) {}
@Get()
@ApiOperation({ summary: 'Get all check list items, optionally filtered by task' })
@ApiQuery({ name: 'taskId', required: false, description: 'Filter check list items by task ID' })
@ApiResponse({ status: 200, description: 'List of check list items', type: [TMCheckListItem] })
findAll(@Query('taskId') taskId?: string): Promise<TMCheckListItem[]> {
if (taskId) return this.checkListItemService.findByTask(taskId);
return this.checkListItemService.findAll();
@ApiOperation({ summary: "Get all check list items, optionally filtered by task" })
@ApiQuery({ name: "taskId", required: false, description: "Filter check list items by task ID" })
@ApiQuery({ name: "cursor", required: false, description: "Cursor for pagination (createdAt ISO string)" })
@ApiQuery({ name: "limit", required: false, description: "Number of records to return (max 50)" })
@ApiResponse({ status: 200, description: "Cursor-paginated list of check list items" })
findAll(@Query() pagination: CursorPaginationDto, @Query("taskId") taskId?: string) {
if (taskId) return this.checkListItemService.findByTask(taskId, pagination);
return this.checkListItemService.findAll(pagination);
}
@Get(':id')
@ApiOperation({ summary: 'Get a check list item by ID' })
@ApiParam({ name: 'id', description: 'CheckListItem ID' })
@ApiResponse({ status: 200, description: 'CheckListItem found', type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem not found' })
findOne(@Param('id') id: string): Promise<TMCheckListItem> {
@Get(":id")
@ApiOperation({ summary: "Get a check list item by ID" })
@ApiParam({ name: "id", description: "CheckListItem ID" })
@ApiResponse({ status: 200, description: "CheckListItem found", type: TMCheckListItem })
@ApiResponse({ status: 404, description: "CheckListItem not found" })
findOne(@Param("id") id: string): Promise<TMCheckListItem> {
return this.checkListItemService.findOne(id);
}
@Post()
@ApiOperation({ summary: 'Create a new check list item' })
@ApiResponse({ status: 201, description: 'CheckListItem created', type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'Task not found' })
@ApiOperation({ summary: "Create a new check list item" })
@ApiResponse({ status: 201, description: "CheckListItem created", type: TMCheckListItem })
@ApiResponse({ status: 404, description: "Task not found" })
create(@Body() body: CreateCheckListItemDto): Promise<TMCheckListItem> {
return this.checkListItemService.create(body);
}
@Patch(':id')
@ApiOperation({ summary: 'Update a check list item' })
@ApiParam({ name: 'id', description: 'CheckListItem ID' })
@ApiResponse({ status: 200, description: 'CheckListItem updated', type: TMCheckListItem })
@ApiResponse({ status: 404, description: 'CheckListItem or Task not found' })
update(@Param('id') id: string, @Body() body: UpdateCheckListItemDto): Promise<TMCheckListItem> {
@Patch(":id")
@ApiOperation({ summary: "Update a check list item" })
@ApiParam({ name: "id", description: "CheckListItem ID" })
@ApiResponse({ status: 200, description: "CheckListItem updated", type: TMCheckListItem })
@ApiResponse({ status: 404, description: "CheckListItem or Task not found" })
update(@Param("id") id: string, @Body() body: UpdateCheckListItemDto): Promise<TMCheckListItem> {
return this.checkListItemService.update(id, body);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a check list item' })
@ApiParam({ name: 'id', description: 'CheckListItem ID' })
@ApiResponse({ status: 200, description: 'CheckListItem deleted' })
@ApiResponse({ status: 404, description: 'CheckListItem not found' })
remove(@Param('id') id: string): Promise<void> {
@Delete(":id")
@ApiOperation({ summary: "Delete a check list item" })
@ApiParam({ name: "id", description: "CheckListItem ID" })
@ApiResponse({ status: 200, description: "CheckListItem deleted", type: TMCheckListItem })
@ApiResponse({ status: 404, description: "CheckListItem not found" })
remove(@Param("id") id: string): Promise<TMCheckListItem> {
return this.checkListItemService.remove(id);
}
}
@@ -1,9 +1,12 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { CheckListItemRepository } from "../repositories/check-list-item.repository";
import { TaskRepository } from "../repositories/task.repository";
import { TMCheckListItem } from "../entities/check-list-item.entity";
import { CreateCheckListItemDto } from "../dto/check-list-item/create-check-list-item.dto";
import { UpdateCheckListItemDto } from "../dto/check-list-item/update-check-list-item.dto";
import { Injectable, NotFoundException } from '@nestjs/common';
import { CheckListItemRepository } from '../repositories/check-list-item.repository';
import { TaskRepository } from '../repositories/task.repository';
import { TMCheckListItem } from '../entities/check-list-item.entity';
import { CreateCheckListItemDto } from '../dto/check-list-item/create-check-list-item.dto';
import { UpdateCheckListItemDto } from '../dto/check-list-item/update-check-list-item.dto';
import { CursorPaginationDto } from '../../../common/DTO/cursor-pagination.dto';
import { CursorPaginatedResult } from '../../../common/interfaces/cursor-paginated-result.interface';
import { buildCursorPageFormat } from '../../../common/helpers/cursor-paginated.helper';
@Injectable()
export class CheckListItemService {
@@ -12,12 +15,25 @@ export class CheckListItemService {
private readonly taskRepository: TaskRepository,
) {}
findAll(): Promise<TMCheckListItem[]> {
return this.checkListItemRepository.findAll();
async findAll(pagination: CursorPaginationDto): Promise<CursorPaginatedResult<TMCheckListItem>> {
const limit = pagination.limit ?? 10;
const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined;
const [data] = await this.checkListItemRepository.findAll(limit, cursorDate);
return buildCursorPageFormat(data, limit, cursorDate);
}
findByTask(taskId: string): Promise<TMCheckListItem[]> {
return this.checkListItemRepository.findByTask(taskId);
async findByTask(
taskId: string,
pagination: CursorPaginationDto,
): Promise<CursorPaginatedResult<TMCheckListItem>> {
const limit = pagination.limit ?? 10;
const cursorDate = pagination.cursor ? new Date(pagination.cursor) : undefined;
const [data] = await this.checkListItemRepository.findByTask(taskId, limit, cursorDate);
return buildCursorPageFormat(data, limit, cursorDate);
}
async findOne(id: string): Promise<TMCheckListItem> {
@@ -46,8 +62,9 @@ export class CheckListItemService {
return this.checkListItemRepository.save(checkListItem);
}
async remove(id: string): Promise<void> {
await this.findOne(id);
async remove(id: string): Promise<TMCheckListItem> {
const checkListItem = await this.findOne(id);
await this.checkListItemRepository.delete(id);
return checkListItem;
}
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Repository, MoreThan } from 'typeorm';
import { TMCheckListItem } from '../entities/check-list-item.entity';
@Injectable()
@@ -10,15 +10,23 @@ export class CheckListItemRepository {
private readonly repository: Repository<TMCheckListItem>,
) {}
findAll() {
return this.repository.find({
findAll(limit: number, cursor?: Date) {
return this.repository.findAndCount({
where: cursor ? { createdAt: MoreThan(cursor) } : {},
order: { createdAt: 'ASC' },
take: limit + 1,
relations: ['task'],
});
}
findByTask(taskId: string) {
return this.repository.find({
where: { taskId },
findByTask(taskId: string, limit: number, cursor?: Date) {
return this.repository.findAndCount({
where: cursor
? { taskId, createdAt: MoreThan(cursor) }
: { taskId },
order: { createdAt: 'ASC' },
take: limit + 1,
relations: ['task'],
});
}
@@ -29,6 +37,13 @@ export class CheckListItemRepository {
});
}
findOneByCreatedAt(createdAt: Date) {
return this.repository.findOne({
where: { createdAt },
order: { createdAt: 'ASC' },
});
}
create(data: Partial<TMCheckListItem>) {
return this.repository.create(data);
}