From 0211726540867e17acbfb19ccd18dbfab4c3bd21 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 26 Sep 2025 13:26:41 +0000 Subject: [PATCH 1/2] feat(redis): integrate Redis basic support into the application --- .../connection/connection.controller.ts | 6 +- .../utils/validate-create-connection-data.ts | 8 +- .../export-csv-from-table.use.case.ts | 1 + backend/src/enums/connection-type.enum.ts | 4 +- .../saas-tests/table-redis-e2e.test.ts | 3716 +++++++++++++++++ backend/test/mock.factory.ts | 11 + backend/test/utils/create-test-table.ts | 74 + backend/test/utils/get-test-data.ts | 2 + docker-compose.yml | 13 + shared-code/package.json | 1 + shared-code/src/caching/caching-constants.ts | 14 + shared-code/src/caching/lru-storage.ts | 14 + .../data-access-object-redis.ts | 696 +++ .../shared/create-data-access-object.ts | 37 +- .../data-structures/connections-params.ds.ts | 3 +- .../shared/enums/connection-types-enum.ts | 2 + yarn.lock | 66 + 17 files changed, 4657 insertions(+), 11 deletions(-) create mode 100644 backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts create mode 100644 shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts diff --git a/backend/src/entities/connection/connection.controller.ts b/backend/src/entities/connection/connection.controller.ts index 666b34d61..9e91d312e 100644 --- a/backend/src/entities/connection/connection.controller.ts +++ b/backend/src/entities/connection/connection.controller.ts @@ -709,7 +709,8 @@ export class ConnectionController { return errors; } - if (!connectionData.username) errors.push(Messages.USERNAME_MISSING); + if (!connectionData.username && connectionData.type !== ConnectionTypesEnum.redis) + errors.push(Messages.USERNAME_MISSING); if ( connectionData.type === ConnectionTypesEnum.dynamodb || @@ -718,7 +719,8 @@ export class ConnectionController { return errors; } - if (!connectionData.database) errors.push(Messages.DATABASE_MISSING); + if (!connectionData.database && connectionData.type !== ConnectionTypesEnum.redis) + errors.push(Messages.DATABASE_MISSING); if (process.env.NODE_ENV !== 'test' && !connectionData.ssh) { if (!this.isMongoHost(connectionData.host)) { if (!validator.isFQDN(connectionData.host) && !validator.isIP(connectionData.host)) diff --git a/backend/src/entities/connection/utils/validate-create-connection-data.ts b/backend/src/entities/connection/utils/validate-create-connection-data.ts index 23e62ba4b..5c3fcba38 100644 --- a/backend/src/entities/connection/utils/validate-create-connection-data.ts +++ b/backend/src/entities/connection/utils/validate-create-connection-data.ts @@ -35,9 +35,13 @@ export async function validateCreateConnectionData( // ); // } // } - if (!username) errors.push(Messages.USERNAME_MISSING); + if (!username && type !== ConnectionTypesEnum.redis) errors.push(Messages.USERNAME_MISSING); - if (type !== ConnectionTypesEnum.dynamodb && type !== ConnectionTypesEnum.elasticsearch) { + if ( + type !== ConnectionTypesEnum.dynamodb && + type !== ConnectionTypesEnum.elasticsearch && + type !== ConnectionTypesEnum.redis + ) { if (!database) errors.push(Messages.DATABASE_MISSING); if (port < 0 || port > 65535 || !port) errors.push(Messages.PORT_MISSING); if (typeof port !== 'number') errors.push(Messages.PORT_FORMAT_INCORRECT); diff --git a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts index fefa5fb6f..cc702d063 100644 --- a/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts +++ b/backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts @@ -108,6 +108,7 @@ export class ExportCSVFromTableUseCase connection.type === 'mongodb' || connection.type === 'dynamodb' || connection.type === 'elasticsearch' || + connection.type === 'redis' || isConnectionTypeAgent(connection.type) ) { return new StreamableFile(csv.stringify(rowsStream as any, { header: true })); diff --git a/backend/src/enums/connection-type.enum.ts b/backend/src/enums/connection-type.enum.ts index 2beb52d76..10fa84e62 100644 --- a/backend/src/enums/connection-type.enum.ts +++ b/backend/src/enums/connection-type.enum.ts @@ -8,7 +8,8 @@ export enum ConnectionTypeTestEnum { mongodb = 'mongodb', dynamodb = 'dynamodb', elasticsearch = 'elasticsearch', - cassandra = 'cassandra', + cassandra = 'cassandra', + redis = 'redis', agent_postgres = 'agent_postgres', agent_mysql = 'agent_mysql', agent_oracledb = 'agent_oracledb', @@ -17,4 +18,5 @@ export enum ConnectionTypeTestEnum { agent_mongodb = 'agent_mongodb', agent_elasticsearch = 'agent_elasticsearch', agent_cassandra = 'agent_cassandra', + agent_redis = 'agent_redis', } diff --git a/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts new file mode 100644 index 000000000..3ce589eb0 --- /dev/null +++ b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts @@ -0,0 +1,3716 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable security/detect-object-injection */ +import { faker } from '@faker-js/faker'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import test from 'ava'; +import { ValidationError } from 'class-validator'; +import cookieParser from 'cookie-parser'; +import fs from 'fs'; +import path, { join } from 'path'; +import request from 'supertest'; +import { fileURLToPath } from 'url'; +import { ApplicationModule } from '../../../src/app.module.js'; +import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; +import { LogOperationTypeEnum, QueryOrderingEnum } from '../../../src/enums/index.js'; +import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; +import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; +import { Messages } from '../../../src/exceptions/text/messages.js'; +import { Cacher } from '../../../src/helpers/cache/cacher.js'; +import { Constants } from '../../../src/helpers/constants/constants.js'; +import { DatabaseModule } from '../../../src/shared/database/database.module.js'; +import { DatabaseService } from '../../../src/shared/database/database.service.js'; +import { MockFactory } from '../../mock.factory.js'; +import { createTestTable } from '../../utils/create-test-table.js'; +import { getTestData } from '../../utils/get-test-data.js'; +import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js'; +import { TestUtils } from '../../utils/test.utils.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const mockFactory = new MockFactory(); +let app: INestApplication; +let testUtils: TestUtils; +const testSearchedUserName = 'Vasia'; +const testTables: Array = []; +let currentTest; + +test.before(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ApplicationModule, DatabaseModule], + providers: [DatabaseService, TestUtils], + }).compile(); + app = moduleFixture.createNestApplication() as any; + testUtils = moduleFixture.get(TestUtils); + + app.use(cookieParser()); + app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory(validationErrors: ValidationError[] = []) { + return new ValidationException(validationErrors); + }, + }), + ); + await app.init(); + app.getHttpServer().listen(0); +}); + +test.after(async () => { + try { + await Cacher.clearAllCache(); + await app.close(); + } catch (e) { + console.error('After tests error ' + e); + } +}); + +currentTest = 'GET /connection/tables/:slug'; + +test.serial(`${currentTest} should return list of tables in connection`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTablesRO = JSON.parse(getTablesResponse.text); + t.is(typeof getTablesRO, 'object'); + t.is(getTablesRO.length > 0, true); + + const testTableIndex = getTablesRO.findIndex((t) => t.table === testTableName); + + t.is(getTablesRO[testTableIndex].hasOwnProperty('table'), true); + t.is(getTablesRO[testTableIndex].hasOwnProperty('permissions'), true); + t.is(typeof getTablesRO[testTableIndex].permissions, 'object'); + t.is(Object.keys(getTablesRO[testTableIndex].permissions).length, 5); + t.is(getTablesRO[testTableIndex].table, testTableName); + t.is(getTablesRO[testTableIndex].permissions.visibility, true); + t.is(getTablesRO[testTableIndex].permissions.readonly, false); + t.is(getTablesRO[testTableIndex].permissions.add, true); + t.is(getTablesRO[testTableIndex].permissions.delete, true); + t.is(getTablesRO[testTableIndex].permissions.edit, true); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should throw an error when connectionId not passed in request`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + createConnectionRO.id = ''; + const getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesResponse.status, 404); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should throw an error when connection id is incorrect`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + t.is(createConnectionResponse.status, 201); + createConnectionRO.id = faker.string.uuid(); + const getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTablesResponse.status, 400); + const { message } = JSON.parse(getTablesResponse.text); + t.is(message, Messages.CONNECTION_NOT_FOUND); + } catch (e) { + console.error(e); + throw e; + } +}); + +currentTest = 'GET /table/rows/:slug'; + +test.serial(`${currentTest} should return rows of selected table without search and without pagination`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testTableSecondColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.hasOwnProperty('large_dataset'), true); + t.is(getTableRowsRO.rows.length, Constants.DEFAULT_PAGINATION.perPage); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[0].hasOwnProperty('key'), true); + t.is(getTableRowsRO.rows[1].hasOwnProperty(testTableColumnName), true); + t.is(getTableRowsRO.rows[10].hasOwnProperty(testTableSecondColumnName), true); + t.is(getTableRowsRO.rows[15].hasOwnProperty('created_at'), true); + t.is(getTableRowsRO.rows[19].hasOwnProperty('updated_at'), true); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should return rows of selected table with search and without pagination`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + ['key'], + undefined, + undefined, + 3, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(createTableSettingsResponse.status, 201); + + const searchedDescription = insertedSearchedIds.find((id) => id.number === 0).id; + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${searchedDescription}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedDescription); + t.is(getTableRowsRO.rows[0].hasOwnProperty(testTableColumnName), true); + t.is(getTableRowsRO.rows[0].hasOwnProperty(testTableSecondColumnName), true); + t.is(getTableRowsRO.rows[0].hasOwnProperty('created_at'), true); + t.is(getTableRowsRO.rows[0].hasOwnProperty('updated_at'), true); + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should return page of all rows with pagination page=1, perPage=2`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + ['key'], + undefined, + undefined, + 3, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=2`) + .set('Content-Type', 'application/json') + .set('Cookie', firstUserToken) + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[0].hasOwnProperty('key'), true); + t.is(getTableRowsRO.rows[1].hasOwnProperty(testTableColumnName), true); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + t.is(getTableRowsRO.primaryColumns[0].column_name, 'key'); + t.is(getTableRowsRO.primaryColumns[0].data_type, 'string'); + + t.is(getTableRowsRO.pagination.total, 42); + t.is(getTableRowsRO.pagination.lastPage, 21); + t.is(getTableRowsRO.pagination.perPage, 2); + t.is(getTableRowsRO.pagination.currentPage, 1); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should return page of all rows with pagination page=3, perPage=2`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + ['key'], + undefined, + undefined, + 3, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=3&perPage=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[0].hasOwnProperty('key'), true); + t.is(getTableRowsRO.rows[1].hasOwnProperty(testTableColumnName), true); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + t.is(getTableRowsRO.primaryColumns[0].column_name, 'key'); + t.is(getTableRowsRO.primaryColumns[0].data_type, 'string'); + + t.is(getTableRowsRO.pagination.total, 42); + t.is(getTableRowsRO.pagination.lastPage, 21); + t.is(getTableRowsRO.pagination.perPage, 2); + t.is(getTableRowsRO.pagination.currentPage, 3); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `${currentTest} with search, with pagination, without sorting +should return all found rows with pagination page=1 perPage=2`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); + + t.is(getTableRowsResponse.status, 200); + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + t.is(getTableRowsRO.primaryColumns[0].column_name, 'key'); + t.is(getTableRowsRO.primaryColumns[0].data_type, 'string'); + + t.is(getTableRowsRO.pagination.total, 3); + t.is(getTableRowsRO.pagination.lastPage, 2); + t.is(getTableRowsRO.pagination.perPage, 2); + t.is(getTableRowsRO.pagination.currentPage, 1); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination, without sorting +should return all found rows with pagination page=1 perPage=3`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + t.is(getTableRowsRO.primaryColumns[0].column_name, 'key'); + t.is(getTableRowsRO.primaryColumns[0].data_type, 'string'); + + t.is(getTableRowsRO.pagination.total, 3); + t.is(getTableRowsRO.pagination.lastPage, 2); + t.is(getTableRowsRO.pagination.perPage, 2); + t.is(getTableRowsRO.pagination.currentPage, 1); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} without search and without pagination and with sorting +should return all found rows with sorting ids by DESC`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 42, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 42); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + // t.is(getTableRowsRO.rows[0].key, 42); + // t.is(getTableRowsRO.rows[1].key, 41); + // t.is(getTableRowsRO.rows[41].id, 1); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} without search and without pagination and with sorting +should return all found rows with sorting age by ASC`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 42, + QueryOrderingEnum.ASC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + // check that rows are sorted by age in ASC order + for (let i = 1; i < getTableRowsRO.rows.length; i++) { + const currentAge = getTableRowsRO.rows[i].age; + const previousAge = getTableRowsRO.rows[i - 1].age; + t.is(currentAge >= previousAge, true); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} without search and with pagination and with sorting +should return all found rows with sorting ages by DESC and with pagination page=1, perPage=2`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + const searchedLastId = insertedSearchedIds.find((id) => id.number === 37).id; + + const preSearchedLastId = insertedSearchedIds.find((id) => id.number === 21).id; + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[1].key, preSearchedLastId); + t.is(getTableRowsRO.rows[0].key, searchedLastId); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} should return all found rows with sorting ages by ASC and with pagination page=1, perPage=2`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.ASC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + + // check that returned rows are sorted + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.skip(`${currentTest} should return all found rows with sorting ages by DESC and with pagination page=2, perPage=3`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=2&perPage=3`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); + + console.log('🚀 ~ insertedSearchedIds:', insertedSearchedIds); + const searchedFirstId = insertedSearchedIds.find((id) => id.number === 37).id; + + const searchedSecondId = insertedSearchedIds.find((id) => id.number === 21).id; + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 3); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedFirstId); + t.is(getTableRowsRO.rows[1].key, searchedSecondId); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `${currentTest} with search, with pagination and with sorting +should return all found rows with search, pagination: page=1, perPage=2 and DESC sorting`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=2&search=${testSearchedUserName}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); + + console.log('🚀 ~ insertedSearchedIds:', insertedSearchedIds); + const searchedFirstId = insertedSearchedIds.find((id) => id.number === 37).id; + + const searchedSecondId = insertedSearchedIds.find((id) => id.number === 21).id; + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[1]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedFirstId); + t.is(getTableRowsRO.rows[1].key, searchedSecondId); + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 2); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination and with sorting +should return all found rows with search, pagination: page=2, perPage=2 and DESC sorting`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=2&perPage=2&search=${testSearchedUserName}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + const searchedFirstId = insertedSearchedIds.find((id) => id.number === 0).id; + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedFirstId); + t.is(getTableRowsRO.pagination.currentPage, 2); + t.is(getTableRowsRO.pagination.perPage, 2); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination and with sorting +should return all found rows with search, pagination: page=1, perPage=2 and ASC sorting`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.ASC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=2&search=${testSearchedUserName}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTableRowsResponse.status, 200); + + const searchedFirstId = insertedSearchedIds.find((id) => id.number === 0).id; + const searchedSecondId = insertedSearchedIds.find((id) => id.number === 21).id; + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 2); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedFirstId); + t.is(getTableRowsRO.rows[1].key, searchedSecondId); + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 2); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination and with sorting +should return all found rows with search, pagination: page=2, perPage=2 and ASC sorting`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.ASC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=2&perPage=2&search=${testSearchedUserName}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + const searchedFirstId = insertedSearchedIds.find((id) => id.number === 37).id; + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + t.is(getTableRowsRO.rows[0].key, searchedFirstId); + t.is(getTableRowsRO.pagination.currentPage, 2); + t.is(getTableRowsRO.pagination.perPage, 2); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// todo: rework for other tables after removing old endpoint +test.serial( + `${currentTest} with search, with pagination, with sorting and with filtering +should return all found rows with search, pagination: page=1, perPage=2 and DESC sorting and filtering in body`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'age'; + const fieldvalue = 18; + + const filters = { + [fieldname]: { lt: fieldvalue }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post( + `/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2`, + ) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + + const foundId = insertedSearchedIds.find((id) => id.number === 0).id; + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + t.is(getTableRowsRO.rows[0].key, foundId); + + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 2); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination, with sorting and with filtering +should return all found rows with search, pagination: page=1, perPage=10 and DESC sorting and filtering'`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'age'; + const fieldvalue = 18; + + const filters = { + [fieldname]: { lt: fieldvalue }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post( + `/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=10`, + ) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 201); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + + const foundId = insertedSearchedIds.find((id) => id.number === 0).id; + + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + t.is(getTableRowsRO.rows[0].key, foundId); + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 10); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `${currentTest} with search, with pagination, with sorting and with filtering +should return all found rows with search, pagination: page=1, perPage=2 and DESC sorting and with multi filtering`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'age'; + const fieldGtvalue = 14; + const fieldLtvalue = 95; + + const filters = { + [fieldname]: { lt: fieldLtvalue, gt: fieldGtvalue }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post( + `/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=3`, + ) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 201); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 6); + + const findRowId = insertedSearchedIds.find((id) => id.number === 21).id; + + t.is(getTableRowsRO.rows[0].key, findRowId); + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 3); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial(`${currentTest} should throw an exception when connection id is not passed in request`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'key'; + const fieldGtvalue = '25'; + const fieldLtvalue = '40'; + createConnectionRO.id = ''; + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2&f_${fieldname}__lt=${fieldLtvalue}&f_${fieldname}__gt=${fieldGtvalue}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 404); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should throw an exception when connection id passed in request is incorrect`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'key'; + const fieldGtvalue = '25'; + const fieldLtvalue = '40'; + + createConnectionRO.id = faker.string.uuid(); + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2&f_${fieldname}__lt=${fieldLtvalue}&f_${fieldname}__gt=${fieldGtvalue}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 403); + + const { message } = JSON.parse(getTableRowsResponse.text); + + t.is(message, Messages.DONT_HAVE_PERMISSIONS); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`${currentTest} should throw an exception when table name passed in request is incorrect`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'key'; + const fieldGtvalue = '25'; + const fieldLtvalue = '40'; + + const fakeTableName = `${faker.lorem.words(1)}_${faker.string.uuid()}`; + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${fakeTableName}&search=${testSearchedUserName}&page=1&perPage=2&f_${fieldname}__lt=${fieldLtvalue}&f_${fieldname}__gt=${fieldGtvalue}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 400); + + const { message } = JSON.parse(getTableRowsResponse.text); + + t.is(message, Messages.TABLE_NOT_FOUND); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `${currentTest} should return an array with searched fields when filtered name passed in request is incorrect`, + async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'age', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = faker.lorem.words(1); + const fieldGtvalue = '25'; + const fieldLtvalue = '40'; + + const getTableRowsResponse = await request(app.getHttpServer()) + .get( + `/table/rows/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2&f_${fieldname}__lt=${fieldLtvalue}&f_${fieldname}__gt=${fieldGtvalue}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTablesRO = JSON.parse(getTableRowsResponse.text); + t.is(getTablesRO.rows.length, 2); + t.is(getTablesRO.rows[0][testTableColumnName], testSearchedUserName); + t.is(getTablesRO.rows[1][testTableColumnName], testSearchedUserName); + t.is(getTablesRO.hasOwnProperty('primaryColumns'), true); + t.is(getTablesRO.hasOwnProperty('pagination'), true); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +currentTest = 'GET /table/structure/:slug'; +test.serial(`${currentTest} should return table structure`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableStructure = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableStructure.status, 200); + const getTableStructureRO = JSON.parse(getTableStructure.text); + + t.is(typeof getTableStructureRO, 'object'); + t.is(typeof getTableStructureRO.structure, 'object'); + t.is(getTableStructureRO.structure.length, 6); + + for (const element of getTableStructureRO.structure) { + t.is(element.hasOwnProperty('column_name'), true); + t.is(element.hasOwnProperty('column_default'), true); + t.is(element.hasOwnProperty('data_type'), true); + t.is(element.hasOwnProperty('isExcluded'), true); + t.is(element.hasOwnProperty('isSearched'), true); + } + + t.is(getTableStructureRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableStructureRO.hasOwnProperty('foreignKeys'), true); + + for (const element of getTableStructureRO.primaryColumns) { + t.is(element.hasOwnProperty('column_name'), true); + t.is(element.hasOwnProperty('data_type'), true); + } + + for (const element of getTableStructureRO.foreignKeys) { + t.is(element.hasOwnProperty('referenced_column_name'), true); + t.is(element.hasOwnProperty('referenced_table_name'), true); + t.is(element.hasOwnProperty('constraint_name'), true); + t.is(element.hasOwnProperty('column_name'), true); + } +}); + +test.serial(`${currentTest} should throw an exception whe connection id not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + createConnectionRO.id = ''; + const getTableStructure = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableStructure.status, 404); +}); + +test.serial(`${currentTest} should throw an exception whe connection id passed in request id incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + createConnectionRO.id = faker.string.uuid(); + const getTableStructure = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableStructure.status, 403); + const { message } = JSON.parse(getTableStructure.text); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest}should throw an exception when tableName not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + const tableName = ''; + const getTableStructure = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${tableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableStructure.status, 400); + const { message } = JSON.parse(getTableStructure.text); + t.is(message, Messages.TABLE_NAME_MISSING); +}); + +test.serial(`${currentTest} should throw an exception when tableName passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + const tableName = faker.lorem.words(1); + const getTableStructure = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${tableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableStructure.status, 400); + const { message } = JSON.parse(getTableStructure.text); + t.is(message, Messages.TABLE_NOT_FOUND); +}); + +currentTest = 'POST /table/row/:slug'; + +test.serial(`${currentTest} should add row in table and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + key: `user_43`, + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${testTableName}`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const addRowInTableRO = JSON.parse(addRowInTableResponse.text); + t.is(addRowInTableResponse.status, 201); + + t.is(addRowInTableRO.hasOwnProperty('row'), true); + t.is(addRowInTableRO.hasOwnProperty('structure'), true); + t.is(addRowInTableRO.hasOwnProperty('foreignKeys'), true); + t.is(addRowInTableRO.hasOwnProperty('primaryColumns'), true); + t.is(addRowInTableRO.hasOwnProperty('readonly_fields'), true); + t.is(addRowInTableRO.row[testTableColumnName], row[testTableColumnName]); + t.is(addRowInTableRO.row[testTableSecondColumnName], row[testTableSecondColumnName]); + + //checking that the line was added + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + console.log('🚀 ~ rows:', rows); + + const addedRowIndex = rows.findIndex((r) => r.key === row.key); + + t.is(rows.length, 43); + t.is(rows[addedRowIndex][testTableColumnName], row[testTableColumnName]); + t.is(rows[addedRowIndex][testTableSecondColumnName], row[testTableSecondColumnName]); + + // check that rows adding was logged + + const getLogsResponse = await request(app.getHttpServer()) + .get(`/logs/${createConnectionRO.id}?page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getLogsResponse.status, 200); + const getLogsRO = JSON.parse(getLogsResponse.text); + t.is(getLogsRO.hasOwnProperty('logs'), true); + t.is(getLogsRO.hasOwnProperty('pagination'), true); + t.is(getLogsRO.logs.length > 0, true); + const addRowLogIndex = getLogsRO.logs.findIndex((log) => log.operationType === 'addRow'); + t.is(getLogsRO.logs[addRowLogIndex].hasOwnProperty('affected_primary_key'), true); + t.is(typeof getLogsRO.logs[addRowLogIndex].affected_primary_key, 'object'); + t.is(getLogsRO.logs[addRowLogIndex].affected_primary_key.hasOwnProperty('key'), true); +}); + +test.serial(`${currentTest} should throw an exception when connection id is not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + key: 999, + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + const fakeConnectionId = ''; + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${fakeConnectionId}?tableName=${testTableName}`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowInTableResponse.status, 404); + + //checking that the line wasn't added + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); +}); + +test.serial(`${currentTest} should throw an exception when table name is not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + key: 999, + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowInTableResponse.status, 400); + const { message } = JSON.parse(addRowInTableResponse.text); + + t.is(message, Messages.TABLE_NAME_MISSING); + + //checking that the line wasn't added + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); +}); + +test.serial(`${currentTest} should throw an exception when row is not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowInTableResponse.status, 400); + const { message } = JSON.parse(addRowInTableResponse.text); + + t.is(message, Messages.PARAMETER_MISSING); + + //checking that the line wasn't added + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); +}); + +test.serial(`${currentTest} should throw an exception when table name passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + key: 999, + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const fakeTableName = `${faker.lorem.words(1)}_${faker.number.int({ min: 1, max: 10000 })}`; + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRowInTableResponse.status, 400); + const { message } = JSON.parse(addRowInTableResponse.text); + + t.is(message, Messages.TABLE_NOT_FOUND); + + //checking that the line wasn't added + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); +}); + +currentTest = 'PUT /table/row/:slug'; + +test.serial(`${currentTest} should update row in table and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + const foundIdForUpdate = insertedSearchedIds[0].id; + console.log('🚀 ~ foundIdForUpdate:', foundIdForUpdate); + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${foundIdForUpdate}`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 200); + const updateRowInTableRO = JSON.parse(updateRowInTableResponse.text); + console.log('🚀 ~ updateRowInTableRO:', updateRowInTableRO); + + t.is(updateRowInTableRO.hasOwnProperty('row'), true); + t.is(updateRowInTableRO.hasOwnProperty('structure'), true); + t.is(updateRowInTableRO.hasOwnProperty('foreignKeys'), true); + t.is(updateRowInTableRO.hasOwnProperty('primaryColumns'), true); + t.is(updateRowInTableRO.hasOwnProperty('readonly_fields'), true); + t.is(updateRowInTableRO.row[testTableColumnName], row[testTableColumnName]); + t.is(updateRowInTableRO.row[testTableSecondColumnName], row[testTableSecondColumnName]); + + //checking that the line was updated + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + const updateRowIndex = rows.map((row) => row.key).indexOf(foundIdForUpdate); + t.is(rows.length, 42); + t.is(rows[updateRowIndex][testTableColumnName], row[testTableColumnName]); + t.is(rows[updateRowIndex][testTableSecondColumnName], row[testTableSecondColumnName]); +}); + +test.serial(`${currentTest} should throw an exception when connection id not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + createConnectionRO.id = ''; + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=1`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 404); +}); + +test.serial(`${currentTest} should throw an exception when connection id passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + createConnectionRO.id = faker.string.uuid(); + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=1`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 403); + const { message } = JSON.parse(updateRowInTableResponse.text); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); +}); + +test.serial(`${currentTest} should throw an exception when tableName not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + createConnectionRO.id = faker.string.uuid(); + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=&key=1`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 400); + const { message } = JSON.parse(updateRowInTableResponse.text); + t.is(message, Messages.TABLE_NAME_MISSING); +}); + +test.serial(`${currentTest} should throw an exception when tableName passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const fakeTableName = faker.string.uuid(); + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}&key=1`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 400); + const { message } = JSON.parse(updateRowInTableResponse.text); + t.is(message, Messages.TABLE_NOT_FOUND); +}); + +test.serial(`${currentTest} should throw an exception when primary key not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 400); + const { message } = JSON.parse(updateRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); +}); + +test.serial( + `${currentTest} should throw an exception when primary key passed in request has incorrect field name`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&IncorrectField=1`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 400); + const { message } = JSON.parse(updateRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); + }, +); + +test.serial( + `${currentTest} should throw an exception when primary key passed in request has incorrect field value`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }; + + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=100000000`) + .send(JSON.stringify(row)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowInTableResponse.status, 400); + const responseObject = JSON.parse(updateRowInTableResponse.text); + t.is(responseObject.message, Messages.ROW_PRIMARY_KEY_NOT_FOUND); + }, +); + +currentTest = 'PUT /table/rows/update/:connectionId'; + +test.serial(`${currentTest} should update multiple rows and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const foundFirstIdForUpdate = insertedSearchedIds[0].id; + const foundSecondIdForUpdate = insertedSearchedIds[1].id; + const requestData = { + primaryKeys: [{ key: foundFirstIdForUpdate }, { key: foundSecondIdForUpdate }], + newValues: { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + }, + }; + + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${testTableName}`) + .send(JSON.stringify(requestData)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const updateRowInTableRO = JSON.parse(updateRowInTableResponse.text); + + t.is(updateRowInTableResponse.status, 200); + t.is(updateRowInTableRO.success, true); + + // check that the rows were updated + const firstRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${foundFirstIdForUpdate}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const firstRow = JSON.parse(firstRowResponse.text); + t.is(firstRowResponse.status, 200); + t.is(firstRow.row[testTableColumnName], fakeName); + t.is(firstRow.row[testTableSecondColumnName], fakeMail); + + const secondRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${foundSecondIdForUpdate}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const secondRow = JSON.parse(secondRowResponse.text); + t.is(secondRowResponse.status, 200); + t.is(secondRow.row[testTableColumnName], fakeName); + t.is(secondRow.row[testTableSecondColumnName], fakeMail); +}); + +currentTest = 'DELETE /table/row/:slug'; + +test.serial(`${currentTest} should delete row in table and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 200); + const deleteRowInTableRO = JSON.parse(deleteRowInTableResponse.text); + + t.is(deleteRowInTableRO.hasOwnProperty('row'), true); + + //checking that the line was deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 41); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, true); +}); + +test.serial(`${currentTest} should throw an exception when connection id not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const connectionId = ''; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${connectionId}?tableName=${testTableName}&key=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 404); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); +}); + +test.serial(`${currentTest} should throw an exception when connection id passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const connectionId = faker.string.uuid(); + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${connectionId}?tableName=${testTableName}&key=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 403); + const { message } = JSON.parse(deleteRowInTableResponse.text); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); +}); + +test.serial(`${currentTest} should throw an exception when tableName not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const fakeTableName = ''; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}&key=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 400); + const { message } = JSON.parse(deleteRowInTableResponse.text); + t.is(message, Messages.TABLE_NAME_MISSING); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); +}); + +test.serial(`${currentTest} should throw an exception when tableName passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const fakeTableName = `${faker.lorem.words(1)}_${faker.string.uuid()}`; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}&key=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 400); + const { message } = JSON.parse(deleteRowInTableResponse.text); + t.is(message, Messages.TABLE_NOT_FOUND); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); +}); + +test.serial(`${currentTest} should throw an exception when primary key not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 400); + const { message } = JSON.parse(deleteRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); +}); + +test.serial( + `${currentTest} should throw an exception when primary key passed in request has incorrect field name`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { + testTableName, + testTableColumnName, + testEntitiesSeedsCount, + testTableSecondColumnName, + insertedSearchedIds, + } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForDeletion = insertedSearchedIds[0].id; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&fakePKey=${idForDeletion}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 400); + const { message } = JSON.parse(deleteRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); + + //checking that the line wasn't deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 42); + const deletedRowIndex = rows.map((row: Record) => row.key).indexOf(idForDeletion); + t.is(deletedRowIndex < 0, false); + }, +); + +test.serial( + `${currentTest} should throw an exception when primary key passed in request has incorrect field value`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const deleteRowInTableResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=100000`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const deleteRowInTableRO = JSON.parse(deleteRowInTableResponse.text); + console.log('🚀 ~ deleteRowInTableRO:', deleteRowInTableRO); + t.is(deleteRowInTableResponse.status, 400); + t.is(deleteRowInTableRO.message, Messages.ROW_PRIMARY_KEY_NOT_FOUND); + }, +); + +currentTest = 'GET /table/row/:slug'; + +test.serial(`${currentTest} found row`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = insertedSearchedIds[0].id; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 200); + const foundRowInTableRO = JSON.parse(foundRowInTableResponse.text); + t.is(foundRowInTableRO.hasOwnProperty('row'), true); + t.is(foundRowInTableRO.hasOwnProperty('structure'), true); + t.is(foundRowInTableRO.hasOwnProperty('foreignKeys'), true); + t.is(foundRowInTableRO.hasOwnProperty('primaryColumns'), true); + t.is(foundRowInTableRO.hasOwnProperty('readonly_fields'), true); + t.is(typeof foundRowInTableRO.row, 'object'); + t.is(typeof foundRowInTableRO.structure, 'object'); + t.is(typeof foundRowInTableRO.primaryColumns, 'object'); + t.is(typeof foundRowInTableRO.readonly_fields, 'object'); + t.is(typeof foundRowInTableRO.foreignKeys, 'object'); + t.is(foundRowInTableRO.row.key, idForSearch); + t.is(foundRowInTableRO.row[testTableColumnName], testSearchedUserName); + t.is(Object.keys(foundRowInTableRO.row).length, 6); +}); + +test.serial(`${currentTest} should throw an exception, when connection id is not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + const idForSearch = 1; + createConnectionRO.id = ''; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 404); +}); + +test.serial( + `${currentTest} should throw an exception, when connection id passed in request is incorrect`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = 1; + createConnectionRO.id = faker.string.uuid(); + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 403); + const { message } = JSON.parse(foundRowInTableResponse.text); + t.is(message, Messages.DONT_HAVE_PERMISSIONS); + }, +); + +test.serial(`${currentTest} should throw an exception, when tableName in not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = 1; + const fakeTableName = ''; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 400); + const { message } = JSON.parse(foundRowInTableResponse.text); + t.is(message, Messages.TABLE_NAME_MISSING); +}); + +test.serial(`${currentTest} should throw an exception, when tableName passed in request is incorrect`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = 1; + const fakeTableName = `${faker.lorem.words(1)}_${faker.string.uuid()}`; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${fakeTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 400); + const { message } = JSON.parse(foundRowInTableResponse.text); + t.is(message, Messages.TABLE_NOT_FOUND); +}); + +test.serial(`${currentTest} should throw an exception, when primary key is not passed in request`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 400); + const { message } = JSON.parse(foundRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); +}); + +test.serial( + `${currentTest} should throw an exception, when primary key passed in request has incorrect name`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { + testTableName, + testTableColumnName, + testEntitiesSeedsCount, + testTableSecondColumnName, + insertedSearchedIds, + } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = insertedSearchedIds[0].id; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&fakeKeyName=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 400); + const { message } = JSON.parse(foundRowInTableResponse.text); + t.is(message, Messages.PRIMARY_KEY_INVALID); + }, +); + +test.serial( + `${currentTest} should throw an exception, when primary key passed in request has incorrect value`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { + testTableName, + testTableColumnName, + testEntitiesSeedsCount, + testTableSecondColumnName, + insertedSearchedIds, + } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const idForSearch = '6604197dab8d910eb77783f9'; + const foundRowInTableResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&key=${idForSearch}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(foundRowInTableResponse.status, 400); + const findRowResponse = JSON.parse(foundRowInTableResponse.text); + t.is(findRowResponse.message, Messages.ROW_PRIMARY_KEY_NOT_FOUND); + }, +); + +currentTest = 'PUT /table/rows/delete/:slug'; + +test.serial(`${currentTest} should delete row in table and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const primaryKeysForDeletion: Array> = [ + { + key: insertedSearchedIds[0].id, + }, + { + key: insertedSearchedIds[1].id, + }, + { + key: insertedSearchedIds[2].id, + }, + ]; + const deleteRowsInTableResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${testTableName}`) + .send(primaryKeysForDeletion) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowsInTableResponse.status, 200); + const deleteRowInTableRO = JSON.parse(deleteRowsInTableResponse.text); + + //checking that the line was deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + // check that lines was deleted + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, testEntitiesSeedsCount - primaryKeysForDeletion.length); + + for (const key of primaryKeysForDeletion) { + t.is( + rows.findIndex((row) => row.key === key.key), + -1, + ); + } + + // check that table deletaion was logged + const tableLogsResponse = await request(app.getHttpServer()) + .get(`/logs/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(tableLogsResponse.status, 200); + + const tableLogsRO = JSON.parse(tableLogsResponse.text); + t.is(tableLogsRO.logs.length, primaryKeysForDeletion.length + 1); + const onlyDeleteLogs = tableLogsRO.logs.filter((log) => log.operationType === LogOperationTypeEnum.deleteRow); + for (const key of primaryKeysForDeletion) { + t.is(onlyDeleteLogs.findIndex((log) => log.received_data.key === key.key) >= 0, true); + } +}); + +currentTest = 'DELETE /table/rows/:slug'; + +test.serial(`${currentTest} should delete row in table and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName, insertedSearchedIds } = + await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const primaryKeysForDeletion = [ + { + key: insertedSearchedIds[0].id, + }, + { + key: insertedSearchedIds[1].id, + }, + { + key: insertedSearchedIds[2].id, + }, + ]; + const deleteRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${testTableName}`) + .send(primaryKeysForDeletion) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowInTableResponse.status, 200); + const deleteRowInTableRO = JSON.parse(deleteRowInTableResponse.text); + + //checking that the line was deleted + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + t.is(rows.length, 39); + for (const primaryKey of primaryKeysForDeletion) { + t.is( + rows.findIndex((row) => row.key === primaryKey.key), + -1, + ); + } +}); + +test.serial(`${currentTest} should test connection and return result`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + const testConnectionResponse = await request(app.getHttpServer()) + .post('/connection/test/') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(testConnectionResponse.status, 201); + const { message } = JSON.parse(testConnectionResponse.text); + t.is(message, 'Successfully connected'); +}); + +currentTest = 'GET table/csv/:slug'; + +test.serial(`${currentTest} should return csv file with table data`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableCsvResponse = await request(app.getHttpServer()) + .post(`/table/csv/export/${createConnectionRO.id}?tableName=${testTableName}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'text/csv') + .set('Accept', 'text/csv'); + + if (getTableCsvResponse.status !== 201) { + console.log(getTableCsvResponse.text); + } + t.is(getTableCsvResponse.status, 201); + const fileName = `${testTableName}.csv`; + const downloadedFilePatch = join(__dirname, 'response-files', fileName); + + const dir = join(__dirname, 'response-files'); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body); + + // eslint-disable-next-line security/detect-non-literal-fs-filename + const isFileExists = fs.existsSync(downloadedFilePatch); + t.is(isFileExists, true); +}); + +test.serial( + `${currentTest} should return csv file with table data with search, with pagination, with sorting, +with search and pagination: page=1, perPage=2 and DESC sorting`, + async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + const { + testTableName, + testTableColumnName, + testEntitiesSeedsCount, + testTableSecondColumnName, + insertedSearchedIds, + } = await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableCsvResponse = await request(app.getHttpServer()) + .post( + `/table/csv/export/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'text/csv') + .set('Accept', 'text/csv'); + + if (getTableCsvResponse.status !== 201) { + console.log(getTableCsvResponse.text); + } + t.is(getTableCsvResponse.status, 201); + const fileName = `${testTableName}.csv`; + const downloadedFilePatch = join(__dirname, 'response-files', fileName); + + const dir = join(__dirname, 'response-files'); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body); + + // eslint-disable-next-line security/detect-non-literal-fs-filename + const isFileExists = fs.existsSync(downloadedFilePatch); + t.is(isFileExists, true); + }, +); + +currentTest = 'POST /table/csv/import/:slug'; +test.serial(`${currentTest} should import csv file with table data`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).redisConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [testTableColumnName], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'key', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const getTableCsvResponse = await request(app.getHttpServer()) + .post( + `/table/csv/export/${createConnectionRO.id}?tableName=${testTableName}&search=${testSearchedUserName}&page=1&perPage=2`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'text/csv') + .set('Accept', 'text/csv'); + + if (getTableCsvResponse.status !== 201) { + const getTableCsvResponseText = JSON.parse(getTableCsvResponse.text); + console.log(getTableCsvResponseText); + } + t.is(getTableCsvResponse.status, 201); + const fileName = `${testTableName}.csv`; + const downloadedFilePatch = join(__dirname, 'response-files', fileName); + + const dir = join(__dirname, 'response-files'); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(downloadedFilePatch, getTableCsvResponse.body); + // eslint-disable-next-line security/detect-non-literal-fs-filename + const isFileExists = fs.existsSync(downloadedFilePatch); + t.is(isFileExists, true); + + function deleteIdColumnsInCSVFile(filePatch: string) { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const fileContent = fs.readFileSync(filePatch).toString(); + const rows = fileContent.split('\n'); + const idColumnIndex = rows[0].split(',').findIndex((column) => column === 'key'); + const newRows = rows.map((row) => { + const columns = row.split(','); + if (columns.length === 1) { + return row; + } + if (idColumnIndex >= 0) { + columns.splice(idColumnIndex, 1); + } + return columns.join(','); + }); + return newRows.join('\n'); + } + + const newFileContent = deleteIdColumnsInCSVFile(downloadedFilePatch); + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.writeFileSync(downloadedFilePatch, newFileContent); + + const importCsvResponse = await request(app.getHttpServer()) + .post(`/table/csv/import/${createConnectionRO.id}?tableName=${testTableName}`) + .attach('file', downloadedFilePatch) + .set('Cookie', firstUserToken) + .set('Accept', 'application/json'); + + t.is(importCsvResponse.status, 201); + + //checking that the lines was added + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, testEntitiesSeedsCount + 2); +}); diff --git a/backend/test/mock.factory.ts b/backend/test/mock.factory.ts index f6893e776..73a8b124e 100644 --- a/backend/test/mock.factory.ts +++ b/backend/test/mock.factory.ts @@ -284,6 +284,17 @@ export class MockFactory { return dto; } + generateConnectionToTestRedisInDocker() { + const dto = new CreateConnectionDto() as any; + dto.title = 'Test connection to Redis in Docker'; + dto.type = ConnectionTypesEnum.redis; + dto.host = 'redis-e2e-testing'; + dto.port = 6379; + dto.password = 'SuperSecretRedisPassword'; + dto.ssh = false; + return dto; + } + generateKnexConfigAgentTests(db_type = 'postgres') { switch (db_type) { case 'postgres': diff --git a/backend/test/utils/create-test-table.ts b/backend/test/utils/create-test-table.ts index bc7762a5b..f98467235 100644 --- a/backend/test/utils/create-test-table.ts +++ b/backend/test/utils/create-test-table.ts @@ -10,6 +10,7 @@ import { BatchWriteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb import { Client } from '@elastic/elasticsearch'; import * as cassandra from 'cassandra-driver'; import { v4 as uuidv4 } from 'uuid'; +import { createClient } from 'redis'; export async function createTestTable( connectionParams: any, @@ -36,6 +37,10 @@ export async function createTestTable( return createTestDynamoDBTable(connectionParams, testEntitiesSeedsCount, testSearchedUserName); } + if (connectionParams.type === ConnectionTypesEnum.redis) { + return createTestRedisTable(connectionParams, testEntitiesSeedsCount, testSearchedUserName); + } + const testTableName = getRandomTestTableName(); const testTableColumnName = `${faker.lorem.words(1)}_${faker.lorem.words(1)}`; const testTableSecondColumnName = `${faker.lorem.words(1)}_${faker.lorem.words(1)}`; @@ -767,3 +772,72 @@ export async function createTestCassandraTable( await client.shutdown().catch((err) => console.error('Error shutting down Cassandra client:', err)); } } + +async function createTestRedisTable( + connectionParams: any, + testEntitiesSeedsCount = 42, + testSearchedUserName = 'Vasia', +): Promise { + const testTableName = getRandomTestTableName().toLowerCase(); + const testTableColumnName = 'name'; + const testTableSecondColumnName = 'email'; + + const redisClient = createClient({ + socket: { + host: connectionParams.host, + port: connectionParams.port, + ca: connectionParams.cert || undefined, + cert: connectionParams.cert || undefined, + rejectUnauthorized: connectionParams.ssl === false ? false : true, + }, + password: connectionParams.password || undefined, + }); + + await redisClient.connect(); + + const existingKeys = await redisClient.keys(`${testTableName}:*`); + if (existingKeys.length > 0) { + await redisClient.del(existingKeys); + } + + const insertedSearchedIds: Array<{ number: number; id: string }> = []; + + for (let i = 0; i < testEntitiesSeedsCount; i++) { + const isSearchedUser = i === 0 || i === testEntitiesSeedsCount - 21 || i === testEntitiesSeedsCount - 5; + const key = `user_${i}`; + const rowKey = `${testTableName}:${key}`; + + const data = { + [testTableColumnName]: isSearchedUser ? testSearchedUserName : faker.person.firstName(), + [testTableSecondColumnName]: faker.internet.email(), + age: isSearchedUser + ? i === 0 + ? 14 + : i === testEntitiesSeedsCount - 21 + ? 90 + : 95 + : faker.number.int({ min: 16, max: 80 }), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + await redisClient.set(rowKey, JSON.stringify(data)); + + if (isSearchedUser) { + insertedSearchedIds.push({ + number: i, + id: key, + }); + } + } + + await redisClient.quit(); + + return { + testTableName: testTableName, + testTableColumnName: testTableColumnName, + testTableSecondColumnName: testTableSecondColumnName, + testEntitiesSeedsCount: testEntitiesSeedsCount, + insertedSearchedIds, + }; +} diff --git a/backend/test/utils/get-test-data.ts b/backend/test/utils/get-test-data.ts index 2ff672a60..c4923def4 100644 --- a/backend/test/utils/get-test-data.ts +++ b/backend/test/utils/get-test-data.ts @@ -30,6 +30,7 @@ export function getTestData(mockFactory: MockFactory) { const elasticsearchTestConnection = mockFactory.generateConnectionToTestElasticsearchInDocker(); const cassandraTestConnection = mockFactory.generateConnectionToTestCassandraInDocker(); const cassandraAgentTestConnection = mockFactory.generateConnectionToTestCassandraAgent(); + const redisConnection = mockFactory.generateConnectionToTestRedisInDocker(); return { newConnection, newEncryptedConnection, @@ -60,5 +61,6 @@ export function getTestData(mockFactory: MockFactory) { elasticsearchTestConnection, cassandraTestConnection, cassandraAgentTestConnection, + redisConnection, }; } diff --git a/docker-compose.yml b/docker-compose.yml index fca26f83b..939ae1a79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: - test-elasticsearch-e2e-testing - cassandra-init - test-cassandra-e2e-testing + - redis-e2e-testing links: - postgres - testMySQL-e2e-testing @@ -33,6 +34,7 @@ services: - test-dynamodb-e2e-testing - test-elasticsearch-e2e-testing - test-cassandra-e2e-testing + - redis-e2e-testing command: ["yarn", "start"] testMySQL-e2e-testing: @@ -201,6 +203,17 @@ services: command: ["/init-cassandra.sh"] restart: "no" + redis-e2e-testing: + image: redis:7.0.11 + ports: + - 6379:6379 + command: ["redis-server", "--requirepass", "SuperSecretRedisPassword"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + rocketadmin-agent_mongo: build: context: . diff --git a/shared-code/package.json b/shared-code/package.json index 70a38c0de..2e208c455 100644 --- a/shared-code/package.json +++ b/shared-code/package.json @@ -26,6 +26,7 @@ "pg": "^8.16.3", "pg-copy-streams": "^7.0.0", "pg-query-stream": "^4.10.3", + "redis": "^5.8.2", "ssh2": "^1.17.0", "tedious": "^18.6.1", "tunnel-ssh": "5.2.0" diff --git a/shared-code/src/caching/caching-constants.ts b/shared-code/src/caching/caching-constants.ts index 403dda0f0..ad5f0c324 100644 --- a/shared-code/src/caching/caching-constants.ts +++ b/shared-code/src/caching/caching-constants.ts @@ -47,6 +47,20 @@ export const CACHING_CONSTANTS = { }, }, + DEFAULT_REDIS_CLIENT_CACHE_OPTIONS: { + max: 150, + ttl: 1000 * 60 * 60, + updateAgeOnGet: false, + updateAgeOnHas: false, + dispose: async (client: any) => { + try { + await client.quit(); + } catch (_e) { + return; + } + }, + }, + DEFAULT_TUNNEL_CACHE_OPTIONS: { max: 100, ttl: 1000 * 60 * 60, diff --git a/shared-code/src/caching/lru-storage.ts b/shared-code/src/caching/lru-storage.ts index 0113404bf..9a2c3ab82 100644 --- a/shared-code/src/caching/lru-storage.ts +++ b/shared-code/src/caching/lru-storage.ts @@ -19,7 +19,21 @@ const cassandraClientCache = new LRUCache(CACHING_CONSTANTS.DEFAULT_CASSANDRA_CL const tableStructureCache = new LRUCache(CACHING_CONSTANTS.DEFAULT_TABLE_STRUCTURE_ELEMENTS_CACHE_OPTIONS); const tableForeignKeysCache = new LRUCache(CACHING_CONSTANTS.DEFAULT_TABLE_STRUCTURE_ELEMENTS_CACHE_OPTIONS); const tablePrimaryKeysCache = new LRUCache(CACHING_CONSTANTS.DEFAULT_TABLE_STRUCTURE_ELEMENTS_CACHE_OPTIONS); +const redisClientCache = new LRUCache(CACHING_CONSTANTS.DEFAULT_REDIS_CLIENT_CACHE_OPTIONS); export class LRUStorage { + public static getRedisClientCache(connection: ConnectionParams): any | null { + const cachedClient = redisClientCache.get(this.getConnectionIdentifier(connection)); + return cachedClient ? cachedClient : null; + } + + public static setRedisClientCache(connection: ConnectionParams, client: any): void { + redisClientCache.set(this.getConnectionIdentifier(connection), client); + } + + public static delRedisClientCache(connection: ConnectionParams): void { + redisClientCache.delete(this.getConnectionIdentifier(connection)); + } + public static setCassandraClientCache(connection: ConnectionParams, client: Client): void { cassandraClientCache.set(this.getConnectionIdentifier(connection), client); } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts new file mode 100644 index 000000000..38dc0ee88 --- /dev/null +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts @@ -0,0 +1,696 @@ +/* eslint-disable security/detect-object-injection */ +import * as csv from 'csv'; +import { nanoid } from 'nanoid'; +import { Readable, Stream } from 'node:stream'; +import { createClient, RedisClientType } from 'redis'; +import { LRUStorage } from '../../caching/lru-storage.js'; +import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; +import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; +import { ConnectionParams } from '../shared/data-structures/connections-params.ds.js'; +import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; +import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; +import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; +import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; +import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; +import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; +import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; +import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; +import { FilterCriteriaEnum } from '../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../shared/interfaces/data-access-object.interface.js'; +import { BasicDataAccessObject } from './basic-data-access-object.js'; + +export class DataAccessObjectRedis extends BasicDataAccessObject implements IDataAccessObject { + constructor(connection: ConnectionParams) { + super(connection); + } + + public async addRowInTable( + tableName: string, + row: Record, + ): Promise | number> { + const redisClient = await this.getClient(); + const key = row.key ? String(row.key) : nanoid(); + const rowKey = `${tableName}:${key}`; + const dataToStore = { ...row }; + delete dataToStore.key; + await redisClient.set(rowKey, JSON.stringify(dataToStore)); + return { key }; + } + + public async deleteRowInTable( + tableName: string, + primaryKey: Record, + ): Promise> { + const redisClient = await this.getClient(); + if (!primaryKey.key) { + throw new Error('Primary key "key" is required for Redis operations'); + } + const rowKey = `${tableName}:${primaryKey.key}`; + await redisClient.del(rowKey); + return primaryKey; + } + + public async getIdentityColumns( + tableName: string, + referencedFieldName: string, + identityColumnName: string, + fieldValues: Array, + ): Promise>> { + const redisClient = await this.getClient(); + const results: Array> = []; + + const pattern = `${tableName}:*`; + const keys = await redisClient.keys(pattern); + + for (const key of keys) { + const data = await redisClient.get(key); + if (data) { + try { + const parsedData = JSON.parse(data as string); + const keyPart = key.split(':').slice(1).join(':'); + const rowData = { key: keyPart, ...parsedData }; + if (fieldValues.includes(rowData[referencedFieldName])) { + const result: Record = { + [referencedFieldName]: rowData[referencedFieldName], + }; + if (identityColumnName && rowData[identityColumnName] !== undefined) { + result[identityColumnName] = rowData[identityColumnName]; + } + results.push(result); + } + } catch (_error) { + continue; + } + } + } + return results; + } + + public async getRowByPrimaryKey( + tableName: string, + primaryKey: Record, + settings: TableSettingsDS, + ): Promise> { + const redisClient = await this.getClient(); + if (!primaryKey.key) { + throw new Error('Primary key "key" is required for Redis operations'); + } + const rowKey = `${tableName}:${primaryKey.key}`; + const data = await redisClient.get(rowKey); + + if (!data) { + return null; + } + + try { + const parsedData = JSON.parse(data as string); + const result = { key: primaryKey.key, ...parsedData }; + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(settings, tableStructure); + + if (availableFields.length > 0) { + const filteredResult: Record = {}; + for (const field of availableFields) { + if (result[field] !== undefined) { + filteredResult[field] = result[field]; + } + } + return filteredResult; + } + } + + return result; + } catch (error) { + throw new Error(`Invalid JSON data for key ${rowKey}: ${error.message}`); + } + } + + public async bulkGetRowsFromTableByPrimaryKeys( + tableName: string, + primaryKeys: Array>, + settings: TableSettingsDS, + ): Promise>> { + const results: Array> = []; + for (const primaryKey of primaryKeys) { + try { + const row = await this.getRowByPrimaryKey(tableName, primaryKey, settings); + if (row) { + results.push(row); + } + } catch (_error) { + continue; + } + } + return results; + } + + public async getRowsFromTable( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: Array, + autocompleteFields: AutocompleteFieldsDS, + ): Promise { + const redisClient = await this.getClient(); + + page = page > 0 ? page : DAO_CONSTANTS.DEFAULT_PAGINATION.page; + perPage = + perPage > 0 + ? perPage + : settings.list_per_page > 0 + ? settings.list_per_page + : DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; + + if (autocompleteFields?.value && autocompleteFields.fields?.length > 0) { + return await this.getAutocompleteRows(tableName, autocompleteFields); + } + + const pattern = `${tableName}:*`; + const allRows: Array> = []; + let cursor = '0'; + let processedCount = 0; + const targetRows = page * perPage; + + do { + const scanResult = await redisClient.scan(cursor, { + MATCH: pattern, + COUNT: 100, + }); + + cursor = scanResult.cursor; + const keys = scanResult.keys; + + if (keys.length > 0) { + const pipeline = redisClient.multi(); + keys.forEach((key) => pipeline.get(key)); + const results = await pipeline.exec(); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const data = results[i] as unknown as string; + + if (data) { + try { + const parsedData = JSON.parse(data); + const keyPart = key.split(':').slice(1).join(':'); + const rowData = { key: keyPart, ...parsedData }; + const passesFilter = await this.passesFilters(rowData, filteringFields, searchedFieldValue, settings); + if (passesFilter) { + allRows.push(rowData); + processedCount++; + } + } catch (_error) { + continue; + } + } + } + + if (processedCount >= targetRows + perPage && cursor === '0') { + break; + } + } + } while (cursor !== '0'); + + if (settings.ordering_field && settings.ordering) { + allRows.sort((a, b) => { + const aVal = a[settings.ordering_field]; + const bVal = b[settings.ordering_field]; + const modifier = settings.ordering === 'ASC' ? 1 : -1; + + if (aVal < bVal) return -1 * modifier; + if (aVal > bVal) return 1 * modifier; + return 0; + }); + } + + const totalRows = allRows.length; + const offset = (page - 1) * perPage; + const paginatedRows = allRows.slice(offset, offset + perPage); + + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(settings, tableStructure); + + const finalRows = paginatedRows.map((row) => { + if (availableFields.length > 0) { + const filteredRow: Record = {}; + availableFields.forEach((field) => { + if (row[field] !== undefined) { + filteredRow[field] = row[field]; + } + }); + return filteredRow; + } + return row; + }); + + return { + data: finalRows, + pagination: { + total: totalRows, + lastPage: Math.ceil(totalRows / perPage), + perPage: perPage, + currentPage: page, + }, + large_dataset: totalRows > 1000, + }; + } + + public async getTableForeignKeys(_tableName: string): Promise> { + return []; + } + + public async getTablePrimaryColumns(_tableName: string): Promise> { + return [ + { + column_name: 'key', + data_type: 'string', + }, + ]; + } + + public async getTablesFromDB(): Promise> { + const redisClient = await this.getClient(); + + const allKeys = await redisClient.keys('*'); + const tableNames = new Set(); + + allKeys.forEach((key) => { + const parts = key.split(':'); + if (parts.length >= 2) { + tableNames.add(parts[0]); + } + }); + + return Array.from(tableNames).map((tableName) => ({ + tableName: tableName, + isView: false, + })); + } + + public async getTableStructure(tableName: string): Promise> { + const redisClient = await this.getClient(); + + const pattern = `${tableName}:*`; + const keys = await redisClient.keys(pattern); + const fieldTypes = new Map(); + + fieldTypes.set('key', 'string'); + + const sampleSize = Math.min(keys.length, 10); + for (let i = 0; i < sampleSize; i++) { + const data = await redisClient.get(keys[i]); + if (data && typeof data === 'string') { + try { + const parsedData = JSON.parse(data); + Object.entries(parsedData).forEach(([field, value]) => { + if (!fieldTypes.has(field)) { + fieldTypes.set(field, this.inferRedisDataType(value)); + } + }); + } catch (_error) { + continue; + } + } + } + + return Array.from(fieldTypes.entries()).map(([columnName, dataType]) => ({ + column_name: columnName, + data_type: dataType, + is_nullable: true, + allow_null: true, + column_default: null, + character_maximum_length: null, + numeric_precision: null, + numeric_scale: null, + datetime_precision: null, + character_set_name: null, + collation_name: null, + data_type_params: '', + udt_name: dataType, + extra: columnName === 'key' ? 'primary_key' : null, + })); + } + + public async testConnect(): Promise { + try { + const redisClient = await this.getClient(); + const response = await redisClient.ping(); + + if (response === 'PONG') { + return { + result: true, + message: 'Successfully connected', + }; + } else { + return { + result: false, + message: 'Redis ping failed', + }; + } + } catch (error) { + return { + result: false, + message: `Connection failed: ${error.message}`, + }; + } + } + + public async updateRowInTable( + tableName: string, + row: Record, + primaryKey: Record, + ): Promise> { + const redisClient = await this.getClient(); + + if (!primaryKey.key) { + throw new Error('Primary key "key" is required for Redis operations'); + } + + const rowKey = `${tableName}:${primaryKey.key}`; + + const exists = await redisClient.exists(rowKey); + if (!exists) { + throw new Error(`Row with key ${primaryKey.key} not found in ${tableName}`); + } + + const existingData = await redisClient.get(rowKey); + const existingParsed = existingData ? JSON.parse(existingData as string) : {}; + + const updatedData = { ...existingParsed }; + Object.entries(row).forEach(([field, value]) => { + if (field !== 'key') { + updatedData[field] = value; + } + }); + await redisClient.set(rowKey, JSON.stringify(updatedData)); + return { key: primaryKey.key, ...updatedData }; + } + + public async bulkUpdateRowsInTable( + tableName: string, + newValues: Record, + primaryKeys: Array>, + ): Promise>> { + const results: Array> = []; + + for (const primaryKey of primaryKeys) { + try { + const updatedRow = await this.updateRowInTable(tableName, newValues, primaryKey); + results.push(updatedRow); + } catch (_error) { + continue; + } + } + + return results; + } + + public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { + const redisClient = await this.getClient(); + let deletedCount = 0; + + for (const primaryKey of primaryKeys) { + if (primaryKey.key) { + const rowKey = `${tableName}:${primaryKey.key}`; + const deleted = await redisClient.del(rowKey); + deletedCount += deleted; + } + } + + return deletedCount; + } + + public async validateSettings(settings: ValidateTableSettingsDS, _tableName: string): Promise> { + const errors: string[] = []; + + if (settings.excluded_fields?.includes('key')) { + errors.push('Cannot exclude the "key" field in Redis tables'); + } + + return errors; + } + + public async getReferencedTableNamesAndColumns(_tableName: string): Promise> { + return []; + } + + public async isView(_tableName: string): Promise { + return false; + } + + public async getTableRowsStream( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: Array, + ): Promise> { + const rowsResult = (await this.getRowsFromTable( + tableName, + settings, + page, + perPage, + searchedFieldValue, + filteringFields, + null, + )) as any; + return rowsResult.data; + } + + public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { + const stream = new Readable(); + stream.push(file.buffer); + stream.push(null); + + const parser = stream.pipe(csv.parse({ columns: true })); + const results: any[] = []; + for await (const record of parser) { + results.push(record); + } + try { + await Promise.allSettled( + results.map(async (row) => { + return await this.addRowInTable(tableName, row); + }), + ); + } catch (error) { + throw error; + } + } + + public async executeRawQuery(query: string, _tableName: string): Promise>> { + const redisClient = await this.getClient(); + + try { + const parts = query.trim().split(/\s+/); + const command = parts[0].toUpperCase(); + + switch (command) { + case 'GET': + if (parts.length !== 2) throw new Error('GET requires exactly one key'); + const value = await redisClient.get(parts[1]); + return [{ key: parts[1], value }]; + + case 'KEYS': + if (parts.length !== 2) throw new Error('KEYS requires exactly one pattern'); + const keys = await redisClient.keys(parts[1]); + return keys.map((key) => ({ key })); + + case 'PING': + const response = await redisClient.ping(); + return [{ response }]; + + default: + throw new Error(`Unsupported command: ${command}`); + } + } catch (error) { + throw new Error(`Redis command failed: ${error.message}`); + } + } + + private async getClient(): Promise { + let client: RedisClientType = LRUStorage.getRedisClientCache(this.connection); + if (!client) { + client = createClient({ + socket: { + host: this.connection.host, + port: this.connection.port, + ca: this.connection.cert || undefined, + cert: this.connection.cert || undefined, + rejectUnauthorized: this.connection.ssl === false ? false : true, + }, + password: this.connection.password || undefined, + }); + await client.connect(); + LRUStorage.setRedisClientCache(this.connection, client); + } + return client; + } + + private inferRedisDataType(value: any): string { + if (value === null || value === undefined) return 'string'; + if (typeof value === 'string') return 'string'; + if (typeof value === 'number') { + return Number.isInteger(value) ? 'integer' : 'decimal'; + } + if (typeof value === 'boolean') return 'boolean'; + if (Array.isArray(value)) return 'array'; + if (typeof value === 'object') return 'json'; + return 'string'; + } + + private async getAutocompleteRows(tableName: string, autocompleteFields: AutocompleteFieldsDS): Promise { + const redisClient = await this.getClient(); + const { fields, value } = autocompleteFields; + const pattern = `${tableName}:*`; + const rows: Array> = []; + let cursor = '0'; + let processedCount = 0; + + do { + const scanResult = await redisClient.scan(cursor, { + MATCH: pattern, + COUNT: 50, + }); + + cursor = scanResult.cursor; + const keys = scanResult.keys.slice(0, DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT - processedCount); + + if (keys.length > 0) { + const pipeline = redisClient.multi(); + keys.forEach((key) => pipeline.get(key)); + const results = await pipeline.exec(); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const data = results[i] as unknown as string; + + if (data) { + try { + const parsedData = JSON.parse(data); + const keyPart = key.split(':').slice(1).join(':'); + const rowData = { key: keyPart, ...parsedData }; + + if ( + value === '*' || + fields.some((field) => + String(rowData[field] || '') + .toLowerCase() + .includes(String(value).toLowerCase()), + ) + ) { + const autocompleteRow: Record = {}; + fields.forEach((field) => { + if (rowData[field] !== undefined) { + autocompleteRow[field] = rowData[field]; + } + }); + rows.push(autocompleteRow); + processedCount++; + } + } catch (_error) { + continue; + } + } + + if (processedCount >= DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT) { + break; + } + } + } + + if (processedCount >= DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT) { + break; + } + } while (cursor !== '0'); + + return { + data: rows, + pagination: {} as any, + large_dataset: false, + }; + } + + private async passesFilters( + row: Record, + filteringFields: Array, + searchedFieldValue: string, + settings: TableSettingsDS, + ): Promise { + if (filteringFields?.length > 0) { + const filterResults: boolean[] = []; + + for (const filterObject of filteringFields) { + const { field, criteria, value } = filterObject; + const rowValue = row[field]; + let result: boolean; + + if (rowValue === undefined) { + result = false; + } else { + switch (criteria) { + case FilterCriteriaEnum.eq: + result = rowValue === value; + break; + case FilterCriteriaEnum.startswith: + result = String(rowValue).toLowerCase().startsWith(String(value).toLowerCase()); + break; + case FilterCriteriaEnum.endswith: + result = String(rowValue).toLowerCase().endsWith(String(value).toLowerCase()); + break; + case FilterCriteriaEnum.contains: + result = String(rowValue).toLowerCase().includes(String(value).toLowerCase()); + break; + case FilterCriteriaEnum.icontains: + result = !String(rowValue).toLowerCase().includes(String(value).toLowerCase()); + break; + case FilterCriteriaEnum.gt: + result = Number(rowValue) > Number(value); + break; + case FilterCriteriaEnum.lt: + result = Number(rowValue) < Number(value); + break; + case FilterCriteriaEnum.gte: + result = Number(rowValue) >= Number(value); + break; + case FilterCriteriaEnum.lte: + result = Number(rowValue) <= Number(value); + break; + case FilterCriteriaEnum.empty: + result = rowValue === null || rowValue === undefined || rowValue === ''; + break; + default: + result = true; + break; + } + } + + filterResults.push(result); + } + const passesFiltering = filterResults.every((r) => r); + if (!passesFiltering) return false; + } + + if (searchedFieldValue) { + const searchFields = + settings.search_fields?.length > 0 + ? settings.search_fields + : await this.getTableStructure(row.key as string).then((structure) => structure.map((s) => s.column_name)); + + const passesSearch = searchFields.some((field) => + String(row[field] || '') + .toLowerCase() + .includes(searchedFieldValue.toLowerCase()), + ); + + if (!passesSearch) return false; + } + return true; + } +} diff --git a/shared-code/src/data-access-layer/shared/create-data-access-object.ts b/shared-code/src/data-access-layer/shared/create-data-access-object.ts index 3dba36e72..10b96e9f4 100644 --- a/shared-code/src/data-access-layer/shared/create-data-access-object.ts +++ b/shared-code/src/data-access-layer/shared/create-data-access-object.ts @@ -9,6 +9,7 @@ import { DataAccessObjectMssql } from '../data-access-objects/data-access-object import { DataAccessObjectMysql } from '../data-access-objects/data-access-object-mysql.js'; import { DataAccessObjectOracle } from '../data-access-objects/data-access-object-oracle.js'; import { DataAccessObjectPostgres } from '../data-access-objects/data-access-object-postgres.js'; +import { DataAccessObjectRedis } from '../data-access-objects/data-access-object-redis.js'; import { ConnectionAgentParams, ConnectionParams } from './data-structures/connections-params.ds.js'; import { ConnectionTypesEnum } from './enums/connection-types-enum.js'; import { IDataAccessObjectAgent } from './interfaces/data-access-object-agent.interface.js'; @@ -28,6 +29,7 @@ export function getDataAccessObject( ConnectionTypesEnum.agent_ibmdb2, ConnectionTypesEnum.agent_mongodb, ConnectionTypesEnum.agent_cassandra, + ConnectionTypesEnum.agent_redis, ]; if (!connectionParams || connectionParams === null) { throw new Error(ERROR_MESSAGES.CONNECTION_PARAMS_SHOULD_BE_DEFINED); @@ -63,6 +65,9 @@ export function getDataAccessObject( case ConnectionTypesEnum.cassandra: const connectionParamsCassandra = buildConnectionParams(connectionParams); return new DataAccessObjectCassandra(connectionParamsCassandra); + case ConnectionTypesEnum.redis: + const connectionParamsRedis = buildConnectionParams(connectionParams); + return new DataAccessObjectRedis(connectionParamsRedis); default: if (!agentTypes.includes(connectionParams.type)) { throw new Error(ERROR_MESSAGES.CONNECTION_TYPE_INVALID); @@ -87,11 +92,33 @@ function buildAgentConnectionParams(connectionParams: IUnknownConnectionParams): } function buildConnectionParams(connectionParams: IUnknownConnectionParams): ConnectionParams { - const requiredKeys = - connectionParams.type !== ConnectionTypesEnum.dynamodb && - connectionParams.type !== ConnectionTypesEnum.elasticsearch - ? ['type', 'host', 'port', 'username', 'password', 'database'] - : ['host', 'username', 'password']; + const requiredKeys = []; + + const sqlAndMongoRequiredKeys = ['type', 'host', 'port', 'username', 'password', 'database']; + const elasticAndDynamoAndRedisRequiredKeys = ['host', 'username', 'password']; + const redisRequiredKeys = ['host', 'port', 'password']; + + switch (connectionParams.type) { + case ConnectionTypesEnum.postgres: + case ConnectionTypesEnum.mysql2: + case ConnectionTypesEnum.mysql: + case ConnectionTypesEnum.mssql: + case ConnectionTypesEnum.oracledb: + case ConnectionTypesEnum.ibmdb2: + case ConnectionTypesEnum.mongodb: + case ConnectionTypesEnum.cassandra: + requiredKeys.push(...sqlAndMongoRequiredKeys); + break; + case ConnectionTypesEnum.dynamodb: + case ConnectionTypesEnum.elasticsearch: + requiredKeys.push(...elasticAndDynamoAndRedisRequiredKeys); + break; + case ConnectionTypesEnum.redis: + requiredKeys.push(...redisRequiredKeys); + break; + default: + throw new Error(ERROR_MESSAGES.CONNECTION_TYPE_INVALID); + } if (connectionParams.ssh) { requiredKeys.push('sshHost', 'sshPort', 'sshUsername'); diff --git a/shared-code/src/data-access-layer/shared/data-structures/connections-params.ds.ts b/shared-code/src/data-access-layer/shared/data-structures/connections-params.ds.ts index 9eccb7737..3a3c7617d 100644 --- a/shared-code/src/data-access-layer/shared/data-structures/connections-params.ds.ts +++ b/shared-code/src/data-access-layer/shared/data-structures/connections-params.ds.ts @@ -12,7 +12,8 @@ export class ConnectionParams { | 'mongodb' | 'dynamodb' | 'elasticsearch' - | 'cassandra'; + | 'cassandra' + | 'redis'; host: string; diff --git a/shared-code/src/data-access-layer/shared/enums/connection-types-enum.ts b/shared-code/src/data-access-layer/shared/enums/connection-types-enum.ts index 956a34380..ed9bed041 100644 --- a/shared-code/src/data-access-layer/shared/enums/connection-types-enum.ts +++ b/shared-code/src/data-access-layer/shared/enums/connection-types-enum.ts @@ -9,6 +9,7 @@ export enum ConnectionTypesEnum { dynamodb = 'dynamodb', elasticsearch = 'elasticsearch', cassandra = 'cassandra', + redis = 'redis', agent_postgres = 'agent_postgres', agent_mysql = 'agent_mysql', agent_oracledb = 'agent_oracledb', @@ -16,4 +17,5 @@ export enum ConnectionTypesEnum { agent_ibmdb2 = 'agent_ibmdb2', agent_mongodb = 'agent_mongodb', agent_cassandra = 'agent_cassandra', + agent_redis = 'agent_redis', } diff --git a/yarn.lock b/yarn.lock index 86b3590f0..5e68bab0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3255,6 +3255,51 @@ __metadata: languageName: node linkType: hard +"@redis/bloom@npm:5.8.2": + version: 5.8.2 + resolution: "@redis/bloom@npm:5.8.2" + peerDependencies: + "@redis/client": ^5.8.2 + checksum: 0d3ee59c2ebf0fc1927c1f66dcd38e34342d9a14cc5cda6e725b3f7f16b707dfd72c972fef64df05a03d46d80d91576e84f396c67543936114cb1ff7339db478 + languageName: node + linkType: hard + +"@redis/client@npm:5.8.2": + version: 5.8.2 + resolution: "@redis/client@npm:5.8.2" + dependencies: + cluster-key-slot: 1.1.2 + checksum: c083e22de7d42222d9547959f5561a04efed03ab9e23c3ad7afbcc85193ad02c92a1fb493bf4a7e308b99b6d5a8c529ae6a46d6441fd85ababa40a9bcd09d6f8 + languageName: node + linkType: hard + +"@redis/json@npm:5.8.2": + version: 5.8.2 + resolution: "@redis/json@npm:5.8.2" + peerDependencies: + "@redis/client": ^5.8.2 + checksum: 2cb6fdfc8940a811fb3897b8a06995d573c89243565d8291289a21e3d47388ab4b81a9f8928b5a60c471e257ad0c496e25dbfd713899ecd37a0fffc1bc097139 + languageName: node + linkType: hard + +"@redis/search@npm:5.8.2": + version: 5.8.2 + resolution: "@redis/search@npm:5.8.2" + peerDependencies: + "@redis/client": ^5.8.2 + checksum: 89fe5409aacc7fef26b157c1fd14d73bfed86c6ff48c370b7fdc37dddee83ec975ef0813f075b8d23b2eb6f13ec65d2f6c1ea809becd325a15c9f7b4e97d63ff + languageName: node + linkType: hard + +"@redis/time-series@npm:5.8.2": + version: 5.8.2 + resolution: "@redis/time-series@npm:5.8.2" + peerDependencies: + "@redis/client": ^5.8.2 + checksum: 91d0fe3320289da94e4159c4639d68e287b3e0dfe4228ed0e5c77c53f30f43393d6f6ecd0e8d1f155b280b43fe5dbc3e65a946674f9eb4f658e472ca02b7ab44 + languageName: node + linkType: hard + "@rocketadmin/shared-code@workspace:*, @rocketadmin/shared-code@workspace:shared-code": version: 0.0.0-use.local resolution: "@rocketadmin/shared-code@workspace:shared-code" @@ -3286,6 +3331,7 @@ __metadata: pg-copy-streams: ^7.0.0 pg-query-stream: ^4.10.3 prettier: ^3.6.2 + redis: ^5.8.2 ssh2: ^1.17.0 tedious: ^18.6.1 tunnel-ssh: 5.2.0 @@ -6554,6 +6600,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:1.1.2": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -12323,6 +12376,19 @@ __metadata: languageName: node linkType: hard +"redis@npm:^5.8.2": + version: 5.8.2 + resolution: "redis@npm:5.8.2" + dependencies: + "@redis/bloom": 5.8.2 + "@redis/client": 5.8.2 + "@redis/json": 5.8.2 + "@redis/search": 5.8.2 + "@redis/time-series": 5.8.2 + checksum: 5d02118e863dabbe07ae7d780b51f6e42f7a34e80187eb14f44bdcab6222cee3264f3ade7b11df9abf7ad6505e7d39dd162500783c39a72a7ff80fc0a686b63f + languageName: node + linkType: hard + "reflect-metadata@npm:0.2.2, reflect-metadata@npm:^0.2.2": version: 0.2.2 resolution: "reflect-metadata@npm:0.2.2" From 4830fa8c21b9b17ae5aa47a162f30b4657b6d50d Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 26 Sep 2025 17:10:15 +0000 Subject: [PATCH 2/2] feat(tests): add tests for updating rows with JSON fields in table-postgres-e2e feat(tests): enhance createTestTable to support JSON fields fix(data-access): update JSON column filtering to include JSONB type --- .../saas-tests/table-postgres-e2e.test.ts | 158 ++++++++++++++++++ backend/test/utils/create-test-table.ts | 8 + .../data-access-object-postgres.ts | 6 +- 3 files changed, 169 insertions(+), 3 deletions(-) diff --git a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts index 046e738e8..3b9abfb2b 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts @@ -2513,6 +2513,164 @@ test.serial(`${currentTest} should update row in table and return result`, async t.is(rows[updateRowIndex][testTableSecondColumnName], row[testTableSecondColumnName]); }); +test.serial(`${currentTest} should update row in table and return result, when table has json field`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, testEntitiesSeedsCount, testTableSecondColumnName } = + await createTestTable(connectionToTestDB, undefined, undefined, true); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const fakeName = faker.person.firstName(); + const fakeMail = faker.internet.email(); + + const row = { + [testTableColumnName]: fakeName, + [testTableSecondColumnName]: fakeMail, + ['json_field']: [ + { + id: 'PgSAwanAwptE', + url: 'www.example.com', + name: 'test', + type: 'test type', + metadata: {}, + reference: null, + created_at: '2025-09-25T18:51:09.188Z', + updated_at: '2025-09-25T18:51:09.188Z', + description: 'some test field', + }, + ], + ['jsonb_field']: [ + { + id: 'PgSAwanAwptE', + url: 'www.example.com', + name: 'test', + type: 'test type', + metadata: {}, + reference: null, + created_at: '2025-09-25T18:51:09.188Z', + updated_at: '2025-09-25T18:51:09.188Z', + description: 'some test field', + }, + ], + }; + const rowToUpdate = JSON.stringify(row); + const updateRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&id=1`) + .send(rowToUpdate) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const updateRowInTableRO = JSON.parse(updateRowInTableResponse.text); + t.is(updateRowInTableResponse.status, 200); + t.is(updateRowInTableRO.hasOwnProperty('row'), true); + t.is(updateRowInTableRO.hasOwnProperty('structure'), true); + t.is(updateRowInTableRO.hasOwnProperty('foreignKeys'), true); + t.is(updateRowInTableRO.hasOwnProperty('primaryColumns'), true); + t.is(updateRowInTableRO.hasOwnProperty('readonly_fields'), true); + t.is(updateRowInTableRO.row[testTableColumnName], row[testTableColumnName]); + t.is(updateRowInTableRO.row[testTableSecondColumnName], row[testTableSecondColumnName]); + + //checking that the line was updated + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=50`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 200); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + + const { rows, primaryColumns, pagination } = getTableRowsRO; + + const updateRowIndex = rows.map((row) => row.id).indexOf(1); + t.is(rows.length, 42); + t.is(rows[updateRowIndex][testTableColumnName], row[testTableColumnName]); + t.is(rows[updateRowIndex][testTableSecondColumnName], row[testTableSecondColumnName]); + + // add row with json field + const newRow = { + [testTableColumnName]: faker.person.firstName(), + [testTableSecondColumnName]: faker.internet.email(), + ['json_field']: { + id: 'newRowId', + url: 'www.newrow.com', + name: 'new row', + type: 'new type', + metadata: {}, + reference: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + description: 'new row description', + }, + }; + const addRowInTableResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${testTableName}`) + .send(JSON.stringify(newRow)) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const addRowInTableRO = JSON.parse(addRowInTableResponse.text); + t.is(addRowInTableResponse.status, 201); + t.is(addRowInTableRO.hasOwnProperty('row'), true); + t.is(typeof addRowInTableRO.row['json_field'], 'object'); + + // update the added row + + const updatedNewRow = { + [testTableColumnName]: faker.person.firstName(), + [testTableSecondColumnName]: faker.internet.email(), + ['json_field']: [ + { + id: 'updatedNewRowId', + url: 'www.updatednewrow.com', + name: 'updated new row', + type: 'updated new type', + metadata: {}, + reference: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + description: 'updated new row description', + }, + ], + }; + + const updateNewRowInTableResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${testTableName}&id=${addRowInTableRO.row.id}`) + .send(updatedNewRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const updateNewRowInTableRO = JSON.parse(updateNewRowInTableResponse.text); + t.is(updateNewRowInTableResponse.status, 200); + t.is(updateNewRowInTableRO.hasOwnProperty('row'), true); + t.is(updateNewRowInTableRO.hasOwnProperty('structure'), true); + t.is(updateNewRowInTableRO.hasOwnProperty('foreignKeys'), true); + t.is(updateNewRowInTableRO.hasOwnProperty('primaryColumns'), true); + t.is(updateNewRowInTableRO.hasOwnProperty('readonly_fields'), true); + t.is(updateNewRowInTableRO.row[testTableColumnName], updatedNewRow[testTableColumnName]); + t.is(updateNewRowInTableRO.row[testTableSecondColumnName], updatedNewRow[testTableSecondColumnName]); + t.is(typeof updateNewRowInTableRO.row['json_field'], 'object'); + t.is(Array.isArray(updateNewRowInTableRO.row['json_field']), true); + t.is(updateNewRowInTableRO.row['json_field'][0].id, 'updatedNewRowId'); +}); + test.serial(`${currentTest} should throw an exception when connection id not passed in request`, async (t) => { const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; diff --git a/backend/test/utils/create-test-table.ts b/backend/test/utils/create-test-table.ts index f98467235..3ae5dff22 100644 --- a/backend/test/utils/create-test-table.ts +++ b/backend/test/utils/create-test-table.ts @@ -16,6 +16,7 @@ export async function createTestTable( connectionParams: any, testEntitiesSeedsCount = 42, testSearchedUserName = 'Vasia', + withJsonField = false, ): Promise { if (connectionParams.type === ConnectionTypesEnum.ibmdb2) { return createTestTableIbmDb2(connectionParams, testEntitiesSeedsCount, testSearchedUserName); @@ -59,6 +60,13 @@ export async function createTestTable( table.timestamps(); }); + if(withJsonField) { + await Knex.schema.table(testTableName, function (table) { + table.json('json_field'); + table.jsonb('jsonb_field'); + }) + } + for (let i = 0; i < testEntitiesSeedsCount; i++) { if (i === 0 || i === testEntitiesSeedsCount - 21 || i === testEntitiesSeedsCount - 5) { await Knex(testTableName).insert({ diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts index 5eddf4edb..ff3decdcc 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts @@ -40,7 +40,7 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I ]); const jsonColumnNames = tableStructure - .filter(({ data_type }) => data_type.toLowerCase() === 'json') + .filter(({ data_type }) => data_type.toLowerCase() === 'json' || data_type.toLowerCase() === 'jsonb') .map(({ column_name }) => column_name); const processedRow = { ...row }; @@ -449,7 +449,7 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I ): Promise> { const tableStructure = await this.getTableStructure(tableName); const jsonColumnNames = tableStructure - .filter(({ data_type }) => data_type.toLowerCase() === 'json') + .filter(({ data_type }) => data_type.toLowerCase() === 'json' || data_type.toLowerCase() === 'jsonb') .map(({ column_name }) => column_name); const updatedRow = { ...row }; @@ -474,7 +474,7 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I ): Promise[]> { const tableStructure = await this.getTableStructure(tableName); const jsonColumnNames = tableStructure - .filter(({ data_type }) => data_type.toLowerCase() === 'json') + .filter(({ data_type }) => data_type.toLowerCase() === 'json' || data_type.toLowerCase() === 'jsonb') .map(({ column_name }) => column_name); const updatedValues = { ...newValues };