|
| 1 | +import { |
| 2 | + Body, |
| 3 | + Controller, |
| 4 | + Delete, |
| 5 | + Get, |
| 6 | + HttpCode, |
| 7 | + HttpException, |
| 8 | + HttpStatus, |
| 9 | + Inject, |
| 10 | + Injectable, |
| 11 | + Post, |
| 12 | + Put, |
| 13 | + Query, |
| 14 | + UseGuards, |
| 15 | + UseInterceptors, |
| 16 | +} from '@nestjs/common'; |
| 17 | +import { ApiBearerAuth, ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; |
| 18 | +import { Throttle } from '@nestjs/throttler'; |
| 19 | +import { getDataAccessObject } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/create-data-access-object.js'; |
| 20 | +import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; |
| 21 | +import { BaseType, UseCaseType } from '../../../common/data-injection.tokens.js'; |
| 22 | +import { MasterPassword } from '../../../decorators/master-password.decorator.js'; |
| 23 | +import { QueryTableName } from '../../../decorators/query-table-name.decorator.js'; |
| 24 | +import { SlugUuid } from '../../../decorators/slug-uuid.decorator.js'; |
| 25 | +import { Timeout, TimeoutDefaults } from '../../../decorators/timeout.decorator.js'; |
| 26 | +import { UserId } from '../../../decorators/user-id.decorator.js'; |
| 27 | +import { InTransactionEnum } from '../../../enums/in-transaction.enum.js'; |
| 28 | +import { ConnectionNotFoundException } from '../../../exceptions/custom-exceptions/connection-not-found-exception.js'; |
| 29 | +import { Messages } from '../../../exceptions/text/messages.js'; |
| 30 | +import { TableAddGuard } from '../../../guards/table-add.guard.js'; |
| 31 | +import { TableDeleteGuard } from '../../../guards/table-delete.guard.js'; |
| 32 | +import { TableEditGuard } from '../../../guards/table-edit.guard.js'; |
| 33 | +import { TableReadGuard } from '../../../guards/table-read.guard.js'; |
| 34 | +import { isConnectionTypeAgent } from '../../../helpers/is-connection-entity-agent.js'; |
| 35 | +import { isObjectEmpty } from '../../../helpers/is-object-empty.js'; |
| 36 | +import { isObjectPropertyExists } from '../../../helpers/validators/is-object-property-exists-validator.js'; |
| 37 | +import { SentryInterceptor } from '../../../interceptors/sentry.interceptor.js'; |
| 38 | +import { FindAllRowsWithBodyFiltersDto } from '../dto/find-rows-with-body-filters.dto.js'; |
| 39 | +import { PureCreateRowDs } from './application/data-structures/pure-create-row.ds.js'; |
| 40 | +import { PureCrudRowResponseDs } from './application/data-structures/pure-crud-row-response.ds.js'; |
| 41 | +import { PureDeleteRowDs } from './application/data-structures/pure-delete-row.ds.js'; |
| 42 | +import { PureFoundRowsResponseDs } from './application/data-structures/pure-found-rows-response.ds.js'; |
| 43 | +import { PureGetRowsDs } from './application/data-structures/pure-get-rows.ds.js'; |
| 44 | +import { PureReadRowDs } from './application/data-structures/pure-read-row.ds.js'; |
| 45 | +import { PureUpdateRowDs } from './application/data-structures/pure-update-row.ds.js'; |
| 46 | +import { |
| 47 | + IPureCreateRowInTable, |
| 48 | + IPureDeleteRowFromTable, |
| 49 | + IPureGetRowsFromTable, |
| 50 | + IPureReadRowFromTable, |
| 51 | + IPureUpdateRowInTable, |
| 52 | +} from './use-cases/table-pure-crud-use-cases.interface.js'; |
| 53 | + |
| 54 | +@UseInterceptors(SentryInterceptor) |
| 55 | +@Timeout() |
| 56 | +@Controller() |
| 57 | +@ApiBearerAuth() |
| 58 | +@ApiTags('Table pure CRUD operations') |
| 59 | +@Injectable() |
| 60 | +export class TablePureCrudOperationsController { |
| 61 | + constructor( |
| 62 | + @Inject(UseCaseType.PURE_CREATE_ROW_IN_TABLE) |
| 63 | + private readonly pureCreateRowInTableUseCase: IPureCreateRowInTable, |
| 64 | + @Inject(UseCaseType.PURE_READ_ROW_FROM_TABLE) |
| 65 | + private readonly pureReadRowFromTableUseCase: IPureReadRowFromTable, |
| 66 | + @Inject(UseCaseType.PURE_UPDATE_ROW_IN_TABLE) |
| 67 | + private readonly pureUpdateRowInTableUseCase: IPureUpdateRowInTable, |
| 68 | + @Inject(UseCaseType.PURE_DELETE_ROW_FROM_TABLE) |
| 69 | + private readonly pureDeleteRowFromTableUseCase: IPureDeleteRowFromTable, |
| 70 | + @Inject(UseCaseType.PURE_GET_ROWS_FROM_TABLE) |
| 71 | + private readonly pureGetRowsFromTableUseCase: IPureGetRowsFromTable, |
| 72 | + @Inject(BaseType.GLOBAL_DB_CONTEXT) |
| 73 | + protected _dbContext: IGlobalDatabaseContext, |
| 74 | + ) {} |
| 75 | + |
| 76 | + @ApiOperation({ |
| 77 | + summary: 'Create a single row in a table. API+', |
| 78 | + description: 'Insert a new row and return only the created row. Support access with api key.', |
| 79 | + }) |
| 80 | + @ApiBody({ type: Object }) |
| 81 | + @ApiResponse({ status: 201, description: 'Row created.', type: PureCrudRowResponseDs }) |
| 82 | + @ApiQuery({ name: 'tableName', required: true }) |
| 83 | + @UseGuards(TableAddGuard) |
| 84 | + @Post('/table/crud/:connectionId') |
| 85 | + async createRow( |
| 86 | + @Body() body: Record<string, unknown>, |
| 87 | + @SlugUuid('connectionId') connectionId: string, |
| 88 | + @UserId() userId: string, |
| 89 | + @MasterPassword() masterPwd: string, |
| 90 | + @QueryTableName() tableName: string, |
| 91 | + ): Promise<PureCrudRowResponseDs> { |
| 92 | + if (!connectionId || isObjectEmpty(body)) { |
| 93 | + throw new HttpException({ message: Messages.PARAMETER_MISSING }, HttpStatus.BAD_REQUEST); |
| 94 | + } |
| 95 | + const inputData: PureCreateRowDs = { |
| 96 | + connectionId, |
| 97 | + masterPwd, |
| 98 | + row: body, |
| 99 | + tableName, |
| 100 | + userId, |
| 101 | + }; |
| 102 | + return await this.pureCreateRowInTableUseCase.execute(inputData, InTransactionEnum.OFF); |
| 103 | + } |
| 104 | + |
| 105 | + @ApiOperation({ |
| 106 | + summary: 'Get table rows with filter parameters in body. API+', |
| 107 | + description: |
| 108 | + 'Return only rows and pagination. Support search, filtering (in body), ordering and pagination. Support access with api key.', |
| 109 | + }) |
| 110 | + @ApiResponse({ status: 200, description: 'Rows found.', type: PureFoundRowsResponseDs }) |
| 111 | + @ApiBody({ type: FindAllRowsWithBodyFiltersDto }) |
| 112 | + @ApiQuery({ name: 'tableName', required: true }) |
| 113 | + @ApiQuery({ name: 'page', required: false }) |
| 114 | + @ApiQuery({ name: 'perPage', required: false }) |
| 115 | + @ApiQuery({ name: 'search', required: false }) |
| 116 | + @UseGuards(TableReadGuard) |
| 117 | + @Timeout(TimeoutDefaults.EXTENDED) |
| 118 | + @Throttle({ default: { limit: 300, ttl: 60000 } }) |
| 119 | + @HttpCode(HttpStatus.OK) |
| 120 | + @Post('/table/crud/rows/:connectionId') |
| 121 | + async getRows( |
| 122 | + @QueryTableName() tableName: string, |
| 123 | + @Query('page') page: string, |
| 124 | + @Query('perPage') perPage: string, |
| 125 | + @Query('search') searchingFieldValue: string, |
| 126 | + @Query() query: Record<string, string>, |
| 127 | + @SlugUuid('connectionId') connectionId: string, |
| 128 | + @UserId() userId: string, |
| 129 | + @MasterPassword() masterPwd: string, |
| 130 | + @Body() body: FindAllRowsWithBodyFiltersDto, |
| 131 | + ): Promise<PureFoundRowsResponseDs> { |
| 132 | + if (!connectionId) { |
| 133 | + throw new HttpException({ message: Messages.CONNECTION_ID_MISSING }, HttpStatus.BAD_REQUEST); |
| 134 | + } |
| 135 | + let parsedPage = 0; |
| 136 | + let parsedPerPage = 0; |
| 137 | + if (page && perPage) { |
| 138 | + parsedPage = parseInt(page, 10); |
| 139 | + parsedPerPage = parseInt(perPage, 10); |
| 140 | + if ((parsedPage && parsedPage <= 0) || (parsedPerPage && parsedPerPage <= 0)) { |
| 141 | + throw new HttpException({ message: Messages.PAGE_AND_PERPAGE_INVALID }, HttpStatus.BAD_REQUEST); |
| 142 | + } |
| 143 | + } |
| 144 | + const inputData: PureGetRowsDs = { |
| 145 | + connectionId, |
| 146 | + masterPwd, |
| 147 | + page: parsedPage, |
| 148 | + perPage: parsedPerPage, |
| 149 | + query, |
| 150 | + searchingFieldValue, |
| 151 | + tableName, |
| 152 | + userId, |
| 153 | + filters: body?.filters, |
| 154 | + }; |
| 155 | + return await this.pureGetRowsFromTableUseCase.execute(inputData, InTransactionEnum.OFF); |
| 156 | + } |
| 157 | + |
| 158 | + @ApiOperation({ |
| 159 | + summary: 'Read a single row from a table by primary key. API+', |
| 160 | + description: 'Return only the found row by primary key. Support access with api key.', |
| 161 | + }) |
| 162 | + @ApiResponse({ status: 200, description: 'Row found.', type: PureCrudRowResponseDs }) |
| 163 | + @ApiQuery({ name: 'tableName', required: true }) |
| 164 | + @UseGuards(TableReadGuard) |
| 165 | + @Get('/table/crud/:connectionId') |
| 166 | + async readRow( |
| 167 | + @Query() query: Record<string, string>, |
| 168 | + @MasterPassword() masterPwd: string, |
| 169 | + @SlugUuid('connectionId') connectionId: string, |
| 170 | + @UserId() userId: string, |
| 171 | + @QueryTableName() tableName: string, |
| 172 | + ): Promise<PureCrudRowResponseDs> { |
| 173 | + const primaryKey = await this.extractPrimaryKeyFromQuery(userId, connectionId, tableName, query, masterPwd); |
| 174 | + if (!connectionId || isObjectEmpty(primaryKey)) { |
| 175 | + throw new HttpException({ message: Messages.PARAMETER_MISSING }, HttpStatus.BAD_REQUEST); |
| 176 | + } |
| 177 | + const inputData: PureReadRowDs = { |
| 178 | + connectionId, |
| 179 | + masterPwd, |
| 180 | + primaryKey, |
| 181 | + tableName, |
| 182 | + userId, |
| 183 | + }; |
| 184 | + return await this.pureReadRowFromTableUseCase.execute(inputData, InTransactionEnum.OFF); |
| 185 | + } |
| 186 | + |
| 187 | + @ApiOperation({ |
| 188 | + summary: 'Update a single row in a table by primary key. API+', |
| 189 | + description: 'Update a row by primary key and return only the updated row. Support access with api key.', |
| 190 | + }) |
| 191 | + @ApiBody({ type: Object }) |
| 192 | + @ApiResponse({ status: 200, description: 'Row updated.', type: PureCrudRowResponseDs }) |
| 193 | + @ApiQuery({ name: 'tableName', required: true }) |
| 194 | + @UseGuards(TableEditGuard) |
| 195 | + @Put('/table/crud/:connectionId') |
| 196 | + async updateRow( |
| 197 | + @Body() body: Record<string, unknown>, |
| 198 | + @Query() query: Record<string, string>, |
| 199 | + @MasterPassword() masterPwd: string, |
| 200 | + @SlugUuid('connectionId') connectionId: string, |
| 201 | + @UserId() userId: string, |
| 202 | + @QueryTableName() tableName: string, |
| 203 | + ): Promise<PureCrudRowResponseDs> { |
| 204 | + if (!connectionId || isObjectEmpty(body)) { |
| 205 | + throw new HttpException({ message: Messages.PARAMETER_MISSING }, HttpStatus.BAD_REQUEST); |
| 206 | + } |
| 207 | + const primaryKey = await this.extractPrimaryKeyFromQuery(userId, connectionId, tableName, query, masterPwd); |
| 208 | + if (isObjectEmpty(primaryKey)) { |
| 209 | + throw new HttpException({ message: Messages.PARAMETER_MISSING }, HttpStatus.BAD_REQUEST); |
| 210 | + } |
| 211 | + const inputData: PureUpdateRowDs = { |
| 212 | + connectionId, |
| 213 | + masterPwd, |
| 214 | + primaryKey, |
| 215 | + row: body, |
| 216 | + tableName, |
| 217 | + userId, |
| 218 | + }; |
| 219 | + return await this.pureUpdateRowInTableUseCase.execute(inputData, InTransactionEnum.OFF); |
| 220 | + } |
| 221 | + |
| 222 | + @ApiOperation({ |
| 223 | + summary: 'Delete a single row from a table by primary key. API+', |
| 224 | + description: 'Delete a row by primary key and return only the deleted row. Support access with api key.', |
| 225 | + }) |
| 226 | + @ApiResponse({ status: 200, description: 'Row deleted.', type: PureCrudRowResponseDs }) |
| 227 | + @ApiQuery({ name: 'tableName', required: true }) |
| 228 | + @UseGuards(TableDeleteGuard) |
| 229 | + @Delete('/table/crud/:connectionId') |
| 230 | + async deleteRow( |
| 231 | + @Query() query: Record<string, string>, |
| 232 | + @MasterPassword() masterPwd: string, |
| 233 | + @SlugUuid('connectionId') connectionId: string, |
| 234 | + @UserId() userId: string, |
| 235 | + @QueryTableName() tableName: string, |
| 236 | + ): Promise<PureCrudRowResponseDs> { |
| 237 | + const primaryKey = await this.extractPrimaryKeyFromQuery(userId, connectionId, tableName, query, masterPwd); |
| 238 | + if (!connectionId || isObjectEmpty(primaryKey)) { |
| 239 | + throw new HttpException({ message: Messages.PARAMETER_MISSING }, HttpStatus.BAD_REQUEST); |
| 240 | + } |
| 241 | + const inputData: PureDeleteRowDs = { |
| 242 | + connectionId, |
| 243 | + masterPwd, |
| 244 | + primaryKey, |
| 245 | + tableName, |
| 246 | + userId, |
| 247 | + }; |
| 248 | + return await this.pureDeleteRowFromTableUseCase.execute(inputData, InTransactionEnum.OFF); |
| 249 | + } |
| 250 | + |
| 251 | + private async extractPrimaryKeyFromQuery( |
| 252 | + userId: string, |
| 253 | + connectionId: string, |
| 254 | + tableName: string, |
| 255 | + query: Record<string, string>, |
| 256 | + masterPwd: string, |
| 257 | + ): Promise<Record<string, unknown>> { |
| 258 | + const connection = await this._dbContext.connectionRepository.findAndDecryptConnection(connectionId, masterPwd); |
| 259 | + if (!connection) { |
| 260 | + throw new ConnectionNotFoundException(HttpStatus.BAD_REQUEST); |
| 261 | + } |
| 262 | + let userEmail = ''; |
| 263 | + if (isConnectionTypeAgent(connection.type)) { |
| 264 | + userEmail = (await this._dbContext.userRepository.getUserEmailOrReturnNull(userId)) ?? ''; |
| 265 | + } |
| 266 | + const dao = getDataAccessObject(connection); |
| 267 | + |
| 268 | + const tablesInConnection = await dao.getTablesFromDB(userEmail); |
| 269 | + const tableNames = tablesInConnection.map((table) => table.tableName); |
| 270 | + if (!tableNames.includes(tableName)) { |
| 271 | + throw new HttpException({ message: Messages.TABLE_NOT_FOUND }, HttpStatus.BAD_REQUEST); |
| 272 | + } |
| 273 | + |
| 274 | + const primaryColumns = await dao.getTablePrimaryColumns(tableName, userEmail); |
| 275 | + const primaryKey: Record<string, unknown> = {}; |
| 276 | + for (const primaryColumn of primaryColumns) { |
| 277 | + if (isObjectPropertyExists(primaryColumn, 'column_name')) { |
| 278 | + primaryKey[primaryColumn.column_name] = query[primaryColumn.column_name]; |
| 279 | + } |
| 280 | + } |
| 281 | + return primaryKey; |
| 282 | + } |
| 283 | +} |
0 commit comments