refactor(core): Move settings repository to @n8n/db (#15310)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2025-05-12 15:21:13 +02:00
committed by GitHub
parent 8cc5a5da3b
commit 41179f71c1
28 changed files with 171 additions and 115 deletions

View File

@@ -19,6 +19,7 @@ export { LicenseMetricsRepository } from './license-metrics.repository';
export { ProjectRelationRepository } from './project-relation.repository'; export { ProjectRelationRepository } from './project-relation.repository';
export { ProjectRepository } from './project.repository'; export { ProjectRepository } from './project.repository';
export { ProcessedDataRepository } from './processed-data.repository'; export { ProcessedDataRepository } from './processed-data.repository';
export { SettingsRepository } from './settings.repository';
export { TagRepository } from './tag.repository'; export { TagRepository } from './tag.repository';
export { TestCaseExecutionRepository } from './test-case-execution.repository.ee'; export { TestCaseExecutionRepository } from './test-case-execution.repository.ee';
export { TestDefinitionRepository } from './test-definition.repository.ee'; export { TestDefinitionRepository } from './test-definition.repository.ee';

View File

@@ -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<Settings> {
constructor(dataSource: DataSource) {
super(Settings, dataSource.manager);
}
async findByKey(key: string): Promise<Settings | null> {
return await this.findOneBy({ key });
}
}

View File

@@ -1,11 +1,10 @@
import type { GlobalConfig } from '@n8n/config'; 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 { mock } from 'jest-mock-extended';
import type { ErrorReporter, Logger } from 'n8n-core'; import type { ErrorReporter, Logger } from 'n8n-core';
import type { IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowBase } from 'n8n-workflow';
import { UnexpectedError } 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 { UserRepository } from '@/databases/repositories/user.repository';
import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { ExternalHooks } from '@/external-hooks'; import { ExternalHooks } from '@/external-hooks';

View File

@@ -3,6 +3,7 @@ import {
AuthProviderSyncHistoryRepository, AuthProviderSyncHistoryRepository,
ProjectRelationRepository, ProjectRelationRepository,
ProjectRepository, ProjectRepository,
SettingsRepository,
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
} from '@n8n/db'; } from '@n8n/db';
@@ -14,7 +15,6 @@ import { UserError } from 'n8n-workflow';
import { UM_FIX_INSTRUCTION } from '@/constants'; import { UM_FIX_INSTRUCTION } from '@/constants';
import { CredentialsService } from '@/credentials/credentials.service'; import { CredentialsService } from '@/credentials/credentials.service';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants';
import { WorkflowService } from '@/workflows/workflow.service'; import { WorkflowService } from '@/workflows/workflow.service';

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { LICENSE_FEATURES } from '@n8n/constants'; import { LICENSE_FEATURES } from '@n8n/constants';
import { ExecutionRepository } from '@n8n/db'; import { ExecutionRepository, SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import glob from 'fast-glob'; import glob from 'fast-glob';
@@ -16,7 +16,6 @@ import { ActiveExecutions } from '@/active-executions';
import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { ActiveWorkflowManager } from '@/active-workflow-manager';
import config from '@/config'; import config from '@/config';
import { EDITOR_UI_DIST_DIR } from '@/constants'; import { EDITOR_UI_DIST_DIR } from '@/constants';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -3,12 +3,12 @@ import {
User, User,
CredentialsRepository, CredentialsRepository,
ProjectRepository, ProjectRepository,
SettingsRepository,
SharedCredentialsRepository, SharedCredentialsRepository,
SharedWorkflowRepository, SharedWorkflowRepository,
} from '@n8n/db'; } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { BaseCommand } from '../base-command'; import { BaseCommand } from '../base-command';

View File

