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
20 changes: 20 additions & 0 deletions redisinsight/api/migration/1743606395647-encrypt-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class EncryptTags1743606395647 implements MigrationInterface {
name = 'EncryptTags1743606395647'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_tag" ("id" varchar PRIMARY KEY NOT NULL, "key" varchar NOT NULL, "value" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "encryption" varchar, CONSTRAINT "UQ_5d6110d4eee64a5a4529ea8fcdc" UNIQUE ("key", "value"))`);
await queryRunner.query(`INSERT INTO "temporary_tag"("id", "key", "value", "createdAt", "updatedAt") SELECT "id", "key", "value", "createdAt", "updatedAt" FROM "tag"`);
await queryRunner.query(`DROP TABLE "tag"`);
await queryRunner.query(`ALTER TABLE "temporary_tag" RENAME TO "tag"`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tag" RENAME TO "temporary_tag"`);
await queryRunner.query(`CREATE TABLE "tag" ("id" varchar PRIMARY KEY NOT NULL, "key" varchar NOT NULL, "value" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), CONSTRAINT "UQ_5d6110d4eee64a5a4529ea8fcdc" UNIQUE ("key", "value"))`);
await queryRunner.query(`INSERT INTO "tag"("id", "key", "value", "createdAt", "updatedAt") SELECT "id", "key", "value", "createdAt", "updatedAt" FROM "temporary_tag"`);
await queryRunner.query(`DROP TABLE "temporary_tag"`);
}

}
2 changes: 2 additions & 0 deletions redisinsight/api/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { DatabaseTags1741610039177 } from './1741610039177-database-tags';
import { PreSetupDatabases1741786803681 } from './1741786803681-pre-setup-databases';
import { KeyNameFormatAdded1742303245547 } from './1742303245547-key-name-format';
import { CascadeTags1743432519891 } from './1743432519891-cascade-tags';
import { EncryptTags1743606395647 } from './1743606395647-encrypt-tags';

export default [
initialMigration1614164490968,
Expand Down Expand Up @@ -108,4 +109,5 @@ export default [
PreSetupDatabases1741786803681,
KeyNameFormatAdded1742303245547,
CascadeTags1743432519891,
EncryptTags1743606395647,
];
3 changes: 3 additions & 0 deletions redisinsight/api/src/__mocks__/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const mockTagsRepository = jest.fn(
getByKeyValuePair: jest.fn().mockResolvedValue(mockTags[0]),
update: jest.fn(),
delete: jest.fn(),
encryptTagEntities: jest.fn().mockImplementation(async (tags) => tags),
decryptTagEntities: jest.fn().mockImplementation(async (tags) => tags),
getOrCreateByKeyValuePairs: jest.fn().mockImplementation(async (tags) => tags),
cleanupUnusedTags: jest.fn(),
}) as TagRepository,
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
mockDatabaseImportResponse,
mockSessionMetadata,
mockSshImportService,
mockTagsRepository,
MockType,
} from 'src/__mocks__';
import { DatabaseRepository } from 'src/modules/database/repositories/database.repository';
Expand All @@ -24,11 +25,13 @@ import {
} from 'src/modules/database-import/exceptions';
import { CertificateImportService } from 'src/modules/database-import/certificate-import.service';
import { SshImportService } from 'src/modules/database-import/ssh-import.service';
import { TagRepository } from '../tag/repository/tag.repository';

describe('DatabaseImportService', () => {
let service: DatabaseImportService;
let certificateImportService: MockType<CertificateImportService>;
let databaseRepository: MockType<DatabaseRepository>;
let tagRepository: MockType<TagRepository>;
let analytics: MockType<DatabaseImportAnalytics>;
let validatoSpy;

Expand Down Expand Up @@ -56,11 +59,16 @@ describe('DatabaseImportService', () => {
provide: DatabaseImportAnalytics,
useFactory: mockDatabaseImportAnalytics,
},
{
provide: TagRepository,
useFactory: mockTagsRepository,
}
],
}).compile();

service = await module.get(DatabaseImportService);
databaseRepository = await module.get(DatabaseRepository);
tagRepository = await module.get(TagRepository);
certificateImportService = await module.get(CertificateImportService);
analytics = await module.get(DatabaseImportAnalytics);
validatoSpy = jest.spyOn(service['validator'], 'validateOrReject');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DatabaseImportResult,
DatabaseImportStatus,
} from 'src/modules/database-import/dto/database-import.response';
import { TagRepository } from '../tag/repository/tag.repository';
import { ValidationError, Validator } from 'class-validator';
import { ImportDatabaseDto } from 'src/modules/database-import/dto/import.database.dto';
import { classToClass } from 'src/utils';
Expand Down Expand Up @@ -80,6 +81,7 @@ export class DatabaseImportService {
private readonly sshImportService: SshImportService,
private readonly databaseRepository: DatabaseRepository,
private readonly analytics: DatabaseImportAnalytics,
private readonly tagRepository: TagRepository,
) {}

