Compare commits
4 Commits
main
...
feat/reduc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e13e673f | ||
|
|
1f4ddfa10b | ||
|
|
5f34171a4a | ||
|
|
4c1ecafa77 |
@ -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()
|
||||||
|
|||||||
17
src/form/dto/create-form.dto.ts
Normal file
17
src/form/dto/create-form.dto.ts
Normal 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[];
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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(),
|
...formDto,
|
||||||
name: dto.name ?? 'Untitled Form',
|
});
|
||||||
});
|
return 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 };
|
|
||||||
|
|
||||||
|
async addField(formId: string, fieldDto: CreateFieldDto): Promise<Form> {
|
||||||
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,22 +93,51 @@ export class FormService {
|
|||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findInterface(formId: string): Promise<string> {
|
async getStats() {
|
||||||
const form = await this.formModel
|
const [totalForms, totalFields, fieldTypeBreakdown, formStatusBreakdown] =
|
||||||
.findOne({ id: formId, deletedAt: null })
|
await Promise.all([
|
||||||
.select(DETAIL_PROJECTION)
|
this.formModel.countDocuments(),
|
||||||
.exec();
|
this.formModel.aggregate([
|
||||||
|
{ $project: { fieldCount: { $size: '$fields' } } },
|
||||||
if (!form) throw new NotFoundException(`Form ${formId} not found`);
|
{ $group: { _id: null, total: { $sum: '$fieldCount' } } },
|
||||||
|
]),
|
||||||
try{
|
this.formModel.aggregate([
|
||||||
let tsInference=await this.llmService.generateFormInterface(form)
|
{ $unwind: '$fields' },
|
||||||
return tsInference
|
{ $group: { _id: '$fields.keyType', count: { $sum: 1 } } },
|
||||||
}catch(err){
|
]),
|
||||||
throw new ServiceUnavailableException('llm down')
|
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> {
|
||||||
|
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(
|
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;
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user