Skip to content

Commit d0f4913

Browse files
authored
Merge pull request #1615 from RedisInsight/feature/RI-3977_recommendation_voting
Feature/ri 3977 recommendation voting
2 parents 84570dc + 0999ce7 commit d0f4913

File tree

29 files changed

+735
-10
lines changed

29 files changed

+735
-10
lines changed

redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import {
22
Body,
3-
Controller, Get, Param, Post, UseInterceptors, UsePipes, ValidationPipe,
3+
Controller, Get, Param, Post, Patch, UseInterceptors, UsePipes, ValidationPipe,
44
} from '@nestjs/common';
55
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
66
import { ApiTags } from '@nestjs/swagger';
77
import { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service';
88
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
99
import { BrowserSerializeInterceptor } from 'src/common/interceptors';
1010
import { ApiQueryRedisStringEncoding, ClientMetadataParam } from 'src/common/decorators';
11-
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
11+
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
1212
import { ClientMetadata } from 'src/common/models';
1313

1414
@UseInterceptors(BrowserSerializeInterceptor)
@@ -72,4 +72,30 @@ export class DatabaseAnalysisController {
7272
): Promise<ShortDatabaseAnalysis[]> {
7373
return this.service.list(databaseId);
7474
}
75+
76+
@Patch(':id')
77+
@ApiEndpoint({
78+
description: 'Update database instance by id',
79+
statusCode: 200,
80+
responses: [
81+
{
82+
status: 200,
83+
description: 'Updated database instance\' response',
84+
type: DatabaseAnalysis,
85+
},
86+
],
87+
})
88+
@UsePipes(
89+
new ValidationPipe({
90+
transform: true,
91+
whitelist: true,
92+
forbidNonWhitelisted: true,
93+
}),
94+
)
95+
async modify(
96+
@Param('id') id: string,
97+
@Body() dto: RecommendationVoteDto,
98+
): Promise<DatabaseAnalysis> {
99+
return await this.service.vote(id, dto);
100+
}
75101
}

redisinsight/api/src/modules/database-analysis/database-analysis.service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/databa
77
import { plainToClass } from 'class-transformer';
88
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
99
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
10-
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
10+
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
1111
import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner';
1212
import { DatabaseConnectionService } from 'src/modules/database/database-connection.service';
1313
import { ClientMetadata } from 'src/common/models';
@@ -112,4 +112,13 @@ export class DatabaseAnalysisService {
112112
async list(databaseId: string): Promise<ShortDatabaseAnalysis[]> {
113113
return this.databaseAnalysisProvider.list(databaseId);
114114
}
115+
116+
/**
117+
* Set user vote for recommendation
118+
* @param id
119+
* @param recommendation
120+
*/
121+
async vote(id: string, recommendation: RecommendationVoteDto): Promise<DatabaseAnalysis> {
122+
return this.databaseAnalysisProvider.recommendationVote(id, recommendation);
123+
}
115124
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './create-database-analysis.dto';
2+
export * from './recommendation-vote.dto';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsString } from 'class-validator';
3+
4+
export class RecommendationVoteDto {
5+
@ApiProperty({
6+
description: 'Recommendation name',
7+
type: String,
8+
})
9+
@IsString()
10+
name: string;
11+
12+
@ApiProperty({
13+
description: 'User vote',
14+
type: String,
15+
})
16+
@IsString()
17+
vote: string;
18+
}

redisinsight/api/src/modules/database-analysis/models/recommendation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@ export class Recommendation {
1616
})
1717
@Expose()
1818
params?: any;
19+
20+
@ApiPropertyOptional({
21+
description: 'User vote',
22+
example: 'useful',
23+
})
24+
@Expose()
25+
vote?: string;
1926
}

redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Repository } from 'typeorm';
1313
import { getRepositoryToken } from '@nestjs/typeorm';
1414
import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider';
1515
import { DatabaseAnalysis } from 'src/modules/database-analysis/models';
16-
import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto';
16+
import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto';
1717
import { RedisDataType } from 'src/modules/browser/dto';
1818
import { plainToClass } from 'class-transformer';
1919
import { ScanFilter } from 'src/modules/database-analysis/models/scan-filter';
@@ -150,6 +150,16 @@ const mockDatabaseAnalysis = {
150150
recommendations: [{ name: 'luaScript' }],
151151
} as DatabaseAnalysis;
152152

153+
const mockDatabaseAnalysisWithVote = {
154+
...mockDatabaseAnalysis,
155+
recommendations: [{ name: 'luaScript', vote: 'useful' }],
156+
} as DatabaseAnalysis;
157+
158+
const mockRecommendationVoteDto: RecommendationVoteDto = {
159+
name: 'luaScript',
160+
vote: 'useful',
161+
};
162+
153163
describe('DatabaseAnalysisProvider', () => {
154164
let service: DatabaseAnalysisProvider;
155165
let repository: MockType<Repository<DatabaseAnalysis>>;
@@ -254,4 +264,27 @@ describe('DatabaseAnalysisProvider', () => {
254264
);
255265
});
256266
});
267+
268+
describe('recommendationVote', () => {
269+
it('should return updated database analysis', async () => {
270+
repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity);
271+
repository.update.mockReturnValueOnce(true);
272+
await encryptionService.encrypt.mockReturnValue(mockEncryptResult);
273+
274+
expect(await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto))
275+
.toEqual(mockDatabaseAnalysisWithVote);
276+
});
277+
278+
it('should throw an error', async () => {
279+
repository.findOneBy.mockReturnValueOnce(null);
280+
281+
try {
282+
await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto);
283+
fail();
284+
} catch (e) {
285+
expect(e).toBeInstanceOf(NotFoundException);
286+
expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);
287+
}
288+
});
289+
});
257290
});

redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
66
import { EncryptionService } from 'src/modules/encryption/encryption.service';
77
import { plainToClass } from 'class-transformer';
88
import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models';
9+
import { RecommendationVoteDto } from 'src/modules/database-analysis/dto';
910
import { classToClass } from 'src/utils';
1011
import config from 'src/utils/config';
1112
import ERROR_MESSAGES from 'src/constants/error-messages';
@@ -70,6 +71,31 @@ export class DatabaseAnalysisProvider {
7071
return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true));
7172
}
7273

74+
/**
75+
* Fetches entity, decrypt, update and return updated DatabaseAnalysis model
76+
* @param id
77+
* @param dto
78+
*/
79+
async recommendationVote(id: string, dto: RecommendationVoteDto): Promise<DatabaseAnalysis> {
80+
this.logger.log('Updating database analysis with recommendation vote');
81+
const { name, vote } = dto;
82+
const oldDatabaseAnalysis = await this.repository.findOneBy({ id });
83+
84+
if (!oldDatabaseAnalysis) {
85+
this.logger.error(`Database analysis with id:${id} was not Found`);
86+
throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND);
87+
}
88+
89+
const entity = classToClass(DatabaseAnalysis, await this.decryptEntity(oldDatabaseAnalysis, true));
90+
91+
entity.recommendations = entity.recommendations.map((recommendation) => (
92+
recommendation.name === name ? { ...recommendation, vote } : recommendation));
93+
94+
await this.repository.update(id, await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, entity)));
95+
96+
return entity;
97+
}
98+
7399
/**
74100
* Return list of database analysis with several fields only
75101
* @param databaseId
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
expect,
3+
describe,
4+
deps,
5+
before,
6+
getMainCheckFn,
7+
Joi,
8+
generateInvalidDataTestCases,
9+
validateInvalidDataTestCase,
10+
} from '../deps';
11+
import { analysisSchema } from './constants';
12+
const { localDb, request, server, constants, rte } = deps;
13+
14+
const endpoint = (
15+
instanceId = constants.TEST_INSTANCE_ID,
16+
id = constants.TEST_DATABASE_ANALYSIS_ID_1,
17+
) =>
18+
request(server).patch(`/${constants.API.DATABASES}/${instanceId}/analysis/${id}`);
19+
20+
// input data schema
21+
const dataSchema = Joi.object({
22+
name: Joi.string(),
23+
vote: Joi.string(),
24+
}).strict();
25+
26+
const validInputData = {
27+
name: constants.getRandomString(),
28+
vote: constants.getRandomString(),
29+
};
30+
31+
const responseSchema = analysisSchema;
32+
const mainCheckFn = getMainCheckFn(endpoint);
33+
let repository;
34+
35+
describe('PATCH /databases/:instanceId/analysis/:id', () => {
36+
before(async () => await localDb.generateNDatabaseAnalysis({
37+
databaseId: constants.TEST_INSTANCE_ID,
38+
id: constants.TEST_DATABASE_ANALYSIS_ID_1,
39+
createdAt: constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1,
40+
}, 1, true),
41+
);
42+
43+
describe('Validation', () => {
44+
generateInvalidDataTestCases(dataSchema, validInputData).map(
45+
validateInvalidDataTestCase(endpoint, dataSchema),
46+
);
47+
});
48+
49+
describe('recommendations', () => {
50+
describe('recommendation vote', () => {
51+
[
52+
{
53+
name: 'Should add vote for RTS recommendation',
54+
data: {
55+
name: 'luaScript',
56+
vote: 'useful',
57+
},
58+
statusCode: 200,
59+
responseSchema,
60+
checkFn: async ({ body }) => {
61+
expect(body.recommendations).to.include.deep.members([
62+
constants.TEST_LUA_SCRIPT_VOTE_RECOMMENDATION
63+
]);
64+
},
65+
},
66+
].map(mainCheckFn);
67+
});
68+
});
69+
});

redisinsight/api/test/api/database-analysis/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Joi } from '../../helpers/test';
22

33
export const typedRecommendationSchema = Joi.object({
44
name: Joi.string().required(),
5+
vote: Joi.string(),
6+
params: Joi.any(),
57
});
68

79
export const typedTotalSchema = Joi.object({

redisinsight/api/test/helpers/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,5 +513,10 @@ export const constants = {
513513
TEST_REDISEARCH_RECOMMENDATION: {
514514
name: RECOMMENDATION_NAMES.REDIS_SEARCH,
515515
},
516+
517+
TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: {
518+
name: RECOMMENDATION_NAMES.LUA_SCRIPT,
519+
vote: 'useful',
520+
},
516521
// etc...
517522
}

0 commit comments

Comments
 (0)