import { Injectable, NotFoundException, ServiceUnavailableException, } 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 { QueryFormDto } from './dto/query-form.dto'; import { FormQueryBuilder } from './helpers/form-query.builder'; import { PaginatedResponse } from 'src/interfaces/paginated-response.interface'; import { LlmService } from 'src/common/services/llm.service'; import { CreateFormDto } from './dto/create-form.dto'; // 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, private llmService: LlmService, ) {} async createForm(formDto: CreateFormDto): Promise
{ const form = await this.formModel.create({ id: uuidv4(), ...formDto, }); return form; } 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(DETAIL_PROJECTION) .exec(); if (!form) throw new NotFoundException(`Form ${formId} not found`); return form; } async findInterface(formId: string): Promise { const form = await this.formModel .findOne({ id: formId, deletedAt: null }) .select(DETAIL_PROJECTION) .exec(); if (!form) throw new NotFoundException(`Form ${formId} not found`); try { let tsInference = await this.llmService.generateFormInterface(form); return tsInference; } catch (err) { throw new ServiceUnavailableException('llm down'); } } 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; } }