Skip to content

Commit 909bb77

Browse files
authored
Merge branch 'main' into feature/frontend-charts
2 parents ce1881f + a179bb2 commit 909bb77

26 files changed

Lines changed: 1748 additions & 696 deletions

backend/src/entities/agent/repository/custom-agent-repository-extension.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import { AgentEntity } from '../agent.entity.js';
44
import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
55

66
export const customAgentRepositoryExtension = {
7+
async saveNewAgent(agent: AgentEntity): Promise<AgentEntity> {
8+
return await this.save(agent);
9+
},
10+
711
async createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise<string> {
812
const newAgent = await this.createNewAgentForConnection(connection);
913
return newAgent.token;
@@ -59,14 +63,8 @@ export const customAgentRepositoryExtension = {
5963
return 'IBMDB2-TEST-AGENT-TOKEN';
6064
case ConnectionTypeTestEnum.agent_mongodb:
6165
return 'MONGODB-TEST-AGENT-TOKEN';
62-
case ConnectionTypeTestEnum.elasticsearch:
63-
return 'ELASTICSEARCH-TEST-AGENT-TOKEN';
64-
case ConnectionTypeTestEnum.agent_cassandra:
65-
return 'CASSANDRA-TEST-AGENT-TOKEN';
66-
case ConnectionTypeTestEnum.agent_redis:
67-
return 'REDIS-TEST-AGENT-TOKEN';
68-
case ConnectionTypeTestEnum.agent_clickhouse:
69-
return 'CLICKHOUSE-TEST-AGENT-TOKEN';
66+
default:
67+
throw new Error(`Unsupported connection type for test agent token: ${connectionType}`);
7068
}
7169
},
7270
};

backend/src/entities/s3-widget/application/data-structures/s3-operation.ds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export class S3UploadUrlResponseDs {
2727
uploadUrl: string;
2828
key: string;
2929
expiresIn: number;
30+
previewUrl: string;
3031
}
Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,66 @@
1-
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
1+
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
22
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
33
import { Injectable } from '@nestjs/common';
4+
import { nanoid } from 'nanoid';
45

56
@Injectable()
67
export class S3HelperService {
7-
public createS3Client(accessKeyId: string, secretAccessKey: string, region: string = 'us-east-1'): S3Client {
8-
return new S3Client({
9-
region,
10-
credentials: {
11-
accessKeyId,
12-
secretAccessKey,
13-
},
14-
});
15-
}
8+
public createS3Client(accessKeyId: string, secretAccessKey: string, region: string = 'us-east-1'): S3Client {
9+
return new S3Client({
10+
region,
11+
credentials: {
12+
accessKeyId,
13+
secretAccessKey,
14+
},
15+
});
16+
}
1617

17-
public async getSignedGetUrl(
18-
client: S3Client,
19-
bucket: string,
20-
key: string,
21-
expiresIn: number = 3600,
22-
): Promise<string> {
23-
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
24-
return getSignedUrl(client, command, { expiresIn });
25-
}
18+
public async getSignedGetUrl(
19+
client: S3Client,
20+
bucket: string,
21+
key: string,
22+
expiresIn: number = 3600,
23+
): Promise<string> {
24+
const command = new GetObjectCommand({ Bucket: bucket, Key: key });
25+
return getSignedUrl(client, command, { expiresIn });
26+
}
2627

27-
public async getSignedPutUrl(
28-
client: S3Client,
29-
bucket: string,
30-
key: string,
31-
contentType: string,
32-
expiresIn: number = 3600,
33-
): Promise<string> {
34-
const command = new PutObjectCommand({
35-
Bucket: bucket,
36-
Key: key,
37-
ContentType: contentType,
38-
});
39-
return getSignedUrl(client, command, { expiresIn });
40-
}
28+
public async getSignedPutUrl(
29+
client: S3Client,
30+
bucket: string,
31+
key: string,
32+
contentType: string,
33+
expiresIn: number = 3600,
34+
): Promise<string> {
35+
const command = new PutObjectCommand({
36+
Bucket: bucket,
37+
Key: key,
38+
ContentType: contentType,
39+
});
40+
return getSignedUrl(client, command, { expiresIn });
41+
}
4142

42-
public generateFileKey(prefix: string | undefined, filename: string): string {
43-
const timestamp = Date.now();
44-
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
45-
if (prefix) {
46-
const normalizedPrefix = prefix.replace(/\/$/, '');
47-
return `${normalizedPrefix}/${timestamp}_${sanitizedFilename}`;
48-
}
49-
return `${timestamp}_${sanitizedFilename}`;
50-
}
43+
public generateFileKey(prefix: string | undefined, filename: string): string {
44+
const id = nanoid(12);
45+
const extension = this._extractFileExtension(filename);
46+
const key = extension ? `${id}${extension}` : id;
47+
48+
if (prefix) {
49+
const normalizedPrefix = prefix.replace(/\/$/, '');
50+
return `${normalizedPrefix}/${key}`;
51+
}
52+
return key;
53+
}
54+
55+
private _extractFileExtension(filename: string): string {
56+
const lastDotIndex = filename.lastIndexOf('.');
57+
if (lastDotIndex === -1 || lastDotIndex === 0) {
58+
return '';
59+
}
60+
const extension = filename.slice(lastDotIndex).toLowerCase();
61+
if (/^\.[a-z0-9]+$/i.test(extension)) {
62+
return extension;
63+
}
64+
return '';
65+
}
5166
}
Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,79 @@
11
import { HttpStatus, Inject, Injectable } from '@nestjs/common';
22
import { HttpException } from '@nestjs/common/exceptions/http.exception.js';
3+
import JSON5 from 'json5';
34
import AbstractUseCase from '../../../common/abstract-use.case.js';
45
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
56
import { BaseType } from '../../../common/data-injection.tokens.js';
7+
import { WidgetTypeEnum } from '../../../enums/index.js';
68
import { Messages } from '../../../exceptions/text/messages.js';
79
import { Encryptor } from '../../../helpers/encryption/encryptor.js';
810
import { S3GetUploadUrlDs, S3UploadUrlResponseDs } from '../application/data-structures/s3-operation.ds.js';
911
import { S3WidgetParams } from '../application/data-structures/s3-widget-params.ds.js';
1012
import { S3HelperService } from '../s3-helper.service.js';
1113
import { IGetS3UploadUrl } from './s3-use-cases.interface.js';
12-
import { WidgetTypeEnum } from '../../../enums/index.js';
13-
import JSON5 from 'json5';
1414

