diff --git a/apps/api/src/app/auth/services/auth.service.ts b/apps/api/src/app/auth/services/auth.service.ts index 5f8d28f28..f70183f28 100644 --- a/apps/api/src/app/auth/services/auth.service.ts +++ b/apps/api/src/app/auth/services/auth.service.ts @@ -170,13 +170,13 @@ export class AuthService { async apiKeyAuthenticate(apiKey: string) { const environment = await this.environmentRepository.findByApiKey(apiKey); - if (!environment) throw new UnauthorizedException('API Key not found'); + if (!environment) throw new UnauthorizedException('API Key not found!'); const key = environment.apiKeys.find((i) => i.key === apiKey); - if (!key) throw new UnauthorizedException('API Key not found'); + if (!key) throw new UnauthorizedException('API Key not found!'); const user = await this.getUser({ _id: key._userId }); - if (!user) throw new UnauthorizedException('User not found'); + if (!user) throw new UnauthorizedException('User not found!'); return this.getSignedToken( { diff --git a/apps/api/src/app/common/common.controller.ts b/apps/api/src/app/common/common.controller.ts index ec2e232de..05b1892ca 100644 --- a/apps/api/src/app/common/common.controller.ts +++ b/apps/api/src/app/common/common.controller.ts @@ -20,11 +20,12 @@ export class CommonController { @ApiOperation({ summary: 'Check if request is valid (Checks Auth)', }) - async isRequestValid(@Body() body: ValidRequestDto): Promise { + async isRequestValid(@Body() body: ValidRequestDto): Promise<{ success: boolean }> { return this.validRequest.execute( ValidRequestCommand.create({ projectId: body.projectId, templateId: body.templateId, + schema: body.schema, }) ); } diff --git a/apps/api/src/app/common/dtos/Schema.dto.ts b/apps/api/src/app/common/dtos/Schema.dto.ts new file mode 100644 index 000000000..a8e335629 --- /dev/null +++ b/apps/api/src/app/common/dtos/Schema.dto.ts @@ -0,0 +1,114 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + IsArray, + IsBoolean, + IsOptional, + IsString, + IsEnum, + IsNumber, + ValidateIf, + IsNotEmpty, + Validate, + ArrayMinSize, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ColumnTypesEnum, Defaults } from '@impler/shared'; +import { IsValidRegex } from '@shared/framework/is-valid-regex.validator'; + +export class SchemaDto { + @ApiProperty({ + description: 'Name of the column', + type: 'string', + }) + @IsString() + name: string; + + @ApiProperty({ + description: 'Key of the column', + }) + @IsNotEmpty({ message: "'key' should not empty" }) + @IsString({ message: "'key' must be a string" }) + key: string; + + @ApiProperty({ + description: 'Alternative possible keys of the column', + type: Array, + }) + @IsArray() + @IsOptional() + @Type(() => Array) + alternateKeys: string[]; + + @ApiPropertyOptional({ + description: 'While true, it Indicates column value should exists in data', + }) + @IsBoolean({ message: "'isRequired' must be boolean" }) + @IsOptional() + isRequired = false; + + @ApiPropertyOptional({ + description: 'While true, it Indicates column value should be unique in data', + }) + @IsBoolean({ message: "'isUnique' must be boolean" }) + @IsOptional() + isUnique = false; + + @ApiProperty({ + description: 'Specifies the type of column', + enum: ColumnTypesEnum, + }) + @IsEnum(ColumnTypesEnum, { + message: `entered type must be one of ${Object.values(ColumnTypesEnum).join(', ')}`, + }) + type: ColumnTypesEnum; + + @ApiPropertyOptional({ + description: 'Regex if type is Regex', + }) + @Validate(IsValidRegex, { + message: 'Invalid regex pattern', + }) + @IsNotEmpty({ + message: "'regex' should not empty, when type is Regex", + }) + @ValidateIf((object) => object.type === ColumnTypesEnum.REGEX) + regex: string; + + @ApiPropertyOptional({ + description: 'Description of Regex', + }) + @ValidateIf((object) => object.type === ColumnTypesEnum.REGEX) + @IsString() + @IsOptional() + regexDescription: string; + + @ApiPropertyOptional({ + description: 'List of possible values for column if type is Select', + }) + @ValidateIf((object) => object.type === ColumnTypesEnum.SELECT, { + message: "'selectValues' should not empty, when type is Select", + }) + @IsArray({ message: "'selectValues' must be an array, when type is Select" }) + @ArrayMinSize(1, { message: "'selectValues' should not empty, when type is Select" }) + @Type(() => Array) + @IsOptional() + selectValues: string[] = []; + + @ApiPropertyOptional({ + description: 'List of date formats for column if type is Date', + }) + @ValidateIf((object) => object.type === ColumnTypesEnum.DATE, { + message: "'dateFormats' should not empty, when type is Date", + }) + @Type(() => Array) + @IsArray({ message: "'dateFormats' must be an array, when type is Date" }) + @ArrayMinSize(1, { message: "'dateFormats' must not be empty, when type is Date" }) + dateFormats: string[] = Defaults.DATE_FORMATS; + + @ApiProperty({ + description: 'Sequence of column', + }) + @IsNumber() + @IsOptional() + sequence: number; +} diff --git a/apps/api/src/app/common/dtos/valid.dto.ts b/apps/api/src/app/common/dtos/valid.dto.ts index 0a3af57b0..9686d561f 100644 --- a/apps/api/src/app/common/dtos/valid.dto.ts +++ b/apps/api/src/app/common/dtos/valid.dto.ts @@ -1,18 +1,25 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsDefined, IsOptional, IsString, IsMongoId } from 'class-validator'; +import { IsDefined, IsOptional, IsMongoId } from 'class-validator'; export class ValidRequestDto { @ApiProperty({ description: 'Id of the project', }) - @IsMongoId() + @IsMongoId({ + message: 'Invalid project id', + }) @IsDefined() projectId: string; @ApiProperty({ description: 'ID of the template', }) - @IsString() + @IsMongoId({ + message: 'Invalid template id', + }) @IsOptional() templateId?: string; + + @IsOptional() + schema?: string; } diff --git a/apps/api/src/app/common/usecases/valid-request/valid-request.command.ts b/apps/api/src/app/common/usecases/valid-request/valid-request.command.ts index 58aa649f8..93e1bcdb2 100644 --- a/apps/api/src/app/common/usecases/valid-request/valid-request.command.ts +++ b/apps/api/src/app/common/usecases/valid-request/valid-request.command.ts @@ -9,4 +9,7 @@ export class ValidRequestCommand extends BaseCommand { @IsString() @IsOptional() templateId: string; + + @IsOptional() + schema: string; } diff --git a/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts b/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts index 3f19c7369..76ec97b2c 100644 --- a/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts +++ b/apps/api/src/app/common/usecases/valid-request/valid-request.usecase.ts @@ -1,31 +1,93 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, HttpStatus, HttpException, UnauthorizedException } from '@nestjs/common'; import { ProjectRepository, TemplateRepository } from '@impler/dal'; import { ValidRequestCommand } from './valid-request.command'; import { DocumentNotFoundException } from '@shared/exceptions/document-not-found.exception'; import { APIMessages } from '@shared/constants'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; +import { SchemaDto } from 'app/common/dtos/Schema.dto'; @Injectable() export class ValidRequest { constructor(private projectRepository: ProjectRepository, private templateRepository: TemplateRepository) {} - async execute(command: ValidRequestCommand) { - if (command.templateId) { - const templateCount = await this.templateRepository.count({ - _id: command.templateId, - _projectId: command.projectId, - }); - if (!templateCount) { - throw new DocumentNotFoundException('Template', command.templateId, APIMessages.PROJECT_WITH_TEMPLATE_MISSING); + async execute(command: ValidRequestCommand): Promise<{ success: boolean }> { + try { + if (command.projectId) { + const projectCount = await this.projectRepository.count({ + _id: command.projectId, + }); + + if (!projectCount) { + throw new DocumentNotFoundException('Project', command.projectId, APIMessages.INCORRECT_KEYS_FOUND); + } } - } else { - const projectCount = await this.projectRepository.count({ - _id: command.projectId, - }); - if (!projectCount) { - throw new DocumentNotFoundException('Project', command.projectId); + + if (command.templateId) { + const templateCount = await this.templateRepository.count({ + _id: command.templateId, + _projectId: command.projectId, + }); + + if (!templateCount) { + throw new DocumentNotFoundException('Template', command.templateId, APIMessages.INCORRECT_KEYS_FOUND); + } } - } - return true; + if (command.schema) { + const parsedSchema: SchemaDto = JSON.parse(command.schema); + + const errors: string[] = []; + if (!Array.isArray(parsedSchema)) { + throw new DocumentNotFoundException( + 'Schema', + command.schema, + 'Invalid schema input. An array of objects is expected.' + ); + } + + for (const item of parsedSchema) { + const columnDto = plainToClass(SchemaDto, item); + const validationErrors = await validate(columnDto); + + // eslint-disable-next-line no-magic-numbers + if (validationErrors.length > 0) { + errors.push( + `Schema Error : ${validationErrors + .map((err) => { + return Object.values(err.constraints); + }) + .join(', ')}` + ); + + throw new DocumentNotFoundException('Schema', command.schema, errors.toString()); + } + } + } + + return { success: true }; + } catch (error) { + if (error instanceof DocumentNotFoundException) { + throw new HttpException( + { + message: error.message, + errorCode: error.getStatus(), + }, + HttpStatus.NOT_FOUND + ); + } + + if (error instanceof UnauthorizedException) { + throw new HttpException( + { + message: APIMessages.INVALID_AUTH_TOKEN, + errorCode: error.getStatus(), + }, + HttpStatus.NOT_FOUND + ); + } + + throw new HttpException('Internal Server Error', HttpStatus.INTERNAL_SERVER_ERROR); + } } } diff --git a/apps/api/src/app/shared/constants.ts b/apps/api/src/app/shared/constants.ts index 170a18acf..ef75a8cac 100644 --- a/apps/api/src/app/shared/constants.ts +++ b/apps/api/src/app/shared/constants.ts @@ -24,6 +24,8 @@ export const APIMessages = { INCORRECT_LOGIN_CREDENTIALS: 'Incorrect email or password provided', OPERATION_NOT_ALLOWED: `You're not allowed to perform this action.`, EMAIL_ALREADY_EXISTS: 'Email already exists', + INCORRECT_KEYS_FOUND: 'Invalid keys found! Please check and correct them from web', + INVALID_AUTH_TOKEN: 'Invalid authentication token', }; export const CONSTANTS = { diff --git a/apps/web/pages/imports/[id].tsx b/apps/web/pages/imports/[id].tsx index 6d2db3fa9..03b9230b0 100644 --- a/apps/web/pages/imports/[id].tsx +++ b/apps/web/pages/imports/[id].tsx @@ -65,6 +65,7 @@ export default function ImportDetails({ template }: ImportDetailProps) {