diff --git a/redisinsight/api/migration/1733740794737-database-createdAt.ts b/redisinsight/api/migration/1733740794737-database-createdAt.ts new file mode 100644 index 0000000000..1b7d0c4517 --- /dev/null +++ b/redisinsight/api/migration/1733740794737-database-createdAt.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DatabaseCreatedAt1733740794737 implements MigrationInterface { + name = 'DatabaseCreatedAt1733740794737' + + public async up(queryRunner: QueryRunner): Promise { + 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')), CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + 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") 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" FROM "database_instance"`); + await queryRunner.query(`DROP TABLE "database_instance"`); + await queryRunner.query(`ALTER TABLE "temporary_database_instance" RENAME TO "database_instance"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "database_instance" RENAME TO "temporary_database_instance"`); + 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, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + 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") 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" FROM "temporary_database_instance"`); + await queryRunner.query(`DROP TABLE "temporary_database_instance"`); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 8d18dff288..0e8b7c5d29 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -44,6 +44,7 @@ import { Rdi1716370509836 } from './1716370509836-rdi'; import { AiHistory1718260230164 } from './1718260230164-ai-history'; import { CloudSession1729085495444 } from './1729085495444-cloud-session'; import { CommandExecution1726058563737 } from './1726058563737-command-execution'; +import { DatabaseCreatedAt1733740794737 } from './1733740794737-database-createdAt'; export default [ initialMigration1614164490968, @@ -92,4 +93,5 @@ export default [ AiHistory1718260230164, CloudSession1729085495444, CommandExecution1726058563737, + DatabaseCreatedAt1733740794737, ]; diff --git a/redisinsight/api/src/modules/database/database.service.spec.ts b/redisinsight/api/src/modules/database/database.service.spec.ts index 06c9b13c2c..8a39c12f36 100644 --- a/redisinsight/api/src/modules/database/database.service.spec.ts +++ b/redisinsight/api/src/modules/database/database.service.spec.ts @@ -530,6 +530,8 @@ describe('DatabaseService', () => { mockSessionMetadata, omit({ ...mockDatabase, username: 'new-name', timeout: 40_000 }, ['sshOptions.id']), ); + expect(databaseRepository.get) + .toHaveBeenCalledWith(mockSessionMetadata, mockDatabase.id, false, ['id', 'sshOptions.id', 'createdAt']); }); it('should create new database with merged ssh options', async () => { diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 6cdfddf594..778a4b0f97 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -294,7 +294,7 @@ export class DatabaseService { public async clone(sessionMetadata: SessionMetadata, id: string, dto: UpdateDatabaseDto): Promise { this.logger.log('Clone existing database'); const database = await this.merge( - await this.get(sessionMetadata, id, false, ['id', 'sshOptions.id']), + await this.get(sessionMetadata, id, false, ['id', 'sshOptions.id', 'createdAt']), dto, ); if (DatabaseService.isConnectionAffected(dto)) { diff --git a/redisinsight/api/src/modules/database/entities/database.entity.ts b/redisinsight/api/src/modules/database/entities/database.entity.ts index 48275d9ca8..4a161acb92 100644 --- a/redisinsight/api/src/modules/database/entities/database.entity.ts +++ b/redisinsight/api/src/modules/database/entities/database.entity.ts @@ -1,5 +1,5 @@ import { - Column, Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn, + Column, CreateDateColumn, Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity'; import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity'; @@ -172,6 +172,12 @@ export class DatabaseEntity { @Column({ type: 'datetime', nullable: true }) lastConnection: Date; + @CreateDateColumn({ + nullable: true, + }) + @Expose() + createdAt: Date; + @Expose() @Column({ nullable: true, @@ -231,4 +237,4 @@ export class DatabaseEntity { @Expose() @Column({ nullable: true }) version: string; -} \ No newline at end of file +} diff --git a/redisinsight/api/src/modules/database/models/database.ts b/redisinsight/api/src/modules/database/models/database.ts index e3697cafbc..4d583e5cbd 100644 --- a/redisinsight/api/src/modules/database/models/database.ts +++ b/redisinsight/api/src/modules/database/models/database.ts @@ -146,6 +146,13 @@ export class Database { @Expose() lastConnection: Date; + @ApiProperty({ + description: 'Date of creation', + type: Date, + }) + @Expose() + createdAt?: Date; + @ApiPropertyOptional({ description: 'Redis OSS Sentinel master group.', type: SentinelMaster, diff --git a/redisinsight/api/src/modules/database/repositories/local.database.repository.ts b/redisinsight/api/src/modules/database/repositories/local.database.repository.ts index 7769f80577..9d40447200 100644 --- a/redisinsight/api/src/modules/database/repositories/local.database.repository.ts +++ b/redisinsight/api/src/modules/database/repositories/local.database.repository.ts @@ -96,6 +96,7 @@ export class LocalDatabaseRepository extends DatabaseRepository { .select([ 'd.id', 'd.name', 'd.host', 'd.port', 'd.db', 'd.new', 'd.timeout', 'd.connectionType', 'd.modules', 'd.lastConnection', 'd.provider', 'd.version', 'cd', + 'd.createdAt', ]) .getMany(); @@ -262,4 +263,4 @@ export class LocalDatabaseRepository extends DatabaseRepository { } } } -} \ No newline at end of file +} diff --git a/redisinsight/api/test/api/database/GET-databases.test.ts b/redisinsight/api/test/api/database/GET-databases.test.ts index 7bb6bb1932..20f5520dbf 100644 --- a/redisinsight/api/test/api/database/GET-databases.test.ts +++ b/redisinsight/api/test/api/database/GET-databases.test.ts @@ -17,6 +17,7 @@ const responseSchema = Joi.array().items(Joi.object().keys({ compressor: Joi.string().valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY').allow(null), connectionType: Joi.string().valid('STANDALONE', 'SENTINEL', 'CLUSTER', 'NOT CONNECTED').required(), lastConnection: Joi.string().isoDate().allow(null).required(), + createdAt: Joi.string().isoDate(), version: Joi.string().allow(null).required(), modules: Joi.array().items(Joi.object().keys({ name: Joi.string().required(), diff --git a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts index 33ba77f555..b612252c62 100644 --- a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts @@ -233,7 +233,7 @@ describe(`PATCH /databases/:id`, () => { after: async () => { newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); expect(newDatabase).to.contain({ - ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection', 'new', 'timeout', 'compressor', 'version']), + ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection', 'new', 'timeout', 'compressor', 'version', 'createdAt']), host: constants.TEST_REDIS_HOST, port: constants.TEST_REDIS_PORT, }); diff --git a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts index acf6efdccf..09075be1d5 100644 --- a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts @@ -161,7 +161,7 @@ describe(`POST /databases/clone/:id`, () => { after: async () => { newDatabase = await localDb.getInstanceByName('some name'); expect(newDatabase).to.contain({ - ..._.omit(oldDatabase, ['id', 'modules', 'name', 'provider', 'lastConnection', 'new', 'timeout', 'compressor', 'version']), + ..._.omit(oldDatabase, ['id', 'modules', 'name', 'provider', 'lastConnection', 'new', 'timeout', 'compressor', 'version', 'createdAt']), host: constants.TEST_REDIS_HOST, port: constants.TEST_REDIS_PORT, }); diff --git a/redisinsight/api/test/api/database/constants.ts b/redisinsight/api/test/api/database/constants.ts index 68024ae1b0..c1129b033b 100644 --- a/redisinsight/api/test/api/database/constants.ts +++ b/redisinsight/api/test/api/database/constants.ts @@ -36,6 +36,7 @@ export const databaseSchema = Joi.object().keys({ compressor: Joi.string().valid('NONE', 'LZ4', 'GZIP', 'ZSTD', 'SNAPPY').required(), nameFromProvider: Joi.string().allow(null), lastConnection: Joi.string().isoDate().allow(null), + createdAt: Joi.string().isoDate(), provider: Joi.string().valid(...providers), new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null),