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,
|
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')
|
||||||
|
|||||||
@ -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;
|
||||||
@ -87,7 +132,7 @@ export class FormService {
|
|||||||
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`);
|
||||||
@ -102,7 +147,7 @@ export class FormService {
|
|||||||
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;
|
||||||
|
|||||||
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