before adding token

This commit is contained in:
OkaykOrhmn 2025-07-23 16:15:58 +03:30
parent 64796fcdf7
commit d2e3063094
13 changed files with 129 additions and 153 deletions

View File

@ -1,9 +1,24 @@
import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator'; import { IsEnum, IsOptional, IsUUID, ValidateNested, IsArray, IsString } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { MetadataDto } from './metadataEntry.dto';
export class MetadataEntryDto {
@IsString()
key: string;
@IsString()
value: string;
}
export class MetadataDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => MetadataEntryDto)
@IsOptional()
entries?: { key: string; value: string }[];
}
export class BaseDto { export class BaseDto {
@IsUUID() @IsUUID('4')
@IsOptional() @IsOptional()
id?: string; id?: string;

View File

@ -1,16 +0,0 @@
import { IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
class MetadataEntryDto {
@IsString()
key: string;
@IsString()
value: string;
}
export class MetadataDto {
@ValidateNested({ each: true })
@Type(() => MetadataEntryDto)
entries: MetadataEntryDto[];
}

View File

@ -1,7 +1,11 @@
import { Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm'; import { Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm';
import { Metadata } from './_metadata.entity';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
export class Metadata {
@Column({ type: 'jsonb', nullable: true, default: () => "'[]'" })
entries?: { key: string; value: string }[];
}
export abstract class BaseEntity { export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@ -13,7 +17,7 @@ export abstract class BaseEntity {
updatedAt: Date; updatedAt: Date;
@Column(() => Metadata) @Column(() => Metadata)
metadata: Metadata = new Metadata(); metadata?: Metadata;
@Column({ @Column({
type: 'enum', type: 'enum',
@ -28,5 +32,4 @@ export abstract class BaseEntity {
this.id = uuidv4(); this.id = uuidv4();
} }
} }
} }

View File

@ -1,17 +0,0 @@
import { Column, Entity } from 'typeorm';
@Entity({ name: 'metadata' })
export class Metadata {
@Column({
type: 'uuid',
primary: true,
default: () => 'uuid_generate_v4()',
})
id?: string;
@Column({
type: 'jsonb',
default: () => "'[]'",
})
entries?: { key: string; value: string }[];
}

View File

@ -14,21 +14,14 @@ export class AnswerService {
async create(data: CreateAnswerDto): Promise<Answer> { async create(data: CreateAnswerDto): Promise<Answer> {
try { try {
const answer = this.answerRepository.create({ const answer = this.answerRepository.create({ ...data });
...data,
metadata: data.metadata || { entries: [] },
status: data.status || 'active',
});
return await this.answerRepository.save(answer); return await this.answerRepository.save(answer);
} catch (error) { } catch (error) {
throw new Error(`Failed to create answer: ${error.message}`); throw new Error(`Failed to create answer: ${error.message}`);
} }
} }
async findAll( async findAll( page = 1, limit = 10 ): Promise<{
page = 1,
limit = 10,
): Promise<{
docs: Answer[]; docs: Answer[];
totalDocs: number; totalDocs: number;
limit: number; limit: number;

View File

@ -2,10 +2,10 @@ import { IsString, IsUUID } from 'class-validator';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
export class CreateAnswerDto extends BaseDto { export class CreateAnswerDto extends BaseDto {
@IsUUID() @IsUUID('4')
participantId: string; participantId: string;
@IsUUID() @IsUUID('4')
questionId: string; questionId: string;
@IsString() @IsString()

View File

@ -1,5 +1,4 @@
import { IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator'; import { IsEnum, IsOptional, IsString, IsUrl, IsUUID } from 'class-validator';
import { Type } from 'class-transformer';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
export class CreateAttachmentDto extends BaseDto { export class CreateAttachmentDto extends BaseDto {
@ -15,6 +14,6 @@ export class CreateAttachmentDto extends BaseDto {
@IsUrl() @IsUrl()
storageUrl: string; storageUrl: string;
@IsUUID() @IsUUID('4')
ownerId: string; ownerId: string;
} }

View File

@ -1,7 +1,6 @@
// src/config/database.config.ts // src/config/database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Participant } from '../participant/entity/participant.entity'; import { Participant } from '../participant/entity/participant.entity';
import { Metadata } from '../_core/entity/_metadata.entity';
import { Form } from '../form/entity/form.entity'; import { Form } from '../form/entity/form.entity';
import { Realm } from '../realm/entity/realm.entity'; import { Realm } from '../realm/entity/realm.entity';
import { Question } from '../question/entity/question.entity'; import { Question } from '../question/entity/question.entity';
@ -12,7 +11,7 @@ import { FormSection } from '../formSection/entity/formSection.entity';
import { FormPage } from '../formPage/entity/formPage.entity'; import { FormPage } from '../formPage/entity/formPage.entity';
import { FormResult } from '../formResult/entity/formResult.entity'; import { FormResult } from '../formResult/entity/formResult.entity';
import { ParticipantGroup } from '../participantGroup/entity/participantGroup.entity'; import { ParticipantGroup } from '../participantGroup/entity/participantGroup.entity';
import { BaseEntity } from '../_core/entity/_base.entity'; import { BaseEntity, Metadata } from '../_core/entity/_base.entity';
export const typeOrmConfig: TypeOrmModuleOptions = { export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'postgres', type: 'postgres',

View File

@ -17,7 +17,7 @@ class CreateOpinionDto {
@Type(() => CreateOptionsDto) @Type(() => CreateOptionsDto)
options: CreateOptionsDto; options: CreateOptionsDto;
@IsUUID() @IsUUID('4')
questionId: string; questionId: string;
} }

View File

@ -26,8 +26,8 @@ export class Opinion {
@Entity() @Entity()
export class FormResult extends BaseEntity { export class FormResult extends BaseEntity {
@OneToOne(() => Form, form => form.formResult, { /*cascade: true, eager: true */}) @Column({ type: 'uuid' })
form: Form; formId: string;
@Column({ type: 'int', default: 0 }) @Column({ type: 'int', default: 0 })
numParticipants: number; numParticipants: number;

View File

@ -44,10 +44,10 @@ export class FormResultController {
return this.formResultService.remove(id); return this.formResultService.remove(id);
} }
@Get('form/:formId/statistics') // @Get('form/:formId/statistics')
async getFormStatistics( // async getFormStatistics(
@Param('formId', new ParseUUIDPipe()) formId: string, // @Param('formId', new ParseUUIDPipe()) formId: string,
): Promise<FormResult> { // ): Promise<FormResult> {
return this.formResultService.getFormStatistics(formId); // return this.formResultService.getFormStatistics(formId);
} // }
} }

View File

@ -94,90 +94,90 @@ export class FormResultService {
throw new NotFoundException(`FormResult with ID "${id}" not found`); throw new NotFoundException(`FormResult with ID "${id}" not found`);
} }
} }
//
async getFormStatistics(formId: string): Promise<FormResult> { // async getFormStatistics(formId: string): Promise<FormResult> {
// 1. Find the Form entity // // 1. Find the Form entity
const form = await this.formRepository.findOne({ where: { id: formId } }); // const form = await this.formRepository.findOne({ where: { id: formId } });
if (!form) { // if (!form) {
throw new NotFoundException(`Form with ID "${formId}" not found`); // throw new NotFoundException(`Form with ID "${formId}" not found`);
} // }
//
// 2. Compute numQuestions // // 2. Compute numQuestions
const numQuestions = await this.questionRepository.count({ // const numQuestions = await this.questionRepository.count({
where: { // where: {
formSection: { // formSection: {
formPage: { // formPage: {
form: { id: formId }, // form: { id: formId },
}, // },
}, // },
}, // },
}); // });
//
// 3. Compute numParticipants // // 3. Compute numParticipants
const numParticipantsResult = await this.answerRepository // const numParticipantsResult = await this.answerRepository
.createQueryBuilder('answer') // .createQueryBuilder('answer')
.select('COUNT(DISTINCT answer.participantId)', 'count') // .select('COUNT(DISTINCT answer.participantId)', 'count')
.innerJoin('answer.question', 'question') // .innerJoin('answer.question', 'question')
.innerJoin('question.formSection', 'formSection') // .innerJoin('question.formSection', 'formSection')
.innerJoin('formSection.formPage', 'formPage') // .innerJoin('formSection.formPage', 'formPage')
.where('formPage.formId = :formId', { formId }) // .where('formPage.formId = :formId', { formId })
.getRawOne(); // .getRawOne();
const numParticipants = numParticipantsResult ? Number(numParticipantsResult.count) : 0; // const numParticipants = numParticipantsResult ? Number(numParticipantsResult.count) : 0;
//
// 4. Compute numAnswers // // 4. Compute numAnswers
const numAnswers = await this.answerRepository.count({ // const numAnswers = await this.answerRepository.count({
where: { // where: {
question: { // question: {
formSection: { // formSection: {
formPage: { // formPage: {
form: { id: formId }, // form: { id: formId },
}, // },
}, // },
}, // },
}, // },
}); // });
//
// 5. Compute numCompleteParticipants // // 5. Compute numCompleteParticipants
const subQuery = this.answerRepository // const subQuery = this.answerRepository
.createQueryBuilder('answer') // .createQueryBuilder('answer')
.select('answer.participantId') // .select('answer.participantId')
.innerJoin('answer.question', 'question') // .innerJoin('answer.question', 'question')
.innerJoin('question.formSection', 'formSection') // .innerJoin('question.formSection', 'formSection')
.innerJoin('formSection.formPage', 'formPage') // .innerJoin('formSection.formPage', 'formPage')
.where('formPage.formId = :formId', { formId }) // .where('formPage.formId = :formId', { formId })
.groupBy('answer.participantId') // .groupBy('answer.participantId')
.having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions }); // .having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions });
//
const numCompleteParticipantsResult = await this.answerRepository.manager // const numCompleteParticipantsResult = await this.answerRepository.manager
.createQueryBuilder() // .createQueryBuilder()
.select('COUNT(*)', 'count') // .select('COUNT(*)', 'count')
.from(`(${subQuery.getQuery()})`, 'subquery') // .from(`(${subQuery.getQuery()})`, 'subquery')
.setParameters(subQuery.getParameters()) // .setParameters(subQuery.getParameters())
.getRawOne(); // .getRawOne();
const numCompleteParticipants = numCompleteParticipantsResult ? Number(numCompleteParticipantsResult.count) : 0; // const numCompleteParticipants = numCompleteParticipantsResult ? Number(numCompleteParticipantsResult.count) : 0;
//
// 6. Find or create FormResult for the Form // // 6. Find or create FormResult for the Form
let formResult = await this.formResultRepo.findOne({ where: { form: { id: formId } }, relations: ['form'] }); // let formResult = await this.formResultRepo.findOne({ where: { form: { id: formId } }, relations: ['form'] });
if (!formResult) { // if (!formResult) {
formResult = this.formResultRepo.create({ // formResult = this.formResultRepo.create({
form, // Link to the Form entity // form, // Link to the Form entity
numQuestions, // numQuestions,
numParticipants, // numParticipants,
numAnswers, // numAnswers,
numComplete: numCompleteParticipants, // numComplete: numCompleteParticipants,
opinions: [], // Initialize as empty or compute if needed // opinions: [], // Initialize as empty or compute if needed
status: 'active', // status: 'active',
}); // });
} else { // } else {
formResult.numQuestions = numQuestions; // formResult.numQuestions = numQuestions;
formResult.numParticipants = numParticipants; // formResult.numParticipants = numParticipants;
formResult.numAnswers = numAnswers; // formResult.numAnswers = numAnswers;
formResult.numComplete = numCompleteParticipants; // formResult.numComplete = numCompleteParticipants;
// Preserve existing opinions or update if needed // // Preserve existing opinions or update if needed
} // }
//
// 7. Save and return the FormResult // // 7. Save and return the FormResult
return await this.formResultRepo.save(formResult); // return await this.formResultRepo.save(formResult);
} // }
} }

View File

@ -49,7 +49,7 @@ export class RealmService {
displayName: data.title + ' Owner', // Default displayName displayName: data.title + ' Owner', // Default displayName
role: 'admin', // Default role for owner role: 'admin', // Default role for owner
userId: decodedToken.sub, userId: decodedToken.sub,
metadata: data.metadata ? { entries: data.metadata.entries } : { entries: [] }, metadata: data.metadata ,
// status: 'active', // status: 'active',
}); });
participant = await this.participantRepo.save(participant); participant = await this.participantRepo.save(participant);
@ -58,7 +58,7 @@ export class RealmService {
// Create the realm with the participant as the owner // Create the realm with the participant as the owner
const realm = this.realmRepo.create({ const realm = this.realmRepo.create({
...data, ...data,
owner: participant, // Set owner to the participant with userId = sub ownerId: participant.userId, // Set owner to the participant with userId = sub
}); });
return await this.realmRepo.save(realm); return await this.realmRepo.save(realm);