mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into ai-396-implement-split-pane
This commit is contained in:
commit
f9e177958c
54
CHANGELOG.md
54
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)
|
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-monorepo",
|
"name": "n8n-monorepo",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.15",
|
"node": ">=20.15",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/n8n-benchmark",
|
"name": "@n8n/n8n-benchmark",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"description": "Cli for running benchmark tests for n8n",
|
"description": "Cli for running benchmark tests for n8n",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/config",
|
"name": "@n8n/config",
|
||||||
"version": "1.14.0",
|
"version": "1.15.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { StringArray } from '../utils';
|
||||||
/** Scopes (areas of functionality) to filter logs by. */
|
/** Scopes (areas of functionality) to filter logs by. */
|
||||||
export const LOG_SCOPES = [
|
export const LOG_SCOPES = [
|
||||||
'concurrency',
|
'concurrency',
|
||||||
|
'external-secrets',
|
||||||
'license',
|
'license',
|
||||||
'multi-main-setup',
|
'multi-main-setup',
|
||||||
'pubsub',
|
'pubsub',
|
||||||
|
@ -64,6 +65,7 @@ export class LoggingConfig {
|
||||||
* Supported log scopes:
|
* Supported log scopes:
|
||||||
*
|
*
|
||||||
* - `concurrency`
|
* - `concurrency`
|
||||||
|
* - `external-secrets`
|
||||||
* - `license`
|
* - `license`
|
||||||
* - `multi-main-setup`
|
* - `multi-main-setup`
|
||||||
* - `pubsub`
|
* - `pubsub`
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/json-schema-to-zod",
|
"name": "@n8n/json-schema-to-zod",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Converts JSON schema objects into Zod schemas",
|
"description": "Converts JSON schema objects into Zod schemas",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
"main": "./dist/cjs/index.js",
|
"main": "./dist/cjs/index.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/n8n-nodes-langchain",
|
"name": "@n8n/n8n-nodes-langchain",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/permissions",
|
"name": "@n8n/permissions",
|
||||||
"version": "0.14.0",
|
"version": "0.15.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"dev": "pnpm watch",
|
"dev": "pnpm watch",
|
||||||
|
|
|
@ -5,45 +5,33 @@ export type Resource = keyof typeof RESOURCES;
|
||||||
|
|
||||||
export type ResourceScope<
|
export type ResourceScope<
|
||||||
R extends Resource,
|
R extends Resource,
|
||||||
Operation extends string = DefaultOperations,
|
Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number],
|
||||||
> = `${R}:${Operation}`;
|
> = `${R}:${Operation}`;
|
||||||
|
|
||||||
export type WildcardScope = `${Resource}:*` | '*';
|
export type WildcardScope = `${Resource}:*` | '*';
|
||||||
|
|
||||||
export type AnnotationTagScope = ResourceScope<'annotationTag'>;
|
export type AnnotationTagScope = ResourceScope<'annotationTag'>;
|
||||||
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
|
export type AuditLogsScope = ResourceScope<'auditLogs'>;
|
||||||
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
|
export type BannerScope = ResourceScope<'banner'>;
|
||||||
export type CommunityScope = ResourceScope<'community', 'register'>;
|
export type CommunityScope = ResourceScope<'community'>;
|
||||||
export type CommunityPackageScope = ResourceScope<
|
export type CommunityPackageScope = ResourceScope<'communityPackage'>;
|
||||||
'communityPackage',
|
export type CredentialScope = ResourceScope<'credential'>;
|
||||||
'install' | 'uninstall' | 'update' | 'list' | 'manage'
|
export type ExternalSecretScope = ResourceScope<'externalSecret'>;
|
||||||
>;
|
export type ExternalSecretProviderScope = ResourceScope<'externalSecretsProvider'>;
|
||||||
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share' | 'move'>;
|
export type EventBusDestinationScope = ResourceScope<'eventBusDestination'>;
|
||||||
export type ExternalSecretScope = ResourceScope<'externalSecret', 'list' | 'use'>;
|
export type LdapScope = ResourceScope<'ldap'>;
|
||||||
export type ExternalSecretProviderScope = ResourceScope<
|
export type LicenseScope = ResourceScope<'license'>;
|
||||||
'externalSecretsProvider',
|
export type LogStreamingScope = ResourceScope<'logStreaming'>;
|
||||||
DefaultOperations | 'sync'
|
export type OrchestrationScope = ResourceScope<'orchestration'>;
|
||||||
>;
|
|
||||||
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 ProjectScope = ResourceScope<'project'>;
|
export type ProjectScope = ResourceScope<'project'>;
|
||||||
export type SamlScope = ResourceScope<'saml', 'manage'>;
|
export type SamlScope = ResourceScope<'saml'>;
|
||||||
export type SecurityAuditScope = ResourceScope<'securityAudit', 'generate'>;
|
export type SecurityAuditScope = ResourceScope<'securityAudit'>;
|
||||||
export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>;
|
export type SourceControlScope = ResourceScope<'sourceControl'>;
|
||||||
export type TagScope = ResourceScope<'tag'>;
|
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 VariableScope = ResourceScope<'variable'>;
|
||||||
export type WorkersViewScope = ResourceScope<'workersView', 'manage'>;
|
export type WorkersViewScope = ResourceScope<'workersView'>;
|
||||||
export type WorkflowScope = ResourceScope<
|
export type WorkflowScope = ResourceScope<'workflow'>;
|
||||||
'workflow',
|
|
||||||
DefaultOperations | 'share' | 'execute' | 'move'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type Scope =
|
export type Scope =
|
||||||
| AnnotationTagScope
|
| AnnotationTagScope
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@n8n/task-runner",
|
"name": "@n8n/task-runner",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist .turbo",
|
"clean": "rimraf dist .turbo",
|
||||||
"start": "node dist/start.js",
|
"start": "node dist/start.js",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { DataSource, In, Repository } from '@n8n/typeorm';
|
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 { Service } from 'typedi';
|
||||||
|
|
||||||
import { ExecutionData } from '../entities/execution-data';
|
import { ExecutionData } from '../entities/execution-data';
|
||||||
|
@ -9,6 +11,13 @@ export class ExecutionDataRepository extends Repository<ExecutionData> {
|
||||||
super(ExecutionData, dataSource.manager);
|
super(ExecutionData, dataSource.manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createExecutionDataForExecution(
|
||||||
|
data: QueryDeepPartialEntity<ExecutionData>,
|
||||||
|
transactionManager: EntityManager,
|
||||||
|
) {
|
||||||
|
return await transactionManager.insert(ExecutionData, data);
|
||||||
|
}
|
||||||
|
|
||||||
async findByExecutionIds(executionIds: string[]) {
|
async findByExecutionIds(executionIds: string[]) {
|
||||||
return await this.find({
|
return await this.find({
|
||||||
select: ['workflowData'],
|
select: ['workflowData'],
|
||||||
|
|
|
@ -304,16 +304,34 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
||||||
* Insert a new execution and its execution data using a transaction.
|
* Insert a new execution and its execution data using a transaction.
|
||||||
*/
|
*/
|
||||||
async createNewExecution(execution: CreateExecutionPayload): Promise<string> {
|
async createNewExecution(execution: CreateExecutionPayload): Promise<string> {
|
||||||
const { data, workflowData, ...rest } = execution;
|
const { data: dataObj, workflowData: currentWorkflow, ...rest } = execution;
|
||||||
const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() });
|
const { connections, nodes, name, settings } = currentWorkflow ?? {};
|
||||||
const { id: executionId } = inserted[0] as { id: string };
|
const workflowData = { connections, nodes, name, settings, id: currentWorkflow.id };
|
||||||
const { connections, nodes, name, settings } = workflowData ?? {};
|
const data = stringify(dataObj);
|
||||||
await this.executionDataRepository.insert({
|
|
||||||
executionId,
|
const { type: dbType, sqlite: sqliteConfig } = this.globalConfig.database;
|
||||||
workflowData: { connections, nodes, name, settings, id: workflowData.id },
|
if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) {
|
||||||
data: stringify(data),
|
// 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
|
||||||
return String(executionId);
|
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[]) {
|
async markAsCrashed(executionIds: string | string[]) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FailedProvider,
|
FailedProvider,
|
||||||
MockProviders,
|
MockProviders,
|
||||||
} from '@test/external-secrets/utils';
|
} from '@test/external-secrets/utils';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance, mockLogger } from '@test/mocking';
|
||||||
|
|
||||||
describe('External Secrets Manager', () => {
|
describe('External Secrets Manager', () => {
|
||||||
const connectedDate = '2023-08-01T12:32:29.000Z';
|
const connectedDate = '2023-08-01T12:32:29.000Z';
|
||||||
|
@ -49,7 +49,7 @@ describe('External Secrets Manager', () => {
|
||||||
license.isExternalSecretsEnabled.mockReturnValue(true);
|
license.isExternalSecretsEnabled.mockReturnValue(true);
|
||||||
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
|
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
|
||||||
manager = new ExternalSecretsManager(
|
manager = new ExternalSecretsManager(
|
||||||
mock(),
|
mockLogger(),
|
||||||
settingsRepo,
|
settingsRepo,
|
||||||
license,
|
license,
|
||||||
providersMock,
|
providersMock,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Cipher } from 'n8n-core';
|
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 { Service } from 'typedi';
|
||||||
|
|
||||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
|
@ -39,7 +39,9 @@ export class ExternalSecretsManager {
|
||||||
private readonly cipher: Cipher,
|
private readonly cipher: Cipher,
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
private readonly publisher: Publisher,
|
private readonly publisher: Publisher,
|
||||||
) {}
|
) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
|
@ -57,6 +59,8 @@ export class ExternalSecretsManager {
|
||||||
}
|
}
|
||||||
return await this.initializingPromise;
|
return await this.initializingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
|
@ -66,6 +70,8 @@ export class ExternalSecretsManager {
|
||||||
void p.disconnect().catch(() => {});
|
void p.disconnect().catch(() => {});
|
||||||
});
|
});
|
||||||
Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v));
|
Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v));
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager shut down');
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadAllProviders(backoff?: number) {
|
async reloadAllProviders(backoff?: number) {
|
||||||
|
@ -77,6 +83,8 @@ export class ExternalSecretsManager {
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
await this.reloadProvider(provider, backoff);
|
await this.reloadProvider(provider, backoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug('External secrets managed reloaded all providers');
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastReloadExternalSecretsProviders() {
|
broadcastReloadExternalSecretsProviders() {
|
||||||
|
@ -191,6 +199,8 @@ export class ExternalSecretsManager {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager updated secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
getProvider(provider: string): SecretsProvider | undefined {
|
getProvider(provider: string): SecretsProvider | undefined {
|
||||||
|
@ -261,6 +271,8 @@ export class ExternalSecretsManager {
|
||||||
if (newProvider) {
|
if (newProvider) {
|
||||||
this.providers[provider] = newProvider;
|
this.providers[provider] = newProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`External secrets manager reloaded provider ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setProviderSettings(provider: string, data: IDataObject, userId?: string) {
|
async setProviderSettings(provider: string, data: IDataObject, userId?: string) {
|
||||||
|
@ -382,8 +394,12 @@ export class ExternalSecretsManager {
|
||||||
try {
|
try {
|
||||||
await this.providers[provider].update();
|
await this.providers[provider].update();
|
||||||
this.broadcastReloadExternalSecretsProviders();
|
this.broadcastReloadExternalSecretsProviders();
|
||||||
|
this.logger.debug(`External secrets manager updated provider ${provider}`);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
this.logger.debug(`External secrets manager failed to update provider ${provider}`, {
|
||||||
|
error: ensureError(error),
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
|
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
|
||||||
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import { AwsSecretsClient } from './aws-secrets-client';
|
import { AwsSecretsClient } from './aws-secrets-client';
|
||||||
import type { AwsSecretsManagerContext } from './types';
|
import type { AwsSecretsManagerContext } from './types';
|
||||||
|
@ -76,10 +78,16 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
|
|
||||||
private client: AwsSecretsClient;
|
private client: AwsSecretsClient;
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: AwsSecretsManagerContext) {
|
async init(context: AwsSecretsManagerContext) {
|
||||||
this.assertAuthType(context);
|
this.assertAuthType(context);
|
||||||
|
|
||||||
this.client = new AwsSecretsClient(context.settings);
|
this.client = new AwsSecretsClient(context.settings);
|
||||||
|
|
||||||
|
this.logger.debug('AWS Secrets Manager provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async test() {
|
async test() {
|
||||||
|
@ -87,9 +95,15 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
const [wasSuccessful] = await this.test();
|
const [wasSuccessful, errorMsg] = await this.test();
|
||||||
|
|
||||||
this.state = wasSuccessful ? 'connected' : 'error';
|
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() {
|
async disconnect() {
|
||||||
|
@ -104,6 +118,8 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
this.cachedSecrets = Object.fromEntries(
|
this.cachedSecrets = Object.fromEntries(
|
||||||
supportedSecrets.map((s) => [s.secretName, s.secretValue]),
|
supportedSecrets.map((s) => [s.secretName, s.secretValue]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug('AWS Secrets Manager provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import type { SecretClient } from '@azure/keyvault-secrets';
|
import type { SecretClient } from '@azure/keyvault-secrets';
|
||||||
|
import { ensureError } from 'n8n-workflow';
|
||||||
import type { INodeProperties } 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 { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import type { AzureKeyVaultContext } from './types';
|
import type { AzureKeyVaultContext } from './types';
|
||||||
|
|
||||||
|
@ -64,8 +67,14 @@ export class AzureKeyVault implements SecretsProvider {
|
||||||
|
|
||||||
private settings: AzureKeyVaultContext['settings'];
|
private settings: AzureKeyVaultContext['settings'];
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: AzureKeyVaultContext) {
|
async init(context: AzureKeyVaultContext) {
|
||||||
this.settings = context.settings;
|
this.settings = context.settings;
|
||||||
|
|
||||||
|
this.logger.debug('Azure Key Vault provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
|
@ -78,8 +87,12 @@ export class AzureKeyVault implements SecretsProvider {
|
||||||
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||||
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
|
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
} catch {
|
this.logger.debug('Azure Key Vault provider connected');
|
||||||
|
} catch (error) {
|
||||||
this.state = '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;
|
acc[cur.name] = cur.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
this.logger.debug('Azure Key Vault provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager';
|
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 { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GcpSecretsManagerContext,
|
GcpSecretsManagerContext,
|
||||||
|
@ -38,6 +40,10 @@ export class GcpSecretsManager implements SecretsProvider {
|
||||||
|
|
||||||
private settings: GcpSecretAccountKey;
|
private settings: GcpSecretAccountKey;
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: GcpSecretsManagerContext) {
|
async init(context: GcpSecretsManagerContext) {
|
||||||
this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey);
|
this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey);
|
||||||
}
|
}
|
||||||
|
@ -53,8 +59,12 @@ export class GcpSecretsManager implements SecretsProvider {
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
} catch {
|
this.logger.debug('GCP Secrets Manager provider connected');
|
||||||
|
} catch (error) {
|
||||||
this.state = '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;
|
if (cur) acc[cur.name] = cur.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
this.logger.debug('GCP Secrets Manager provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -237,6 +237,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
|
|
||||||
constructor(readonly logger = Container.get(Logger)) {
|
constructor(readonly logger = Container.get(Logger)) {
|
||||||
super();
|
super();
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(settings: SecretsProviderSettings): Promise<void> {
|
async init(settings: SecretsProviderSettings): Promise<void> {
|
||||||
|
@ -257,6 +258,8 @@ export class VaultProvider extends SecretsProvider {
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.debug('Vault provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
|
@ -408,6 +411,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
kvVersion: string,
|
kvVersion: string,
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<[string, IDataObject] | null> {
|
): Promise<[string, IDataObject] | null> {
|
||||||
|
this.logger.debug(`Getting kv secrets from ${mountPath}${path} (version ${kvVersion})`);
|
||||||
let listPath = mountPath;
|
let listPath = mountPath;
|
||||||
if (kvVersion === '2') {
|
if (kvVersion === '2') {
|
||||||
listPath += 'metadata/';
|
listPath += 'metadata/';
|
||||||
|
@ -441,6 +445,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
secretPath += path + key;
|
secretPath += path + key;
|
||||||
try {
|
try {
|
||||||
const secretResp = await this.#http.get<VaultResponse<IDataObject>>(secretPath);
|
const secretResp = await this.#http.get<VaultResponse<IDataObject>>(secretPath);
|
||||||
|
this.logger.debug(`Vault provider retrieved secrets from ${secretPath}`);
|
||||||
return [
|
return [
|
||||||
key,
|
key,
|
||||||
kvVersion === '2'
|
kvVersion === '2'
|
||||||
|
@ -457,6 +462,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
.filter((v): v is [string, IDataObject] => v !== null),
|
.filter((v): v is [string, IDataObject] => v !== null),
|
||||||
);
|
);
|
||||||
const name = path.substring(0, path.length - 1);
|
const name = path.substring(0, path.length - 1);
|
||||||
|
this.logger.debug(`Vault provider retrieved kv secrets from ${name}`);
|
||||||
return [name, data];
|
return [name, data];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,6 +485,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
).filter((v): v is [string, IDataObject] => v !== null),
|
).filter((v): v is [string, IDataObject] => v !== null),
|
||||||
);
|
);
|
||||||
this.cachedSecrets = secrets;
|
this.cachedSecrets = secrets;
|
||||||
|
this.logger.debug('Vault provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
async test(): Promise<[boolean] | [boolean, string]> {
|
async test(): Promise<[boolean] | [boolean, string]> {
|
||||||
|
|
|
@ -355,7 +355,7 @@ export type NumericLicenseFeature = ValuesOf<typeof LICENSE_QUOTAS>;
|
||||||
|
|
||||||
export interface ILicenseReadResponse {
|
export interface ILicenseReadResponse {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
limit: number;
|
limit: number;
|
||||||
value: number;
|
value: number;
|
||||||
warningThreshold: number;
|
warningThreshold: number;
|
||||||
|
|
|
@ -41,7 +41,7 @@ describe('LicenseService', () => {
|
||||||
const data = await licenseService.getLicenseData();
|
const data = await licenseService.getLicenseData();
|
||||||
expect(data).toEqual({
|
expect(data).toEqual({
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
limit: 400,
|
limit: 400,
|
||||||
value: 7,
|
value: 7,
|
||||||
warningThreshold: 0.8,
|
warningThreshold: 0.8,
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class LicenseService {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
value: triggerCount,
|
value: triggerCount,
|
||||||
limit: this.license.getTriggerLimit(),
|
limit: this.license.getTriggerLimit(),
|
||||||
warningThreshold: 0.8,
|
warningThreshold: 0.8,
|
||||||
|
|
|
@ -1,10 +1,42 @@
|
||||||
|
jest.mock('n8n-workflow', () => ({
|
||||||
|
...jest.requireActual('n8n-workflow'),
|
||||||
|
LoggerProxy: { init: jest.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
import type { GlobalConfig } from '@n8n/config';
|
import type { GlobalConfig } from '@n8n/config';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { InstanceSettings } from 'n8n-core';
|
import type { InstanceSettings } from 'n8n-core';
|
||||||
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
|
||||||
import { Logger } from '@/logging/logger.service';
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
describe('Logger', () => {
|
describe('Logger', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
logging: {
|
||||||
|
level: 'info',
|
||||||
|
outputs: ['console'],
|
||||||
|
scopes: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if root, should initialize `LoggerProxy` with instance', () => {
|
||||||
|
const logger = new Logger(globalConfig, mock<InstanceSettings>(), { isRoot: true });
|
||||||
|
|
||||||
|
expect(LoggerProxy.init).toHaveBeenCalledWith(logger);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('if scoped, should not initialize `LoggerProxy`', () => {
|
||||||
|
new Logger(globalConfig, mock<InstanceSettings>(), { isRoot: false });
|
||||||
|
|
||||||
|
expect(LoggerProxy.init).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('transports', () => {
|
describe('transports', () => {
|
||||||
test('if `console` selected, should set console transport', () => {
|
test('if `console` selected, should set console transport', () => {
|
||||||
const globalConfig = mock<GlobalConfig>({
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
|
|
@ -30,6 +30,7 @@ export class Logger {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly globalConfig: GlobalConfig,
|
private readonly globalConfig: GlobalConfig,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
private readonly instanceSettings: InstanceSettings,
|
||||||
|
{ isRoot }: { isRoot?: boolean } = { isRoot: true },
|
||||||
) {
|
) {
|
||||||
this.level = this.globalConfig.logging.level;
|
this.level = this.globalConfig.logging.level;
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ export class Logger {
|
||||||
this.scopes = new Set(scopes);
|
this.scopes = new Set(scopes);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoggerProxy.init(this);
|
if (isRoot) LoggerProxy.init(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setInternalLogger(internalLogger: winston.Logger) {
|
private setInternalLogger(internalLogger: winston.Logger) {
|
||||||
|
@ -61,7 +62,7 @@ export class Logger {
|
||||||
/** Create a logger that injects the given scopes into its log metadata. */
|
/** Create a logger that injects the given scopes into its log metadata. */
|
||||||
scoped(scopes: LogScope | LogScope[]) {
|
scoped(scopes: LogScope | LogScope[]) {
|
||||||
scopes = Array.isArray(scopes) ? scopes : [scopes];
|
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 });
|
const childLogger = this.internalLogger.child({ scopes });
|
||||||
|
|
||||||
scopedLogger.setInternalLogger(childLogger);
|
scopedLogger.setInternalLogger(childLogger);
|
||||||
|
|
|
@ -6,11 +6,14 @@ import { LicenseMetricsService } from '@/metrics/license-metrics.service';
|
||||||
|
|
||||||
describe('LicenseMetricsService', () => {
|
describe('LicenseMetricsService', () => {
|
||||||
const workflowRepository = mock<WorkflowRepository>();
|
const workflowRepository = mock<WorkflowRepository>();
|
||||||
|
const licenseMetricsRespository = mock<LicenseMetricsRepository>();
|
||||||
const licenseMetricsService = new LicenseMetricsService(
|
const licenseMetricsService = new LicenseMetricsService(
|
||||||
mock<LicenseMetricsRepository>(),
|
licenseMetricsRespository,
|
||||||
workflowRepository,
|
workflowRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
describe('collectPassthroughData', () => {
|
describe('collectPassthroughData', () => {
|
||||||
test('should return an object with active workflow IDs', async () => {
|
test('should return an object with active workflow IDs', async () => {
|
||||||
/**
|
/**
|
||||||
|
@ -30,4 +33,36 @@ describe('LicenseMetricsService', () => {
|
||||||
expect(result).toEqual({ activeWorkflowIds });
|
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 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,8 @@ export class LicenseMetricsService {
|
||||||
manualExecutions,
|
manualExecutions,
|
||||||
} = await this.licenseMetricsRepository.getLicenseRenewalMetrics();
|
} = await this.licenseMetricsRepository.getLicenseRenewalMetrics();
|
||||||
|
|
||||||
|
const activeTriggerCount = await this.workflowRepository.getActiveTriggerCount();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ name: 'activeWorkflows', value: activeWorkflows },
|
{ name: 'activeWorkflows', value: activeWorkflows },
|
||||||
{ name: 'totalWorkflows', value: totalWorkflows },
|
{ name: 'totalWorkflows', value: totalWorkflows },
|
||||||
|
@ -29,6 +31,7 @@ export class LicenseMetricsService {
|
||||||
{ name: 'totalCredentials', value: totalCredentials },
|
{ name: 'totalCredentials', value: totalCredentials },
|
||||||
{ name: 'productionExecutions', value: productionExecutions },
|
{ name: 'productionExecutions', value: productionExecutions },
|
||||||
{ name: 'manualExecutions', value: manualExecutions },
|
{ name: 'manualExecutions', value: manualExecutions },
|
||||||
|
{ name: 'activeWorkflowTriggers', value: activeTriggerCount },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,16 @@ describe('Publisher', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('publishCommand', () => {
|
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<PubSub.Command>({ command: 'reload-license' });
|
||||||
|
|
||||||
|
await publisher.publishCommand(msg);
|
||||||
|
|
||||||
|
expect(client.publish).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should publish command into `n8n.commands` pubsub channel', async () => {
|
it('should publish command into `n8n.commands` pubsub channel', async () => {
|
||||||
const publisher = new Publisher(logger, redisClientService, instanceSettings);
|
const publisher = new Publisher(logger, redisClientService, instanceSettings);
|
||||||
const msg = mock<PubSub.Command>({ command: 'reload-license' });
|
const msg = mock<PubSub.Command>({ command: 'reload-license' });
|
||||||
|
|
|
@ -23,7 +23,7 @@ export class Publisher {
|
||||||
private readonly redisClientService: RedisClientService,
|
private readonly redisClientService: RedisClientService,
|
||||||
private readonly instanceSettings: InstanceSettings,
|
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;
|
if (config.getEnv('executions.mode') !== 'queue') return;
|
||||||
|
|
||||||
this.logger = this.logger.scoped(['scaling', 'pubsub']);
|
this.logger = this.logger.scoped(['scaling', 'pubsub']);
|
||||||
|
@ -46,6 +46,9 @@ export class Publisher {
|
||||||
|
|
||||||
/** Publish a command into the `n8n.commands` channel. */
|
/** Publish a command into the `n8n.commands` channel. */
|
||||||
async publishCommand(msg: Omit<PubSub.Command, 'senderId'>) {
|
async publishCommand(msg: Omit<PubSub.Command, 'senderId'>) {
|
||||||
|
// @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(
|
await this.client.publish(
|
||||||
'n8n.commands',
|
'n8n.commands',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository';
|
import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository';
|
||||||
|
@ -54,5 +55,38 @@ describe('ExecutionRepository', () => {
|
||||||
});
|
});
|
||||||
expect(executionData?.data).toEqual('[{"resultData":"1"},{}]');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
MockProviders,
|
MockProviders,
|
||||||
TestFailProvider,
|
TestFailProvider,
|
||||||
} from '../../shared/external-secrets/utils';
|
} from '../../shared/external-secrets/utils';
|
||||||
import { mockInstance } from '../../shared/mocking';
|
import { mockInstance, mockLogger } from '../../shared/mocking';
|
||||||
import { createOwner, createUser } from '../shared/db/users';
|
import { createOwner, createUser } from '../shared/db/users';
|
||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
import { setupTestServer } from '../shared/utils';
|
import { setupTestServer } from '../shared/utils';
|
||||||
|
@ -52,12 +52,14 @@ async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | n
|
||||||
|
|
||||||
const eventService = mock<EventService>();
|
const eventService = mock<EventService>();
|
||||||
|
|
||||||
|
const logger = mockLogger();
|
||||||
|
|
||||||
const resetManager = async () => {
|
const resetManager = async () => {
|
||||||
Container.get(ExternalSecretsManager).shutdown();
|
Container.get(ExternalSecretsManager).shutdown();
|
||||||
Container.set(
|
Container.set(
|
||||||
ExternalSecretsManager,
|
ExternalSecretsManager,
|
||||||
new ExternalSecretsManager(
|
new ExternalSecretsManager(
|
||||||
mock(),
|
logger,
|
||||||
Container.get(SettingsRepository),
|
Container.get(SettingsRepository),
|
||||||
Container.get(License),
|
Container.get(License),
|
||||||
mockProvidersInstance,
|
mockProvidersInstance,
|
||||||
|
@ -108,6 +110,18 @@ beforeAll(async () => {
|
||||||
const member = await createUser();
|
const member = await createUser();
|
||||||
authMemberAgent = testServer.authAgentFor(member);
|
authMemberAgent = testServer.authAgentFor(member);
|
||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
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 () => {
|
beforeEach(async () => {
|
||||||
|
|
|
@ -116,7 +116,7 @@ describe('POST /license/renew', () => {
|
||||||
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {
|
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {
|
||||||
data: {
|
data: {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
value: 0,
|
value: 0,
|
||||||
limit: -1,
|
limit: -1,
|
||||||
warningThreshold: 0.8,
|
warningThreshold: 0.8,
|
||||||
|
@ -132,7 +132,7 @@ const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {
|
||||||
const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = {
|
const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = {
|
||||||
data: {
|
data: {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
value: 0,
|
value: 0,
|
||||||
limit: -1,
|
limit: -1,
|
||||||
warningThreshold: 0.8,
|
warningThreshold: 0.8,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-core",
|
"name": "n8n-core",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "Core functionality of n8n",
|
"description": "Core functionality of n8n",
|
||||||
"main": "dist/index",
|
"main": "dist/index",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-design-system",
|
"name": "n8n-design-system",
|
||||||
"version": "1.54.0",
|
"version": "1.55.0",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"import": "src/main.ts",
|
"import": "src/main.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import { configure } from '@testing-library/vue';
|
||||||
import { config } from '@vue/test-utils';
|
import { config } from '@vue/test-utils';
|
||||||
|
|
||||||
import { N8nPlugin } from 'n8n-design-system/plugin';
|
import { N8nPlugin } from 'n8n-design-system/plugin';
|
||||||
|
|
||||||
|
configure({ testIdAttribute: 'data-test-id' });
|
||||||
|
|
||||||
config.global.plugins = [N8nPlugin];
|
config.global.plugins = [N8nPlugin];
|
||||||
|
|
||||||
window.ResizeObserver =
|
window.ResizeObserver =
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1296,7 +1296,7 @@ export type UsageState = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
data: {
|
data: {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
limit: number; // -1 for unlimited, from license
|
limit: number; // -1 for unlimited, from license
|
||||||
value: number;
|
value: number;
|
||||||
warningThreshold: number; // hardcoded value in BE
|
warningThreshold: number; // hardcoded value in BE
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
||||||
import { EditorSelection, EditorState, type SelectionRange } from '@codemirror/state';
|
|
||||||
import { type Completion, CompletionContext } from '@codemirror/autocomplete';
|
|
||||||
import { datatypeCompletions } from '@/plugins/codemirror/completions/datatype.completions';
|
|
||||||
import { watchDebounced } from '@vueuse/core';
|
|
||||||
import { FIELDS_SECTION } from '@/plugins/codemirror/completions/constants';
|
import { FIELDS_SECTION } from '@/plugins/codemirror/completions/constants';
|
||||||
|
import { datatypeCompletions } from '@/plugins/codemirror/completions/datatype.completions';
|
||||||
import { isCompletionSection } from '@/plugins/codemirror/completions/utils';
|
import { isCompletionSection } from '@/plugins/codemirror/completions/utils';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { type Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||||
|
import { EditorSelection, EditorState, type SelectionRange } from '@codemirror/state';
|
||||||
|
import { watchDebounced } from '@vueuse/core';
|
||||||
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
|
|
||||||
type TipId = 'executePrevious' | 'drag' | 'default' | 'dotObject' | 'dotPrimitive';
|
type TipId = 'executePrevious' | 'drag' | 'default' | 'dotObject' | 'dotPrimitive';
|
||||||
|
|
||||||
|
@ -36,7 +36,11 @@ const canDragToFocusedInput = computed(
|
||||||
const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0);
|
const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0);
|
||||||
|
|
||||||
const tip = computed<TipId>(() => {
|
const tip = computed<TipId>(() => {
|
||||||
if (!ndvStore.hasInputData && ndvStore.isInputParentOfActiveNode) {
|
if (
|
||||||
|
!ndvStore.hasInputData &&
|
||||||
|
ndvStore.isInputParentOfActiveNode &&
|
||||||
|
ndvStore.focusedMappableInput
|
||||||
|
) {
|
||||||
return 'executePrevious';
|
return 'executePrevious';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -539,7 +539,8 @@ const showDragnDropTip = computed(
|
||||||
!isDropDisabled.value &&
|
!isDropDisabled.value &&
|
||||||
(!ndvStore.hasInputData || !isInputDataEmpty.value) &&
|
(!ndvStore.hasInputData || !isInputDataEmpty.value) &&
|
||||||
!ndvStore.isMappingOnboarded &&
|
!ndvStore.isMappingOnboarded &&
|
||||||
ndvStore.isInputParentOfActiveNode,
|
ndvStore.isInputParentOfActiveNode &&
|
||||||
|
!props.isForCredential,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldCaptureForPosthog = computed(() => {
|
const shouldCaptureForPosthog = computed(() => {
|
||||||
|
|
|
@ -220,4 +220,20 @@ describe('Canvas', () => {
|
||||||
expect(container.querySelector('#diagonalHatch')).toBeInTheDocument();
|
expect(container.querySelector('#diagonalHatch')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pane', () => {
|
||||||
|
describe('onPaneMouseDown', () => {
|
||||||
|
it('should enable panning when middle mouse button is pressed', async () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
const canvas = getByTestId('canvas');
|
||||||
|
const pane = canvas.querySelector('.vue-flow__pane');
|
||||||
|
|
||||||
|
if (!pane) throw new Error('VueFlow pane not in the document');
|
||||||
|
|
||||||
|
await fireEvent.mouseDown(canvas, { button: 1, view: window });
|
||||||
|
|
||||||
|
expect(canvas).toHaveClass('draggable');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,7 @@ import { onKeyDown, onKeyUp, useDebounceFn } from '@vueuse/core';
|
||||||
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
|
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
|
||||||
|
import { isMiddleMouseButton } from '@/utils/eventUtils';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
@ -107,6 +108,7 @@ const props = withDefaults(
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
vueFlowRef,
|
||||||
getSelectedNodes: selectedNodes,
|
getSelectedNodes: selectedNodes,
|
||||||
addSelectedNodes,
|
addSelectedNodes,
|
||||||
removeSelectedNodes,
|
removeSelectedNodes,
|
||||||
|
@ -143,18 +145,16 @@ const disableKeyBindings = computed(() => !props.keyBindings);
|
||||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isPanningEnabled = ref(false);
|
|
||||||
const panningKeyCode = ' ';
|
const panningKeyCode = ' ';
|
||||||
|
const isPanningEnabled = ref(false);
|
||||||
const selectionKeyCode = ref<true | null>(true);
|
const selectionKeyCode = ref<true | null>(true);
|
||||||
|
|
||||||
onKeyDown(panningKeyCode, () => {
|
onKeyDown(panningKeyCode, () => {
|
||||||
isPanningEnabled.value = true;
|
setPanningEnabled(true);
|
||||||
selectionKeyCode.value = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onKeyUp(panningKeyCode, () => {
|
onKeyUp(panningKeyCode, () => {
|
||||||
isPanningEnabled.value = false;
|
setPanningEnabled(false);
|
||||||
selectionKeyCode.value = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyMap = computed(() => ({
|
const keyMap = computed(() => ({
|
||||||
|
@ -186,6 +186,16 @@ const keyMap = computed(() => ({
|
||||||
|
|
||||||
useKeybindings(keyMap, { disabled: disableKeyBindings });
|
useKeybindings(keyMap, { disabled: disableKeyBindings });
|
||||||
|
|
||||||
|
function setPanningEnabled(value: boolean) {
|
||||||
|
if (value) {
|
||||||
|
isPanningEnabled.value = true;
|
||||||
|
selectionKeyCode.value = null;
|
||||||
|
} else {
|
||||||
|
isPanningEnabled.value = false;
|
||||||
|
selectionKeyCode.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the window is focused, the selection key code is lost.
|
* When the window is focused, the selection key code is lost.
|
||||||
* We trigger a value refresh to ensure that the selection key code is set correctly again.
|
* We trigger a value refresh to ensure that the selection key code is set correctly again.
|
||||||
|
@ -384,12 +394,28 @@ function setReadonly(value: boolean) {
|
||||||
elementsSelectable.value = true;
|
elementsSelectable.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPaneMouseDown(event: MouseEvent) {
|
||||||
|
if (isMiddleMouseButton(event)) {
|
||||||
|
setPanningEnabled(true);
|
||||||
|
|
||||||
|
// Re-emit the event to start panning after setting the panning state to true
|
||||||
|
// This workaround is necessary because the Vue Flow library does not provide a way to
|
||||||
|
// start panning programmatically
|
||||||
|
void nextTick(() =>
|
||||||
|
vueFlowRef.value
|
||||||
|
?.querySelector('.vue-flow__pane')
|
||||||
|
?.dispatchEvent(new MouseEvent('mousedown', event)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onPaneMoveStart() {
|
function onPaneMoveStart() {
|
||||||
isPaneMoving.value = true;
|
isPaneMoving.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaneMoveEnd() {
|
function onPaneMoveEnd() {
|
||||||
isPaneMoving.value = false;
|
isPaneMoving.value = false;
|
||||||
|
setPanningEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -559,6 +585,7 @@ provide(CanvasKey, {
|
||||||
@nodes-change="onNodesChange"
|
@nodes-change="onNodesChange"
|
||||||
@move-start="onPaneMoveStart"
|
@move-start="onPaneMoveStart"
|
||||||
@move-end="onPaneMoveEnd"
|
@move-end="onPaneMoveEnd"
|
||||||
|
@mousedown="onPaneMouseDown"
|
||||||
>
|
>
|
||||||
<template #node-canvas-node="canvasNodeProps">
|
<template #node-canvas-node="canvasNodeProps">
|
||||||
<Node
|
<Node
|
||||||
|
@ -647,6 +674,10 @@ provide(CanvasKey, {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.vue-flow__pane) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
:global(.vue-flow__pane.dragging) {
|
:global(.vue-flow__pane.dragging) {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,15 +74,29 @@ const edgeStyle = computed(() => ({
|
||||||
stroke: isHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
stroke: isHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeLabelStyle = computed(() => ({ color: edgeColor.value }));
|
const edgeClasses = computed(() => ({
|
||||||
|
[$style.edge]: true,
|
||||||
|
hovered: isHovered.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const edgeLabelStyle = computed(() => ({
|
||||||
|
color: edgeColor.value,
|
||||||
|
}));
|
||||||
|
|
||||||
const edgeToolbarStyle = computed(() => {
|
const edgeToolbarStyle = computed(() => {
|
||||||
const [, labelX, labelY] = path.value;
|
const [, labelX, labelY] = path.value;
|
||||||
return {
|
return {
|
||||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
...(isHovered.value ? { zIndex: 1 } : {}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const edgeToolbarClasses = computed(() => ({
|
||||||
|
[$style.edgeLabelWrapper]: true,
|
||||||
|
'vue-flow__edge-label': true,
|
||||||
|
selected: props.selected,
|
||||||
|
}));
|
||||||
|
|
||||||
const path = computed(() =>
|
const path = computed(() =>
|
||||||
getCustomPath(props, {
|
getCustomPath(props, {
|
||||||
connectionType: connectionType.value,
|
connectionType: connectionType.value,
|
||||||
|
@ -108,7 +122,7 @@ function onDelete() {
|
||||||
<template>
|
<template>
|
||||||
<BaseEdge
|
<BaseEdge
|
||||||
:id="id"
|
:id="id"
|
||||||
:class="$style.edge"
|
:class="edgeClasses"
|
||||||
:style="edgeStyle"
|
:style="edgeStyle"
|
||||||
:path="path[0]"
|
:path="path[0]"
|
||||||
:marker-end="markerEnd"
|
:marker-end="markerEnd"
|
||||||
|
@ -122,7 +136,7 @@ function onDelete() {
|
||||||
:data-target-node-name="targetNode?.label"
|
:data-target-node-name="targetNode?.label"
|
||||||
:data-edge-status="status"
|
:data-edge-status="status"
|
||||||
:style="edgeToolbarStyle"
|
:style="edgeToolbarStyle"
|
||||||
:class="$style.edgeLabelWrapper"
|
:class="edgeToolbarClasses"
|
||||||
@mouseenter="isHovered = true"
|
@mouseenter="isHovered = true"
|
||||||
@mouseleave="isHovered = false"
|
@mouseleave="isHovered = false"
|
||||||
>
|
>
|
||||||
|
|
|
@ -126,7 +126,7 @@ function onOpenContextMenu(event: MouseEvent) {
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.canvasNodeToolbar {
|
.canvasNodeToolbar {
|
||||||
padding-bottom: var(--spacing-2xs);
|
padding-bottom: var(--spacing-xs);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -137,6 +137,7 @@ function onOpenContextMenu(event: MouseEvent) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--color-canvas-background);
|
background-color: var(--color-canvas-background);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
|
||||||
:global(.button) {
|
:global(.button) {
|
||||||
--button-font-color: var(--color-text-light);
|
--button-font-color: var(--color-text-light);
|
||||||
|
|
|
@ -21,7 +21,7 @@ describe('Usage and plan store', () => {
|
||||||
const store = useUsageStore();
|
const store = useUsageStore();
|
||||||
store.setData({
|
store.setData({
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
limit,
|
limit,
|
||||||
value,
|
value,
|
||||||
warningThreshold,
|
warningThreshold,
|
||||||
|
|
|
@ -18,7 +18,7 @@ const DEFAULT_STATE: UsageState = {
|
||||||
loading: true,
|
loading: true,
|
||||||
data: {
|
data: {
|
||||||
usage: {
|
usage: {
|
||||||
executions: {
|
activeWorkflowTriggers: {
|
||||||
limit: -1,
|
limit: -1,
|
||||||
value: 0,
|
value: 0,
|
||||||
warningThreshold: 0.8,
|
warningThreshold: 0.8,
|
||||||
|
@ -39,9 +39,11 @@ export const useUsageStore = defineStore('usage', () => {
|
||||||
|
|
||||||
const planName = computed(() => state.data.license.planName || DEFAULT_PLAN_NAME);
|
const planName = computed(() => state.data.license.planName || DEFAULT_PLAN_NAME);
|
||||||
const planId = computed(() => state.data.license.planId);
|
const planId = computed(() => state.data.license.planId);
|
||||||
const executionLimit = computed(() => state.data.usage.executions.limit);
|
const activeWorkflowTriggersLimit = computed(() => state.data.usage.activeWorkflowTriggers.limit);
|
||||||
const executionCount = computed(() => state.data.usage.executions.value);
|
const activeWorkflowTriggersCount = computed(() => state.data.usage.activeWorkflowTriggers.value);
|
||||||
const executionPercentage = computed(() => (executionCount.value / executionLimit.value) * 100);
|
const executionPercentage = computed(
|
||||||
|
() => (activeWorkflowTriggersCount.value / activeWorkflowTriggersLimit.value) * 100,
|
||||||
|
);
|
||||||
const instanceId = computed(() => settingsStore.settings.instanceId);
|
const instanceId = computed(() => settingsStore.settings.instanceId);
|
||||||
const managementToken = computed(() => state.data.managementToken);
|
const managementToken = computed(() => state.data.managementToken);
|
||||||
const appVersion = computed(() => settingsStore.settings.versionCli);
|
const appVersion = computed(() => settingsStore.settings.versionCli);
|
||||||
|
@ -99,17 +101,17 @@ export const useUsageStore = defineStore('usage', () => {
|
||||||
registerCommunityEdition,
|
registerCommunityEdition,
|
||||||
planName,
|
planName,
|
||||||
planId,
|
planId,
|
||||||
executionLimit,
|
activeWorkflowTriggersLimit,
|
||||||
executionCount,
|
activeWorkflowTriggersCount,
|
||||||
executionPercentage,
|
executionPercentage,
|
||||||
instanceId,
|
instanceId,
|
||||||
managementToken,
|
managementToken,
|
||||||
appVersion,
|
appVersion,
|
||||||
isCloseToLimit: computed(() =>
|
isCloseToLimit: computed(() =>
|
||||||
state.data.usage.executions.limit < 0
|
state.data.usage.activeWorkflowTriggers.limit < 0
|
||||||
? false
|
? false
|
||||||
: executionCount.value / executionLimit.value >=
|
: activeWorkflowTriggersCount.value / activeWorkflowTriggersLimit.value >=
|
||||||
state.data.usage.executions.warningThreshold,
|
state.data.usage.activeWorkflowTriggers.warningThreshold,
|
||||||
),
|
),
|
||||||
viewPlansUrl: computed(
|
viewPlansUrl: computed(
|
||||||
() => `${subscriptionAppUrl.value}?${commonSubscriptionAppUrlQueryParams.value}`,
|
() => `${subscriptionAppUrl.value}?${commonSubscriptionAppUrlQueryParams.value}`,
|
||||||
|
@ -123,8 +125,8 @@ export const useUsageStore = defineStore('usage', () => {
|
||||||
instance_id: instanceId.value,
|
instance_id: instanceId.value,
|
||||||
action: 'view_plans',
|
action: 'view_plans',
|
||||||
plan_name_current: planName.value,
|
plan_name_current: planName.value,
|
||||||
usage: executionCount.value,
|
usage: activeWorkflowTriggersCount.value,
|
||||||
quota: executionLimit.value,
|
quota: activeWorkflowTriggersLimit.value,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,13 @@ import {
|
||||||
WAIT_NODE_TYPE,
|
WAIT_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from '@/Interface';
|
import type {
|
||||||
|
IExecutionResponse,
|
||||||
|
IExecutionsCurrentSummaryExtended,
|
||||||
|
INodeUi,
|
||||||
|
IWorkflowDb,
|
||||||
|
IWorkflowSettings,
|
||||||
|
} from '@/Interface';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -594,6 +600,50 @@ describe('useWorkflowsStore', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('finishActiveExecution', () => {
|
||||||
|
it('should update execution', async () => {
|
||||||
|
const cursor = 1;
|
||||||
|
const ids = ['0', '1', '2'];
|
||||||
|
workflowsStore.setActiveExecutions(
|
||||||
|
ids.map((id) => ({ id })) as IExecutionsCurrentSummaryExtended[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const stoppedAt = new Date();
|
||||||
|
|
||||||
|
workflowsStore.finishActiveExecution({
|
||||||
|
executionId: ids[cursor],
|
||||||
|
data: {
|
||||||
|
finished: true,
|
||||||
|
stoppedAt,
|
||||||
|
},
|
||||||
|
} as PushPayload<'executionFinished'>);
|
||||||
|
|
||||||
|
expect(workflowsStore.activeExecutions[cursor]).toStrictEqual({
|
||||||
|
id: ids[cursor],
|
||||||
|
finished: true,
|
||||||
|
stoppedAt,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle parameter casting', async () => {
|
||||||
|
const cursor = 1;
|
||||||
|
const ids = ['0', '1', '2'];
|
||||||
|
workflowsStore.setActiveExecutions(
|
||||||
|
ids.map((id) => ({ id })) as IExecutionsCurrentSummaryExtended[],
|
||||||
|
);
|
||||||
|
|
||||||
|
workflowsStore.finishActiveExecution({
|
||||||
|
executionId: ids[cursor],
|
||||||
|
} as PushPayload<'executionFinished'>);
|
||||||
|
|
||||||
|
expect(workflowsStore.activeExecutions[cursor]).toStrictEqual({
|
||||||
|
id: ids[cursor],
|
||||||
|
finished: undefined,
|
||||||
|
stoppedAt: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function getMockEditFieldsNode() {
|
function getMockEditFieldsNode() {
|
||||||
|
|
|
@ -47,7 +47,6 @@ import type {
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
IPinData,
|
IPinData,
|
||||||
IRun,
|
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
@ -1344,23 +1343,16 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeExecution = activeExecutions.value[activeExecutionIndex];
|
Object.assign(activeExecutions.value[activeExecutionIndex], {
|
||||||
|
...(finishedActiveExecution.executionId !== undefined
|
||||||
|
? { id: finishedActiveExecution.executionId }
|
||||||
|
: {}),
|
||||||
|
finished: finishedActiveExecution.data?.finished,
|
||||||
|
stoppedAt: finishedActiveExecution.data?.stoppedAt,
|
||||||
|
});
|
||||||
|
|
||||||
activeExecutions.value = [
|
if (finishedActiveExecution.data?.data) {
|
||||||
...activeExecutions.value.slice(0, activeExecutionIndex),
|
setWorkflowExecutionRunData(finishedActiveExecution.data.data);
|
||||||
{
|
|
||||||
...activeExecution,
|
|
||||||
...(finishedActiveExecution.executionId !== undefined
|
|
||||||
? { id: finishedActiveExecution.executionId }
|
|
||||||
: {}),
|
|
||||||
finished: finishedActiveExecution.data.finished,
|
|
||||||
stoppedAt: finishedActiveExecution.data.stoppedAt,
|
|
||||||
},
|
|
||||||
...activeExecutions.value.slice(activeExecutionIndex + 1),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (finishedActiveExecution.data && (finishedActiveExecution.data as IRun).data) {
|
|
||||||
setWorkflowExecutionRunData((finishedActiveExecution.data as IRun).data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,17 +58,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pane
|
|
||||||
*/
|
|
||||||
|
|
||||||
.vue-flow__pane {
|
|
||||||
&,
|
|
||||||
&.draggable {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
|
@ -83,10 +72,6 @@
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
z-index: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:has(.sticky--active) {
|
&:has(.sticky--active) {
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
@ -107,7 +92,8 @@
|
||||||
* Edges
|
* Edges
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.vue-flow__edges,
|
.vue-flow__edges:has(.selected),
|
||||||
.vue-flow__edge-labels {
|
.vue-flow__edges:has(.hovered),
|
||||||
|
.vue-flow__edge-label.selected {
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
3
packages/editor-ui/src/utils/eventUtils.ts
Normal file
3
packages/editor-ui/src/utils/eventUtils.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function isMiddleMouseButton(event: MouseEvent) {
|
||||||
|
return event.which === 2 || event.button === 1;
|
||||||
|
}
|
|
@ -198,7 +198,7 @@ const openCommunityRegisterModal = () => {
|
||||||
{{ locale.baseText('settings.usageAndPlan.activeWorkflows') }}
|
{{ locale.baseText('settings.usageAndPlan.activeWorkflows') }}
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
<div :class="$style.chart">
|
<div :class="$style.chart">
|
||||||
<span v-if="usageStore.executionLimit > 0" :class="$style.chartLine">
|
<span v-if="usageStore.activeWorkflowTriggersLimit > 0" :class="$style.chartLine">
|
||||||
<span
|
<span
|
||||||
:class="$style.chartBar"
|
:class="$style.chartBar"
|
||||||
:style="{ width: `${usageStore.executionPercentage}%` }"
|
:style="{ width: `${usageStore.executionPercentage}%` }"
|
||||||
|
@ -209,12 +209,12 @@ const openCommunityRegisterModal = () => {
|
||||||
:class="$style.count"
|
:class="$style.count"
|
||||||
keypath="settings.usageAndPlan.activeWorkflows.count"
|
keypath="settings.usageAndPlan.activeWorkflows.count"
|
||||||
>
|
>
|
||||||
<template #count>{{ usageStore.executionCount }}</template>
|
<template #count>{{ usageStore.activeWorkflowTriggersCount }}</template>
|
||||||
<template #limit>
|
<template #limit>
|
||||||
<span v-if="usageStore.executionLimit < 0">{{
|
<span v-if="usageStore.activeWorkflowTriggersLimit < 0">{{
|
||||||
locale.baseText('settings.usageAndPlan.activeWorkflows.unlimited')
|
locale.baseText('settings.usageAndPlan.activeWorkflows.unlimited')
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-else>{{ usageStore.executionLimit }}</span>
|
<span v-else>{{ usageStore.activeWorkflowTriggersLimit }}</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-node-dev",
|
"name": "n8n-node-dev",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "CLI to simplify n8n credentials/node development",
|
"description": "CLI to simplify n8n credentials/node development",
|
||||||
"main": "dist/src/index",
|
"main": "dist/src/index",
|
||||||
"types": "dist/src/index.d.ts",
|
"types": "dist/src/index.d.ts",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-base",
|
"name": "n8n-nodes-base",
|
||||||
"version": "1.64.0",
|
"version": "1.65.0",
|
||||||
"description": "Base nodes of n8n",
|
"description": "Base nodes of n8n",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-workflow",
|
"name": "n8n-workflow",
|
||||||
"version": "1.63.0",
|
"version": "1.64.0",
|
||||||
"description": "Workflow base code of n8n",
|
"description": "Workflow base code of n8n",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class FilterError extends ApplicationError {
|
||||||
message: string,
|
message: string,
|
||||||
readonly description: string,
|
readonly description: string,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message, { level: 'warning' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue