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
@@ -1,14 +1,14 @@
import {
Body,
Controller, Get, Param, Post, UseInterceptors, UsePipes, ValidationPipe,
Controller, Get, Param, Post, Patch, UseInterceptors, UsePipes, ValidationPipe,
} from '@nestjs/common';
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
import { ApiTags } from '@nestjs/swagger';
import { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service';
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
import { BrowserSerializeInterceptor } from 'src/common/interceptors';
import { ApiQueryRedisStringEncoding, ClientMetadataParam } from 'src/common/decorators';
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
import { ClientMetadata } from 'src/common/models';

@UseInterceptors(BrowserSerializeInterceptor)
Expand Down Expand Up @@ -72,4 +72,30 @@ export class DatabaseAnalysisController {
): Promise<ShortDatabaseAnalysis[]> {
return this.service.list(databaseId);
}

@Patch(':id')
@ApiEndpoint({
description: 'Update database instance by id',
statusCode: 200,
responses: [
{
status: 200,
description: 'Updated database instance\' response',
type: DatabaseAnalysis,
},
],
})
@UsePipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
)
async modify(
@Param('id') id: string,
@Body() dto: RecommendationVoteDto,
): Promise<DatabaseAnalysis> {
return await this.service.vote(id, dto);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/databa
import { plainToClass } from 'class-transformer';
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
import { ClientMetadata } from 'src/common/models';
Expand Down Expand Up @@ -112,4 +112,13 @@ export class DatabaseAnalysisService {
async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {
return this.databaseAnalysisProvider.list(databaseId);
}

/**
* Set user vote for recommendation
* @param id
* @param recommendation
*/
async vote(id: string, recommendation: RecommendationVoteDto): Promise<DatabaseAnalysis> {
return this.databaseAnalysisProvider.recommendationVote(id, recommendation);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './create-database-analysis.dto';
export * from './recommendation-vote.dto';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';

export class RecommendationVoteDto {
@ApiProperty({
description: 'Recommendation name',
type: String,
})
@IsString()
name: string;

@ApiProperty({
description: 'User vote',
type: String,
})
@IsString()
vote: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ export class Recommendation {
})
@Expose()
params?: any;

@ApiPropertyOptional({
description: 'User vote',
example: 'Amazing',
})
@Expose()
vote?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
import { DatabaseAnalysis } from 'src/modules/database-analysis/models';
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
import { RedisDataType } from 'src/modules/browser/dto';
import { plainToClass } from 'class-transformer';
import { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';
Expand Down Expand Up @@ -150,6 +150,16 @@ const mockDatabaseAnalysis = {
recommendations: [{ name: 'luaScript' }],
} as DatabaseAnalysis;

const mockDatabaseAnalysisWithVote = {
...mockDatabaseAnalysis,
recommendations: [{ name: 'luaScript', vote: 'amazing' }],
} as DatabaseAnalysis;

const mockRecommendationVoteDto: RecommendationVoteDto = {
name: 'luaScript',
vote: 'amazing',
};

describe('DatabaseAnalysisProvider', () => {
let service: DatabaseAnalysisProvider;
let repository: MockType<Repository<DatabaseAnalysis>>;
Expand Down Expand Up @@ -254,4 +264,27 @@ describe('DatabaseAnalysisProvider', () => {
);
});
});

describe('recommendationVote', () => {
it('should return updated database analysis', async () => {
repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity);
repository.update.mockReturnValueOnce(true);
await encryptionService.encrypt.mockReturnValue(mockEncryptResult);

expect(await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto))
.toEqual(mockDatabaseAnalysisWithVote);
});

it('should throw an error', async () => {
repository.findOneBy.mockReturnValueOnce(null);

try {
await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto);
fail();
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
import { EncryptionService } from 'src/modules/encryption/encryption.service';
import { plainToClass } from 'class-transformer';
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
import { RecommendationVoteDto } from 'src/modules/database-analysis/dto';
import { classToClass } from 'src/utils';
import config from 'src/utils/config';
import ERROR_MESSAGES from 'src/constants/error-messages';
Expand Down Expand Up @@ -70,6 +71,31 @@ export class DatabaseAnalysisProvider {
return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true));
}

/**
* Fetches entity, decrypt, update and return updated DatabaseAnalysis model
* @param id
* @param dto
*/
async recommendationVote(id: string, dto: RecommendationVoteDto): Promise<DatabaseAnalysis> {
this.logger.log('Updating database analysis with recommendation vote');
const { name, vote } = dto;
const oldDatabaseAnalysis = await this.repository.findOneBy({ id });

if (!oldDatabaseAnalysis) {
this.logger.error(`Database analysis with id:${id} was not Found`);
throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);
}

const entity = classToClass(DatabaseAnalysis, await this.decryptEntity(oldDatabaseAnalysis, true));

entity.recommendations = entity.recommendations.map((recommendation) => (
recommendation.name === name ? { ...recommendation, vote } : recommendation));

await this.repository.update(id, await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, entity)));

return entity;
}

/**
* Return list of database analysis with several fields only
* @param databaseId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
expect,
describe,
deps,
before,
getMainCheckFn,
Joi,
generateInvalidDataTestCases,
validateInvalidDataTestCase,
} from '../deps';
import { analysisSchema } from './constants';
const { localDb, request, server, constants, rte } = deps;

const endpoint = (
instanceId = constants.TEST_INSTANCE_ID,
id = constants.TEST_DATABASE_ANALYSIS_ID_1,
) =>
request(server).patch(`/${constants.API.DATABASES}/${instanceId}/analysis/${id}`);

// input data schema
const dataSchema = Joi.object({
name: Joi.string(),
vote: Joi.string(),
}).strict();

const validInputData = {
name: constants.getRandomString(),
vote: constants.getRandomString(),
};

const responseSchema = analysisSchema;
const mainCheckFn = getMainCheckFn(endpoint);
let repository;

describe('PATCH /databases/:instanceId/analysis/:id', () => {
before(async () => await localDb.generateNDatabaseAnalysis({
databaseId: constants.TEST_INSTANCE_ID,
id: constants.TEST_DATABASE_ANALYSIS_ID_1,
createdAt: constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1,
}, 1, true),
);

describe('Validation', () => {
generateInvalidDataTestCases(dataSchema, validInputData).map(
validateInvalidDataTestCase(endpoint, dataSchema),
);
});

describe('recommendations', () => {
describe('recommendation vote', () => {
[
{
name: 'Should add vote for RTS recommendation',
data: {
name: 'luaScript',
vote: 'amazing',
},
statusCode: 200,
responseSchema,
checkFn: async ({ body }) => {
expect(body.recommendations).to.include.deep.members([
constants.TEST_LUA_SCRIPT_VOTE_RECOMMENDATION
]);
},
},
].map(mainCheckFn);
});
});
});
2 changes: 2 additions & 0 deletions redisinsight/api/test/api/database-analysis/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Joi } from '../../helpers/test';

export const typedRecommendationSchema = Joi.object({
name: Joi.string().required(),
vote: Joi.string(),
params: Joi.any(),
});

export const typedTotalSchema = Joi.object({
Expand Down
5 changes: 5 additions & 0 deletions redisinsight/api/test/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,5 +513,10 @@ export const constants = {
TEST_REDISEARCH_RECOMMENDATION: {
name: RECOMMENDATION_NAMES.REDIS_SEARCH,
},

TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: {
name: RECOMMENDATION_NAMES.LUA_SCRIPT,
vote: 'amazing',
},
// etc...
}