Skip to content

Commit 2707c17

Browse files
authored
RI-3619: Add database key name formatting (#4418)
* implement UI * extend database dto with keyNameFormat * disable Tree View button on HEX key format * CONFIG_DATABASES_DATABASE_EDITED_BY_USER telemetry * CONFIG_DATABASES_DATABASE_ADDED
1 parent 000e939 commit 2707c17

File tree

25 files changed

+585
-121
lines changed

25 files changed

+585
-121
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class KeyNameFormatAdded1742303245547 implements MigrationInterface {
4+
name = 'KeyNameFormatAdded1742303245547'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "temporary_database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, "new" boolean, "ssh" boolean, "timeout" integer, "compressor" varchar NOT NULL DEFAULT ('NONE'), "version" varchar, "createdAt" datetime DEFAULT (datetime('now')), "forceStandalone" boolean, "isPreSetup" boolean, "keyNameFormat" varchar DEFAULT ('Unicode'), CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`);
8+
await queryRunner.query(`INSERT INTO "temporary_database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new", "ssh", "timeout", "compressor", "version", "createdAt", "forceStandalone", "isPreSetup") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new", "ssh", "timeout", "compressor", "version", "createdAt", "forceStandalone", "isPreSetup" FROM "database_instance"`);
9+
await queryRunner.query(`DROP TABLE "database_instance"`);
10+
await queryRunner.query(`ALTER TABLE "temporary_database_instance" RENAME TO "database_instance"`);
11+
}
12+
13+
public async down(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(`ALTER TABLE "database_instance" RENAME TO "temporary_database_instance"`);
15+
await queryRunner.query(`CREATE TABLE "database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, "new" boolean, "ssh" boolean, "timeout" integer, "compressor" varchar NOT NULL DEFAULT ('NONE'), "version" varchar, "createdAt" datetime DEFAULT (datetime('now')), "forceStandalone" boolean, "isPreSetup" boolean, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`);
16+
await queryRunner.query(`INSERT INTO "database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new", "ssh", "timeout", "compressor", "version", "createdAt", "forceStandalone", "isPreSetup") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername", "new", "ssh", "timeout", "compressor", "version", "createdAt", "forceStandalone", "isPreSetup" FROM "temporary_database_instance"`);
17+
await queryRunner.query(`DROP TABLE "temporary_database_instance"`);
18+
}
19+
20+
}

redisinsight/api/migration/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { databaseDbIndex1634219846022 } from './1634219846022-database-db-index'
1212
import { encryption1634557312500 } from './1634557312500-encryption';
1313
import { commandExecution1641795882696 } from './1641795882696-command-execution';
1414
import { pluginState1641805606399 } from './1641805606399-plugin-state';
15-
import { sni1650278664000 } from "./1650278664000-sni";
15+
import { sni1650278664000 } from './1650278664000-sni';
1616
import { notification1655821010349 } from './1655821010349-notification';
1717
import { notificationCategory1659687030433 } from './1659687030433-notification-category';
1818
import { workbenchMode1660664717573 } from './1660664717573-workbench-mode';
@@ -49,6 +49,7 @@ import { DbSettings1737362130798 } from './1737362130798-db-settings';
4949
import { DatabaseForceStandalone1738829743482 } from './1738829743482-database-forceStandalone';
5050
import { RdiOptionalAuth1740579711635 } from './1740579711635-rdi-optional-auth';
5151
import { PreSetupDatabases1741786803681 } from './1741786803681-pre-setup-databases';
52+
import { KeyNameFormatAdded1742303245547 } from './1742303245547-key-name-format';
5253

5354
export default [
5455
initialMigration1614164490968,
@@ -102,4 +103,5 @@ export default [
102103
DatabaseForceStandalone1738829743482,
103104
RdiOptionalAuth1740579711635,
104105
PreSetupDatabases1741786803681,
106+
KeyNameFormatAdded1742303245547,
105107
];

redisinsight/api/src/modules/database/database.analytics.spec.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import { TelemetryEvents } from 'src/constants';
1111
import { DEFAULT_SUMMARY as DEFAULT_REDIS_MODULES_SUMMARY } from 'src/utils/redis-modules-summary';
1212
import { DatabaseAnalytics } from 'src/modules/database/database.analytics';
13-
import { HostingProvider } from 'src/modules/database/entities/database.entity';
13+
import { Encoding, HostingProvider } from 'src/modules/database/entities/database.entity';
1414

1515
describe('DatabaseAnalytics', () => {
1616
let service: DatabaseAnalytics;
@@ -64,6 +64,7 @@ describe('DatabaseAnalytics', () => {
6464
forceStandalone: 'false',
6565
useDecompression: mockDatabaseWithTlsAuth.compressor,
6666
serverName: 'valkey',
67+
keyNameFormat: Encoding.UNICODE,
6768
...DEFAULT_REDIS_MODULES_SUMMARY,
6869
},
6970
);
@@ -98,6 +99,7 @@ describe('DatabaseAnalytics', () => {
9899
forceStandalone: 'false',
99100
useDecompression: mockDatabaseWithTlsAuth.compressor,
100101
serverName: 'valkey',
102+
keyNameFormat: Encoding.UNICODE,
101103
...DEFAULT_REDIS_MODULES_SUMMARY,
102104
},
103105
);
@@ -137,6 +139,7 @@ describe('DatabaseAnalytics', () => {
137139
databaseIndex: 0,
138140
forceStandalone: 'false',
139141
useDecompression: mockDatabaseWithTlsAuth.compressor,
142+
keyNameFormat: Encoding.UNICODE,
140143
...DEFAULT_REDIS_MODULES_SUMMARY,
141144
RediSearch: {
142145
loaded: true,
@@ -183,6 +186,7 @@ describe('DatabaseAnalytics', () => {
183186
databaseIndex: 2,
184187
forceStandalone: 'false',
185188
useDecompression: mockDatabaseWithTlsAuth.compressor,
189+
keyNameFormat: Encoding.UNICODE,
186190
...DEFAULT_REDIS_MODULES_SUMMARY,
187191
RediSearch: {
188192
loaded: true,
@@ -193,6 +197,25 @@ describe('DatabaseAnalytics', () => {
193197
},
194198
);
195199
});
200+
201+
it('should emit event with keyNameFormat', () => {
202+
service.sendInstanceAddedEvent(
203+
mockSessionMetadata,
204+
{
205+
...mockDatabaseWithTlsAuth,
206+
keyNameFormat: Encoding.HEX,
207+
},
208+
mockRedisGeneralInfo,
209+
);
210+
211+
expect(sendEventSpy).toHaveBeenCalledWith(
212+
mockSessionMetadata,
213+
TelemetryEvents.RedisInstanceAdded,
214+
expect.objectContaining({
215+
keyNameFormat: Encoding.HEX,
216+
}),
217+
);
218+
});
196219
});
197220

198221
describe('sendInstanceEditedEvent', () => {
@@ -223,6 +246,7 @@ describe('DatabaseAnalytics', () => {
223246
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
224247
useDecompression: mockDatabaseWithTlsAuth.compressor,
225248
forceStandalone: 'false',
249+
keyNameFormat: Encoding.UNICODE,
226250
previousValues: {
227251
connectionType: prev.connectionType,
228252
provider: prev.provider,
@@ -232,6 +256,7 @@ describe('DatabaseAnalytics', () => {
232256
useSNI: 'enabled',
233257
useSSH: 'disabled',
234258
forceStandalone: 'false',
259+
keyNameFormat: Encoding.UNICODE,
235260
},
236261
},
237262
);
@@ -262,6 +287,7 @@ describe('DatabaseAnalytics', () => {
262287
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
263288
useDecompression: mockDatabaseWithTlsAuth.compressor,
264289
forceStandalone: 'false',
290+
keyNameFormat: Encoding.UNICODE,
265291
previousValues: {
266292
connectionType: prev.connectionType,
267293
provider: prev.provider,
@@ -271,6 +297,7 @@ describe('DatabaseAnalytics', () => {
271297
verifyTLSCertificate: 'disabled',
272298
useTLSAuthClients: 'disabled',
273299
forceStandalone: 'false',
300+
keyNameFormat: Encoding.UNICODE,
274301
},
275302
},
276303
);
@@ -301,6 +328,7 @@ describe('DatabaseAnalytics', () => {
301328
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
302329
useDecompression: mockDatabaseWithTlsAuth.compressor,
303330
forceStandalone: 'true',
331+
keyNameFormat: Encoding.UNICODE,
304332
previousValues: {
305333
connectionType: prev.connectionType,
306334
provider: prev.provider,
@@ -310,6 +338,7 @@ describe('DatabaseAnalytics', () => {
310338
verifyTLSCertificate: 'disabled',
311339
useTLSAuthClients: 'disabled',
312340
forceStandalone: 'false',
341+
keyNameFormat: Encoding.UNICODE,
313342
},
314343
},
315344
);
@@ -340,6 +369,7 @@ describe('DatabaseAnalytics', () => {
340369
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
341370
useDecompression: mockDatabaseWithTlsAuth.compressor,
342371
forceStandalone: 'false',
372+
keyNameFormat: Encoding.UNICODE,
343373
previousValues: {
344374
connectionType: prev.connectionType,
345375
provider: prev.provider,
@@ -349,6 +379,7 @@ describe('DatabaseAnalytics', () => {
349379
verifyTLSCertificate: 'disabled',
350380
useTLSAuthClients: 'disabled',
351381
forceStandalone: 'false',
382+
keyNameFormat: Encoding.UNICODE,
352383
},
353384
},
354385
);
@@ -380,6 +411,7 @@ describe('DatabaseAnalytics', () => {
380411
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
381412
useDecompression: mockDatabaseWithTlsAuth.compressor,
382413
forceStandalone: 'true',
414+
keyNameFormat: Encoding.UNICODE,
383415
previousValues: {
384416
connectionType: prev.connectionType,
385417
provider: prev.provider,
@@ -389,6 +421,51 @@ describe('DatabaseAnalytics', () => {
389421
verifyTLSCertificate: 'disabled',
390422
useTLSAuthClients: 'disabled',
391423
forceStandalone: 'true',
424+
keyNameFormat: Encoding.UNICODE,
425+
},
426+
},
427+
);
428+
});
429+
430+
it('should emit event when keyNameFormat is changed', () => {
431+
const prev = mockDatabaseWithTlsAuth;
432+
const cur = {
433+
...mockDatabaseWithTlsAuth,
434+
provider: HostingProvider.RE_CLUSTER,
435+
tls: undefined,
436+
verifyServerCert: false,
437+
caCert: null,
438+
clientCert: null,
439+
keyNameFormat: Encoding.HEX,
440+
};
441+
service.sendInstanceEditedEvent(mockSessionMetadata, prev, cur);
442+
443+
expect(sendEventSpy).toHaveBeenCalledWith(
444+
mockSessionMetadata,
445+
TelemetryEvents.RedisInstanceEditedByUser,
446+
{
447+
databaseId: cur.id,
448+
connectionType: cur.connectionType,
449+
provider: HostingProvider.RE_CLUSTER,
450+
useTLS: 'disabled',
451+
verifyTLSCertificate: 'disabled',
452+
useTLSAuthClients: 'disabled',
453+
useSNI: 'enabled',
454+
useSSH: 'disabled',
455+
timeout: mockDatabaseWithTlsAuth.timeout / 1_000, // milliseconds to seconds
456+
useDecompression: mockDatabaseWithTlsAuth.compressor,
457+
forceStandalone: 'false',
458+
keyNameFormat: Encoding.HEX,
459+
previousValues: {
460+
connectionType: prev.connectionType,
461+
provider: prev.provider,
462+
useTLS: 'enabled',
463+
verifyTLSCertificate: 'enabled',
464+
useTLSAuthClients: 'enabled',
465+
useSNI: 'enabled',
466+
useSSH: 'disabled',
467+
forceStandalone: 'false',
468+
keyNameFormat: Encoding.UNICODE,
392469
},
393470
},
394471
);

redisinsight/api/src/modules/database/database.analytics.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export class DatabaseAnalytics extends TelemetryBaseService {
5757
useDecompression: instance.compressor || null,
5858
serverName: additionalInfo?.server?.server_name || null,
5959
forceStandalone: instance?.forceStandalone ? 'true' : 'false',
60+
keyNameFormat: instance?.keyNameFormat || null,
6061
...modulesSummary,
6162
},
6263
);
@@ -94,6 +95,7 @@ export class DatabaseAnalytics extends TelemetryBaseService {
9495
timeout: cur?.timeout / 1_000, // milliseconds to seconds
9596
useDecompression: cur?.compressor || null,
9697
forceStandalone: cur?.forceStandalone ? 'true' : 'false',
98+
keyNameFormat: cur?.keyNameFormat || null,
9799
previousValues: {
98100
connectionType: prev.connectionType,
99101
provider: prev.provider,
@@ -107,6 +109,7 @@ export class DatabaseAnalytics extends TelemetryBaseService {
107109
? 'enabled'
108110
: 'disabled',
109111
forceStandalone: prev?.forceStandalone ? 'true' : 'false',
112+
keyNameFormat: prev?.keyNameFormat || null,
110113
},
111114
},
112115
);

redisinsight/api/src/modules/database/database.service.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ describe('DatabaseService', () => {
217217
new: false,
218218
version: '7.0',
219219
ssh: true,
220+
keyNameFormat: 'Unicode',
220221
sshOptions: {
221222
id: 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ssh-id',
222223
host: 'ssh.host.test',
@@ -263,6 +264,7 @@ describe('DatabaseService', () => {
263264
new: false,
264265
version: '7.0',
265266
ssh: true,
267+
keyNameFormat: 'Unicode',
266268
sshOptions: {
267269
host: 'ssh.host.test',
268270
port: 22,

redisinsight/api/src/modules/database/dto/create.database.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { CloudDatabaseDetails } from 'src/modules/cloud/database/models/cloud-da
2525
export class CreateDatabaseDto extends PickType(Database, [
2626
'host', 'port', 'name', 'db', 'username', 'password', 'timeout', 'nameFromProvider', 'provider',
2727
'tls', 'tlsServername', 'verifyServerCert', 'sentinelMaster', 'ssh', 'compressor', 'cloudDetails',
28-
'forceStandalone',
28+
'forceStandalone', 'keyNameFormat',
2929
] as const) {
3030
@ApiPropertyOptional({
3131
description: 'CA Certificate',

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {
2-
Column, CreateDateColumn, Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn,
2+
Column,
3+
CreateDateColumn,
4+
Entity,
5+
ManyToOne,
6+
OneToOne,
7+
PrimaryGeneratedColumn,
38
} from 'typeorm';
49
import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity';
510
import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity';
@@ -49,6 +54,11 @@ export enum Compressor {
4954
PHPGZCompress = 'PHPGZCompress',
5055
}
5156

57+
export enum Encoding {
58+
UNICODE = 'Unicode',
59+
HEX = 'HEX',
60+
}
61+
5262
@Entity('database_instance')
5363
export class DatabaseEntity {
5464
@Expose()
@@ -259,4 +269,8 @@ export class DatabaseEntity {
259269
@Expose()
260270
@Column({ nullable: true })
261271
isPreSetup: boolean;
272+
273+
@Expose()
274+
@Column({ nullable: true, default: Encoding.UNICODE })
275+
keyNameFormat: string;
262276
}

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2-
import { Expose, Type } from 'class-transformer';
2+
import { Expose, Transform, Type } from 'class-transformer';
33
import config from 'src/utils/config';
44
import { CaCertificate } from 'src/modules/certificate/models/ca-certificate';
55
import { ClientCertificate } from 'src/modules/certificate/models/client-certificate';
6-
import { Compressor, ConnectionType, HostingProvider } from 'src/modules/database/entities/database.entity';
76
import {
8-
IsBoolean, IsEnum,
7+
Compressor,
8+
ConnectionType,
9+
Encoding,
10+
HostingProvider,
11+
} from 'src/modules/database/entities/database.entity';
12+
import {
13+
IsBoolean,
14+
IsEnum,
915
IsInt,
1016
IsNotEmpty,
1117
IsNotEmptyObject,
@@ -291,6 +297,20 @@ export class Database {
291297
@IsOptional()
292298
compressor?: Compressor = Compressor.NONE;
293299

300+
@ApiPropertyOptional({
301+
description: 'Key name format',
302+
default: Encoding.UNICODE,
303+
enum: Encoding,
304+
})
305+
@Expose()
306+
@IsEnum(Encoding, {
307+
message: `Key name format must be a valid enum value. Valid values: ${Object.values(
308+
Encoding,
309+
)}.`,
310+
})
311+
@IsOptional()
312+
keyNameFormat?: Encoding = Encoding.UNICODE;
313+
294314
@ApiPropertyOptional({
295315
description: 'The version your Redis server',
296316
type: String,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const databaseSchema = Joi.object().keys({
4444
verifyServerCert: Joi.boolean().allow(null),
4545
caCert: caCertSchema.strict(true).allow(null),
4646
clientCert: clientCertSchema.strict(true).allow(null),
47+
keyNameFormat: Joi.string().valid('Unicode', 'HEX').allow(null),
4748
sentinelMaster: Joi.object({
4849
name: Joi.string().required(),
4950
username: Joi.string().allow(null),

0 commit comments

Comments
 (0)