/**
Expand Down Expand Up @@ -254,6 +256,10 @@ export class DatabaseImportService {
},
);

if (dto.tags) {
dto.tags = await this.tagRepository.getOrCreateByKeyValuePairs(dto.tags);
}

await this.validator.validateOrReject(dto, {
whitelist: true,
});
Expand Down
79 changes: 1 addition & 78 deletions redisinsight/api/src/modules/database/database.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { InternalServerErrorException, NotFoundException, ServiceUnavailableException, ConflictException } from '@nestjs/common';
import { InternalServerErrorException, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { omit, get, update } from 'lodash';
import { plainToClass } from 'class-transformer';

import { classToClass } from 'src/utils';
import {
Expand All @@ -15,8 +14,6 @@ import {
mockClientCertificate,
MockType,
mockRedisGeneralInfo,
mockTagsService,
mockDatabaseWithTags,
mockDatabaseWithTls,
mockDatabaseWithTlsAuth,
mockDatabaseWithSshPrivateKey,
Expand All @@ -35,8 +32,6 @@ import { Compressor } from 'src/modules/database/entities/database.entity';
import { RedisClientFactory } from 'src/modules/redis/redis.client.factory';
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
import { ExportDatabase } from './models/export-database';
import { TagService } from '../tag/tag.service';
import { Tag } from '../tag/models/tag';

const updateDatabaseTests = [
{ input: {}, expected: 0 },
Expand All @@ -61,7 +56,6 @@ describe('DatabaseService', () => {
let databaseFactory: MockType<DatabaseFactory>;
let redisClientFactory: MockType<RedisClientFactory>;
let analytics: MockType<DatabaseAnalytics>;
let tagService: TagService;
const exportSecurityFields: string[] = [
'password',
'clientCert.key',
Expand Down Expand Up @@ -102,10 +96,6 @@ describe('DatabaseService', () => {
provide: DatabaseAnalytics,
useFactory: mockDatabaseAnalytics,
},
{
provide: TagService,
useFactory: mockTagsService,
},
],
}).compile();

Expand All @@ -114,7 +104,6 @@ describe('DatabaseService', () => {
databaseFactory = await module.get(DatabaseFactory);
redisClientFactory = await module.get(RedisClientFactory);
analytics = await module.get(DatabaseAnalytics);
tagService = await module.get<TagService>(TagService);
});

describe('exists', () => {
Expand Down Expand Up @@ -182,33 +171,6 @@ describe('DatabaseService', () => {
new NotFoundException(),
);
});
it('should create new database with tags', async() => {
const dtoTags = [
{ key: 'env', value: 'prod' },
{ key: 'region', value: 'us-east' },
] as Tag[];

databaseFactory.createDatabaseModel.mockResolvedValueOnce(mockDatabaseWithTags);
databaseRepository.create.mockResolvedValueOnce(mockDatabaseWithTags);
jest
.spyOn(tagService, 'getOrCreateByKeyValuePairs')
.mockResolvedValue(mockDatabaseWithTags.tags);

const result = await service.create(mockSessionMetadata, {
...mockDatabaseWithTags,
tags: dtoTags,
});

expect(tagService.getOrCreateByKeyValuePairs).toHaveBeenCalledWith(
dtoTags,
);
expect(result).toEqual(mockDatabaseWithTags);
expect(databaseRepository.create).toHaveBeenCalledWith(
mockSessionMetadata,
expect.objectContaining({ tags: mockDatabaseWithTags.tags }),
false,
);
})
});

describe('update', () => {
Expand Down Expand Up @@ -315,37 +277,6 @@ describe('DatabaseService', () => {
);
});

it('should update existing database with tags', async () => {
const dtoTags = [
{ key: 'env', value: 'prod' },
{ key: 'region', value: 'us-east' },
] as Tag[];

databaseRepository.get.mockResolvedValueOnce(mockDatabaseWithTags);
databaseRepository.update.mockResolvedValueOnce(mockDatabaseWithTags);
jest
.spyOn(tagService, 'getOrCreateByKeyValuePairs')
.mockResolvedValue(mockDatabaseWithTags.tags);

const result = await service.update(
mockSessionMetadata,
mockDatabase.id,
plainToClass(UpdateDatabaseDto, {
tags: dtoTags,
}),
true,
);

expect(tagService.getOrCreateByKeyValuePairs).toHaveBeenCalledWith(dtoTags);
expect(result).toEqual(mockDatabaseWithTags);
expect(databaseRepository.update).toHaveBeenCalledWith(
mockSessionMetadata,
mockDatabase.id,
expect.objectContaining({ tags: mockDatabaseWithTags.tags }),
);
expect(tagService.cleanupUnusedTags).toHaveBeenCalled();
});

describe('test connection', () => {
test.each(updateDatabaseTests)(
'%j',
Expand Down Expand Up @@ -500,14 +431,6 @@ describe('DatabaseService', () => {
databaseRepository.delete.mockRejectedValueOnce(new NotFoundException());
await expect(service.delete(mockSessionMetadata, mockDatabase.id)).rejects.toThrow(InternalServerErrorException);
});
it('should call cleanupUnusedTags if database had tags', async () => {
databaseRepository.get.mockResolvedValueOnce({
...mockDatabase,
tags: [{ id: 'tagId' }],
});
await service.delete(mockSessionMetadata, mockDatabase.id);
expect(tagService.cleanupUnusedTags).toHaveBeenCalled();
});
});

describe('bulkDelete', () => {
Expand Down
20 changes: 1 addition & 19 deletions redisinsight/api/src/modules/database/database.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
Injectable, InternalServerErrorException, Logger, NotFoundException, ConflictException,
Injectable, InternalServerErrorException, Logger, NotFoundException,
} from '@nestjs/common';
import {
isEmpty, omit, reject, sum, omitBy, isUndefined,
Expand All @@ -25,7 +25,6 @@ import { CaCertificate } from 'src/modules/certificate/models/ca-certificate';
import { ClientCertificate } from 'src/modules/certificate/models/client-certificate';
import { IRedisConnectionOptions, RedisClientFactory } from 'src/modules/redis/redis.client.factory';
import { RedisClientStorage } from 'src/modules/redis/redis.client.storage';
import { TagService } from 'src/modules/tag/tag.service';

@Injectable()
export class DatabaseService {
Expand Down Expand Up @@ -69,7 +68,6 @@ export class DatabaseService {
private databaseFactory: DatabaseFactory,
private analytics: DatabaseAnalytics,
private eventEmitter: EventEmitter2,
private tagService: TagService,
) {}

static isConnectionAffected(dto: object) {
Expand Down Expand Up @@ -162,10 +160,6 @@ export class DatabaseService {
try {
this.logger.debug('Creating new database.', sessionMetadata);

if (dto.tags) {
dto.tags = await this.tagService.getOrCreateByKeyValuePairs(dto.tags);
}

const database = await this.repository.create(
sessionMetadata,
{
Expand Down Expand Up @@ -222,15 +216,10 @@ export class DatabaseService {
manualUpdate: boolean = true, // todo: remove manualUpdate flag logic
): Promise<Database> {
this.logger.debug(`Updating database: ${id}`, sessionMetadata);

const oldDatabase = await this.get(sessionMetadata, id, true);

let database: Database;
try {
if (dto.tags) {
dto.tags = await this.tagService.getOrCreateByKeyValuePairs(dto.tags);
}

database = await this.merge(oldDatabase, dto);

if (DatabaseService.isConnectionAffected(dto)) {
Expand All @@ -245,10 +234,6 @@ export class DatabaseService {

database = await this.repository.update(sessionMetadata, id, database);

if (dto.tags) {
await this.tagService.cleanupUnusedTags();
}

// todo: rethink
this.analytics.sendInstanceEditedEvent(
sessionMetadata,
Expand Down Expand Up @@ -346,9 +331,6 @@ export class DatabaseService {
const database = await this.get(sessionMetadata, id, true);
try {
await this.repository.delete(sessionMetadata, id);
if (database.tags) {
await this.tagService.cleanupUnusedTags();
}
// todo: rethink
await this.redisClientStorage.removeManyByMetadata({ databaseId: id });
this.logger.debug('Succeed to delete database instance.', sessionMetadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export class DatabaseEntity {
referencedColumnName: 'id',
},
})
@Type(() => TagEntity)
tags: TagEntity[];

@Expose()
Expand Down
Loading
Loading