Skip to content

Commit 91635df

Browse files
#RI-4661 - remove duplicates
1 parent ffb6820 commit 91635df

File tree

4 files changed

+177
-1
lines changed

4 files changed

+177
-1
lines changed

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/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: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { when } from 'jest-when';
2+
import { pick } from 'lodash';
3+
import { InternalServerErrorException } from '@nestjs/common';
4+
import { Test, TestingModule } from '@nestjs/testing';
5+
import { getRepositoryToken } from '@nestjs/typeorm';
6+
import { Repository } from 'typeorm';
7+
import { EventEmitter2 } from '@nestjs/event-emitter';
8+
import {
9+
mockEncryptionService,
10+
mockRepository,
11+
mockDatabaseRecommendationEntity,
12+
mockRecommendationName,
13+
mockClientMetadata,
14+
mockDatabaseRecommendation,
15+
MockType,
16+
} from 'src/__mocks__';
17+
import { EncryptionService } from 'src/modules/encryption/encryption.service';
18+
import { LocalDatabaseRecommendationRepository }
19+
from 'src/modules/database-recommendation/repositories/local.database.recommendation.repository';
20+
import { DatabaseRecommendationEntity }
21+
from 'src/modules/database-recommendation/entities/database-recommendation.entity';
22+
import ERROR_MESSAGES from 'src/constants/error-messages';
23+
24+
describe('LocalDatabaseRecommendationRepository', () => {
25+
let service: LocalDatabaseRecommendationRepository;
26+
let encryptionService: MockType<EncryptionService>;
27+
let repository: MockType<Repository<DatabaseRecommendationEntity>>;
28+
29+
beforeEach(async () => {
30+
jest.clearAllMocks();
31+
32+
const module: TestingModule = await Test.createTestingModule({
33+
providers: [
34+
LocalDatabaseRecommendationRepository,
35+
{
36+
provide: getRepositoryToken(DatabaseRecommendationEntity),
37+
useFactory: mockRepository,
38+
},
39+
{
40+
provide: EncryptionService,
41+
useFactory: mockEncryptionService,
42+
},
43+
EventEmitter2,
44+
],
45+
}).compile();
46+
47+
repository = await module.get(getRepositoryToken(DatabaseRecommendationEntity));
48+
encryptionService = await module.get(EncryptionService);
49+
service = module.get(LocalDatabaseRecommendationRepository);
50+
51+
repository.findOneBy.mockResolvedValue(mockDatabaseRecommendationEntity);
52+
repository.save.mockResolvedValue(mockDatabaseRecommendationEntity);
53+
repository.update.mockResolvedValue(mockDatabaseRecommendationEntity);
54+
55+
when(encryptionService.encrypt)
56+
.calledWith(JSON.stringify(mockDatabaseRecommendation.params))
57+
.mockReturnValue({
58+
encryption: mockDatabaseRecommendationEntity.encryption,
59+
data: mockDatabaseRecommendationEntity.params,
60+
});
61+
when(encryptionService.decrypt)
62+
.calledWith(mockDatabaseRecommendationEntity.params, jasmine.anything())
63+
.mockReturnValue(JSON.stringify(mockDatabaseRecommendation.params));
64+
});
65+
66+
describe('isExist', () => {
67+
it('should return true when receive database entity', async () => {
68+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(true);
69+
});
70+
71+
it('should return false when no database received', async () => {
72+
repository.findOneBy.mockResolvedValueOnce(null);
73+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(false);
74+
});
75+
76+
it('should return false when received error', async () => {
77+
repository.findOneBy.mockRejectedValueOnce(new Error());
78+
expect(await service.isExist(mockClientMetadata, mockRecommendationName)).toEqual(false);
79+
});
80+
});
81+
82+
describe('create', () => {
83+
it('should create recommendation', async () => {
84+
service.cleanupDatabaseRecommendations = jest
85+
.fn()
86+
.mockResolvedValue(undefined);
87+
88+
const result = await service.create(mockDatabaseRecommendation);
89+
90+
expect(result).toEqual(mockDatabaseRecommendation);
91+
expect(service.cleanupDatabaseRecommendations).toHaveBeenCalledWith(
92+
mockDatabaseRecommendation.databaseId
93+
);
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+
117+
describe('cleanupDatabaseRecommendations', () => {
118+
it('Should not return anything on cleanup', async () => {
119+
repository.createQueryBuilder().getRawMany.mockResolvedValueOnce([
120+
{ id: mockDatabaseRecommendationEntity.id },
121+
{ id: mockDatabaseRecommendationEntity.id },
122+
]);
123+
124+
expect(await service.cleanupDatabaseRecommendations(mockDatabaseRecommendationEntity.databaseId)).toEqual(undefined);
125+
});
126+
});
127+
});

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ export class LocalDatabaseRecommendationRepository extends DatabaseRecommendatio
4949
const model = await this.repository.save(
5050
await this.modelEncryptor.encryptEntity(plainToClass(DatabaseRecommendationEntity, entity)),
5151
);
52+
// cleanup recommendations and ignore error if any
53+
try {
54+
await this.cleanupDatabaseRecommendations(entity.databaseId);
55+
} catch (e) {
56+
this.logger.error('Error when trying to cleanup recommendations after insert', e);
57+
}
5258

5359
const recommendation = classToClass(
5460
DatabaseRecommendation,
@@ -57,6 +63,7 @@ export class LocalDatabaseRecommendationRepository extends DatabaseRecommendatio
5763
this.eventEmitter.emit(RecommendationEvents.NewRecommendation, [recommendation]);
5864

5965
return recommendation;
66+
6067
}
6168

6269
/**
@@ -226,4 +233,25 @@ export class LocalDatabaseRecommendationRepository extends DatabaseRecommendatio
226233
throw new InternalServerErrorException(error.message);
227234
}
228235
}
236+
237+
/**
238+
* Remove duplicates for particular database
239+
* @param databaseId
240+
*/
241+
async cleanupDatabaseRecommendations(databaseId: string): Promise<void> {
242+
// todo: investigate why delete with sub-query doesn't works
243+
const idsDuplicates = (await this.repository
244+
.createQueryBuilder()
245+
.where({ databaseId })
246+
.select('id')
247+
.groupBy('name')
248+
.having('COUNT(name) > 1')
249+
.getRawMany()).map((item) => item.id);
250+
251+
await this.repository
252+
.createQueryBuilder()
253+
.delete()
254+
.whereInIds(idsDuplicates)
255+
.execute();
256+
}
229257
}

0 commit comments

Comments
 (0)