1515
@Injectable()
1616
export class GetS3UploadUrlUseCase
17-
extends AbstractUseCase<S3GetUploadUrlDs, S3UploadUrlResponseDs>
18-
implements IGetS3UploadUrl
17+
extends AbstractUseCase<S3GetUploadUrlDs, S3UploadUrlResponseDs>
18+
implements IGetS3UploadUrl
1919
{
20-
constructor(
21-
@Inject(BaseType.GLOBAL_DB_CONTEXT)
22-
protected _dbContext: IGlobalDatabaseContext,
23-
private readonly s3Helper: S3HelperService,
24-
) {
25-
super();
26-
}
20+
constructor(
21+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
22+
protected _dbContext: IGlobalDatabaseContext,
23+
private readonly s3Helper: S3HelperService,
24+
) {
25+
super();
26+
}
2727

28-
protected async implementation(inputData: S3GetUploadUrlDs): Promise<S3UploadUrlResponseDs> {
29-
const { connectionId, tableName, fieldName, userId, masterPwd, filename, contentType } = inputData;
28+
protected async implementation(inputData: S3GetUploadUrlDs): Promise<S3UploadUrlResponseDs> {
29+
const { connectionId, tableName, fieldName, userId, masterPwd, filename, contentType } = inputData;
3030

31-
const user = await this._dbContext.userRepository.findOneUserByIdWithCompany(userId);
32-
if (!user || !user.company) {
33-
throw new HttpException({ message: Messages.USER_NOT_FOUND_OR_NOT_IN_COMPANY }, HttpStatus.NOT_FOUND);
34-
}
31+
const user = await this._dbContext.userRepository.findOneUserByIdWithCompany(userId);
32+
if (!user || !user.company) {
33+
throw new HttpException({ message: Messages.USER_NOT_FOUND_OR_NOT_IN_COMPANY }, HttpStatus.NOT_FOUND);
34+
}
3535

36-
const foundTableWidgets = await this._dbContext.tableWidgetsRepository.findTableWidgets(connectionId, tableName);
37-
const widget = foundTableWidgets.find((w) => w.field_name === fieldName);
36+
const foundTableWidgets = await this._dbContext.tableWidgetsRepository.findTableWidgets(connectionId, tableName);
37+
const widget = foundTableWidgets.find((w) => w.field_name === fieldName);
3838

39-
if (!widget || widget.widget_type !== WidgetTypeEnum.S3) {
40-
throw new HttpException({ message: 'S3 widget not configured for this field' }, HttpStatus.BAD_REQUEST);
41-
}
39+
if (!widget || widget.widget_type !== WidgetTypeEnum.S3) {
40+
throw new HttpException({ message: 'S3 widget not configured for this field' }, HttpStatus.BAD_REQUEST);
41+
}
4242

43-
const params: S3WidgetParams =
44-
typeof widget.widget_params === 'string' ? JSON5.parse(widget.widget_params) : widget.widget_params;
43+
const params: S3WidgetParams =
44+
typeof widget.widget_params === 'string' ? JSON5.parse(widget.widget_params) : widget.widget_params;
4545

46-
const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
47-
params.aws_access_key_id_secret_name,
48-
user.company.id,
49-
);
46+
const accessKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
47+
params.aws_access_key_id_secret_name,
48+
user.company.id,
49+
);
5050

51-
const secretKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
52-
params.aws_secret_access_key_secret_name,
53-
user.company.id,
54-
);
51+
const secretKeySecret = await this._dbContext.userSecretRepository.findSecretBySlugAndCompanyId(
52+
params.aws_secret_access_key_secret_name,
53+
user.company.id,
54+
);
5555

