added pagination, searching, sorting
This commit is contained in:
parent
680918618b
commit
2079f3873c
47
src/form/dto/query-form.dto.ts
Normal file
47
src/form/dto/query-form.dto.ts
Normal 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;
|
||||
}
|
||||
@ -6,12 +6,13 @@ import {
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { FormService } from './form.service';
|
||||
import { ApiOperation } from '@nestjs/swagger';
|
||||
import { UpdateFieldDto } from './dto/update-field.dto';
|
||||
import { CreateUpdateDto } from './dto/create-update.dto';
|
||||
|
||||
import { QueryFormDto } from './dto/query-form.dto';
|
||||
|
||||
@Controller('form')
|
||||
export class FormController {
|
||||
@ -24,9 +25,9 @@ export class FormController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Find all forms' })
|
||||
async findAll() {
|
||||
return await this.formService.findAll();
|
||||
@ApiOperation({ summary: 'Get all forms with pagination, search, sort' })
|
||||
findAll(@Query() query: QueryFormDto) {
|
||||
return this.formService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':formId')
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { FormModule } from './form.module';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Form, FormDocument, Status } from './schemas/form.schema';
|
||||
@ -6,7 +5,20 @@ import { Model } from 'mongoose';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { UpdateFieldDto } from './dto/update-field.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()
|
||||
export class FormService {
|
||||
@ -30,18 +42,50 @@ export class FormService {
|
||||
const updatedForm = await this.formModel.findOneAndUpdate(
|
||||
{ id: dto.id },
|
||||
{ $push: { fields: newField } },
|
||||
{ new: true },
|
||||
{ new: true, projection: DETAIL_PROJECTION },
|
||||
);
|
||||
|
||||
if (!updatedForm) throw new NotFoundException(`Form ${dto.id} not found`);
|
||||
return form;
|
||||
return updatedForm;
|
||||
}
|
||||
|
||||
async findAll(): Promise<Form[]> {
|
||||
return await this.formModel.find().exec();
|
||||
async findAll(query: QueryFormDto): Promise<PaginatedResponse<Form>> {
|
||||
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> {
|
||||
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`);
|
||||
return form;
|
||||
}
|
||||
@ -63,6 +107,7 @@ export class FormService {
|
||||
{
|
||||
new: true,
|
||||
arrayFilters: [{ 'elem.id': fieldId }],
|
||||
projection: DETAIL_PROJECTION,
|
||||
},
|
||||
);
|
||||
if (!form) throw new NotFoundException(`Form ${formId} not found`);
|
||||
@ -73,7 +118,7 @@ export class FormService {
|
||||
const form = await this.formModel.findOneAndUpdate(
|
||||
{ id: formId },
|
||||
{ $pull: { fields: { id: fieldId } } },
|
||||
{ new: true },
|
||||
{ new: true, projection: DETAIL_PROJECTION },
|
||||
);
|
||||
if (!form) throw new NotFoundException(`Form ${formId} not found`);
|
||||
return form;
|
||||
@ -81,13 +126,13 @@ export class FormService {
|
||||
|
||||
async softDelete(FormId: string): Promise<Form> {
|
||||
const form = await this.formModel.findOneAndUpdate(
|
||||
{ id: FormId, deletedAt:null }, // prevent double deletion
|
||||
{ id: FormId, deletedAt: null }, // prevent double deletion
|
||||
{
|
||||
$set: {
|
||||
deletedAt: new Date(),
|
||||
},
|
||||
},
|
||||
{ new: true },
|
||||
{ new: true, projection: DETAIL_PROJECTION },
|
||||
);
|
||||
|
||||
if (!form) throw new NotFoundException(`Form ${FormId} not found`);
|
||||
@ -96,13 +141,13 @@ export class FormService {
|
||||
|
||||
async restore(FormId: string): Promise<Form> {
|
||||
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: {
|
||||
deletedAt: null,
|
||||
},
|
||||
},
|
||||
{ new: true },
|
||||
{ new: true, projection: DETAIL_PROJECTION },
|
||||
);
|
||||
if (!form) throw new NotFoundException(`Form ${FormId} not found`);
|
||||
return form;
|
||||
|
||||
30
src/form/helpers/form-query.builder.ts
Normal file
30
src/form/helpers/form-query.builder.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
src/interfaces/paginated-response.interface.ts
Normal file
11
src/interfaces/paginated-response.interface.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
meta: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user