diff --git a/CHANGELOG.md b/CHANGELOG.md index b1558420d0..baa7b95b7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24) + + +### Bug Fixes + +* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167)) +* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64)) +* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e)) +* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b)) +* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d)) +* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c)) +* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31)) +* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef)) +* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a)) +* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1)) +* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc)) +* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9)) +* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06)) +* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78)) +* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e)) +* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d)) +* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a)) +* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800)) +* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14)) +* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513)) +* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a)) +* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088)) +* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5)) +* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5)) +* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500)) +* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11)) +* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3)) +* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2)) +* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624)) +* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc)) +* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e)) + + +### Features + +* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a)) +* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3)) +* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b)) +* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2)) +* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4)) +* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70)) +* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6)) +* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de)) +* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb)) +* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9)) +* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141)) + + + # [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16) diff --git a/package.json b/package.json index b75eb0aab1..09c576a8c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "n8n-monorepo", - "version": "1.64.0", + "version": "1.65.0", "private": true, "engines": { "node": ">=20.15", diff --git a/packages/@n8n/benchmark/package.json b/packages/@n8n/benchmark/package.json index 40a77afac5..abb8514c3d 100644 --- a/packages/@n8n/benchmark/package.json +++ b/packages/@n8n/benchmark/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-benchmark", - "version": "1.7.0", + "version": "1.8.0", "description": "Cli for running benchmark tests for n8n", "main": "dist/index", "scripts": { diff --git a/packages/@n8n/config/package.json b/packages/@n8n/config/package.json index 627e9b7ef3..ad583c1108 100644 --- a/packages/@n8n/config/package.json +++ b/packages/@n8n/config/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/config", - "version": "1.14.0", + "version": "1.15.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/config/src/configs/logging.config.ts b/packages/@n8n/config/src/configs/logging.config.ts index 736e32a903..0568eaf791 100644 --- a/packages/@n8n/config/src/configs/logging.config.ts +++ b/packages/@n8n/config/src/configs/logging.config.ts @@ -4,6 +4,7 @@ import { StringArray } from '../utils'; /** Scopes (areas of functionality) to filter logs by. */ export const LOG_SCOPES = [ 'concurrency', + 'external-secrets', 'license', 'multi-main-setup', 'pubsub', @@ -64,6 +65,7 @@ export class LoggingConfig { * Supported log scopes: * * - `concurrency` + * - `external-secrets` * - `license` * - `multi-main-setup` * - `pubsub` diff --git a/packages/@n8n/json-schema-to-zod/package.json b/packages/@n8n/json-schema-to-zod/package.json index 859d62226f..e1ae6beaa9 100644 --- a/packages/@n8n/json-schema-to-zod/package.json +++ b/packages/@n8n/json-schema-to-zod/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/json-schema-to-zod", - "version": "1.0.0", + "version": "1.1.0", "description": "Converts JSON schema objects into Zod schemas", "types": "./dist/types/index.d.ts", "main": "./dist/cjs/index.js", diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 0a3dab15cb..3672f73464 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/n8n-nodes-langchain", - "version": "1.64.0", + "version": "1.65.0", "description": "", "main": "index.js", "scripts": { diff --git a/packages/@n8n/permissions/package.json b/packages/@n8n/permissions/package.json index 0dbc5ee515..d92c2c20ac 100644 --- a/packages/@n8n/permissions/package.json +++ b/packages/@n8n/permissions/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/permissions", - "version": "0.14.0", + "version": "0.15.0", "scripts": { "clean": "rimraf dist .turbo", "dev": "pnpm watch", diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index 07ed750f91..f32c00ad41 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -5,45 +5,33 @@ export type Resource = keyof typeof RESOURCES; export type ResourceScope< R extends Resource, - Operation extends string = DefaultOperations, + Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number], > = `${R}:${Operation}`; export type WildcardScope = `${Resource}:*` | '*'; export type AnnotationTagScope = ResourceScope<'annotationTag'>; -export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>; -export type BannerScope = ResourceScope<'banner', 'dismiss'>; -export type CommunityScope = ResourceScope<'community', 'register'>; -export type CommunityPackageScope = ResourceScope< - 'communityPackage', - 'install' | 'uninstall' | 'update' | 'list' | 'manage' ->; -export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share' | 'move'>; -export type ExternalSecretScope = ResourceScope<'externalSecret', 'list' | 'use'>; -export type ExternalSecretProviderScope = ResourceScope< - 'externalSecretsProvider', - DefaultOperations | 'sync' ->; -export type EventBusDestinationScope = ResourceScope< - 'eventBusDestination', - DefaultOperations | 'test' ->; -export type LdapScope = ResourceScope<'ldap', 'manage' | 'sync'>; -export type LicenseScope = ResourceScope<'license', 'manage'>; -export type LogStreamingScope = ResourceScope<'logStreaming', 'manage'>; -export type OrchestrationScope = ResourceScope<'orchestration', 'read' | 'list'>; +export type AuditLogsScope = ResourceScope<'auditLogs'>; +export type BannerScope = ResourceScope<'banner'>; +export type CommunityScope = ResourceScope<'community'>; +export type CommunityPackageScope = ResourceScope<'communityPackage'>; +export type CredentialScope = ResourceScope<'credential'>; +export type ExternalSecretScope = ResourceScope<'externalSecret'>; +export type ExternalSecretProviderScope = ResourceScope<'externalSecretsProvider'>; +export type EventBusDestinationScope = ResourceScope<'eventBusDestination'>; +export type LdapScope = ResourceScope<'ldap'>; +export type LicenseScope = ResourceScope<'license'>; +export type LogStreamingScope = ResourceScope<'logStreaming'>; +export type OrchestrationScope = ResourceScope<'orchestration'>; export type ProjectScope = ResourceScope<'project'>; -export type SamlScope = ResourceScope<'saml', 'manage'>; -export type SecurityAuditScope = ResourceScope<'securityAudit', 'generate'>; -export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>; +export type SamlScope = ResourceScope<'saml'>; +export type SecurityAuditScope = ResourceScope<'securityAudit'>; +export type SourceControlScope = ResourceScope<'sourceControl'>; export type TagScope = ResourceScope<'tag'>; -export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>; +export type UserScope = ResourceScope<'user'>; export type VariableScope = ResourceScope<'variable'>; -export type WorkersViewScope = ResourceScope<'workersView', 'manage'>; -export type WorkflowScope = ResourceScope< - 'workflow', - DefaultOperations | 'share' | 'execute' | 'move' ->; +export type WorkersViewScope = ResourceScope<'workersView'>; +export type WorkflowScope = ResourceScope<'workflow'>; export type Scope = | AnnotationTagScope diff --git a/packages/@n8n/task-runner/package.json b/packages/@n8n/task-runner/package.json index f88fbd7412..d889bde6f3 100644 --- a/packages/@n8n/task-runner/package.json +++ b/packages/@n8n/task-runner/package.json @@ -1,6 +1,6 @@ { "name": "@n8n/task-runner", - "version": "1.2.0", + "version": "1.3.0", "scripts": { "clean": "rimraf dist .turbo", "start": "node dist/start.js", diff --git a/packages/cli/package.json b/packages/cli/package.json index dd2f8e6499..2d95aa1cfa 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "1.64.0", + "version": "1.65.0", "description": "n8n Workflow Automation Tool", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/cli/src/databases/repositories/execution-data.repository.ts b/packages/cli/src/databases/repositories/execution-data.repository.ts index 7f54a6f214..f7de742941 100644 --- a/packages/cli/src/databases/repositories/execution-data.repository.ts +++ b/packages/cli/src/databases/repositories/execution-data.repository.ts @@ -1,4 +1,6 @@ import { DataSource, In, Repository } from '@n8n/typeorm'; +import type { EntityManager } from '@n8n/typeorm'; +import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity'; import { Service } from 'typedi'; import { ExecutionData } from '../entities/execution-data'; @@ -9,6 +11,13 @@ export class ExecutionDataRepository extends Repository { super(ExecutionData, dataSource.manager); } + async createExecutionDataForExecution( + data: QueryDeepPartialEntity, + transactionManager: EntityManager, + ) { + return await transactionManager.insert(ExecutionData, data); + } + async findByExecutionIds(executionIds: string[]) { return await this.find({ select: ['workflowData'], diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index 7b26463969..ce4bedac2a 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -304,16 +304,34 @@ export class ExecutionRepository extends Repository { * Insert a new execution and its execution data using a transaction. */ async createNewExecution(execution: CreateExecutionPayload): Promise { - const { data, workflowData, ...rest } = execution; - const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() }); - const { id: executionId } = inserted[0] as { id: string }; - const { connections, nodes, name, settings } = workflowData ?? {}; - await this.executionDataRepository.insert({ - executionId, - workflowData: { connections, nodes, name, settings, id: workflowData.id }, - data: stringify(data), - }); - return String(executionId); + const { data: dataObj, workflowData: currentWorkflow, ...rest } = execution; + const { connections, nodes, name, settings } = currentWorkflow ?? {}; + const workflowData = { connections, nodes, name, settings, id: currentWorkflow.id }; + const data = stringify(dataObj); + + const { type: dbType, sqlite: sqliteConfig } = this.globalConfig.database; + if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) { + // TODO: Delete this block of code once the sqlite legacy (non-pooling) driver is dropped. + // In the non-pooling sqlite driver we can't use transactions, because that creates nested transactions under highly concurrent loads, leading to errors in the database + const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() }); + const { id: executionId } = inserted[0] as { id: string }; + await this.executionDataRepository.insert({ executionId, workflowData, data }); + return String(executionId); + } else { + // All other database drivers should create executions and execution-data atomically + return await this.manager.transaction(async (transactionManager) => { + const { identifiers: inserted } = await transactionManager.insert(ExecutionEntity, { + ...rest, + createdAt: new Date(), + }); + const { id: executionId } = inserted[0] as { id: string }; + await this.executionDataRepository.createExecutionDataForExecution( + { executionId, workflowData, data }, + transactionManager, + ); + return String(executionId); + }); + } } async markAsCrashed(executionIds: string | string[]) { diff --git a/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts b/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts index b1a87271f9..05eabd104f 100644 --- a/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts +++ b/packages/cli/src/external-secrets/__tests__/external-secrets-manager.ee.test.ts @@ -13,7 +13,7 @@ import { FailedProvider, MockProviders, } from '@test/external-secrets/utils'; -import { mockInstance } from '@test/mocking'; +import { mockInstance, mockLogger } from '@test/mocking'; describe('External Secrets Manager', () => { const connectedDate = '2023-08-01T12:32:29.000Z'; @@ -49,7 +49,7 @@ describe('External Secrets Manager', () => { license.isExternalSecretsEnabled.mockReturnValue(true); settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings); manager = new ExternalSecretsManager( - mock(), + mockLogger(), settingsRepo, license, providersMock, diff --git a/packages/cli/src/external-secrets/external-secrets-manager.ee.ts b/packages/cli/src/external-secrets/external-secrets-manager.ee.ts index ec7c3ed0cf..2de681a7d6 100644 --- a/packages/cli/src/external-secrets/external-secrets-manager.ee.ts +++ b/packages/cli/src/external-secrets/external-secrets-manager.ee.ts @@ -1,5 +1,5 @@ import { Cipher } from 'n8n-core'; -import { jsonParse, type IDataObject, ApplicationError } from 'n8n-workflow'; +import { jsonParse, type IDataObject, ApplicationError, ensureError } from 'n8n-workflow'; import { Service } from 'typedi'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; @@ -39,7 +39,9 @@ export class ExternalSecretsManager { private readonly cipher: Cipher, private readonly eventService: EventService, private readonly publisher: Publisher, - ) {} + ) { + this.logger = this.logger.scoped('external-secrets'); + } async init(): Promise { if (!this.initialized) { @@ -57,6 +59,8 @@ export class ExternalSecretsManager { } return await this.initializingPromise; } + + this.logger.debug('External secrets manager initialized'); } shutdown() { @@ -66,6 +70,8 @@ export class ExternalSecretsManager { void p.disconnect().catch(() => {}); }); Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v)); + + this.logger.debug('External secrets manager shut down'); } async reloadAllProviders(backoff?: number) { @@ -77,6 +83,8 @@ export class ExternalSecretsManager { for (const provider of providers) { await this.reloadProvider(provider, backoff); } + + this.logger.debug('External secrets managed reloaded all providers'); } broadcastReloadExternalSecretsProviders() { @@ -191,6 +199,8 @@ export class ExternalSecretsManager { } }), ); + + this.logger.debug('External secrets manager updated secrets'); } getProvider(provider: string): SecretsProvider | undefined { @@ -261,6 +271,8 @@ export class ExternalSecretsManager { if (newProvider) { this.providers[provider] = newProvider; } + + this.logger.debug(`External secrets manager reloaded provider ${provider}`); } async setProviderSettings(provider: string, data: IDataObject, userId?: string) { @@ -382,8 +394,12 @@ export class ExternalSecretsManager { try { await this.providers[provider].update(); this.broadcastReloadExternalSecretsProviders(); + this.logger.debug(`External secrets manager updated provider ${provider}`); return true; - } catch { + } catch (error) { + this.logger.debug(`External secrets manager failed to update provider ${provider}`, { + error: ensureError(error), + }); return false; } } diff --git a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts b/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts index 2c17aee5a6..6c2c0669fb 100644 --- a/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts +++ b/packages/cli/src/external-secrets/providers/aws-secrets/aws-secrets-manager.ts @@ -1,8 +1,10 @@ import type { INodeProperties } from 'n8n-workflow'; +import Container from 'typedi'; import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; +import { Logger } from '@/logging/logger.service'; import { AwsSecretsClient } from './aws-secrets-client'; import type { AwsSecretsManagerContext } from './types'; @@ -76,10 +78,16 @@ export class AwsSecretsManager implements SecretsProvider { private client: AwsSecretsClient; + constructor(private readonly logger = Container.get(Logger)) { + this.logger = this.logger.scoped('external-secrets'); + } + async init(context: AwsSecretsManagerContext) { this.assertAuthType(context); this.client = new AwsSecretsClient(context.settings); + + this.logger.debug('AWS Secrets Manager provider initialized'); } async test() { @@ -87,9 +95,15 @@ export class AwsSecretsManager implements SecretsProvider { } async connect() { - const [wasSuccessful] = await this.test(); + const [wasSuccessful, errorMsg] = await this.test(); this.state = wasSuccessful ? 'connected' : 'error'; + + if (wasSuccessful) { + this.logger.debug('AWS Secrets Manager provider connected'); + } else { + this.logger.error('AWS Secrets Manager provider failed to connect', { errorMsg }); + } } async disconnect() { @@ -104,6 +118,8 @@ export class AwsSecretsManager implements SecretsProvider { this.cachedSecrets = Object.fromEntries( supportedSecrets.map((s) => [s.secretName, s.secretValue]), ); + + this.logger.debug('AWS Secrets Manager provider secrets updated'); } getSecret(name: string) { diff --git a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts b/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts index e753f0abbf..7961f21bad 100644 --- a/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts +++ b/packages/cli/src/external-secrets/providers/azure-key-vault/azure-key-vault.ts @@ -1,8 +1,11 @@ import type { SecretClient } from '@azure/keyvault-secrets'; +import { ensureError } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow'; +import Container from 'typedi'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; +import { Logger } from '@/logging/logger.service'; import type { AzureKeyVaultContext } from './types'; @@ -64,8 +67,14 @@ export class AzureKeyVault implements SecretsProvider { private settings: AzureKeyVaultContext['settings']; + constructor(private readonly logger = Container.get(Logger)) { + this.logger = this.logger.scoped('external-secrets'); + } + async init(context: AzureKeyVaultContext) { this.settings = context.settings; + + this.logger.debug('Azure Key Vault provider initialized'); } async connect() { @@ -78,8 +87,12 @@ export class AzureKeyVault implements SecretsProvider { const credential = new ClientSecretCredential(tenantId, clientId, clientSecret); this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential); this.state = 'connected'; - } catch { + this.logger.debug('Azure Key Vault provider connected'); + } catch (error) { this.state = 'error'; + this.logger.error('Azure Key Vault provider failed to connect', { + error: ensureError(error), + }); } } @@ -119,6 +132,8 @@ export class AzureKeyVault implements SecretsProvider { acc[cur.name] = cur.value; return acc; }, {}); + + this.logger.debug('Azure Key Vault provider secrets updated'); } getSecret(name: string) { diff --git a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts b/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts index e6bcd11209..c4bf71cb72 100644 --- a/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts +++ b/packages/cli/src/external-secrets/providers/gcp-secrets-manager/gcp-secrets-manager.ts @@ -1,8 +1,10 @@ import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager'; -import { jsonParse, type INodeProperties } from 'n8n-workflow'; +import { ensureError, jsonParse, type INodeProperties } from 'n8n-workflow'; +import Container from 'typedi'; import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants'; import type { SecretsProvider, SecretsProviderState } from '@/interfaces'; +import { Logger } from '@/logging/logger.service'; import type { GcpSecretsManagerContext, @@ -38,6 +40,10 @@ export class GcpSecretsManager implements SecretsProvider { private settings: GcpSecretAccountKey; + constructor(private readonly logger = Container.get(Logger)) { + this.logger = this.logger.scoped('external-secrets'); + } + async init(context: GcpSecretsManagerContext) { this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey); } @@ -53,8 +59,12 @@ export class GcpSecretsManager implements SecretsProvider { projectId, }); this.state = 'connected'; - } catch { + this.logger.debug('GCP Secrets Manager provider connected'); + } catch (error) { this.state = 'error'; + this.logger.debug('GCP Secrets Manager provider failed to connect', { + error: ensureError(error), + }); } } @@ -114,6 +124,8 @@ export class GcpSecretsManager implements SecretsProvider { if (cur) acc[cur.name] = cur.value; return acc; }, {}); + + this.logger.debug('GCP Secrets Manager provider secrets updated'); } getSecret(name: string) { diff --git a/packages/cli/src/external-secrets/providers/vault.ts b/packages/cli/src/external-secrets/providers/vault.ts index 398c40745d..0f1e93a5da 100644 --- a/packages/cli/src/external-secrets/providers/vault.ts +++ b/packages/cli/src/external-secrets/providers/vault.ts @@ -237,6 +237,7 @@ export class VaultProvider extends SecretsProvider { constructor(readonly logger = Container.get(Logger)) { super(); + this.logger = this.logger.scoped('external-secrets'); } async init(settings: SecretsProviderSettings): Promise { @@ -257,6 +258,8 @@ export class VaultProvider extends SecretsProvider { } return config; }); + + this.logger.debug('Vault provider initialized'); } async connect(): Promise { @@ -408,6 +411,7 @@ export class VaultProvider extends SecretsProvider { kvVersion: string, path: string, ): Promise<[string, IDataObject] | null> { + this.logger.debug(`Getting kv secrets from ${mountPath}${path} (version ${kvVersion})`); let listPath = mountPath; if (kvVersion === '2') { listPath += 'metadata/'; @@ -441,6 +445,7 @@ export class VaultProvider extends SecretsProvider { secretPath += path + key; try { const secretResp = await this.#http.get>(secretPath); + this.logger.debug(`Vault provider retrieved secrets from ${secretPath}`); return [ key, kvVersion === '2' @@ -457,6 +462,7 @@ export class VaultProvider extends SecretsProvider { .filter((v): v is [string, IDataObject] => v !== null), ); const name = path.substring(0, path.length - 1); + this.logger.debug(`Vault provider retrieved kv secrets from ${name}`); return [name, data]; } @@ -479,6 +485,7 @@ export class VaultProvider extends SecretsProvider { ).filter((v): v is [string, IDataObject] => v !== null), ); this.cachedSecrets = secrets; + this.logger.debug('Vault provider secrets updated'); } async test(): Promise<[boolean] | [boolean, string]> { diff --git a/packages/cli/src/interfaces.ts b/packages/cli/src/interfaces.ts index 4d2cd9b2d9..65b3a56346 100644 --- a/packages/cli/src/interfaces.ts +++ b/packages/cli/src/interfaces.ts @@ -355,7 +355,7 @@ export type NumericLicenseFeature = ValuesOf; export interface ILicenseReadResponse { usage: { - executions: { + activeWorkflowTriggers: { limit: number; value: number; warningThreshold: number; diff --git a/packages/cli/src/license/__tests__/license.service.test.ts b/packages/cli/src/license/__tests__/license.service.test.ts index 77afe04a2c..9cd9c0ee0b 100644 --- a/packages/cli/src/license/__tests__/license.service.test.ts +++ b/packages/cli/src/license/__tests__/license.service.test.ts @@ -41,7 +41,7 @@ describe('LicenseService', () => { const data = await licenseService.getLicenseData(); expect(data).toEqual({ usage: { - executions: { + activeWorkflowTriggers: { limit: 400, value: 7, warningThreshold: 0.8, diff --git a/packages/cli/src/license/license.service.ts b/packages/cli/src/license/license.service.ts index 43f9961334..cdd6454036 100644 --- a/packages/cli/src/license/license.service.ts +++ b/packages/cli/src/license/license.service.ts @@ -37,7 +37,7 @@ export class LicenseService { return { usage: { - executions: { + activeWorkflowTriggers: { value: triggerCount, limit: this.license.getTriggerLimit(), warningThreshold: 0.8, diff --git a/packages/cli/src/logging/__tests__/logger.service.test.ts b/packages/cli/src/logging/__tests__/logger.service.test.ts index d01a709639..2ffbf2120e 100644 --- a/packages/cli/src/logging/__tests__/logger.service.test.ts +++ b/packages/cli/src/logging/__tests__/logger.service.test.ts @@ -1,10 +1,42 @@ +jest.mock('n8n-workflow', () => ({ + ...jest.requireActual('n8n-workflow'), + LoggerProxy: { init: jest.fn() }, +})); + import type { GlobalConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; import type { InstanceSettings } from 'n8n-core'; +import { LoggerProxy } from 'n8n-workflow'; import { Logger } from '@/logging/logger.service'; describe('Logger', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('constructor', () => { + const globalConfig = mock({ + logging: { + level: 'info', + outputs: ['console'], + scopes: [], + }, + }); + + test('if root, should initialize `LoggerProxy` with instance', () => { + const logger = new Logger(globalConfig, mock(), { isRoot: true }); + + expect(LoggerProxy.init).toHaveBeenCalledWith(logger); + }); + + test('if scoped, should not initialize `LoggerProxy`', () => { + new Logger(globalConfig, mock(), { isRoot: false }); + + expect(LoggerProxy.init).not.toHaveBeenCalled(); + }); + }); + describe('transports', () => { test('if `console` selected, should set console transport', () => { const globalConfig = mock({ diff --git a/packages/cli/src/logging/logger.service.ts b/packages/cli/src/logging/logger.service.ts index 87e5fbe015..46471c0611 100644 --- a/packages/cli/src/logging/logger.service.ts +++ b/packages/cli/src/logging/logger.service.ts @@ -30,6 +30,7 @@ export class Logger { constructor( private readonly globalConfig: GlobalConfig, private readonly instanceSettings: InstanceSettings, + { isRoot }: { isRoot?: boolean } = { isRoot: true }, ) { this.level = this.globalConfig.logging.level; @@ -51,7 +52,7 @@ export class Logger { this.scopes = new Set(scopes); } - LoggerProxy.init(this); + if (isRoot) LoggerProxy.init(this); } private setInternalLogger(internalLogger: winston.Logger) { @@ -61,7 +62,7 @@ export class Logger { /** Create a logger that injects the given scopes into its log metadata. */ scoped(scopes: LogScope | LogScope[]) { scopes = Array.isArray(scopes) ? scopes : [scopes]; - const scopedLogger = new Logger(this.globalConfig, this.instanceSettings); + const scopedLogger = new Logger(this.globalConfig, this.instanceSettings, { isRoot: false }); const childLogger = this.internalLogger.child({ scopes }); scopedLogger.setInternalLogger(childLogger); diff --git a/packages/cli/src/metrics/__tests__/license-metrics.service.test.ts b/packages/cli/src/metrics/__tests__/license-metrics.service.test.ts index ec123a3f01..6771b13fb7 100644 --- a/packages/cli/src/metrics/__tests__/license-metrics.service.test.ts +++ b/packages/cli/src/metrics/__tests__/license-metrics.service.test.ts @@ -6,11 +6,14 @@ import { LicenseMetricsService } from '@/metrics/license-metrics.service'; describe('LicenseMetricsService', () => { const workflowRepository = mock(); + const licenseMetricsRespository = mock(); const licenseMetricsService = new LicenseMetricsService( - mock(), + licenseMetricsRespository, workflowRepository, ); + beforeEach(() => jest.clearAllMocks()); + describe('collectPassthroughData', () => { test('should return an object with active workflow IDs', async () => { /** @@ -30,4 +33,36 @@ describe('LicenseMetricsService', () => { expect(result).toEqual({ activeWorkflowIds }); }); }); + + describe('collectUsageMetrics', () => { + test('should return an array of expected usage metrics', async () => { + const mockActiveTriggerCount = 1234; + workflowRepository.getActiveTriggerCount.mockResolvedValue(mockActiveTriggerCount); + + const mockRenewalMetrics = { + activeWorkflows: 100, + totalWorkflows: 200, + enabledUsers: 300, + totalUsers: 400, + totalCredentials: 500, + productionExecutions: 600, + manualExecutions: 700, + }; + + licenseMetricsRespository.getLicenseRenewalMetrics.mockResolvedValue(mockRenewalMetrics); + + const result = await licenseMetricsService.collectUsageMetrics(); + + expect(result).toEqual([ + { name: 'activeWorkflows', value: mockRenewalMetrics.activeWorkflows }, + { name: 'totalWorkflows', value: mockRenewalMetrics.totalWorkflows }, + { name: 'enabledUsers', value: mockRenewalMetrics.enabledUsers }, + { name: 'totalUsers', value: mockRenewalMetrics.totalUsers }, + { name: 'totalCredentials', value: mockRenewalMetrics.totalCredentials }, + { name: 'productionExecutions', value: mockRenewalMetrics.productionExecutions }, + { name: 'manualExecutions', value: mockRenewalMetrics.manualExecutions }, + { name: 'activeWorkflowTriggers', value: mockActiveTriggerCount }, + ]); + }); + }); }); diff --git a/packages/cli/src/metrics/license-metrics.service.ts b/packages/cli/src/metrics/license-metrics.service.ts index ba546b3cc0..338fa70a3e 100644 --- a/packages/cli/src/metrics/license-metrics.service.ts +++ b/packages/cli/src/metrics/license-metrics.service.ts @@ -21,6 +21,8 @@ export class LicenseMetricsService { manualExecutions, } = await this.licenseMetricsRepository.getLicenseRenewalMetrics(); + const activeTriggerCount = await this.workflowRepository.getActiveTriggerCount(); + return [ { name: 'activeWorkflows', value: activeWorkflows }, { name: 'totalWorkflows', value: totalWorkflows }, @@ -29,6 +31,7 @@ export class LicenseMetricsService { { name: 'totalCredentials', value: totalCredentials }, { name: 'productionExecutions', value: productionExecutions }, { name: 'manualExecutions', value: manualExecutions }, + { name: 'activeWorkflowTriggers', value: activeTriggerCount }, ]; } diff --git a/packages/cli/src/scaling/__tests__/publisher.service.test.ts b/packages/cli/src/scaling/__tests__/publisher.service.test.ts index f77b6b5d5a..f69ad08cb5 100644 --- a/packages/cli/src/scaling/__tests__/publisher.service.test.ts +++ b/packages/cli/src/scaling/__tests__/publisher.service.test.ts @@ -44,6 +44,16 @@ describe('Publisher', () => { }); describe('publishCommand', () => { + it('should do nothing if not in scaling mode', async () => { + config.set('executions.mode', 'regular'); + const publisher = new Publisher(logger, redisClientService, instanceSettings); + const msg = mock({ command: 'reload-license' }); + + await publisher.publishCommand(msg); + + expect(client.publish).not.toHaveBeenCalled(); + }); + it('should publish command into `n8n.commands` pubsub channel', async () => { const publisher = new Publisher(logger, redisClientService, instanceSettings); const msg = mock({ command: 'reload-license' }); diff --git a/packages/cli/src/scaling/pubsub/publisher.service.ts b/packages/cli/src/scaling/pubsub/publisher.service.ts index cc28c2d339..248a455e3e 100644 --- a/packages/cli/src/scaling/pubsub/publisher.service.ts +++ b/packages/cli/src/scaling/pubsub/publisher.service.ts @@ -23,7 +23,7 @@ export class Publisher { private readonly redisClientService: RedisClientService, private readonly instanceSettings: InstanceSettings, ) { - // @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead. + // @TODO: Once this class is only ever initialized in scaling mode, assert in the next line. if (config.getEnv('executions.mode') !== 'queue') return; this.logger = this.logger.scoped(['scaling', 'pubsub']); @@ -46,6 +46,9 @@ export class Publisher { /** Publish a command into the `n8n.commands` channel. */ async publishCommand(msg: Omit) { + // @TODO: Once this class is only ever used in scaling mode, remove next line. + if (config.getEnv('executions.mode') !== 'queue') return; + await this.client.publish( 'n8n.commands', JSON.stringify({ diff --git a/packages/cli/test/integration/database/repositories/execution.repository.test.ts b/packages/cli/test/integration/database/repositories/execution.repository.test.ts index 52884bd3e6..1b50415686 100644 --- a/packages/cli/test/integration/database/repositories/execution.repository.test.ts +++ b/packages/cli/test/integration/database/repositories/execution.repository.test.ts @@ -1,3 +1,4 @@ +import { GlobalConfig } from '@n8n/config'; import Container from 'typedi'; import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository'; @@ -54,5 +55,38 @@ describe('ExecutionRepository', () => { }); expect(executionData?.data).toEqual('[{"resultData":"1"},{}]'); }); + + it('should not create execution if execution data insert fails', async () => { + const { type: dbType, sqlite: sqliteConfig } = Container.get(GlobalConfig).database; + // Do not run this test for the legacy sqlite driver + if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) return; + + const executionRepo = Container.get(ExecutionRepository); + const executionDataRepo = Container.get(ExecutionDataRepository); + + const workflow = await createWorkflow({ settings: { executionOrder: 'v1' } }); + jest + .spyOn(executionDataRepo, 'createExecutionDataForExecution') + .mockRejectedValueOnce(new Error()); + + await expect( + async () => + await executionRepo.createNewExecution({ + workflowId: workflow.id, + data: { + //@ts-expect-error This is not needed for tests + resultData: {}, + }, + workflowData: workflow, + mode: 'manual', + startedAt: new Date(), + status: 'new', + finished: false, + }), + ).rejects.toThrow(); + + const executionEntities = await executionRepo.find(); + expect(executionEntities).toBeEmptyArray(); + }); }); }); diff --git a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts index 3418576be1..c36340108e 100644 --- a/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts +++ b/packages/cli/test/integration/external-secrets/external-secrets.api.test.ts @@ -18,7 +18,7 @@ import { MockProviders, TestFailProvider, } from '../../shared/external-secrets/utils'; -import { mockInstance } from '../../shared/mocking'; +import { mockInstance, mockLogger } from '../../shared/mocking'; import { createOwner, createUser } from '../shared/db/users'; import type { SuperAgentTest } from '../shared/types'; import { setupTestServer } from '../shared/utils'; @@ -52,12 +52,14 @@ async function getExternalSecretsSettings(): Promise(); +const logger = mockLogger(); + const resetManager = async () => { Container.get(ExternalSecretsManager).shutdown(); Container.set( ExternalSecretsManager, new ExternalSecretsManager( - mock(), + logger, Container.get(SettingsRepository), Container.get(License), mockProvidersInstance, @@ -108,6 +110,18 @@ beforeAll(async () => { const member = await createUser(); authMemberAgent = testServer.authAgentFor(member); config.set('userManagement.isInstanceOwnerSetUp', true); + Container.set( + ExternalSecretsManager, + new ExternalSecretsManager( + logger, + Container.get(SettingsRepository), + Container.get(License), + mockProvidersInstance, + Container.get(Cipher), + eventService, + mock(), + ), + ); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/license.api.test.ts b/packages/cli/test/integration/license.api.test.ts index ff40a699ea..a06408c6cb 100644 --- a/packages/cli/test/integration/license.api.test.ts +++ b/packages/cli/test/integration/license.api.test.ts @@ -116,7 +116,7 @@ describe('POST /license/renew', () => { const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = { data: { usage: { - executions: { + activeWorkflowTriggers: { value: 0, limit: -1, warningThreshold: 0.8, @@ -132,7 +132,7 @@ const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = { const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = { data: { usage: { - executions: { + activeWorkflowTriggers: { value: 0, limit: -1, warningThreshold: 0.8, diff --git a/packages/core/package.json b/packages/core/package.json index 8d6088ccba..0e010052b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "1.64.0", + "version": "1.65.0", "description": "Core functionality of n8n", "main": "dist/index", "types": "dist/index.d.ts", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index 6b59977d56..8aec0ab60f 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -1,6 +1,6 @@ { "name": "n8n-design-system", - "version": "1.54.0", + "version": "1.55.0", "main": "src/main.ts", "import": "src/main.ts", "scripts": { diff --git a/packages/design-system/src/__tests__/setup.ts b/packages/design-system/src/__tests__/setup.ts index 6eb1c426fc..981c9d5a60 100644 --- a/packages/design-system/src/__tests__/setup.ts +++ b/packages/design-system/src/__tests__/setup.ts @@ -1,8 +1,11 @@ import '@testing-library/jest-dom'; +import { configure } from '@testing-library/vue'; import { config } from '@vue/test-utils'; import { N8nPlugin } from 'n8n-design-system/plugin'; +configure({ testIdAttribute: 'data-test-id' }); + config.global.plugins = [N8nPlugin]; window.ResizeObserver = diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 69d52a8138..5a4280252e 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "1.64.0", + "version": "1.65.0", "description": "Workflow Editor UI for n8n", "main": "index.js", "scripts": { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index add7905fcf..e639f537df 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -1296,7 +1296,7 @@ export type UsageState = { loading: boolean; data: { usage: { - executions: { + activeWorkflowTriggers: { limit: number; // -1 for unlimited, from license value: number; warningThreshold: number; // hardcoded value in BE diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue index 897d8304b6..19258d2df9 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionTip.vue @@ -1,13 +1,13 @@