Skip to content
Merged
6 changes: 3 additions & 3 deletions redisinsight/api/src/__mocks__/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const mockSshOptionsBasic = Object.assign(new SshOptions(), {
port: 22,
username: mockSshOptionsUsernamePlain,
password: mockSshOptionsPasswordPlain,
privateKey: null,
passphrase: null,
privateKey: undefined,
passphrase: undefined,
});

export const mockSshOptionsBasicEntity = Object.assign(new SshOptionsEntity(), {
Expand All @@ -32,7 +32,7 @@ export const mockSshOptionsBasicEntity = Object.assign(new SshOptionsEntity(), {

export const mockSshOptionsPrivateKey = Object.assign(new SshOptions(), {
...mockSshOptionsBasic,
password: null,
password: undefined,
privateKey: mockSshOptionsPrivateKeyPlain,
passphrase: mockSshOptionsPassphrasePlain,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Transform } from 'class-transformer';

export function HiddenField(field: any): PropertyDecorator {
return Transform((value: string) => (value ? field : undefined), {
toPlainOnly: true,
});
}
1 change: 1 addition & 0 deletions redisinsight/api/src/common/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './certificate-import.util';
export * from './errors.util';
export * from './merge.util';
106 changes: 106 additions & 0 deletions redisinsight/api/src/common/utils/merge.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { deepMerge } from 'src/common/utils/merge.util';

const deepMergeTests = [
{ obj1: {}, obj2: {}, result: {} },
{ obj1: { value: 1 }, obj2: { value: 2 }, result: { value: 2 } },
{ obj1: { value: 1 }, obj2: { value: 1.0000001 }, result: { value: 1.0000001 } },
{ obj1: { value: 1 }, obj2: { value: '2' }, result: { value: '2' } },
{ obj1: { value: 1 }, obj2: { value: undefined }, result: { value: 1 } },
{ obj1: { value: 1 }, obj2: { value: null }, result: { value: null } },
{ obj1: { value: 0 }, obj2: { value: null }, result: { value: null } },
{ obj1: { value: false }, obj2: { value: 1 }, result: { value: 1 } },
{ obj1: { value: false }, obj2: { value: true }, result: { value: true } },
{ obj1: { value: false }, obj2: { value: '1' }, result: { value: '1' } },
{ obj1: { value: false }, obj2: { value: false }, result: { value: false } },
{ obj1: { value: false }, obj2: { value: 1 }, result: { value: 1 } },
{ obj1: { value: false }, obj2: { value: true }, result: { value: true } },
{ obj1: { value: false }, obj2: { value: '1' }, result: { value: '1' } },
{ obj1: { value: '1' }, obj2: { value: '2' }, result: { value: '2' } },
{ obj1: { value: undefined }, obj2: { value: 2 }, result: { value: 2 } },
{ obj1: { value: undefined }, obj2: { value: null }, result: { value: null } },
{ obj1: { value: null }, obj2: { value: null }, result: { value: null } },
{ obj1: {}, obj2: { value: 2 }, result: { value: 2 } },
{ obj1: { value: {} }, obj2: { value: 1 }, result: { value: 1 } },
{ obj1: { value: [] }, obj2: { value: 0 }, result: { value: 0 } },
{ obj1: { value: [] }, obj2: { value: [1] }, result: { value: [1] } },
{ obj1: { value: [] }, obj2: { value: undefined }, result: { value: [] } },
{ obj1: { value: { name: 1 } }, obj2: [], result: { value: { name: 1 } } },
{ obj1: { value: [] }, obj2: { value: { name: 1 } }, result: { value: { name: 1 } } },
{ obj1: [1, 2, 3], obj2: [3, 5, 6], result: [3, 5, 6] },

{
obj1: {
value: 1, value2: 'string', value3: null, value4: undefined, nested: { nestedValue1: 1, nestedValue2: 2 },
},
obj2: {
value2: undefined, value3: 1, value4: null, value5: 'new', nested: { nestedValue2: 4, nestedValue3: 'value' },
},
result: {
value: 1,
value2: 'string',
value3: 1,
value4: null,
value5: 'new',
nested: { nestedValue1: 1, nestedValue2: 4, nestedValue3: 'value' },
},
},
{
obj1: {
value: 0,
value2: '',
value3: [],
value4: {},
nested: {
nestedValue1: undefined,
nestedValue2: {
key1: null, key2: undefined, key3: {}, key4: [],
},
nestedValue3: 'value',
},
},
obj2: {
value: 'value',
value3: { name: 1 },
value4: [1, 2, 3],
value5: null,
nested: {
nestedValue1: { key1: 0, key2: undefined, key3: null },
nestedValue2: {
key1: 'value', key3: { name: 1 }, key5: null, key6: 1,
},
nestedValue4: 1.2,
},
},
result: {
value: 'value',
value2: '',
value3: { name: 1 },
value4: [1, 2, 3],
value5: null,
nested: {
nestedValue1: { key1: 0, key2: undefined, key3: null },
nestedValue2: {
key1: 'value', key2: undefined, key3: { name: 1 }, key4: [], key5: null, key6: 1,
},
nestedValue3: 'value',
nestedValue4: 1.2,
},
},
},
];

describe('deepMerge', () => {
test.each(deepMergeTests)('%j', ({ obj1, obj2, result }) => {
expect(
JSON.parse(
JSON.stringify(
deepMerge(obj1, obj2),
),
),
).toStrictEqual(
JSON.parse(
JSON.stringify(result),
),
);
});
});
15 changes: 15 additions & 0 deletions redisinsight/api/src/common/utils/merge.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
isObjectLike, isUndefined, mergeWith, isArray,
} from 'lodash';

export const deepMerge = (target: object, source: object) => mergeWith(target, source, (targetValue, sourceValue) => {
if (isUndefined(sourceValue)) {
return targetValue;
}

if (isObjectLike(sourceValue) && !isArray(sourceValue) && !isArray(targetValue)) {
return deepMerge(targetValue, sourceValue);
}

return sourceValue;
});
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('DatabaseConnectionService', () => {
});

describe('createClient', () => {
it('should create client for standalone datbaase', async () => {
it('should create client for standalone database', async () => {
expect(await service.createClient(mockCommonClientMetadata)).toEqual(mockIORedisClient);
});
it('should throw Unauthorized error in case of NOAUTH', async () => {
Expand Down
6 changes: 3 additions & 3 deletions redisinsight/api/src/modules/database/database.analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class DatabaseAnalytics extends TelemetryBaseService {

sendInstanceAddedEvent(
instance: Database,
additionalInfo: RedisDatabaseInfoResponse,
additionalInfo?: RedisDatabaseInfoResponse,
): void {
try {
const modulesSummary = getRedisModulesSummary(instance.modules);
Expand All @@ -45,8 +45,8 @@ export class DatabaseAnalytics extends TelemetryBaseService {
version: additionalInfo?.version,
numberOfKeys: additionalInfo?.totalKeys,
numberOfKeysRange: getRangeForNumber(additionalInfo?.totalKeys, TOTAL_KEYS_BREAKPOINTS),
totalMemory: additionalInfo.usedMemory,
numberedDatabases: additionalInfo.databases,
totalMemory: additionalInfo?.usedMemory,
numberedDatabases: additionalInfo?.databases,
numberOfModules: instance.modules?.length || 0,
timeout: instance.timeout / 1_000, // milliseconds to seconds
databaseIndex: instance.db || 0,
Expand Down
65 changes: 45 additions & 20 deletions redisinsight/api/src/modules/database/database.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Param,
Patch,
Post,
Put,
UseInterceptors,
UsePipes,
ValidationPipe,
Expand All @@ -26,9 +25,10 @@ import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dt
import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response';
import { ClientMetadataParam } from 'src/common/decorators';
import { ClientMetadata } from 'src/common/models';
import { ModifyDatabaseDto } from 'src/modules/database/dto/modify.database.dto';
import { ExportDatabasesDto } from 'src/modules/database/dto/export.databases.dto';
import { ExportDatabase } from 'src/modules/database/models/export-database';
import { DatabaseResponse } from 'src/modules/database/dto/database.response';
import { classToClass } from 'src/utils';

@ApiTags('Database')
@Controller('databases')
Expand Down Expand Up @@ -64,14 +64,14 @@ export class DatabaseController {
{
status: 200,
description: 'Database instance',
type: Database,
type: DatabaseResponse,
},
],
})
async get(
@Param('id') id: string,
): Promise<Database> {
return await this.service.get(id);
): Promise<DatabaseResponse> {
return classToClass(DatabaseResponse, await this.service.get(id));
}

@UseInterceptors(ClassSerializerInterceptor)
Expand All @@ -84,7 +84,7 @@ export class DatabaseController {
{
status: 201,
description: 'Created database instance',
type: Database,
type: DatabaseResponse,
},
],
})
Expand All @@ -97,21 +97,21 @@ export class DatabaseController {
)
async create(
@Body() dto: CreateDatabaseDto,
): Promise<Database> {
return await this.service.create(dto, true);
): Promise<DatabaseResponse> {
return classToClass(DatabaseResponse, await this.service.create(dto, true));
}

@UseInterceptors(ClassSerializerInterceptor)
@UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))
@Put(':id')
@Patch(':id')
@ApiEndpoint({
description: 'Update database instance by id',
statusCode: 200,
responses: [
{
status: 200,
description: 'Updated database instance\' response',
type: Database,
type: DatabaseResponse,
},
],
})
Expand All @@ -125,21 +125,21 @@ export class DatabaseController {
async update(
@Param('id') id: string,
@Body() database: UpdateDatabaseDto,
): Promise<Database> {
return await this.service.update(id, database, true);
): Promise<DatabaseResponse> {
return classToClass(DatabaseResponse, await this.service.update(id, database, true));
}

@UseInterceptors(ClassSerializerInterceptor)
@UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT))
@Patch(':id')
@Post('clone/:id')
@ApiEndpoint({
description: 'Update database instance by id',
statusCode: 200,
responses: [
{
status: 200,
description: 'Updated database instance\' response',
type: Database,
type: DatabaseResponse,
},
],
})
Expand All @@ -150,11 +150,11 @@ export class DatabaseController {
forbidNonWhitelisted: true,
}),
)
async modify(
async clone(
@Param('id') id: string,
@Body() database: ModifyDatabaseDto,
): Promise<Database> {
return await this.service.update(id, database, true);
@Body() database: UpdateDatabaseDto,
): Promise<DatabaseResponse> {
return classToClass(DatabaseResponse, await this.service.clone(id, database));
}

@UseInterceptors(ClassSerializerInterceptor)
Expand All @@ -175,9 +175,34 @@ export class DatabaseController {
}),
)
async testConnection(
@Body() database: CreateDatabaseDto,
@Body() dto: CreateDatabaseDto,
): Promise<void> {
return await this.service.testConnection(dto);
}

@UseInterceptors(ClassSerializerInterceptor)
@Post('/test/:id')
@ApiEndpoint({
description: 'Test connection',
statusCode: 200,
responses: [
{
status: 200,
},
],
})
@UsePipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
)
async testExistConnection(
@Param('id') id: string,
@Body() dto: UpdateDatabaseDto,
): Promise<void> {
return await this.service.testConnection(database);
return this.service.testConnection(dto, id);
}

@Delete('/:id')
Expand Down
Loading