Skip to content

Commit bb7db07

Browse files
authored
Merge pull request #1764 from rocket-admin/backend_uncashed_table_structure
feat: add GET_TABLE_STRUCTURE_WITHOUT_CACHE use case and endpoint
2 parents 398561e + 71b111e commit bb7db07

21 files changed

Lines changed: 632 additions & 14 deletions

backend/src/common/data-injection.tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export enum UseCaseType {
9292
FIND_TABLES_IN_CONNECTION_V2 = 'FIND_TABLES_IN_CONNECTION_V2',
9393
GET_ALL_TABLE_ROWS = 'GET_ALL_TABLE_ROWS',
9494
GET_TABLE_STRUCTURE = 'GET_TABLE_STRUCTURE',
95+
GET_TABLE_STRUCTURE_WITHOUT_CACHE = 'GET_TABLE_STRUCTURE_WITHOUT_CACHE',
9596
ADD_ROW_IN_TABLE = 'ADD_ROW_IN_TABLE',
9697
UPDATE_ROW_IN_TABLE = 'UPDATE_ROW_IN_TABLE',
9798
BULK_UPDATE_ROWS_IN_TABLE = 'BULK_UPDATE_ROWS_IN_TABLE',

backend/src/entities/table/table.controller.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
IGetRowByPrimaryKey,
7070
IGetTableRows,
7171
IGetTableStructure,
72+
IGetTableStructureWithoutCache,
7273
IImportCSVFinTable,
7374
IUpdateRowInTable,
7475
} from './use-cases/table-use-cases.interface.js';
@@ -90,6 +91,8 @@ export class TableController {
9091
private readonly getTableRowsUseCase: IGetTableRows,
9192
@Inject(UseCaseType.GET_TABLE_STRUCTURE)
9293
private readonly getTableStructureUseCase: IGetTableStructure,
94+
@Inject(UseCaseType.GET_TABLE_STRUCTURE_WITHOUT_CACHE)
95+
private readonly getTableStructureWithoutCacheUseCase: IGetTableStructureWithoutCache,
9396
@Inject(UseCaseType.ADD_ROW_IN_TABLE)
9497
private readonly addRowInTableUseCase: IAddRowInTable,
9598
@Inject(UseCaseType.UPDATE_ROW_IN_TABLE)
@@ -344,6 +347,42 @@ export class TableController {
344347
return await this.getTableStructureUseCase.execute(inputData, InTransactionEnum.OFF);
345348
}
346349

350+
@ApiOperation({
351+
summary: 'Get table structure without cache. API+',
352+
description:
353+
'Return table structure fetched directly from the database, bypassing the structure cache. Support access with api key.',
354+
})
355+
@ApiResponse({
356+
status: 200,
357+
description: 'Table structure found.',
358+
type: TableStructureDs,
359+
})
360+
@ApiQuery({ name: 'tableName', required: true })
361+
@UseGuards(TableReadGuard)
362+
@Get('/table/structure/no-cache/:connectionId')
363+
async getTableStructureWithoutCache(
364+
@QueryTableName() tableName: string,
365+
@UserId() userId: string,
366+
@SlugUuid('connectionId') connectionId: string,
367+
@MasterPassword() masterPwd: string,
368+
): Promise<TableStructureDs> {
369+
if (!connectionId) {
370+
throw new HttpException(
371+
{
372+
message: Messages.CONNECTION_ID_MISSING,
373+
},
374+
HttpStatus.BAD_REQUEST,
375+
);
376+
}
377+
const inputData: GetTableStructureDs = {
378+
connectionId: connectionId,
379+
masterPwd: masterPwd,
380+
tableName: tableName,
381+
userId: userId,
382+
};
383+
return await this.getTableStructureWithoutCacheUseCase.execute(inputData, InTransactionEnum.OFF);
384+
}
385+
347386
@ApiOperation({
348387
summary: 'Add new row in table. API+',
349388
description: 'Add new row in table. Support access with api key.',

backend/src/entities/table/table.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { FindTablesInConnectionV2UseCase } from './use-cases/find-tables-in-conn
2626
import { GetRowByPrimaryKeyUseCase } from './use-cases/get-row-by-primary-key.use.case.js';
2727
import { GetTableRowsUseCase } from './use-cases/get-table-rows.use.case.js';
2828
import { GetTableStructureUseCase } from './use-cases/get-table-structure.use.case.js';
29+
import { GetTableStructureWithoutCacheUseCase } from './use-cases/get-table-structure-without-cache.use.case.js';
2930
import { ImportCSVInTableUseCase } from './use-cases/import-csv-in-table-user.case.js';
3031
import { UpdateRowInTableUseCase } from './use-cases/update-row-in-table.use.case.js';
3132

@@ -67,6 +68,10 @@ import { UpdateRowInTableUseCase } from './use-cases/update-row-in-table.use.cas
6768
provide: UseCaseType.GET_TABLE_STRUCTURE,
6869
useClass: GetTableStructureUseCase,
6970
},
71+
{
72+
provide: UseCaseType.GET_TABLE_STRUCTURE_WITHOUT_CACHE,
73+
useClass: GetTableStructureWithoutCacheUseCase,
74+
},
7075
{
7176
provide: UseCaseType.ADD_ROW_IN_TABLE,
7277
useClass: AddRowInTableUseCase,
@@ -115,6 +120,7 @@ export class TableModule {
115120
{ path: '/table/rows/:connectionId', method: RequestMethod.GET },
116121
{ path: '/table/rows/find/:connectionId', method: RequestMethod.POST },
117122
{ path: '/table/structure/:connectionId', method: RequestMethod.GET },
123+
{ path: '/table/structure/no-cache/:connectionId', method: RequestMethod.GET },
118124
{ path: '/table/row/:connectionId', method: RequestMethod.POST },
119125
{ path: '/table/row/:connectionId', method: RequestMethod.PUT },
120126
{ path: '/table/row/:connectionId', method: RequestMethod.DELETE },
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
2+
import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js';
3+
import { ForeignKeyWithAutocompleteColumnsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/foreign-key-with-autocomplete-columns.ds.js';
4+
import AbstractUseCase from '../../../common/abstract-use.case.js';
5+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
6+
import { BaseType } from '../../../common/data-injection.tokens.js';
7+
import { ExceptionOperations } from '../../../exceptions/custom-exceptions/exception-operation.js';
8+
import { UnknownSQLException } from '../../../exceptions/custom-exceptions/unknown-sql-exception.js';
9+
import { Messages } from '../../../exceptions/text/messages.js';
10+
import { CedarPermissionsService } from '../../cedar-authorization/cedar-permissions.service.js';
11+
import { buildFoundTableWidgetDs } from '../../widget/utils/build-found-table-widget-ds.js';
12+
import { GetTableStructureDs } from '../application/data-structures/get-table-structure-ds.js';
13+
import { TableStructureDs } from '../table-datastructures.js';
14+
import { attachForeignColumnNames } from '../utils/attach-foreign-column-names.util.js';
15+
import { extractForeignKeysFromWidgets } from '../utils/extract-foreign-keys-from-widgets.util.js';
16+
import { filterForeignKeysByReadPermission } from '../utils/filter-foreign-keys-by-permission.util.js';
17+
import { formFullTableStructure } from '../utils/form-full-table-structure.js';
18+
import { getUserEmailForAgent, validateConnection } from '../utils/validate-connection.util.js';
19+
import { IGetTableStructureWithoutCache } from './table-use-cases.interface.js';
20+
21+
@Injectable()
22+
export class GetTableStructureWithoutCacheUseCase
23+
extends AbstractUseCase<GetTableStructureDs, TableStructureDs>
24+
implements IGetTableStructureWithoutCache
25+
{
26+
constructor(
27+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
28+
protected _dbContext: IGlobalDatabaseContext,
29+
private readonly cedarPermissions: CedarPermissionsService,
30+
) {
31+
super();
32+
}
33+
34+
protected async implementation(inputData: GetTableStructureDs): Promise<TableStructureDs> {
35+
const { connectionId, masterPwd, tableName, userId } = inputData;
36+
const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
37+
connectionId,
38+
masterPwd,
39+
);
40+
validateConnection(foundConnection);
41+
42+
try {
43+
const dao = getDataAccessObject(foundConnection);
44+
const foundTalesInConnection = await dao.getTablesFromDB();
45+
if (!foundTalesInConnection.find((el) => el.tableName === tableName)) {
46+
throw new HttpException(
47+
{
48+
message: Messages.TABLE_NOT_FOUND,
49+
},
50+
HttpStatus.BAD_REQUEST,
51+
);
52+
}
53+
const userEmail = await getUserEmailForAgent(foundConnection, userId, this._dbContext.userRepository);
54+
55+
// eslint-disable-next-line prefer-const
56+
let [tableSettings, personalTableSettings, tablePrimaryColumns, tableForeignKeys, tableStructure, tableWidgets] =
57+
await Promise.all([
58+
this._dbContext.tableSettingsRepository.findTableSettings(connectionId, tableName),
59+
this._dbContext.personalTableSettingsRepository.findUserTableSettings(userId, connectionId, tableName),
60+
dao.getTablePrimaryColumns(tableName, userEmail),
61+
dao.getTableForeignKeys(tableName, userEmail),
62+
dao.getTableStructureWithoutCache(tableName, userEmail),
63+
this._dbContext.tableWidgetsRepository.findTableWidgets(connectionId, tableName),
64+
]);
65+
const foreignKeysFromWidgets = extractForeignKeysFromWidgets(tableWidgets);
66+
67+
tableForeignKeys = tableForeignKeys.concat(foreignKeysFromWidgets);
68+
let transformedTableForeignKeys: Array<ForeignKeyWithAutocompleteColumnsDS> = [];
69+
tableForeignKeys = await filterForeignKeysByReadPermission(
70+
tableForeignKeys,
71+
userId,
72+
connectionId,
73+
masterPwd,
74+
this.cedarPermissions,
75+
);
76+
77+
if (tableForeignKeys && tableForeignKeys.length > 0) {
78+
transformedTableForeignKeys = await Promise.all(
79+
tableForeignKeys.map((el) =>
80+
attachForeignColumnNames(
81+
el,
82+
userEmail,
83+
connectionId,
84+
dao,
85+
this._dbContext.tableSettingsRepository.findTableSettings.bind(this._dbContext.tableSettingsRepository),
86+
).catch(() => el as ForeignKeyWithAutocompleteColumnsDS),
87+
),
88+
);
89+
}
90+
const readonly_fields = tableSettings?.readonly_fields?.length > 0 ? tableSettings.readonly_fields : [];
91+
const formedTableStructure = formFullTableStructure(tableStructure, tableSettings);
92+
return {
93+
structure: formedTableStructure,
94+
primaryColumns: tablePrimaryColumns,
95+
foreignKeys: transformedTableForeignKeys,
96+
readonly_fields: readonly_fields,
97+
table_widgets: tableWidgets?.length > 0 ? tableWidgets.map((widget) => buildFoundTableWidgetDs(widget)) : [],
98+
list_fields: personalTableSettings?.list_fields ? personalTableSettings.list_fields : [],
99+
display_name: tableSettings?.display_name ? tableSettings.display_name : null,
100+
excluded_fields: tableSettings?.excluded_fields ? tableSettings.excluded_fields : [],
101+
};
102+
} catch (e) {
103+
if (e instanceof HttpException) {
104+
throw e;
105+
}
106+
throw new UnknownSQLException(e.message, ExceptionOperations.FAILED_TO_GET_TABLE_STRUCTURE);
107+
}
108+
}
109+
}

backend/src/entities/table/use-cases/table-use-cases.interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export interface IGetTableStructure {
3434
execute(inputData: GetTableStructureDs, inTransaction: InTransactionEnum): Promise<TableStructureDs>;
3535
}
3636

37+
export interface IGetTableStructureWithoutCache {
38+
execute(inputData: GetTableStructureDs, inTransaction: InTransactionEnum): Promise<TableStructureDs>;
39+
}
40+
3741
export interface IAddRowInTable {
3842
execute(inputData: AddRowInTableDs, inTransaction: InTransactionEnum): Promise<TableRowRODs>;
3943
}

0 commit comments

Comments
 (0)