From 41179f71c139dad156ab71b6810f7748c762d083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 12 May 2025 15:21:13 +0200 Subject: [PATCH] refactor(core): Move settings repository to @n8n/db (#15310) --- packages/@n8n/db/src/repositories/index.ts | 1 + .../src/repositories/settings.repository.ts | 15 +++++ .../cli/src/__tests__/external-hooks.test.ts | 3 +- packages/cli/src/commands/ldap/reset.ts | 2 +- packages/cli/src/commands/start.ts | 3 +- .../cli/src/commands/user-management/reset.ts | 2 +- .../__tests__/owner.controller.test.ts | 10 ++-- .../cli/src/controllers/e2e.controller.ts | 3 +- .../cli/src/controllers/owner.controller.ts | 6 +- .../repositories/settings.repository.ts | 58 ------------------ .../source-control-preferences.service.ee.ts | 3 +- packages/cli/src/external-hooks.ts | 3 +- .../external-secrets-manager.ee.test.ts | 17 +++--- .../external-secrets-manager.ee.ts | 22 +++++-- .../ldap.ee/__tests__/ldap.service.test.ts | 3 +- packages/cli/src/ldap.ee/ldap.service.ee.ts | 2 +- packages/cli/src/license.ts | 2 +- .../services/__tests__/banner.service.test.ts | 59 +++++++++++++++++++ .../services/__tests__/hooks.service.test.ts | 3 +- packages/cli/src/services/banner.service.ts | 37 ++++++++++++ packages/cli/src/services/hooks.service.ts | 3 +- .../saml/__tests__/saml.service.ee.test.ts | 2 +- .../cli/src/sso.ee/saml/saml.service.ee.ts | 2 +- packages/cli/src/sso.ee/sso-helpers.ts | 3 +- .../integration/commands/reset.cmd.test.ts | 3 +- .../external-secrets.api.test.ts | 14 ++--- packages/cli/test/integration/shared/ldap.ts | 2 +- .../test/integration/shared/utils/index.ts | 3 +- 28 files changed, 171 insertions(+), 115 deletions(-) create mode 100644 packages/@n8n/db/src/repositories/settings.repository.ts delete mode 100644 packages/cli/src/databases/repositories/settings.repository.ts create mode 100644 packages/cli/src/services/__tests__/banner.service.test.ts create mode 100644 packages/cli/src/services/banner.service.ts diff --git a/packages/@n8n/db/src/repositories/index.ts b/packages/@n8n/db/src/repositories/index.ts index dd9181d582..4e7b4aa1c6 100644 --- a/packages/@n8n/db/src/repositories/index.ts +++ b/packages/@n8n/db/src/repositories/index.ts @@ -19,6 +19,7 @@ export { LicenseMetricsRepository } from './license-metrics.repository'; export { ProjectRelationRepository } from './project-relation.repository'; export { ProjectRepository } from './project.repository'; export { ProcessedDataRepository } from './processed-data.repository'; +export { SettingsRepository } from './settings.repository'; export { TagRepository } from './tag.repository'; export { TestCaseExecutionRepository } from './test-case-execution.repository.ee'; export { TestDefinitionRepository } from './test-definition.repository.ee'; diff --git a/packages/@n8n/db/src/repositories/settings.repository.ts b/packages/@n8n/db/src/repositories/settings.repository.ts new file mode 100644 index 0000000000..8632264afa --- /dev/null +++ b/packages/@n8n/db/src/repositories/settings.repository.ts @@ -0,0 +1,15 @@ +import { Service } from '@n8n/di'; +import { DataSource, Repository } from '@n8n/typeorm'; + +import { Settings } from '../entities'; + +@Service() +export class SettingsRepository extends Repository { + constructor(dataSource: DataSource) { + super(Settings, dataSource.manager); + } + + async findByKey(key: string): Promise { + return await this.findOneBy({ key }); + } +} diff --git a/packages/cli/src/__tests__/external-hooks.test.ts b/packages/cli/src/__tests__/external-hooks.test.ts index f9255b927a..35cad959b2 100644 --- a/packages/cli/src/__tests__/external-hooks.test.ts +++ b/packages/cli/src/__tests__/external-hooks.test.ts @@ -1,11 +1,10 @@ import type { GlobalConfig } from '@n8n/config'; -import type { CredentialsRepository } from '@n8n/db'; +import type { CredentialsRepository, SettingsRepository } from '@n8n/db'; import { mock } from 'jest-mock-extended'; import type { ErrorReporter, Logger } from 'n8n-core'; import type { IWorkflowBase } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow'; -import type { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { UserRepository } from '@/databases/repositories/user.repository'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { ExternalHooks } from '@/external-hooks'; diff --git a/packages/cli/src/commands/ldap/reset.ts b/packages/cli/src/commands/ldap/reset.ts index e70ef5790b..c829671f3a 100644 --- a/packages/cli/src/commands/ldap/reset.ts +++ b/packages/cli/src/commands/ldap/reset.ts @@ -3,6 +3,7 @@ import { AuthProviderSyncHistoryRepository, ProjectRelationRepository, ProjectRepository, + SettingsRepository, SharedCredentialsRepository, SharedWorkflowRepository, } from '@n8n/db'; @@ -14,7 +15,6 @@ import { UserError } from 'n8n-workflow'; import { UM_FIX_INSTRUCTION } from '@/constants'; import { CredentialsService } from '@/credentials/credentials.service'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; import { WorkflowService } from '@/workflows/workflow.service'; diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 86d64daeef..674fc85dbc 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { LICENSE_FEATURES } from '@n8n/constants'; -import { ExecutionRepository } from '@n8n/db'; +import { ExecutionRepository, SettingsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { Flags } from '@oclif/core'; import glob from 'fast-glob'; @@ -16,7 +16,6 @@ import { ActiveExecutions } from '@/active-executions'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; import { EDITOR_UI_DIST_DIR } from '@/constants'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { EventService } from '@/events/event.service'; diff --git a/packages/cli/src/commands/user-management/reset.ts b/packages/cli/src/commands/user-management/reset.ts index cec45a1aa2..8909e3be0a 100644 --- a/packages/cli/src/commands/user-management/reset.ts +++ b/packages/cli/src/commands/user-management/reset.ts @@ -3,12 +3,12 @@ import { User, CredentialsRepository, ProjectRepository, + SettingsRepository, SharedCredentialsRepository, SharedWorkflowRepository, } from '@n8n/db'; import { Container } from '@n8n/di'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { BaseCommand } from '../base-command'; diff --git a/packages/cli/src/controllers/__tests__/owner.controller.test.ts b/packages/cli/src/controllers/__tests__/owner.controller.test.ts index 44a8888d09..5a35cbacc7 100644 --- a/packages/cli/src/controllers/__tests__/owner.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/owner.controller.test.ts @@ -1,6 +1,6 @@ import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import type { User } from '@n8n/db'; -import type { PublicUser } from '@n8n/db'; +import type { PublicUser, SettingsRepository } from '@n8n/db'; import type { Response } from 'express'; import { mock } from 'jest-mock-extended'; import type { Logger } from 'n8n-core'; @@ -8,11 +8,11 @@ import type { Logger } from 'n8n-core'; import type { AuthService } from '@/auth/auth.service'; import config from '@/config'; import { OwnerController } from '@/controllers/owner.controller'; -import type { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { UserRepository } from '@/databases/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import type { EventService } from '@/events/event.service'; import type { AuthenticatedRequest } from '@/requests'; +import type { BannerService } from '@/services/banner.service'; import type { PasswordUtility } from '@/services/password.utility'; import type { UserService } from '@/services/user.service'; @@ -23,6 +23,7 @@ describe('OwnerController', () => { const logger = mock(); const eventService = mock(); const authService = mock(); + const bannerService = mock(); const userService = mock(); const userRepository = mock(); const settingsRepository = mock(); @@ -33,6 +34,7 @@ describe('OwnerController', () => { eventService, settingsRepository, authService, + bannerService, userService, passwordUtility, mock(), @@ -100,7 +102,7 @@ describe('OwnerController', () => { const result = await controller.dismissBanner(mock(), mock(), payload); - expect(settingsRepository.dismissBanner).not.toHaveBeenCalled(); + expect(bannerService.dismissBanner).not.toHaveBeenCalled(); expect(result).toBeUndefined(); }); @@ -109,7 +111,7 @@ describe('OwnerController', () => { await controller.dismissBanner(mock(), mock(), payload); - expect(settingsRepository.dismissBanner).toHaveBeenCalledWith({ bannerName: 'TRIAL' }); + expect(bannerService.dismissBanner).toHaveBeenCalledWith('TRIAL'); }); }); }); diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 28d26d0ce0..e2fba58f8d 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -1,7 +1,7 @@ import type { PushMessage } from '@n8n/api-types'; import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants'; import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA } from '@n8n/constants'; -import { AuthUserRepository } from '@n8n/db'; +import { AuthUserRepository, SettingsRepository } from '@n8n/db'; import { Patch, Post, RestController } from '@n8n/decorators'; import { Container } from '@n8n/di'; import { Request } from 'express'; @@ -11,7 +11,6 @@ import { v4 as uuid } from 'uuid'; import { ActiveWorkflowManager } from '@/active-workflow-manager'; import config from '@/config'; import { inE2ETests } from '@/constants'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import type { FeatureReturnType } from '@/license'; diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 7497a099a2..35c61ee02e 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,17 +1,18 @@ import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; +import { SettingsRepository } from '@n8n/db'; import { Body, GlobalScope, Post, RestController } from '@n8n/decorators'; import { Response } from 'express'; import { Logger } from 'n8n-core'; import { AuthService } from '@/auth/auth.service'; import config from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { EventService } from '@/events/event.service'; import { validateEntity } from '@/generic-helpers'; import { PostHogClient } from '@/posthog'; import { AuthenticatedRequest } from '@/requests'; +import { BannerService } from '@/services/banner.service'; import { PasswordUtility } from '@/services/password.utility'; import { UserService } from '@/services/user.service'; @@ -22,6 +23,7 @@ export class OwnerController { private readonly eventService: EventService, private readonly settingsRepository: SettingsRepository, private readonly authService: AuthService, + private readonly bannerService: BannerService, private readonly userService: UserService, private readonly passwordUtility: PasswordUtility, private readonly postHog: PostHogClient, @@ -83,6 +85,6 @@ export class OwnerController { ) { const bannerName = payload.banner; if (!bannerName) return; - return await this.settingsRepository.dismissBanner({ bannerName }); + await this.bannerService.dismissBanner(bannerName); } } diff --git a/packages/cli/src/databases/repositories/settings.repository.ts b/packages/cli/src/databases/repositories/settings.repository.ts deleted file mode 100644 index 7c8a8ef29d..0000000000 --- a/packages/cli/src/databases/repositories/settings.repository.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Settings } from '@n8n/db'; -import { Service } from '@n8n/di'; -import { DataSource, Repository } from '@n8n/typeorm'; -import { ErrorReporter } from 'n8n-core'; - -import config from '@/config'; -import { EXTERNAL_SECRETS_DB_KEY } from '@/external-secrets.ee/constants'; - -@Service() -export class SettingsRepository extends Repository { - constructor( - dataSource: DataSource, - private readonly errorReporter: ErrorReporter, - ) { - super(Settings, dataSource.manager); - } - - async getEncryptedSecretsProviderSettings(): Promise { - return (await this.findByKey(EXTERNAL_SECRETS_DB_KEY))?.value ?? null; - } - - async findByKey(key: string): Promise { - return await this.findOneBy({ key }); - } - - async saveEncryptedSecretsProviderSettings(data: string): Promise { - await this.upsert( - { - key: EXTERNAL_SECRETS_DB_KEY, - value: data, - loadOnStartup: false, - }, - ['key'], - ); - } - - async dismissBanner({ bannerName }: { bannerName: string }): Promise<{ success: boolean }> { - const key = 'ui.banners.dismissed'; - const dismissedBannersSetting = await this.findOneBy({ key }); - try { - let value: string; - if (dismissedBannersSetting) { - const dismissedBanners = JSON.parse(dismissedBannersSetting.value) as string[]; - const updatedValue = [...new Set([...dismissedBanners, bannerName].sort())]; - value = JSON.stringify(updatedValue); - await this.update({ key }, { value, loadOnStartup: true }); - } else { - value = JSON.stringify([bannerName]); - await this.save({ key, value, loadOnStartup: true }, { transaction: false }); - } - config.set(key, value); - return { success: true }; - } catch (error) { - this.errorReporter.error(error); - } - return { success: false }; - } -} diff --git a/packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts b/packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts index 5ae7068a5e..d1cd320169 100644 --- a/packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts +++ b/packages/cli/src/environments.ee/source-control/source-control-preferences.service.ee.ts @@ -1,3 +1,4 @@ +import { SettingsRepository } from '@n8n/db'; import { Service } from '@n8n/di'; import type { ValidationError } from 'class-validator'; import { validate } from 'class-validator'; @@ -7,8 +8,6 @@ import { jsonParse, UnexpectedError } from 'n8n-workflow'; import { writeFile, chmod, readFile } from 'node:fs/promises'; import path from 'path'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; - import { SOURCE_CONTROL_SSH_FOLDER, SOURCE_CONTROL_GIT_FOLDER, diff --git a/packages/cli/src/external-hooks.ts b/packages/cli/src/external-hooks.ts index 6cae7572fb..470b61b312 100644 --- a/packages/cli/src/external-hooks.ts +++ b/packages/cli/src/external-hooks.ts @@ -2,7 +2,7 @@ import type { FrontendSettings, UserUpdateRequestDto } from '@n8n/api-types'; import type { ClientOAuth2Options } from '@n8n/client-oauth2'; import { GlobalConfig } from '@n8n/config'; import type { TagEntity, User, ICredentialsDb, PublicUser } from '@n8n/db'; -import { CredentialsRepository } from '@n8n/db'; +import { CredentialsRepository, SettingsRepository } from '@n8n/db'; import { Service } from '@n8n/di'; import { ErrorReporter, Logger } from 'n8n-core'; import type { IRun, IWorkflowBase, Workflow, WorkflowExecuteMode } from 'n8n-workflow'; @@ -11,7 +11,6 @@ import type clientOAuth1 from 'oauth-1.0a'; import type { AbstractServer } from '@/abstract-server'; import type { Config } from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; diff --git a/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts index e7a308586f..68aa52efc2 100644 --- a/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts +++ b/packages/cli/src/external-secrets.ee/__tests__/external-secrets-manager.ee.test.ts @@ -1,6 +1,6 @@ -import { mock } from 'jest-mock-extended'; +import type { Settings, SettingsRepository } from '@n8n/db'; +import { captor, mock } from 'jest-mock-extended'; -import type { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { License } from '@/license'; import { AnotherDummyProvider, @@ -11,6 +11,7 @@ import { } from '@test/external-secrets/utils'; import { mockCipher, mockLogger } from '@test/mocking'; +import { EXTERNAL_SECRETS_DB_KEY } from '../constants'; import { ExternalSecretsManager } from '../external-secrets-manager.ee'; import type { ExternalSecretsSettings } from '../types'; @@ -44,9 +45,9 @@ describe('External Secrets Manager', () => { }); license.isExternalSecretsEnabled.mockReturnValue(true); - settingsRepo.getEncryptedSecretsProviderSettings.mockImplementation(async () => - JSON.stringify(settings), - ); + settingsRepo.findByKey + .calledWith(EXTERNAL_SECRETS_DB_KEY) + .mockImplementation(async () => mock({ value: JSON.stringify(settings) })); manager = new ExternalSecretsManager( mockLogger(), @@ -251,7 +252,7 @@ describe('External Secrets Manager', () => { describe('setProviderSettings', () => { test('should save provider settings', async () => { - const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings'); + const settingsSpy = jest.spyOn(settingsRepo, 'upsert'); await manager.init(); @@ -259,7 +260,9 @@ describe('External Secrets Manager', () => { test: 'value', }); - expect(JSON.parse(settingsSpy.mock.calls[0][0])).toEqual( + const settingsCaptor = captor(); + expect(settingsSpy).toHaveBeenCalledWith(settingsCaptor, ['key']); + expect(JSON.parse(settingsCaptor.value.value)).toEqual( expect.objectContaining({ dummy: { connected: true, diff --git a/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts b/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts index 84e6943cb6..94c72cd443 100644 --- a/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts +++ b/packages/cli/src/external-secrets.ee/external-secrets-manager.ee.ts @@ -1,14 +1,18 @@ +import { SettingsRepository } from '@n8n/db'; import { OnShutdown } from '@n8n/decorators'; import { Service } from '@n8n/di'; import { Cipher, Logger } from 'n8n-core'; import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { EventService } from '@/events/event.service'; import { License } from '@/license'; import { Publisher } from '@/scaling/pubsub/publisher.service'; -import { EXTERNAL_SECRETS_INITIAL_BACKOFF, EXTERNAL_SECRETS_MAX_BACKOFF } from './constants'; +import { + EXTERNAL_SECRETS_DB_KEY, + EXTERNAL_SECRETS_INITIAL_BACKOFF, + EXTERNAL_SECRETS_MAX_BACKOFF, +} from './constants'; import { ExternalSecretsProviders } from './external-secrets-providers.ee'; import { ExternalSecretsConfig } from './external-secrets.config'; import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types'; @@ -89,8 +93,9 @@ export class ExternalSecretsManager { void this.publisher.publishCommand({ command: 'reload-external-secrets-providers' }); } - private async getDecryptedSettings(): Promise { - const encryptedSettings = await this.settingsRepo.getEncryptedSecretsProviderSettings(); + async getDecryptedSettings(): Promise { + const encryptedSettings = + (await this.settingsRepo.findByKey(EXTERNAL_SECRETS_DB_KEY))?.value ?? null; if (encryptedSettings === null) { return null; } @@ -324,7 +329,14 @@ export class ExternalSecretsManager { async saveAndSetSettings(settings: ExternalSecretsSettings) { const encryptedSettings = this.encryptSecretsSettings(settings); - await this.settingsRepo.saveEncryptedSecretsProviderSettings(encryptedSettings); + await this.settingsRepo.upsert( + { + key: EXTERNAL_SECRETS_DB_KEY, + value: encryptedSettings, + loadOnStartup: false, + }, + ['key'], + ); } async testProviderSettings( diff --git a/packages/cli/src/ldap.ee/__tests__/ldap.service.test.ts b/packages/cli/src/ldap.ee/__tests__/ldap.service.test.ts index 7577addacc..6ebfeb21e8 100644 --- a/packages/cli/src/ldap.ee/__tests__/ldap.service.test.ts +++ b/packages/cli/src/ldap.ee/__tests__/ldap.service.test.ts @@ -1,5 +1,5 @@ import type { Settings } from '@n8n/db'; -import { AuthIdentityRepository } from '@n8n/db'; +import { AuthIdentityRepository, SettingsRepository } from '@n8n/db'; import { QueryFailedError } from '@n8n/typeorm'; import { mock } from 'jest-mock-extended'; import { Client } from 'ldapts'; @@ -7,7 +7,6 @@ import type { Cipher } from 'n8n-core'; import { randomString } from 'n8n-workflow'; import config from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { EventService } from '@/events/event.service'; import { mockInstance, mockLogger } from '@test/mocking'; diff --git a/packages/cli/src/ldap.ee/ldap.service.ee.ts b/packages/cli/src/ldap.ee/ldap.service.ee.ts index d3c16373e5..f85dc7c5a9 100644 --- a/packages/cli/src/ldap.ee/ldap.service.ee.ts +++ b/packages/cli/src/ldap.ee/ldap.service.ee.ts @@ -1,3 +1,4 @@ +import { SettingsRepository } from '@n8n/db'; import type { User, RunningMode, SyncStatus } from '@n8n/db'; import { Service } from '@n8n/di'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import @@ -9,7 +10,6 @@ import { jsonParse, UnexpectedError } from 'n8n-workflow'; import type { ConnectionOptions } from 'tls'; import config from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { EventService } from '@/events/event.service'; diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index f338e798ee..bb185b2699 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -7,6 +7,7 @@ import { type BooleanLicenseFeature, type NumericLicenseFeature, } from '@n8n/constants'; +import { SettingsRepository } from '@n8n/db'; import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators'; import { Container, Service } from '@n8n/di'; import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk'; @@ -14,7 +15,6 @@ import { LicenseManager } from '@n8n_io/license-sdk'; import { InstanceSettings, Logger } from 'n8n-core'; import config from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { LicenseMetricsService } from '@/metrics/license-metrics.service'; import { N8N_VERSION, SETTINGS_LICENSE_CERT_KEY, Time } from './constants'; diff --git a/packages/cli/src/services/__tests__/banner.service.test.ts b/packages/cli/src/services/__tests__/banner.service.test.ts new file mode 100644 index 0000000000..f2bc558297 --- /dev/null +++ b/packages/cli/src/services/__tests__/banner.service.test.ts @@ -0,0 +1,59 @@ +import type { BannerName } from '@n8n/api-types'; +import type { SettingsRepository } from '@n8n/db'; +import { mock } from 'jest-mock-extended'; +import type { ErrorReporter } from 'n8n-core'; +import { UnexpectedError } from 'n8n-workflow'; + +import { BannerService } from '@/services/banner.service'; + +describe('BannerService', () => { + const settingsRepo = mock(); + const errorReporter = mock(); + const bannerService = new BannerService(settingsRepo, errorReporter); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('dismissBanner', () => { + const key = 'ui.banners.dismissed'; + const bannerName: BannerName = 'TRIAL'; + + it('should save the banner name to settings if no banners are dismissed yet', async () => { + settingsRepo.findOneBy.mockResolvedValue(null); + + await bannerService.dismissBanner(bannerName); + + expect(settingsRepo.save).toHaveBeenCalledWith( + { key, value: JSON.stringify([bannerName]), loadOnStartup: true }, + { transaction: false }, + ); + }); + + it('should update settings with the new banner name if banners are already dismissed', async () => { + const dismissedBanners = ['TRIAL_OVER']; + settingsRepo.findOneBy.mockResolvedValue({ + key, + value: JSON.stringify(dismissedBanners), + loadOnStartup: false, + }); + + await bannerService.dismissBanner(bannerName); + + expect(settingsRepo.update).toHaveBeenCalledWith( + { key }, + { value: JSON.stringify([bannerName, 'TRIAL_OVER']), loadOnStartup: true }, + ); + }); + + it('should handle errors when saving settings', async () => { + const error = new UnexpectedError('Test error'); + settingsRepo.findOneBy.mockResolvedValue(null); + settingsRepo.save.mockRejectedValue(error); + + await bannerService.dismissBanner(bannerName); + + expect(errorReporter.error).toHaveBeenCalledWith(error); + }); + }); +}); diff --git a/packages/cli/src/services/__tests__/hooks.service.test.ts b/packages/cli/src/services/__tests__/hooks.service.test.ts index bdc71da141..d091486d52 100644 --- a/packages/cli/src/services/__tests__/hooks.service.test.ts +++ b/packages/cli/src/services/__tests__/hooks.service.test.ts @@ -1,4 +1,4 @@ -import type { AuthUser } from '@n8n/db'; +import type { AuthUser, SettingsRepository } from '@n8n/db'; import type { AuthUserRepository } from '@n8n/db'; import type { CredentialsRepository } from '@n8n/db'; import RudderStack from '@rudderstack/rudder-sdk-node'; @@ -6,7 +6,6 @@ import type { Response } from 'express'; import { mock } from 'jest-mock-extended'; import type { AuthService } from '@/auth/auth.service'; -import type { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { UserRepository } from '@/databases/repositories/user.repository'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { Invitation } from '@/interfaces'; diff --git a/packages/cli/src/services/banner.service.ts b/packages/cli/src/services/banner.service.ts new file mode 100644 index 0000000000..b035586024 --- /dev/null +++ b/packages/cli/src/services/banner.service.ts @@ -0,0 +1,37 @@ +import type { BannerName } from '@n8n/api-types'; +import { SettingsRepository } from '@n8n/db'; +import { Service } from '@n8n/di'; +import { ErrorReporter } from 'n8n-core'; + +import config from '@/config'; + +@Service() +export class BannerService { + constructor( + private readonly settingsRepository: SettingsRepository, + private readonly errorReporter: ErrorReporter, + ) {} + + async dismissBanner(bannerName: BannerName) { + const key = 'ui.banners.dismissed'; + const dismissedBannersSetting = await this.settingsRepository.findOneBy({ key }); + try { + let value: string; + if (dismissedBannersSetting) { + const dismissedBanners = JSON.parse(dismissedBannersSetting.value) as string[]; + const updatedValue = [...new Set([...dismissedBanners, bannerName].sort())]; + value = JSON.stringify(updatedValue); + await this.settingsRepository.update({ key }, { value, loadOnStartup: true }); + } else { + value = JSON.stringify([bannerName]); + await this.settingsRepository.save( + { key, value, loadOnStartup: true }, + { transaction: false }, + ); + } + config.set(key, value); + } catch (error) { + this.errorReporter.error(error); + } + } +} diff --git a/packages/cli/src/services/hooks.service.ts b/packages/cli/src/services/hooks.service.ts index 08b05cca63..1fe622d307 100644 --- a/packages/cli/src/services/hooks.service.ts +++ b/packages/cli/src/services/hooks.service.ts @@ -1,5 +1,5 @@ import type { Settings, CredentialsEntity, User, WorkflowEntity, AuthUser } from '@n8n/db'; -import { AuthUserRepository, CredentialsRepository } from '@n8n/db'; +import { AuthUserRepository, CredentialsRepository, SettingsRepository } from '@n8n/db'; import { Service } from '@n8n/di'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import type { FindManyOptions, FindOneOptions, FindOptionsWhere } from '@n8n/typeorm'; @@ -8,7 +8,6 @@ import RudderStack, { type constructorOptions } from '@rudderstack/rudder-sdk-no import type { NextFunction, Response } from 'express'; import { AuthService } from '@/auth/auth.service'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { Invitation } from '@/interfaces'; diff --git a/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts index ebf34e3075..a7c48e449d 100644 --- a/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts +++ b/packages/cli/src/sso.ee/saml/__tests__/saml.service.ee.test.ts @@ -1,8 +1,8 @@ +import { SettingsRepository } from '@n8n/db'; import type express from 'express'; import { mock } from 'jest-mock-extended'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import * as samlHelpers from '@/sso.ee/saml/saml-helpers'; import { SamlService } from '@/sso.ee/saml/saml.service.ee'; import { mockInstance } from '@test/mocking'; diff --git a/packages/cli/src/sso.ee/saml/saml.service.ee.ts b/packages/cli/src/sso.ee/saml/saml.service.ee.ts index 9eb5682bb2..66467db934 100644 --- a/packages/cli/src/sso.ee/saml/saml.service.ee.ts +++ b/packages/cli/src/sso.ee/saml/saml.service.ee.ts @@ -1,4 +1,5 @@ import type { SamlPreferences } from '@n8n/api-types'; +import { SettingsRepository } from '@n8n/db'; import type { Settings, User } from '@n8n/db'; import { Service } from '@n8n/di'; import axios from 'axios'; @@ -9,7 +10,6 @@ import { jsonParse, UnexpectedError } from 'n8n-workflow'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { AuthError } from '@/errors/response-errors/auth.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; diff --git a/packages/cli/src/sso.ee/sso-helpers.ts b/packages/cli/src/sso.ee/sso-helpers.ts index ae19f8929f..1e5ad54e96 100644 --- a/packages/cli/src/sso.ee/sso-helpers.ts +++ b/packages/cli/src/sso.ee/sso-helpers.ts @@ -1,8 +1,7 @@ -import type { AuthProviderType } from '@n8n/db'; +import { SettingsRepository, type AuthProviderType } from '@n8n/db'; import { Container } from '@n8n/di'; import config from '@/config'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; /** * Only one authentication method can be active at a time. This function sets diff --git a/packages/cli/test/integration/commands/reset.cmd.test.ts b/packages/cli/test/integration/commands/reset.cmd.test.ts index b81e26612f..c4c61f91eb 100644 --- a/packages/cli/test/integration/commands/reset.cmd.test.ts +++ b/packages/cli/test/integration/commands/reset.cmd.test.ts @@ -1,11 +1,10 @@ -import { CredentialsEntity } from '@n8n/db'; +import { CredentialsEntity, SettingsRepository } from '@n8n/db'; import { CredentialsRepository } from '@n8n/db'; import { SharedCredentialsRepository } from '@n8n/db'; import { SharedWorkflowRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { Reset } from '@/commands/user-management/reset'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { NodeTypes } from '@/node-types'; diff --git a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts index f31b322a73..ae58970133 100644 --- a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts +++ b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts @@ -1,11 +1,11 @@ +import { SettingsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; import { Cipher } from 'n8n-core'; -import { jsonParse, type IDataObject } from 'n8n-workflow'; +import type { IDataObject } from 'n8n-workflow'; import config from '@/config'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import type { EventService } from '@/events/event.service'; import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { ExternalSecretsProviders } from '@/external-secrets.ee/external-secrets-providers.ee'; @@ -37,17 +37,11 @@ const testServer = setupTestServer({ const connectedDate = '2023-08-01T12:32:29.000Z'; async function setExternalSecretsSettings(settings: ExternalSecretsSettings) { - return await Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings( - Container.get(Cipher).encrypt(settings), - ); + await Container.get(ExternalSecretsManager).saveAndSetSettings(settings); } async function getExternalSecretsSettings(): Promise { - const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings(); - if (encSettings === null) { - return null; - } - return await jsonParse(Container.get(Cipher).decrypt(encSettings)); + return await Container.get(ExternalSecretsManager).getDecryptedSettings(); } const eventService = mock(); diff --git a/packages/cli/test/integration/shared/ldap.ts b/packages/cli/test/integration/shared/ldap.ts index 9cf37e6b62..e0fd6e1a59 100644 --- a/packages/cli/test/integration/shared/ldap.ts +++ b/packages/cli/test/integration/shared/ldap.ts @@ -1,7 +1,7 @@ +import { SettingsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { jsonParse } from 'n8n-workflow'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; import type { LdapConfig } from '@/ldap.ee/types'; diff --git a/packages/cli/test/integration/shared/utils/index.ts b/packages/cli/test/integration/shared/utils/index.ts index 5f326e604d..643bfcf3f2 100644 --- a/packages/cli/test/integration/shared/utils/index.ts +++ b/packages/cli/test/integration/shared/utils/index.ts @@ -1,4 +1,4 @@ -import { WorkflowEntity } from '@n8n/db'; +import { SettingsRepository, WorkflowEntity } from '@n8n/db'; import { Container } from '@n8n/di'; import { mock } from 'jest-mock-extended'; import { @@ -21,7 +21,6 @@ import { v4 as uuid } from 'uuid'; import config from '@/config'; import { AUTH_COOKIE_NAME } from '@/constants'; -import { SettingsRepository } from '@/databases/repositories/settings.repository'; import { ExecutionService } from '@/executions/execution.service'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { Push } from '@/push';