Skip to content

Commit 90c4754

Browse files
authored
Merge pull request #2221 from RedisInsight/be/bugfix/RI-4661_cloud_recommendation
#RI-4661 - remove duplicates
2 parents 803c459 + 3e72ff6 commit 90c4754

File tree

8 files changed

+186
-12
lines changed

8 files changed

+186
-12
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class DatabaseRecommendationUnique1687435940110 implements MigrationInterface {
4+
name = 'DatabaseRecommendationUnique1687435940110'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`DROP INDEX "IDX_2487bdd9dbde3fdf65bcb96fc5"`);
8+
await queryRunner.query(`DROP INDEX "IDX_d6107e5e16648b038c511f3b00"`);
9+
await queryRunner.query(`CREATE TABLE "temporary_database_recommendations" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "name" varchar NOT NULL, "read" boolean NOT NULL DEFAULT (0), "disabled" boolean NOT NULL DEFAULT (0), "vote" varchar, "hide" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "params" blob, "encryption" varchar, CONSTRAINT "UQ_b772d2856a42685ce4227321251" UNIQUE ("databaseId", "name"), CONSTRAINT "FK_2487bdd9dbde3fdf65bcb96fc52" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
10+
await queryRunner.query(`INSERT OR IGNORE INTO "temporary_database_recommendations"("id", "databaseId", "name", "read", "disabled", "vote", "hide", "createdAt", "params", "encryption") SELECT "id", "databaseId", "name", "read", "disabled", "vote", "hide", "createdAt", "params", "encryption" FROM "database_recommendations"`);
11+
await queryRunner.query(`DROP TABLE "database_recommendations"`);
12+
await queryRunner.query(`ALTER TABLE "temporary_database_recommendations" RENAME TO "database_recommendations"`);
13+
await queryRunner.query(`CREATE INDEX "IDX_2487bdd9dbde3fdf65bcb96fc5" ON "database_recommendations" ("databaseId") `);
14+
await queryRunner.query(`CREATE INDEX "IDX_d6107e5e16648b038c511f3b00" ON "database_recommendations" ("createdAt") `);
15+
}
16+
17+
public async down(queryRunner: QueryRunner): Promise<void> {
18+
await queryRunner.query(`DROP INDEX "IDX_d6107e5e16648b038c511f3b00"`);
19+
await queryRunner.query(`DROP INDEX "IDX_2487bdd9dbde3fdf65bcb96fc5"`);
20+
await queryRunner.query(`ALTER TABLE "database_recommendations" RENAME TO "temporary_database_recommendations"`);
21+
await queryRunner.query(`CREATE TABLE "database_recommendations" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "name" varchar NOT NULL, "read" boolean NOT NULL DEFAULT (0), "disabled" boolean NOT NULL DEFAULT (0), "vote" varchar, "hide" boolean NOT NULL DEFAULT (0), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "params" blob, "encryption" varchar, CONSTRAINT "FK_2487bdd9dbde3fdf65bcb96fc52" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`);
22+
await queryRunner.query(`INSERT INTO "database_recommendations"("id", "databaseId", "name", "read", "disabled", "vote", "hide", "createdAt", "params", "encryption") SELECT "id", "databaseId", "name", "read", "disabled", "vote", "hide", "createdAt", "params", "encryption" FROM "temporary_database_recommendations"`);
23+
await queryRunner.query(`DROP TABLE "temporary_database_recommendations"`);
24+
await queryRunner.query(`CREATE INDEX "IDX_d6107e5e16648b038c511f3b00" ON "database_recommendations" ("createdAt") `);
25+
await queryRunner.query(`CREATE INDEX "IDX_2487bdd9dbde3fdf65bcb96fc5" ON "database_recommendations" ("databaseId") `);
26+
}
27+
28+
}

redisinsight/api/migration/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { databaseRecommendations1681900503586 } from './1681900503586-database-r
3333
import { databaseRecommendationParams1683006064293 } from './1683006064293-database-recommendation-params';
3434
import { Feature1684931530343 } from './1684931530343-feature';
3535
import { DatabaseRedisServer1686719451753 } from './1686719451753-database-redis-server';
36+
import { DatabaseRecommendationUnique1687435940110 } from './1687435940110-database-recommendation-unique';
3637
import { CloudDatabaseDetails1687166457712 } from './1687166457712-cloud-database-details';
3738

3839
export default [
@@ -71,5 +72,6 @@ export default [
7172
databaseRecommendationParams1683006064293,
7273
Feature1684931530343,
7374
DatabaseRedisServer1686719451753,
75+
DatabaseRecommendationUnique1687435940110,
7476
CloudDatabaseDetails1687166457712,
7577
];

redisinsight/api/src/__mocks__/database-recommendation.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
11
import { DatabaseRecommendation } from 'src/modules/database-recommendation/models';
2+
import { DatabaseRecommendationEntity }
3+
from 'src/modules/database-recommendation/entities/database-recommendation.entity';
4+
import { EncryptionStrategy } from 'src/modules/encryption/models';
25
import { mockDatabaseId } from 'src/__mocks__/databases';
36

47
export const mockDatabaseRecommendationId = 'databaseRecommendationID';
58

9+
export const mockRecommendationName = 'string';
10+
11+
export const mockDatabaseRecommendationParamsEncrypted = 'recommendation.params_ENCRYPTED';
12+
13+
export const mockDatabaseRecommendationParamsPlain = [];
14+
615
export const mockDatabaseRecommendation = Object.assign(new DatabaseRecommendation(), {
716
id: mockDatabaseRecommendationId,
8-
name: 'string',
17+
name: mockRecommendationName,
918
databaseId: mockDatabaseId,
1019
read: false,
1120
disabled: false,
1221
hide: false,
22+
params: mockDatabaseRecommendationParamsPlain,
1323
});
1424

25+
export const mockDatabaseRecommendationEntity = new DatabaseRecommendationEntity(
26+
{
27+
...mockDatabaseRecommendation,
28+
params: mockDatabaseRecommendationParamsEncrypted,
29+
encryption: EncryptionStrategy.KEYTAR,
30+
},
31+
);
32+
1533
export const mockDatabaseRecommendationService = () => ({
1634
create: jest.fn(),
1735
list: jest.fn(),

redisinsight/api/src/modules/database-recommendation/entities/database-recommendation.entity.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {
2-
Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, JoinColumn, Index,
2+
Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, JoinColumn, Index, Unique,
33
} from 'typeorm';
44
import { Expose } from 'class-transformer';
55
import { DataAsJsonString } from 'src/common/decorators';
66
import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
77

88
@Entity('database_recommendations')
9+
@Unique(['databaseId', 'name'])
910
export class DatabaseRecommendationEntity {
1011
@PrimaryGeneratedColumn('uuid')
1112
@Expose()

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export class DatabaseRecommendation {
5050
type: Boolean,
5151
example: false,
5252
})
53+
@Expose()
54+
@IsOptional()
55+
@IsBoolean()
5356
disabled?: boolean;
5457

5558
@ApiPropertyOptional({
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { when } from 'jest-when';
2+
import { InternalServerErrorException } from '@nestjs/common';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
import { getRepositoryToken } from '@nestjs/typeorm';
5+
import { Repository } from 'typeorm';
6+
import { EventEmitter2 } from '@nestjs/event-emitter';
7+
import {
8+
mockEncryptionService,
9+
mockRepository,
10+
mockDatabaseRecommendationEntity,
11+
mockRecommendationName,
12+
mockClientMetadata,
13+
mockDatabaseRecommendation,
14+
MockType,
15+
} from 'src/__mocks__';
16+
import { EncryptionService } from 'src/modules/encryption/encryption.service';
17+
import { LocalDatabaseRecommendationRepository }
18+
from 'src/modules/database-recommendation/repositories/local.database.recommendation.repository';
19+
import { DatabaseRecommendationEntity }
20+
from 'src/modules/database-recommendation/entities/database-recommendation.entity';
21+
import ERROR_MESSAGES from 'src/constants/error-messages';
22+
23+
describe('LocalDatabaseRecommendationRepository', () => {
24+
let service: LocalDatabaseRecommendationRepository;
25+
let encryptionService: MockType<EncryptionService>;
26+
let repository: MockType<Repository<DatabaseRecommendationEntity>>;
27+
28+
beforeEach(async () => {
29+
jest.clearAllMocks();
30+
31+
const module: TestingModule = await Test.createTestingModule({
32+
providers: [
33+
LocalDatabaseRecommendationRepository,
34+
{
35+
provide: getRepositoryToken(DatabaseRecommendationEntity),
36+
useFactory: mockRepository,
37+
},
38+
{
39+
provide: EncryptionService,
40+
useFactory: mockEncryptionService,
41+
},
42+
EventEmitter2,
43+
],
44+
}).compile();
45+
46+
repository = await module.get(getRepositoryToken(DatabaseRecommendationEntity));
47+
encryptionService = await module.get(EncryptionService);
48+
service = module.get(LocalDatabaseRecommendationRepository);
49+
50+
repository.findOneBy.mockResolvedValue(mockDatabaseRecommendationEntity);
51+
repository.save.mockResolvedValue(mockDatabaseRecommendationEntity);
52+
repository.update.mockResolvedValue(mockDatabaseRecommendationEntity);
53+
54+
when(encryptionService.encrypt)
55+
.calledWith(JSON.stringify(mockDatabaseRecommendation.params))
56+
.mockReturnValue({
57+
encryption: mockDatabaseRecommendationEntity.encryption,
58+
data: mockDatabaseRecommendationEntity.params,
59+
});
60+
when(encryptionService.decrypt)
61+
.calledWith(mockDatabaseRecommendationEntity.params, jasmine.anything())
62+
.mockReturnValue(JSON.stringify(mockDatabaseRecommendation.params));
63+
});
64+
65+
describe('isExist', () => {
66+
it('should return true when receive database entity', async () => {
67+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(true);
68+
});
69+
70+
it('should return false when no database received', async () => {
71+
repository.findOneBy.mockResolvedValueOnce(null);
72+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(false);
73+
});
74+
75+
it('should return false when received error', async () => {
76+
repository.findOneBy.mockRejectedValueOnce(new Error());
77+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(false);
78+
});
79+
});
80+
81+
describe('create', () => {
82+
it('should create recommendation', async () => {
83+
const result = await service.create(mockDatabaseRecommendation);
84+
85+
expect(result).toEqual(mockDatabaseRecommendation);
86+
});
87+
88+
it('should not create recommendation', async () => {
89+
repository.save.mockRejectedValueOnce(new Error());
90+
91+
const result = await service.create(mockDatabaseRecommendation);
92+
93+
expect(result).toEqual(null);
94+
});
95+
});
96+
97+
describe('delete', () => {
98+
it('should delete recommendation by id', async () => {
99+
repository.delete.mockResolvedValueOnce({ affected: 1 });
100+
101+
expect(await service.delete(mockClientMetadata, 'id')).toEqual(undefined);
102+
});
103+
104+
it('should return InternalServerErrorException when recommendation does not found', async () => {
105+
repository.delete.mockResolvedValueOnce({ affected: 0 });
106+
107+
try {
108+
await service.delete(mockClientMetadata, 'id');
109+
fail();
110+
} catch (e) {
111+
expect(e).toBeInstanceOf(InternalServerErrorException);
112+
expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_RECOMMENDATION_NOT_FOUND);
113+
}
114+
});
115+
});
116+
});

redisinsight/api/src/modules/database-recommendation/repositories/local.database.recommendation.repository.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,23 @@ export class LocalDatabaseRecommendationRepository extends DatabaseRecommendatio
4646
async create(entity: DatabaseRecommendation): Promise<DatabaseRecommendation> {
4747
this.logger.log('Creating database recommendation');
4848

49-
const model = await this.repository.save(
50-
await this.modelEncryptor.encryptEntity(plainToClass(DatabaseRecommendationEntity, entity)),
51-
);
49+
try {
50+
const model = await this.repository.save(
51+
await this.modelEncryptor.encryptEntity(plainToClass(DatabaseRecommendationEntity, entity)),
52+
);
5253

53-
const recommendation = classToClass(
54-
DatabaseRecommendation,
55-
await this.modelEncryptor.decryptEntity(model, true),
56-
);
57-
this.eventEmitter.emit(RecommendationEvents.NewRecommendation, [recommendation]);
54+
const recommendation = classToClass(
55+
DatabaseRecommendation,
56+
await this.modelEncryptor.decryptEntity(model, true),
57+
);
58+
this.eventEmitter.emit(RecommendationEvents.NewRecommendation, [recommendation]);
5859

59-
return recommendation;
60+
return recommendation;
61+
} catch (err) {
62+
this.logger.error('Failed to create database recommendation', err);
63+
64+
return null;
65+
}
6066
}
6167

6268
/**

redisinsight/api/test/helpers/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ export const constants = {
496496

497497
TEST_RECOMMENDATION_NAME_1: RECOMMENDATION_NAMES.BIG_SETS,
498498
TEST_RECOMMENDATION_NAME_2: RECOMMENDATION_NAMES.BIG_STRINGS,
499-
TEST_RECOMMENDATION_NAME_3: RECOMMENDATION_NAMES.BIG_STRINGS,
499+
TEST_RECOMMENDATION_NAME_3: RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,
500500

501501
TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION: {
502502
name: RECOMMENDATION_NAMES.LUA_SCRIPT,

0 commit comments

Comments
 (0)