diff --git a/backend/src/entities/table/application/data-structures/delete-row-from-table.ds.ts b/backend/src/entities/table/application/data-structures/delete-row-from-table.ds.ts index 367e9c533..211ef188d 100644 --- a/backend/src/entities/table/application/data-structures/delete-row-from-table.ds.ts +++ b/backend/src/entities/table/application/data-structures/delete-row-from-table.ds.ts @@ -4,6 +4,7 @@ export class DeleteRowFromTableDs { primaryKey: Record; tableName: string; userId: string; + uncached?: boolean; } export class DeleteRowsFromTableDs { diff --git a/backend/src/entities/table/application/data-structures/get-row-by-primary-key.ds.ts b/backend/src/entities/table/application/data-structures/get-row-by-primary-key.ds.ts index 9e9064b1a..79f275f8c 100644 --- a/backend/src/entities/table/application/data-structures/get-row-by-primary-key.ds.ts +++ b/backend/src/entities/table/application/data-structures/get-row-by-primary-key.ds.ts @@ -4,4 +4,5 @@ export class GetRowByPrimaryKeyDs { primaryKey: Record; tableName: string; userId: string; + uncached?: boolean; } diff --git a/backend/src/entities/table/application/data-structures/update-row-in-table.ds.ts b/backend/src/entities/table/application/data-structures/update-row-in-table.ds.ts index 05538f036..2d94f4e58 100644 --- a/backend/src/entities/table/application/data-structures/update-row-in-table.ds.ts +++ b/backend/src/entities/table/application/data-structures/update-row-in-table.ds.ts @@ -2,4 +2,5 @@ import { AddRowInTableDs } from './add-row-in-table.ds.js'; export class UpdateRowInTableDs extends AddRowInTableDs { primaryKey: Record; + uncached?: boolean; } diff --git a/backend/src/entities/table/table.controller.ts b/backend/src/entities/table/table.controller.ts index 8b3865a2a..1748502b7 100644 --- a/backend/src/entities/table/table.controller.ts +++ b/backend/src/entities/table/table.controller.ts @@ -261,10 +261,11 @@ export class TableController { @ApiQuery({ name: 'perPage', required: false }) @ApiQuery({ name: 'search', required: false }) @ApiQuery({ - name: 'uncached', + name: '_uncached', required: false, type: Boolean, - description: 'Invalidate table metadata cache before reading rows', + description: + 'Invalidate table metadata cache before reading rows. Underscore prefix avoids column-name collisions.', }) @UseGuards(TableReadGuard) @Timeout(TimeoutDefaults.EXTENDED) @@ -276,7 +277,7 @@ export class TableController { @Query('page') page: string, @Query('perPage') perPage: string, @Query('search') searchingFieldValue: string, - @Query('uncached') uncached: string, + @Query('_uncached') uncachedFlag: string, @Query() query: Record, @SlugUuid('connectionId') connectionId: string, @UserId() userId: string, @@ -310,12 +311,12 @@ export class TableController { masterPwd: masterPwd, page: parsedPage, perPage: parsedPerPage, - query: query, + query: this.stripReservedQueryParams(query), searchingFieldValue: searchingFieldValue, tableName: tableName, userId: userId, filters: body?.filters, - uncached: uncached === 'true', + uncached: uncachedFlag === 'true', }; return await this.getTableRowsUseCase.execute(inputData, InTransactionEnum.OFF); } @@ -439,11 +440,18 @@ export class TableController { type: TableRowRODs, }) @ApiQuery({ name: 'tableName', required: true }) + @ApiQuery({ + name: '_uncached', + required: false, + type: Boolean, + description: 'Invalidate table metadata cache before reading. Underscore prefix avoids column-name collisions.', + }) @UseGuards(TableEditGuard) @Put('/table/row/:connectionId') async updateRowInTable( @Body() body: Record, @Query() query: Record, + @Query('_uncached') uncachedFlag: string, @UserId() userId: string, @MasterPassword() masterPwd: string, @SlugUuid('connectionId') connectionId: string, @@ -457,7 +465,16 @@ export class TableController { HttpStatus.BAD_REQUEST, ); } - const primaryKeys = await this.getPrimaryKeys(userId, connectionId, tableName, query, masterPwd); + const uncached = uncachedFlag === 'true'; + const primaryKeyQuery = this.stripReservedQueryParams(query); + const primaryKeys = await this.getPrimaryKeys( + userId, + connectionId, + tableName, + primaryKeyQuery, + masterPwd, + uncached, + ); const propertiesArray = primaryKeys.map((el) => { return Object.entries(el)[0]; }); @@ -470,6 +487,7 @@ export class TableController { row: body, tableName: tableName, userId: userId, + uncached: uncached, }; return await this.updateRowInTableUseCase.execute(inputData, InTransactionEnum.OFF); } @@ -484,16 +502,32 @@ export class TableController { type: DeletedRowFromTableDs, }) @ApiQuery({ name: 'tableName', required: true }) + @ApiQuery({ + name: '_uncached', + required: false, + type: Boolean, + description: 'Invalidate table metadata cache before reading. Underscore prefix avoids column-name collisions.', + }) @UseGuards(TableDeleteGuard) @Delete('/table/row/:connectionId') async deleteRowInTable( @Query() query: Record, + @Query('_uncached') uncachedFlag: string, @MasterPassword() masterPwd: string, @SlugUuid('connectionId') connectionId: string, @UserId() userId: string, @QueryTableName() tableName: string, ): Promise { - const primaryKeys = await this.getPrimaryKeys(userId, connectionId, tableName, query, masterPwd); + const uncached = uncachedFlag === 'true'; + const primaryKeyQuery = this.stripReservedQueryParams(query); + const primaryKeys = await this.getPrimaryKeys( + userId, + connectionId, + tableName, + primaryKeyQuery, + masterPwd, + uncached, + ); const propertiesArray = primaryKeys.map((el) => { return Object.entries(el)[0]; }); @@ -513,6 +547,7 @@ export class TableController { primaryKey: primaryKey, tableName: tableName, userId: userId, + uncached: uncached, }; return await this.deleteRowFromTableUseCase.execute(inputData, InTransactionEnum.OFF); } @@ -614,16 +649,32 @@ export class TableController { type: TableRowRODs, }) @ApiQuery({ name: 'tableName', required: true }) + @ApiQuery({ + name: '_uncached', + required: false, + type: Boolean, + description: 'Invalidate table metadata cache before reading. Underscore prefix avoids column-name collisions.', + }) @UseGuards(TableReadGuard) @Get('/table/row/:connectionId') async getRowByPrimaryKey( @Query() query: Record, + @Query('_uncached') uncachedFlag: string, @MasterPassword() masterPwd: string, @SlugUuid('connectionId') connectionId: string, @UserId() userId: string, @QueryTableName() tableName: string, ): Promise { - const primaryKeys = await this.getPrimaryKeys(userId, connectionId, tableName, query, masterPwd); + const uncached = uncachedFlag === 'true'; + const primaryKeyQuery = this.stripReservedQueryParams(query); + const primaryKeys = await this.getPrimaryKeys( + userId, + connectionId, + tableName, + primaryKeyQuery, + masterPwd, + uncached, + ); const propertiesArray = primaryKeys.map((el) => { return Object.entries(el)[0]; @@ -644,6 +695,7 @@ export class TableController { primaryKey: primaryKey, tableName: tableName, userId: userId, + uncached: uncached, }; return await this.getRowByPrimaryKeyUseCase.execute(inputData, InTransactionEnum.OFF); } finally { @@ -767,12 +819,18 @@ export class TableController { }; } + private stripReservedQueryParams(query: Record): Record { + const { _uncached: _uncachedReserved, ...rest } = query ?? {}; + return rest; + } + private async getPrimaryKeys( userId: string, connectionId: string, tableName: string, query: Record, masterPwd: string, + uncached = false, ): Promise>> { const primaryKeys = []; const connection = await this._dbContext.connectionRepository.findAndDecryptConnection(connectionId, masterPwd); @@ -781,6 +839,9 @@ export class TableController { userEmail = await this._dbContext.userRepository.getUserEmailOrReturnNull(userId); } const dao = getDataAccessObject(connection); + if (uncached) { + dao.invalidateMetadataCache(); + } const tablesInConnection = await dao.getTablesFromDB(userEmail); const tableNames = tablesInConnection.map((table) => table.tableName); diff --git a/backend/src/entities/table/use-cases/delete-row-from-table.use.case.ts b/backend/src/entities/table/use-cases/delete-row-from-table.use.case.ts index a538e46fe..4436bed23 100644 --- a/backend/src/entities/table/use-cases/delete-row-from-table.use.case.ts +++ b/backend/src/entities/table/use-cases/delete-row-from-table.use.case.ts @@ -41,6 +41,7 @@ export class DeleteRowFromTableUseCase protected async implementation(inputData: DeleteRowFromTableDs): Promise { // eslint-disable-next-line prefer-const let { connectionId, masterPwd, primaryKey, tableName, userId } = inputData; + const { uncached } = inputData; let operationResult = OperationResultStatusEnum.unknown; if (!primaryKey) { @@ -56,6 +57,9 @@ export class DeleteRowFromTableUseCase validateConnection(connection); const dao = getDataAccessObject(connection); + if (uncached) { + dao.invalidateMetadataCache(); + } const userEmail = await getUserEmailForAgent(connection, userId, this._dbContext.userRepository); const isView = await dao.isView(tableName, userEmail); diff --git a/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts b/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts index 0aca9faf4..23befe8cc 100644 --- a/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts +++ b/backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts @@ -45,6 +45,7 @@ export class GetRowByPrimaryKeyUseCase protected async implementation(inputData: GetRowByPrimaryKeyDs): Promise { let { connectionId, masterPwd, primaryKey, tableName, userId } = inputData; + const { uncached } = inputData; if (!primaryKey) { throw new HttpException( { @@ -62,6 +63,9 @@ export class GetRowByPrimaryKeyUseCase const userEmail = await getUserEmailForAgent(connection, userId, this._dbContext.userRepository); await validateSchemaCache(dao, userEmail); + if (uncached) { + dao.invalidateMetadataCache(); + } let [ tableStructure, diff --git a/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts b/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts index 9490ac457..0d625714b 100644 --- a/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts +++ b/backend/src/entities/table/use-cases/update-row-in-table.use.case.ts @@ -57,6 +57,7 @@ export class UpdateRowInTableUseCase protected async implementation(inputData: UpdateRowInTableDs): Promise { let { connectionId, masterPwd, primaryKey, row, tableName, userId } = inputData; + const { uncached } = inputData; let operationResult = OperationResultStatusEnum.unknown; const errors = []; @@ -68,6 +69,9 @@ export class UpdateRowInTableUseCase validateConnection(connection); const dao = getDataAccessObject(connection); + if (uncached) { + dao.invalidateMetadataCache(); + } const userEmail = await getUserEmailForAgent(connection, userId, this._dbContext.userRepository); const isView = await dao.isView(tableName, userEmail);