From 59445f9b941eb509caeae15d8a10258ce4395dde Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 10:04:12 +0000 Subject: [PATCH 1/7] refactor: enhance token management in AgentEntity and update repository methods --- backend/src/entities/agent/agent.entity.ts | 13 ++++++++++++- backend/src/entities/agent/agent.module.ts | 9 ++------- .../repository/agent.repository.interface.ts | 3 +++ .../custom-agent-repository-extension.ts | 18 +++++------------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/backend/src/entities/agent/agent.entity.ts b/backend/src/entities/agent/agent.entity.ts index 06b23c400..9fdd643d5 100644 --- a/backend/src/entities/agent/agent.entity.ts +++ b/backend/src/entities/agent/agent.entity.ts @@ -25,15 +25,26 @@ export class AgentEntity { @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) updatedAt: Date; + private _tokenChanged = false; + + setToken(token: string): void { + this.token = token; + this._tokenChanged = true; + } + @BeforeInsert() encryptToken(): void { this.token = Encryptor.hashDataHMAC(this.token); + this._tokenChanged = false; } @BeforeUpdate() updateTimestampAndEncryptToken(): void { this.updatedAt = new Date(); - this.token = Encryptor.hashDataHMAC(this.token); + if (this._tokenChanged) { + this.token = Encryptor.hashDataHMAC(this.token); + this._tokenChanged = false; + } } @OneToOne( diff --git a/backend/src/entities/agent/agent.module.ts b/backend/src/entities/agent/agent.module.ts index c4fe7b780..b1df0a500 100644 --- a/backend/src/entities/agent/agent.module.ts +++ b/backend/src/entities/agent/agent.module.ts @@ -1,6 +1,5 @@ -import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { AuthMiddleware } from '../../authorization/index.js'; import { GlobalDatabaseContext } from '../../common/application/global-database-context.js'; import { BaseType } from '../../common/data-injection.tokens.js'; import { LogOutEntity } from '../log-out/log-out.entity.js'; @@ -17,8 +16,4 @@ import { AgentEntity } from './agent.entity.js'; ], exports: [], }) -export class AgentModule implements NestModule { - public configure(consumer: MiddlewareConsumer): any { - consumer.apply(AuthMiddleware).forRoutes(); - } -} +export class AgentModule {} diff --git a/backend/src/entities/agent/repository/agent.repository.interface.ts b/backend/src/entities/agent/repository/agent.repository.interface.ts index 5a262133c..11fa6cf93 100644 --- a/backend/src/entities/agent/repository/agent.repository.interface.ts +++ b/backend/src/entities/agent/repository/agent.repository.interface.ts @@ -1,3 +1,4 @@ +import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { AgentEntity } from '../agent.entity.js'; @@ -7,4 +8,6 @@ export interface IAgentRepository { createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise; renewOrCreateConnectionToken(connectionId: string): Promise; + + getTestAgentToken(connectionType: ConnectionTypeTestEnum): string; } diff --git a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts index c9d82035f..6ce19698f 100644 --- a/backend/src/entities/agent/repository/custom-agent-repository-extension.ts +++ b/backend/src/entities/agent/repository/custom-agent-repository-extension.ts @@ -2,12 +2,9 @@ import { ConnectionTypeTestEnum } from '@rocketadmin/shared-code/dist/src/shared import { nanoid } from 'nanoid'; import { ConnectionEntity } from '../../connection/connection.entity.js'; import { AgentEntity } from '../agent.entity.js'; +import { IAgentRepository } from './agent.repository.interface.js'; -export const customAgentRepositoryExtension = { - async saveNewAgent(agent: AgentEntity): Promise { - return await this.save(agent); - }, - +export const customAgentRepositoryExtension: IAgentRepository = { async createNewAgentForConnectionAndReturnToken(connection: ConnectionEntity): Promise { const newAgent = await this.createNewAgentForConnection(connection); return newAgent.token; @@ -15,13 +12,8 @@ export const customAgentRepositoryExtension = { async createNewAgentForConnection(connection: ConnectionEntity): Promise { const agent = new AgentEntity(); - let token = nanoid(64); - if (process.env.NODE_ENV !== 'test') { - agent.token = token; - } else { - token = this.getTestAgentToken(connection.type); - agent.token = token; - } + const token = process.env.NODE_ENV !== 'test' ? nanoid(64) : this.getTestAgentToken(connection.type); + agent.setToken(token); agent.connection = connection; const savedAgent = await this.save(agent); savedAgent.token = token; @@ -42,7 +34,7 @@ export const customAgentRepositoryExtension = { return await this.createNewAgentForConnectionAndReturnToken(foundConnection); } else { const newToken = nanoid(64); - foundAgent.token = newToken; + foundAgent.setToken(newToken); await this.save(foundAgent); return newToken; } From 2b676181835fa43cb9c495b44ceb65d6ad7391f6 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 10:24:05 +0000 Subject: [PATCH 2/7] refactor: implement OnModuleInit for AmplitudeService and enhance log options handling --- .../entities/amplitude/amplitude.service.ts | 64 ++++++++++++------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/backend/src/entities/amplitude/amplitude.service.ts b/backend/src/entities/amplitude/amplitude.service.ts index b7901a3f4..1a52fe1cd 100644 --- a/backend/src/entities/amplitude/amplitude.service.ts +++ b/backend/src/entities/amplitude/amplitude.service.ts @@ -1,36 +1,56 @@ import Amplitude from '@amplitude/node'; -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { AmplitudeEventTypeEnum } from '../../enums/index.js'; import { UserEntity } from '../user/user.entity.js'; +export interface AmplitudeLogOptions { + user_email?: string; + tablesCount?: number; + reason?: string; + message?: string; + operationCount?: number; +} + @Injectable() -export class AmplitudeService { +export class AmplitudeService implements OnModuleInit { + private client: ReturnType; + constructor( @InjectRepository(UserEntity) private readonly userRepository: Repository, ) {} - public async formAndSendLogRecord(event_type: AmplitudeEventTypeEnum, user_id: string, options = null) { + public onModuleInit(): void { + if (process.env.AMPLITUDE_API_KEY) { + this.client = Amplitude.init(process.env.AMPLITUDE_API_KEY); + } + } + + public async formAndSendLogRecord( + event_type: AmplitudeEventTypeEnum, + user_id: string, + options?: AmplitudeLogOptions, + ): Promise { try { if (process.env.NODE_ENV === 'test') return; let user_email = (await this.userRepository.findOne({ where: { id: user_id } }))?.email; if (!user_email && options) { - user_email = options?.user_email; + user_email = options.user_email; } - let event_properties; + let event_properties: Record | undefined; if (user_email) { event_properties = { user_properties: { - email: user_email ? user_email : 'unknown', - tablesCount: options?.tablesCount ? options.tablesCount : undefined, - reason: options?.reason ? options?.reason : undefined, - message: options?.message ? options.message : undefined, + email: user_email ?? 'unknown', + tablesCount: options?.tablesCount, + reason: options?.reason, + message: options?.message, }, }; } - if (options?.operationCount && options?.operationCount > 0) { + if (options?.operationCount && options.operationCount > 0) { const promisesArr = Array.from(Array(options.operationCount), () => this.sendLog(event_type, user_id, event_properties), ); @@ -43,21 +63,19 @@ export class AmplitudeService { } } - private async sendLog(eventType, cognitoUserName, eventProperties) { - const client = Amplitude.init(process.env.AMPLITUDE_API_KEY); + private async sendLog( + eventType: AmplitudeEventTypeEnum, + userId: string, + eventProperties?: Record, + ): Promise { + if (!this.client) return; try { - client - .logEvent({ - event_type: eventType, - user_id: cognitoUserName, - event_properties: eventProperties ? eventProperties : undefined, - }) - .catch((e) => { - throw new Error(e); - }); - client.flush().catch((e) => { - throw new Error(e); + await this.client.logEvent({ + event_type: eventType, + user_id: userId, + event_properties: eventProperties, }); + await this.client.flush(); } catch (e) { console.error(e); } From 666b8f2c6d111ce8c176c134dc6111ff847b675e Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 11:12:11 +0000 Subject: [PATCH 3/7] refactor: optimize database queries and fix method name inconsistencies in company info handling --- .../company-info-helper.service.ts | 7 ++++--- .../company-info/company-info.controller.ts | 3 ++- ...-in-company-custom-repository-extension.ts | 2 +- ...ompany-info-custom-repository.extension.ts | 21 +++++++------------ .../company-info-repository.interface.ts | 2 -- .../company-info-use-cases.interface.ts | 2 ++ ...nnections-company-display-mode.use.case.ts | 2 +- .../use-cases/unfreeze-connection.use.case.ts | 2 +- .../change-usual-password-use.case.ts | 2 +- .../user/use-cases/disable-otp.use.case.ts | 2 +- .../user/use-cases/otp-login-use.case.ts | 2 +- .../request-email-verification.use.case.ts | 2 +- .../user/use-cases/usual-login-use.case.ts | 2 +- .../verify-reset-user-password.use.case.ts | 2 +- 14 files changed, 25 insertions(+), 28 deletions(-) diff --git a/backend/src/entities/company-info/company-info-helper.service.ts b/backend/src/entities/company-info/company-info-helper.service.ts index e0d74d112..e1a4d9556 100644 --- a/backend/src/entities/company-info/company-info-helper.service.ts +++ b/backend/src/entities/company-info/company-info-helper.service.ts @@ -21,9 +21,10 @@ export class CompanyInfoHelperService { const companyInformationFromSaaS = await this.saasCompanyGatewayService.getCompanyInfo(companyId); - const countUsersInCompany = await this._dbContext.userRepository.countUsersInCompany(companyId); - const countInvitationsInCompany = - await this._dbContext.invitationInCompanyRepository.countNonExpiredInvitationsInCompany(companyId); + const [countUsersInCompany, countInvitationsInCompany] = await Promise.all([ + this._dbContext.userRepository.countUsersInCompany(companyId), + this._dbContext.invitationInCompanyRepository.countNonExpiredInvitationsInCompany(companyId), + ]); if (companyInformationFromSaaS.subscriptionLevel === SubscriptionLevelEnum.FREE_PLAN) { return countUsersInCompany + countInvitationsInCompany < Constants.FREE_PLAN_USERS_COUNT; diff --git a/backend/src/entities/company-info/company-info.controller.ts b/backend/src/entities/company-info/company-info.controller.ts index 83b888128..4a5a6ece5 100644 --- a/backend/src/entities/company-info/company-info.controller.ts +++ b/backend/src/entities/company-info/company-info.controller.ts @@ -81,6 +81,7 @@ import { IRemoveUserFromCompany, IRevokeUserInvitationInCompany, ISuspendUsersInCompany, + IUnsuspendUsersInCompany, IToggleCompanyTestConnectionsMode, IUpdateCompanyName, IUpdateUsers2faStatusInCompany, @@ -128,7 +129,7 @@ export class CompanyInfoController { @Inject(UseCaseType.SUSPEND_USERS_IN_COMPANY) private readonly suspendUsersInCompanyUseCase: ISuspendUsersInCompany, @Inject(UseCaseType.UNSUSPEND_USERS_IN_COMPANY) - private readonly unSuspendUsersInCompanyUseCase: ISuspendUsersInCompany, + private readonly unSuspendUsersInCompanyUseCase: IUnsuspendUsersInCompany, @Inject(UseCaseType.TOGGLE_TEST_CONNECTIONS_DISPLAY_MODE_IN_COMPANY) private readonly toggleTestConnectionsCompanyDisplayModeUseCase: IToggleCompanyTestConnectionsMode, @Inject(UseCaseType.UPLOAD_COMPANY_LOGO) diff --git a/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts b/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts index 414be8a64..10ee08b19 100644 --- a/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts +++ b/backend/src/entities/company-info/invitation-in-company/repository/invitation-in-company-custom-repository-extension.ts @@ -52,7 +52,7 @@ export const invitationInCompanyCustomRepositoryExtension: IInvitationInCompanyR .leftJoinAndSelect('invitation_in_company.company', 'company') .leftJoinAndSelect('company.users', 'users') .where("invitation_in_company.createdAt > NOW() - INTERVAL '1 day'") - .where('invitation_in_company.verification_string = :verificationString', { verificationString }); + .andWhere('invitation_in_company.verification_string = :verificationString', { verificationString }); return await qb.getOne(); }, diff --git a/backend/src/entities/company-info/repository/company-info-custom-repository.extension.ts b/backend/src/entities/company-info/repository/company-info-custom-repository.extension.ts index 672ad6428..07a9e3931 100644 --- a/backend/src/entities/company-info/repository/company-info-custom-repository.extension.ts +++ b/backend/src/entities/company-info/repository/company-info-custom-repository.extension.ts @@ -26,13 +26,6 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = { .getOne(); }, - async finOneCompanyInfoByUserId(userId: string): Promise { - return await this.createQueryBuilder('company_info') - .leftJoinAndSelect('company_info.users', 'users') - .where('users.id = :userId', { userId }) - .getOne(); - }, - async findCompanyInfoByUserId(userId: string): Promise { return await this.createQueryBuilder('company_info') .leftJoinAndSelect('company_info.users', 'users') @@ -94,9 +87,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = { .andWhere('connections.isTestConnection IS FALSE') .andWhere('connections.is_frozen IS FALSE') .getMany(); - return foundCompaniesWithPaidConnections.map((companyInfo: CompanyInfoEntity) => { - return companyInfo.connections; - }); + return foundCompaniesWithPaidConnections + .map((companyInfo: CompanyInfoEntity) => companyInfo.connections) + .filter(Boolean) + .flat(); }, async findCompanyFrozenPaidConnections(companyIds: Array): Promise> { @@ -108,9 +102,10 @@ export const companyInfoRepositoryExtension: ICompanyInfoRepository = { .andWhere('connections.isTestConnection IS FALSE') .andWhere('connections.is_frozen IS TRUE') .getMany(); - return foundCompaniesWithPaidConnections.map((companyInfo: CompanyInfoEntity) => { - return companyInfo.connections; - }); + return foundCompaniesWithPaidConnections + .map((companyInfo: CompanyInfoEntity) => companyInfo.connections) + .filter(Boolean) + .flat(); }, async findCompanyWithLogo(companyId: string): Promise { diff --git a/backend/src/entities/company-info/repository/company-info-repository.interface.ts b/backend/src/entities/company-info/repository/company-info-repository.interface.ts index 388a8ad6d..e1492d606 100644 --- a/backend/src/entities/company-info/repository/company-info-repository.interface.ts +++ b/backend/src/entities/company-info/repository/company-info-repository.interface.ts @@ -8,8 +8,6 @@ export interface ICompanyInfoRepository { findOneCompanyInfoByUserIdWithConnections(userId: string): Promise; - finOneCompanyInfoByUserId(userId: string): Promise; - findCompanyInfoByUserId(userId: string): Promise; findFullCompanyInfoByUserId(userId: string): Promise; diff --git a/backend/src/entities/company-info/use-cases/company-info-use-cases.interface.ts b/backend/src/entities/company-info/use-cases/company-info-use-cases.interface.ts index e8ababec2..c98a908b8 100644 --- a/backend/src/entities/company-info/use-cases/company-info-use-cases.interface.ts +++ b/backend/src/entities/company-info/use-cases/company-info-use-cases.interface.ts @@ -84,6 +84,8 @@ export interface ISuspendUsersInCompany { execute(inputData: SuspendUsersInCompanyDS, inTransaction: InTransactionEnum): Promise; } +export type IUnsuspendUsersInCompany = ISuspendUsersInCompany; + export interface IToggleCompanyTestConnectionsMode { execute(inputData: ToggleTestConnectionDisplayModeDs, inTransaction: InTransactionEnum): Promise; } diff --git a/backend/src/entities/company-info/use-cases/toggle-test-connections-company-display-mode.use.case.ts b/backend/src/entities/company-info/use-cases/toggle-test-connections-company-display-mode.use.case.ts index ce2705f68..c97cdf30b 100644 --- a/backend/src/entities/company-info/use-cases/toggle-test-connections-company-display-mode.use.case.ts +++ b/backend/src/entities/company-info/use-cases/toggle-test-connections-company-display-mode.use.case.ts @@ -21,7 +21,7 @@ export class ToggleCompanyTestConnectionsDisplayModeUseCase public async implementation(inputData: ToggleTestConnectionDisplayModeDs): Promise { const { userId, displayMode } = inputData; - const foundCompanyInfo = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(userId); + const foundCompanyInfo = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId); if (!foundCompanyInfo) { throw new NotFoundException(Messages.COMPANY_NOT_FOUND); } diff --git a/backend/src/entities/connection/use-cases/unfreeze-connection.use.case.ts b/backend/src/entities/connection/use-cases/unfreeze-connection.use.case.ts index d0ea827bf..d7a26b9b3 100644 --- a/backend/src/entities/connection/use-cases/unfreeze-connection.use.case.ts +++ b/backend/src/entities/connection/use-cases/unfreeze-connection.use.case.ts @@ -25,7 +25,7 @@ export class UnfreezeConnectionUseCase const connection = await this._dbContext.connectionRepository.findOne({ where: { id: connectionId } }); // if (isSaaS()) { - // const userCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(userId); + // const userCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId); // const companyInfoFromSaas = await this.saasCompanyGatewayService.getCompanyInfo(userCompany.id); // if (companyInfoFromSaas.subscriptionLevel === SubscriptionLevelEnum.FREE_PLAN) { // if (Constants.NON_FREE_PLAN_CONNECTION_TYPES.includes(connection.type as ConnectionTypesEnum)) { diff --git a/backend/src/entities/user/use-cases/change-usual-password-use.case.ts b/backend/src/entities/user/use-cases/change-usual-password-use.case.ts index 22937bd84..31ecaf60a 100644 --- a/backend/src/entities/user/use-cases/change-usual-password-use.case.ts +++ b/backend/src/entities/user/use-cases/change-usual-password-use.case.ts @@ -35,7 +35,7 @@ export class ChangeUsualPasswordUseCase user.password = await Encryptor.hashUserPassword(userData.newPassword); const updatedUser = await this._dbContext.userRepository.saveUserEntity(user); - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(updatedUser.id); + const foundUserCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(updatedUser.id); return generateGwtToken(updatedUser, get2FaScope(updatedUser, foundUserCompany)); } } diff --git a/backend/src/entities/user/use-cases/disable-otp.use.case.ts b/backend/src/entities/user/use-cases/disable-otp.use.case.ts index f08e1b58d..c2a06a2f9 100644 --- a/backend/src/entities/user/use-cases/disable-otp.use.case.ts +++ b/backend/src/entities/user/use-cases/disable-otp.use.case.ts @@ -31,7 +31,7 @@ export class DisableOtpUseCase extends AbstractUseCase implem await this.recordSignInAudit(foundUser.email, userId, SignInStatusEnum.SUCCESS, ipAddress, userAgent); - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id); + const foundUserCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(foundUser.id); return generateGwtToken(foundUser, get2FaScope(foundUser, foundUserCompany)); } diff --git a/backend/src/entities/user/use-cases/request-email-verification.use.case.ts b/backend/src/entities/user/use-cases/request-email-verification.use.case.ts index a089be47f..239543a9b 100644 --- a/backend/src/entities/user/use-cases/request-email-verification.use.case.ts +++ b/backend/src/entities/user/use-cases/request-email-verification.use.case.ts @@ -40,7 +40,7 @@ export class RequestEmailVerificationUseCase HttpStatus.BAD_REQUEST, ); } - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(foundUser.id); + const foundUserCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(foundUser.id); const companyCustomDomain = await this.saasCompanyGatewayService.getCompanyCustomDomainById(foundUserCompany.id); const { rawToken } = await this._dbContext.emailVerificationRepository.createOrUpdateEmailVerification(foundUser); diff --git a/backend/src/entities/user/use-cases/usual-login-use.case.ts b/backend/src/entities/user/use-cases/usual-login-use.case.ts index 232e2ac48..a25ae6047 100644 --- a/backend/src/entities/user/use-cases/usual-login-use.case.ts +++ b/backend/src/entities/user/use-cases/usual-login-use.case.ts @@ -121,7 +121,7 @@ export class UsualLoginUseCase extends AbstractUseCase imp await this.recordSignInAudit(email, user.id, SignInStatusEnum.SUCCESS, ipAddress, userAgent); - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(user.id); + const foundUserCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(user.id); return generateGwtToken(user, get2FaScope(user, foundUserCompany)); } diff --git a/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts b/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts index 4b82c8e2a..56e1b5c4f 100644 --- a/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts +++ b/backend/src/entities/user/use-cases/verify-reset-user-password.use.case.ts @@ -49,7 +49,7 @@ export class VerifyResetUserPasswordUseCase foundUser.password = await Encryptor.hashUserPassword(newUserPassword); await this._dbContext.passwordResetRepository.removePasswordResetEntity(verificationEntity); const savedUser = await this._dbContext.userRepository.saveUserEntity(foundUser); - const foundUserCompany = await this._dbContext.companyInfoRepository.finOneCompanyInfoByUserId(savedUser.id); + const foundUserCompany = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(savedUser.id); return { id: foundUser.id, email: foundUser.email, From fedd8918670babe3e30b9ea25b838e043cb65088 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 12:29:57 +0000 Subject: [PATCH 4/7] refactor: update connection properties data structure and sync table categories logic --- .../create-connection-properties.ds.ts | 22 +++---- .../connection-properties.controller.ts | 55 ++++++---------- .../connection-properties.entity.ts | 10 +-- .../connection-properties.interface.ts | 5 -- .../update-connection-properties.use.case.ts | 55 +++------------- ...ild-update-connection-properties-object.ts | 45 ++++++------- .../utils/sync-table-categories.ts | 66 +++++++++++++++++++ 7 files changed, 132 insertions(+), 126 deletions(-) delete mode 100644 backend/src/entities/connection-properties/connection-properties.interface.ts create mode 100644 backend/src/entities/connection-properties/utils/sync-table-categories.ts diff --git a/backend/src/entities/connection-properties/application/data-structures/create-connection-properties.ds.ts b/backend/src/entities/connection-properties/application/data-structures/create-connection-properties.ds.ts index 06d670ef3..49c933f20 100644 --- a/backend/src/entities/connection-properties/application/data-structures/create-connection-properties.ds.ts +++ b/backend/src/entities/connection-properties/application/data-structures/create-connection-properties.ds.ts @@ -1,18 +1,18 @@ export class CreateConnectionPropertiesDs { - hidden_tables: Array; userId: string; connectionId: string; master_password: string; - logo_url: string; - primary_color: string; - secondary_color: string; - hostname: string; - company_name: string; - tables_audit: boolean; - human_readable_table_names: boolean; - allow_ai_requests: boolean; - default_showing_table: string; - table_categories: Array<{ + hidden_tables?: Array; + logo_url?: string; + primary_color?: string; + secondary_color?: string; + hostname?: string; + company_name?: string; + tables_audit?: boolean; + human_readable_table_names?: boolean; + allow_ai_requests?: boolean; + default_showing_table?: string; + table_categories?: Array<{ category_name: string; tables: Array; category_color: string; diff --git a/backend/src/entities/connection-properties/connection-properties.controller.ts b/backend/src/entities/connection-properties/connection-properties.controller.ts index d04b930c4..4b61c79c8 100644 --- a/backend/src/entities/connection-properties/connection-properties.controller.ts +++ b/backend/src/entities/connection-properties/connection-properties.controller.ts @@ -22,7 +22,6 @@ import { ConnectionEditGuard, ConnectionReadGuard } from '../../guards/index.js' import { SentryInterceptor } from '../../interceptors/index.js'; import { CreateConnectionPropertiesDs } from './application/data-structures/create-connection-properties.ds.js'; import { FoundConnectionPropertiesDs } from './application/data-structures/found-connection-properties.ds.js'; -import { IConnectionPropertiesRO } from './connection-properties.interface.js'; import { CreateConnectionPropertiesDto } from './dto/create-connection-properties.dto.js'; import { ICreateConnectionProperties, @@ -96,24 +95,9 @@ export class ConnectionPropertiesController { HttpStatus.BAD_REQUEST, ); } - const createConnectionPropertiesDs: CreateConnectionPropertiesDs = { - connectionId: connectionId, - master_password: masterPwd, - hidden_tables: connectionPropertiesData.hidden_tables, - userId: userId, - logo_url: connectionPropertiesData.logo_url, - primary_color: connectionPropertiesData.primary_color, - secondary_color: connectionPropertiesData.secondary_color, - hostname: connectionPropertiesData.hostname, - company_name: connectionPropertiesData.company_name, - tables_audit: connectionPropertiesData.tables_audit, - human_readable_table_names: connectionPropertiesData.human_readable_table_names, - allow_ai_requests: connectionPropertiesData.allow_ai_requests, - default_showing_table: connectionPropertiesData.default_showing_table, - table_categories: connectionPropertiesData.table_categories, - }; + const inputData = this.buildConnectionPropertiesDs(connectionPropertiesData, connectionId, userId, masterPwd); - return await this.createConnectionPropertiesUseCase.execute(createConnectionPropertiesDs, InTransactionEnum.ON); + return await this.createConnectionPropertiesUseCase.execute(inputData, InTransactionEnum.ON); } @ApiOperation({ summary: 'Update connection properties' }) @@ -132,7 +116,7 @@ export class ConnectionPropertiesController { @UserId() userId: string, @MasterPassword() masterPwd: string, @SlugUuid('connectionId') connectionId: string, - ): Promise { + ): Promise { if (!connectionId) { throw new HttpException( { @@ -142,22 +126,7 @@ export class ConnectionPropertiesController { ); } - const inputData: CreateConnectionPropertiesDs = { - connectionId: connectionId, - master_password: masterPwd, - hidden_tables: connectionPropertiesData.hidden_tables, - userId: userId, - logo_url: connectionPropertiesData.logo_url, - primary_color: connectionPropertiesData.primary_color, - secondary_color: connectionPropertiesData.secondary_color, - company_name: connectionPropertiesData.company_name, - hostname: connectionPropertiesData.hostname, - tables_audit: connectionPropertiesData.tables_audit, - human_readable_table_names: connectionPropertiesData.human_readable_table_names, - allow_ai_requests: connectionPropertiesData.allow_ai_requests, - default_showing_table: connectionPropertiesData.default_showing_table, - table_categories: connectionPropertiesData.table_categories, - }; + const inputData = this.buildConnectionPropertiesDs(connectionPropertiesData, connectionId, userId, masterPwd); return await this.updateConnectionPropertiesUseCase.execute(inputData, InTransactionEnum.ON); } @@ -170,7 +139,7 @@ export class ConnectionPropertiesController { }) @UseGuards(ConnectionEditGuard) @Delete('/connection/properties/:connectionId') - async deleteConnectionProperties(@SlugUuid('connectionId') connectionId: string): Promise { + async deleteConnectionProperties(@SlugUuid('connectionId') connectionId: string): Promise { if (!connectionId) { throw new HttpException( { @@ -181,4 +150,18 @@ export class ConnectionPropertiesController { } return await this.deleteConnectionPropertiesUseCase.execute(connectionId, InTransactionEnum.ON); } + + private buildConnectionPropertiesDs( + dto: CreateConnectionPropertiesDto, + connectionId: string, + userId: string, + masterPwd: string, + ): CreateConnectionPropertiesDs { + return { + connectionId, + userId, + master_password: masterPwd, + ...dto, + }; + } } diff --git a/backend/src/entities/connection-properties/connection-properties.entity.ts b/backend/src/entities/connection-properties/connection-properties.entity.ts index c1d349c6e..fbcaea08f 100644 --- a/backend/src/entities/connection-properties/connection-properties.entity.ts +++ b/backend/src/entities/connection-properties/connection-properties.entity.ts @@ -8,10 +8,10 @@ export class ConnectionPropertiesEntity { id: string; @Column('varchar', { array: true, default: null }) - hidden_tables: string[]; + hidden_tables: string[] | null; @Column({ default: null }) - logo_url: string; + logo_url: string | null; @Column({ default: '' }) primary_color: string; @@ -20,10 +20,10 @@ export class ConnectionPropertiesEntity { secondary_color: string; @Column({ default: null }) - hostname: string; + hostname: string | null; @Column({ default: null }) - company_name: string; + company_name: string | null; @Column({ default: true, type: 'boolean' }) tables_audit: boolean; @@ -35,7 +35,7 @@ export class ConnectionPropertiesEntity { allow_ai_requests: boolean; @Column({ default: null }) - default_showing_table: string; + default_showing_table: string | null; @OneToOne( (_) => ConnectionEntity, diff --git a/backend/src/entities/connection-properties/connection-properties.interface.ts b/backend/src/entities/connection-properties/connection-properties.interface.ts deleted file mode 100644 index 7b6b8506c..000000000 --- a/backend/src/entities/connection-properties/connection-properties.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface IConnectionPropertiesRO { - id: string; - hidden_tables: Array; - connectionId: string; -} diff --git a/backend/src/entities/connection-properties/use-cases/update-connection-properties.use.case.ts b/backend/src/entities/connection-properties/use-cases/update-connection-properties.use.case.ts index 15236e880..0c974c4f2 100644 --- a/backend/src/entities/connection-properties/use-cases/update-connection-properties.use.case.ts +++ b/backend/src/entities/connection-properties/use-cases/update-connection-properties.use.case.ts @@ -4,7 +4,6 @@ import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; import { Messages } from '../../../exceptions/text/messages.js'; -import { TableCategoriesEntity } from '../../table-categories/table-categories.entity.js'; import { CreateConnectionPropertiesDs } from '../application/data-structures/create-connection-properties.ds.js'; import { FoundConnectionPropertiesDs } from '../application/data-structures/found-connection-properties.ds.js'; import { buildFoundConnectionPropertiesDs } from '../utils/build-found-connection-properties-ds.js'; @@ -12,6 +11,7 @@ import { buildUpdateConnectionPropertiesObject, IUpdateConnectionPropertiesObject, } from '../utils/build-update-connection-properties-object.js'; +import { syncTableCategories } from '../utils/sync-table-categories.js'; import { validateCreateConnectionPropertiesDs } from '../utils/validate-create-connection-properties-ds.js'; import { IUpdateConnectionProperties } from './connection-properties-use.cases.interface.js'; @@ -51,53 +51,14 @@ export class UpdateConnectionPropertiesUseCase where: { connection_properties_id: connectionPropertiesToUpdate.id }, }); - const newCategories: Array = []; - + let newCategories = foundCategories; if (table_categories && table_categories.length > 0) { - const categoriesToRemove = foundCategories.filter((foundCategory) => { - return !table_categories?.some((inputCategory) => inputCategory.category_id === foundCategory.category_id); - }); - if (categoriesToRemove && categoriesToRemove.length > 0) { - await this._dbContext.tableCategoriesRepository.remove(categoriesToRemove); - } - - const categoriesToCreate = table_categories.filter((inputCategory) => { - return !foundCategories.some((foundCategory) => foundCategory.category_id === inputCategory.category_id); - }); - - if (categoriesToCreate && categoriesToCreate.length > 0) { - const createdCategories = categoriesToCreate.map((category) => { - const newCategory = this._dbContext.tableCategoriesRepository.create({ - category_name: category.category_name, - tables: category.tables, - category_color: category.category_color, - category_id: category.category_id, - }); - newCategory.connection_properties = connectionPropertiesToUpdate; - return newCategory; - }); - const savedNewCategories = await this._dbContext.tableCategoriesRepository.save(createdCategories); - newCategories.push(...savedNewCategories); - } - - const categoriesToUpdate = table_categories.filter((inputCategory) => { - return foundCategories.some((foundCategory) => foundCategory.category_id === inputCategory.category_id); - }); - - for (const category of categoriesToUpdate) { - const categoryToUpdate = foundCategories.find( - (foundCategory) => foundCategory.category_id === category.category_id, - ); - if (categoryToUpdate) { - categoryToUpdate.category_name = category.category_name; - categoryToUpdate.category_color = category.category_color; - categoryToUpdate.tables = category.tables; - const savedUpdatedCategory = await this._dbContext.tableCategoriesRepository.save(categoryToUpdate); - newCategories.push(savedUpdatedCategory); - } - } - } else { - newCategories.push(...foundCategories); + newCategories = await syncTableCategories( + table_categories, + foundCategories, + connectionPropertiesToUpdate, + this._dbContext.tableCategoriesRepository, + ); } const updatedProperties = await this._dbContext.connectionPropertiesRepository.saveNewConnectionProperties(updated); diff --git a/backend/src/entities/connection-properties/utils/build-update-connection-properties-object.ts b/backend/src/entities/connection-properties/utils/build-update-connection-properties-object.ts index bec05ea59..02386da3f 100644 --- a/backend/src/entities/connection-properties/utils/build-update-connection-properties-object.ts +++ b/backend/src/entities/connection-properties/utils/build-update-connection-properties-object.ts @@ -15,29 +15,30 @@ export function buildUpdateConnectionPropertiesObject( allow_ai_requests, default_showing_table, } = inputData; - return { - hidden_tables: hidden_tables, - logo_url: logo_url, - primary_color: primary_color, - secondary_color: secondary_color, - hostname: hostname, - company_name: company_name, - tables_audit: tables_audit, - human_readable_table_names: human_readable_table_names, - allow_ai_requests: allow_ai_requests, - default_showing_table: default_showing_table, - }; + + const result: IUpdateConnectionPropertiesObject = {}; + if (hidden_tables !== undefined) result.hidden_tables = hidden_tables; + if (logo_url !== undefined) result.logo_url = logo_url; + if (primary_color !== undefined) result.primary_color = primary_color; + if (secondary_color !== undefined) result.secondary_color = secondary_color; + if (hostname !== undefined) result.hostname = hostname; + if (company_name !== undefined) result.company_name = company_name; + if (tables_audit !== undefined) result.tables_audit = tables_audit; + if (human_readable_table_names !== undefined) result.human_readable_table_names = human_readable_table_names; + if (allow_ai_requests !== undefined) result.allow_ai_requests = allow_ai_requests; + if (default_showing_table !== undefined) result.default_showing_table = default_showing_table; + return result; } export interface IUpdateConnectionPropertiesObject { - hidden_tables: Array; - logo_url: string; - primary_color: string; - secondary_color: string; - hostname: string; - company_name: string; - tables_audit: boolean; - human_readable_table_names: boolean; - allow_ai_requests: boolean; - default_showing_table: string; + hidden_tables?: Array; + logo_url?: string; + primary_color?: string; + secondary_color?: string; + hostname?: string; + company_name?: string; + tables_audit?: boolean; + human_readable_table_names?: boolean; + allow_ai_requests?: boolean; + default_showing_table?: string; } diff --git a/backend/src/entities/connection-properties/utils/sync-table-categories.ts b/backend/src/entities/connection-properties/utils/sync-table-categories.ts new file mode 100644 index 000000000..86284d37f --- /dev/null +++ b/backend/src/entities/connection-properties/utils/sync-table-categories.ts @@ -0,0 +1,66 @@ +import { Repository } from 'typeorm'; +import { TableCategoriesEntity } from '../../table-categories/table-categories.entity.js'; +import { ConnectionPropertiesEntity } from '../connection-properties.entity.js'; + +interface ICategoryInput { + category_name: string; + tables: Array; + category_color: string; + category_id: string; +} + +export async function syncTableCategories( + inputCategories: Array, + existingCategories: Array, + connectionProperties: ConnectionPropertiesEntity, + tableCategoriesRepository: Repository, +): Promise> { + const result: Array = []; + + const categoriesToRemove = existingCategories.filter((existing) => { + return !inputCategories.some((input) => input.category_id === existing.category_id); + }); + if (categoriesToRemove.length > 0) { + await tableCategoriesRepository.remove(categoriesToRemove); + } + + const categoriesToCreate = inputCategories.filter((input) => { + return !existingCategories.some((existing) => existing.category_id === input.category_id); + }); + if (categoriesToCreate.length > 0) { + const created = categoriesToCreate.map((category) => { + const entity = tableCategoriesRepository.create({ + category_name: category.category_name, + tables: category.tables, + category_color: category.category_color, + category_id: category.category_id, + }); + entity.connection_properties = connectionProperties; + return entity; + }); + const saved = await tableCategoriesRepository.save(created); + result.push(...saved); + } + + const categoriesToUpdate = inputCategories.filter((input) => { + return existingCategories.some((existing) => existing.category_id === input.category_id); + }); + const updatedEntities = categoriesToUpdate + .map((category) => { + const entity = existingCategories.find((existing) => existing.category_id === category.category_id); + if (entity) { + entity.category_name = category.category_name; + entity.category_color = category.category_color; + entity.tables = category.tables; + } + return entity; + }) + .filter((entity): entity is TableCategoriesEntity => !!entity); + + if (updatedEntities.length > 0) { + const saved = await tableCategoriesRepository.save(updatedEntities); + result.push(...saved); + } + + return result; +} From bf4994aaee1a20c4f3f1de3bc33805c79e277e74 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 13:56:50 +0000 Subject: [PATCH 5/7] refactor: correct variable naming and improve error handling in custom field use cases --- .../common/application/global-database-context.ts | 4 ++-- .../custom-field/custom-field.controller.ts | 13 ++----------- .../repository/custom-field-repository-extension.ts | 2 +- .../use-cases/create-custom-fields.use.case.ts | 2 +- .../use-cases/delete-custom-field.use.case.ts | 4 ++-- .../use-cases/update-custom-field.use.case.ts | 10 +++++----- .../utils/build-found-custom-fields-ds.ts | 7 +------ .../utils/validate-create-custom-field-dto.ts | 1 - .../non-saas-custom-field-e2e.test.ts | 4 ++-- .../ava-tests/saas-tests/custom-field-e2e.test.ts | 4 ++-- 10 files changed, 18 insertions(+), 33 deletions(-) diff --git a/backend/src/common/application/global-database-context.ts b/backend/src/common/application/global-database-context.ts index df13c3468..c8703645c 100644 --- a/backend/src/common/application/global-database-context.ts +++ b/backend/src/common/application/global-database-context.ts @@ -31,7 +31,7 @@ import { ConnectionPropertiesEntity } from '../../entities/connection-properties import { IConnectionPropertiesRepository } from '../../entities/connection-properties/repository/connection-properties.repository.interface.js'; import { customConnectionPropertiesRepositoryExtension } from '../../entities/connection-properties/repository/custom-connection-properties-repository-extension.js'; import { CustomFieldsEntity } from '../../entities/custom-field/custom-fields.entity.js'; -import { cusomFieldsCustomRepositoryExtension } from '../../entities/custom-field/repository/custom-field-repository-extension.js'; +import { customFieldsCustomRepositoryExtension } from '../../entities/custom-field/repository/custom-field-repository-extension.js'; import { ICustomFieldsRepository } from '../../entities/custom-field/repository/custom-fields-repository.interface.js'; import { EmailVerificationEntity } from '../../entities/email/email-verification.entity.js'; import { IEmailVerificationRepository } from '../../entities/email/repository/email-verification.repository.interface.js'; @@ -192,7 +192,7 @@ export class GlobalDatabaseContext implements IGlobalDatabaseContext { .extend(customConnectionPropertiesRepositoryExtension); this._customFieldsRepository = this.appDataSource .getRepository(CustomFieldsEntity) - .extend(cusomFieldsCustomRepositoryExtension); + .extend(customFieldsCustomRepositoryExtension); this._tableLogsRepository = this.appDataSource .getRepository(TableLogsEntity) .extend(tableLogsCustomRepositoryExtension); diff --git a/backend/src/entities/custom-field/custom-field.controller.ts b/backend/src/entities/custom-field/custom-field.controller.ts index 5b5c1e7c6..8c3527916 100644 --- a/backend/src/entities/custom-field/custom-field.controller.ts +++ b/backend/src/entities/custom-field/custom-field.controller.ts @@ -93,11 +93,7 @@ export class CustomFieldController { @UserId() userId: string, ): Promise { const { type, template_string, text } = customFieldData; - const createFieldDto = { - type: type, - text: text, - template_string: template_string, - }; + const createFieldDto = { type, text, template_string }; const inputData: CreateCustomFieldsDs = { connectionId: connectionId, createFieldDto: createFieldDto, @@ -127,12 +123,7 @@ export class CustomFieldController { @UserId() userId: string, ): Promise { const { id, type, template_string, text } = customFieldData; - const updateFieldDto = { - id: id, - type: type, - text: text, - template_string: template_string, - }; + const updateFieldDto = { id, type, text, template_string }; if (!updateFieldDto.id) { throw new HttpException( { diff --git a/backend/src/entities/custom-field/repository/custom-field-repository-extension.ts b/backend/src/entities/custom-field/repository/custom-field-repository-extension.ts index d74380bce..20babef7c 100644 --- a/backend/src/entities/custom-field/repository/custom-field-repository-extension.ts +++ b/backend/src/entities/custom-field/repository/custom-field-repository-extension.ts @@ -1,7 +1,7 @@ import { TableSettingsEntity } from '../../table-settings/common-table-settings/table-settings.entity.js'; import { CustomFieldsEntity } from '../custom-fields.entity.js'; -export const cusomFieldsCustomRepositoryExtension = { +export const customFieldsCustomRepositoryExtension = { async getCustomFields(connectionId: string, tableName: string): Promise> { const qb = this.manager .getRepository(TableSettingsEntity) diff --git a/backend/src/entities/custom-field/use-cases/create-custom-fields.use.case.ts b/backend/src/entities/custom-field/use-cases/create-custom-fields.use.case.ts index bbf20d4a5..c84fdb1e3 100644 --- a/backend/src/entities/custom-field/use-cases/create-custom-fields.use.case.ts +++ b/backend/src/entities/custom-field/use-cases/create-custom-fields.use.case.ts @@ -30,7 +30,7 @@ export class CreateCustomFieldsUseCase connectionId, masterPwd, ); - await validateCreateCustomFieldDto(createFieldDto, foundConnection, userId, tableName); + await validateCreateCustomFieldDto(createFieldDto, foundConnection, tableName); const foundTableSettingToUpdate: TableSettingsEntity = await this._dbContext.tableSettingsRepository.findTableSettingsWithCustomFields(connectionId, tableName); const newCustomFieldEntity = buildNewCustomFieldsEntity(inputData); diff --git a/backend/src/entities/custom-field/use-cases/delete-custom-field.use.case.ts b/backend/src/entities/custom-field/use-cases/delete-custom-field.use.case.ts index ab4192833..915e41061 100644 --- a/backend/src/entities/custom-field/use-cases/delete-custom-field.use.case.ts +++ b/backend/src/entities/custom-field/use-cases/delete-custom-field.use.case.ts @@ -29,7 +29,7 @@ export class DeleteCustomFieldUseCase { message: Messages.CUSTOM_FIELD_NOT_FOUND, }, - HttpStatus.BAD_REQUEST, + HttpStatus.NOT_FOUND, ); } const tableSettingsToUpdate = await this._dbContext.tableSettingsRepository.findTableSettingsWithCustomFields( @@ -41,7 +41,7 @@ export class DeleteCustomFieldUseCase { message: Messages.TABLE_SETTINGS_NOT_FOUND, }, - HttpStatus.BAD_REQUEST, + HttpStatus.NOT_FOUND, ); } const delIndex = tableSettingsToUpdate.custom_fields.findIndex((field) => field.id === fieldId); diff --git a/backend/src/entities/custom-field/use-cases/update-custom-field.use.case.ts b/backend/src/entities/custom-field/use-cases/update-custom-field.use.case.ts index f081a3b0c..eb735ef58 100644 --- a/backend/src/entities/custom-field/use-cases/update-custom-field.use.case.ts +++ b/backend/src/entities/custom-field/use-cases/update-custom-field.use.case.ts @@ -24,23 +24,23 @@ export class UpdateCustomFieldUseCase protected async implementation(inputData: UpdateCustomFieldsDs): Promise { const updateFieldDto = inputData.updateFieldDto; - const { connectionId, tableName, masterPwd, userId } = inputData; + const { connectionId, tableName, masterPwd } = inputData; const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection( connectionId, masterPwd, ); - await validateCreateCustomFieldDto(updateFieldDto, foundConnection, userId, tableName); + await validateCreateCustomFieldDto(updateFieldDto, foundConnection, tableName); const fieldToUpdate = await this._dbContext.customFieldsRepository.findCustomFieldById(updateFieldDto.id); if (!fieldToUpdate) { throw new HttpException( { message: Messages.CUSTOM_FIELD_NOT_FOUND, }, - HttpStatus.BAD_REQUEST, + HttpStatus.NOT_FOUND, ); } - delete updateFieldDto.id; - const updated = Object.assign(fieldToUpdate, updateFieldDto); + const { id: _, ...fieldsToUpdate } = updateFieldDto; + const updated = Object.assign(fieldToUpdate, fieldsToUpdate); const updatedCustomFields = await this._dbContext.customFieldsRepository.saveCustomFieldsEntity(updated); return buildFoundCustomFieldsDs(updatedCustomFields); } diff --git a/backend/src/entities/custom-field/utils/build-found-custom-fields-ds.ts b/backend/src/entities/custom-field/utils/build-found-custom-fields-ds.ts index 29704bc35..06c011f64 100644 --- a/backend/src/entities/custom-field/utils/build-found-custom-fields-ds.ts +++ b/backend/src/entities/custom-field/utils/build-found-custom-fields-ds.ts @@ -3,10 +3,5 @@ import { CustomFieldsEntity } from '../custom-fields.entity.js'; export function buildFoundCustomFieldsDs(customField: CustomFieldsEntity): FoundCustomFieldsDs { const { id, template_string, text, type } = customField; - return { - id: id, - template_string: template_string, - text: text, - type: type, - }; + return { id, template_string, text, type }; } diff --git a/backend/src/entities/custom-field/utils/validate-create-custom-field-dto.ts b/backend/src/entities/custom-field/utils/validate-create-custom-field-dto.ts index 6b518f0f0..531030f55 100644 --- a/backend/src/entities/custom-field/utils/validate-create-custom-field-dto.ts +++ b/backend/src/entities/custom-field/utils/validate-create-custom-field-dto.ts @@ -9,7 +9,6 @@ import { CreateFieldDto } from '../application/data-structures/create-custom-fie export async function validateCreateCustomFieldDto( createFieldDto: CreateFieldDto, connection: ConnectionEntity, - _userId: string, tableName: string, ): Promise { const errors = []; diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-custom-field-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-custom-field-e2e.test.ts index 263bca1d0..15713e2ea 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-custom-field-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-custom-field-e2e.test.ts @@ -1638,7 +1638,7 @@ test.serial(`${currentTest} should throw exception, when tableName passed in req .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Accept', 'application/json'); - t.is(deleteCustomField.status, 400); + t.is(deleteCustomField.status, 404); const deleteCustomFieldsRO = JSON.parse(deleteCustomField.text); t.is(deleteCustomFieldsRO.message, Messages.TABLE_SETTINGS_NOT_FOUND); }); @@ -1746,7 +1746,7 @@ test.serial(`${currentTest} should throw exception, when field id passed in requ .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Accept', 'application/json'); - t.is(deleteCustomField.status, 400); + t.is(deleteCustomField.status, 404); const deleteCustomFieldsRO = JSON.parse(deleteCustomField.text); t.is(deleteCustomFieldsRO.message, Messages.CUSTOM_FIELD_NOT_FOUND); }); diff --git a/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts b/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts index 9c9d449d4..c235ad1ac 100644 --- a/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts @@ -1632,7 +1632,7 @@ test.serial(`${currentTest} should throw exception, when tableName passed in req .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Accept', 'application/json'); - t.is(deleteCustomField.status, 400); + t.is(deleteCustomField.status, 404); const deleteCustomFieldsRO = JSON.parse(deleteCustomField.text); t.is(deleteCustomFieldsRO.message, Messages.TABLE_SETTINGS_NOT_FOUND); }); @@ -1740,7 +1740,7 @@ test.serial(`${currentTest} should throw exception, when field id passed in requ .set('Cookie', token) .set('masterpwd', 'ahalaimahalai') .set('Accept', 'application/json'); - t.is(deleteCustomField.status, 400); + t.is(deleteCustomField.status, 404); const deleteCustomFieldsRO = JSON.parse(deleteCustomField.text); t.is(deleteCustomFieldsRO.message, Messages.CUSTOM_FIELD_NOT_FOUND); }); From a7cd65fa5cd91605cd7f47e437635ed0ca7b1748 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 14:05:22 +0000 Subject: [PATCH 6/7] refactor: enhance async handling and improve variable usage in group-related use cases --- backend/src/entities/group/group.controller.ts | 10 +++++----- backend/src/entities/group/group.module.ts | 2 +- .../repository/group-custom-repository-extension.ts | 10 +++++----- .../group/repository/group.repository.interface.ts | 2 +- .../use-cases/find-all-users-in-group.use.case.ts | 2 +- .../use-cases/saas-add-user-in-group-v2.use.case.ts | 3 ++- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/backend/src/entities/group/group.controller.ts b/backend/src/entities/group/group.controller.ts index f6dc23745..61ed095cc 100644 --- a/backend/src/entities/group/group.controller.ts +++ b/backend/src/entities/group/group.controller.ts @@ -90,7 +90,7 @@ export class GroupController { @SlugUuid('groupId') groupId: string, ): Promise> { try { - return this.findAllUsersInGroupUseCase.execute(groupId, InTransactionEnum.OFF); + return await this.findAllUsersInGroupUseCase.execute(groupId, InTransactionEnum.OFF); } finally { await this.amplitudeService.formAndSendLogRecord(AmplitudeEventTypeEnum.groupUserListReceived, userId); } @@ -118,9 +118,9 @@ export class GroupController { Cacher.increaseUserInvitationsCacheCount(userId); Cacher.increaseGroupInvitationsCacheCount(groupId); const inputData: AddUserInGroupWithSaaSDs = { - companyId: companyId, - email: email, - groupId: groupId, + companyId, + email, + groupId, userSaasRole: role, inviterId: userId, }; @@ -141,7 +141,7 @@ export class GroupController { @Delete('/group/:groupId') async delete(@SlugUuid('groupId') groupId: string, @UserId() userId: string): Promise { try { - return this.deleteGroupUseCase.execute(groupId, InTransactionEnum.ON); + return await this.deleteGroupUseCase.execute(groupId, InTransactionEnum.ON); } finally { await this.amplitudeService.formAndSendLogRecord(AmplitudeEventTypeEnum.groupDeleted, userId); } diff --git a/backend/src/entities/group/group.module.ts b/backend/src/entities/group/group.module.ts index 06eb82f92..63e092f8b 100644 --- a/backend/src/entities/group/group.module.ts +++ b/backend/src/entities/group/group.module.ts @@ -64,7 +64,7 @@ import { UpdateGroupTitleUseCase } from './use-cases/update-group-title.use.case exports: [], }) export class GroupModule { - public configure(consumer: MiddlewareConsumer): any { + public configure(consumer: MiddlewareConsumer): void { consumer .apply(AuthMiddleware) .forRoutes( diff --git a/backend/src/entities/group/repository/group-custom-repository-extension.ts b/backend/src/entities/group/repository/group-custom-repository-extension.ts index 48df0bee7..38afa14e3 100644 --- a/backend/src/entities/group/repository/group-custom-repository-extension.ts +++ b/backend/src/entities/group/repository/group-custom-repository-extension.ts @@ -27,8 +27,8 @@ export const groupCustomRepositoryExtension: IGroupRepository = { async findGroupInConnection(groupId: string, connectionId: string): Promise { const qb = this.createQueryBuilder('group') .leftJoinAndSelect('group.connection', 'connection') - .andWhere('group.id = :groupId', { groupId: groupId }) - .andWhere('connection.id = :connectionId', { connectionId: connectionId }); + .andWhere('group.id = :groupId', { groupId }) + .andWhere('connection.id = :connectionId', { connectionId }); return await qb.getOne(); }, @@ -36,12 +36,12 @@ export const groupCustomRepositoryExtension: IGroupRepository = { return await this.remove(group); }, - async findAllUserGroupsInConnection(connectionId: string, cognitoUserName: string): Promise> { + async findAllUserGroupsInConnection(connectionId: string, userId: string): Promise> { const qb = this.createQueryBuilder('group') .leftJoinAndSelect('group.connection', 'connection') .leftJoinAndSelect('group.users', 'user') - .andWhere('connection.id = :connectionId', { connectionId: connectionId }) - .andWhere('user.id = :cognitoUserName', { cognitoUserName: cognitoUserName }); + .andWhere('connection.id = :connectionId', { connectionId }) + .andWhere('user.id = :userId', { userId }); return await qb.getMany(); }, diff --git a/backend/src/entities/group/repository/group.repository.interface.ts b/backend/src/entities/group/repository/group.repository.interface.ts index 4b3dc7390..5f803603c 100644 --- a/backend/src/entities/group/repository/group.repository.interface.ts +++ b/backend/src/entities/group/repository/group.repository.interface.ts @@ -13,7 +13,7 @@ export interface IGroupRepository { removeGroupEntity(group: GroupEntity): Promise; - findAllUserGroupsInConnection(connectionId: string, cognitoUserName: string): Promise>; + findAllUserGroupsInConnection(connectionId: string, userId: string): Promise>; findGroupByIdWithConnectionAndUsers(groupId: string): Promise; diff --git a/backend/src/entities/group/use-cases/find-all-users-in-group.use.case.ts b/backend/src/entities/group/use-cases/find-all-users-in-group.use.case.ts index c85b4b05a..6c7d2b24d 100644 --- a/backend/src/entities/group/use-cases/find-all-users-in-group.use.case.ts +++ b/backend/src/entities/group/use-cases/find-all-users-in-group.use.case.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { UserEntity } from 'src/entities/user/user.entity.js'; +import { UserEntity } from '../../user/user.entity.js'; import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; diff --git a/backend/src/entities/group/use-cases/saas-add-user-in-group-v2.use.case.ts b/backend/src/entities/group/use-cases/saas-add-user-in-group-v2.use.case.ts index df4521138..c8d9e00d7 100644 --- a/backend/src/entities/group/use-cases/saas-add-user-in-group-v2.use.case.ts +++ b/backend/src/entities/group/use-cases/saas-add-user-in-group-v2.use.case.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpStatus, Inject } from '@nestjs/common'; +import { HttpException, HttpStatus, Inject, Injectable, Scope } from '@nestjs/common'; import AbstractUseCase from '../../../common/abstract-use.case.js'; import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; import { BaseType } from '../../../common/data-injection.tokens.js'; @@ -9,6 +9,7 @@ import { AddedUserInGroupDs } from '../application/data-sctructures/added-user-i import { buildFoundGroupResponseDto } from '../utils/biuld-found-group-response.dto.js'; import { IAddUserInGroup } from './use-cases.interfaces.js'; +@Injectable({ scope: Scope.REQUEST }) export class AddUserInGroupUseCase extends AbstractUseCase implements IAddUserInGroup From 07a446795b3af9f0401f2fd8e77afb58b68b4808 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 24 Mar 2026 14:13:14 +0000 Subject: [PATCH 7/7] refactor: simplify email validation and optimize mailing results handling in CronJobsService --- .../src/entities/cron-jobs/cron-jobs.service.ts | 16 +++++++--------- .../src/entities/cron-jobs/job-list.entity.ts | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/src/entities/cron-jobs/cron-jobs.service.ts b/backend/src/entities/cron-jobs/cron-jobs.service.ts index 4a4ece987..39d24eee8 100644 --- a/backend/src/entities/cron-jobs/cron-jobs.service.ts +++ b/backend/src/entities/cron-jobs/cron-jobs.service.ts @@ -7,6 +7,7 @@ import { UseCaseType } from '../../common/data-injection.tokens.js'; import { Constants } from '../../helpers/constants/constants.js'; import { slackPostMessage } from '../../helpers/index.js'; import { ValidationHelper } from '../../helpers/validators/validation-helper.js'; +import Mail from 'nodemailer/lib/mailer/index.js'; import { EmailService, ICronMessagingResults } from '../email/email/email.service.js'; import { ICheckUsersActionsAndMailingUsers, @@ -60,9 +61,7 @@ export class CronJobsService { ); const emailsBefore = emails.length; - emails = emails.filter((email) => { - return ValidationHelper.isValidEmail(email); - }); + emails = emails.filter(ValidationHelper.isValidEmail); const filteredOutEmailsCount = emailsBefore - emails.length; @@ -72,13 +71,13 @@ export class CronJobsService { ); const batchSize = 10; - let allMailingResults = []; + const allMailingResults: Array = []; for (let i = 0; i < emails.length; i += batchSize) { const emailsBatch = emails.slice(i, i + batchSize); try { const batchResults = await this.emailService.sendRemindersToUsers(emailsBatch); - allMailingResults = [...allMailingResults, ...batchResults]; + allMailingResults.push(...batchResults); } catch (error) { console.error(`Error processing batch ${Math.floor(i / batchSize) + 1}: ${error.message}`); Sentry.captureException(error); @@ -89,11 +88,10 @@ export class CronJobsService { if (allMailingResults.length === 0) { const mailingResultToString = 'Sending emails triggered, but no emails sent (no users found)'; await slackPostMessage(mailingResultToString, Constants.EXCEPTIONS_CHANNELS); - await slackPostMessage(`morning cron finished at ${this.getCurrentTime()}`, Constants.EXCEPTIONS_CHANNELS); } else { await this.sendEmailResultsToSlack(allMailingResults, emails); - await slackPostMessage(`morning cron finished at ${this.getCurrentTime()}`, Constants.EXCEPTIONS_CHANNELS); } + await slackPostMessage(`morning cron finished at ${this.getCurrentTime()}`, Constants.EXCEPTIONS_CHANNELS); } catch (innerError) { console.error('Detailed error in email processing:', innerError); const errorMessage = innerError.stack @@ -158,11 +156,11 @@ export class CronJobsService { results: Array, allFoundEmails: Array, ): Promise { - const filteredResults = results.filter((result) => !!result); + const filteredResults = results.filter((result): result is ICronMessagingResults => !!result); const nullResultsCount = results.length - filteredResults.length; const chunkSize = 20; - const foundEmails = new Set(); + const foundEmails = new Set(); filteredResults.forEach((result) => { if (result?.accepted) { result.accepted.forEach((email) => foundEmails.add(email)); diff --git a/backend/src/entities/cron-jobs/job-list.entity.ts b/backend/src/entities/cron-jobs/job-list.entity.ts index 3fd87a62c..670621853 100644 --- a/backend/src/entities/cron-jobs/job-list.entity.ts +++ b/backend/src/entities/cron-jobs/job-list.entity.ts @@ -2,7 +2,7 @@ import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('job_list') export class JobListEntity { - @PrimaryColumn({ type: 'int', nullable: false, unique: true }) + @PrimaryColumn({ type: 'int' }) id: number; @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })