Compare commits

...

4 Commits

Author SHA1 Message Date
Suman991
62e13e673f added get stat api 2026-04-13 17:43:52 +05:30
Suman991
1f4ddfa10b optimized schema 2026-04-13 16:41:00 +05:30
Suman991
5f34171a4a add field to form api added 2026-04-13 15:25:39 +05:30
Suman991
4c1ecafa77 one step create form api added 2026-04-13 14:09:32 +05:30
6 changed files with 114 additions and 75 deletions

View File

@ -30,7 +30,7 @@ export class CreateFieldDto {
@IsOptional() @IsOptional()
placeholder?: string; placeholder?: string;
@ApiPropertyOptional({ type: [String], example: ['Very Satisfied', 'Satisfied'] }) @ApiPropertyOptional({ type: [String], example: ['Very Satisfied', 'Satisfied']})
@IsArray() @IsArray()
@IsString({ each: true }) @IsString({ each: true })
@IsOptional() @IsOptional()

View File

@ -0,0 +1,17 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator";
import { CreateFieldDto } from "./create-field.dto";
import { Type } from "class-transformer";
export class CreateFormDto {
@ApiPropertyOptional({ example: 'Customer Feedback Survey' })
@IsString()
name?: string;
@ApiPropertyOptional({ type: [CreateFieldDto] })
@ValidateNested({each:true})
@Type(() => CreateFieldDto)
@IsArray()
@IsOptional()
fields?: CreateFieldDto[];
}

View File

@ -1,22 +0,0 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional, IsString, ValidateNested } from "class-validator";
import { CreateFieldDto } from "./create-field.dto";
import { Type } from "class-transformer";
export class CreateUpdateDto {
@ApiPropertyOptional({ example: '550e8400-uuid', description: 'Form ID — omit to create a new form' })
@IsString()
@IsOptional()
id?: string;
@ApiPropertyOptional({ example: 'Customer Feedback Survey' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ type: CreateFieldDto })
@ValidateNested()
@Type(() => CreateFieldDto)
@IsOptional()
field?: CreateFieldDto;
}

View File

@ -11,17 +11,18 @@ import {
import { FormService } from './form.service'; import { FormService } from './form.service';
import { ApiOperation } from '@nestjs/swagger'; import { ApiOperation } from '@nestjs/swagger';
import { UpdateFieldDto } from './dto/update-field.dto'; import { UpdateFieldDto } from './dto/update-field.dto';
import { CreateUpdateDto } from './dto/create-update.dto';
import { QueryFormDto } from './dto/query-form.dto'; import { QueryFormDto } from './dto/query-form.dto';
import { CreateFormDto } from './dto/create-form.dto';
import { CreateFieldDto } from './dto/create-field.dto';
@Controller('form') @Controller('form')
export class FormController { export class FormController {
constructor(private readonly formService: FormService) {} constructor(private readonly formService: FormService) {}
@Post('create-or-update') @Post('create-form')
@ApiOperation({ summary: 'Create Empty Form, Add Field to form' }) @ApiOperation({ summary: 'Create a Form' })
async createOne(@Body() createUpdatedDto: CreateUpdateDto) { async createOne(@Body() createFormDto: CreateFormDto) {
return await this.formService.createOrUpdate(createUpdatedDto); return await this.formService.createForm(createFormDto);
} }
@Get() @Get()
@ -30,17 +31,33 @@ export class FormController {
return this.formService.findAll(query); return this.formService.findAll(query);
} }
@Get('stats')
@ApiOperation({ summary: 'Get Stats' })
async getStats() {
return await this.formService.getStats();
}
@Get(':formId') @Get(':formId')
@ApiOperation({ summary: 'Find a form' }) @ApiOperation({ summary: 'Find a form' })
async find(@Param('formId') formId: string) { async find(@Param('formId') formId: string) {
return await this.formService.find(formId); return await this.formService.find(formId);
} }
@Get(':formId/interface') @Get(':formId/interface')
@ApiOperation({summary:'Get interface for the form provided by llm'}) @ApiOperation({ summary: 'Get interface for the form provided by llm' })
async getInterface(@Param('formId') formId:string){ async getInterface(@Param('formId') formId: string) {
return await this.formService.findInterface(formId); return await this.formService.findInterface(formId);
} }
@Patch(':formId/field')
@ApiOperation({ summary: 'Add a field to a form' })
async addField(
@Param('formId') formId: string,
@Body() createFieldDto: CreateFieldDto,
) {
return await this.formService.addField(formId, createFieldDto);
}
@Patch(':id/fields/:fieldId') @Patch(':id/fields/:fieldId')
@ApiOperation({ summary: 'update a field in a form' }) @ApiOperation({ summary: 'update a field in a form' })
async updateField( async updateField(

View File

@ -1,14 +1,19 @@
import { Injectable, NotFoundException, ServiceUnavailableException } from '@nestjs/common'; import {
Injectable,
NotFoundException,
ServiceUnavailableException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Form, FormDocument, Status } from './schemas/form.schema'; import { Form, FormDocument, Status } from './schemas/form.schema';
import { Model } from 'mongoose'; import { Model } from 'mongoose';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { UpdateFieldDto } from './dto/update-field.dto'; import { UpdateFieldDto } from './dto/update-field.dto';
import { CreateUpdateDto } from './dto/create-update.dto';
import { QueryFormDto } from './dto/query-form.dto'; import { QueryFormDto } from './dto/query-form.dto';
import { FormQueryBuilder } from './helpers/form-query.builder'; import { FormQueryBuilder } from './helpers/form-query.builder';
import { PaginatedResponse } from 'src/interfaces/paginated-response.interface'; import { PaginatedResponse } from 'src/interfaces/paginated-response.interface';
import { LlmService } from 'src/common/services/llm.service'; import { LlmService } from 'src/common/services/llm.service';
import { CreateFormDto } from './dto/create-form.dto';
import { CreateFieldDto } from './dto/create-field.dto';
// Reusable projections // Reusable projections
const LIST_PROJECTION = { const LIST_PROJECTION = {
@ -28,28 +33,21 @@ export class FormService {
private llmService: LlmService, private llmService: LlmService,
) {} ) {}
async createOrUpdate(dto: CreateUpdateDto): Promise<Form> { async createForm(formDto: CreateFormDto): Promise<Form> {
if (!dto.id) { const form = await this.formModel.create({
return this.formModel.create({
id: uuidv4(), id: uuidv4(),
name: dto.name ?? 'Untitled Form', ...formDto,
}); });
return form;
} }
const form = await this.formModel.findOne({ id: dto.id }); async addField(formId: string, fieldDto: CreateFieldDto): Promise<Form> {
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( const updatedForm = await this.formModel.findOneAndUpdate(
{ id: dto.id }, { id: formId },
{ $push: { fields: newField } }, { $push: { fields: fieldDto } },
{ new: true, projection: DETAIL_PROJECTION }, { returnDocument: 'after' }, //as {new:true} will be deprecated
); );
if (!updatedForm) throw new NotFoundException(`Form ${formId} not found`);
if (!updatedForm) throw new NotFoundException(`Form ${dto.id} not found`);
return updatedForm; return updatedForm;
} }
@ -95,6 +93,36 @@ export class FormService {
return form; return form;
} }
async getStats() {
const [totalForms, totalFields, fieldTypeBreakdown, formStatusBreakdown] =
await Promise.all([
this.formModel.countDocuments(),
this.formModel.aggregate([
{ $project: { fieldCount: { $size: '$fields' } } },
{ $group: { _id: null, total: { $sum: '$fieldCount' } } },
]),
this.formModel.aggregate([
{ $unwind: '$fields' },
{ $group: { _id: '$fields.keyType', count: { $sum: 1 } } },
]),
this.formModel.aggregate([
{
$group: {
_id: {
$cond: {
if: { $ifNull: ['$deletedAt', false] },
then: 'deleted',
else: 'active',
},
},
count: { $sum: 1 },
},
},
]),
]);
return { totalForms, totalFields, fieldTypeBreakdown, formStatusBreakdown };
}
async findInterface(formId: string): Promise<string> { async findInterface(formId: string): Promise<string> {
const form = await this.formModel const form = await this.formModel
.findOne({ id: formId, deletedAt: null }) .findOne({ id: formId, deletedAt: null })
@ -103,14 +131,13 @@ export class FormService {
if (!form) throw new NotFoundException(`Form ${formId} not found`); if (!form) throw new NotFoundException(`Form ${formId} not found`);
try{ try {
let tsInference=await this.llmService.generateFormInterface(form) let tsInference = await this.llmService.generateFormInterface(form);
return tsInference return tsInference;
}catch(err){ } catch (err) {
throw new ServiceUnavailableException('llm down') throw new ServiceUnavailableException('llm down');
}
} }
}
async updateField( async updateField(
formId: string, formId: string,
@ -127,7 +154,7 @@ export class FormService {
{ id: formId }, { id: formId },
{ $set: setPayload }, { $set: setPayload },
{ {
new: true, returnDocument: 'after',
arrayFilters: [{ 'elem.id': fieldId }], arrayFilters: [{ 'elem.id': fieldId }],
projection: DETAIL_PROJECTION, projection: DETAIL_PROJECTION,
}, },
@ -140,7 +167,7 @@ export class FormService {
const form = await this.formModel.findOneAndUpdate( const form = await this.formModel.findOneAndUpdate(
{ id: formId }, { id: formId },
{ $pull: { fields: { id: fieldId } } }, { $pull: { fields: { id: fieldId } } },
{ new: true, projection: DETAIL_PROJECTION }, { returnDocument: 'after', projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${formId} not found`); if (!form) throw new NotFoundException(`Form ${formId} not found`);
return form; return form;
@ -154,7 +181,7 @@ export class FormService {
deletedAt: new Date(), deletedAt: new Date(),
}, },
}, },
{ new: true, projection: DETAIL_PROJECTION }, { returnDocument: 'after', projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${FormId} not found`); if (!form) throw new NotFoundException(`Form ${FormId} not found`);
@ -169,7 +196,7 @@ export class FormService {
deletedAt: null, deletedAt: null,
}, },
}, },
{ new: true, projection: DETAIL_PROJECTION }, { returnDocument: 'after', projection: DETAIL_PROJECTION },
); );
if (!form) throw new NotFoundException(`Form ${FormId} not found`); if (!form) throw new NotFoundException(`Form ${FormId} not found`);
return form; return form;

View File

@ -1,6 +1,6 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose'; import { Document } from 'mongoose';
import { v4 as uuidv4 } from 'uuid';
import { PARENT_TYPE_KEYS, INPUT_SUB_TYPE_KEYS } from '../types/field.type'; import { PARENT_TYPE_KEYS, INPUT_SUB_TYPE_KEYS } from '../types/field.type';
import type { ParentKeyType, InputSubType } from '../types/field.type'; import type { ParentKeyType, InputSubType } from '../types/field.type';
@ -9,7 +9,7 @@ export type FieldDocument = Field & Document;
@Schema({ timestamps: true }) @Schema({ timestamps: true })
export class Field { export class Field {
@Prop({ required: true, unique:true }) @Prop({ required: true, unique: true, default: () => uuidv4() })
id!: string; id!: string;
@Prop({ required: true, enum: PARENT_TYPE_KEYS }) @Prop({ required: true, enum: PARENT_TYPE_KEYS })
@ -27,7 +27,7 @@ export class Field {
@Prop({ required: false }) @Prop({ required: false })
placeholder?: string; placeholder?: string;
@Prop({ required: false }) @Prop({ required: false, default: undefined })
options?: string[]; options?: string[];
@Prop({ required: true, default: false }) @Prop({ required: true, default: false })