@@ -1,6 +1,6 @@
import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import type { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
import type { User } from '@n8n/db'; 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 type { Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { Logger } from 'n8n-core'; import type { Logger } from 'n8n-core';
@@ -8,11 +8,11 @@ import type { Logger } from 'n8n-core';
import type { AuthService } from '@/auth/auth.service'; import type { AuthService } from '@/auth/auth.service';
import config from '@/config'; import config from '@/config';
import { OwnerController } from '@/controllers/owner.controller'; import { OwnerController } from '@/controllers/owner.controller';
import type { SettingsRepository } from '@/databases/repositories/settings.repository';
import type { UserRepository } from '@/databases/repositories/user.repository'; import type { UserRepository } from '@/databases/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import type { EventService } from '@/events/event.service'; import type { EventService } from '@/events/event.service';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import type { BannerService } from '@/services/banner.service';
import type { PasswordUtility } from '@/services/password.utility'; import type { PasswordUtility } from '@/services/password.utility';
import type { UserService } from '@/services/user.service'; import type { UserService } from '@/services/user.service';
@@ -23,6 +23,7 @@ describe('OwnerController', () => {
const logger = mock<Logger>(); const logger = mock<Logger>();
const eventService = mock<EventService>(); const eventService = mock<EventService>();
const authService = mock<AuthService>(); const authService = mock<AuthService>();
const bannerService = mock<BannerService>();
const userService = mock<UserService>(); const userService = mock<UserService>();
const userRepository = mock<UserRepository>(); const userRepository = mock<UserRepository>();
const settingsRepository = mock<SettingsRepository>(); const settingsRepository = mock<SettingsRepository>();
@@ -33,6 +34,7 @@ describe('OwnerController', () => {
eventService, eventService,
settingsRepository, settingsRepository,
authService, authService,
bannerService,
userService, userService,
passwordUtility, passwordUtility,
mock(), mock(),
@@ -100,7 +102,7 @@ describe('OwnerController', () => {
const result = await controller.dismissBanner(mock(), mock(), payload); const result = await controller.dismissBanner(mock(), mock(), payload);
expect(settingsRepository.dismissBanner).not.toHaveBeenCalled(); expect(bannerService.dismissBanner).not.toHaveBeenCalled();
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
@@ -109,7 +111,7 @@ describe('OwnerController', () => {
await controller.dismissBanner(mock(), mock(), payload); await controller.dismissBanner(mock(), mock(), payload);
expect(settingsRepository.dismissBanner).toHaveBeenCalledWith({ bannerName: 'TRIAL' }); expect(bannerService.dismissBanner).toHaveBeenCalledWith('TRIAL');
}); });
}); });
}); });

View File

@@ -1,7 +1,7 @@
import type { PushMessage } from '@n8n/api-types'; import type { PushMessage } from '@n8n/api-types';
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants'; import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA } 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 { Patch, Post, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Request } from 'express'; import { Request } from 'express';
@@ -11,7 +11,6 @@ import { v4 as uuid } from 'uuid';
import { ActiveWorkflowManager } from '@/active-workflow-manager'; import { ActiveWorkflowManager } from '@/active-workflow-manager';
import config from '@/config'; import config from '@/config';
import { inE2ETests } from '@/constants'; import { inE2ETests } from '@/constants';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus';
import type { FeatureReturnType } from '@/license'; import type { FeatureReturnType } from '@/license';

View File

@@ -1,17 +1,18 @@
import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types'; import { DismissBannerRequestDto, OwnerSetupRequestDto } from '@n8n/api-types';
import { SettingsRepository } from '@n8n/db';
import { Body, GlobalScope, Post, RestController } from '@n8n/decorators'; import { Body, GlobalScope, Post, RestController } from '@n8n/decorators';
import { Response } from 'express'; import { Response } from 'express';
import { Logger } from 'n8n-core'; import { Logger } from 'n8n-core';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { validateEntity } from '@/generic-helpers'; import { validateEntity } from '@/generic-helpers';
import { PostHogClient } from '@/posthog'; import { PostHogClient } from '@/posthog';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { BannerService } from '@/services/banner.service';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
import { UserService } from '@/services/user.service'; import { UserService } from '@/services/user.service';
@@ -22,6 +23,7 @@ export class OwnerController {
private readonly eventService: EventService, private readonly eventService: EventService,
private readonly settingsRepository: SettingsRepository, private readonly settingsRepository: SettingsRepository,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly bannerService: BannerService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
private readonly postHog: PostHogClient, private readonly postHog: PostHogClient,
@@ -83,6 +85,6 @@ export class OwnerController {
) { ) {
const bannerName = payload.banner; const bannerName = payload.banner;
if (!bannerName) return; if (!bannerName) return;
return await this.settingsRepository.dismissBanner({ bannerName }); await this.bannerService.dismissBanner(bannerName);
} }
} }

View File

