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) {} async createOrUpdate(dto: CreateUpdateDto): Promise
{ 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> { 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 { 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 { 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 { 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 { 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 { 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; } }