156 lines
4.4 KiB
TypeScript
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;
|
|
}
|
|
}
|