Form-utility-backend/src/form/form.service.ts
2026-04-13 14:09:32 +05:30

164 lines
4.4 KiB
TypeScript

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<FormDocument>,
private llmService: LlmService,
) {}
async createForm(formDto: CreateFormDto): Promise<Form> {
const form = await this.formModel.create({
id: uuidv4(),
...formDto,
});
return form;
}
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(DETAIL_PROJECTION)
.exec();
if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form;
}
async findInterface(formId: string): Promise<string> {
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<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;
}
}