From dbf25e4a39a1264456f010fd5ce735b54fa4b255 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 20 Feb 2026 08:42:28 +0000 Subject: [PATCH] enable sorting for all table category --- ...ate-or-update-table-categories.use.case.ts | 24 ++- ...d-table-categories-with-tables.use.case.ts | 33 ++-- .../saas-tests/table-categories-e2e.test.ts | 148 +++++++++++++++++- 3 files changed, 180 insertions(+), 25 deletions(-) diff --git a/backend/src/entities/table-categories/use-cases/create-or-update-table-categories.use.case.ts b/backend/src/entities/table-categories/use-cases/create-or-update-table-categories.use.case.ts index 95feec3da..4a0f2f69e 100644 --- a/backend/src/entities/table-categories/use-cases/create-or-update-table-categories.use.case.ts +++ b/backend/src/entities/table-categories/use-cases/create-or-update-table-categories.use.case.ts @@ -23,8 +23,8 @@ export class CreateOrUpdateTableCategoriesUseCase protected async implementation(inputData: CreateOrUpdateTableCategoriesDS): Promise> { const { connectionId, master_password, table_categories } = inputData; - const allTablesCategory = table_categories.find((category) => category.category_id === null); - const filteredCategories = table_categories.filter((category) => category.category_id !== null); + const allTablesCategory = table_categories.find((category) => category.category_id === 'all-tables-kitten'); + const filteredCategories = table_categories.filter((category) => category.category_id !== 'all-tables-kitten'); const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, @@ -49,7 +49,12 @@ export class CreateOrUpdateTableCategoriesUseCase connectionProperties = await this._dbContext.connectionPropertiesRepository.save(newConnectionProperties); } - const createdCategories = filteredCategories.map((category) => { + const categoriesToSave = [...filteredCategories]; + if (allTablesCategory) { + categoriesToSave.unshift(allTablesCategory); + } + + const createdCategories = categoriesToSave.map((category) => { const newCategory = this._dbContext.tableCategoriesRepository.create({ category_name: category.category_name, tables: category.tables, @@ -60,17 +65,6 @@ export class CreateOrUpdateTableCategoriesUseCase return newCategory; }); const savedCategories = await this._dbContext.tableCategoriesRepository.save(createdCategories); - const result = savedCategories.map((category) => buildFoundTableCategoryRo(category)); - - if (allTablesCategory) { - result.unshift({ - category_id: null, - category_name: allTablesCategory.category_name, - category_color: allTablesCategory.category_color, - tables: allTablesCategory.tables, - }); - } - - return result; + return savedCategories.map((category) => buildFoundTableCategoryRo(category)); } } diff --git a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts index bdfd98c8c..a40ed469c 100644 --- a/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts +++ b/backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts @@ -90,19 +90,34 @@ export class FindTableCategoriesWithTablesUseCase const foundTableCategories = await this._dbContext.tableCategoriesRepository.findTableCategoriesForConnection(connectionId); - // const sortedTables = tablesRO.sort((tableRO1, tableRO2) => { - // const name1 = tableRO1.display_name || tableRO1.table; - // const name2 = tableRO2.display_name || tableRO2.table; - // return name1.localeCompare(name2); - // }); + const storedAllTablesCategory = foundTableCategories.find((category) => category.category_id === 'all-tables-kitten'); + const otherCategories = foundTableCategories.filter((category) => category.category_id !== 'all-tables-kitten'); + + let allTablesOrdered: Array; + if (storedAllTablesCategory) { + allTablesOrdered = storedAllTablesCategory.tables + .map((tableName) => tablesRO.find((tableRO) => tableRO.table === tableName)) + .filter(Boolean); + + const storedTableNames = new Set(storedAllTablesCategory.tables); + const newTables = tablesRO.filter((tableRO) => !storedTableNames.has(tableRO.table)); + + if (newTables.length > 0) { + allTablesOrdered = [...allTablesOrdered, ...newTables]; + storedAllTablesCategory.tables = allTablesOrdered.map((t) => t.table); + await this._dbContext.tableCategoriesRepository.save(storedAllTablesCategory); + } + } else { + allTablesOrdered = tablesRO; + } const allTableCategory: FoundTableCategoriesWithTablesRo = { - category_id: null, - category_color: null, + category_id: 'all-tables-kitten', + category_color: storedAllTablesCategory?.category_color ?? null, category_name: 'All tables', - tables: tablesRO, + tables: allTablesOrdered, }; - const foundTableCategoriesRO = foundTableCategories.map((category) => { + const foundTableCategoriesRO = otherCategories.map((category) => { const tablesInCategory = category.tables .map((tableName) => tablesRO.find((tableRO) => tableRO.table === tableName)) .filter(Boolean); diff --git a/backend/test/ava-tests/saas-tests/table-categories-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-categories-e2e.test.ts index 61d229e5d..4afa8bfaa 100644 --- a/backend/test/ava-tests/saas-tests/table-categories-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-categories-e2e.test.ts @@ -352,7 +352,7 @@ test.serial(`${currentTest} find table categories with tables`, async (t) => { t.is(findTableCategoriesWithTablesResponse.status, 200); t.is(findTableCategoriesWithTablesRO[0].category_name, 'All tables'); - t.is(findTableCategoriesWithTablesRO[0].category_id, null); + t.is(findTableCategoriesWithTablesRO[0].category_id, 'all-tables-kitten'); t.is(findTableCategoriesWithTablesRO[0].category_color, null); t.true(findTableCategoriesWithTablesRO[0].tables.length >= 2); @@ -389,6 +389,152 @@ test.serial(`${currentTest} find table categories with tables`, async (t) => { } }); +test.serial(`${currentTest} should store all-tables-kitten and preserve its order in v2 response`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName: firstTestTableName } = await createTestTable(connectionToTestDB); + const { testTableName: secondTestTableName } = 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); + + // create all-tables-kitten with intentional order: second, first + const categoriesDTO: Array = [ + { + category_name: 'All tables', + category_color: null, + tables: [secondTestTableName, firstTestTableName], + category_id: 'all-tables-kitten', + }, + { + category_name: 'Category 1', + category_color: '#FF5733', + tables: [firstTestTableName], + category_id: 'cat-001', + }, + ]; + + const createResponse = await request(app.getHttpServer()) + .put(`/table-categories/${createConnectionRO.id}`) + .send(categoriesDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createResponse.status, 200); + const createRO = JSON.parse(createResponse.text); + + // all-tables-kitten should be first in the response + t.is(createRO[0].category_id, 'all-tables-kitten'); + t.deepEqual(createRO[0].tables, [secondTestTableName, firstTestTableName]); + t.is(createRO[1].category_id, 'cat-001'); + + // v2 GET should use stored order from all-tables-kitten + const findResponse = await request(app.getHttpServer()) + .get(`/table-categories/v2/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const findRO = JSON.parse(findResponse.text); + t.is(findResponse.status, 200); + + t.is(findRO[0].category_id, 'all-tables-kitten'); + t.is(findRO[0].category_name, 'All tables'); + // stored order should be preserved: second, first + const allTablesTableNames = findRO[0].tables.map((t) => t.table); + const secondIdx = allTablesTableNames.indexOf(secondTestTableName); + const firstIdx = allTablesTableNames.indexOf(firstTestTableName); + t.true(secondIdx < firstIdx); + } catch (e) { + console.error(e); + t.fail(); + } +}); + +test.serial(`${currentTest} should append new tables at the end of all-tables-kitten`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName: firstTestTableName } = await createTestTable(connectionToTestDB); + const { testTableName: secondTestTableName } = 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); + + // create all-tables-kitten with only the first two tables (second, first order) + const categoriesDTO: Array = [ + { + category_name: 'All tables', + category_color: null, + tables: [secondTestTableName, firstTestTableName], + category_id: 'all-tables-kitten', + }, + ]; + + const createResponse = await request(app.getHttpServer()) + .put(`/table-categories/${createConnectionRO.id}`) + .send(categoriesDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createResponse.status, 200); + + // now create a third table in the database (simulates a new table appearing) + const { testTableName: thirdTestTableName } = await createTestTable(connectionToTestDB); + + // v2 GET should detect the new table and append it at the end + const findResponse = await request(app.getHttpServer()) + .get(`/table-categories/v2/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const findRO = JSON.parse(findResponse.text); + t.is(findResponse.status, 200); + + const allTablesCategory = findRO[0]; + t.is(allTablesCategory.category_id, 'all-tables-kitten'); + const allTablesTableNames = allTablesCategory.tables.map((t) => t.table); + + // original order preserved: second before first + const secondIdx = allTablesTableNames.indexOf(secondTestTableName); + const firstIdx = allTablesTableNames.indexOf(firstTestTableName); + const thirdIdx = allTablesTableNames.indexOf(thirdTestTableName); + t.true(secondIdx < firstIdx); + // new table should be appended after both existing tables + t.true(thirdIdx > firstIdx); + t.truthy(thirdIdx >= 0); + + // second GET should still have the same order (updated in DB) + const findResponse2 = await request(app.getHttpServer()) + .get(`/table-categories/v2/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const findRO2 = JSON.parse(findResponse2.text); + const allTablesTableNames2 = findRO2[0].tables.map((t) => t.table); + const secondIdx2 = allTablesTableNames2.indexOf(secondTestTableName); + const firstIdx2 = allTablesTableNames2.indexOf(firstTestTableName); + const thirdIdx2 = allTablesTableNames2.indexOf(thirdTestTableName); + t.true(secondIdx2 < firstIdx2); + t.true(thirdIdx2 > firstIdx2); + } catch (e) { + console.error(e); + t.fail(); + } +}); + test.serial(`${currentTest} should preserve table order from database in category response`, async (t) => { try { const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;