feat: add endpoint to find table categories with associated tables#1597
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new GET /table-categories/v2/:connectionId endpoint that returns table categories along with the resolved tables (including permissions/display metadata), and validates it via a new SaaS E2E test.
Changes:
- Added
GET /table-categories/v2/:connectionIdcontroller route + DI wiring for a new “find categories with tables” use case. - Implemented
FindTableCategoriesWithTablesUseCaseto fetch tables, apply permissions, enrich with display names, and group by categories (plus an “All tables” bucket). - Added a new response DTO and an E2E test covering the new endpoint.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/test/ava-tests/saas-tests/table-categories-e2e.test.ts | Adds E2E coverage for the new v2 endpoint response shape and contents. |
| backend/src/entities/table-categories/use-cases/table-categories-use-cases.interface.ts | Introduces a new use case interface for fetching categories with tables. |
| backend/src/entities/table-categories/use-cases/find-table-categories-with-tables.use.case.ts | New use case implementing the endpoint behavior (tables + permissions + grouping). |
| backend/src/entities/table-categories/table-categories.module.ts | Wires new use case provider and applies auth middleware to the new route. |
| backend/src/entities/table-categories/table-categories.controller.ts | Exposes GET /table-categories/v2/:connectionId and calls the new use case. |
| backend/src/entities/table-categories/dto/found-table-categories-with-tables.ro.ts | Adds response model for “categories with tables” endpoint. |
| backend/src/common/data-injection.tokens.ts | Adds a new UseCaseType token for the use case wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| protected async implementation(inputData: FindTablesDs): Promise<Array<FoundTableCategoriesWithTablesRo>> { | ||
| const { connectionId, masterPwd, userId } = inputData; |
There was a problem hiding this comment.
FindTablesDs includes hiddenTablesOption, but this use case ignores it (it destructures only connectionId, masterPwd, userId) and always filters out hidden tables. Either honor hiddenTablesOption (including the same permission/guard behavior as the existing find-tables use cases) or remove the field from this endpoint’s input to avoid misleading callers.
| const { connectionId, masterPwd, userId } = inputData; | |
| const { connectionId, masterPwd, userId, hiddenTablesOption } = inputData; | |
| if (typeof hiddenTablesOption !== 'undefined' && hiddenTablesOption !== null) { | |
| // This use case intentionally always excludes hidden tables. | |
| // To avoid misleading callers, explicitly reject requests that try to | |
| // control hidden table visibility via `hiddenTablesOption`. | |
| throw new HttpException( | |
| { | |
| message: 'The hiddenTablesOption parameter is not supported for this endpoint.', | |
| }, | |
| HttpStatus.BAD_REQUEST, | |
| ); | |
| } |
| private async addDisplayNamesForTables( | ||
| connectionId: string, | ||
| tablesObjArr: Array<ITableAndViewPermissionData>, | ||
| ): Promise<Array<FoundTableDs>> { | ||
| const tableSettings = await this._dbContext.tableSettingsRepository.findTableSettingsInConnectionPure(connectionId); |
There was a problem hiding this comment.
This use case duplicates substantial logic from the existing table listing use cases (e.g., display-name enrichment and permissions calculation). Keeping multiple copies increases the risk of behavior drift when one copy changes. Consider extracting these helpers into a shared service/util or reusing the existing implementation where possible.
| @ApiProperty({ type: String }) | ||
| category_color: string; |
There was a problem hiding this comment.
category_color is returned as null for the "All tables" category (see use case), but this response model declares it as non-nullable string. Update the DTO type to string | null and mark it nullable in the Swagger decorator.
| @ApiProperty({ type: String }) | |
| category_color: string; | |
| @ApiProperty({ type: String, nullable: true }) | |
| category_color: string | null; |
| const foundTableCategories = | ||
| await this._dbContext.tableCategoriesRepository.findTableCategoriesForConnection(connectionId); | ||
|
|
||
| const sortedTables = tablesRO.sort((tableRO1, tableRO2) => { | ||
| const name1 = tableRO1.display_name || tableRO1.table; |
There was a problem hiding this comment.
findTableCategoriesForConnection() has no ORDER BY, so category order in the response can be nondeterministic. To keep the API output stable (and avoid flaky tests), sort the returned categories (e.g., by category_name/category_id) or add an orderBy in the repository query.
| t.is(findTableCategoriesWithTablesRO[1].category_name, 'Category 1'); | ||
| t.is(findTableCategoriesWithTablesRO[1].category_color, '#FF5733'); | ||
| t.is(findTableCategoriesWithTablesRO[1].category_id, 'cat-001'); | ||
| t.is(findTableCategoriesWithTablesRO[1].tables.length, 1); | ||
| t.is(findTableCategoriesWithTablesRO[1].tables[0].table, firstTestTableName); | ||
|
|
||
| t.is(findTableCategoriesWithTablesRO[2].category_name, 'Category 2'); | ||
| t.is(findTableCategoriesWithTablesRO[2].category_color, '#33FF57'); | ||
| t.is(findTableCategoriesWithTablesRO[2].category_id, 'cat-002'); | ||
| t.is(findTableCategoriesWithTablesRO[2].tables.length, 1); | ||
| t.is(findTableCategoriesWithTablesRO[2].tables[0].table, secondTestTableName); |
There was a problem hiding this comment.
This test assumes a stable ordering of categories in the response (using indexes [1] and [2]). Since the underlying repository query doesn’t specify an ORDER BY, DB return order can be nondeterministic and make the test flaky. Prefer asserting by locating categories via category_id/category_name (e.g., find()) instead of relying on array positions (other than the special "All tables" element).
| t.is(findTableCategoriesWithTablesRO[1].category_name, 'Category 1'); | |
| t.is(findTableCategoriesWithTablesRO[1].category_color, '#FF5733'); | |
| t.is(findTableCategoriesWithTablesRO[1].category_id, 'cat-001'); | |
| t.is(findTableCategoriesWithTablesRO[1].tables.length, 1); | |
| t.is(findTableCategoriesWithTablesRO[1].tables[0].table, firstTestTableName); | |
| t.is(findTableCategoriesWithTablesRO[2].category_name, 'Category 2'); | |
| t.is(findTableCategoriesWithTablesRO[2].category_color, '#33FF57'); | |
| t.is(findTableCategoriesWithTablesRO[2].category_id, 'cat-002'); | |
| t.is(findTableCategoriesWithTablesRO[2].tables.length, 1); | |
| t.is(findTableCategoriesWithTablesRO[2].tables[0].table, secondTestTableName); | |
| const category1 = findTableCategoriesWithTablesRO.find( | |
| (category) => category.category_id === 'cat-001', | |
| ); | |
| t.truthy(category1); | |
| t.is(category1.category_name, 'Category 1'); | |
| t.is(category1.category_color, '#FF5733'); | |
| t.is(category1.category_id, 'cat-001'); | |
| t.is(category1.tables.length, 1); | |
| t.is(category1.tables[0].table, firstTestTableName); | |
| const category2 = findTableCategoriesWithTablesRO.find( | |
| (category) => category.category_id === 'cat-002', | |
| ); | |
| t.truthy(category2); | |
| t.is(category2.category_name, 'Category 2'); | |
| t.is(category2.category_color, '#33FF57'); | |
| t.is(category2.category_id, 'cat-002'); | |
| t.is(category2.tables.length, 1); | |
| t.is(category2.tables[0].table, secondTestTableName); |
| @ApiParam({ name: 'connectionId', required: true }) | ||
| @UseGuards(TablesReceiveGuard) | ||
| @Get('/v2/:connectionId') | ||
| async findTableCategoriesWithTAbles( |
There was a problem hiding this comment.
Method name findTableCategoriesWithTAbles has inconsistent casing (capital "A" in the middle), which makes the API surface harder to scan and search. Rename to standard lowerCamelCase (e.g., findTableCategoriesWithTables).
| async findTableCategoriesWithTAbles( | |
| async findTableCategoriesWithTables( |
| @ApiProperty({ type: String }) | ||
| category_id: string; |
There was a problem hiding this comment.
category_id is returned as null for the "All tables" category (see use case), but this response model declares it as non-nullable string. Update the DTO type to string | null and mark it nullable in the Swagger decorator.
| @ApiProperty({ type: String }) | |
| category_id: string; | |
| @ApiProperty({ type: String, nullable: true }) | |
| category_id: string | null; |
…es-with-tables.ro.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
No description provided.