Skip to content

Commit 19b3eac

Browse files
Merge branch 'main' into add-related-record
2 parents a026e35 + 869e97a commit 19b3eac

18 files changed

Lines changed: 1334 additions & 0 deletions

backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { PermissionModule } from './entities/permission/permission.module.js';
2626
import { S3WidgetModule } from './entities/s3-widget/s3-widget.module.js';
2727
import { SharedJobsModule } from './entities/shared-jobs/shared-jobs.module.js';
2828
import { TableModule } from './entities/table/table.module.js';
29+
import { TablePureCrudOperationsModule } from './entities/table/table-pure-crud-operations/table-pure-crud-operations.module.js';
2930
import { TableTriggersModule } from './entities/table-actions/table-action-rules-module/action-rules.module.js';
3031
import { TableActionModule } from './entities/table-actions/table-actions-module/table-action.module.js';
3132
import { TableCategoriesModule } from './entities/table-categories/table-categories.module.js';
@@ -73,6 +74,7 @@ import { GetHelloUseCase } from './use-cases-app/get-hello.use.case.js';
7374
PermissionModule,
7475
TableLogsModule,
7576
TableModule,
77+
TablePureCrudOperationsModule,
7678
TableSettingsModule,
7779
TableWidgetModule,
7880
UserModule,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ export enum UseCaseType {
103103
EXPORT_CSV_FROM_TABLE = 'EXPORT_CSV_FROM_TABLE',
104104
IMPORT_CSV_TO_TABLE = 'IMPORT_CSV_TO_TABLE',
105105

106+
PURE_CREATE_ROW_IN_TABLE = 'PURE_CREATE_ROW_IN_TABLE',
107+
PURE_READ_ROW_FROM_TABLE = 'PURE_READ_ROW_FROM_TABLE',
108+
PURE_UPDATE_ROW_IN_TABLE = 'PURE_UPDATE_ROW_IN_TABLE',
109+
PURE_DELETE_ROW_FROM_TABLE = 'PURE_DELETE_ROW_FROM_TABLE',
110+
PURE_GET_ROWS_FROM_TABLE = 'PURE_GET_ROWS_FROM_TABLE',
111+
106112
SAAS_COMPANY_REGISTRATION = 'SAAS_COMPANY_REGISTRATION',
107113
SAAS_GET_USER_INFO = 'SAAS_GET_USER_INFO',
108114
SAAS_USUAL_REGISTER_USER = 'SAAS_USUAL_REGISTER_USER',
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class PureCreateRowDs {
2+
connectionId: string;
3+
masterPwd: string;
4+
row: Record<string, unknown>;
5+
tableName: string;
6+
userId: string;
7+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
3+
export class PureCrudRowResponseDs {
4+
@ApiProperty({ type: Object })
5+
row: Record<string, unknown>;
6+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class PureDeleteRowDs {
2+
connectionId: string;
3+
masterPwd: string;
4+
primaryKey: Record<string, unknown>;
5+
tableName: string;
6+
userId: string;
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { RowsPaginationDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/rows-pagination.ds.js';
3+
4+
export class PureFoundRowsResponseDs {
5+
@ApiProperty({ isArray: true, type: Object })
6+
rows: Array<Record<string, unknown>>;
7+
8+
@ApiProperty({ type: RowsPaginationDS })
9+
pagination: RowsPaginationDS;
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export class PureGetRowsDs {
2+
connectionId: string;
3+
masterPwd: string;
4+
page: number;
5+
perPage: number;
6+
query: Record<string, unknown>;
7+
searchingFieldValue: string;
8+
tableName: string;
9+
userId: string;
10+
filters?: Record<string, unknown>;
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class PureReadRowDs {
2+
connectionId: string;
3+
masterPwd: string;
4+
primaryKey: Record<string, unknown>;
5+
tableName: string;
6+
userId: string;
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class PureUpdateRowDs {
2+
connectionId: string;
3+
masterPwd: string;
4+
primaryKey: Record<string, unknown>;
5+
row: Record<string, unknown>;
6+
tableName: string;
7+
userId: string;
8+
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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

Comments
 (0)