added pagination, searching, sorting

This commit is contained in:
Suman991 2026-04-09 16:31:07 +05:30
parent 680918618b
commit 2079f3873c
5 changed files with 149 additions and 15 deletions

View File

@ -0,0 +1,47 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
export enum SortBy {
NAME = 'name',
CREATED_AT = 'createdAt',
UPDATED_AT = 'updatedAt',
STATUS = 'status',
}
export class QueryFormDto {
@ApiPropertyOptional({ example: 1 })
@Type(() => Number)
@IsInt()
@Min(1)
@IsOptional()
page?: number = 1;
@ApiPropertyOptional({ example: 10 })
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
@IsOptional()
limit?: number = 10;
@ApiPropertyOptional({ example: '' })
@IsString()
@IsOptional()
search?: string;
@ApiPropertyOptional({ enum: SortBy, example: SortBy.CREATED_AT })
@IsEnum(SortBy)
@IsOptional()
sortBy?: SortBy = SortBy.CREATED_AT;
@ApiPropertyOptional({ enum: SortOrder, example: SortOrder.DESC })
@IsEnum(SortOrder)
@IsOptional()
sortOrder?: SortOrder = SortOrder.DESC;
}

View File

@ -6,12 +6,13 @@ import {
Patch, Patch,
Param, Param,
Delete, Delete,
Query,
} from '@nestjs/common'; } from '@nestjs/common';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { ApiOperation } from '@nestjs/swagger'; import { ApiOperation } from '@nestjs/swagger';
import { UpdateFieldDto } from './dto/update-field.dto'; import { UpdateFieldDto } from './dto/update-field.dto';
import { CreateUpdateDto } from './dto/create-update.dto'; import { CreateUpdateDto } from './dto/create-update.dto';
import { QueryFormDto } from './dto/query-form.dto';
@Controller('form') @Controller('form')
export class FormController { export class FormController {
@ -24,9 +25,9 @@ export class FormController {
} }
@Get() @Get()
@ApiOperation({ summary: 'Find all forms' }) @ApiOperation({ summary: 'Get all forms with pagination, search, sort' })
async findAll() { findAll(@Query() query: QueryFormDto) {
return await this.formService.findAll(); return this.formService.findAll(query);
} }
@Get(':formId') @Get(':formId')

View File

@ -1,4 +1,3 @@
import { FormModule } from './form.module';
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Form, FormDocument, Status } from './schemas/form.schema'; import { Form, FormDocument, Status } from './schemas/form.schema';
@ -6,7 +5,20 @@ import { Model } from 'mongoose';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { UpdateFieldDto } from './dto/update-field.dto'; import { UpdateFieldDto } from './dto/update-field.dto';
import { CreateUpdateDto } from './dto/create-update.dto'; import { CreateUpdateDto } from './dto/create-update.dto';
import { QueryFormDto } from './dto/query-form.dto';
import { FormQueryBuilder } from './helpers/form-query.builder';
import { PaginatedResponse } from 'src/interfaces/paginated-response.interface';
// Reusable projections
const LIST_PROJECTION = {
_id: 0,
id: 1,
name: 1,
status: 1,
deletedAt: 1,
createdAt: 1,
};
const DETAIL_PROJECTION = { _id: 0, __v: 0 };
@Injectable() @Injectable()
export class FormService { export class FormService {
@ -30,18 +42,50 @@ export class FormService {
const updatedForm = await this.formModel.findOneAndUpdate( const updatedForm = await this.formModel.findOneAndUpdate(
{ id: dto.id }, { id: dto.id },
{ $push: { fields: newField } }, { $push: { fields: newField } },
{ new: true }, { new: true, projection: DETAIL_PROJECTION },
); );
if (!updatedForm) throw new NotFoundException(`Form ${dto.id} not found`); if (!updatedForm) throw new NotFoundException(`Form ${dto.id} not found`);
return form; return updatedForm;
} }
async findAll(): Promise<Form[]> { async findAll(query: QueryFormDto): Promise<PaginatedResponse<Form>> {
return await this.formModel.find().exec(); const filter = FormQueryBuilder.buildFilter(query);
const sort = FormQueryBuilder.buildSort(query);
const { skip, limit } = FormQueryBuilder.buildPagination(query);
// Run count and data fetch in parallel
const [total, data] = await Promise.all([
this.formModel.countDocuments(filter),
this.formModel
.find(filter)
.select(DETAIL_PROJECTION)
.sort(sort)
.skip(skip)
.limit(limit)
.exec(),
]);
const page = query.page ?? 1;
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
},
};
} }
async find(formId: string): Promise<Form> { async find(formId: string): Promise<Form> {
const form = await this.formModel.findOne({ id: formId, deletedAt:null }).exec(); const form = await this.formModel
.findOne({ id: formId, deletedAt: null })
.select(LIST_PROJECTION)
.exec();
if (!form) throw new NotFoundException(`Form ${formId} not found`); if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form; return form;
} }
@ -63,6 +107,7 @@ export class FormService {
{ {
new: true, new: true,
arrayFilters: [{ 'elem.id': fieldId }], arrayFilters: [{ 'elem.id': fieldId }],
projection: DETAIL_PROJECTION,
}, },
); );
if (!form) throw new NotFoundException(`Form ${formId} not found`); if (!form) throw new NotFoundException(`Form ${formId} not found`);
@ -73,7 +118,7 @@ export class FormService {
const form = await this.formModel.findOneAndUpdate( const form = await this.formModel.findOneAndUpdate(
{ id: formId }, { id: formId },
{ $pull: { fields: { id: fieldId } } }, { $pull: { fields: { id: fieldId } } },
{ new: true }, { new: true, projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${formId} not found`); if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form; return form;
@ -81,13 +126,13 @@ export class FormService {
async softDelete(FormId: string): Promise<Form> { async softDelete(FormId: string): Promise<Form> {
const form = await this.formModel.findOneAndUpdate( const form = await this.formModel.findOneAndUpdate(
{ id: FormId, deletedAt:null }, // prevent double deletion { id: FormId, deletedAt: null }, // prevent double deletion
{ {
$set: { $set: {
deletedAt: new Date(), deletedAt: new Date(),
}, },
}, },
{ new: true }, { new: true, projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${FormId} not found`); if (!form) throw new NotFoundException(`Form ${FormId} not found`);
@ -96,13 +141,13 @@ export class FormService {
async restore(FormId: string): Promise<Form> { async restore(FormId: string): Promise<Form> {
const form = await this.formModel.findOneAndUpdate( const form = await this.formModel.findOneAndUpdate(
{ id: FormId, deletedAt:{$ne:null} }, // only restore if actually deleted { id: FormId, deletedAt: { $ne: null } }, // only restore if actually deleted
{ {
$set: { $set: {
deletedAt: null, deletedAt: null,
}, },
}, },
{ new: true }, { new: true, projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${FormId} not found`); if (!form) throw new NotFoundException(`Form ${FormId} not found`);
return form; return form;

View File

@ -0,0 +1,30 @@
import { QueryFormDto, SortOrder } from '../dto/query-form.dto';
export class FormQueryBuilder {
static buildFilter(query: QueryFormDto): Record<string, unknown> {
const filter: Record<string, unknown> = {};
if (query.search) {
filter.name = { $regex: query.search, $options: 'i' };
}
return filter;
}
// Build sort
static buildSort(query: QueryFormDto): Record<string, 1 | -1> {
return {
[query.sortBy ?? 'createdAt']: query.sortOrder === SortOrder.ASC ? 1 : -1,
};
}
// Build pagination
static buildPagination(query: QueryFormDto): { skip: number; limit: number } {
const page = query.page ?? 1;
const limit = query.limit ?? 10;
return {
skip: (page - 1) * limit,
limit,
};
}
}

View File

@ -0,0 +1,11 @@
export interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}