Form-utility-backend/src/form/form.service.ts
2026-04-09 16:31:07 +05:30

156 lines
4.4 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Form, FormDocument, Status } from './schemas/form.schema';
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 {
constructor(@InjectModel(Form.name) private formModel: Model<FormDocument>) {}
async createOrUpdate(dto: CreateUpdateDto): Promise<Form> {
if (!dto.id) {
return this.formModel.create({
id: uuidv4(),
name: dto.name ?? 'Untitled Form',
});
}
const form = await this.formModel.findOne({ id: dto.id });
if (!form) throw new NotFoundException(`Form ${dto.id} not found`);
if (!dto.field) return form; // nothing to add, return as-is
const newField = { id: uuidv4(), ...dto.field };
const updatedForm = await this.formModel.findOneAndUpdate(
{ id: dto.id },
{ $push: { fields: newField } },
{ new: true, projection: DETAIL_PROJECTION },
);
if (!updatedForm) throw new NotFoundException(`Form ${dto.id} not found`);
return updatedForm;
}
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 })
.select(LIST_PROJECTION)
.exec();
if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form;
}
async updateField(
formId: string,
fieldId: string,
updateFieldDto: UpdateFieldDto,
): Promise<Form> {
const setPayload = Object.fromEntries(
Object.entries(updateFieldDto).map(([k, v]) => [
`fields.$[elem].${k}`,
v,
]),
);
const form = await this.formModel.findOneAndUpdate(
{ id: formId },
{ $set: setPayload },
{
new: true,
arrayFilters: [{ 'elem.id': fieldId }],
projection: DETAIL_PROJECTION,
},
);
if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form;
}
async deleteField(formId: string, fieldId: string): Promise<Form> {
const form = await this.formModel.findOneAndUpdate(
{ id: formId },
{ $pull: { fields: { id: fieldId } } },
{ new: true, projection: DETAIL_PROJECTION },
);
if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form;
}
async softDelete(FormId: string): Promise<Form> {
const form = await this.formModel.findOneAndUpdate(
{ id: FormId, deletedAt: null }, // prevent double deletion
{
$set: {
deletedAt: new Date(),
},
},
{ new: true, projection: DETAIL_PROJECTION },
);
if (!form) throw new NotFoundException(`Form ${FormId} not found`);
return form;
}
async restore(FormId: string): Promise<Form> {
const form = await this.formModel.findOneAndUpdate(
{ id: FormId, deletedAt: { $ne: null } }, // only restore if actually deleted
{
$set: {
deletedAt: null,
},
},
{ new: true, projection: DETAIL_PROJECTION },
);
if (!form) throw new NotFoundException(`Form ${FormId} not found`);
return form;
}
}