56-
if (!accessKeySecret || !secretKeySecret) {
57-
throw new HttpException({ message: 'AWS credentials secrets not found' }, HttpStatus.NOT_FOUND);
58-
}
56+
if (!accessKeySecret || !secretKeySecret) {
57+
throw new HttpException({ message: 'AWS credentials secrets not found' }, HttpStatus.NOT_FOUND);
58+
}
5959

60-
let accessKeyId = Encryptor.decryptData(accessKeySecret.encryptedValue);
61-
let secretAccessKey = Encryptor.decryptData(secretKeySecret.encryptedValue);
60+
let accessKeyId = Encryptor.decryptData(accessKeySecret.encryptedValue);
61+
let secretAccessKey = Encryptor.decryptData(secretKeySecret.encryptedValue);
6262

63-
if (accessKeySecret.masterEncryption && masterPwd) {
64-
accessKeyId = Encryptor.decryptDataMasterPwd(accessKeyId, masterPwd);
65-
}
66-
if (secretKeySecret.masterEncryption && masterPwd) {
67-
secretAccessKey = Encryptor.decryptDataMasterPwd(secretAccessKey, masterPwd);
68-
}
63+
if (accessKeySecret.masterEncryption && masterPwd) {
64+
accessKeyId = Encryptor.decryptDataMasterPwd(accessKeyId, masterPwd);
65+
}
66+
if (secretKeySecret.masterEncryption && masterPwd) {
67+
secretAccessKey = Encryptor.decryptDataMasterPwd(secretAccessKey, masterPwd);
68+
}
6969

70-
const client = this.s3Helper.createS3Client(accessKeyId, secretAccessKey, params.region || 'us-east-1');
70+
const client = this.s3Helper.createS3Client(accessKeyId, secretAccessKey, params.region || 'us-east-1');
7171

72-
const key = this.s3Helper.generateFileKey(params.prefix, filename);
73-
const expiresIn = 3600;
74-
const uploadUrl = await this.s3Helper.getSignedPutUrl(client, params.bucket, key, contentType, expiresIn);
72+
const key = this.s3Helper.generateFileKey(params.prefix, filename);
73+
const expiresIn = 3600;
74+
const uploadUrl = await this.s3Helper.getSignedPutUrl(client, params.bucket, key, contentType, expiresIn);
75+
const previewUrl = await this.s3Helper.getSignedGetUrl(client, params.bucket, key, expiresIn);
7576

76-
return { uploadUrl, key, expiresIn };
77-
}
77+
return { uploadUrl, key, expiresIn, previewUrl };
78+
}
7879
}

frontend/src/app/components/dashboard/db-table-view/db-table-filters-dialog/db-table-filters-dialog.component.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ <h1 mat-dialog-title class="filters-header">
3434
label: tableWidgets[value.key].name || value.key,
3535
value: tableRowFieldsShown[value.key],
3636
widgetStructure: tableWidgets[value.key],
37-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined
37+
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
38+
autofocus: autofocusField === value.key
3839
}"
3940
[ndcDynamicOutputs]="{
4041
onFieldChange: { handler: updateField, args: ['$event', value.key] }
@@ -49,7 +50,8 @@ <h1 mat-dialog-title class="filters-header">
4950
label: value.key,
5051
value: tableRowFieldsShown[value.key],
5152
structure: tableRowStructure[value.key],
52-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined
53+
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
54+
autofocus: autofocusField === value.key
5355
}"
5456
[ndcDynamicOutputs]="{
5557
onFieldChange: { handler: updateField, args: ['$event', value.key] }
@@ -117,7 +119,8 @@ <h1 mat-dialog-title class="filters-header">
117119
value: tableRowFieldsShown[value.key],
118120
readonly: tableRowFieldsComparator[value.key] === 'empty',
119121
structure: tableRowStructure[value.key],
120-
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined
122+
relations: tableTypes[value.key] === 'foreign key' ? tableForeignKeys[value.key] : undefined,
123+
autofocus: autofocusField === value.key
121124
}"
122125
[ndcDynamicOutputs]="{
123126
onFieldChange: { handler: updateField, args: ['$event', value.key] }

0 commit comments

Comments
 (0)