diff --git a/redisinsight/api/src/__mocks__/rdi.ts b/redisinsight/api/src/__mocks__/rdi.ts index e5b06762c6..512074492a 100644 --- a/redisinsight/api/src/__mocks__/rdi.ts +++ b/redisinsight/api/src/__mocks__/rdi.ts @@ -4,7 +4,7 @@ import { RdiPipeline, RdiStatisticsData, } from 'src/modules/rdi/models'; -import { ApiRdiClient } from 'src/modules/rdi/client/api.rdi.client'; +import { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client'; import { RdiEntity } from 'src/modules/rdi/entities/rdi.entity'; import { EncryptionStrategy } from 'src/modules/encryption/models'; import { RdiDryRunJobDto } from 'src/modules/rdi/dto'; diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.spec.ts b/redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.spec.ts similarity index 98% rename from redisinsight/api/src/modules/rdi/client/api.rdi.client.spec.ts rename to redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.spec.ts index 616b80d216..99506bb7d7 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.spec.ts +++ b/redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.spec.ts @@ -11,9 +11,17 @@ import { mockRdiUnauthorizedError, } from 'src/__mocks__'; import { sign } from 'jsonwebtoken'; -import { ApiRdiClient } from './api.rdi.client'; -import { RdiDyRunJobStatus, RdiPipeline, RdiStatisticsStatus } from '../models'; -import { PipelineActions, RdiUrl, TOKEN_THRESHOLD } from '../constants'; +import { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client'; +import { + RdiDyRunJobStatus, + RdiPipeline, + RdiStatisticsStatus, +} from 'src/modules/rdi/models'; +import { + PipelineActions, + RdiUrl, + TOKEN_THRESHOLD, +} from 'src/modules/rdi/constants'; const mockedAxios = axios as jest.Mocked; jest.mock('axios'); diff --git a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.ts similarity index 96% rename from redisinsight/api/src/modules/rdi/client/api.rdi.client.ts rename to redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.ts index 75bdd4d3cf..ff0d83ff6f 100644 --- a/redisinsight/api/src/modules/rdi/client/api.rdi.client.ts +++ b/redisinsight/api/src/modules/rdi/client/api/v1/api.rdi.client.ts @@ -26,6 +26,9 @@ import { RdiPipelineInternalServerErrorException, parseErrorMessage, wrapRdiPipelineError, + RdiResetPipelineFailedException, + RdiStartPipelineFailedException, + RdiStopPipelineFailedException, } from 'src/modules/rdi/exceptions'; import { RdiPipeline, @@ -42,9 +45,6 @@ import { convertApiDataToRdiPipeline, convertRdiPipelineToApiPayload, } from 'src/modules/rdi/utils/pipeline.util'; -import { RdiResetPipelineFailedException } from '../exceptions/rdi-reset-pipeline-failed.exception'; -import { RdiStartPipelineFailedException } from '../exceptions/rdi-start-pipeline-failed.exception'; -import { RdiStopPipelineFailedException } from '../exceptions/rdi-stop-pipeline-failed.exception'; interface ConnectionsConfig { sources: Record>; @@ -53,7 +53,7 @@ interface ConnectionsConfig { export class ApiRdiClient extends RdiClient { protected readonly client: AxiosInstance; - private readonly logger = new Logger('ApiRdiClient'); + protected readonly logger = new Logger('ApiRdiClient'); private auth: { jwt: string; exp: number }; diff --git a/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.spec.ts b/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.spec.ts new file mode 100644 index 0000000000..05a1f91a8a --- /dev/null +++ b/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.spec.ts @@ -0,0 +1,166 @@ +import axios from 'axios'; +import { + mockRdi, + mockRdiClientMetadata, + mockRdiUnauthorizedError, +} from 'src/__mocks__'; +import { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client'; +import { RdiUrlV2 } from 'src/modules/rdi/constants'; +import { RdiInfo } from 'src/modules/rdi/models'; +import { RdiPipelineInternalServerErrorException } from 'src/modules/rdi/exceptions'; + +const mockedAxios = axios as jest.Mocked; +jest.mock('axios'); +mockedAxios.create = jest.fn(() => mockedAxios); + +describe('ApiV2RdiClient', () => { + let client: ApiV2RdiClient; + + beforeEach(() => { + jest.clearAllMocks(); + client = new ApiV2RdiClient(mockRdiClientMetadata, mockRdi); + }); + + describe('getInfo', () => { + it('should return RDI info when API call is successful', async () => { + const mockInfoResponse = { version: '2.0.1' }; + const expectedRdiInfo = Object.assign(new RdiInfo(), { + version: '2.0.1', + }); + mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse }); + + const result = await client.getInfo(); + + expect(result).toEqual(expectedRdiInfo); + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo); + }); + + it('should throw wrapped error when API call fails', async () => { + mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError); + + await expect(client.getInfo()).rejects.toThrow( + mockRdiUnauthorizedError.message, + ); + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo); + }); + + it('should transform response data to RdiInfo instance', async () => { + const mockInfoResponse = { version: '2.1.0' }; + mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse }); + + const result = await client.getInfo(); + + expect(result).toBeInstanceOf(RdiInfo); + expect(result.version).toBe('2.1.0'); + }); + }); + + describe('selectPipeline', () => { + it('should select first pipeline when pipelines are available', async () => { + const mockPipelinesResponse = [ + { + name: 'pipeline-1', + active: true, + config: {}, + status: 'running', + errors: [], + components: [], + current: true, + }, + { + name: 'pipeline-2', + active: false, + config: {}, + status: 'stopped', + errors: [], + components: [], + current: false, + }, + ]; + mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse }); + + await client.selectPipeline(); + + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines); + expect(client['selectedPipeline']).toBe('pipeline-1'); + }); + + it('should throw RdiPipelineInternalServerErrorException when no pipelines available', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: [] }); + + await expect(client.selectPipeline()).rejects.toThrow( + RdiPipelineInternalServerErrorException, + ); + }); + + it('should throw error with message "Unable to select pipeline" when no pipelines available', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: [] }); + + await expect(client.selectPipeline()).rejects.toThrow( + 'Unable to select pipeline', + ); + }); + + it('should throw RdiPipelineInternalServerErrorException when data is null', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: null }); + + await expect(client.selectPipeline()).rejects.toThrow( + RdiPipelineInternalServerErrorException, + ); + }); + + it('should throw RdiPipelineInternalServerErrorException when data is undefined', async () => { + mockedAxios.get.mockResolvedValueOnce({ data: undefined }); + + await expect(client.selectPipeline()).rejects.toThrow( + RdiPipelineInternalServerErrorException, + ); + }); + + it('should throw wrapped error when API call fails', async () => { + mockedAxios.get.mockRejectedValueOnce(mockRdiUnauthorizedError); + + await expect(client.selectPipeline()).rejects.toThrow( + mockRdiUnauthorizedError.message, + ); + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines); + }); + + it('should select first pipeline even when multiple pipelines exist', async () => { + const mockPipelinesResponse = [ + { + name: 'first', + active: false, + config: {}, + status: 'stopped', + errors: [], + components: [], + current: false, + }, + { + name: 'second', + active: true, + config: {}, + status: 'running', + errors: [], + components: [], + current: true, + }, + { + name: 'third', + active: false, + config: {}, + status: 'stopped', + errors: [], + components: [], + current: false, + }, + ]; + mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse }); + + await client.selectPipeline(); + + expect(client['selectedPipeline']).toBe('first'); + }); + }); +}); diff --git a/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.ts b/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.ts new file mode 100644 index 0000000000..ebc96e525c --- /dev/null +++ b/redisinsight/api/src/modules/rdi/client/api/v2/api.v2.rdi.client.ts @@ -0,0 +1,78 @@ +import { plainToInstance } from 'class-transformer'; +import { Logger } from '@nestjs/common'; +import { RdiUrlV2 } from 'src/modules/rdi/constants'; +import { + RdiPipelineInternalServerErrorException, + wrapRdiPipelineError, +} from 'src/modules/rdi/exceptions'; +import { RdiInfo } from 'src/modules/rdi/models'; + +import { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client'; +import { + GetInfoResponse, + GetPipelinesResponse, +} from 'src/modules/rdi/client/api/v2/responses'; + +export class ApiV2RdiClient extends ApiRdiClient { + protected readonly logger = new Logger('ApiV2RdiClient'); + + protected selectedPipeline = 'default'; + + /** + * Retrieves comprehensive information about the RDI (Redis Data Integration) instance. + * + * This method is available starting from RDI API v2 and provides detailed metadata + * about the RDI instance including version, status, and configuration details. + * + * @returns {Promise} A promise that resolves to an RdiInfo object containing + * instance metadata such as version, status, and capabilities + * + * @example + * const info = await client.getInfo(); + * console.log(info.version); // e.g., "1.2.0" + */ + async getInfo(): Promise { + try { + const { data } = await this.client.get(RdiUrlV2.GetInfo); + + return plainToInstance(RdiInfo, data); + } catch (e) { + throw wrapRdiPipelineError(e); + } + } + + /** + * Selects the active pipeline for subsequent RDI operations. + * + * This method fetches all available pipelines from the RDI instance and automatically + * selects the first pipeline in the list. The selected pipeline is stored in the + * `selectedPipeline` property and will be used for all pipeline-specific operations. + * + * In RDI v2, multiple pipelines can exist, but this implementation currently defaults + * to selecting the first available pipeline. If no pipelines exist, an error is thrown. + * + * @returns {Promise} A promise that resolves when the pipeline is successfully selected + * + * @example + * await client.selectPipeline(); + * // client.selectedPipeline is now set to the first available pipeline name + */ + async selectPipeline(): Promise { + try { + const { data } = await this.client.get( + RdiUrlV2.GetPipelines, + ); + + // todo: handle cases when no pipelines differently + if (!data?.length) { + throw new RdiPipelineInternalServerErrorException( + 'Unable to select pipeline', + ); + } + + this.selectedPipeline = data[0].name; + } catch (e) { + throw wrapRdiPipelineError(e); + } + } +} diff --git a/redisinsight/api/src/modules/rdi/client/api/v2/responses/index.ts b/redisinsight/api/src/modules/rdi/client/api/v2/responses/index.ts new file mode 100644 index 0000000000..633d466891 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/client/api/v2/responses/index.ts @@ -0,0 +1,2 @@ +export * from './info.responses'; +export * from './pipeline.responses'; diff --git a/redisinsight/api/src/modules/rdi/client/api/v2/responses/info.responses.ts b/redisinsight/api/src/modules/rdi/client/api/v2/responses/info.responses.ts new file mode 100644 index 0000000000..65368340e1 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/client/api/v2/responses/info.responses.ts @@ -0,0 +1,3 @@ +export interface GetInfoResponse { + version: string; +} diff --git a/redisinsight/api/src/modules/rdi/client/api/v2/responses/pipeline.responses.ts b/redisinsight/api/src/modules/rdi/client/api/v2/responses/pipeline.responses.ts new file mode 100644 index 0000000000..957a6f3fca --- /dev/null +++ b/redisinsight/api/src/modules/rdi/client/api/v2/responses/pipeline.responses.ts @@ -0,0 +1,11 @@ +export interface PipelineResponses { + name: string; + active: boolean; + config: any; // todo: define + status: string; // todo: define enum + errors: any[]; // todo: define + components: any[]; // todo: define + current: true; +} + +export type GetPipelinesResponse = PipelineResponses[]; diff --git a/redisinsight/api/src/modules/rdi/constants/index.ts b/redisinsight/api/src/modules/rdi/constants/index.ts index 1b7296ce93..a09c2f0efe 100644 --- a/redisinsight/api/src/modules/rdi/constants/index.ts +++ b/redisinsight/api/src/modules/rdi/constants/index.ts @@ -19,6 +19,11 @@ export enum RdiUrl { Action = 'api/v1/actions', } +export const RdiUrlV2 = { + GetInfo: 'api/v2/info', + GetPipelines: 'api/v2/pipelines', +}; + export const IDLE_THRESHOLD = 10 * 60 * 1000; // 10 min export const RDI_TIMEOUT = 30_000; // 30 sec export const TOKEN_THRESHOLD = 2 * 60 * 1000; // 2 min diff --git a/redisinsight/api/src/modules/rdi/exceptions/index.ts b/redisinsight/api/src/modules/rdi/exceptions/index.ts index 98701cab01..5e7f09768a 100644 --- a/redisinsight/api/src/modules/rdi/exceptions/index.ts +++ b/redisinsight/api/src/modules/rdi/exceptions/index.ts @@ -4,3 +4,6 @@ export * from './rdi-pipeline.internal-server-error.exception'; export * from './rdi-pipeline.not-found.exception'; export * from './rdi-pipeline.unauthorized.exception'; export * from './rdi-pipeline.validation.exception'; +export * from './rdi-reset-pipeline-failed.exception'; +export * from './rdi-start-pipeline-failed.exception'; +export * from './rdi-stop-pipeline-failed.exception'; diff --git a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.ts b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.ts index f7429a11c1..7796ce6dda 100644 --- a/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.ts +++ b/redisinsight/api/src/modules/rdi/exceptions/rdi-pipeline.error.handler.ts @@ -7,7 +7,7 @@ import { RdiPipelineValidationException, } from 'src/modules/rdi/exceptions'; import { RdiPipelineForbiddenException } from './rdi-pipeline.forbidden.exception'; -import { RdiPipelineBadRequestException } from "src/modules/rdi/exceptions/rdi-pipeline.bad-request.exception"; +import { RdiPipelineBadRequestException } from 'src/modules/rdi/exceptions/rdi-pipeline.bad-request.exception'; export const parseErrorMessage = (error: AxiosError): string => { const data = error.response?.data; diff --git a/redisinsight/api/src/modules/rdi/models/index.ts b/redisinsight/api/src/modules/rdi/models/index.ts index 187b8471fe..3881a847d0 100644 --- a/redisinsight/api/src/modules/rdi/models/index.ts +++ b/redisinsight/api/src/modules/rdi/models/index.ts @@ -3,3 +3,4 @@ export * from './rdi'; export * from './rdi-pipeline'; export * from './rdi-dry-run'; export * from './rdi-statistics'; +export * from './rdi-info'; diff --git a/redisinsight/api/src/modules/rdi/models/rdi-info.ts b/redisinsight/api/src/modules/rdi/models/rdi-info.ts new file mode 100644 index 0000000000..05b0bed723 --- /dev/null +++ b/redisinsight/api/src/modules/rdi/models/rdi-info.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Expose } from 'class-transformer'; + +export class RdiInfo { + @ApiProperty({ + description: 'Current RDI collector version', + type: String, + }) + @Expose() + version: string; +} 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 92e7e5342e..206c50dbf0 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 @@ -7,8 +7,10 @@ import { mockRdiUnauthorizedError, } from 'src/__mocks__'; import { RdiClientFactory } from 'src/modules/rdi/providers/rdi.client.factory'; -import { RdiUrl } from 'src/modules/rdi/constants'; +import { RdiUrl, RdiUrlV2 } from 'src/modules/rdi/constants'; import { RdiPipelineUnauthorizedException } from 'src/modules/rdi/exceptions'; +import { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client'; +import { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client'; const mockedAxios = axios as jest.Mocked; jest.mock('axios'); @@ -19,6 +21,7 @@ describe('RdiClientFactory', () => { let service: RdiClientFactory; beforeEach(async () => { + jest.clearAllMocks(); module = await Test.createTestingModule({ providers: [RdiClientFactory], }).compile(); @@ -27,31 +30,151 @@ describe('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, - }, + describe('v2 client creation', () => { + it('should create v2 client when getInfo succeeds', async () => { + const mockedAccessToken = sign( + { exp: Math.trunc(Date.now() / 1000) + 3600 }, + 'test', + ); + const mockInfoResponse = { version: '2.0.1' }; + const mockPipelinesResponse = [ + { + name: 'pipeline-1', + active: true, + config: {}, + status: 'running', + errors: [], + components: [], + current: true, + }, + ]; + + // Mock getInfo call + mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse }); + // Mock connect call + mockedAxios.post.mockResolvedValueOnce({ + status: 200, + data: { + access_token: mockedAccessToken, + }, + }); + // Mock selectPipeline call + mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse }); + + const client = await service.createClient( + mockRdiClientMetadata, + mockRdi, + ); + + expect(client).toBeInstanceOf(ApiV2RdiClient); + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetInfo); + expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, { + password: mockRdi.password, + username: mockRdi.username, + }); + expect(mockedAxios.get).toHaveBeenCalledWith(RdiUrlV2.GetPipelines); }); - await service.createClient(mockRdiClientMetadata, mockRdi); - expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, { - password: mockRdi.password, - username: mockRdi.username, + it('should call connect and selectPipeline for v2 client', async () => { + const mockedAccessToken = sign( + { exp: Math.trunc(Date.now() / 1000) + 3600 }, + 'test', + ); + const mockInfoResponse = { version: '2.1.0' }; + const mockPipelinesResponse = [ + { + name: 'default', + active: true, + config: {}, + status: 'running', + errors: [], + components: [], + current: true, + }, + ]; + + mockedAxios.get.mockResolvedValueOnce({ data: mockInfoResponse }); + mockedAxios.post.mockResolvedValueOnce({ + status: 200, + data: { access_token: mockedAccessToken }, + }); + mockedAxios.get.mockResolvedValueOnce({ data: mockPipelinesResponse }); + + const client = await service.createClient( + mockRdiClientMetadata, + mockRdi, + ); + + expect(client).toBeInstanceOf(ApiV2RdiClient); + expect(client['selectedPipeline']).toBe('default'); }); }); - it('should not create client if auth request will failed', async () => { - mockedAxios.post.mockRejectedValue(mockRdiUnauthorizedError); - await expect( - service.createClient(mockRdiClientMetadata, mockRdi), - ).rejects.toThrow(RdiPipelineUnauthorizedException); + describe('v1 client fallback', () => { + it('should fallback to v1 client when getInfo fails', async () => { + const mockedAccessToken = sign( + { exp: Math.trunc(Date.now() / 1000) + 3600 }, + 'test', + ); + + // Mock getInfo failure (v2 not available) + mockedAxios.get.mockRejectedValueOnce(new Error('Not found')); + // Mock v1 connect call + mockedAxios.post.mockResolvedValueOnce({ + status: 200, + data: { + access_token: mockedAccessToken, + }, + }); + + const client = await service.createClient( + mockRdiClientMetadata, + mockRdi, + ); + + expect(client).toBeInstanceOf(ApiRdiClient); + expect(client).not.toBeInstanceOf(ApiV2RdiClient); + expect(mockedAxios.post).toHaveBeenCalledWith(RdiUrl.Login, { + password: mockRdi.password, + username: mockRdi.username, + }); + }); + + it('should fallback to v1 client when getInfo returns null', async () => { + const mockedAccessToken = sign( + { exp: Math.trunc(Date.now() / 1000) + 3600 }, + 'test', + ); + + // Mock getInfo returning null (endpoint exists but returns null) + mockedAxios.get.mockResolvedValueOnce({ data: null }); + // Mock v1 connect call + mockedAxios.post.mockResolvedValueOnce({ + status: 200, + data: { + access_token: mockedAccessToken, + }, + }); + + const client = await service.createClient( + mockRdiClientMetadata, + mockRdi, + ); + + expect(client).toBeInstanceOf(ApiRdiClient); + expect(client).not.toBeInstanceOf(ApiV2RdiClient); + }); + + it('should not create client if v1 auth request fails', async () => { + // Mock getInfo failure (v2 not available) + mockedAxios.get.mockRejectedValueOnce(new Error('Not found')); + // Mock v1 auth failure + mockedAxios.post.mockRejectedValueOnce(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 8508130198..059a7837a2 100644 --- a/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts +++ b/redisinsight/api/src/modules/rdi/providers/rdi.client.factory.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; 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'; +import { ApiRdiClient } from 'src/modules/rdi/client/api/v1/api.rdi.client'; +import { ApiV2RdiClient } from 'src/modules/rdi/client/api/v2/api.v2.rdi.client'; @Injectable() export class RdiClientFactory { @@ -9,9 +10,25 @@ export class RdiClientFactory { clientMetadata: RdiClientMetadata, rdi: Rdi, ): Promise { + let rdiClientV2 = new ApiV2RdiClient(clientMetadata, rdi); + let info = null; + + try { + info = await rdiClientV2.getInfo(); + } catch (error) { + // info endpoint is not available + // skip the error and continue without info + } + + // todo: properly verify version from info to determine which client to use + if (info) { + await rdiClientV2.connect(); + await rdiClientV2.selectPipeline(); + return rdiClientV2; + } + const rdiClient = new ApiRdiClient(clientMetadata, rdi); await rdiClient.connect(); - return rdiClient; } }