Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,33 +105,33 @@ describe('CommandExecutionProvider', () => {

describe('create', () => {
it('should process new entity', async () => {
repository.save.mockReturnValueOnce(mockCommandExecutionEntity);
repository.save.mockReturnValueOnce([mockCommandExecutionEntity]);
encryptionService.encrypt.mockReturnValue(mockEncryptResult);

expect(await service.create(mockCommandExecutionPartial)).toEqual(new CommandExecution({
expect(await service.createMany([mockCommandExecutionPartial])).toEqual([new CommandExecution({
...mockCommandExecutionPartial,
id: mockCommandExecutionEntity.id,
createdAt: mockCommandExecutionEntity.createdAt,
}));
})]);
});
it('should return full result even if size limit exceeded', async () => {
repository.save.mockReturnValueOnce(mockCommandExecutionEntity);
repository.save.mockReturnValueOnce([mockCommandExecutionEntity]);
encryptionService.encrypt.mockReturnValue(mockEncryptResult);

const executionResult = [new CommandExecutionResult({
status: CommandExecutionStatus.Success,
response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`,
})];

expect(await service.create({
expect(await service.createMany([{
...mockCommandExecutionPartial,
result: executionResult,
})).toEqual(new CommandExecution({
}])).toEqual([new CommandExecution({
...mockCommandExecutionPartial,
id: mockCommandExecutionEntity.id,
createdAt: mockCommandExecutionEntity.createdAt,
result: executionResult,
}));
})]);

expect(encryptionService.encrypt).toHaveBeenLastCalledWith(JSON.stringify([
new CommandExecutionResult({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,40 +25,50 @@ export class CommandExecutionProvider {
) {}

/**
* Encrypt command execution and save entire entity
* Encrypt command executions and save entire entities
* Should always throw and error in case when unable to encrypt for some reason
* @param commandExecution
* @param commandExecutions
*/
async create(commandExecution: Partial<CommandExecution>): Promise<CommandExecution> {
const entity = plainToClass(CommandExecutionEntity, commandExecution);
async createMany(commandExecutions: Partial<CommandExecution>[]): Promise<CommandExecution[]> {
// todo: limit by 30 max to insert
let entities = await Promise.all(commandExecutions.map(async (commandExecution) => {
const entity = plainToClass(CommandExecutionEntity, commandExecution);

// Do not store command execution result that exceeded limitation
if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) {
entity.result = JSON.stringify([
{
status: CommandExecutionStatus.Success,
response: ERROR_MESSAGES.WORKBENCH_RESPONSE_TOO_BIG(),
},
]);
}

return this.encryptEntity(entity);
}));

entities = await this.commandExecutionRepository.save(entities);

// Do not store command execution result that exceeded limitation
if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) {
entity.result = JSON.stringify([
const response = await Promise.all(
entities.map((entity, idx) => classToClass(
CommandExecution,
{
status: CommandExecutionStatus.Success,
response: ERROR_MESSAGES.WORKBENCH_RESPONSE_TOO_BIG(),
...entity,
command: commandExecutions[idx].command,
mode: commandExecutions[idx].mode,
result: commandExecutions[idx].result,
nodeOptions: commandExecutions[idx].nodeOptions,
},
]);
}

const response = await classToClass(
CommandExecution,
{
...await this.commandExecutionRepository.save(await this.encryptEntity(entity)),
command: commandExecution.command,
mode: commandExecution.mode,
result: commandExecution.result,
nodeOptions: commandExecution.nodeOptions,
},
)),
);

// cleanup history and ignore error if any
try {
await this.cleanupDatabaseHistory(entity.databaseId);
await this.cleanupDatabaseHistory(entities[0].databaseId);
} catch (e) {
this.logger.error('Error when trying to cleanup history after insert', e);
}

return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class WorkbenchController {
},
],
})
@Post('/commands-execution')
@Post('/command-executions')
@UseInterceptors(ClassSerializerInterceptor)
@ApiRedisParams()
async sendCommands(
Expand Down
64 changes: 43 additions & 21 deletions redisinsight/api/src/modules/workbench/workbench.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CommandExecutionResult } from 'src/modules/workbench/models/command-exe
import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto';
import { BadRequestException, InternalServerErrorException } from '@nestjs/common';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto';
import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service';

const mockClientOptions: IFindRedisClientInstanceByOptions = {
Expand All @@ -31,6 +32,13 @@ const mockCreateCommandExecutionDto: CreateCommandExecutionDto = {
role: ClusterNodeRole.All,
mode: RunQueryMode.ASCII,
};
const mockCreateCommandExecutionsDto: CreateCommandExecutionsDto = {
commands: [
mockCreateCommandExecutionDto.command,
mockCreateCommandExecutionDto.command,
],
...mockCreateCommandExecutionDto,
};

const mockCommandExecutionResults: CommandExecutionResult[] = [
new CommandExecutionResult({
Expand All @@ -43,16 +51,20 @@ const mockCommandExecutionResults: CommandExecutionResult[] = [
},
}),
];
const mockCommandExecution: CommandExecution = new CommandExecution({
const mockCommandExecutionToRun: CommandExecution = new CommandExecution({
...mockCreateCommandExecutionDto,
databaseId: mockStandaloneDatabaseEntity.id,
});

const mockCommandExecution: CommandExecution = new CommandExecution({
...mockCommandExecutionToRun,
id: uuidv4(),
createdAt: new Date(),
result: mockCommandExecutionResults,
});

const mockCommandExecutionProvider = () => ({
create: jest.fn(),
createMany: jest.fn(),
getList: jest.fn(),
getOne: jest.fn(),
delete: jest.fn(),
Expand Down Expand Up @@ -91,13 +103,8 @@ describe('WorkbenchService', () => {

describe('createCommandExecution', () => {
it('should successfully execute command and save it', async () => {
workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults);
commandExecutionProvider.create.mockResolvedValueOnce(mockCommandExecution);

const result = await service.createCommandExecution(mockClientOptions, mockCreateCommandExecutionDto);

expect(result).toBeInstanceOf(CommandExecution);
expect(result).toEqual(mockCommandExecution);
expect(await service.createCommandExecution(mockClientOptions, mockCreateCommandExecutionDto))
.toEqual(mockCommandExecutionToRun);
});
it('should save result as unsupported command message', async () => {
workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults);
Expand All @@ -108,9 +115,7 @@ describe('WorkbenchService', () => {
mode: RunQueryMode.ASCII,
};

await service.createCommandExecution(mockClientOptions, dto);

expect(commandExecutionProvider.create).toHaveBeenCalledWith({
expect(await service.createCommandExecution(mockClientOptions, dto)).toEqual({
...dto,
databaseId: mockClientOptions.instanceId,
result: [
Expand All @@ -137,18 +142,35 @@ describe('WorkbenchService', () => {
expect(e).toBeInstanceOf(BadRequestException);
}
});
it('should throw an error from command execution provider (create)', async () => {
workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults);
commandExecutionProvider.create.mockRejectedValueOnce(new InternalServerErrorException('db error'));
});

const dto = {
...mockCommandExecutionResults,
command: 'scan 0',
mode: RunQueryMode.ASCII,
};
describe('createCommandExecutions', () => {
it('should successfully execute commands and save them', async () => {
workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(
[mockCommandExecutionResults, mockCommandExecutionResults],
);
commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]);

const result = await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto);

expect(result).toEqual([mockCommandExecution, mockCommandExecution]);
});
it('should throw an error when command execution failed', async () => {
workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(new BadRequestException('error'));

try {
await service.createCommandExecution(mockClientOptions, dto);
await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto);
fail();
} catch (e) {
expect(e).toBeInstanceOf(BadRequestException);
}
});
it('should throw an error from command execution provider (create)', async () => {
workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce([mockCommandExecutionResults]);
commandExecutionProvider.createMany.mockRejectedValueOnce(new InternalServerErrorException('db error'));

try {
await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto);
fail();
} catch (e) {
expect(e).toBeInstanceOf(InternalServerErrorException);
Expand Down
15 changes: 11 additions & 4 deletions redisinsight/api/src/modules/workbench/workbench.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { omit } from 'lodash';
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor';
import { CommandExecutionProvider } from 'src/modules/workbench/providers/command-execution.provider';
Expand Down Expand Up @@ -29,9 +30,9 @@ export class WorkbenchService {
async createCommandExecution(
clientOptions: IFindRedisClientInstanceByOptions,
dto: CreateCommandExecutionDto,
): Promise<CommandExecution> {
): Promise<Partial<CommandExecution>> {
const commandExecution: Partial<CommandExecution> = {
...dto,
...omit(dto, 'commands'),
databaseId: clientOptions.instanceId,
};

Expand All @@ -48,7 +49,7 @@ export class WorkbenchService {
commandExecution.result = await this.commandsExecutor.sendCommand(clientOptions, { ...dto, command });
}

return this.commandExecutionProvider.create(commandExecution);
return commandExecution;
}

/**
Expand All @@ -61,9 +62,15 @@ export class WorkbenchService {
clientOptions: IFindRedisClientInstanceByOptions,
dto: CreateCommandExecutionsDto,
): Promise<CommandExecution[]> {
return Promise.all(
// todo: rework to support pipeline
// prepare and execute commands
const commandExecutions = await Promise.all(
dto.commands.map(async (command) => await this.createCommandExecution(clientOptions, { ...dto, command })),
);

// save history
// todo: rework
return this.commandExecutionProvider.createMany(commandExecutions);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ $marginIcon: 12px;
}

.titleWrapper {
width: calc(100% - 350px);
width: calc(100% - 380px);

@media (min-width: $breakpoint-m) {
width: calc(100% - 420px);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { decode } from 'html-entities'
import { useParams } from 'react-router-dom'
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
import { chunk, reverse, without } from 'lodash'
import { chunk, without } from 'lodash'

import {
Nullable,
Expand Down Expand Up @@ -145,21 +145,18 @@ const WBViewWrapper = () => {
) => {
const { loading, batchSize } = state
const isNewCommand = () => !commandId
const [commands, ...rest] = chunk(splitMonacoValuePerLines(commandInit), batchSize > 1 ? batchSize : 1)
const commandsForExecuting = splitMonacoValuePerLines(removeMonacoComments(commandInit))
.map((command) => removeMonacoComments(decode(command).trim()))
const [commands, ...rest] = chunk(commandsForExecuting, batchSize > 1 ? batchSize : 1)
const multiCommands = rest.map((command) => getMultiCommands(command))
const commandLine = without(
commands.map((command) => removeMonacoComments(decode(command).trim())),
''
)

if (!commandLine.length || loading) {
if (!commands.length || loading) {
setMultiCommands(multiCommands)
return
}

isNewCommand() && scrollResults('start')

sendCommand(reverse(commandLine), multiCommands)
sendCommand(commands, multiCommands)
}

const sendCommand = (
Expand Down
Loading