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: 'useful',
})
@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: 'useful' }],
} as DatabaseAnalysis;

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

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: 'useful',
},
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: 'useful',
},
// etc...
}
3 changes: 3 additions & 0 deletions redisinsight/ui/src/assets/img/icons/dislike.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions redisinsight/ui/src/assets/img/icons/double_like.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions redisinsight/ui/src/assets/img/icons/like.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@
{
"id": "2",
"type": "span",
"value": "was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the "
"value": " was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the "
},
{
"id": "3",
Expand Down
1 change: 1 addition & 0 deletions redisinsight/ui/src/constants/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const EXTERNAL_LINKS = {
githubIssues: 'https://github.com/RedisInsight/RedisInsight/issues',
releaseNotes: 'https://github.com/RedisInsight/RedisInsight/releases',
userSurvey: 'https://www.surveymonkey.com/r/redisinsight',
recommendationFeedback: 'https://github.com/RedisInsight/RedisInsight/issues/new/choose',
}
5 changes: 5 additions & 0 deletions redisinsight/ui/src/constants/recommendations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum Vote {
DoubleLike = 'very useful',
Like = 'useful',
Dislike = 'not useful'
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AnalysisDataView from './analysis-data-view'
import ExpirationGroupsView from './analysis-ttl-view'
import EmptyAnalysisMessage from './empty-analysis-message'
import Header from './header'
import RecommendationVoting from './recommendation-voting'
import SummaryPerData from './summary-per-data'
import TableLoader from './table-loader'
import TopKeys from './top-keys'
Expand All @@ -12,6 +13,7 @@ export {
ExpirationGroupsView,
EmptyAnalysisMessage,
Header,
RecommendationVoting,
SummaryPerData,
TableLoader,
TopKeys,
Expand Down
Loading