@@ -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<Settings> {
constructor(
dataSource: DataSource,
private readonly errorReporter: ErrorReporter,
) {
super(Settings, dataSource.manager);
}
async getEncryptedSecretsProviderSettings(): Promise<string | null> {
return (await this.findByKey(EXTERNAL_SECRETS_DB_KEY))?.value ?? null;
}
async findByKey(key: string): Promise<Settings | null> {
return await this.findOneBy({ key });
}
async saveEncryptedSecretsProviderSettings(data: string): Promise<void> {
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 };
}
}

View File

@@ -1,3 +1,4 @@
import { SettingsRepository } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import type { ValidationError } from 'class-validator'; import type { ValidationError } from 'class-validator';
import { validate } 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 { writeFile, chmod, readFile } from 'node:fs/promises';
import path from 'path'; import path from 'path';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { import {
SOURCE_CONTROL_SSH_FOLDER, SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_GIT_FOLDER, SOURCE_CONTROL_GIT_FOLDER,

View File

@@ -2,7 +2,7 @@ import type { FrontendSettings, UserUpdateRequestDto } from '@n8n/api-types';
import type { ClientOAuth2Options } from '@n8n/client-oauth2'; import type { ClientOAuth2Options } from '@n8n/client-oauth2';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import type { TagEntity, User, ICredentialsDb, PublicUser } from '@n8n/db'; 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 { Service } from '@n8n/di';
import { ErrorReporter, Logger } from 'n8n-core'; import { ErrorReporter, Logger } from 'n8n-core';
import type { IRun, IWorkflowBase, Workflow, WorkflowExecuteMode } from 'n8n-workflow'; 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 { AbstractServer } from '@/abstract-server';
import type { Config } from '@/config'; import type { Config } from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';

View File

@@ -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 type { License } from '@/license';
import { import {
AnotherDummyProvider, AnotherDummyProvider,
@@ -11,6 +11,7 @@ import {
} from '@test/external-secrets/utils'; } from '@test/external-secrets/utils';
import { mockCipher, mockLogger } from '@test/mocking'; import { mockCipher, mockLogger } from '@test/mocking';
import { EXTERNAL_SECRETS_DB_KEY } from '../constants';
import { ExternalSecretsManager } from '../external-secrets-manager.ee'; import { ExternalSecretsManager } from '../external-secrets-manager.ee';
import type { ExternalSecretsSettings } from '../types'; import type { ExternalSecretsSettings } from '../types';
@@ -44,9 +45,9 @@ describe('External Secrets Manager', () => {
}); });
license.isExternalSecretsEnabled.mockReturnValue(true); license.isExternalSecretsEnabled.mockReturnValue(true);
settingsRepo.getEncryptedSecretsProviderSettings.mockImplementation(async () => settingsRepo.findByKey
JSON.stringify(settings), .calledWith(EXTERNAL_SECRETS_DB_KEY)
); .mockImplementation(async () => mock<Settings>({ value: JSON.stringify(settings) }));
manager = new ExternalSecretsManager( manager = new ExternalSecretsManager(
mockLogger(), mockLogger(),
@@ -251,7 +252,7 @@ describe('External Secrets Manager', () => {
describe('setProviderSettings', () => { describe('setProviderSettings', () => {
test('should save provider settings', async () => { test('should save provider settings', async () => {
const settingsSpy = jest.spyOn(settingsRepo, 'saveEncryptedSecretsProviderSettings'); const settingsSpy = jest.spyOn(settingsRepo, 'upsert');
await manager.init(); await manager.init();
@@ -259,7 +260,9 @@ describe('External Secrets Manager', () => {
test: 'value', test: 'value',
}); });
expect(JSON.parse(settingsSpy.mock.calls[0][0])).toEqual( const settingsCaptor = captor<Settings>();
expect(settingsSpy).toHaveBeenCalledWith(settingsCaptor, ['key']);
expect(JSON.parse(settingsCaptor.value.value)).toEqual(
expect.objectContaining({ expect.objectContaining({
dummy: { dummy: {
connected: true, connected: true,

View File

@@ -1,14 +1,18 @@
import { SettingsRepository } from '@n8n/db';
import { OnShutdown } from '@n8n/decorators'; import { OnShutdown } from '@n8n/decorators';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { Cipher, Logger } from 'n8n-core'; import { Cipher, Logger } from 'n8n-core';
import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow'; import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';
import { License } from '@/license'; import { License } from '@/license';
import { Publisher } from '@/scaling/pubsub/publisher.service'; 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 { ExternalSecretsProviders } from './external-secrets-providers.ee';
import { ExternalSecretsConfig } from './external-secrets.config'; import { ExternalSecretsConfig } from './external-secrets.config';
import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types'; import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types';
@@ -89,8 +93,9 @@ export class ExternalSecretsManager {
void this.publisher.publishCommand({ command: 'reload-external-secrets-providers' }); void this.publisher.publishCommand({ command: 'reload-external-secrets-providers' });
} }
private async getDecryptedSettings(): Promise<ExternalSecretsSettings | null> { async getDecryptedSettings(): Promise<ExternalSecretsSettings | null> {
const encryptedSettings = await this.settingsRepo.getEncryptedSecretsProviderSettings(); const encryptedSettings =
(await this.settingsRepo.findByKey(EXTERNAL_SECRETS_DB_KEY))?.value ?? null;
if (encryptedSettings === null) { if (encryptedSettings === null) {
return null; return null;
} }
@@ -324,7 +329,14 @@ export class ExternalSecretsManager {
async saveAndSetSettings(settings: ExternalSecretsSettings) { async saveAndSetSettings(settings: ExternalSecretsSettings) {
const encryptedSettings = this.encryptSecretsSettings(settings); 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( async testProviderSettings(

View File

@@ -1,5 +1,5 @@
import type { Settings } from '@n8n/db'; import type { Settings } from '@n8n/db';
import { AuthIdentityRepository } from '@n8n/db'; import { AuthIdentityRepository, SettingsRepository } from '@n8n/db';
import { QueryFailedError } from '@n8n/typeorm'; import { QueryFailedError } from '@n8n/typeorm';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { Client } from 'ldapts'; import { Client } from 'ldapts';
@@ -7,7 +7,6 @@ import type { Cipher } from 'n8n-core';
import { randomString } from 'n8n-workflow'; import { randomString } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import type { EventService } from '@/events/event.service'; import type { EventService } from '@/events/event.service';
import { mockInstance, mockLogger } from '@test/mocking'; import { mockInstance, mockLogger } from '@test/mocking';

View File

@@ -1,3 +1,4 @@
import { SettingsRepository } from '@n8n/db';
import type { User, RunningMode, SyncStatus } from '@n8n/db'; import type { User, RunningMode, SyncStatus } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // 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 type { ConnectionOptions } from 'tls';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { EventService } from '@/events/event.service'; import { EventService } from '@/events/event.service';

View File

@@ -7,6 +7,7 @@ import {
type BooleanLicenseFeature, type BooleanLicenseFeature,
type NumericLicenseFeature, type NumericLicenseFeature,
} from '@n8n/constants'; } from '@n8n/constants';
import { SettingsRepository } from '@n8n/db';
import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators'; import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators';
import { Container, Service } from '@n8n/di'; import { Container, Service } from '@n8n/di';
import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk'; 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 { InstanceSettings, Logger } from 'n8n-core';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { LicenseMetricsService } from '@/metrics/license-metrics.service'; import { LicenseMetricsService } from '@/metrics/license-metrics.service';
import { N8N_VERSION, SETTINGS_LICENSE_CERT_KEY, Time } from './constants'; import { N8N_VERSION, SETTINGS_LICENSE_CERT_KEY, Time } from './constants';

View File

@@ -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<SettingsRepository>();
const errorReporter = mock<ErrorReporter>();
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);
});
});
});

View File

@@ -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 { AuthUserRepository } from '@n8n/db';
import type { CredentialsRepository } from '@n8n/db'; import type { CredentialsRepository } from '@n8n/db';
import RudderStack from '@rudderstack/rudder-sdk-node'; import RudderStack from '@rudderstack/rudder-sdk-node';
@@ -6,7 +6,6 @@ import type { Response } from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { AuthService } from '@/auth/auth.service'; 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 { UserRepository } from '@/databases/repositories/user.repository';
import type { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import type { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import type { Invitation } from '@/interfaces'; import type { Invitation } from '@/interfaces';

View File

@@ -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);
}
}
}

View File

@@ -1,5 +1,5 @@
import type { Settings, CredentialsEntity, User, WorkflowEntity, AuthUser } from '@n8n/db'; 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'; import { Service } from '@n8n/di';
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import type { FindManyOptions, FindOneOptions, FindOptionsWhere } from '@n8n/typeorm'; 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 type { NextFunction, Response } from 'express';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import type { Invitation } from '@/interfaces'; import type { Invitation } from '@/interfaces';

View File

@@ -1,8 +1,8 @@
import { SettingsRepository } from '@n8n/db';
import type express from 'express'; import type express from 'express';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import * as samlHelpers from '@/sso.ee/saml/saml-helpers'; import * as samlHelpers from '@/sso.ee/saml/saml-helpers';
import { SamlService } from '@/sso.ee/saml/saml.service.ee'; import { SamlService } from '@/sso.ee/saml/saml.service.ee';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';

View File

@@ -1,4 +1,5 @@
import type { SamlPreferences } from '@n8n/api-types'; import type { SamlPreferences } from '@n8n/api-types';
import { SettingsRepository } from '@n8n/db';
import type { Settings, User } from '@n8n/db'; import type { Settings, User } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import axios from 'axios'; import axios from 'axios';
@@ -9,7 +10,6 @@ import { jsonParse, UnexpectedError } from 'n8n-workflow';
import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify'; import type { IdentityProviderInstance, ServiceProviderInstance } from 'samlify';
import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity'; import type { BindingContext, PostBindingContext } from 'samlify/types/src/entity';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';

View File

@@ -1,8 +1,7 @@
import type { AuthProviderType } from '@n8n/db'; import { SettingsRepository, type AuthProviderType } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import config from '@/config'; import config from '@/config';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
/** /**
* Only one authentication method can be active at a time. This function sets * Only one authentication method can be active at a time. This function sets

View File

@@ -1,11 +1,10 @@
import { CredentialsEntity } from '@n8n/db'; import { CredentialsEntity, SettingsRepository } from '@n8n/db';
import { CredentialsRepository } from '@n8n/db'; import { CredentialsRepository } from '@n8n/db';
import { SharedCredentialsRepository } from '@n8n/db'; import { SharedCredentialsRepository } from '@n8n/db';
import { SharedWorkflowRepository } from '@n8n/db'; import { SharedWorkflowRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { Reset } from '@/commands/user-management/reset'; import { Reset } from '@/commands/user-management/reset';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { NodeTypes } from '@/node-types'; import { NodeTypes } from '@/node-types';

View File

@@ -1,11 +1,11 @@
import { SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { Cipher } from 'n8n-core'; import { Cipher } from 'n8n-core';
import { jsonParse, type IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { CREDENTIAL_BLANKING_VALUE } from '@/constants'; import { CREDENTIAL_BLANKING_VALUE } from '@/constants';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import type { EventService } from '@/events/event.service'; import type { EventService } from '@/events/event.service';
import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee'; import { ExternalSecretsManager } from '@/external-secrets.ee/external-secrets-manager.ee';
import { ExternalSecretsProviders } from '@/external-secrets.ee/external-secrets-providers.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'; const connectedDate = '2023-08-01T12:32:29.000Z';
async function setExternalSecretsSettings(settings: ExternalSecretsSettings) { async function setExternalSecretsSettings(settings: ExternalSecretsSettings) {
return await Container.get(SettingsRepository).saveEncryptedSecretsProviderSettings( await Container.get(ExternalSecretsManager).saveAndSetSettings(settings);
Container.get(Cipher).encrypt(settings),
);
} }
async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> { async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | null> {
const encSettings = await Container.get(SettingsRepository).getEncryptedSecretsProviderSettings(); return await Container.get(ExternalSecretsManager).getDecryptedSettings();
if (encSettings === null) {
return null;
}
return await jsonParse(Container.get(Cipher).decrypt(encSettings));
} }
const eventService = mock<EventService>(); const eventService = mock<EventService>();

View File

@@ -1,7 +1,7 @@
import { SettingsRepository } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants'; import { LDAP_DEFAULT_CONFIGURATION, LDAP_FEATURE_NAME } from '@/ldap.ee/constants';
import type { LdapConfig } from '@/ldap.ee/types'; import type { LdapConfig } from '@/ldap.ee/types';

View File

@@ -1,4 +1,4 @@
import { WorkflowEntity } from '@n8n/db'; import { SettingsRepository, WorkflowEntity } from '@n8n/db';
import { Container } from '@n8n/di'; import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { import {
@@ -21,7 +21,6 @@ import { v4 as uuid } from 'uuid';
import config from '@/config'; import config from '@/config';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
import { ExecutionService } from '@/executions/execution.service'; import { ExecutionService } from '@/executions/execution.service';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { Push } from '@/push'; import { Push } from '@/push';