mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +00:00
refactor(core): Separate license state from license service (#15097)
This commit is contained in:
@@ -21,6 +21,9 @@
|
|||||||
"dist/**/*"
|
"dist/**/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@n8n/constants": "workspace:^",
|
||||||
|
"@n8n/di": "workspace:^",
|
||||||
|
"n8n-workflow": "workspace:^",
|
||||||
"reflect-metadata": "catalog:"
|
"reflect-metadata": "catalog:"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export {};
|
export * from './license-state';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
192
packages/@n8n/backend-common/src/license-state.ts
Normal file
192
packages/@n8n/backend-common/src/license-state.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { UNLIMITED_LICENSE_QUOTA, type BooleanLicenseFeature } from '@n8n/constants';
|
||||||
|
import { Service } from '@n8n/di';
|
||||||
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { FeatureReturnType, LicenseProvider } from './types';
|
||||||
|
|
||||||
|
class ProviderNotSetError extends UnexpectedError {
|
||||||
|
constructor() {
|
||||||
|
super('Cannot query license state because license provider has not been set');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class LicenseState {
|
||||||
|
licenseProvider: LicenseProvider | null = null;
|
||||||
|
|
||||||
|
setLicenseProvider(provider: LicenseProvider) {
|
||||||
|
this.licenseProvider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertProvider(): asserts this is { licenseProvider: LicenseProvider } {
|
||||||
|
if (!this.licenseProvider) throw new ProviderNotSetError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// core queries
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
isLicensed(feature: BooleanLicenseFeature) {
|
||||||
|
this.assertProvider();
|
||||||
|
|
||||||
|
return this.licenseProvider.isLicensed(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
|
||||||
|
this.assertProvider();
|
||||||
|
|
||||||
|
return this.licenseProvider.getValue(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// booleans
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
isSharingLicensed() {
|
||||||
|
return this.isLicensed('feat:sharing');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLogStreamingLicensed() {
|
||||||
|
return this.isLicensed('feat:logStreaming');
|
||||||
|
}
|
||||||
|
|
||||||
|
isLdapLicensed() {
|
||||||
|
return this.isLicensed('feat:ldap');
|
||||||
|
}
|
||||||
|
|
||||||
|
isSamlLicensed() {
|
||||||
|
return this.isLicensed('feat:saml');
|
||||||
|
}
|
||||||
|
|
||||||
|
isApiKeyScopesLicensed() {
|
||||||
|
return this.isLicensed('feat:apiKeyScopes');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAiAssistantLicensed() {
|
||||||
|
return this.isLicensed('feat:aiAssistant');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAskAiLicensed() {
|
||||||
|
return this.isLicensed('feat:askAi');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAiCreditsLicensed() {
|
||||||
|
return this.isLicensed('feat:aiCredits');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdvancedExecutionFiltersLicensed() {
|
||||||
|
return this.isLicensed('feat:advancedExecutionFilters');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdvancedPermissionsLicensed() {
|
||||||
|
return this.isLicensed('feat:advancedPermissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
isDebugInEditorLicensed() {
|
||||||
|
return this.isLicensed('feat:debugInEditor');
|
||||||
|
}
|
||||||
|
|
||||||
|
isBinaryDataS3Licensed() {
|
||||||
|
return this.isLicensed('feat:binaryDataS3');
|
||||||
|
}
|
||||||
|
|
||||||
|
isMultiMainLicensed() {
|
||||||
|
return this.isLicensed('feat:multipleMainInstances');
|
||||||
|
}
|
||||||
|
|
||||||
|
isVariablesLicensed() {
|
||||||
|
return this.isLicensed('feat:variables');
|
||||||
|
}
|
||||||
|
|
||||||
|
isSourceControlLicensed() {
|
||||||
|
return this.isLicensed('feat:sourceControl');
|
||||||
|
}
|
||||||
|
|
||||||
|
isExternalSecretsLicensed() {
|
||||||
|
return this.isLicensed('feat:externalSecrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
isWorkflowHistoryLicensed() {
|
||||||
|
return this.isLicensed('feat:workflowHistory');
|
||||||
|
}
|
||||||
|
|
||||||
|
isAPIDisabled() {
|
||||||
|
return this.isLicensed('feat:apiDisabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
isWorkerViewLicensed() {
|
||||||
|
return this.isLicensed('feat:workerView');
|
||||||
|
}
|
||||||
|
|
||||||
|
isProjectRoleAdminLicensed() {
|
||||||
|
return this.isLicensed('feat:projectRole:admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
isProjectRoleEditorLicensed() {
|
||||||
|
return this.isLicensed('feat:projectRole:editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
isProjectRoleViewerLicensed() {
|
||||||
|
return this.isLicensed('feat:projectRole:viewer');
|
||||||
|
}
|
||||||
|
|
||||||
|
isCustomNpmRegistryLicensed() {
|
||||||
|
return this.isLicensed('feat:communityNodes:customRegistry');
|
||||||
|
}
|
||||||
|
|
||||||
|
isFoldersLicensed() {
|
||||||
|
return this.isLicensed('feat:folders');
|
||||||
|
}
|
||||||
|
|
||||||
|
isInsightsSummaryLicensed() {
|
||||||
|
return this.isLicensed('feat:insights:viewSummary');
|
||||||
|
}
|
||||||
|
|
||||||
|
isInsightsDashboardLicensed() {
|
||||||
|
return this.isLicensed('feat:insights:viewDashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
isInsightsHourlyDataLicensed() {
|
||||||
|
return this.isLicensed('feat:insights:viewHourlyData');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------
|
||||||
|
// integers
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
getMaxUsers() {
|
||||||
|
return this.getValue('quota:users') ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxActiveWorkflows() {
|
||||||
|
return this.getValue('quota:activeWorkflows') ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxVariables() {
|
||||||
|
return this.getValue('quota:maxVariables') ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxAiCredits() {
|
||||||
|
return this.getValue('quota:aiCredits') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkflowHistoryPruneQuota() {
|
||||||
|
return this.getValue('quota:workflowHistoryPrune') ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInsightsMaxHistory() {
|
||||||
|
return this.getValue('quota:insights:maxHistoryDays') ?? 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInsightsRetentionMaxAge() {
|
||||||
|
return this.getValue('quota:insights:retention:maxAgeDays') ?? 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInsightsRetentionPruneInterval() {
|
||||||
|
return this.getValue('quota:insights:retention:pruneIntervalDays') ?? 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxTeamProjects() {
|
||||||
|
return this.getValue('quota:maxTeamProjects') ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
packages/@n8n/backend-common/src/types.ts
Normal file
15
packages/@n8n/backend-common/src/types.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
|
||||||
|
|
||||||
|
export type FeatureReturnType = Partial<
|
||||||
|
{
|
||||||
|
planName: string;
|
||||||
|
} & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface LicenseProvider {
|
||||||
|
/** Returns whether a feature is included in the user's license plan. */
|
||||||
|
isLicensed(feature: BooleanLicenseFeature): boolean;
|
||||||
|
|
||||||
|
/** Returns the value of a feature in the user's license plan, typically a boolean or integer. */
|
||||||
|
getValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T];
|
||||||
|
}
|
||||||
@@ -108,15 +108,15 @@ describe('ControllerRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should disallow when feature is missing', async () => {
|
it('should disallow when feature is missing', async () => {
|
||||||
license.isFeatureEnabled.calledWith('feat:sharing').mockReturnValue(false);
|
license.isLicensed.calledWith('feat:sharing').mockReturnValue(false);
|
||||||
await agent.get('/rest/test/with-sharing').expect(403);
|
await agent.get('/rest/test/with-sharing').expect(403);
|
||||||
expect(license.isFeatureEnabled).toHaveBeenCalled();
|
expect(license.isLicensed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow when feature is available', async () => {
|
it('should allow when feature is available', async () => {
|
||||||
license.isFeatureEnabled.calledWith('feat:sharing').mockReturnValue(true);
|
license.isLicensed.calledWith('feat:sharing').mockReturnValue(true);
|
||||||
await agent.get('/rest/test/with-sharing').expect(200);
|
await agent.get('/rest/test/with-sharing').expect(200);
|
||||||
expect(license.isFeatureEnabled).toHaveBeenCalled();
|
expect(license.isLicensed).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -111,13 +111,13 @@ describe('License', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('check if feature is enabled', () => {
|
test('check if feature is enabled', () => {
|
||||||
license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
license.isLicensed(MOCK_FEATURE_FLAG);
|
||||||
|
|
||||||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('check if sharing feature is enabled', () => {
|
test('check if sharing feature is enabled', () => {
|
||||||
license.isFeatureEnabled(MOCK_FEATURE_FLAG);
|
license.isLicensed(MOCK_FEATURE_FLAG);
|
||||||
|
|
||||||
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
expect(LicenseManager.prototype.hasFeatureEnabled).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||||
});
|
});
|
||||||
@@ -129,7 +129,7 @@ describe('License', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('check fetching feature values', async () => {
|
test('check fetching feature values', async () => {
|
||||||
license.getFeatureValue(MOCK_FEATURE_FLAG);
|
license.getValue(MOCK_FEATURE_FLAG);
|
||||||
|
|
||||||
expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
expect(LicenseManager.prototype.getFeatureValue).toHaveBeenCalledWith(MOCK_FEATURE_FLAG);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
@@ -197,7 +198,7 @@ export abstract class BaseCommand extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = Container.get(License).isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
const isLicensed = Container.get(License).isLicensed(LICENSE_FEATURES.BINARY_DATA_S3);
|
||||||
if (!isLicensed) {
|
if (!isLicensed) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'No license found for S3 storage. \n Either set `N8N_DEFAULT_BINARY_DATA_MODE` to something else, or upgrade to a license that supports this feature.',
|
'No license found for S3 storage. \n Either set `N8N_DEFAULT_BINARY_DATA_MODE` to something else, or upgrade to a license that supports this feature.',
|
||||||
@@ -241,6 +242,8 @@ export abstract class BaseCommand extends Command {
|
|||||||
this.license = Container.get(License);
|
this.license = Container.get(License);
|
||||||
await this.license.init();
|
await this.license.init();
|
||||||
|
|
||||||
|
Container.get(LicenseState).setLicenseProvider(this.license);
|
||||||
|
|
||||||
const { activationKey } = this.globalConfig.license;
|
const { activationKey } = this.globalConfig.license;
|
||||||
|
|
||||||
if (activationKey) {
|
if (activationKey) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class ControllerRegistry {
|
|||||||
|
|
||||||
private createLicenseMiddleware(feature: BooleanLicenseFeature): RequestHandler {
|
private createLicenseMiddleware(feature: BooleanLicenseFeature): RequestHandler {
|
||||||
return (_req, res, next) => {
|
return (_req, res, next) => {
|
||||||
if (!this.license.isFeatureEnabled(feature)) {
|
if (!this.license.isLicensed(feature)) {
|
||||||
res.status(403).json({ status: 'error', message: 'Plan lacks license for this feature' });
|
res.status(403).json({ status: 'error', message: 'Plan lacks license for this feature' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,8 +153,7 @@ export class E2EController {
|
|||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly authUserRepository: AuthUserRepository,
|
private readonly authUserRepository: AuthUserRepository,
|
||||||
) {
|
) {
|
||||||
license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
|
license.isLicensed = (feature: BooleanLicenseFeature) => this.enabledFeatures[feature] ?? false;
|
||||||
this.enabledFeatures[feature] ?? false;
|
|
||||||
|
|
||||||
// Ugly hack to satisfy biome parser
|
// Ugly hack to satisfy biome parser
|
||||||
const getFeatureValue = <T extends keyof FeatureReturnType>(
|
const getFeatureValue = <T extends keyof FeatureReturnType>(
|
||||||
@@ -166,7 +165,7 @@ export class E2EController {
|
|||||||
return UNLIMITED_LICENSE_QUOTA as FeatureReturnType[T];
|
return UNLIMITED_LICENSE_QUOTA as FeatureReturnType[T];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
license.getFeatureValue = getFeatureValue;
|
license.getValue = getFeatureValue;
|
||||||
|
|
||||||
license.getPlanName = () => 'Enterprise';
|
license.getPlanName = () => 'Enterprise';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { LicenseProvider } from '@n8n/backend-common';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import {
|
import {
|
||||||
LICENSE_FEATURES,
|
LICENSE_FEATURES,
|
||||||
@@ -28,7 +29,7 @@ export type FeatureReturnType = Partial<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class License {
|
export class License implements LicenseProvider {
|
||||||
private manager: LicenseManager | undefined;
|
private manager: LicenseManager | undefined;
|
||||||
|
|
||||||
private isShuttingDown = false;
|
private isShuttingDown = false;
|
||||||
@@ -218,123 +219,135 @@ export class License {
|
|||||||
this.logger.debug('License shut down');
|
this.logger.debug('License shut down');
|
||||||
}
|
}
|
||||||
|
|
||||||
isFeatureEnabled(feature: BooleanLicenseFeature) {
|
isLicensed(feature: BooleanLicenseFeature) {
|
||||||
return this.manager?.hasFeatureEnabled(feature) ?? false;
|
return this.manager?.hasFeatureEnabled(feature) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isSharingLicensed` instead. */
|
||||||
isSharingEnabled() {
|
isSharingEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.SHARING);
|
return this.isLicensed(LICENSE_FEATURES.SHARING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isLogStreamingLicensed` instead. */
|
||||||
isLogStreamingEnabled() {
|
isLogStreamingEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.LOG_STREAMING);
|
return this.isLicensed(LICENSE_FEATURES.LOG_STREAMING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isLdapLicensed` instead. */
|
||||||
isLdapEnabled() {
|
isLdapEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.LDAP);
|
return this.isLicensed(LICENSE_FEATURES.LDAP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isSamlLicensed` instead. */
|
||||||
isSamlEnabled() {
|
isSamlEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.SAML);
|
return this.isLicensed(LICENSE_FEATURES.SAML);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isApiKeyScopesLicensed` instead. */
|
||||||
isApiKeyScopesEnabled() {
|
isApiKeyScopesEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.API_KEY_SCOPES);
|
return this.isLicensed(LICENSE_FEATURES.API_KEY_SCOPES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAiAssistantLicensed` instead. */
|
||||||
isAiAssistantEnabled() {
|
isAiAssistantEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.AI_ASSISTANT);
|
return this.isLicensed(LICENSE_FEATURES.AI_ASSISTANT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAskAiLicensed` instead. */
|
||||||
isAskAiEnabled() {
|
isAskAiEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.ASK_AI);
|
return this.isLicensed(LICENSE_FEATURES.ASK_AI);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAiCreditsLicensed` instead. */
|
||||||
isAiCreditsEnabled() {
|
isAiCreditsEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.AI_CREDITS);
|
return this.isLicensed(LICENSE_FEATURES.AI_CREDITS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAdvancedExecutionFiltersLicensed` instead. */
|
||||||
isAdvancedExecutionFiltersEnabled() {
|
isAdvancedExecutionFiltersEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
|
return this.isLicensed(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAdvancedPermissionsLicensed` instead. */
|
||||||
isAdvancedPermissionsLicensed() {
|
isAdvancedPermissionsLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_PERMISSIONS);
|
return this.isLicensed(LICENSE_FEATURES.ADVANCED_PERMISSIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isDebugInEditorLicensed` instead. */
|
||||||
isDebugInEditorLicensed() {
|
isDebugInEditorLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.DEBUG_IN_EDITOR);
|
return this.isLicensed(LICENSE_FEATURES.DEBUG_IN_EDITOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isBinaryDataS3Licensed` instead. */
|
||||||
isBinaryDataS3Licensed() {
|
isBinaryDataS3Licensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
return this.isLicensed(LICENSE_FEATURES.BINARY_DATA_S3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isMultiMainLicensed` instead. */
|
||||||
isMultiMainLicensed() {
|
isMultiMainLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
return this.isLicensed(LICENSE_FEATURES.MULTIPLE_MAIN_INSTANCES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isVariablesLicensed` instead. */
|
||||||
isVariablesEnabled() {
|
isVariablesEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.VARIABLES);
|
return this.isLicensed(LICENSE_FEATURES.VARIABLES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isSourceControlLicensed` instead. */
|
||||||
isSourceControlLicensed() {
|
isSourceControlLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.SOURCE_CONTROL);
|
return this.isLicensed(LICENSE_FEATURES.SOURCE_CONTROL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isExternalSecretsLicensed` instead. */
|
||||||
isExternalSecretsEnabled() {
|
isExternalSecretsEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.EXTERNAL_SECRETS);
|
return this.isLicensed(LICENSE_FEATURES.EXTERNAL_SECRETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isWorkflowHistoryLicensed` instead. */
|
||||||
isWorkflowHistoryLicensed() {
|
isWorkflowHistoryLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.WORKFLOW_HISTORY);
|
return this.isLicensed(LICENSE_FEATURES.WORKFLOW_HISTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isAPIDisabled` instead. */
|
||||||
isAPIDisabled() {
|
isAPIDisabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.API_DISABLED);
|
return this.isLicensed(LICENSE_FEATURES.API_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isWorkerViewLicensed` instead. */
|
||||||
isWorkerViewLicensed() {
|
isWorkerViewLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.WORKER_VIEW);
|
return this.isLicensed(LICENSE_FEATURES.WORKER_VIEW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isProjectRoleAdminLicensed` instead. */
|
||||||
isProjectRoleAdminLicensed() {
|
isProjectRoleAdminLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.PROJECT_ROLE_ADMIN);
|
return this.isLicensed(LICENSE_FEATURES.PROJECT_ROLE_ADMIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isProjectRoleEditorLicensed` instead. */
|
||||||
isProjectRoleEditorLicensed() {
|
isProjectRoleEditorLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.PROJECT_ROLE_EDITOR);
|
return this.isLicensed(LICENSE_FEATURES.PROJECT_ROLE_EDITOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isProjectRoleViewerLicensed` instead. */
|
||||||
isProjectRoleViewerLicensed() {
|
isProjectRoleViewerLicensed() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.PROJECT_ROLE_VIEWER);
|
return this.isLicensed(LICENSE_FEATURES.PROJECT_ROLE_VIEWER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isCustomNpmRegistryLicensed` instead. */
|
||||||
isCustomNpmRegistryEnabled() {
|
isCustomNpmRegistryEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
|
return this.isLicensed(LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState.isFoldersLicensed` instead. */
|
||||||
isFoldersEnabled() {
|
isFoldersEnabled() {
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.FOLDERS);
|
return this.isLicensed(LICENSE_FEATURES.FOLDERS);
|
||||||
}
|
|
||||||
|
|
||||||
isInsightsSummaryEnabled() {
|
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.INSIGHTS_VIEW_SUMMARY);
|
|
||||||
}
|
|
||||||
|
|
||||||
isInsightsDashboardEnabled() {
|
|
||||||
return this.isFeatureEnabled(LICENSE_FEATURES.INSIGHTS_VIEW_DASHBOARD);
|
|
||||||
}
|
|
||||||
|
|
||||||
isInsightsHourlyDataEnabled() {
|
|
||||||
return this.getFeatureValue(LICENSE_FEATURES.INSIGHTS_VIEW_HOURLY_DATA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentEntitlements() {
|
getCurrentEntitlements() {
|
||||||
return this.manager?.getCurrentEntitlements() ?? [];
|
return this.manager?.getCurrentEntitlements() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatureValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
|
getValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
|
||||||
return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
|
return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,46 +383,54 @@ export class License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for computed data
|
// Helper functions for computed data
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getUsersLimit() {
|
getUsersLimit() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
return this.getValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getTriggerLimit() {
|
getTriggerLimit() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
return this.getValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getVariablesLimit() {
|
getVariablesLimit() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
return this.getValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getAiCredits() {
|
getAiCredits() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.AI_CREDITS) ?? 0;
|
return this.getValue(LICENSE_QUOTAS.AI_CREDITS) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getWorkflowHistoryPruneLimit() {
|
getWorkflowHistoryPruneLimit() {
|
||||||
return (
|
return this.getValue(LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
|
||||||
this.getFeatureValue(LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT) ?? UNLIMITED_LICENSE_QUOTA
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getInsightsMaxHistory() {
|
getInsightsMaxHistory() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.INSIGHTS_MAX_HISTORY_DAYS) ?? 7;
|
return this.getValue(LICENSE_QUOTAS.INSIGHTS_MAX_HISTORY_DAYS) ?? 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getInsightsRetentionMaxAge() {
|
getInsightsRetentionMaxAge() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.INSIGHTS_RETENTION_MAX_AGE_DAYS) ?? 180;
|
return this.getValue(LICENSE_QUOTAS.INSIGHTS_RETENTION_MAX_AGE_DAYS) ?? 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getInsightsRetentionPruneInterval() {
|
getInsightsRetentionPruneInterval() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.INSIGHTS_RETENTION_PRUNE_INTERVAL_DAYS) ?? 24;
|
return this.getValue(LICENSE_QUOTAS.INSIGHTS_RETENTION_PRUNE_INTERVAL_DAYS) ?? 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
getTeamProjectLimit() {
|
getTeamProjectLimit() {
|
||||||
return this.getFeatureValue(LICENSE_QUOTAS.TEAM_PROJECT_LIMIT) ?? 0;
|
return this.getValue(LICENSE_QUOTAS.TEAM_PROJECT_LIMIT) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlanName(): string {
|
getPlanName(): string {
|
||||||
return this.getFeatureValue('planName') ?? 'Community';
|
return this.getValue('planName') ?? 'Community';
|
||||||
}
|
}
|
||||||
|
|
||||||
getInfo(): string {
|
getInfo(): string {
|
||||||
@@ -420,6 +441,7 @@ export class License {
|
|||||||
return this.manager.toString();
|
return this.manager.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use `LicenseState` instead. */
|
||||||
isWithinUsersLimit() {
|
isWithinUsersLimit() {
|
||||||
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
|
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
import type { AuthenticatedRequest } from '@/requests';
|
import type { AuthenticatedRequest } from '@/requests';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
import { LicenseMocker } from '@test-integration/license';
|
||||||
import * as testDb from '@test-integration/test-db';
|
import * as testDb from '@test-integration/test-db';
|
||||||
|
|
||||||
import { TypeToNumber } from '../database/entities/insights-shared';
|
import { TypeToNumber } from '../database/entities/insights-shared';
|
||||||
@@ -12,6 +14,7 @@ import { InsightsController } from '../insights.controller';
|
|||||||
// Initialize DB once for all tests
|
// Initialize DB once for all tests
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await testDb.init();
|
await testDb.init();
|
||||||
|
new LicenseMocker().mockLicenseState(Container.get(LicenseState));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate DB once after all tests complete
|
// Terminate DB once after all tests complete
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { InsightsDateRange } from '@n8n/api-types';
|
import type { InsightsDateRange } from '@n8n/api-types';
|
||||||
|
import type { LicenseState } from '@n8n/backend-common';
|
||||||
import type { Project } from '@n8n/db';
|
import type { Project } from '@n8n/db';
|
||||||
import type { WorkflowEntity } from '@n8n/db';
|
import type { WorkflowEntity } from '@n8n/db';
|
||||||
import type { IWorkflowDb } from '@n8n/db';
|
import type { IWorkflowDb } from '@n8n/db';
|
||||||
@@ -6,7 +7,6 @@ import { Container } from '@n8n/di';
|
|||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import type { License } from '@/license';
|
|
||||||
import { createTeamProject } from '@test-integration/db/projects';
|
import { createTeamProject } from '@test-integration/db/projects';
|
||||||
import { createWorkflow } from '@test-integration/db/workflows';
|
import { createWorkflow } from '@test-integration/db/workflows';
|
||||||
import * as testDb from '@test-integration/test-db';
|
import * as testDb from '@test-integration/test-db';
|
||||||
@@ -492,10 +492,10 @@ describe('getInsightsByTime', () => {
|
|||||||
|
|
||||||
describe('getAvailableDateRanges', () => {
|
describe('getAvailableDateRanges', () => {
|
||||||
let insightsService: InsightsService;
|
let insightsService: InsightsService;
|
||||||
let licenseMock: jest.Mocked<License>;
|
let licenseMock: jest.Mocked<LicenseState>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
licenseMock = mock<License>();
|
licenseMock = mock<LicenseState>();
|
||||||
insightsService = new InsightsService(
|
insightsService = new InsightsService(
|
||||||
mock<InsightsByPeriodRepository>(),
|
mock<InsightsByPeriodRepository>(),
|
||||||
mock<InsightsCompactionService>(),
|
mock<InsightsCompactionService>(),
|
||||||
@@ -506,7 +506,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
test('returns correct ranges when hourly data is enabled and max history is unlimited', () => {
|
test('returns correct ranges when hourly data is enabled and max history is unlimited', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
const result = insightsService.getAvailableDateRanges();
|
const result = insightsService.getAvailableDateRanges();
|
||||||
|
|
||||||
@@ -523,7 +523,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
test('returns correct ranges when hourly data is enabled and max history is 365 days', () => {
|
test('returns correct ranges when hourly data is enabled and max history is 365 days', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
const result = insightsService.getAvailableDateRanges();
|
const result = insightsService.getAvailableDateRanges();
|
||||||
|
|
||||||
@@ -540,7 +540,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
test('returns correct ranges when hourly data is disabled and max history is 30 days', () => {
|
test('returns correct ranges when hourly data is disabled and max history is 30 days', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||||
|
|
||||||
const result = insightsService.getAvailableDateRanges();
|
const result = insightsService.getAvailableDateRanges();
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
test('returns correct ranges when max history is less than 7 days', () => {
|
test('returns correct ranges when max history is less than 7 days', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(5);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(5);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||||
|
|
||||||
const result = insightsService.getAvailableDateRanges();
|
const result = insightsService.getAvailableDateRanges();
|
||||||
|
|
||||||
@@ -574,7 +574,7 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
test('returns correct ranges when max history is 90 days and hourly data is enabled', () => {
|
test('returns correct ranges when max history is 90 days and hourly data is enabled', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
const result = insightsService.getAvailableDateRanges();
|
const result = insightsService.getAvailableDateRanges();
|
||||||
|
|
||||||
@@ -592,10 +592,10 @@ describe('getAvailableDateRanges', () => {
|
|||||||
|
|
||||||
describe('getMaxAgeInDaysAndGranularity', () => {
|
describe('getMaxAgeInDaysAndGranularity', () => {
|
||||||
let insightsService: InsightsService;
|
let insightsService: InsightsService;
|
||||||
let licenseMock: jest.Mocked<License>;
|
let licenseMock: jest.Mocked<LicenseState>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
licenseMock = mock<License>();
|
licenseMock = mock<LicenseState>();
|
||||||
insightsService = new InsightsService(
|
insightsService = new InsightsService(
|
||||||
mock<InsightsByPeriodRepository>(),
|
mock<InsightsByPeriodRepository>(),
|
||||||
mock<InsightsCompactionService>(),
|
mock<InsightsCompactionService>(),
|
||||||
@@ -606,7 +606,7 @@ describe('getMaxAgeInDaysAndGranularity', () => {
|
|||||||
|
|
||||||
test('returns correct maxAgeInDays and granularity for a valid licensed date range', () => {
|
test('returns correct maxAgeInDays and granularity for a valid licensed date range', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
const result = insightsService.getMaxAgeInDaysAndGranularity('month');
|
const result = insightsService.getMaxAgeInDaysAndGranularity('month');
|
||||||
|
|
||||||
@@ -620,7 +620,7 @@ describe('getMaxAgeInDaysAndGranularity', () => {
|
|||||||
|
|
||||||
test('throws an error if the date range is not available', () => {
|
test('throws an error if the date range is not available', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(365);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
insightsService.getMaxAgeInDaysAndGranularity('invalidKey' as InsightsDateRange['key']);
|
insightsService.getMaxAgeInDaysAndGranularity('invalidKey' as InsightsDateRange['key']);
|
||||||
@@ -629,7 +629,7 @@ describe('getMaxAgeInDaysAndGranularity', () => {
|
|||||||
|
|
||||||
test('throws an error if the date range is not licensed', () => {
|
test('throws an error if the date range is not licensed', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(30);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||||
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
insightsService.getMaxAgeInDaysAndGranularity('year');
|
insightsService.getMaxAgeInDaysAndGranularity('year');
|
||||||
@@ -638,7 +638,7 @@ describe('getMaxAgeInDaysAndGranularity', () => {
|
|||||||
|
|
||||||
test('returns correct maxAgeInDays and granularity for a valid date range with hourly data disabled', () => {
|
test('returns correct maxAgeInDays and granularity for a valid date range with hourly data disabled', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(90);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(false);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(false);
|
||||||
|
|
||||||
const result = insightsService.getMaxAgeInDaysAndGranularity('quarter');
|
const result = insightsService.getMaxAgeInDaysAndGranularity('quarter');
|
||||||
|
|
||||||
@@ -652,7 +652,7 @@ describe('getMaxAgeInDaysAndGranularity', () => {
|
|||||||
|
|
||||||
test('returns correct maxAgeInDays and granularity for a valid date range with unlimited history', () => {
|
test('returns correct maxAgeInDays and granularity for a valid date range with unlimited history', () => {
|
||||||
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
licenseMock.getInsightsMaxHistory.mockReturnValue(-1);
|
||||||
licenseMock.isInsightsHourlyDataEnabled.mockReturnValue(true);
|
licenseMock.isInsightsHourlyDataLicensed.mockReturnValue(true);
|
||||||
|
|
||||||
const result = insightsService.getMaxAgeInDaysAndGranularity('day');
|
const result = insightsService.getMaxAgeInDaysAndGranularity('day');
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import {
|
|||||||
type InsightsDateRange,
|
type InsightsDateRange,
|
||||||
INSIGHTS_DATE_RANGE_KEYS,
|
INSIGHTS_DATE_RANGE_KEYS,
|
||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { OnShutdown } from '@n8n/decorators';
|
import { OnShutdown } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { License } from '@/license';
|
|
||||||
|
|
||||||
import type { PeriodUnit, TypeUnit } from './database/entities/insights-shared';
|
import type { PeriodUnit, TypeUnit } from './database/entities/insights-shared';
|
||||||
import { NumberToType } from './database/entities/insights-shared';
|
import { NumberToType } from './database/entities/insights-shared';
|
||||||
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
|
import { InsightsByPeriodRepository } from './database/repositories/insights-by-period.repository';
|
||||||
@@ -31,7 +30,7 @@ export class InsightsService {
|
|||||||
private readonly insightsByPeriodRepository: InsightsByPeriodRepository,
|
private readonly insightsByPeriodRepository: InsightsByPeriodRepository,
|
||||||
private readonly compactionService: InsightsCompactionService,
|
private readonly compactionService: InsightsCompactionService,
|
||||||
private readonly collectionService: InsightsCollectionService,
|
private readonly collectionService: InsightsCollectionService,
|
||||||
private readonly license: License,
|
private readonly licenseState: LicenseState,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
startBackgroundProcess() {
|
startBackgroundProcess() {
|
||||||
@@ -191,15 +190,15 @@ export class InsightsService {
|
|||||||
*/
|
*/
|
||||||
getAvailableDateRanges(): InsightsDateRange[] {
|
getAvailableDateRanges(): InsightsDateRange[] {
|
||||||
const maxHistoryInDays =
|
const maxHistoryInDays =
|
||||||
this.license.getInsightsMaxHistory() === -1
|
this.licenseState.getInsightsMaxHistory() === -1
|
||||||
? Number.MAX_SAFE_INTEGER
|
? Number.MAX_SAFE_INTEGER
|
||||||
: this.license.getInsightsMaxHistory();
|
: this.licenseState.getInsightsMaxHistory();
|
||||||
const isHourlyDateEnabled = this.license.isInsightsHourlyDataEnabled();
|
const isHourlyDateLicensed = this.licenseState.isInsightsHourlyDataLicensed();
|
||||||
|
|
||||||
return INSIGHTS_DATE_RANGE_KEYS.map((key) => ({
|
return INSIGHTS_DATE_RANGE_KEYS.map((key) => ({
|
||||||
key,
|
key,
|
||||||
licensed:
|
licensed:
|
||||||
key === 'day' ? (isHourlyDateEnabled ?? false) : maxHistoryInDays >= keyRangeToDays[key],
|
key === 'day' ? (isHourlyDateLicensed ?? false) : maxHistoryInDays >= keyRangeToDays[key],
|
||||||
granularity: key === 'day' ? 'hour' : keyRangeToDays[key] <= 30 ? 'day' : 'week',
|
granularity: key === 'day' ? 'hour' : keyRangeToDays[key] <= 30 ? 'day' : 'week',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const validLicenseWithUserQuota = (
|
|||||||
|
|
||||||
export const isLicensed = (feature: BooleanLicenseFeature) => {
|
export const isLicensed = (feature: BooleanLicenseFeature) => {
|
||||||
return async (_: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
|
return async (_: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => {
|
||||||
if (Container.get(License).isFeatureEnabled(feature)) return next();
|
if (Container.get(License).isLicensed(feature)) return next();
|
||||||
|
|
||||||
return res.status(403).json({ message: new FeatureNotLicensedError(feature).message });
|
return res.status(403).json({ message: new FeatureNotLicensedError(feature).message });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { FrontendSettings, ITelemetrySettings } from '@n8n/api-types';
|
import type { FrontendSettings, ITelemetrySettings } from '@n8n/api-types';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { GlobalConfig, SecurityConfig } from '@n8n/config';
|
import { GlobalConfig, SecurityConfig } from '@n8n/config';
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
@@ -52,6 +53,7 @@ export class FrontendService {
|
|||||||
private readonly pushConfig: PushConfig,
|
private readonly pushConfig: PushConfig,
|
||||||
private readonly binaryDataConfig: BinaryDataConfig,
|
private readonly binaryDataConfig: BinaryDataConfig,
|
||||||
private readonly insightsService: InsightsService,
|
private readonly insightsService: InsightsService,
|
||||||
|
private readonly licenseState: LicenseState,
|
||||||
) {
|
) {
|
||||||
loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes());
|
loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes());
|
||||||
void this.generateTypes();
|
void this.generateTypes();
|
||||||
@@ -324,7 +326,7 @@ export class FrontendService {
|
|||||||
variables: this.license.isVariablesEnabled(),
|
variables: this.license.isVariablesEnabled(),
|
||||||
sourceControl: this.license.isSourceControlLicensed(),
|
sourceControl: this.license.isSourceControlLicensed(),
|
||||||
externalSecrets: this.license.isExternalSecretsEnabled(),
|
externalSecrets: this.license.isExternalSecretsEnabled(),
|
||||||
showNonProdBanner: this.license.isFeatureEnabled(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
|
showNonProdBanner: this.license.isLicensed(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
|
||||||
debugInEditor: this.license.isDebugInEditorLicensed(),
|
debugInEditor: this.license.isDebugInEditorLicensed(),
|
||||||
binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
|
binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
|
||||||
workflowHistory:
|
workflowHistory:
|
||||||
@@ -378,8 +380,8 @@ export class FrontendService {
|
|||||||
|
|
||||||
Object.assign(this.settings.insights, {
|
Object.assign(this.settings.insights, {
|
||||||
enabled: this.modulesConfig.loadedModules.has('insights'),
|
enabled: this.modulesConfig.loadedModules.has('insights'),
|
||||||
summary: this.license.isInsightsSummaryEnabled(),
|
summary: this.licenseState.isInsightsSummaryLicensed(),
|
||||||
dashboard: this.license.isInsightsDashboardEnabled(),
|
dashboard: this.licenseState.isInsightsDashboardLicensed(),
|
||||||
dateRanges: this.insightsService.getAvailableDateRanges(),
|
dateRanges: this.insightsService.getAvailableDateRanges(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { LicenseProvider, LicenseState } from '@n8n/backend-common';
|
||||||
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
|
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
|
||||||
|
|
||||||
import type { License } from '@/license';
|
import type { License } from '@/license';
|
||||||
@@ -17,8 +18,17 @@ export class LicenseMocker {
|
|||||||
private _defaultQuotas: Map<NumericLicenseFeature, number> = new Map();
|
private _defaultQuotas: Map<NumericLicenseFeature, number> = new Map();
|
||||||
|
|
||||||
mock(license: License) {
|
mock(license: License) {
|
||||||
license.isFeatureEnabled = this.isFeatureEnabled.bind(this);
|
license.isLicensed = this.isFeatureEnabled.bind(this);
|
||||||
license.getFeatureValue = this.getFeatureValue.bind(this);
|
license.getValue = this.getFeatureValue.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLicenseState(licenseState: LicenseState) {
|
||||||
|
const licenseProvider: LicenseProvider = {
|
||||||
|
isLicensed: this.isFeatureEnabled.bind(this),
|
||||||
|
getValue: this.getFeatureValue.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
|
licenseState.setLicenseProvider(licenseProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import type { User } from '@n8n/db';
|
import type { User } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
@@ -125,6 +126,8 @@ export const setupTestServer = ({
|
|||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
|
|
||||||
testServer.license.mock(Container.get(License));
|
testServer.license.mock(Container.get(License));
|
||||||
|
testServer.license.mockLicenseState(Container.get(LicenseState));
|
||||||
|
|
||||||
if (enabledFeatures) {
|
if (enabledFeatures) {
|
||||||
testServer.license.setDefaults({
|
testServer.license.setDefaults({
|
||||||
features: enabledFeatures,
|
features: enabledFeatures,
|
||||||
|
|||||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -387,6 +387,15 @@ importers:
|
|||||||
|
|
||||||
packages/@n8n/backend-common:
|
packages/@n8n/backend-common:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@n8n/constants':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../constants
|
||||||
|
'@n8n/di':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../di
|
||||||
|
n8n-workflow:
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../workflow
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -690,7 +699,7 @@ importers:
|
|||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
'@getzep/zep-cloud':
|
'@getzep/zep-cloud':
|
||||||
specifier: 1.0.12
|
specifier: 1.0.12
|
||||||
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6))
|
version: 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(e320b1d8e94e7308fefdef3743329630))
|
||||||
'@getzep/zep-js':
|
'@getzep/zep-js':
|
||||||
specifier: 0.9.0
|
specifier: 0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
@@ -717,7 +726,7 @@ importers:
|
|||||||
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
version: 0.3.2(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
'@langchain/community':
|
'@langchain/community':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.24(d72b3dbd91eb98a3175f929d13e7c0a7)
|
version: 0.3.24(a23560be5fb93c23c5c4ed2a6b67082b)
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
version: 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
@@ -819,7 +828,7 @@ importers:
|
|||||||
version: 23.0.1
|
version: 23.0.1
|
||||||
langchain:
|
langchain:
|
||||||
specifier: 0.3.11
|
specifier: 0.3.11
|
||||||
version: 0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6)
|
version: 0.3.11(e320b1d8e94e7308fefdef3743329630)
|
||||||
lodash:
|
lodash:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
@@ -16363,7 +16372,7 @@ snapshots:
|
|||||||
'@gar/promisify@1.1.3':
|
'@gar/promisify@1.1.3':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6))':
|
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(e320b1d8e94e7308fefdef3743329630))':
|
||||||
dependencies:
|
dependencies:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
@@ -16372,7 +16381,7 @@ snapshots:
|
|||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
langchain: 0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6)
|
langchain: 0.3.11(e320b1d8e94e7308fefdef3743329630)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
@@ -16887,7 +16896,7 @@ snapshots:
|
|||||||
- aws-crt
|
- aws-crt
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
'@langchain/community@0.3.24(d72b3dbd91eb98a3175f929d13e7c0a7)':
|
'@langchain/community@0.3.24(a23560be5fb93c23c5c4ed2a6b67082b)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.49.1)(deepmerge@4.3.1)(dotenv@16.4.5)(encoding@0.1.13)(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))(zod@3.24.1)
|
||||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||||
@@ -16898,7 +16907,7 @@ snapshots:
|
|||||||
flat: 5.0.2
|
flat: 5.0.2
|
||||||
ibm-cloud-sdk-core: 5.3.2
|
ibm-cloud-sdk-core: 5.3.2
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
langchain: 0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6)
|
langchain: 0.3.11(e320b1d8e94e7308fefdef3743329630)
|
||||||
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
langsmith: 0.2.15(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
openai: 4.78.1(encoding@0.1.13)(zod@3.24.1)
|
||||||
uuid: 10.0.0
|
uuid: 10.0.0
|
||||||
@@ -16913,7 +16922,7 @@ snapshots:
|
|||||||
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
'@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0)
|
||||||
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
'@azure/storage-blob': 12.18.0(encoding@0.1.13)
|
||||||
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
'@browserbasehq/sdk': 2.0.0(encoding@0.1.13)
|
||||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6))
|
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)(langchain@0.3.11(e320b1d8e94e7308fefdef3743329630))
|
||||||
'@getzep/zep-js': 0.9.0
|
'@getzep/zep-js': 0.9.0
|
||||||
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
'@google-ai/generativelanguage': 2.6.0(encoding@0.1.13)
|
||||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||||
@@ -23107,7 +23116,7 @@ snapshots:
|
|||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 18.16.16
|
'@types/node': 18.16.16
|
||||||
'@types/tough-cookie': 4.0.2
|
'@types/tough-cookie': 4.0.2
|
||||||
axios: 1.8.3(debug@4.4.0)
|
axios: 1.8.3
|
||||||
camelcase: 6.3.0
|
camelcase: 6.3.0
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
debug: 4.4.0(supports-color@8.1.1)
|
||||||
dotenv: 16.4.5
|
dotenv: 16.4.5
|
||||||
@@ -23117,7 +23126,7 @@ snapshots:
|
|||||||
isstream: 0.1.2
|
isstream: 0.1.2
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
retry-axios: 2.6.0(axios@1.8.3)
|
retry-axios: 2.6.0(axios@1.8.3(debug@4.4.0))
|
||||||
tough-cookie: 4.1.3
|
tough-cookie: 4.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -24111,7 +24120,7 @@ snapshots:
|
|||||||
|
|
||||||
kuler@2.0.0: {}
|
kuler@2.0.0: {}
|
||||||
|
|
||||||
langchain@0.3.11(73c39badb3fd5b3eb4d1084b1fb22de6):
|
langchain@0.3.11(e320b1d8e94e7308fefdef3743329630):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
'@langchain/core': 0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1))
|
||||||
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
'@langchain/openai': 0.3.17(@langchain/core@0.3.30(openai@4.78.1(encoding@0.1.13)(zod@3.24.1)))(encoding@0.1.13)
|
||||||
@@ -26484,7 +26493,7 @@ snapshots:
|
|||||||
onetime: 5.1.2
|
onetime: 5.1.2
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
|
|
||||||
retry-axios@2.6.0(axios@1.8.3):
|
retry-axios@2.6.0(axios@1.8.3(debug@4.4.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.8.3
|
axios: 1.8.3
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user