From e9f687dd0f05bddcf9273813e8fd619b46f208ab Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 23 May 2024 02:57:38 +0400 Subject: [PATCH 01/13] #RI-5661 - add rdi auth --- .../api/migration/1716370509836-rdi.ts | 14 ++ redisinsight/api/package.json | 5 +- redisinsight/api/src/__mocks__/index.ts | 1 + redisinsight/api/src/__mocks__/rdi.ts | 65 +++++++ .../api/src/constants/custom-error-codes.ts | 2 + .../src/modules/rdi/client/api.rdi.client.ts | 119 +++++++++---- .../api/src/modules/rdi/client/rdi.client.ts | 20 ++- .../api/src/modules/rdi/constants/index.ts | 6 + .../api/src/modules/rdi/dto/create.rdi.dto.ts | 16 +- .../src/modules/rdi/entities/rdi.entity.ts | 16 +- .../api/src/modules/rdi/exceptions/index.ts | 4 + ...ipeline.internal-server-error.exception.ts | 19 ++ .../rdi-pipeline.not-found.exception.ts | 19 ++ .../rdi-pipeline.unauthorized.exception.ts | 19 ++ .../exceptions/rdi-pipiline.error.handler.ts | 25 +++ .../modules/rdi/models/rdi.client.metadata.ts | 5 +- .../api/src/modules/rdi/models/rdi.ts | 61 +------ .../rdi/providers/rdi.client.factory.spec.ts | 60 +++++++ .../rdi/providers/rdi.client.factory.ts | 25 +-- .../rdi/providers/rdi.client.provider.ts | 20 ++- .../rdi/providers/rdi.client.storage.spec.ts | 167 ++++++++++++++++++ .../rdi/providers/rdi.client.storage.ts | 74 ++++++-- .../api/src/modules/rdi/rdi.controller.ts | 37 +++- .../api/src/modules/rdi/rdi.service.ts | 66 ++++++- .../rdi/repository/local.rdi.repository.ts | 7 +- 25 files changed, 687 insertions(+), 185 deletions(-) create mode 100644 redisinsight/api/migration/1716370509836-rdi.ts create mode 100644 redisinsight/api/src/__mocks__/rdi.ts create mode 100644 redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts create mode 100644 redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.not-found.exception.ts create mode 100644 redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.unauthorized.exception.ts create mode 100644 redisinsight/api/src/modules/rdi/exceptions/rdi-pipiline.error.handler.ts create mode 100644 redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts create mode 100644 redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts diff --git a/redisinsight/api/migration/1716370509836-rdi.ts b/redisinsight/api/migration/1716370509836-rdi.ts new file mode 100644 index 0000000000..a03d3e6828 --- /dev/null +++ b/redisinsight/api/migration/1716370509836-rdi.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Rdi1716370509836 implements MigrationInterface { + name = 'Rdi1716370509836' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "rdi" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar, "name" varchar NOT NULL, "username" varchar NOT NULL, "password" varchar NOT NULL, "lastConnection" datetime, "version" varchar NOT NULL, "encryption" varchar)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "rdi"`); + } + +} diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 18b5f0d660..86be960962 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -33,8 +33,9 @@ "test:api": "cross-env NODE_ENV=test ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml", "test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api", "test:api:ci:cov": "cross-env nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json", - "typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration", - "typeorm:run": "yarn typeorm migration:run" + "typeorm:migrate": " yarncross-env NODE_ENV=staging typeorm migration:generate ./migration/migration", + "typeorm:run": "yarn typeorm migration:run", + "typeorm:stage:run": "cross-env NODE_ENV=staging yarn typeorm migration:run" }, "resolutions": { "nanoid": "^3.1.31", diff --git a/redisinsight/api/src/__mocks__/index.ts b/redisinsight/api/src/__mocks__/index.ts index 387bff8375..43c94d7922 100644 --- a/redisinsight/api/src/__mocks__/index.ts +++ b/redisinsight/api/src/__mocks__/index.ts @@ -39,3 +39,4 @@ export * from './session'; export * from './cloud-session'; export * from './database-info'; export * from './cloud-job'; +export * from './rdi'; diff --git a/redisinsight/api/src/__mocks__/rdi.ts b/redisinsight/api/src/__mocks__/rdi.ts new file mode 100644 index 0000000000..2829b8ebba --- /dev/null +++ b/redisinsight/api/src/__mocks__/rdi.ts @@ -0,0 +1,65 @@ +import { RdiClient } from 'src/modules/rdi/client/rdi.client'; +import { + Rdi, + RdiClientMetadata, +} from 'src/modules/rdi/models'; +import { ApiRdiClient } from 'src/modules/rdi/client/api.rdi.client'; + +export const mockRdiId = 'rdiId'; + +export class MockRdiClient extends ApiRdiClient { + constructor(metadata: RdiClientMetadata, client: any = jest.fn()) { + super(metadata, client); + } + + public getSchema = jest.fn(); + + public getPipeline = jest.fn(); + + public getTemplate = jest.fn(); + + public getStrategies = jest.fn(); + + public deploy = jest.fn(); + + public deployJob = jest.fn(); + + public dryRunJob = jest.fn(); + + public testConnections = jest.fn(); + + public getStatistics = jest.fn(); + + public getPipelineStatus = jest.fn(); + + public getJobFunctions = jest.fn(); + + public connect = jest.fn(); + + public ensureAuth = jest.fn(); +} + +export const generateMockRdiClient = ( + metadata: RdiClientMetadata, + client = jest.fn(), +): MockRdiClient => new MockRdiClient(metadata as RdiClientMetadata, client); + +export const mockRdiClientMetadata: RdiClientMetadata = { + sessionMetadata: undefined, + id: mockRdiId, +}; + +export const mockRdi = Object.assign(new Rdi(), { + name: 'name', + version: '1.2', + url: 'http://localhost:4000', + password: 'pass', + username: 'user', +}); + +export const mockRdiUnauthorizedError = { + message: 'Request failed with status code 401', + response: { + status: 401, + }, +}; diff --git a/redisinsight/api/src/constants/custom-error-codes.ts b/redisinsight/api/src/constants/custom-error-codes.ts index dcba7cf2e2..01b4cae089 100644 --- a/redisinsight/api/src/constants/custom-error-codes.ts +++ b/redisinsight/api/src/constants/custom-error-codes.ts @@ -52,4 +52,6 @@ export enum CustomErrorCodes { // RDI errors [11400, 11599] RdiDeployPipelineFailure = 11_401, + RdiUnauthorized = 11_402, + RdiInternalServerError = 11_403, } diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index fcaaf935bb..979e51b526 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -1,81 +1,115 @@ -import { AxiosInstance } from 'axios'; +import axios, { AxiosInstance } from 'axios'; import { plainToClass } from 'class-transformer'; +import { decode } from 'jsonwebtoken'; import { RdiClient } from 'src/modules/rdi/client/rdi.client'; -import { RdiUrl } from 'src/modules/rdi/constants'; +import { RDI_TIMEOUT, RdiUrl, TOKEN_TRESHOLD } from 'src/modules/rdi/constants'; import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult } from 'src/modules/rdi/dto'; -import { RdiPipelineDeployFailedException } from 'src/modules/rdi/exceptions'; +import { RdiPipelineDeployFailedException, wrapRdiPipelineError } from 'src/modules/rdi/exceptions'; import { RdiJob, RdiPipeline, RdiStatisticsResult, - RdiType, - RdiDryRunJobResult, - RdiDyRunJobStatus, RdiStatisticsStatus, - RdiStatisticsData, + RdiStatisticsData, RdiClientMetadata, Rdi, } from 'src/modules/rdi/models'; import { convertKeysToCamelCase } from 'src/utils/base.helper'; const RDI_DEPLOY_FAILED_STATUS = 'failed'; export class ApiRdiClient extends RdiClient { - public type = RdiType.API; - protected readonly client: AxiosInstance; - async isConnected(): Promise { - // todo: check if needed and possible - return true; + private auth: { jwt: string, exp: number }; + + constructor(clientMetadata: RdiClientMetadata, rdi: Rdi) { + super(clientMetadata, rdi); + this.client = axios.create({ + baseURL: rdi.url, + timeout: RDI_TIMEOUT, + }); } async getSchema(): Promise { - const response = await this.client.get(RdiUrl.GetSchema); - return response.data; + try { + const response = await this.client.get(RdiUrl.GetSchema); + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async getPipeline(): Promise { - const response = await this.client.get(RdiUrl.GetPipeline); - return response.data; + try { + const response = await this.client.get(RdiUrl.GetPipeline); + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async getStrategies(): Promise { - const response = await this.client.get(RdiUrl.GetStrategies); - return response.data; + try { + const response = await this.client.get(RdiUrl.GetStrategies); + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async getTemplate(options: object): Promise { - const response = await this.client.get(RdiUrl.GetTemplate, { params: options }); - return response.data; + try { + const response = await this.client.get(RdiUrl.GetTemplate, { params: options }); + return response.data; + } catch (error) { + throw wrapRdiPipelineError(error); + } } async deploy(pipeline: RdiPipeline): Promise { - const response = await this.client.post(RdiUrl.Deploy, { ...pipeline }); + let response; + try { + response = await this.client.post(RdiUrl.Deploy, { ...pipeline }); + } catch (error) { + throw wrapRdiPipelineError(error, error.response.data.message); + } if (response.data?.status === RDI_DEPLOY_FAILED_STATUS) { throw new RdiPipelineDeployFailedException(undefined, { error: response.data?.error }); } } - async deployJob(job: RdiJob): Promise { + async deployJob(): Promise { return null; } async dryRunJob(data: RdiDryRunJobDto): Promise { - const response = await this.client.post(RdiUrl.DryRunJob, data); - return response.data; + try { + const response = await this.client.post(RdiUrl.DryRunJob, data); + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async testConnections(config: string): Promise { - const response = await this.client.post(RdiUrl.TestConnections, config); + try { + const response = await this.client.post(RdiUrl.TestConnections, config); - return response.data; + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async getPipelineStatus(): Promise { - const response = await this.client.get(RdiUrl.GetPipelineStatus); + try { + const response = await this.client.get(RdiUrl.GetPipelineStatus); - return response.data; + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } async getStatistics(sections?: string): Promise { @@ -91,11 +125,32 @@ export class ApiRdiClient extends RdiClient { } async getJobFunctions(): Promise { - const response = await this.client.post(RdiUrl.JobFunctions); - return response.data; + try { + const response = await this.client.post(RdiUrl.JobFunctions); + return response.data; + } catch (e) { + throw wrapRdiPipelineError(e); + } } - async disconnect(): Promise { - return undefined; + async connect(): Promise { + try { + const response = await this.client.post(RdiUrl.Login, { username: this.rdi.username, password: this.rdi.password }); + const accessToken = response.data.access_token; + const decodedJwt = decode(accessToken); + + this.auth = { jwt: accessToken, exp: decodedJwt.exp }; + this.client.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; + } catch (e) { + throw wrapRdiPipelineError(e); + } + } + + async ensureAuth(): Promise { + const expiresIn = this.auth.exp * 1_000 - Date.now(); + + if (expiresIn < TOKEN_TRESHOLD) { + await this.connect(); + } } } diff --git a/redisinsight/api/src/modules/rdi/client/rdi.client.ts b/redisinsight/api/src/modules/rdi/client/rdi.client.ts index dcfbea2e5b..98a326ea8f 100644 --- a/redisinsight/api/src/modules/rdi/client/rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/rdi.client.ts @@ -1,25 +1,27 @@ import { - RdiClientMetadata, RdiJob, RdiPipeline, RdiType, RdiDryRunJobResult, RdiStatisticsResult, + Rdi, + RdiClientMetadata, RdiJob, RdiPipeline, RdiStatisticsResult, } from 'src/modules/rdi/models'; import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult, } from 'src/modules/rdi/dto'; +import { IDLE_TRESHOLD } from 'src/modules/rdi/constants'; export abstract class RdiClient { - abstract type: RdiType; - public readonly id: string; public lastUsed: number = Date.now(); - constructor( - protected readonly metadata: RdiClientMetadata, - protected readonly client: unknown, + protected constructor( + public readonly metadata: RdiClientMetadata, + protected readonly rdi: Rdi, ) { this.id = RdiClient.generateId(this.metadata); } - abstract isConnected(): Promise; + public isIdle(): boolean { + return Date.now() - this.lastUsed > IDLE_TRESHOLD; + } abstract getSchema(): Promise; @@ -44,7 +46,9 @@ export abstract class RdiClient { abstract getJobFunctions(): Promise; - abstract disconnect(): Promise; + abstract ensureAuth(): Promise; + + abstract connect(): Promise; public setLastUsed(): void { this.lastUsed = Date.now(); diff --git a/redisinsight/api/src/modules/rdi/constants/index.ts b/redisinsight/api/src/modules/rdi/constants/index.ts index f80f9ffebe..712e82e7d7 100644 --- a/redisinsight/api/src/modules/rdi/constants/index.ts +++ b/redisinsight/api/src/modules/rdi/constants/index.ts @@ -9,4 +9,10 @@ export enum RdiUrl { TestConnections = 'api/v1/pipelines/targets/dry-run', GetStatistics = 'api/v1/monitoring/statistics', GetPipelineStatus = 'api/v1/status', + Login = 'api/v1/login', } + +export const IDLE_TRESHOLD = 10 * 60 * 1000; // 10 min +export const RDI_TIMEOUT = 30_000; // 30 sec +export const TOKEN_TRESHOLD = 2 * 60 * 1000; // 2 min +export const RDI_SYNC_INTERVAL = 5 * 60 * 1_000; // 5 min diff --git a/redisinsight/api/src/modules/rdi/dto/create.rdi.dto.ts b/redisinsight/api/src/modules/rdi/dto/create.rdi.dto.ts index 47839fdad9..554bd57f38 100644 --- a/redisinsight/api/src/modules/rdi/dto/create.rdi.dto.ts +++ b/redisinsight/api/src/modules/rdi/dto/create.rdi.dto.ts @@ -1,16 +1,4 @@ import { OmitType } from '@nestjs/swagger'; -import { Rdi, RdiType } from 'src/modules/rdi/models'; -import { ValidateIf } from 'class-validator'; +import { Rdi } from 'src/modules/rdi/models'; -export class CreateRdiDto extends OmitType(Rdi, [ - 'id', 'lastConnection', -] as const) { - @ValidateIf(({ type }) => type === RdiType.API) - url?: string; - - @ValidateIf(({ type }) => type === RdiType.GEARS) - host?: string; - - @ValidateIf(({ type }) => type === RdiType.GEARS) - port?: number; -} +export class CreateRdiDto extends OmitType(Rdi, ['id', 'lastConnection', 'version'] as const) {} diff --git a/redisinsight/api/src/modules/rdi/entities/rdi.entity.ts b/redisinsight/api/src/modules/rdi/entities/rdi.entity.ts index 8fc2b77d9c..72fdad04ec 100644 --- a/redisinsight/api/src/modules/rdi/entities/rdi.entity.ts +++ b/redisinsight/api/src/modules/rdi/entities/rdi.entity.ts @@ -7,32 +7,20 @@ export class RdiEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Expose() - @Column({ nullable: false }) - type: string; - @Expose() @Column({ nullable: true }) url: string; - @Expose() - @Column({ nullable: true }) - host?: string; - - @Expose() - @Column({ nullable: true }) - port?: number; - @Expose() @Column({ nullable: false }) name: string; @Expose() - @Column({ nullable: true }) + @Column({ nullable: false }) username: string; @Expose() - @Column({ nullable: true }) + @Column({ nullable: false }) password: string; @Expose() diff --git a/redisinsight/api/src/modules/rdi/exceptions/index.ts b/redisinsight/api/src/modules/rdi/exceptions/index.ts index a09c0162a0..a1654db4e9 100644 --- a/redisinsight/api/src/modules/rdi/exceptions/index.ts +++ b/redisinsight/api/src/modules/rdi/exceptions/index.ts @@ -1 +1,5 @@ export * from './rdi-deploy-failed.exception'; +export * from './rdi-pipiline.error.handler'; +export * from './rdi-pipeline.internal-server-error.exception'; +export * from './rdi-pipeline.not-found.exception'; +export * from './rdi-pipeline.unauthorized.exception'; diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts new file mode 100644 index 0000000000..4ec392a9f6 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts @@ -0,0 +1,19 @@ +import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class RdiPipelineInternalServerErrorException extends HttpException { + constructor( + message = ERROR_MESSAGES.INTERNAL_SERVER_ERROR, + options?: HttpExceptionOptions, + ) { + const response = { + message, + statusCode: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'RdiInternalServerError', + errorCode: CustomErrorCodes.ConvAiInternalServerError, + }; + + super(response, response.statusCode, options); + } +} diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.not-found.exception.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.not-found.exception.ts new file mode 100644 index 0000000000..600a8b0570 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.not-found.exception.ts @@ -0,0 +1,19 @@ +import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class RdiPipelineNotFoundException extends HttpException { + constructor( + message = ERROR_MESSAGES.BAD_REQUEST, + options?: HttpExceptionOptions & { details?: unknown }, + ) { + const response = { + message, + statusCode: HttpStatus.NOT_FOUND, + error: 'RdiNotFound', + errorCode: CustomErrorCodes.ConvAiNotFound, + details: options?.details, + }; + super(response, response.statusCode, options); + } +} diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.unauthorized.exception.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.unauthorized.exception.ts new file mode 100644 index 0000000000..7ce1e517aa --- /dev/null +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.unauthorized.exception.ts @@ -0,0 +1,19 @@ +import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class RdiPipelineUnauthorizedException extends HttpException { + constructor( + message = ERROR_MESSAGES.UNAUTHORIZED, + options?: HttpExceptionOptions & { details?: unknown }, + ) { + const response = { + message, + statusCode: HttpStatus.UNAUTHORIZED, + error: 'RdiUnauthorized', + errorCode: CustomErrorCodes.RdiUnauthorized, + }; + + super(response, response.statusCode, options); + } +} diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipiline.error.handler.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipiline.error.handler.ts new file mode 100644 index 0000000000..91f3d080a7 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipiline.error.handler.ts @@ -0,0 +1,25 @@ +import { AxiosError } from 'axios'; +import { HttpException } from '@nestjs/common'; +import { + RdiPipelineInternalServerErrorException, + RdiPipelineUnauthorizedException, RdiPipelineNotFoundException, +} from 'src/modules/rdi/exceptions'; + +export const wrapRdiPipelineError = (error: AxiosError, message?: string): HttpException => { + if (error instanceof HttpException) { + return error; + } + + const { response } = error; + if (response) { + const errorOptions = response?.data?.detail; + switch (response?.status) { + case 401: + return new RdiPipelineUnauthorizedException(message, errorOptions); + default: + return new RdiPipelineNotFoundException(message, errorOptions); + } + } + + return new RdiPipelineInternalServerErrorException(message); +}; diff --git a/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts b/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts index 882738469b..089635fb39 100644 --- a/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts +++ b/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts @@ -1,7 +1,7 @@ import { Session, SessionMetadata } from 'src/common/models/session'; import { Type } from 'class-transformer'; import { - IsNotEmpty, IsString, + IsNotEmpty, IsOptional, IsString, } from 'class-validator'; export class RdiClientMetadata { @@ -10,6 +10,7 @@ export class RdiClientMetadata { sessionMetadata: SessionMetadata; @IsNotEmpty() + @IsOptional() @IsString() - id: string; + id?: string; } diff --git a/redisinsight/api/src/modules/rdi/models/rdi.ts b/redisinsight/api/src/modules/rdi/models/rdi.ts index 31e2522488..2b54a331ff 100644 --- a/redisinsight/api/src/modules/rdi/models/rdi.ts +++ b/redisinsight/api/src/modules/rdi/models/rdi.ts @@ -1,11 +1,8 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose } from 'class-transformer'; -import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator'; - -export enum RdiType { - API = 'api', - GEARS = 'gears', -} +import { + IsNotEmpty, IsOptional, IsString, MaxLength, +} from 'class-validator'; export class Rdi { @ApiProperty({ @@ -15,48 +12,15 @@ export class Rdi { @Expose() id: string; - @ApiProperty({ - description: 'RDI type', - enum: RdiType, - }) - @Expose() - @IsEnum(RdiType, { - message: `Type must be a valid enum value from: ${Object.values(RdiType)}.`, - }) - @IsOptional() - type?: RdiType; - @ApiPropertyOptional({ description: 'Base url of API to connect to (for API type only)', example: 'https://example.com', type: String, }) - @IsOptional() @Expose() @IsNotEmpty() @IsString() - url?: string; - - @ApiPropertyOptional({ - description: 'The hostname of RDI database, for example redis.acme.com. (for GEARS type only)', - type: String, - }) - @IsOptional() - @Expose() - @IsNotEmpty() - @IsString() - host?: string; - - @ApiPropertyOptional({ - description: 'The port RDI database is available on. (for GEARS type only)', - type: Number, - default: 6379, - }) - @IsOptional() - @Expose() - @IsNotEmpty() - @IsInt() - port?: number; + url: string; @ApiProperty({ description: 'A name to associate with RDI', @@ -74,9 +38,7 @@ export class Rdi { }) @Expose() @IsString() - @IsNotEmpty() - @IsOptional() - username?: string; + username: string; @ApiPropertyOptional({ description: 'RDI or API password', @@ -85,8 +47,7 @@ export class Rdi { @Expose() @IsString() @IsNotEmpty() - @IsOptional() - password?: string; + password: string; @ApiProperty({ description: 'Time of the last connection to RDI.', @@ -106,14 +67,4 @@ export class Rdi { @IsNotEmpty() @IsString() version?: string; - - @ApiPropertyOptional({ - description: 'A newly created connection', - type: Boolean, - default: false, - }) - @Expose() - @IsOptional() - @IsBoolean({ always: true }) - new?: boolean; } diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts new file mode 100644 index 0000000000..6bef222619 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts @@ -0,0 +1,60 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import axios from 'axios'; +import { sign } from 'jsonwebtoken'; +import { + mockRdi, + mockRdiClientMetadata, + mockRdiUnauthorizedError, +} from 'src/__mocks__'; +import { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory'; +import { RdiUrl } from 'src/modules/rdi/constants'; +import { RdiPipelineUnauthorizedException } from 'src/modules/rdi/exceptions'; + +const mockedAxios = axios as jest.Mocked; +jest.mock('axios'); +mockedAxios.create = jest.fn(() => mockedAxios); + +describe('RdiClientFactory', () => { + let module: TestingModule; + let service: RdiClientFactory; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + RdiClientFactory, + ], + }).compile(); + + service = await module.get(RdiClientFactory); + }); + + describe('createClient', () => { + it('should create client', async () => { + const mockedAccessToken = sign({ exp: Math.trunc(Date.now() / 1000) + 3600 }, 'test'); + + mockedAxios.post.mockResolvedValue({ + status: 200, + data: { + access_token: mockedAccessToken, + }, + }); + const client = await service.createClient(mockRdiClientMetadata, mockRdi); + + expect(mockedAxios.post).toHaveBeenCalledWith( + RdiUrl.Login, + { + password: mockRdi.password, + username: mockRdi.username, + }, + ); + // expect(client).toEqual(mockRdiClient); + }); + it('should not create client if auth request will failed', async () => { + mockedAxios.post.mockRejectedValue(mockRdiUnauthorizedError); + + await expect(service.createClient(mockRdiClientMetadata, mockRdi)).rejects.toThrow( + RdiPipelineUnauthorizedException, + ); + }); + }); +}); diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts index 91c3318e41..e702514245 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts @@ -1,29 +1,14 @@ -import { BadRequestException, Injectable, NotImplementedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { RdiClient } from 'src/modules/rdi/client/rdi.client'; -import { Rdi, RdiClientMetadata, RdiType } from 'src/modules/rdi/models'; -import axios from 'axios'; +import { Rdi, RdiClientMetadata } from 'src/modules/rdi/models'; import { ApiRdiClient } from 'src/modules/rdi/client/api.rdi.client'; @Injectable() export class RdiClientFactory { async createClient(clientMetadata: RdiClientMetadata, rdi: Rdi): Promise { - switch (rdi.type) { - case RdiType.API: - return this.createApiRdiClient(clientMetadata, rdi); - case RdiType.GEARS: - throw new NotImplementedException(); - default: - throw new BadRequestException('Unsupported RDI type'); - } - } - - async createApiRdiClient(clientMetadata: RdiClientMetadata, rdi: Rdi): Promise { - const apiClient = axios.create({ - baseURL: rdi.url, - }); - - // todo: login with credentials and store them in the apiClient + const rdiClient = new ApiRdiClient(clientMetadata, rdi); + await rdiClient.connect(); - return new ApiRdiClient(clientMetadata, apiClient); + return rdiClient; } } diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.provider.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.provider.ts index f9701e08aa..cfd69db1a2 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.provider.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.provider.ts @@ -1,22 +1,22 @@ import { RdiClient } from 'src/modules/rdi/client/rdi.client'; import { Injectable } from '@nestjs/common'; import { RdiClientMetadata } from 'src/modules/rdi/models'; -import { RdiService } from 'src/modules/rdi/rdi.service'; import { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage'; import { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory'; +import { RdiRepository } from 'src/modules/rdi/repository/rdi.repository'; @Injectable() export class RdiClientProvider { constructor( - private readonly rdiService: RdiService, + private readonly repository: RdiRepository, private readonly rdiClientStorage: RdiClientStorage, private readonly rdiClientFactory: RdiClientFactory, ) {} async getOrCreate(rdiClientMetadata: RdiClientMetadata): Promise { let client = await this.rdiClientStorage.getByMetadata(rdiClientMetadata); - if (client) { + await client.ensureAuth(); return client; } @@ -26,8 +26,20 @@ export class RdiClientProvider { } async create(clientMetadata: RdiClientMetadata): Promise { - const rdi = await this.rdiService.get(clientMetadata.id); + const rdi = await this.repository.get(clientMetadata.id); return this.rdiClientFactory.createClient(clientMetadata, rdi); } + + async delete(rdiClientMetadata: RdiClientMetadata): Promise { + return this.rdiClientStorage.delete(rdiClientMetadata.id); + } + + async deleteById(id: string): Promise { + return this.rdiClientStorage.delete(id); + } + + async deleteManyByRdiId(id: string): Promise { + return this.rdiClientStorage.deleteManyByRdiId(id); + } } diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts new file mode 100644 index 0000000000..06ca739c80 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts @@ -0,0 +1,167 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { generateMockRdiClient } from 'src/__mocks__'; +import { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage'; +import { IDLE_TRESHOLD } from 'src/modules/rdi/constants'; + +describe('RdiClientStorage', () => { + let service: RdiClientStorage; + const mockClientMetadata1 = { + sessionMetadata: { + userId: 'u1', + sessionId: 's1', + }, + id: 'id1', + }; + + const mockRdiClient1 = generateMockRdiClient(mockClientMetadata1); + const mockRdiClient2 = generateMockRdiClient({ + ...mockClientMetadata1, + sessionMetadata: { userId: 'u2', sessionId: 's1' }, + }); + const mockRdiClient3 = generateMockRdiClient({ + ...mockClientMetadata1, + sessionMetadata: { userId: 'u2', sessionId: 's3' }, + }); + const mockRdiClient4 = generateMockRdiClient({ + ...mockClientMetadata1, + id: 'id2', + }); + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RdiClientStorage, + ], + }).compile(); + + service = await module.get(RdiClientStorage); + + service['clients'].set(mockRdiClient1.id, mockRdiClient1); + service['clients'].set(mockRdiClient2.id, mockRdiClient2); + service['clients'].set(mockRdiClient3.id, mockRdiClient3); + service['clients'].set(mockRdiClient4.id, mockRdiClient4); + }); + + afterEach(() => { + service.onModuleDestroy(); + }); + + describe('syncClients', () => { + it('should not remove any client since no idle time passed', async () => { + expect(service['clients'].size).toEqual(4); + + service['syncClients'](); + + expect(service['clients'].size).toEqual(4); + }); + + it('should remove client with exceeded time in idle', async () => { + expect(service['clients'].size).toEqual(4); + const toDelete = service['clients'].get(mockRdiClient1.id); + toDelete['lastUsed'] = Date.now() - IDLE_TRESHOLD - 1; + service['syncClients'](); + + expect(service['clients'].size).toEqual(3); + expect(service['clients'].get(mockRdiClient1.id)).toEqual(undefined); + }); + describe('get', () => { + it('should correctly get client instance and update last used time', async () => { + // eslint-disable-next-line prefer-destructuring + const lastUsed = mockRdiClient1['lastUsed']; + + const result = await service.get(mockRdiClient1.id); + + expect(result).toEqual(service['clients'].get(mockRdiClient1.id)); + expect(result['lastUsed']).toBeGreaterThan(lastUsed); + }); + it('should not fail when there is no client', async () => { + const result = await service.get('not-existing'); + + expect(result).toBeUndefined(); + }); + }); + + describe('getByMetadata', () => { + it('should correctly get client instance and update last used time', async () => { + // eslint-disable-next-line prefer-destructuring + const lastUsed = mockRdiClient1['lastUsed']; + + const result = await service.getByMetadata(mockClientMetadata1); + + expect(result).toEqual(service['clients'].get(mockRdiClient1.id)); + expect(result['lastUsed']).toBeGreaterThan(lastUsed); + }); + }); + + describe('set', () => { + beforeEach(() => { + // @ts-ignore + service['clients'] = new Map(); + }); + + it('should add new client', async () => { + expect(service['clients'].size).toEqual(0); + + const result = await service.set(mockRdiClient1); + + expect(result).toEqual(mockRdiClient1); + expect(service['clients'].size).toEqual(1); + expect(await service.get(mockRdiClient1.id)).toEqual(mockRdiClient1); + }); + + it('should replace new client with existing', async () => { + const existingClient = generateMockRdiClient(mockClientMetadata1); + + expect(service['clients'].size).toEqual(0); + expect(await service.set(existingClient)).toEqual(existingClient); + expect(service['clients'].size).toEqual(1); + + const newClient = generateMockRdiClient(mockClientMetadata1); + const result = await service.set(newClient); + expect(result).not.toEqual(existingClient); + expect(result).toEqual(newClient); + expect(service['clients'].size).toEqual(1); + }); + }); + + describe('delete', () => { + it('should remove only one', async () => { + expect(service['clients'].size).toEqual(4); + const result = await service.delete(mockRdiClient1.id); + + expect(result).toEqual(1); + expect(service['clients'].size).toEqual(3); + expect(service['clients'].get(mockRdiClient1.id)).toEqual(undefined); + }); + it('should not fail in case when no client found', async () => { + const result = await service.delete('not-existing'); + + expect(result).toEqual(0); + expect(service['clients'].size).toEqual(4); + }); + }); + + describe('findClients + deleteManyByRdiId', () => { + it('should correctly find clients for particular rdi instance', async () => { + const result = service['findClientsById'](mockClientMetadata1.id); + + expect(result.length).toEqual(3); + result.forEach((id) => { + expect(service['clients'].get(id)['metadata'].id).toEqual(mockClientMetadata1.id); + }); + + expect(await service.deleteManyByRdiId(mockClientMetadata1.id)).toEqual(3); + expect(service['clients'].size).toEqual(1); + }); + + it('should not find any instances', async () => { + const result = service['findClientsById']('not existing'); + + expect(result).toEqual([]); + + expect(await service.deleteManyByRdiId('not existing')).toEqual(0); + expect(service['clients'].size).toEqual(4); + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts index fe7f3894f1..c5b5f1b9c4 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts @@ -1,6 +1,8 @@ import { RdiClient } from 'src/modules/rdi/client/rdi.client'; -import { Injectable, Logger } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { RdiClientMetadata } from 'src/modules/rdi/models'; +import { sum } from 'lodash'; +import { RDI_SYNC_INTERVAL } from 'src/modules/rdi/constants'; @Injectable() export class RdiClientStorage { @@ -8,47 +10,87 @@ export class RdiClientStorage { private readonly clients: Map = new Map(); - // todo: sync clients (idle check) + private readonly syncInterval: NodeJS.Timeout; + + constructor() { + this.syncInterval = setInterval(this.syncClients.bind(this), RDI_SYNC_INTERVAL); + } + + onModuleDestroy() { + clearInterval(this.syncInterval); + } + + /** + * Removes all clients with exceeded idle threshold + * @private + */ + private syncClients(): void { + this.clients.forEach((client) => { + if (client.isIdle()) { + this.clients.delete(client.id); + } + }); + } async get(id: string): Promise { const client = this.clients.get(id); - if (client) { client.setLastUsed(); - if (!(await client.isConnected())) { - await this.delete(id); - return null; - } } return client; } async getByMetadata(rdiClientMetadata: RdiClientMetadata): Promise { - return this.clients.get(RdiClient.generateId(rdiClientMetadata)); + return this.get(RdiClient.generateId(rdiClientMetadata)); } async delete(id: string): Promise { const client = this.clients.get(id); if (client) { - await client.disconnect() - .catch((e) => this.logger.warn('Unable to disconnect client', e)); - this.clients.delete(id); - return 1; } return 0; } + /** + * Finds clients by rdi instance id and returns array of ids + * @param id + * @private + */ + private findClientsById(id: string): string[] { + return [...this.clients.values()] + .filter((rdiClient) => rdiClient.metadata.id === id) + .map((rdiClient) => rdiClient.id); + } + + async deleteManyByRdiId(id: string): Promise { + const toRemove = this.findClientsById(id); + + this.logger.debug(`Trying to remove ${toRemove.length} clients`); + + return sum(await Promise.all(toRemove.map(this.delete.bind(this)))); + } + + /** + * Saves client into the clients pool + * When client with such "id" exists: + * Will replace the current client with a new one + * @param client + */ async set(client: RdiClient): Promise { - // todo: client metadata check - // todo: existing client check - // const existingClient = this.get(client.id); + // Additional validation + if ( + !client.id + || !client.metadata.sessionMetadata?.sessionId + || !client.metadata.sessionMetadata.userId + ) { + throw new BadRequestException('Client metadata missed required properties'); + } this.clients.set(client.id, client); - return client; } } diff --git a/redisinsight/api/src/modules/rdi/rdi.controller.ts b/redisinsight/api/src/modules/rdi/rdi.controller.ts index 561a6d52fd..de9c1e6260 100644 --- a/redisinsight/api/src/modules/rdi/rdi.controller.ts +++ b/redisinsight/api/src/modules/rdi/rdi.controller.ts @@ -2,11 +2,14 @@ import { Body, ClassSerializerInterceptor, Controller, Delete, Get, Param, Patch, Post, UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; -import { Rdi } from 'src/modules/rdi/models'; +import { Rdi, RdiClientMetadata } from 'src/modules/rdi/models'; import { ApiTags } from '@nestjs/swagger'; import { RdiService } from 'src/modules/rdi/rdi.service'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto'; +import { RequestRdiClientMetadata } from 'src/modules/rdi/decorators'; +import { RequestSessionMetadata } from 'src/common/decorators'; +import {SessionMetadata} from "src/common/models"; @ApiTags('RDI') @UsePipes(new ValidationPipe({ transform: true })) @@ -41,8 +44,11 @@ export class RdiController { statusCode: 201, responses: [{ status: 201, type: Rdi }], }) - async create(@Body() dto: CreateRdiDto): Promise { - return this.rdiService.create(dto); + async create( + @RequestSessionMetadata() sessionMetadata: SessionMetadata, + @Body() dto: CreateRdiDto, + ): Promise { + return this.rdiService.create(sessionMetadata, dto); } @Patch('/:id') @@ -51,10 +57,10 @@ export class RdiController { responses: [{ status: 200, type: Rdi }], }) async update( - @Param('id') id: string, + @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata, @Body() dto: UpdateRdiDto, ): Promise { - return this.rdiService.update(id, dto); + return this.rdiService.update(rdiClientMetadata, dto); } @Delete() @@ -62,7 +68,26 @@ export class RdiController { description: 'Delete RDI', responses: [{ status: 200 }], }) - async delete(@Body() body: { ids: string[] }): Promise { + async delete( + @Body() body: { ids: string[] }, + ): Promise { return this.rdiService.delete(body.ids); } + + @Get(':id/connect') + @ApiEndpoint({ + description: 'Connect to RDI', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Successfully connected to rdi instance', + }, + ], + }) + async connect( + @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata, + ): Promise { + return this.rdiService.connect(rdiClientMetadata); + } } diff --git a/redisinsight/api/src/modules/rdi/rdi.service.ts b/redisinsight/api/src/modules/rdi/rdi.service.ts index d2dfd0c026..841f384e35 100644 --- a/redisinsight/api/src/modules/rdi/rdi.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi.service.ts @@ -1,9 +1,17 @@ -import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; import { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto'; -import { Rdi, RdiType } from 'src/modules/rdi/models'; +import { Rdi, RdiClientMetadata } from 'src/modules/rdi/models'; import { RdiRepository } from 'src/modules/rdi/repository/rdi.repository'; import { classToClass } from 'src/utils'; +import { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider'; +import { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory'; +import { SessionMetadata } from 'src/common/models'; +import { wrapRdiPipelineError } from 'src/modules/rdi/exceptions'; import { RdiAnalytics } from './rdi.analytics'; @Injectable() @@ -13,6 +21,8 @@ export class RdiService { constructor( private readonly repository: RdiRepository, private readonly analytics: RdiAnalytics, + private readonly rdiClientProvider: RdiClientProvider, + private readonly rdiClientFactory: RdiClientFactory, ) {} async list(): Promise { @@ -29,22 +39,51 @@ export class RdiService { return rdi; } - async update(id: string, dto: UpdateRdiDto): Promise { - return await this.repository.update(id, dto); + async update(rdiClientMetadata: RdiClientMetadata, dto: UpdateRdiDto): Promise { + // TODO update dto to get only updated fields + const model = classToClass(Rdi, dto); + + await this.rdiClientProvider.delete(rdiClientMetadata); + + try { + await this.rdiClientFactory.createClient(rdiClientMetadata, model); + } catch (error) { + throw wrapRdiPipelineError(error); + } + return await this.repository.update(rdiClientMetadata.id, model); } - async create(dto: CreateRdiDto): Promise { + async create(sessionMetadata: SessionMetadata, dto: CreateRdiDto): Promise { const model = classToClass(Rdi, dto); model.lastConnection = new Date(); - model.type = RdiType.API; + // TODO add request to get version model.version = '1.2'; + const rdiClientMetadata = { + sessionMetadata, + }; + + try { + await this.rdiClientFactory.createClient(rdiClientMetadata, model); + } catch (error) { + this.logger.error('Failed to create rdi instance'); + + throw wrapRdiPipelineError(error); + } + + this.logger.log(`Succeed to create rdi instance`); return await this.repository.create(model); } async delete(ids: string[]): Promise { try { await this.repository.delete(ids); + await Promise.all( + ids.map(async (id) => { + await this.rdiClientProvider.deleteManyByRdiId(id); + }), + ); + this.analytics.sendRdiInstanceDeleted(ids.length); } catch (error) { this.logger.error(`Failed to delete instance(s): ${ids}`, error.message); @@ -52,4 +91,19 @@ export class RdiService { throw new InternalServerErrorException(); } } + + /** + * Connect to rdi instance last connected time + * @param rdiClientMetadata + */ + async connect(rdiClientMetadata: RdiClientMetadata): Promise { + try { + await this.rdiClientProvider.getOrCreate(rdiClientMetadata); + } catch (error) { + this.logger.error(`Failed to connect to rdi instance ${rdiClientMetadata.id}`); + throw wrapRdiPipelineError(error); + } + + this.logger.log(`Succeed to connect to rdi instance ${rdiClientMetadata.id}`); + } } diff --git a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts index 8bbe1f28cf..2b53f689b5 100644 --- a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts +++ b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts @@ -44,10 +44,7 @@ export class LocalRdiRepository extends RdiRepository { .select([ 'r.id', 'r.name', - 'r.host', - 'r.port', 'r.url', - 'r.type', 'r.version', 'r.username', 'r.lastConnection', @@ -64,9 +61,7 @@ export class LocalRdiRepository extends RdiRepository { return classToClass( Rdi, - await this.modelEncryptor.decryptEntity( - await this.repository.save(await this.modelEncryptor.encryptEntity(entity)), - ), + await this.repository.save(await this.modelEncryptor.encryptEntity(entity)), ); } From df52fd6183f2972dc0f6008b115ba8c0c4f47e87 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 23 May 2024 03:06:02 +0400 Subject: [PATCH 02/13] #RI-5661 - remove commented code --- .../api/src/modules/rdi/providers/rdi.client.factory.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts index 6bef222619..028d6ec344 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts @@ -47,7 +47,6 @@ describe('RdiClientFactory', () => { username: mockRdi.username, }, ); - // expect(client).toEqual(mockRdiClient); }); it('should not create client if auth request will failed', async () => { mockedAxios.post.mockRejectedValue(mockRdiUnauthorizedError); From b35d09e1444655b9f3274bf63f058cd2e13b9039 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 09:50:51 +0400 Subject: [PATCH 03/13] #RI-5619 - update rdi test connections --- .../TestConnectionsTable.tsx | 14 ++--- redisinsight/ui/src/slices/interfaces/rdi.ts | 63 ++++++++++++------- .../ui/src/slices/rdi/testConnections.ts | 18 +++--- .../transformers/transformRdiPipeline.spec.ts | 55 +++++++++++++++- .../transformers/transformRdiPipeline.ts | 29 ++++++++- 5 files changed, 135 insertions(+), 44 deletions(-) diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx index 5db3745219..2c7540635d 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx @@ -16,22 +16,16 @@ const TestConnectionsTable = (props: Props) => { const columns: EuiBasicTableColumn[] = [ { - name: '#', - field: 'index', - width: '4%', - render: (index: number) => (({index})) - }, - { - name: 'Endpoint', - field: 'endpoint', - width: '48%', + name: 'Name', + field: 'target', + width: '50%', truncateText: true, render: (endpoint: string, { index }) => (
{endpoint}
) }, { name: 'Result', field: 'error', - width: '48%', + width: '50%', truncateText: true, render: (error: Maybe, { status, index }) => (
diff --git a/redisinsight/ui/src/slices/interfaces/rdi.ts b/redisinsight/ui/src/slices/interfaces/rdi.ts index 67a5ee0d35..0972155fd9 100644 --- a/redisinsight/ui/src/slices/interfaces/rdi.ts +++ b/redisinsight/ui/src/slices/interfaces/rdi.ts @@ -39,29 +39,6 @@ export interface IDryRunJobResults { output: IDryRunJobOutput[] } -export enum TestConnectionStatus { - Fail = 'fail', - Success = 'success', -} - -export interface ITestConnection { - fail: TestConnectionResult[] - success: TestConnectionResult[] -} - -export interface TestConnectionResult { - endpoint: string - index: number - error?: string - status: TestConnectionStatus -} - -export interface IStateRdiTestConnections { - loading: boolean - error: string - results: Nullable -} - export interface IRdiPipelineStrategy { strategy: string databases: string[] @@ -224,3 +201,43 @@ export interface IStateJobFunction { detail: string documentation: string } + +// Rdi test target connections +export enum TestConnectionStatus { + Fail = 'fail', + Success = 'success', +} + +interface IErrorDetail { + code: string; + message: string; +} + +interface ISourceDetail { + status: TestConnectionStatus; + error?: IErrorDetail; +} + +export interface ISources { + [key: string]: ISourceDetail; +} + +export interface TestConnectionsResponse { + sources: ISources +} + +interface Result { + target: string; + error?: string; +} + +export interface TransformResult { + success: Result[]; + fail: Result[]; +} + +export interface IStateRdiTestConnections { + loading: boolean + error: string + results: Nullable +} diff --git a/redisinsight/ui/src/slices/rdi/testConnections.ts b/redisinsight/ui/src/slices/rdi/testConnections.ts index 321afb6cbf..332be24de4 100644 --- a/redisinsight/ui/src/slices/rdi/testConnections.ts +++ b/redisinsight/ui/src/slices/rdi/testConnections.ts @@ -1,10 +1,14 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' +import yaml from 'js-yaml' import { apiService, } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' -import { getApiErrorMessage, getRdiUrl, isStatusSuccessful } from 'uiSrc/utils' -import { IStateRdiTestConnections, ITestConnection } from 'uiSrc/slices/interfaces' - +import { getApiErrorMessage, getRdiUrl, isStatusSuccessful, transformConnectionResults } from 'uiSrc/utils' +import { + IStateRdiTestConnections, + TestConnectionsResponse, + TransformResult +} from 'uiSrc/slices/interfaces' import { ApiEndpoints } from 'uiSrc/constants' import { AppDispatch, RootState } from '../store' @@ -23,7 +27,7 @@ const rdiTestConnectionsSlice = createSlice({ state.loading = true state.results = null }, - testConnectionsSuccess: (state, { payload }: PayloadAction) => { + testConnectionsSuccess: (state, { payload }: PayloadAction) => { state.loading = false state.results = payload state.error = '' @@ -57,13 +61,13 @@ export function testConnectionsAction( return async (dispatch: AppDispatch) => { try { dispatch(testConnections()) - const { status, data } = await apiService.post( + const { status, data } = await apiService.post( getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_TEST_CONNECTIONS), - config, + yaml.load(config), ) if (isStatusSuccessful(status)) { - dispatch(testConnectionsSuccess(data)) + dispatch(testConnectionsSuccess(transformConnectionResults(data?.sources))) onSuccessAction?.() } } catch (_err) { diff --git a/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts b/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts index 47fb68d064..a0e47eea92 100644 --- a/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts +++ b/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts @@ -1,4 +1,4 @@ -import { pipelineToYaml, pipelineToJson } from 'uiSrc/utils' +import { pipelineToYaml, pipelineToJson, transformConnectionResults } from 'uiSrc/utils' const pipelineToJsonTests: any[] = [ [ @@ -128,3 +128,56 @@ describe('pipelineToYaml', () => { expect(result).toEqual(expected) }) }) + +const transformConnectionResultsTests: any[] = [ + [ + null, + { success: [], fail: [] } + ], + [ + { + target1: { + status: 'success', + error: { + code: 'INVALID_CREDENTIALS', + message: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' + } + }, + target2: { + status: 'fail', + error: { + code: 'INVALID_CREDENTIALS', + message: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' + } + }, + target3: { + status: 'wrong status', + }, + target4: { + status: 'wrong status', + error: { + code: 'INVALID_CREDENTIALS', + message: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' + } + }, + target5: { + status: 'success', + }, + target6: { + unknownProperty: 'foo bar' + }, + }, + { + success: [{ target: 'target1' }, { target: 'target5' }], + fail: [{ target: 'target2', error: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' }], + } + ] +] + +describe('transformConnectionResults', () => { + it.each(transformConnectionResultsTests)('for input: %s (input), should be output: %s', + (input, expected) => { + const result = transformConnectionResults(input) + expect(result).toEqual(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts b/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts index b43d0b4f9b..490c986acd 100644 --- a/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts +++ b/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts @@ -1,5 +1,5 @@ import yaml from 'js-yaml' -import { IPipeline, IPipelineJSON } from 'uiSrc/slices/interfaces' +import { IPipeline, IPipelineJSON, ISources, TestConnectionStatus, TransformResult } from 'uiSrc/slices/interfaces' export const pipelineToYaml = (pipeline: IPipelineJSON) => ({ config: yaml.dump(pipeline.config), @@ -9,10 +9,33 @@ export const pipelineToYaml = (pipeline: IPipelineJSON) => ({ })) }) -export const pipelineToJson = ({ config, jobs }: IPipeline): IPipelineJSON => ({ +export const pipelineToJson = ({ config, jobs }: IPipeline): IPipelineJSON => ({ config: yaml.load(config) || {}, - jobs: jobs.reduce((acc, job) => { + jobs: jobs.reduce<{ [key: string]: unknown }>((acc, job) => { acc[job.name] = yaml.load(job.value) return acc }, {}) }) + +export const transformConnectionResults = (sources: ISources): TransformResult => { + const result: TransformResult = { success: [], fail: [] } + + if (!sources) { + return result + } + + try { + Object.entries(sources).forEach(([source, details]) => { + if (details.status === TestConnectionStatus.Success) { + result.success.push({ target: source }) + } else if (details.status === TestConnectionStatus.Fail) { + const errorMessage = details.error?.message + result.fail.push({ target: source, error: errorMessage }) + } + }) + } catch (error) { + // ignore + } + + return result +} From 16322a9af7276004d94d1f1807c1342710d60a4f Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 11:49:17 +0400 Subject: [PATCH 04/13] #RI-5619 - add action result requests --- .../api/src/constants/error-messages.ts | 1 + .../src/modules/rdi/client/api.rdi.client.ts | 58 +++++++++++++++++-- .../api/src/modules/rdi/client/rdi.client.ts | 4 +- .../api/src/modules/rdi/constants/index.ts | 3 + .../dto/rdi-test-connections.response.dto.ts | 43 ++++++-------- .../rdi-pipeline.timeout-error.exception.ts | 15 +++++ 6 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.ts diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index c9ada82986..5701ed5ddb 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -105,4 +105,5 @@ export default { COMMON_DEFAULT_IMPORT_ERROR: 'Unable to import default data', RDI_DEPLOY_PIPELINE_FAILURE: 'Failed to deploy pipeline', + RDI_TIMEOUT_ERROR: 'Encountered a timeout error while attempting to retrieve data', }; diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index 979e51b526..e0d5ecc317 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -3,9 +3,23 @@ import { plainToClass } from 'class-transformer'; import { decode } from 'jsonwebtoken'; import { RdiClient } from 'src/modules/rdi/client/rdi.client'; -import { RDI_TIMEOUT, RdiUrl, TOKEN_TRESHOLD } from 'src/modules/rdi/constants'; -import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult } from 'src/modules/rdi/dto'; -import { RdiPipelineDeployFailedException, wrapRdiPipelineError } from 'src/modules/rdi/exceptions'; +import { + RdiUrl, + RDI_TIMEOUT, + TOKEN_TRESHOLD, + POLLING_INTERVAL, + MAX_POLLING_TIME, +} from 'src/modules/rdi/constants'; +import { + RdiDryRunJobDto, + RdiDryRunJobResponseDto, + RdiTestConnectionsResponseDto, +} from 'src/modules/rdi/dto'; +import { + RdiPipelineDeployFailedException, + RdiPipelineInternalServerErrorException, + wrapRdiPipelineError +} from 'src/modules/rdi/exceptions'; import { RdiJob, RdiPipeline, @@ -14,6 +28,7 @@ import { RdiStatisticsData, RdiClientMetadata, Rdi, } from 'src/modules/rdi/models'; import { convertKeysToCamelCase } from 'src/utils/base.helper'; +import { RdiPipelineTimeoutException } from 'src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception'; const RDI_DEPLOY_FAILED_STATUS = 'failed'; @@ -92,11 +107,13 @@ export class ApiRdiClient extends RdiClient { } } - async testConnections(config: string): Promise { + async testConnections(config: string): Promise { try { const response = await this.client.post(RdiUrl.TestConnections, config); - return response.data; + const actionId = response.data.action_id; + + return this.pollActionStatus(actionId); } catch (e) { throw wrapRdiPipelineError(e); } @@ -135,7 +152,10 @@ export class ApiRdiClient extends RdiClient { async connect(): Promise { try { - const response = await this.client.post(RdiUrl.Login, { username: this.rdi.username, password: this.rdi.password }); + const response = await this.client.post( + RdiUrl.Login, + { username: this.rdi.username, password: this.rdi.password }, + ); const accessToken = response.data.access_token; const decodedJwt = decode(accessToken); @@ -153,4 +173,30 @@ export class ApiRdiClient extends RdiClient { await this.connect(); } } + + private async pollActionStatus(actionId: string): Promise { + const startTime = Date.now(); + while (true) { + if (Date.now() - startTime > MAX_POLLING_TIME) { + throw new RdiPipelineTimeoutException(); + } + + await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL)); + + try { + const response = await this.client.get(`${RdiUrl.Action}/${actionId}`); + const { status, data, error } = response.data; + + if (status === 'failed') { + throw new RdiPipelineInternalServerErrorException(error); + } + + if (status === 'completed') { + return data; + } + } catch (e) { + throw wrapRdiPipelineError(e); + } + } + } } diff --git a/redisinsight/api/src/modules/rdi/client/rdi.client.ts b/redisinsight/api/src/modules/rdi/client/rdi.client.ts index 98a326ea8f..1b381699d8 100644 --- a/redisinsight/api/src/modules/rdi/client/rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/rdi.client.ts @@ -3,7 +3,7 @@ import { RdiClientMetadata, RdiJob, RdiPipeline, RdiStatisticsResult, } from 'src/modules/rdi/models'; import { - RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult, + RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionsResponseDto, } from 'src/modules/rdi/dto'; import { IDLE_TRESHOLD } from 'src/modules/rdi/constants'; @@ -38,7 +38,7 @@ export abstract class RdiClient { abstract dryRunJob(data: RdiDryRunJobDto): Promise; - abstract testConnections(config: string): Promise; + abstract testConnections(config: string): Promise; abstract getStatistics(sections?: string): Promise; diff --git a/redisinsight/api/src/modules/rdi/constants/index.ts b/redisinsight/api/src/modules/rdi/constants/index.ts index 712e82e7d7..8dfec4c28c 100644 --- a/redisinsight/api/src/modules/rdi/constants/index.ts +++ b/redisinsight/api/src/modules/rdi/constants/index.ts @@ -10,9 +10,12 @@ export enum RdiUrl { GetStatistics = 'api/v1/monitoring/statistics', GetPipelineStatus = 'api/v1/status', Login = 'api/v1/login', + Action = 'api/v1/actions', } export const IDLE_TRESHOLD = 10 * 60 * 1000; // 10 min export const RDI_TIMEOUT = 30_000; // 30 sec export const TOKEN_TRESHOLD = 2 * 60 * 1000; // 2 min export const RDI_SYNC_INTERVAL = 5 * 60 * 1_000; // 5 min +export const POLLING_INTERVAL = 1_000; +export const MAX_POLLING_TIME = 2 * 60 * 1000; // 2 min diff --git a/redisinsight/api/src/modules/rdi/dto/rdi-test-connections.response.dto.ts b/redisinsight/api/src/modules/rdi/dto/rdi-test-connections.response.dto.ts index 9b10d05e4c..c8ad260ed3 100644 --- a/redisinsight/api/src/modules/rdi/dto/rdi-test-connections.response.dto.ts +++ b/redisinsight/api/src/modules/rdi/dto/rdi-test-connections.response.dto.ts @@ -6,49 +6,44 @@ export enum RdiTestConnectionStatus { Fail = 'fail', } -export class RdiTestConnectionResult { +class ErrorDetails { @ApiProperty({ - type: Number, + description: 'Error code', + type: String, }) @Expose() - index: number; + code: string; @ApiProperty({ - description: 'Connection status', - enum: RdiTestConnectionStatus, + description: 'Error message', + type: String, }) @Expose() - status: RdiTestConnectionStatus; + message: string; +} - @ApiPropertyOptional({ - description: 'Rdi target endpoint', - type: String, +export class RdiTestConnectionResult { + @ApiProperty({ + description: 'Connection status', + enum: RdiTestConnectionStatus, }) @Expose() - endpoint: string; + status: RdiTestConnectionStatus; @ApiPropertyOptional({ - description: 'Error message if any', - type: String, + description: 'Error details if any', + type: ErrorDetails, }) @Expose() - error?: string; + @Type(() => ErrorDetails) + error?: ErrorDetails; } export class RdiTestConnectionsResponseDto { @ApiProperty({ - description: 'Successfully connected targets', - type: RdiTestConnectionResult, - }) - @Expose() - @Type(() => RdiTestConnectionResult) - success: RdiTestConnectionResult; - - @ApiProperty({ - description: 'Failed connected targets', - type: RdiTestConnectionResult, + description: 'Sources connection results', }) @Expose() @Type(() => RdiTestConnectionResult) - fail: RdiTestConnectionResult; + sources: Record; } diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.ts new file mode 100644 index 0000000000..7a3b7d988b --- /dev/null +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.timeout-error.exception.ts @@ -0,0 +1,15 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class RdiPipelineTimeoutException extends HttpException { + constructor(message = ERROR_MESSAGES.RDI_TIMEOUT_ERROR) { + super( + { + statusCode: HttpStatus.REQUEST_TIMEOUT, + message, + error: 'Timeout Error', + }, + HttpStatus.REQUEST_TIMEOUT, + ); + } +} From 894d81a47601f42f82dc7e8a26bb31f052aa3eef Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 11:52:49 +0400 Subject: [PATCH 05/13] #RI-5619 - add trailing comma --- redisinsight/api/src/modules/rdi/client/api.rdi.client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index e0d5ecc317..eaf428985c 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -18,7 +18,7 @@ import { import { RdiPipelineDeployFailedException, RdiPipelineInternalServerErrorException, - wrapRdiPipelineError + wrapRdiPipelineError, } from 'src/modules/rdi/exceptions'; import { RdiJob, From 918bfc4064449fa71277a4976a3259ec1a9bffc5 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 11:54:44 +0400 Subject: [PATCH 06/13] #RI-5661 - update rdi internal error --- .../rdi-pipeline.internal-server-error.exception.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts index 4ec392a9f6..6dc500622c 100644 --- a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.internal-server-error.exception.ts @@ -1,5 +1,4 @@ import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common'; -import { CustomErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; export class RdiPipelineInternalServerErrorException extends HttpException { @@ -11,9 +10,8 @@ export class RdiPipelineInternalServerErrorException extends HttpException { message, statusCode: HttpStatus.INTERNAL_SERVER_ERROR, error: 'RdiInternalServerError', - errorCode: CustomErrorCodes.ConvAiInternalServerError, }; - super(response, response.statusCode, options); + super(response, HttpStatus.INTERNAL_SERVER_ERROR, options); } } From ad36948015b18fdf6ff06dbb2522cd2f37b40df0 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 12:53:08 +0400 Subject: [PATCH 07/13] #RI-5619 - update interfaces --- .../TestConnectionsLog.tsx | 4 +-- .../TestConnectionsTable.spec.tsx | 25 +++++++------------ .../TestConnectionsTable.tsx | 14 +++++------ .../pages/config/Config.tsx | 3 ++- redisinsight/ui/src/slices/interfaces/rdi.ts | 6 ++--- .../ui/src/slices/rdi/testConnections.ts | 5 ++-- 6 files changed, 25 insertions(+), 32 deletions(-) diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.tsx index 937cb03927..4c9a56f529 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-log/TestConnectionsLog.tsx @@ -3,7 +3,7 @@ import cx from 'classnames' import React, { useState } from 'react' import { Nullable } from 'uiSrc/utils' -import { ITestConnection } from 'uiSrc/slices/interfaces' +import { TransformResult } from 'uiSrc/slices/interfaces' import TestConnectionsTable from 'uiSrc/pages/rdi/pipeline-management/components/test-connections-table' import styles from './styles.module.scss' @@ -14,7 +14,7 @@ enum ResultsStatus { } export interface Props { - data: Nullable + data: Nullable } const TestConnectionsLog = (props: Props) => { diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.spec.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.spec.tsx index 46e65db02e..4963fe6a5b 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.spec.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.spec.tsx @@ -1,12 +1,11 @@ import React from 'react' import { render, screen } from 'uiSrc/utils/test-utils' -import { TestConnectionStatus } from 'uiSrc/slices/interfaces' import TestConnectionsTable from './TestConnectionsTable' describe('TestConnectionsTable', () => { it('should render', () => { - render() + render() }) it('should not render table for empty data', () => { @@ -18,31 +17,25 @@ describe('TestConnectionsTable', () => { it('should render table data with success messages', () => { render( ) - expect(screen.getByTestId('table-index-0')).toHaveTextContent('(0)') - expect(screen.getByTestId('table-index-1')).toHaveTextContent('(1)') - expect(screen.getByTestId('table-endpoint-0')).toHaveTextContent('localhost:8080') - expect(screen.getByTestId('table-endpoint-1')).toHaveTextContent('localhost:8081') - expect(screen.getByTestId('table-result-0')).toHaveTextContent('Successful') - expect(screen.getByTestId('table-result-1')).toHaveTextContent('Successful') + expect(screen.getByTestId('table-target-localhost:8080')).toHaveTextContent('localhost:8080') + expect(screen.getByTestId('table-target-localhost:8081')).toHaveTextContent('localhost:8081') + expect(screen.getByTestId('table-result-localhost:8080')).toHaveTextContent('Successful') + expect(screen.getByTestId('table-result-localhost:8081')).toHaveTextContent('Successful') }) it('should render table data with error messages', () => { render( ) - expect(screen.getByTestId('table-result-0')).toHaveTextContent('error') - expect(screen.getByTestId('table-result-1')).toHaveTextContent('Error') - expect(screen.getByTestId('table-result-2')).toHaveTextContent('Error') + expect(screen.getByTestId('table-result-localhost:8080')).toHaveTextContent('error') }) }) diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx index 2c7540635d..e12f353670 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/test-connections-table/TestConnectionsTable.tsx @@ -3,33 +3,33 @@ import cx from 'classnames' import React from 'react' import { Maybe } from 'uiSrc/utils' -import { TestConnectionResult, TestConnectionStatus } from 'uiSrc/slices/interfaces' +import { IRdiConnectionResult } from 'uiSrc/slices/interfaces' import styles from './styles.module.scss' export interface Props { - data: Array + data: Array } const TestConnectionsTable = (props: Props) => { const { data } = props - const columns: EuiBasicTableColumn[] = [ + const columns: EuiBasicTableColumn[] = [ { name: 'Name', field: 'target', width: '50%', truncateText: true, - render: (endpoint: string, { index }) => (
{endpoint}
) + render: (target: string) =>
{target}
}, { name: 'Result', field: 'error', width: '50%', truncateText: true, - render: (error: Maybe, { status, index }) => ( -
- {status === TestConnectionStatus.Fail ? error || 'Error' : 'Successful'} + render: (error: Maybe, { target }) => ( +
+ {error || 'Successful'}
) } diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx index 9b6851d786..3c1ea45eac 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/pages/config/Config.tsx @@ -6,6 +6,7 @@ import cx from 'classnames' import { useParams } from 'react-router-dom' import { get, throttle } from 'lodash' +import yaml from 'js-yaml' import { sendPageViewTelemetry, sendEventTelemetry, TelemetryPageView, TelemetryEvent } from 'uiSrc/telemetry' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { rdiPipelineSelector, setChangedFile, deleteChangedFile } from 'uiSrc/slices/rdi/pipeline' @@ -52,7 +53,7 @@ const Config = () => { const testConnections = () => { setIsPanelOpen(true) - dispatch(testConnectionsAction(rdiInstanceId, config)) + dispatch(testConnectionsAction(rdiInstanceId, yaml.load(config))) sendEventTelemetry({ event: TelemetryEvent.RDI_TEST_TARGET_CONNECTIONS_CLICKED, eventData: { diff --git a/redisinsight/ui/src/slices/interfaces/rdi.ts b/redisinsight/ui/src/slices/interfaces/rdi.ts index 0972155fd9..9dc4cdc156 100644 --- a/redisinsight/ui/src/slices/interfaces/rdi.ts +++ b/redisinsight/ui/src/slices/interfaces/rdi.ts @@ -226,14 +226,14 @@ export interface TestConnectionsResponse { sources: ISources } -interface Result { +export interface IRdiConnectionResult { target: string; error?: string; } export interface TransformResult { - success: Result[]; - fail: Result[]; + success: IRdiConnectionResult[]; + fail: IRdiConnectionResult[]; } export interface IStateRdiTestConnections { diff --git a/redisinsight/ui/src/slices/rdi/testConnections.ts b/redisinsight/ui/src/slices/rdi/testConnections.ts index 332be24de4..bd76343362 100644 --- a/redisinsight/ui/src/slices/rdi/testConnections.ts +++ b/redisinsight/ui/src/slices/rdi/testConnections.ts @@ -1,6 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' -import yaml from 'js-yaml' import { apiService, } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { getApiErrorMessage, getRdiUrl, isStatusSuccessful, transformConnectionResults } from 'uiSrc/utils' @@ -54,7 +53,7 @@ export default rdiTestConnectionsSlice.reducer // Asynchronous thunk action export function testConnectionsAction( rdiInstanceId: string, - config: string, + config: unknown, onSuccessAction?: () => void, onFailAction?: () => void, ) { @@ -63,7 +62,7 @@ export function testConnectionsAction( dispatch(testConnections()) const { status, data } = await apiService.post( getRdiUrl(rdiInstanceId, ApiEndpoints.RDI_TEST_CONNECTIONS), - yaml.load(config), + config, ) if (isStatusSuccessful(status)) { From 2760059076fb39db87ed60ac8a85b870a173c03d Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 12:56:57 +0400 Subject: [PATCH 08/13] #RI-5619 - update transform function --- .../utils/tests/transformers/transformRdiPipeline.spec.ts | 8 +++++++- .../ui/src/utils/transformers/transformRdiPipeline.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts b/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts index a0e47eea92..20dbe5e289 100644 --- a/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts +++ b/redisinsight/ui/src/utils/tests/transformers/transformRdiPipeline.spec.ts @@ -166,10 +166,16 @@ const transformConnectionResultsTests: any[] = [ target6: { unknownProperty: 'foo bar' }, + target7: { + status: 'fail', + }, }, { success: [{ target: 'target1' }, { target: 'target5' }], - fail: [{ target: 'target2', error: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' }], + fail: [ + { target: 'target2', error: 'Failed to establish connection to the PostgreSQL database. Invalid credentials provided' }, + { target: 'target7', error: 'Error' } + ], } ] ] diff --git a/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts b/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts index 490c986acd..bf8c74d16e 100644 --- a/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts +++ b/redisinsight/ui/src/utils/transformers/transformRdiPipeline.ts @@ -29,7 +29,7 @@ export const transformConnectionResults = (sources: ISources): TransformResult = if (details.status === TestConnectionStatus.Success) { result.success.push({ target: source }) } else if (details.status === TestConnectionStatus.Fail) { - const errorMessage = details.error?.message + const errorMessage = details.error?.message || 'Error' result.fail.push({ target: source, error: errorMessage }) } }) From 8c1b23ca9563178351393eb94827c750eca66284 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 13:02:17 +0400 Subject: [PATCH 09/13] #RI-5619 - fix test --- .../ui/src/slices/tests/rdi/testConnections.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/slices/tests/rdi/testConnections.spec.ts b/redisinsight/ui/src/slices/tests/rdi/testConnections.spec.ts index 09aaa2a2f7..4bac164679 100644 --- a/redisinsight/ui/src/slices/tests/rdi/testConnections.spec.ts +++ b/redisinsight/ui/src/slices/tests/rdi/testConnections.spec.ts @@ -13,8 +13,11 @@ import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' const mockData = { - success: [{ index: 0, status: 'success', endpoint: 'redis-13386.c278.us-east-1-4.ec2.cloud.redislabs.com:13386' }], - fail: [], + sources: { + target: { + status: 'success', + } + } } let store: typeof mockedStore @@ -109,7 +112,7 @@ describe('rdi test connections slice', () => { // Assert const expectedActions = [ testConnections(), - testConnectionsSuccess(mockData), + testConnectionsSuccess({ success: [{ target: 'target' }], fail: [] }), ] expect(store.getActions()).toEqual(expectedActions) From d4e05e7ff538dbca6d66829bbdbe562c644828a2 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 13:35:48 +0400 Subject: [PATCH 10/13] #RI-5619 - resolve comments --- redisinsight/api/src/modules/rdi/client/api.rdi.client.ts | 4 ++-- redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts | 6 +++--- redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index eaf428985c..7cb84929ef 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -181,8 +181,6 @@ export class ApiRdiClient extends RdiClient { throw new RdiPipelineTimeoutException(); } - await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL)); - try { const response = await this.client.get(`${RdiUrl.Action}/${actionId}`); const { status, data, error } = response.data; @@ -197,6 +195,8 @@ export class ApiRdiClient extends RdiClient { } catch (e) { throw wrapRdiPipelineError(e); } + + await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVAL)); } } } diff --git a/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts b/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts index 861bf24bf5..7979384b71 100644 --- a/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts +++ b/redisinsight/api/src/modules/rdi/rdi-pipeline.controller.ts @@ -8,7 +8,7 @@ import { ApiTags } from '@nestjs/swagger'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { RdiPipelineService } from 'src/modules/rdi/rdi-pipeline.service'; import { RequestRdiClientMetadata } from 'src/modules/rdi/decorators'; -import { RdiDryRunJobDto, RdiTestConnectionResult } from 'src/modules/rdi/dto'; +import { RdiDryRunJobDto, RdiTestConnectionsResponseDto } from 'src/modules/rdi/dto'; import { RdiDryRunJobResponseDto } from 'src/modules/rdi/dto/rdi.dry-run.job.response.dto'; @ApiTags('RDI') @@ -69,12 +69,12 @@ export class RdiPipelineController { @Post('/test-connections') @ApiEndpoint({ description: 'Test target connections', - responses: [{ status: 200, type: RdiTestConnectionResult }], + responses: [{ status: 200, type: RdiTestConnectionsResponseDto }], }) async testConnections( @RequestRdiClientMetadata() rdiClientMetadata: RdiClientMetadata, @Body() config: string, - ): Promise { + ): Promise { return this.rdiPipelineService.testConnections(rdiClientMetadata, config); } diff --git a/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts b/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts index 8e4f9300bc..c22f0a4e07 100644 --- a/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi-pipeline.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { RdiClientMetadata, RdiPipeline } from 'src/modules/rdi/models'; import { RdiClientProvider } from 'src/modules/rdi/providers/rdi.client.provider'; -import { RdiDryRunJobDto, RdiTestConnectionResult } from 'src/modules/rdi/dto'; +import { RdiDryRunJobDto, RdiTestConnectionsResponseDto } from 'src/modules/rdi/dto'; import { RdiDryRunJobResponseDto } from 'src/modules/rdi/dto/rdi.dry-run.job.response.dto'; import { RdiPipelineAnalytics } from 'src/modules/rdi/rdi-pipeline.analytics'; import { wrapHttpError } from 'src/common/utils'; @@ -69,7 +69,7 @@ export class RdiPipelineService { } } - async testConnections(rdiClientMetadata: RdiClientMetadata, config: string): Promise { + async testConnections(rdiClientMetadata: RdiClientMetadata, config: string): Promise { this.logger.log('Trying to test connections'); const client = await this.rdiClientProvider.getOrCreate(rdiClientMetadata); From 0c31b53fd9f34c0077e0025c0b6d0a419a89162d Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 18:46:23 +0400 Subject: [PATCH 11/13] #RI-5661 - add test --- .../rdi/providers/rdi.client.storage.spec.ts | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts index 06ca739c80..7d7558b051 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts @@ -3,29 +3,38 @@ import { generateMockRdiClient } from 'src/__mocks__'; import { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage'; import { IDLE_TRESHOLD } from 'src/modules/rdi/constants'; +const mockClientMetadata1 = { + sessionMetadata: { + userId: 'u1', + sessionId: 's1', + }, + id: 'id1', +}; + +const mockNotExistClientMetadata = { + sessionMetadata: { + userId: 'not exist', + sessionId: 'not exist', + }, + id: 'not exist', +}; + +const mockRdiClient1 = generateMockRdiClient(mockClientMetadata1); +const mockRdiClient2 = generateMockRdiClient({ + ...mockClientMetadata1, + sessionMetadata: { userId: 'u2', sessionId: 's1' }, +}); +const mockRdiClient3 = generateMockRdiClient({ + ...mockClientMetadata1, + sessionMetadata: { userId: 'u2', sessionId: 's3' }, +}); +const mockRdiClient4 = generateMockRdiClient({ + ...mockClientMetadata1, + id: 'id2', +}); + describe('RdiClientStorage', () => { let service: RdiClientStorage; - const mockClientMetadata1 = { - sessionMetadata: { - userId: 'u1', - sessionId: 's1', - }, - id: 'id1', - }; - - const mockRdiClient1 = generateMockRdiClient(mockClientMetadata1); - const mockRdiClient2 = generateMockRdiClient({ - ...mockClientMetadata1, - sessionMetadata: { userId: 'u2', sessionId: 's1' }, - }); - const mockRdiClient3 = generateMockRdiClient({ - ...mockClientMetadata1, - sessionMetadata: { userId: 'u2', sessionId: 's3' }, - }); - const mockRdiClient4 = generateMockRdiClient({ - ...mockClientMetadata1, - id: 'id2', - }); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -64,6 +73,7 @@ describe('RdiClientStorage', () => { expect(service['clients'].size).toEqual(3); expect(service['clients'].get(mockRdiClient1.id)).toEqual(undefined); }); + describe('get', () => { it('should correctly get client instance and update last used time', async () => { // eslint-disable-next-line prefer-destructuring @@ -91,6 +101,12 @@ describe('RdiClientStorage', () => { expect(result).toEqual(service['clients'].get(mockRdiClient1.id)); expect(result['lastUsed']).toBeGreaterThan(lastUsed); }); + + it('should not fail when there is no client', async () => { + const result = await service.getByMetadata(mockNotExistClientMetadata); + + expect(result).toBeUndefined(); + }); }); describe('set', () => { From 5f87925e70044adaadfaf2c94d49a944e8eedb4b Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 24 May 2024 19:41:41 +0400 Subject: [PATCH 12/13] #RI-5661 - add test --- redisinsight/api/migration/index.ts | 2 ++ redisinsight/api/package.json | 2 +- redisinsight/api/src/__mocks__/rdi.ts | 1 - .../rdi/providers/rdi.client.factory.spec.ts | 2 +- .../rdi/providers/rdi.client.storage.spec.ts | 29 +++++++++++++++++++ .../api/src/modules/rdi/rdi.controller.ts | 2 +- .../api/src/modules/rdi/rdi.service.ts | 4 +-- .../rdi/repository/local.rdi.repository.ts | 4 ++- 8 files changed, 39 insertions(+), 7 deletions(-) diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 7817e5c5c1..61051fff00 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -40,6 +40,7 @@ import { CloudCapiKeys1691061058385 } from './1691061058385-cloud-capi-keys'; import { FeatureSso1691476419592 } from './1691476419592-feature-sso'; import { AiHistory1713515657364 } from './1713515657364-ai-history'; import { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps'; +import { Rdi1716370509836 } from './1716370509836-rdi'; export default [ initialMigration1614164490968, @@ -84,4 +85,5 @@ export default [ FeatureSso1691476419592, AiHistory1713515657364, AiHistorySteps1714501203616, + Rdi1716370509836, ]; diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 86be960962..5aa3a11d51 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -33,7 +33,7 @@ "test:api": "cross-env NODE_ENV=test ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml", "test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api", "test:api:ci:cov": "cross-env nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json", - "typeorm:migrate": " yarncross-env NODE_ENV=staging typeorm migration:generate ./migration/migration", + "typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration", "typeorm:run": "yarn typeorm migration:run", "typeorm:stage:run": "cross-env NODE_ENV=staging yarn typeorm migration:run" }, diff --git a/redisinsight/api/src/__mocks__/rdi.ts b/redisinsight/api/src/__mocks__/rdi.ts index 2829b8ebba..a7304496a8 100644 --- a/redisinsight/api/src/__mocks__/rdi.ts +++ b/redisinsight/api/src/__mocks__/rdi.ts @@ -1,4 +1,3 @@ -import { RdiClient } from 'src/modules/rdi/client/rdi.client'; import { Rdi, RdiClientMetadata, diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts index 028d6ec344..7d394a3ef7 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.spec.ts @@ -38,7 +38,7 @@ describe('RdiClientFactory', () => { access_token: mockedAccessToken, }, }); - const client = await service.createClient(mockRdiClientMetadata, mockRdi); + await service.createClient(mockRdiClientMetadata, mockRdi); expect(mockedAxios.post).toHaveBeenCalledWith( RdiUrl.Login, diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts index 7d7558b051..ab15b93679 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts @@ -1,7 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; import { generateMockRdiClient } from 'src/__mocks__'; import { RdiClientStorage } from 'src/modules/rdi/providers/rdi.client.storage'; import { IDLE_TRESHOLD } from 'src/modules/rdi/constants'; +import { SessionMetadata } from 'src/common/models'; const mockClientMetadata1 = { sessionMetadata: { @@ -138,6 +140,33 @@ describe('RdiClientStorage', () => { expect(result).toEqual(newClient); expect(service['clients'].size).toEqual(1); }); + + it('should throw BadRequestException when metadata is invalid', async () => { + await expect(service.set(generateMockRdiClient({ + sessionMetadata: {} as SessionMetadata, + id: 'id', + }))).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + + await expect(service.set(generateMockRdiClient({ + sessionMetadata: { + userId: 'u2', sessionId: undefined, + } as SessionMetadata, + id: 'id', + }))).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + + await expect(service.set(generateMockRdiClient({ + sessionMetadata: { + userId: undefined, sessionId: 's2', + } as SessionMetadata, + id: 'id', + }))).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + }); }); describe('delete', () => { diff --git a/redisinsight/api/src/modules/rdi/rdi.controller.ts b/redisinsight/api/src/modules/rdi/rdi.controller.ts index de9c1e6260..1fcd6a5d05 100644 --- a/redisinsight/api/src/modules/rdi/rdi.controller.ts +++ b/redisinsight/api/src/modules/rdi/rdi.controller.ts @@ -9,7 +9,7 @@ import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto'; import { RequestRdiClientMetadata } from 'src/modules/rdi/decorators'; import { RequestSessionMetadata } from 'src/common/decorators'; -import {SessionMetadata} from "src/common/models"; +import { SessionMetadata } from 'src/common/models'; @ApiTags('RDI') @UsePipes(new ValidationPipe({ transform: true })) diff --git a/redisinsight/api/src/modules/rdi/rdi.service.ts b/redisinsight/api/src/modules/rdi/rdi.service.ts index 841f384e35..cca119c6c1 100644 --- a/redisinsight/api/src/modules/rdi/rdi.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi.service.ts @@ -71,7 +71,7 @@ export class RdiService { throw wrapRdiPipelineError(error); } - this.logger.log(`Succeed to create rdi instance`); + this.logger.log('Succeed to create rdi instance'); return await this.repository.create(model); } @@ -93,7 +93,7 @@ export class RdiService { } /** - * Connect to rdi instance last connected time + * Connect to rdi instance * @param rdiClientMetadata */ async connect(rdiClientMetadata: RdiClientMetadata): Promise { diff --git a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts index 2b53f689b5..64eab7d36a 100644 --- a/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts +++ b/redisinsight/api/src/modules/rdi/repository/local.rdi.repository.ts @@ -61,7 +61,9 @@ export class LocalRdiRepository extends RdiRepository { return classToClass( Rdi, - await this.repository.save(await this.modelEncryptor.encryptEntity(entity)), + await this.modelEncryptor.decryptEntity( + await this.repository.save(await this.modelEncryptor.encryptEntity(entity)), + ), ); } From 9feec72755a3bb904ab88f48a60f8bd0aab390bb Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 27 May 2024 11:39:22 +0400 Subject: [PATCH 13/13] #RI-5661 - resolve comments --- redisinsight/api/package.json | 2 +- .../src/modules/rdi/client/api.rdi.client.ts | 5 --- .../api/src/modules/rdi/client/rdi.client.ts | 2 - .../modules/rdi/models/rdi.client.metadata.ts | 8 ++-- .../rdi/providers/rdi.client.storage.spec.ts | 39 ++++++++++++++++++- .../rdi/providers/rdi.client.storage.ts | 8 ++++ .../api/src/modules/rdi/rdi.service.ts | 2 + 7 files changed, 53 insertions(+), 13 deletions(-) diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 5aa3a11d51..5e1dc7e7b3 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -35,7 +35,7 @@ "test:api:ci:cov": "cross-env nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json", "typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration", "typeorm:run": "yarn typeorm migration:run", - "typeorm:stage:run": "cross-env NODE_ENV=staging yarn typeorm migration:run" + "typeorm:run:stage": "cross-env NODE_ENV=staging yarn typeorm migration:run" }, "resolutions": { "nanoid": "^3.1.31", diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts index 979e51b526..24fe56b8c2 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts @@ -7,7 +7,6 @@ import { RDI_TIMEOUT, RdiUrl, TOKEN_TRESHOLD } from 'src/modules/rdi/constants'; import { RdiDryRunJobDto, RdiDryRunJobResponseDto, RdiTestConnectionResult } from 'src/modules/rdi/dto'; import { RdiPipelineDeployFailedException, wrapRdiPipelineError } from 'src/modules/rdi/exceptions'; import { - RdiJob, RdiPipeline, RdiStatisticsResult, RdiStatisticsStatus, @@ -79,10 +78,6 @@ export class ApiRdiClient extends RdiClient { } } - async deployJob(): Promise { - return null; - } - async dryRunJob(data: RdiDryRunJobDto): Promise { try { const response = await this.client.post(RdiUrl.DryRunJob, data); diff --git a/redisinsight/api/src/modules/rdi/client/rdi.client.ts b/redisinsight/api/src/modules/rdi/client/rdi.client.ts index 98a326ea8f..ce0117c0ad 100644 --- a/redisinsight/api/src/modules/rdi/client/rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/rdi.client.ts @@ -34,8 +34,6 @@ export abstract class RdiClient { abstract deploy(pipeline: RdiPipeline): Promise; - abstract deployJob(job: RdiJob): Promise; - abstract dryRunJob(data: RdiDryRunJobDto): Promise; abstract testConnections(config: string): Promise; diff --git a/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts b/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts index 089635fb39..bbeaeaaf9f 100644 --- a/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts +++ b/redisinsight/api/src/modules/rdi/models/rdi.client.metadata.ts @@ -1,7 +1,8 @@ -import { Session, SessionMetadata } from 'src/common/models/session'; +import {Session, SessionMetadata} from 'src/common/models/session'; import { Type } from 'class-transformer'; import { - IsNotEmpty, IsOptional, IsString, + IsNotEmpty, + IsString, } from 'class-validator'; export class RdiClientMetadata { @@ -10,7 +11,6 @@ export class RdiClientMetadata { sessionMetadata: SessionMetadata; @IsNotEmpty() - @IsOptional() @IsString() - id?: string; + id: string; } diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts index ab15b93679..c92f918548 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.spec.ts @@ -109,6 +109,35 @@ describe('RdiClientStorage', () => { expect(result).toBeUndefined(); }); + + it('should throw BadRequestException when metadata is invalid', async () => { + await expect(service.getByMetadata({ + ...mockNotExistClientMetadata, + id: undefined, + })).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + + await expect(service.getByMetadata({ + ...mockNotExistClientMetadata, + sessionMetadata: { + ...mockNotExistClientMetadata.sessionMetadata, + sessionId: undefined, + }, + })).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + + await expect(service.getByMetadata({ + ...mockNotExistClientMetadata, + sessionMetadata: { + ...mockNotExistClientMetadata.sessionMetadata, + userId: undefined, + }, + })).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + }); }); describe('set', () => { @@ -119,7 +148,6 @@ describe('RdiClientStorage', () => { it('should add new client', async () => { expect(service['clients'].size).toEqual(0); - const result = await service.set(mockRdiClient1); expect(result).toEqual(mockRdiClient1); @@ -149,6 +177,15 @@ describe('RdiClientStorage', () => { new BadRequestException('Client metadata missed required properties'), ); + await expect(service.set(generateMockRdiClient({ + sessionMetadata: { + userId: 'u2', sessionId: 's1', + } as SessionMetadata, + id: undefined, + }))).rejects.toThrow( + new BadRequestException('Client metadata missed required properties'), + ); + await expect(service.set(generateMockRdiClient({ sessionMetadata: { userId: 'u2', sessionId: undefined, diff --git a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts index c5b5f1b9c4..2166032d77 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.storage.ts @@ -42,6 +42,13 @@ export class RdiClientStorage { } async getByMetadata(rdiClientMetadata: RdiClientMetadata): Promise { + if ( + !rdiClientMetadata.id + || !rdiClientMetadata.sessionMetadata?.sessionId + || !rdiClientMetadata.sessionMetadata.userId + ) { + throw new BadRequestException('Client metadata missed required properties'); + } return this.get(RdiClient.generateId(rdiClientMetadata)); } @@ -87,6 +94,7 @@ export class RdiClientStorage { !client.id || !client.metadata.sessionMetadata?.sessionId || !client.metadata.sessionMetadata.userId + || !client.metadata.id ) { throw new BadRequestException('Client metadata missed required properties'); } diff --git a/redisinsight/api/src/modules/rdi/rdi.service.ts b/redisinsight/api/src/modules/rdi/rdi.service.ts index cca119c6c1..e308a76f80 100644 --- a/redisinsight/api/src/modules/rdi/rdi.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi.service.ts @@ -3,6 +3,7 @@ import { InternalServerErrorException, Logger, } from '@nestjs/common'; +import { v4 as uuidv4 } from 'uuid'; import { CreateRdiDto, UpdateRdiDto } from 'src/modules/rdi/dto'; import { Rdi, RdiClientMetadata } from 'src/modules/rdi/models'; @@ -61,6 +62,7 @@ export class RdiService { const rdiClientMetadata = { sessionMetadata, + id: uuidv4(), }; try {