Merge branch 'master' into seatable_node_rework2

This commit is contained in:
Christoph Dyllick-Brenzinger 2024-06-27 18:29:18 +02:00 committed by GitHub
commit 9ae72e8e8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 1944 additions and 601 deletions

26
.github/workflows/notify-pr-status.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Notify PR status changed
on:
pull_request_review:
types: [submitted, dismissed]
pull_request:
types: [closed]
jobs:
notify:
runs-on: ubuntu-latest
if: >-
(github.event_name == 'pull_request_review' && github.event.review.state == 'approved') ||
(github.event_name == 'pull_request_review' && github.event.review.state == 'dismissed') ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == false && github.event.action == 'closed')
steps:
- uses: fjogeleit/http-request-action@dea46570591713c7de04a5b556bf2ff7bdf0aa9c # v1
name: Notify
env:
PR_URL: ${{ github.event.pull_request.html_url }}
with:
url: ${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_URL }}
method: 'POST'
customHeaders: '{ "x-api-token": "${{ secrets.N8N_NOTIFY_PR_STATUS_CHANGED_TOKEN }}" }'
data: '{ "event_name": "${{ github.event_name }}", "pr_url": "${{ env.PR_URL }}", "event": ${{ toJSON(github.event) }} }'

View file

@ -1,3 +1,28 @@
# [1.48.0](https://github.com/n8n-io/n8n/compare/n8n@1.47.0...n8n@1.48.0) (2024-06-27)
### Bug Fixes
* **core:** Fix init for `AuditEventRelay` ([#9839](https://github.com/n8n-io/n8n/issues/9839)) ([16d3083](https://github.com/n8n-io/n8n/commit/16d3083af7465d0788f25d843e497b4c7d69de92))
* **core:** Fix telemetry for concurrency control ([#9845](https://github.com/n8n-io/n8n/issues/9845)) ([e25682d](https://github.com/n8n-io/n8n/commit/e25682ddad6ee961a1afe5365d7bbad871a20a4c))
* **editor:** Fix initialize authenticated features ([#9867](https://github.com/n8n-io/n8n/issues/9867)) ([4de58dc](https://github.com/n8n-io/n8n/commit/4de58dcbf5f29bf5414414aa4703356f69a29356))
* **editor:** Load credentials for workflow before determining credentials errors ([#9876](https://github.com/n8n-io/n8n/issues/9876)) ([4008c14](https://github.com/n8n-io/n8n/commit/4008c147d76daa6ff6d43f30c9a18bf1cef7e5d5))
* **editor:** Optimizing main sidebar to have more space for Projects ([#9686](https://github.com/n8n-io/n8n/issues/9686)) ([5cdcb61](https://github.com/n8n-io/n8n/commit/5cdcb61f668a6e00829bee25f40cc869376a9cd7))
* **editor:** Properly update workflow info in main header ([#9789](https://github.com/n8n-io/n8n/issues/9789)) ([1ba656e](https://github.com/n8n-io/n8n/commit/1ba656ef4aae97c78162114ad8de533b275db280))
* **editor:** Show error state correctly in options parameter with remote options ([#9836](https://github.com/n8n-io/n8n/issues/9836)) ([5bc58ef](https://github.com/n8n-io/n8n/commit/5bc58efde9c127eef8082b23cf5d8dcd91162cf4))
* **editor:** Use pinned data to resolve expressions in unexecuted nodes ([#9693](https://github.com/n8n-io/n8n/issues/9693)) ([6cb3072](https://github.com/n8n-io/n8n/commit/6cb3072a5db366404f3d16323498371d28582c06))
* Fix missing node logos ([#9844](https://github.com/n8n-io/n8n/issues/9844)) ([1eeaf32](https://github.com/n8n-io/n8n/commit/1eeaf32523c30f000a1bb8f362c478a086ca7928))
* **Zulip Node:** Fix a typo preventing some messages from updating ([#7078](https://github.com/n8n-io/n8n/issues/7078)) ([553b135](https://github.com/n8n-io/n8n/commit/553b135b0b73fa29062d2b6ef28f98c47bcd186b))
### Features
* Add RS client to hooks service ([#9834](https://github.com/n8n-io/n8n/issues/9834)) ([b807e67](https://github.com/n8n-io/n8n/commit/b807e6726f6ac86df9078c25275b6360a4fcee42))
* **Anthropic Chat Model Node:** Add support for Claude 3.5 Sonnet ([#9832](https://github.com/n8n-io/n8n/issues/9832)) ([2ce97be](https://github.com/n8n-io/n8n/commit/2ce97be33e8aa4f3023d486441ccc4860a0e07ca))
* **editor:** Show multiple nodes in input pane schema view ([#9816](https://github.com/n8n-io/n8n/issues/9816)) ([e51de9d](https://github.com/n8n-io/n8n/commit/e51de9d3916e3fcaa05e92dfb8b9b5c722bff33c))
# [1.47.0](https://github.com/n8n-io/n8n/compare/n8n@1.46.0...n8n@1.47.0) (2024-06-20) # [1.47.0](https://github.com/n8n-io/n8n/compare/n8n@1.46.0...n8n@1.47.0) (2024-06-20)

View file

@ -42,7 +42,6 @@ n8n is split up in different modules which are all in a single mono repository.
The most important directories: The most important directories:
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers - [/docker/image](/docker/images) - Dockerfiles to create n8n containers
- [/docker/compose](/docker/compose) - Examples Docker Setups
- [/packages](/packages) - The different n8n modules - [/packages](/packages) - The different n8n modules
- [/packages/cli](/packages/cli) - CLI code to run front- & backend - [/packages/cli](/packages/cli) - CLI code to run front- & backend
- [/packages/core](/packages/core) - Core code which handles workflow - [/packages/core](/packages/core) - Core code which handles workflow

View file

@ -0,0 +1,135 @@
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2111 expressions should support pinned data', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
it('supports pinned data in expressions unexecuted and executed parent nodes', () => {
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
// test previous node unexecuted
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
// test can resolve correctly based on item
ndv.actions.switchInputMode('Table');
ndv.getters.inputTableRow(2).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
// test previous node executed
ndv.actions.execute();
ndv.getters.inputTableRow(1).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
ndv.getters.inputTableRow(2).realHover();
cy.wait(50);
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
// check it resolved correctly on the backend
ndv.getters
.outputTbodyCell(1, 0)
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
ndv.getters
.outputTbodyCell(2, 0)
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
ndv.getters
.outputTbodyCell(1, 1)
.should('contain.text', '0,0\\nJoe\\n\\nJoe\\n\\nJoe\\n\\nJoe\\nJoe');
ndv.getters
.outputTbodyCell(2, 1)
.should('contain.text', '0,1\\nJoan\\n\\nJoan\\n\\nJoan\\n\\nJoan\\nJoan');
});
it('resets expressions after node is unpinned', () => {
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
// test previous node unexecuted
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters
.parameterExpressionPreview('value')
.eq(0)
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
ndv.getters
.parameterExpressionPreview('value')
.eq(1)
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
ndv.actions.close();
// unpin pinned node
workflowPage.getters
.canvasNodeByName('PinnedSet')
.eq(0)
.find('.node-pin-data-icon')
.should('exist');
workflowPage.getters.canvasNodeByName('PinnedSet').eq(0).click();
workflowPage.actions.hitPinNodeShortcut();
workflowPage.getters
.canvasNodeByName('PinnedSet')
.eq(0)
.find('.node-pin-data-icon')
.should('not.exist');
workflowPage.actions.openNode('NotPinnedWithExpressions');
ndv.getters.nodeParameters().find('parameter-expression-preview-value').should('not.exist');
ndv.getters.parameterInput('value').eq(0).click();
ndv.getters
.inlineExpressionEditorOutput()
.should(
'have.text',
'[Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute previous nodes for preview][Execute previous nodes for preview][undefined]',
);
// close open expression
ndv.getters.inputLabel().eq(0).click();
ndv.getters.parameterInput('value').eq(1).click();
ndv.getters
.inlineExpressionEditorOutput()
.should(
'have.text',
'0,0[Execute node PinnedSet for preview][Execute node PinnedSet for preview][Execute previous nodes for preview][Execute previous nodes for preview][Execute previous nodes for preview]',
);
});
});

View file

@ -329,7 +329,15 @@ describe('Projects', { disableAutoLogin: true }, () => {
// Go to the first project and create a workflow // Go to the first project and create a workflow
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.workflowCards().should('not.have.length');
cy.intercept('GET', '/rest/credentials/for-workflow*').as('getCredentialsForWorkflow');
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
cy.wait('@getCredentialsForWorkflow').then((interception) => {
expect(interception.request.query).to.have.property('projectId');
expect(interception.request.query).not.to.have.property('workflowId');
});
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
@ -342,6 +350,10 @@ describe('Projects', { disableAutoLogin: true }, () => {
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
cy.wait('@getCredentialsForWorkflow').then((interception) => {
expect(interception.request.query).not.to.have.property('projectId');
expect(interception.request.query).to.have.property('workflowId');
});
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick(); workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect() getVisibleSelect()

View file

@ -0,0 +1,112 @@
{
"meta": {
"instanceId": "5bd32b91ed2a88e542012920460f736c3687a32fbb953718f6952d182231c0ff"
},
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a482f1fd-4815-4da4-a733-7beafb43c500",
"name": "static",
"value": "={{ $('PinnedSet').first().json.firstName }}\n{{ $('PinnedSet').itemMatching(0).json.firstName }}\n{{ $('PinnedSet').itemMatching(1).json.firstName }}\n{{ $('PinnedSet').last().json.firstName }}\n{{ $('PinnedSet').all()[0].json.firstName }}\n{{ $('PinnedSet').all()[1].json.firstName }}\n\n{{ $input.first().json.firstName }}\n{{ $input.last().json.firstName }}\n\n{{ $items()[0].json.firstName }}",
"type": "string"
},
{
"id": "2c973f2a-7ca0-41bc-903c-7174bee251b0",
"name": "variable",
"value": "={{ $runIndex }},{{ $itemIndex }}\n{{ $node['PinnedSet'].json.firstName }}\n\n{{ $('PinnedSet').item.json.firstName }}\n\n{{ $input.item.json.firstName }}\n\n{{ $json.firstName }}\n{{ $data.firstName }}",
"type": "string"
}
]
},
"options": {}
},
"id": "ac55ee16-4598-48bf-ace3-a48fed1d4ff3",
"name": "NotPinnedWithExpressions",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1600,
640
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3058c300-b377-41b7-9c90-a01372f9b581",
"name": "firstName",
"value": "Joe",
"type": "string"
},
{
"id": "bb871662-c23c-4234-ac0c-b78c279bbf34",
"name": "lastName",
"value": "Smith",
"type": "string"
}
]
},
"options": {}
},
"id": "300a3888-cc2f-4e61-8578-b0adbcf33450",
"name": "PinnedSet",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1340,
640
]
},
{
"parameters": {},
"id": "426ff39a-3408-48b4-899f-60db732675f8",
"name": "Start",
"type": "n8n-nodes-base.manualTrigger",
"position": [
1100,
640
],
"typeVersion": 1
}
],
"connections": {
"PinnedSet": {
"main": [
[
{
"node": "NotPinnedWithExpressions",
"type": "main",
"index": 0
}
]
]
},
"Start": {
"main": [
[
{
"node": "PinnedSet",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"PinnedSet": [
{
"firstName": "Joe",
"lastName": "Smith"
},
{
"firstName": "Joan",
"lastName": "Summers"
}
]
}
}

View file

@ -27,6 +27,7 @@ export class NDV extends BasePage {
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'), nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
savePinnedDataButton: () => savePinnedDataButton: () =>
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'), this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
inputLabel: () => cy.getByTestId('input-label'),
outputTableRows: () => this.getters.outputDataContainer().find('table tr'), outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'), outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'),
outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text), outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text),

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-monorepo", "name": "n8n-monorepo",
"version": "1.47.0", "version": "1.48.0",
"private": true, "private": true,
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"engines": { "engines": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/chat", "name": "@n8n/chat",
"version": "0.18.0", "version": "0.19.0",
"scripts": { "scripts": {
"dev": "pnpm run storybook", "dev": "pnpm run storybook",
"build": "pnpm build:vite && pnpm run build:individual && npm run build:prepare", "build": "pnpm build:vite && pnpm run build:individual && npm run build:prepare",

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/client-oauth2", "name": "@n8n/client-oauth2",
"version": "0.17.0", "version": "0.18.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/n8n-io/n8n.git" "url": "git+https://github.com/n8n-io/n8n.git"

View file

@ -120,6 +120,7 @@ export class Code implements INodeType {
displayName: 'LangChain Code', displayName: 'LangChain Code',
name: 'code', name: 'code',
icon: 'fa:code', icon: 'fa:code',
iconColor: 'black',
group: ['transform'], group: ['transform'],
version: 1, version: 1,
description: 'LangChain Code Node', description: 'LangChain Code Node',

View file

@ -0,0 +1,113 @@
import type { IExecuteFunctions, INodeType, INodeTypeDescription, SupplyData } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { VectorStoreQATool } from 'langchain/tools';
import type { VectorStore } from '@langchain/core/vectorstores';
import type { BaseLanguageModel } from '@langchain/core/language_models/base';
import { VectorDBQAChain } from 'langchain/chains';
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
import { logWrapper } from '../../../utils/logWrapper';
export class ToolVectorStore implements INodeType {
description: INodeTypeDescription = {
displayName: 'Vector Store Tool',
name: 'toolVectorStore',
icon: 'fa:database',
group: ['transform'],
version: [1],
description: 'Retrieve context from vector store',
defaults: {
name: 'Vector Store Tool',
},
codex: {
categories: ['AI'],
subcategories: {
AI: ['Tools'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolvectorstore/',
},
],
},
},
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs: [
{
displayName: 'Vector Store',
maxConnections: 1,
type: NodeConnectionType.AiVectorStore,
required: true,
},
{
displayName: 'Model',
maxConnections: 1,
type: NodeConnectionType.AiLanguageModel,
required: true,
},
],
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: [NodeConnectionType.AiTool],
outputNames: ['Tool'],
properties: [
getConnectionHintNoticeField([NodeConnectionType.AiAgent]),
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. state_of_union_address',
validateType: 'string-alphanumeric',
description: 'Name of the vector store',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
placeholder: 'The most recent state of the Union address',
typeOptions: {
rows: 3,
},
},
{
displayName: 'Limit',
name: 'topK',
type: 'number',
default: 4,
description: 'The maximum number of results to return',
},
],
};
async supplyData(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData> {
const name = this.getNodeParameter('name', itemIndex) as string;
const toolDescription = this.getNodeParameter('description', itemIndex) as string;
const topK = this.getNodeParameter('topK', itemIndex, 4) as number;
const vectorStore = (await this.getInputConnectionData(
NodeConnectionType.AiVectorStore,
itemIndex,
)) as VectorStore;
const llm = (await this.getInputConnectionData(
NodeConnectionType.AiLanguageModel,
0,
)) as BaseLanguageModel;
const description = VectorStoreQATool.getDescription(name, toolDescription);
const vectorStoreTool = new VectorStoreQATool(name, description, {
llm,
vectorStore,
});
vectorStoreTool.chain = VectorDBQAChain.fromLLM(llm, vectorStore, {
k: topK,
});
return {
response: logWrapper(vectorStoreTool, this),
};
}
}

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/n8n-nodes-langchain", "name": "@n8n/n8n-nodes-langchain",
"version": "1.47.0", "version": "1.48.0",
"description": "", "description": "",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -100,6 +100,7 @@
"dist/nodes/tools/ToolCode/ToolCode.node.js", "dist/nodes/tools/ToolCode/ToolCode.node.js",
"dist/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.js", "dist/nodes/tools/ToolHttpRequest/ToolHttpRequest.node.js",
"dist/nodes/tools/ToolSerpApi/ToolSerpApi.node.js", "dist/nodes/tools/ToolSerpApi/ToolSerpApi.node.js",
"dist/nodes/tools/ToolVectorStore/ToolVectorStore.node.js",
"dist/nodes/tools/ToolWikipedia/ToolWikipedia.node.js", "dist/nodes/tools/ToolWikipedia/ToolWikipedia.node.js",
"dist/nodes/tools/ToolWolframAlpha/ToolWolframAlpha.node.js", "dist/nodes/tools/ToolWolframAlpha/ToolWolframAlpha.node.js",
"dist/nodes/tools/ToolWorkflow/ToolWorkflow.node.js", "dist/nodes/tools/ToolWorkflow/ToolWorkflow.node.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "1.47.0", "version": "1.48.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -100,7 +100,7 @@
"@n8n/permissions": "workspace:*", "@n8n/permissions": "workspace:*",
"@n8n/typeorm": "0.3.20-10", "@n8n/typeorm": "0.3.20-10",
"@n8n_io/license-sdk": "2.13.0", "@n8n_io/license-sdk": "2.13.0",
"@oclif/core": "3.26.6", "@oclif/core": "4.0.7",
"@pinecone-database/pinecone": "2.1.0", "@pinecone-database/pinecone": "2.1.0",
"@rudderstack/rudder-sdk-node": "2.0.7", "@rudderstack/rudder-sdk-node": "2.0.7",
"@sentry/integrations": "7.87.0", "@sentry/integrations": "7.87.0",

View file

@ -1,7 +1,6 @@
import 'reflect-metadata'; import 'reflect-metadata';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Command } from '@oclif/core'; import { Command, Errors } from '@oclif/core';
import { ExitError } from '@oclif/core/lib/errors';
import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core'; import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer'; import type { AbstractServer } from '@/AbstractServer';
@ -308,7 +307,7 @@ export abstract class BaseCommand extends Command {
await sleep(100); // give any in-flight query some time to finish await sleep(100); // give any in-flight query some time to finish
await Db.close(); await Db.close();
} }
const exitCode = error instanceof ExitError ? error.oclif.exit : error ? 1 : 0; const exitCode = error instanceof Errors.ExitError ? error.oclif.exit : error ? 1 : 0;
this.exit(exitCode); this.exit(exitCode);
} }

View file

@ -1,6 +1,10 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import config from '@/config'; import config from '@/config';
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service'; import {
CLOUD_TEMP_PRODUCTION_LIMIT,
CLOUD_TEMP_REPORTABLE_THRESHOLDS,
ConcurrencyControlService,
} from '@/concurrency/concurrency-control.service';
import type { Logger } from '@/Logger'; import type { Logger } from '@/Logger';
import { InvalidConcurrencyLimitError } from '@/errors/invalid-concurrency-limit.error'; import { InvalidConcurrencyLimitError } from '@/errors/invalid-concurrency-limit.error';
import { ConcurrencyQueue } from '../concurrency-queue'; import { ConcurrencyQueue } from '../concurrency-queue';
@ -366,4 +370,91 @@ describe('ConcurrencyControlService', () => {
}); });
}); });
}); });
// ----------------------------------
// telemetry
// ----------------------------------
describe('telemetry', () => {
describe('on cloud', () => {
test.each(CLOUD_TEMP_REPORTABLE_THRESHOLDS)(
'for capacity %d, should report temp cloud threshold if reached',
(threshold) => {
/**
* Arrange
*/
config.set('executions.concurrency.productionLimit', CLOUD_TEMP_PRODUCTION_LIMIT);
config.set('deployment.type', 'cloud');
const service = new ConcurrencyControlService(logger, executionRepository, telemetry);
/**
* Act
*/
// @ts-expect-error Private property
service.productionQueue.emit('concurrency-check', {
capacity: CLOUD_TEMP_PRODUCTION_LIMIT - threshold,
});
/**
* Assert
*/
expect(telemetry.track).toHaveBeenCalledWith('User hit concurrency limit', { threshold });
},
);
test.each(CLOUD_TEMP_REPORTABLE_THRESHOLDS.map((t) => t - 1))(
'for capacity %d, should not report temp cloud threshold if not reached',
(threshold) => {
/**
* Arrange
*/
config.set('executions.concurrency.productionLimit', CLOUD_TEMP_PRODUCTION_LIMIT);
config.set('deployment.type', 'cloud');
const service = new ConcurrencyControlService(logger, executionRepository, telemetry);
/**
* Act
*/
// @ts-expect-error Private property
service.productionQueue.emit('concurrency-check', {
capacity: CLOUD_TEMP_PRODUCTION_LIMIT - threshold,
});
/**
* Assert
*/
expect(telemetry.track).not.toHaveBeenCalledWith('User hit concurrency limit', {
threshold,
});
},
);
test.each(CLOUD_TEMP_REPORTABLE_THRESHOLDS.map((t) => t + 1))(
'for capacity %d, should not report temp cloud threshold if exceeded',
(threshold) => {
/**
* Arrange
*/
config.set('executions.concurrency.productionLimit', CLOUD_TEMP_PRODUCTION_LIMIT);
config.set('deployment.type', 'cloud');
const service = new ConcurrencyControlService(logger, executionRepository, telemetry);
/**
* Act
*/
// @ts-expect-error Private property
service.productionQueue.emit('concurrency-check', {
capacity: CLOUD_TEMP_PRODUCTION_LIMIT - threshold,
});
/**
* Assert
*/
expect(telemetry.track).not.toHaveBeenCalledWith('User hit concurrency limit', {
threshold,
});
},
);
});
});
}); });

View file

@ -1,3 +1,4 @@
import { sleep } from 'n8n-workflow';
import { ConcurrencyQueue } from '../concurrency-queue'; import { ConcurrencyQueue } from '../concurrency-queue';
describe('ConcurrencyQueue', () => { describe('ConcurrencyQueue', () => {
@ -10,12 +11,12 @@ describe('ConcurrencyQueue', () => {
const state: Record<string, 'started' | 'finished'> = {}; const state: Record<string, 'started' | 'finished'> = {};
// eslint-disable-next-line @typescript-eslint/promise-function-async // eslint-disable-next-line @typescript-eslint/promise-function-async
const sleep = jest.fn(() => new Promise((resolve) => setTimeout(resolve, 500))); const sleepSpy = jest.fn(() => sleep(500));
const testFn = async (item: { executionId: string }) => { const testFn = async (item: { executionId: string }) => {
await queue.enqueue(item.executionId); await queue.enqueue(item.executionId);
state[item.executionId] = 'started'; state[item.executionId] = 'started';
await sleep(); await sleepSpy();
queue.dequeue(); queue.dequeue();
state[item.executionId] = 'finished'; state[item.executionId] = 'finished';
}; };
@ -29,33 +30,46 @@ describe('ConcurrencyQueue', () => {
]); ]);
// At T+0 seconds this method hasn't yielded to the event-loop, so no `testFn` calls are made // At T+0 seconds this method hasn't yielded to the event-loop, so no `testFn` calls are made
expect(sleep).toHaveBeenCalledTimes(0); expect(sleepSpy).toHaveBeenCalledTimes(0);
expect(state).toEqual({}); expect(state).toEqual({});
// At T+0.4 seconds the first `testFn` has been called, but hasn't resolved // At T+0.4 seconds the first `testFn` has been called, but hasn't resolved
await jest.advanceTimersByTimeAsync(400); await jest.advanceTimersByTimeAsync(400);
expect(sleep).toHaveBeenCalledTimes(1); expect(sleepSpy).toHaveBeenCalledTimes(1);
expect(state).toEqual({ 1: 'started' }); expect(state).toEqual({ 1: 'started' });
// At T+0.5 seconds the first promise has resolved, and the second one has stared // At T+0.5 seconds the first promise has resolved, and the second one has stared
await jest.advanceTimersByTimeAsync(100); await jest.advanceTimersByTimeAsync(100);
expect(sleep).toHaveBeenCalledTimes(2); expect(sleepSpy).toHaveBeenCalledTimes(2);
expect(state).toEqual({ 1: 'finished', 2: 'started' }); expect(state).toEqual({ 1: 'finished', 2: 'started' });
// At T+1 seconds the first two promises have resolved, and the third one has stared // At T+1 seconds the first two promises have resolved, and the third one has stared
await jest.advanceTimersByTimeAsync(500); await jest.advanceTimersByTimeAsync(500);
expect(sleep).toHaveBeenCalledTimes(3); expect(sleepSpy).toHaveBeenCalledTimes(3);
expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started' }); expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started' });
// If the fourth promise is removed, the fifth one is started in the next tick // If the fourth promise is removed, the fifth one is started in the next tick
queue.remove('4'); queue.remove('4');
await jest.advanceTimersByTimeAsync(1); await jest.advanceTimersByTimeAsync(1);
expect(sleep).toHaveBeenCalledTimes(4); expect(sleepSpy).toHaveBeenCalledTimes(4);
expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started', 5: 'started' }); expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started', 5: 'started' });
// at T+5 seconds, all but the fourth promise should be resolved // at T+5 seconds, all but the fourth promise should be resolved
await jest.advanceTimersByTimeAsync(4000); await jest.advanceTimersByTimeAsync(4000);
expect(sleep).toHaveBeenCalledTimes(4); expect(sleepSpy).toHaveBeenCalledTimes(4);
expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'finished', 5: 'finished' }); expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'finished', 5: 'finished' });
}); });
it('should debounce emitting of the `concurrency-check` event', async () => {
const queue = new ConcurrencyQueue(10);
const emitSpy = jest.fn();
queue.on('concurrency-check', emitSpy);
// eslint-disable-next-line @typescript-eslint/promise-function-async
Array.from({ length: 10 }, (_, i) => i).forEach(() => queue.enqueue('1'));
expect(queue.currentCapacity).toBe(0);
await jest.advanceTimersByTimeAsync(1000);
expect(emitSpy).toHaveBeenCalledTimes(1);
});
}); });

View file

@ -9,6 +9,9 @@ import type { WorkflowExecuteMode as ExecutionMode } from 'n8n-workflow';
import type { IExecutingWorkflowData } from '@/Interfaces'; import type { IExecutingWorkflowData } from '@/Interfaces';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
export const CLOUD_TEMP_PRODUCTION_LIMIT = 999;
export const CLOUD_TEMP_REPORTABLE_THRESHOLDS = [5, 10, 20, 50, 100, 200];
@Service() @Service()
export class ConcurrencyControlService { export class ConcurrencyControlService {
private readonly isEnabled: boolean; private readonly isEnabled: boolean;
@ -17,7 +20,9 @@ export class ConcurrencyControlService {
private readonly productionQueue: ConcurrencyQueue; private readonly productionQueue: ConcurrencyQueue;
private readonly limitsToReport = [5, 10, 20, 50, 100, 200]; private readonly limitsToReport = CLOUD_TEMP_REPORTABLE_THRESHOLDS.map(
(t) => CLOUD_TEMP_PRODUCTION_LIMIT - t,
);
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
@ -46,19 +51,17 @@ export class ConcurrencyControlService {
this.isEnabled = true; this.isEnabled = true;
this.productionQueue.on( this.productionQueue.on('concurrency-check', ({ capacity }: { capacity: number }) => {
'execution-throttled', if (this.shouldReport(capacity)) {
async ({ executionId, capacity }: { executionId: string; capacity: number }) => { void this.telemetry.track('User hit concurrency limit', {
this.log('Execution throttled', { executionId }); threshold: CLOUD_TEMP_PRODUCTION_LIMIT - capacity,
});
}
});
/** this.productionQueue.on('execution-throttled', ({ executionId }: { executionId: string }) => {
* Temporary until base data for cloud plans is collected. this.log('Execution throttled', { executionId });
*/ });
if (this.shouldReport(capacity)) {
await this.telemetry.track('User hit concurrency limit', { threshold: capacity });
}
},
);
this.productionQueue.on('execution-released', async (executionId: string) => { this.productionQueue.on('execution-released', async (executionId: string) => {
this.log('Execution released', { executionId }); this.log('Execution released', { executionId });

View file

@ -1,5 +1,6 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import debounce from 'lodash/debounce';
@Service() @Service()
export class ConcurrencyQueue extends EventEmitter { export class ConcurrencyQueue extends EventEmitter {
@ -15,14 +16,20 @@ export class ConcurrencyQueue extends EventEmitter {
async enqueue(executionId: string) { async enqueue(executionId: string) {
this.capacity--; this.capacity--;
this.debouncedEmit('concurrency-check', { capacity: this.capacity });
if (this.capacity < 0) { if (this.capacity < 0) {
this.emit('execution-throttled', { executionId, capacity: this.capacity }); this.emit('execution-throttled', { executionId });
// eslint-disable-next-line @typescript-eslint/return-await // eslint-disable-next-line @typescript-eslint/return-await
return new Promise<void>((resolve) => this.queue.push({ executionId, resolve })); return new Promise<void>((resolve) => this.queue.push({ executionId, resolve }));
} }
} }
get currentCapacity() {
return this.capacity;
}
dequeue() { dequeue() {
this.capacity++; this.capacity++;
@ -56,4 +63,9 @@ export class ConcurrencyQueue extends EventEmitter {
resolve(); resolve();
} }
private debouncedEmit = debounce(
(event: string, payload: object) => this.emit(event, payload),
300,
);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "1.47.0", "version": "1.48.0",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-design-system", "name": "n8n-design-system",
"version": "1.37.0", "version": "1.38.0",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"author": { "author": {

View file

@ -79,16 +79,19 @@
--color-sticky-border-7: var(--prim-gray-670); --color-sticky-border-7: var(--prim-gray-670);
// NodeIcon // NodeIcon
--color-node-icon-gray: var(--prim-gray-200); --color-node-icon-gray: var(--prim-gray-120);
--color-node-icon-black: var(--prim-gray-70); --color-node-icon-black: var(--prim-gray-10);
--color-node-icon-blue: #766dfb; --color-node-icon-blue: #898fff;
--color-node-icon-dark-blue: #6275ad; --color-node-icon-light-blue: #58abff;
--color-node-icon-dark-blue: #7ba7ff;
--color-node-icon-orange-red: var(--prim-color-primary); --color-node-icon-orange-red: var(--prim-color-primary);
--color-node-icon-pink-red: #f85d82;
--color-node-icon-red: var(--prim-color-alt-k); --color-node-icon-red: var(--prim-color-alt-k);
--color-node-icon-light-green: #20b69e; --color-node-icon-light-green: #20b69e;
--color-node-icon-green: #38cb7a;
--color-node-icon-dark-green: #86decc; --color-node-icon-dark-green: #86decc;
--color-node-icon-purple: #9b6dd5; --color-node-icon-purple: #9b6dd5;
--color-node-icon-crimson: #d05876; --color-node-icon-crimson: #f188a2;
// Expressions, autocomplete, infobox // Expressions, autocomplete, infobox
--color-valid-resolvable-foreground: var(--prim-color-alt-a-tint-300); --color-valid-resolvable-foreground: var(--prim-color-alt-a-tint-300);

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "1.47.0", "version": "1.48.0",
"description": "Workflow Editor UI for n8n", "description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",

View file

@ -6,16 +6,20 @@ export function createCanvasNodeData({
id = 'node', id = 'node',
type = 'test', type = 'test',
typeVersion = 1, typeVersion = 1,
disabled = false,
inputs = [], inputs = [],
outputs = [], outputs = [],
connections = { input: {}, output: {} },
renderType = 'default', renderType = 'default',
}: Partial<CanvasElementData> = {}): CanvasElementData { }: Partial<CanvasElementData> = {}): CanvasElementData {
return { return {
id, id,
type, type,
typeVersion, typeVersion,
disabled,
inputs, inputs,
outputs, outputs,
connections,
renderType, renderType,
}; };
} }

View file

@ -29,12 +29,14 @@ export const mockNode = ({
name, name,
type, type,
position = [0, 0], position = [0, 0],
disabled = false,
}: { }: {
id?: INode['id']; id?: INode['id'];
name: INode['name']; name: INode['name'];
type: INode['type']; type: INode['type'];
position?: INode['position']; position?: INode['position'];
}) => mock<INode>({ id, name, type, position }); disabled?: INode['disabled'];
}) => mock<INode>({ id, name, type, position, disabled });
export const mockNodeTypeDescription = ({ export const mockNodeTypeDescription = ({
name, name,

View file

@ -53,7 +53,7 @@ export const codeNodeEditorTheme = ({
}, },
'.cm-content': { '.cm-content': {
fontFamily: BASE_STYLING.fontFamily, fontFamily: BASE_STYLING.fontFamily,
caretColor: 'var(--color-code-caret)', caretColor: isReadOnly ? 'transparent' : 'var(--color-code-caret)',
}, },
'.cm-cursor, .cm-dropCursor': { '.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--color-code-caret)', borderLeftColor: 'var(--color-code-caret)',

View file

@ -613,6 +613,7 @@ function copySuccess() {
} }
&__header-description { &__header-description {
overflow: hidden;
padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s); padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s);
font-size: var(--font-size-xs); font-size: var(--font-size-xs);

View file

@ -97,7 +97,6 @@ onMounted(() => {
extensions: [ extensions: [
EditorState.readOnly.of(true), EditorState.readOnly.of(true),
EditorView.lineWrapping, EditorView.lineWrapping,
EditorView.editable.of(false),
EditorView.domEventHandlers({ scroll: forceParse }), EditorView.domEventHandlers({ scroll: forceParse }),
...props.extensions, ...props.extensions,
], ],

View file

@ -53,7 +53,7 @@ const extensions = computed(() => [
), ),
n8nLang(), n8nLang(),
n8nAutocompletion(), n8nAutocompletion(),
inputTheme({ rows: props.rows }), inputTheme({ isReadOnly: props.isReadOnly, rows: props.rows }),
history(), history(),
expressionInputHandler(), expressionInputHandler(),
EditorView.lineWrapping, EditorView.lineWrapping,

View file

@ -1,24 +1,24 @@
import { EditorView } from '@codemirror/view'; import { EditorView } from '@codemirror/view';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter'; import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
const commonThemeProps = { const commonThemeProps = (isReadOnly = false) => ({
'&.cm-focused': { '&.cm-focused': {
outline: '0 !important', outline: '0 !important',
}, },
'.cm-content': { '.cm-content': {
fontFamily: 'var(--font-family-monospace)', fontFamily: 'var(--font-family-monospace)',
color: 'var(--input-font-color, var(--color-text-dark))', color: 'var(--input-font-color, var(--color-text-dark))',
caretColor: 'var(--color-code-caret)', caretColor: isReadOnly ? 'transparent' : 'var(--color-code-caret)',
}, },
'.cm-line': { '.cm-line': {
padding: '0', padding: '0',
}, },
}; });
export const inputTheme = ({ rows } = { rows: 5 }) => { export const inputTheme = ({ rows, isReadOnly } = { rows: 5, isReadOnly: false }) => {
const maxHeight = Math.max(rows * 22 + 8); const maxHeight = Math.max(rows * 22 + 8);
const theme = EditorView.theme({ const theme = EditorView.theme({
...commonThemeProps, ...commonThemeProps(isReadOnly),
'&': { '&': {
maxHeight: `${maxHeight}px`, maxHeight: `${maxHeight}px`,
minHeight: '30px', minHeight: '30px',
@ -54,7 +54,7 @@ export const inputTheme = ({ rows } = { rows: 5 }) => {
export const outputTheme = () => { export const outputTheme = () => {
const theme = EditorView.theme({ const theme = EditorView.theme({
...commonThemeProps, ...commonThemeProps(true),
'&': { '&': {
maxHeight: '95px', maxHeight: '95px',
width: '100%', width: '100%',

View file

@ -61,7 +61,6 @@ const extensions = computed(() => {
lineNumbers(), lineNumbers(),
EditorView.lineWrapping, EditorView.lineWrapping,
EditorState.readOnly.of(props.isReadOnly), EditorState.readOnly.of(props.isReadOnly),
EditorView.editable.of(!props.isReadOnly),
codeNodeEditorTheme({ codeNodeEditorTheme({
isReadOnly: props.isReadOnly, isReadOnly: props.isReadOnly,
maxHeight: props.fillParent ? '100%' : '40vh', maxHeight: props.fillParent ? '100%' : '40vh',

View file

@ -54,7 +54,6 @@ const extensions = computed(() => {
lineNumbers(), lineNumbers(),
EditorView.lineWrapping, EditorView.lineWrapping,
EditorState.readOnly.of(props.isReadOnly), EditorState.readOnly.of(props.isReadOnly),
EditorView.editable.of(!props.isReadOnly),
codeNodeEditorTheme({ codeNodeEditorTheme({
isReadOnly: props.isReadOnly, isReadOnly: props.isReadOnly,
maxHeight: props.fillParent ? '100%' : '40vh', maxHeight: props.fillParent ? '100%' : '40vh',

View file

@ -61,7 +61,6 @@ describe('ExpressionParameterInput', () => {
await waitFor(() => { await waitFor(() => {
const editor = container.querySelector('.cm-content') as HTMLDivElement; const editor = container.querySelector('.cm-content') as HTMLDivElement;
expect(editor).toBeInTheDocument(); expect(editor).toBeInTheDocument();
expect(editor.getAttribute('contenteditable')).toEqual('false');
expect(editor.getAttribute('aria-readonly')).toEqual('true'); expect(editor.getAttribute('aria-readonly')).toEqual('true');
}); });
}); });

View file

@ -15,6 +15,7 @@ const emit = defineEmits<{
'update:modelValue': [elements: CanvasElement[]]; 'update:modelValue': [elements: CanvasElement[]];
'update:node:position': [id: string, position: XYPosition]; 'update:node:position': [id: string, position: XYPosition];
'update:node:active': [id: string]; 'update:node:active': [id: string];
'update:node:enabled': [id: string];
'update:node:selected': [id?: string]; 'update:node:selected': [id?: string];
'delete:node': [id: string]; 'delete:node': [id: string];
'delete:connection': [connection: Connection]; 'delete:connection': [connection: Connection];
@ -66,6 +67,10 @@ function onSelectNode() {
emit('update:node:selected', selectedNodeId); emit('update:node:selected', selectedNodeId);
} }
function onToggleNodeEnabled(id: string) {
emit('update:node:enabled', id);
}
function onDeleteNode(id: string) { function onDeleteNode(id: string) {
emit('delete:node', id); emit('delete:node', id);
} }
@ -126,6 +131,7 @@ function onClickPane(event: MouseEvent) {
v-bind="canvasNodeProps" v-bind="canvasNodeProps"
@delete="onDeleteNode" @delete="onDeleteNode"
@select="onSelectNode" @select="onSelectNode"
@toggle="onToggleNodeEnabled"
@activate="onSetNodeActive" @activate="onSetNodeActive"
/> />
</template> </template>

View file

@ -18,21 +18,24 @@ import type { NodeProps } from '@vue-flow/core';
const emit = defineEmits<{ const emit = defineEmits<{
delete: [id: string]; delete: [id: string];
select: [id: string, selected: boolean]; select: [id: string, selected: boolean];
toggle: [id: string];
activate: [id: string]; activate: [id: string];
}>(); }>();
const props = defineProps<NodeProps<CanvasElementData>>(); const props = defineProps<NodeProps<CanvasElementData>>();
const inputs = computed(() => props.data.inputs);
const outputs = computed(() => props.data.outputs);
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const inputs = computed(() => props.data.inputs);
const outputs = computed(() => props.data.outputs);
const connections = computed(() => props.data.connections);
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs } = useNodeConnections({ const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs } = useNodeConnections({
inputs, inputs,
outputs, outputs,
connections,
}); });
const isDisabled = computed(() => props.data.disabled);
const nodeType = computed(() => { const nodeType = computed(() => {
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion); return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
}); });
@ -107,6 +110,10 @@ function onDelete() {
emit('delete', props.id); emit('delete', props.id);
} }
function onDisabledToggle() {
emit('toggle', props.id);
}
function onActivate() { function onActivate() {
emit('activate', props.id); emit('activate', props.id);
} }
@ -143,12 +150,12 @@ function onActivate() {
data-test-id="canvas-node-toolbar" data-test-id="canvas-node-toolbar"
:class="$style.canvasNodeToolbar" :class="$style.canvasNodeToolbar"
@delete="onDelete" @delete="onDelete"
@toggle="onDisabledToggle"
/> />
<CanvasNodeRenderer v-if="nodeType" @dblclick="onActivate"> <CanvasNodeRenderer v-if="nodeType" @dblclick="onActivate">
<NodeIcon :node-type="nodeType" :size="40" :shrink="false" /> <NodeIcon :node-type="nodeType" :size="40" :shrink="false" :disabled="isDisabled" />
<!-- :color-default="iconColorDefault"--> <!-- :color-default="iconColorDefault"-->
<!-- :disabled="data.disabled"-->
</CanvasNodeRenderer> </CanvasNodeRenderer>
</div> </div>
</template> </template>

View file

@ -53,39 +53,39 @@ describe('CanvasNodeToolbar', () => {
}); });
it('should call toggleDisableNode function when disable node button is clicked', async () => { it('should call toggleDisableNode function when disable node button is clicked', async () => {
const toggleDisableNode = vi.fn(); const onToggleNode = vi.fn();
const { getByTestId } = renderComponent({ const { getByTestId } = renderComponent({
global: { global: {
provide: { provide: {
...createCanvasNodeProvide(), ...createCanvasNodeProvide(),
}, },
mocks: { mocks: {
toggleDisableNode, onToggleNode,
}, },
}, },
}); });
await fireEvent.click(getByTestId('disable-node-button')); await fireEvent.click(getByTestId('disable-node-button'));
expect(toggleDisableNode).toHaveBeenCalled(); expect(onToggleNode).toHaveBeenCalled();
}); });
it('should call deleteNode function when delete node button is clicked', async () => { it('should call deleteNode function when delete node button is clicked', async () => {
const deleteNode = vi.fn(); const onDeleteNode = vi.fn();
const { getByTestId } = renderComponent({ const { getByTestId } = renderComponent({
global: { global: {
provide: { provide: {
...createCanvasNodeProvide(), ...createCanvasNodeProvide(),
}, },
mocks: { mocks: {
deleteNode, onDeleteNode,
}, },
}, },
}); });
await fireEvent.click(getByTestId('delete-node-button')); await fireEvent.click(getByTestId('delete-node-button'));
expect(deleteNode).toHaveBeenCalled(); expect(onDeleteNode).toHaveBeenCalled();
}); });
it('should call openContextMenu function when overflow node button is clicked', async () => { it('should call openContextMenu function when overflow node button is clicked', async () => {

View file

@ -3,12 +3,14 @@ import { computed, inject, useCssModule } from 'vue';
import { CanvasNodeKey } from '@/constants'; import { CanvasNodeKey } from '@/constants';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
const emit = defineEmits(['delete']); const emit = defineEmits<{
delete: [];
toggle: [];
}>();
const $style = useCssModule(); const $style = useCssModule();
const node = inject(CanvasNodeKey);
const i18n = useI18n(); const i18n = useI18n();
const node = inject(CanvasNodeKey);
const data = computed(() => node?.data.value); const data = computed(() => node?.data.value);
@ -21,10 +23,11 @@ const nodeDisabledTitle = 'Test';
// @TODO // @TODO
function executeNode() {} function executeNode() {}
// @TODO function onToggleNode() {
function toggleDisableNode() {} emit('toggle');
}
function deleteNode() { function onDeleteNode() {
emit('delete'); emit('delete');
} }
@ -53,7 +56,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
size="small" size="small"
icon="power-off" icon="power-off"
:title="nodeDisabledTitle" :title="nodeDisabledTitle"
@click="toggleDisableNode" @click="onToggleNode"
/> />
<N8nIconButton <N8nIconButton
data-test-id="delete-node-button" data-test-id="delete-node-button"
@ -62,7 +65,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
text text
icon="trash" icon="trash"
:title="i18n.baseText('node.delete')" :title="i18n.baseText('node.delete')"
@click="deleteNode" @click="onDeleteNode"
/> />
<N8nIconButton <N8nIconButton
data-test-id="overflow-node-button" data-test-id="overflow-node-button"

View file

@ -44,6 +44,36 @@ describe('CanvasNodeConfigurable', () => {
}); });
}); });
describe('disabled', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
disabled: true,
},
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
expect(getByText('(Deactivated)')).toBeVisible();
});
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
});
});
describe('inputs', () => { describe('inputs', () => {
it('should adjust width css variable based on the number of non-main inputs', () => { it('should adjust width css variable based on the number of non-main inputs', () => {
const { getByText } = renderComponent({ const { getByText } = renderComponent({

View file

@ -2,24 +2,32 @@
import { computed, inject, useCssModule } from 'vue'; import { computed, inject, useCssModule } from 'vue';
import { CanvasNodeKey, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants'; import { CanvasNodeKey, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
import { useNodeConnections } from '@/composables/useNodeConnections'; import { useNodeConnections } from '@/composables/useNodeConnections';
import { useI18n } from '@/composables/useI18n';
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
const node = inject(CanvasNodeKey); const node = inject(CanvasNodeKey);
const $style = useCssModule(); const $style = useCssModule();
const i18n = useI18n();
const label = computed(() => node?.label.value ?? ''); const label = computed(() => node?.label.value ?? '');
const inputs = computed(() => node?.data.value.inputs ?? []); const inputs = computed(() => node?.data.value.inputs ?? []);
const outputs = computed(() => node?.data.value.outputs ?? []); const outputs = computed(() => node?.data.value.outputs ?? []);
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({ const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
inputs, inputs,
outputs, outputs,
connections,
}); });
const isDisabled = computed(() => node?.data.value.disabled ?? false);
const classes = computed(() => { const classes = computed(() => {
return { return {
[$style.node]: true, [$style.node]: true,
[$style.selected]: node?.selected.value, [$style.selected]: node?.selected.value,
[$style.disabled]: isDisabled.value,
}; };
}); });
@ -46,7 +54,11 @@ const styles = computed(() => {
<template> <template>
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable"> <div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
<slot /> <slot />
<div :class="$style.label">{{ label }}</div> <CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
<div :class="$style.label">
{{ label }}
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
</div>
</div> </div>
</template> </template>
@ -54,12 +66,14 @@ const styles = computed(() => {
.node { .node {
--configurable-node-min-input-count: 4; --configurable-node-min-input-count: 4;
--configurable-node-input-width: 65px; --configurable-node-input-width: 65px;
--canvas-node--height: 100px;
width: calc( --canvas-node--width: calc(
max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) * max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) *
var(--configurable-node-input-width) var(--configurable-node-input-width)
); );
height: 100px;
width: var(--canvas-node--width);
height: var(--canvas-node--height);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -82,4 +96,8 @@ const styles = computed(() => {
.selected { .selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected); box-shadow: 0 0 0 4px var(--color-canvas-selected);
} }
.disabled {
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
}
</style> </style>

View file

@ -40,4 +40,34 @@ describe('CanvasNodeConfiguration', () => {
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected'); expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
}); });
}); });
describe('disabled', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
disabled: true,
},
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
expect(getByText('(Deactivated)')).toBeVisible();
});
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
});
});
}); });

View file

@ -1,17 +1,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, useCssModule } from 'vue'; import { computed, inject, useCssModule } from 'vue';
import { CanvasNodeKey } from '@/constants'; import { CanvasNodeKey } from '@/constants';
import { useI18n } from '@/composables/useI18n';
const node = inject(CanvasNodeKey); const node = inject(CanvasNodeKey);
const $style = useCssModule(); const $style = useCssModule();
const i18n = useI18n();
const label = computed(() => node?.label.value ?? ''); const label = computed(() => node?.label.value ?? '');
const isDisabled = computed(() => node?.data.value.disabled ?? false);
const classes = computed(() => { const classes = computed(() => {
return { return {
[$style.node]: true, [$style.node]: true,
[$style.selected]: node?.selected.value, [$style.selected]: node?.selected.value,
[$style.disabled]: isDisabled.value,
}; };
}); });
</script> </script>
@ -19,14 +24,20 @@ const classes = computed(() => {
<template> <template>
<div :class="classes" data-test-id="canvas-node-configuration"> <div :class="classes" data-test-id="canvas-node-configuration">
<slot /> <slot />
<div v-if="label" :class="$style.label">{{ label }}</div> <div v-if="label" :class="$style.label">
{{ label }}
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
</div>
</div> </div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.node { .node {
width: 75px; --canvas-node--width: 75px;
height: 75px; --canvas-node--height: 75px;
width: var(--canvas-node--width);
height: var(--canvas-node--height);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -35,10 +46,6 @@ const classes = computed(() => {
border-radius: 50%; border-radius: 50%;
} }
.selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected);
}
.label { .label {
top: 100%; top: 100%;
position: absolute; position: absolute;
@ -48,4 +55,12 @@ const classes = computed(() => {
min-width: 200px; min-width: 200px;
margin-top: var(--spacing-2xs); margin-top: var(--spacing-2xs);
} }
.selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected);
}
.disabled {
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
}
</style> </style>

View file

@ -81,4 +81,34 @@ describe('CanvasNodeDefault', () => {
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected'); expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
}); });
}); });
describe('disabled', () => {
it('should apply disabled class when node is disabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
disabled: true,
},
}),
},
},
});
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
expect(getByText('(Deactivated)')).toBeVisible();
});
it('should not apply disabled class when node is enabled', () => {
const { getByText } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide(),
},
},
});
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
});
});
}); });

View file

@ -2,24 +2,32 @@
import { computed, inject, useCssModule } from 'vue'; import { computed, inject, useCssModule } from 'vue';
import { useNodeConnections } from '@/composables/useNodeConnections'; import { useNodeConnections } from '@/composables/useNodeConnections';
import { CanvasNodeKey } from '@/constants'; import { CanvasNodeKey } from '@/constants';
import { useI18n } from '@/composables/useI18n';
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
const node = inject(CanvasNodeKey); const node = inject(CanvasNodeKey);
const $style = useCssModule(); const $style = useCssModule();
const i18n = useI18n();
const label = computed(() => node?.label.value ?? ''); const label = computed(() => node?.label.value ?? '');
const inputs = computed(() => node?.data.value.inputs ?? []); const inputs = computed(() => node?.data.value.inputs ?? []);
const outputs = computed(() => node?.data.value.outputs ?? []); const outputs = computed(() => node?.data.value.outputs ?? []);
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
const { mainOutputs } = useNodeConnections({ const { mainOutputs } = useNodeConnections({
inputs, inputs,
outputs, outputs,
connections,
}); });
const isDisabled = computed(() => node?.data.value.disabled ?? false);
const classes = computed(() => { const classes = computed(() => {
return { return {
[$style.node]: true, [$style.node]: true,
[$style.selected]: node?.selected.value, [$style.selected]: node?.selected.value,
[$style.disabled]: isDisabled.value,
}; };
}); });
@ -33,14 +41,21 @@ const styles = computed(() => {
<template> <template>
<div v-if="node" :class="classes" :style="styles" data-test-id="canvas-node-default"> <div v-if="node" :class="classes" :style="styles" data-test-id="canvas-node-default">
<slot /> <slot />
<div v-if="label" :class="$style.label">{{ label }}</div> <CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
<div v-if="label" :class="$style.label">
{{ label }}
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
</div>
</div> </div>
</template> </template>
<style lang="scss" module> <style lang="scss" module>
.node { .node {
height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px); --canvas-node--height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px);
width: 100px; --canvas-node--width: 100px;
height: var(--canvas-node--height);
width: var(--canvas-node--width);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -62,4 +77,8 @@ const styles = computed(() => {
.selected { .selected {
box-shadow: 0 0 0 4px var(--color-canvas-selected); box-shadow: 0 0 0 4px var(--color-canvas-selected);
} }
.disabled {
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
}
</style> </style>

View file

@ -0,0 +1,35 @@
import CanvasNodeDisabledStrikeThrough from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { NodeConnectionType } from 'n8n-workflow';
import { createCanvasNodeProvide } from '@/__tests__/data';
const renderComponent = createComponentRenderer(CanvasNodeDisabledStrikeThrough);
describe('CanvasNodeDisabledStrikeThrough', () => {
it('should render node correctly', () => {
const { container } = renderComponent({
global: {
provide: {
...createCanvasNodeProvide({
data: {
connections: {
input: {
[NodeConnectionType.Main]: [
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
],
},
output: {
[NodeConnectionType.Main]: [
[{ node: 'node', type: NodeConnectionType.Main, index: 0 }],
],
},
},
},
}),
},
},
});
expect(container.firstChild).toBeVisible();
});
});

View file

@ -0,0 +1,53 @@
<script setup lang="ts">
import { computed, inject, useCssModule } from 'vue';
import { CanvasNodeKey } from '@/constants';
import { useNodeConnections } from '@/composables/useNodeConnections';
const $style = useCssModule();
const node = inject(CanvasNodeKey);
const inputs = computed(() => node?.data.value.inputs ?? []);
const outputs = computed(() => node?.data.value.outputs ?? []);
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
const { mainInputConnections, mainOutputConnections } = useNodeConnections({
inputs,
outputs,
connections,
});
const isVisible = computed(
() => mainInputConnections.value.length === 1 && mainOutputConnections.value.length === 1,
);
const isSuccessStatus = computed(
() => false,
// @TODO Implement this
// () => !['unknown'].includes(node.status) && workflowDataItems > 0,
);
const classes = computed(() => {
return {
[$style.disabledStrikeThrough]: true,
[$style.success]: isSuccessStatus.value,
};
});
</script>
<template>
<div v-if="isVisible" :class="classes"></div>
</template>
<style lang="scss" module>
.disabledStrikeThrough {
border: 1px solid var(--color-foreground-dark);
position: absolute;
top: calc(var(--canvas-node--height) / 2 - 1px);
left: -4px;
width: calc(100% + 12px);
pointer-events: none;
}
.success {
border-color: var(--color-success-light);
}
</style>

View file

@ -7,7 +7,8 @@ import { mock } from 'vitest-mock-extended';
import { useCanvasMapping } from '@/composables/useCanvasMapping'; import { useCanvasMapping } from '@/composables/useCanvasMapping';
import type { IWorkflowDb } from '@/Interface'; import type { IWorkflowDb } from '@/Interface';
import { createTestWorkflowObject, mockNodes } from '@/__tests__/mocks'; import { createTestWorkflowObject, mockNode, mockNodes } from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
vi.mock('@/stores/nodeTypes.store', () => ({ vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({ useNodeTypesStore: vi.fn(() => ({
@ -48,7 +49,11 @@ describe('useCanvasMapping', () => {
describe('elements', () => { describe('elements', () => {
it('should map nodes to canvas elements', () => { it('should map nodes to canvas elements', () => {
const manualTriggerNode = mockNodes[0]; const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: false,
});
const workflow = mock<IWorkflowDb>({ const workflow = mock<IWorkflowDb>({
nodes: [manualTriggerNode], nodes: [manualTriggerNode],
}); });
@ -69,13 +74,75 @@ describe('useCanvasMapping', () => {
id: manualTriggerNode.id, id: manualTriggerNode.id,
type: manualTriggerNode.type, type: manualTriggerNode.type,
typeVersion: expect.anything(), typeVersion: expect.anything(),
disabled: false,
inputs: [], inputs: [],
outputs: [], outputs: [],
connections: {
input: {},
output: {},
},
renderType: 'default', renderType: 'default',
}, },
}, },
]); ]);
}); });
it('should handle node disabled state', () => {
const manualTriggerNode = mockNode({
name: 'Manual Trigger',
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: true,
});
const workflow = mock<IWorkflowDb>({
nodes: [manualTriggerNode],
});
const workflowObject = createTestWorkflowObject(workflow);
const { elements } = useCanvasMapping({
workflow: ref(workflow),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(elements.value[0]?.data?.disabled).toEqual(true);
});
it('should handle input and output connections', () => {
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
const workflow = mock<IWorkflowDb>({
nodes: [manualTriggerNode, setNode],
connections: {
[manualTriggerNode.name]: {
[NodeConnectionType.Main]: [
[{ node: setNode.name, type: NodeConnectionType.Main, index: 0 }],
],
},
},
});
const workflowObject = createTestWorkflowObject(workflow);
const { elements } = useCanvasMapping({
workflow: ref(workflow),
workflowObject: ref(workflowObject) as Ref<Workflow>,
});
expect(elements.value[0]?.data?.connections.output).toHaveProperty(NodeConnectionType.Main);
expect(elements.value[0]?.data?.connections.output[NodeConnectionType.Main][0][0]).toEqual(
expect.objectContaining({
node: setNode.name,
type: NodeConnectionType.Main,
index: 0,
}),
);
expect(elements.value[1]?.data?.connections.input).toHaveProperty(NodeConnectionType.Main);
expect(elements.value[1]?.data?.connections.input[NodeConnectionType.Main][0][0]).toEqual(
expect.objectContaining({
node: manualTriggerNode.name,
type: NodeConnectionType.Main,
index: 0,
}),
);
});
}); });
describe('connections', () => { describe('connections', () => {

View file

@ -4,6 +4,7 @@ import { useNodeConnections } from '@/composables/useNodeConnections';
import type { CanvasElementData } from '@/types'; import type { CanvasElementData } from '@/types';
describe('useNodeConnections', () => { describe('useNodeConnections', () => {
const defaultConnections = { input: {}, output: {} };
describe('mainInputs', () => { describe('mainInputs', () => {
it('should return main inputs when provided with main inputs', () => { it('should return main inputs when provided with main inputs', () => {
const inputs = ref<CanvasElementData['inputs']>([ const inputs = ref<CanvasElementData['inputs']>([
@ -14,7 +15,11 @@ describe('useNodeConnections', () => {
]); ]);
const outputs = ref<CanvasElementData['outputs']>([]); const outputs = ref<CanvasElementData['outputs']>([]);
const { mainInputs } = useNodeConnections({ inputs, outputs }); const { mainInputs } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
expect(mainInputs.value.length).toBe(3); expect(mainInputs.value.length).toBe(3);
expect(mainInputs.value).toEqual(inputs.value.slice(0, 3)); expect(mainInputs.value).toEqual(inputs.value.slice(0, 3));
@ -30,7 +35,11 @@ describe('useNodeConnections', () => {
]); ]);
const outputs = ref<CanvasElementData['outputs']>([]); const outputs = ref<CanvasElementData['outputs']>([]);
const { nonMainInputs } = useNodeConnections({ inputs, outputs }); const { nonMainInputs } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
expect(nonMainInputs.value.length).toBe(2); expect(nonMainInputs.value.length).toBe(2);
expect(nonMainInputs.value).toEqual(inputs.value.slice(1)); expect(nonMainInputs.value).toEqual(inputs.value.slice(1));
@ -46,13 +55,42 @@ describe('useNodeConnections', () => {
]); ]);
const outputs = ref<CanvasElementData['outputs']>([]); const outputs = ref<CanvasElementData['outputs']>([]);
const { requiredNonMainInputs } = useNodeConnections({ inputs, outputs }); const { requiredNonMainInputs } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
expect(requiredNonMainInputs.value.length).toBe(1); expect(requiredNonMainInputs.value.length).toBe(1);
expect(requiredNonMainInputs.value).toEqual([inputs.value[1]]); expect(requiredNonMainInputs.value).toEqual([inputs.value[1]]);
}); });
}); });
describe('mainInputConnections', () => {
it('should return main input connections when provided with main input connections', () => {
const inputs = ref<CanvasElementData['inputs']>([]);
const outputs = ref<CanvasElementData['outputs']>([]);
const connections = ref<CanvasElementData['connections']>({
input: {
[NodeConnectionType.Main]: [
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
],
},
output: {},
});
const { mainInputConnections } = useNodeConnections({
inputs,
outputs,
connections,
});
expect(mainInputConnections.value.length).toBe(2);
expect(mainInputConnections.value).toEqual(connections.value.input[NodeConnectionType.Main]);
});
});
describe('mainOutputs', () => { describe('mainOutputs', () => {
it('should return main outputs when provided with main outputs', () => { it('should return main outputs when provided with main outputs', () => {
const inputs = ref<CanvasElementData['inputs']>([]); const inputs = ref<CanvasElementData['inputs']>([]);
@ -63,7 +101,11 @@ describe('useNodeConnections', () => {
{ type: NodeConnectionType.AiAgent, index: 0 }, { type: NodeConnectionType.AiAgent, index: 0 },
]); ]);
const { mainOutputs } = useNodeConnections({ inputs, outputs }); const { mainOutputs } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
expect(mainOutputs.value.length).toBe(3); expect(mainOutputs.value.length).toBe(3);
expect(mainOutputs.value).toEqual(outputs.value.slice(0, 3)); expect(mainOutputs.value).toEqual(outputs.value.slice(0, 3));
@ -79,10 +121,41 @@ describe('useNodeConnections', () => {
{ type: NodeConnectionType.AiAgent, index: 1 }, { type: NodeConnectionType.AiAgent, index: 1 },
]); ]);
const { nonMainOutputs } = useNodeConnections({ inputs, outputs }); const { nonMainOutputs } = useNodeConnections({
inputs,
outputs,
connections: defaultConnections,
});
expect(nonMainOutputs.value.length).toBe(2); expect(nonMainOutputs.value.length).toBe(2);
expect(nonMainOutputs.value).toEqual(outputs.value.slice(1)); expect(nonMainOutputs.value).toEqual(outputs.value.slice(1));
}); });
}); });
describe('mainOutputConnections', () => {
it('should return main output connections when provided with main output connections', () => {
const inputs = ref<CanvasElementData['inputs']>([]);
const outputs = ref<CanvasElementData['outputs']>([]);
const connections = ref<CanvasElementData['connections']>({
input: {},
output: {
[NodeConnectionType.Main]: [
[{ node: 'node1', type: NodeConnectionType.Main, index: 0 }],
[{ node: 'node2', type: NodeConnectionType.Main, index: 0 }],
],
},
});
const { mainOutputConnections } = useNodeConnections({
inputs,
outputs,
connections,
});
expect(mainOutputConnections.value.length).toBe(2);
expect(mainOutputConnections.value).toEqual(
connections.value.output[NodeConnectionType.Main],
);
});
});
}); });

View file

@ -89,12 +89,20 @@ export function useCanvasMapping({
const elements = computed<CanvasElement[]>(() => [ const elements = computed<CanvasElement[]>(() => [
...workflow.value.nodes.map<CanvasElement>((node) => { ...workflow.value.nodes.map<CanvasElement>((node) => {
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {};
const data: CanvasElementData = { const data: CanvasElementData = {
id: node.id, id: node.id,
type: node.type, type: node.type,
typeVersion: node.typeVersion, typeVersion: node.typeVersion,
disabled: !!node.disabled,
inputs: nodeInputsById.value[node.id] ?? [], inputs: nodeInputsById.value[node.id] ?? [],
outputs: nodeOutputsById.value[node.id] ?? [], outputs: nodeOutputsById.value[node.id] ?? [],
connections: {
input: inputConnections,
output: outputConnections,
},
renderType: renderTypeByNodeType.value[node.type] ?? 'default', renderType: renderTypeByNodeType.value[node.type] ?? 'default',
}; };

View file

@ -46,6 +46,7 @@ import { useCredentialsStore } from '@/stores/credentials.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import type { useRouter } from 'vue-router'; import type { useRouter } from 'vue-router';
import { useCanvasStore } from '@/stores/canvas.store'; import { useCanvasStore } from '@/stores/canvas.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
type AddNodeData = { type AddNodeData = {
name?: string; name?: string;
@ -78,6 +79,7 @@ export function useCanvasOperations({
const i18n = useI18n(); const i18n = useI18n();
const toast = useToast(); const toast = useToast();
const workflowHelpers = useWorkflowHelpers({ router }); const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
@ -235,6 +237,18 @@ export function useCanvasOperations({
uiStore.lastSelectedNode = node.name; uiStore.lastSelectedNode = node.name;
} }
function toggleNodeDisabled(
id: string,
{ trackHistory = true }: { trackHistory?: boolean } = {},
) {
const node = workflowsStore.getNodeById(id);
if (!node) {
return;
}
nodeHelpers.disableNodes([node], trackHistory);
}
async function addNodes( async function addNodes(
nodes: AddedNodesAndConnections['nodes'], nodes: AddedNodesAndConnections['nodes'],
{ {
@ -886,6 +900,7 @@ export function useCanvasOperations({
setNodeActive, setNodeActive,
setNodeActiveByName, setNodeActiveByName,
setNodeSelected, setNodeSelected,
toggleNodeDisabled,
renameNode, renameNode,
revertRenameNode, revertRenameNode,
deleteNode, deleteNode,

View file

@ -185,10 +185,7 @@ export const useExpressionEditor = ({
doc: toValue(editorValue), doc: toValue(editorValue),
extensions: [ extensions: [
customExtensions.value.of(toValue(extensions)), customExtensions.value.of(toValue(extensions)),
readOnlyExtensions.value.of([ readOnlyExtensions.value.of([EditorState.readOnly.of(toValue(isReadOnly))]),
EditorState.readOnly.of(toValue(isReadOnly)),
EditorView.editable.of(!toValue(isReadOnly)),
]),
telemetryExtensions.value.of([]), telemetryExtensions.value.of([]),
EditorView.updateListener.of(onEditorUpdate), EditorView.updateListener.of(onEditorUpdate),
EditorView.focusChangeEffect.of((_, newHasFocus) => { EditorView.focusChangeEffect.of((_, newHasFocus) => {
@ -229,7 +226,6 @@ export const useExpressionEditor = ({
editor.value.dispatch({ editor.value.dispatch({
effects: readOnlyExtensions.value.reconfigure([ effects: readOnlyExtensions.value.reconfigure([
EditorState.readOnly.of(toValue(isReadOnly)), EditorState.readOnly.of(toValue(isReadOnly)),
EditorView.editable.of(!toValue(isReadOnly)),
]), ]),
}); });
} }

View file

@ -6,9 +6,11 @@ import { NodeConnectionType } from 'n8n-workflow';
export function useNodeConnections({ export function useNodeConnections({
inputs, inputs,
outputs, outputs,
connections,
}: { }: {
inputs: MaybeRef<CanvasElementData['inputs']>; inputs: MaybeRef<CanvasElementData['inputs']>;
outputs: MaybeRef<CanvasElementData['outputs']>; outputs: MaybeRef<CanvasElementData['outputs']>;
connections: MaybeRef<CanvasElementData['connections']>;
}) { }) {
/** /**
* Inputs * Inputs
@ -26,6 +28,10 @@ export function useNodeConnections({
nonMainInputs.value.filter((input) => input.required), nonMainInputs.value.filter((input) => input.required),
); );
const mainInputConnections = computed(
() => unref(connections).input[NodeConnectionType.Main] ?? [],
);
/** /**
* Outputs * Outputs
*/ */
@ -33,15 +39,22 @@ export function useNodeConnections({
const mainOutputs = computed(() => const mainOutputs = computed(() =>
unref(outputs).filter((output) => output.type === NodeConnectionType.Main), unref(outputs).filter((output) => output.type === NodeConnectionType.Main),
); );
const nonMainOutputs = computed(() => const nonMainOutputs = computed(() =>
unref(outputs).filter((output) => output.type !== NodeConnectionType.Main), unref(outputs).filter((output) => output.type !== NodeConnectionType.Main),
); );
const mainOutputConnections = computed(
() => unref(connections).output[NodeConnectionType.Main] ?? [],
);
return { return {
mainInputs, mainInputs,
nonMainInputs, nonMainInputs,
requiredNonMainInputs, requiredNonMainInputs,
mainInputConnections,
mainOutputs, mainOutputs,
nonMainOutputs, nonMainOutputs,
mainOutputConnections,
}; };
} }

View file

@ -218,7 +218,6 @@ export function resolveParameter<T = IDataObject>(
ExpressionEvaluatorProxy.setEvaluator( ExpressionEvaluatorProxy.setEvaluator(
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl', useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
); );
return workflow.expression.getParameterValue( return workflow.expression.getParameterValue(
parameter, parameter,
runExecutionData, runExecutionData,
@ -342,39 +341,6 @@ function connectionInputData(
} }
} }
const workflowsStore = useWorkflowsStore();
if (workflowsStore.shouldReplaceInputDataWithPinData) {
const parentPinData = parentNode.reduce<INodeExecutionData[]>((acc, parentNodeName, index) => {
const pinData = workflowsStore.pinDataByNodeName(parentNodeName);
if (pinData) {
acc.push({
json: pinData[0],
pairedItem: {
item: index,
input: 1,
},
});
}
return acc;
}, []);
if (parentPinData.length > 0) {
if (connectionInputData && connectionInputData.length > 0) {
parentPinData.forEach((parentPinDataEntry) => {
connectionInputData![0].json = {
...connectionInputData![0].json,
...parentPinDataEntry.json,
};
});
} else {
connectionInputData = parentPinData;
}
}
}
return connectionInputData; return connectionInputData;
} }

View file

@ -362,7 +362,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function getCurrentWorkflow(copyData?: boolean): Workflow { function getCurrentWorkflow(copyData?: boolean): Workflow {
const nodes = getNodes(); const nodes = getNodes();
const connections = allConnections.value; const connections = allConnections.value;
const cacheKey = JSON.stringify({ nodes, connections }); const cacheKey = JSON.stringify({ nodes, connections, pinData: pinnedWorkflowData.value });
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
return cachedWorkflow; return cachedWorkflow;
} }

View file

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
import type { ConnectionTypes, INodeTypeDescription } from 'n8n-workflow'; import type { ConnectionTypes, INodeConnections, INodeTypeDescription } from 'n8n-workflow';
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core'; import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
@ -25,8 +25,13 @@ export interface CanvasElementData {
id: INodeUi['id']; id: INodeUi['id'];
type: INodeUi['type']; type: INodeUi['type'];
typeVersion: INodeUi['typeVersion']; typeVersion: INodeUi['typeVersion'];
disabled: INodeUi['disabled'];
inputs: CanvasConnectionPort[]; inputs: CanvasConnectionPort[];
outputs: CanvasConnectionPort[]; outputs: CanvasConnectionPort[];
connections: {
input: INodeConnections;
output: INodeConnections;
};
renderType: 'default' | 'trigger' | 'configuration' | 'configurable'; renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
} }

View file

@ -95,6 +95,7 @@ const {
revertRenameNode, revertRenameNode,
setNodeActive, setNodeActive,
setNodeSelected, setNodeSelected,
toggleNodeDisabled,
deleteNode, deleteNode,
revertDeleteNode, revertDeleteNode,
addNodes, addNodes,
@ -363,6 +364,14 @@ function onRevertDeleteNode({ node }: { node: INodeUi }) {
revertDeleteNode(node); revertDeleteNode(node);
} }
function onToggleNodeDisabled(id: string) {
if (!checkIfEditingIsAllowed()) {
return;
}
toggleNodeDisabled(id);
}
function onSetNodeActive(id: string) { function onSetNodeActive(id: string) {
setNodeActive(id); setNodeActive(id);
} }
@ -680,6 +689,7 @@ onBeforeUnmount(() => {
@update:node:position="onUpdateNodePosition" @update:node:position="onUpdateNodePosition"
@update:node:active="onSetNodeActive" @update:node:active="onSetNodeActive"
@update:node:selected="onSetNodeSelected" @update:node:selected="onSetNodeSelected"
@update:node:enabled="onToggleNodeDisabled"
@delete:node="onDeleteNode" @delete:node="onDeleteNode"
@create:connection="onCreateConnection" @create:connection="onCreateConnection"
@delete:connection="onDeleteConnection" @delete:connection="onDeleteConnection"

View file

@ -3743,6 +3743,8 @@ export default defineComponent({
} }
}, },
async initView(): Promise<void> { async initView(): Promise<void> {
await this.loadCredentialsForWorkflow();
if (this.$route.params.action === 'workflowSave') { if (this.$route.params.action === 'workflowSave') {
// In case the workflow got saved we do not have to run init // In case the workflow got saved we do not have to run init
// as only the route changed but all the needed data is already loaded // as only the route changed but all the needed data is already loaded
@ -3820,7 +3822,7 @@ export default defineComponent({
await this.newWorkflow(); await this.newWorkflow();
} }
} }
await this.loadCredentials();
this.historyStore.reset(); this.historyStore.reset();
this.uiStore.nodeViewInitialized = true; this.uiStore.nodeViewInitialized = true;
document.addEventListener('keydown', this.keyDown); document.addEventListener('keydown', this.keyDown);
@ -4468,12 +4470,13 @@ export default defineComponent({
async loadCredentialTypes(): Promise<void> { async loadCredentialTypes(): Promise<void> {
await this.credentialsStore.fetchCredentialTypes(true); await this.credentialsStore.fetchCredentialTypes(true);
}, },
async loadCredentials(): Promise<void> { async loadCredentialsForWorkflow(): Promise<void> {
const workflow = this.workflowsStore.getWorkflowById(this.currentWorkflow); const workflow = this.workflowsStore.getWorkflowById(this.currentWorkflow);
const workflowId = workflow?.id ?? this.$route.params.name;
let options: { workflowId: string } | { projectId: string }; let options: { workflowId: string } | { projectId: string };
if (workflow) { if (workflowId) {
options = { workflowId: workflow.id }; options = { workflowId };
} else { } else {
const queryParam = const queryParam =
typeof this.$route.query?.projectId === 'string' typeof this.$route.query?.projectId === 'string'
@ -4801,7 +4804,7 @@ export default defineComponent({
await Promise.all([ await Promise.all([
this.loadVariables(), this.loadVariables(),
this.tagsStore.fetchAll(), this.tagsStore.fetchAll(),
this.loadCredentials(), this.loadCredentialsForWorkflow(),
]); ]);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "1.47.0", "version": "1.48.0",
"description": "CLI to simplify n8n credentials/node development", "description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -48,7 +48,7 @@
"@types/inquirer": "^6.5.0" "@types/inquirer": "^6.5.0"
}, },
"dependencies": { "dependencies": {
"@oclif/core": "3.26.6", "@oclif/core": "4.0.7",
"change-case": "^4.1.1", "change-case": "^4.1.1",
"fast-glob": "^3.2.5", "fast-glob": "^3.2.5",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",

View file

@ -37,7 +37,6 @@ export async function calApiRequest(
try { try {
return await this.helpers.httpRequestWithAuthentication.call(this, 'calApi', options); return await this.helpers.httpRequestWithAuthentication.call(this, 'calApi', options);
} catch (error) { } catch (error) {
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject); throw new NodeApiError(this.getNode(), error as JsonObject);
} }
} }

View file

@ -435,7 +435,6 @@ export class Coda implements INodeType {
}); });
continue; continue;
} }
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject); throw new NodeApiError(this.getNode(), error as JsonObject);
} }
} }
@ -803,7 +802,6 @@ export class Coda implements INodeType {
}); });
continue; continue;
} }
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject); throw new NodeApiError(this.getNode(), error as JsonObject);
} }
} }

View file

@ -67,6 +67,7 @@ const versionDescription: INodeTypeDescription = {
displayName: 'Email Trigger (IMAP)', displayName: 'Email Trigger (IMAP)',
name: 'emailReadImap', name: 'emailReadImap',
icon: 'fa:inbox', icon: 'fa:inbox',
iconColor: 'green',
group: ['trigger'], group: ['trigger'],
version: 2, version: 2,
description: 'Triggers the workflow when a new email is received', description: 'Triggers the workflow when a new email is received',

View file

@ -276,6 +276,11 @@ export class GetResponse implements INodeType {
if (updateFields.customFieldsUi) { if (updateFields.customFieldsUi) {
const customFieldValues = (updateFields.customFieldsUi as IDataObject) const customFieldValues = (updateFields.customFieldsUi as IDataObject)
.customFieldValues as IDataObject[]; .customFieldValues as IDataObject[];
customFieldValues.forEach((entry) => {
if (typeof entry.value === 'string') {
entry.value = entry.value.split(',').map((value) => value.trim());
}
});
if (customFieldValues) { if (customFieldValues) {
body.customFieldValues = customFieldValues; body.customFieldValues = customFieldValues;
delete body.customFieldsUi; delete body.customFieldsUi;

View file

@ -57,8 +57,6 @@ export async function googleApiRequest(
); );
} }
} catch (error) { } catch (error) {
if (error instanceof NodeApiError) throw error;
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') { if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
error.statusCode = '401'; error.statusCode = '401';
} }

View file

@ -1869,8 +1869,20 @@ export class HttpRequestV3 implements INodeType {
if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) { if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) {
responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString(); responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString();
} }
const error = new NodeApiError(this.getNode(), responseData as JsonObject, { itemIndex });
let error;
if (responseData?.reason instanceof NodeApiError) {
error = responseData.reason;
set(error, 'context.itemIndex', itemIndex);
} else {
const errorData = (
responseData.reason ? responseData.reason : responseData
) as JsonObject;
error = new NodeApiError(this.getNode(), errorData, { itemIndex });
}
set(error, 'context.request', sanitizedRequests[itemIndex]); set(error, 'context.request', sanitizedRequests[itemIndex]);
throw error; throw error;
} else { } else {
removeCircularRefs(responseData.reason as JsonObject); removeCircularRefs(responseData.reason as JsonObject);

View file

@ -27,7 +27,6 @@ export async function uprocApiRequest(
try { try {
return await this.helpers.httpRequestWithAuthentication.call(this, 'uprocApi', options); return await this.helpers.httpRequestWithAuthentication.call(this, 'uprocApi', options);
} catch (error) { } catch (error) {
if (error instanceof NodeApiError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject); throw new NodeApiError(this.getNode(), error as JsonObject);
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-nodes-base", "name": "n8n-nodes-base",
"version": "1.47.0", "version": "1.48.0",
"description": "Base nodes of n8n", "description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-workflow", "name": "n8n-workflow",
"version": "1.46.0", "version": "1.47.0",
"description": "Workflow base code of n8n", "description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",

View file

@ -310,7 +310,7 @@ type ICredentialHttpRequestNode = {
export interface ICredentialType { export interface ICredentialType {
name: string; name: string;
displayName: string; displayName: string;
icon?: Themed<Icon>; icon?: Icon;
iconUrl?: Themed<string>; iconUrl?: Themed<string>;
extends?: string[]; extends?: string[];
properties: INodeProperties[]; properties: INodeProperties[];
@ -1571,13 +1571,15 @@ export type NodeIconColor =
| 'azure' | 'azure'
| 'purple' | 'purple'
| 'crimson'; | 'crimson';
export type Icon = `fa:${string}` | `file:${string}` | `node:${string}`;
export type Themed<T> = T | { light: T; dark: T }; export type Themed<T> = T | { light: T; dark: T };
export type IconRef = `fa:${string}` | `node:${string}.${string}`;
export type IconFile = `file:${string}.png` | `file:${string}.svg`;
export type Icon = IconRef | Themed<IconFile>;
export interface INodeTypeBaseDescription { export interface INodeTypeBaseDescription {
displayName: string; displayName: string;
name: string; name: string;
icon?: Themed<Icon>; icon?: Icon;
iconColor?: NodeIconColor; iconColor?: NodeIconColor;
iconUrl?: Themed<string>; iconUrl?: Themed<string>;
badgeIconUrl?: Themed<string>; badgeIconUrl?: Themed<string>;

View file

@ -63,6 +63,18 @@ function dedupe<T>(arr: T[]): T[] {
return [...new Set(arr)]; return [...new Set(arr)];
} }
export interface WorkflowParameters {
id?: string;
name?: string;
nodes: INode[];
connections: IConnections;
active: boolean;
nodeTypes: INodeTypes;
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
}
export class Workflow { export class Workflow {
id: string; id: string;
@ -90,18 +102,7 @@ export class Workflow {
pinData?: IPinData; pinData?: IPinData;
// constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) { constructor(parameters: WorkflowParameters) {
constructor(parameters: {
id?: string;
name?: string;
nodes: INode[];
connections: IConnections;
active: boolean;
nodeTypes: INodeTypes;
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
}) {
this.id = parameters.id as string; // @tech_debt Ensure this is not optional this.id = parameters.id as string; // @tech_debt Ensure this is not optional
this.name = parameters.name; this.name = parameters.name;
this.nodeTypes = parameters.nodeTypes; this.nodeTypes = parameters.nodeTypes;
@ -251,16 +252,14 @@ export class Workflow {
* is fine. If there are issues it returns the issues * is fine. If there are issues it returns the issues
* which have been found for the different nodes. * which have been found for the different nodes.
* TODO: Does currently not check for credential issues! * TODO: Does currently not check for credential issues!
*
*/ */
checkReadyForExecution(inputData: { checkReadyForExecution(
startNode?: string; inputData: {
destinationNode?: string; startNode?: string;
pinDataNodeNames?: string[]; destinationNode?: string;
}): IWorkflowIssues | null { pinDataNodeNames?: string[];
let node: INode; } = {},
let nodeType: INodeType | undefined; ): IWorkflowIssues | null {
let nodeIssues: INodeIssues | null = null;
const workflowIssues: IWorkflowIssues = {}; const workflowIssues: IWorkflowIssues = {};
let checkNodes: string[] = []; let checkNodes: string[] = [];
@ -277,14 +276,14 @@ export class Workflow {
} }
for (const nodeName of checkNodes) { for (const nodeName of checkNodes) {
nodeIssues = null; let nodeIssues: INodeIssues | null = null;
node = this.nodes[nodeName]; const node = this.nodes[nodeName];
if (node.disabled === true) { if (node.disabled === true) {
continue; continue;
} }
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) { if (nodeType === undefined) {
// Node type is not known // Node type is not known
@ -415,7 +414,7 @@ export class Workflow {
* *
* @param {string} nodeName Name of the node to return the pinData of * @param {string} nodeName Name of the node to return the pinData of
*/ */
getPinDataOfNode(nodeName: string): IDataObject[] | undefined { getPinDataOfNode(nodeName: string): INodeExecutionData[] | undefined {
return this.pinData ? this.pinData[nodeName] : undefined; return this.pinData ? this.pinData[nodeName] : undefined;
} }

View file

@ -29,6 +29,7 @@ import { deepCopy } from './utils';
import { getGlobalState } from './GlobalState'; import { getGlobalState } from './GlobalState';
import { ApplicationError } from './errors/application.error'; import { ApplicationError } from './errors/application.error';
import { SCRIPTING_NODE_TYPES } from './Constants'; import { SCRIPTING_NODE_TYPES } from './Constants';
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator { export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
return Boolean( return Boolean(
@ -241,6 +242,29 @@ export class WorkflowDataProxy {
}); });
} }
private getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
shortSyntax = false,
}: {
nodeName: string;
branchIndex?: number;
runIndex?: number;
shortSyntax?: boolean;
}) {
try {
return this.getNodeExecutionData(nodeName, shortSyntax, branchIndex, runIndex);
} catch (e) {
const pinData = getPinDataIfManualExecution(this.workflow, nodeName, this.mode);
if (pinData) {
return pinData;
}
throw e;
}
}
/** /**
* Returns the node ExecutionData * Returns the node ExecutionData
* *
@ -283,7 +307,7 @@ export class WorkflowDataProxy {
if ( if (
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) && !that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName) !getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
) { ) {
throw new ExpressionError('Referenced node is unexecuted', { throw new ExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex, runIndex: that.runIndex,
@ -383,7 +407,10 @@ export class WorkflowDataProxy {
} }
if (['binary', 'data', 'json'].includes(name)) { if (['binary', 'data', 'json'].includes(name)) {
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined); const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
shortSyntax,
});
if (executionData.length === 0) { if (executionData.length === 0) {
if (that.workflow.getParentNodes(nodeName).length === 0) { if (that.workflow.getParentNodes(nodeName).length === 0) {
@ -619,11 +646,6 @@ export class WorkflowDataProxy {
getDataProxy(): IWorkflowDataProxyData { getDataProxy(): IWorkflowDataProxyData {
const that = this; const that = this;
const getNodeOutput = (nodeName: string, branchIndex: number, runIndex?: number) => {
runIndex = runIndex === undefined ? -1 : runIndex;
return that.getNodeExecutionData(nodeName, false, branchIndex, runIndex);
};
// replacing proxies with the actual data. // replacing proxies with the actual data.
const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => { const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => {
if (typeof data !== 'object' || typeof query !== 'string') { if (typeof data !== 'object' || typeof query !== 'string') {
@ -662,7 +684,7 @@ export class WorkflowDataProxy {
if (context?.nodeCause) { if (context?.nodeCause) {
const nodeName = context.nodeCause; const nodeName = context.nodeCause;
const pinData = this.workflow.getPinDataOfNode(nodeName); const pinData = getPinDataIfManualExecution(that.workflow, nodeName, that.mode);
if (pinData) { if (pinData) {
if (!context) { if (!context) {
@ -776,7 +798,8 @@ export class WorkflowDataProxy {
const previousNodeOutputData = const previousNodeOutputData =
taskData?.data?.main?.[previousNodeOutput] ?? taskData?.data?.main?.[previousNodeOutput] ??
(that.workflow.getPinDataOfNode(sourceData.previousNode) as INodeExecutionData[]); getPinDataIfManualExecution(that.workflow, sourceData.previousNode, that.mode) ??
[];
const source = taskData?.source ?? []; const source = taskData?.source ?? [];
if (pairedItem.item >= previousNodeOutputData.length) { if (pairedItem.item >= previousNodeOutputData.length) {
@ -897,10 +920,22 @@ export class WorkflowDataProxy {
} }
taskData = taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][ that.runExecutionData!.resultData.runData[sourceData.previousNode]?.[
sourceData?.previousNodeRun || 0 sourceData?.previousNodeRun || 0
]; ];
if (!taskData) {
const pinData = getPinDataIfManualExecution(
that.workflow,
sourceData.previousNode,
that.mode,
);
if (pinData) {
taskData = { data: { main: [pinData] }, startTime: 0, executionTime: 0, source: [] };
}
}
const previousNodeOutput = sourceData.previousNodeOutput || 0; const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) { if (previousNodeOutput >= taskData.data!.main.length) {
throw createExpressionError('Cant get data for expression', { throw createExpressionError('Cant get data for expression', {
@ -944,7 +979,7 @@ export class WorkflowDataProxy {
const ensureNodeExecutionData = () => { const ensureNodeExecutionData = () => {
if ( if (
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) && !that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName) !getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
) { ) {
throw createExpressionError('Referenced node is unexecuted', { throw createExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex, runIndex: that.runIndex,
@ -1009,8 +1044,20 @@ export class WorkflowDataProxy {
itemIndex = that.itemIndex; itemIndex = that.itemIndex;
} }
if (!that.connectionInputData.length) {
const pinnedData = getPinDataIfManualExecution(
that.workflow,
nodeName,
that.mode,
);
if (pinnedData) {
return pinnedData[itemIndex];
}
}
const executionData = that.connectionInputData; const executionData = that.connectionInputData;
const input = executionData[itemIndex]; const input = executionData?.[itemIndex];
if (!input) { if (!input) {
throw createExpressionError('Cant get data for expression', { throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%% field', messageTemplate: 'Cant get data for expression under %%PARAMETER%% field',
@ -1061,6 +1108,7 @@ export class WorkflowDataProxy {
} }
return pairedItemMethod; return pairedItemMethod;
} }
if (property === 'first') { if (property === 'first') {
ensureNodeExecutionData(); ensureNodeExecutionData();
return (branchIndex?: number, runIndex?: number) => { return (branchIndex?: number, runIndex?: number) => {
@ -1070,7 +1118,11 @@ export class WorkflowDataProxy {
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName) that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ?? ?.sourceIndex ??
0; 0;
const executionData = getNodeOutput(nodeName, branchIndex, runIndex); const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
});
if (executionData[0]) return executionData[0]; if (executionData[0]) return executionData[0];
return undefined; return undefined;
}; };
@ -1084,7 +1136,11 @@ export class WorkflowDataProxy {
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName) that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ?? ?.sourceIndex ??
0; 0;
const executionData = getNodeOutput(nodeName, branchIndex, runIndex); const executionData = that.getNodeExecutionOrPinnedData({
nodeName,
branchIndex,
runIndex,
});
if (!executionData.length) return undefined; if (!executionData.length) return undefined;
if (executionData[executionData.length - 1]) { if (executionData[executionData.length - 1]) {
return executionData[executionData.length - 1]; return executionData[executionData.length - 1];
@ -1101,7 +1157,7 @@ export class WorkflowDataProxy {
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName) that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
?.sourceIndex ?? ?.sourceIndex ??
0; 0;
return getNodeOutput(nodeName, branchIndex, runIndex); return that.getNodeExecutionOrPinnedData({ nodeName, branchIndex, runIndex });
}; };
} }
if (property === 'context') { if (property === 'context') {

View file

@ -0,0 +1,12 @@
import type { INodeExecutionData, Workflow, WorkflowExecuteMode } from '.';
export function getPinDataIfManualExecution(
workflow: Workflow,
nodeName: string,
mode: WorkflowExecuteMode,
): INodeExecutionData[] | undefined {
if (mode !== 'manual') {
return undefined;
}
return workflow.getPinDataOfNode(nodeName);
}

View file

@ -1,4 +1,4 @@
import { isTraversableObject } from '../../utils'; import { isTraversableObject, jsonParse } from '../../utils';
import type { IDataObject, INode, JsonObject } from '@/Interfaces'; import type { IDataObject, INode, JsonObject } from '@/Interfaces';
import { ExecutionBaseError } from './execution-base.error'; import { ExecutionBaseError } from './execution-base.error';
@ -81,9 +81,16 @@ export abstract class NodeError extends ExecutionBaseError {
traversalKeys: string[] = [], traversalKeys: string[] = [],
): string | null { ): string | null {
for (const key of potentialKeys) { for (const key of potentialKeys) {
const value = jsonError[key]; let value = jsonError[key];
if (value) { if (value) {
if (typeof value === 'string') return value; if (typeof value === 'string') {
try {
value = jsonParse(value);
} catch (error) {
return value as string;
}
if (typeof value === 'string') return value;
}
if (typeof value === 'number') return value.toString(); if (typeof value === 'number') return value.toString();
if (Array.isArray(value)) { if (Array.isArray(value)) {
const resolvedErrors: string[] = value const resolvedErrors: string[] = value

View file

@ -39,8 +39,9 @@ interface NodeApiErrorOptions extends NodeOperationErrorOptions {
/** /**
* Top-level properties where an error message can be found in an API response. * Top-level properties where an error message can be found in an API response.
* order is important, precedence is from top to bottom
*/ */
const ERROR_MESSAGE_PROPERTIES = [ const POSSIBLE_ERROR_MESSAGE_KEYS = [
'cause', 'cause',
'error', 'error',
'message', 'message',
@ -60,6 +61,7 @@ const ERROR_MESSAGE_PROPERTIES = [
'errorDescription', 'errorDescription',
'error_description', 'error_description',
'error_summary', 'error_summary',
'error_info',
'title', 'title',
'text', 'text',
'field', 'field',
@ -67,10 +69,15 @@ const ERROR_MESSAGE_PROPERTIES = [
'type', 'type',
]; ];
/**
* Properties where a nested object can be found in an API response.
*/
const POSSIBLE_NESTED_ERROR_OBJECT_KEYS = ['Error', 'error', 'err', 'response', 'body', 'data'];
/** /**
* Top-level properties where an HTTP error code can be found in an API response. * Top-level properties where an HTTP error code can be found in an API response.
*/ */
const ERROR_STATUS_PROPERTIES = [ const POSSIBLE_ERROR_STATUS_KEYS = [
'statusCode', 'statusCode',
'status', 'status',
'code', 'code',
@ -79,11 +86,6 @@ const ERROR_STATUS_PROPERTIES = [
'error_code', 'error_code',
]; ];
/**
* Properties where a nested object can be found in an API response.
*/
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
/** /**
* Descriptive messages for common HTTP status codes * Descriptive messages for common HTTP status codes
* this is used by NodeApiError class * this is used by NodeApiError class
@ -187,7 +189,11 @@ export class NodeApiError extends NodeError {
this.httpCode = errorResponse.httpCode as string; this.httpCode = errorResponse.httpCode as string;
} else { } else {
this.httpCode = this.httpCode =
this.findProperty(errorResponse, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES) ?? null; this.findProperty(
errorResponse,
POSSIBLE_ERROR_STATUS_KEYS,
POSSIBLE_NESTED_ERROR_OBJECT_KEYS,
) ?? null;
} }
this.level = level ?? 'warning'; this.level = level ?? 'warning';
@ -222,8 +228,8 @@ export class NodeApiError extends NodeError {
} else { } else {
this.description = this.findProperty( this.description = this.findProperty(
errorResponse, errorResponse,
ERROR_MESSAGE_PROPERTIES, POSSIBLE_ERROR_MESSAGE_KEYS,
ERROR_NESTING_PROPERTIES, POSSIBLE_NESTED_ERROR_OBJECT_KEYS,
); );
} }
} }
@ -266,8 +272,8 @@ export class NodeApiError extends NodeError {
const topLevelKey = Object.keys(result)[0]; const topLevelKey = Object.keys(result)[0];
this.description = this.findProperty( this.description = this.findProperty(
result[topLevelKey], result[topLevelKey],
ERROR_MESSAGE_PROPERTIES, POSSIBLE_ERROR_MESSAGE_KEYS,
['Error'].concat(ERROR_NESTING_PROPERTIES), POSSIBLE_NESTED_ERROR_OBJECT_KEYS,
); );
}); });
} }

View file

@ -571,6 +571,37 @@ const setNode: LoadedClass<INodeType> = {
}, },
}; };
const manualTriggerNode: LoadedClass<INodeType> = {
sourcePath: '',
type: {
description: {
displayName: 'Manual Trigger',
name: 'n8n-nodes-base.manualTrigger',
icon: 'fa:mouse-pointer',
group: ['trigger'],
version: 1,
description: 'Runs the flow on clicking a button in n8n',
eventTriggerDescription: '',
maxNodes: 1,
defaults: {
name: 'When clicking Test workflow',
color: '#909298',
},
inputs: [],
outputs: ['main'],
properties: [
{
displayName:
'This node is where the workflow execution starts (when you click the test button on the canvas).<br><br> <a data-action="showNodeCreator">Explore other ways to trigger your workflow</a> (e.g on a schedule, or a webhook)',
name: 'notice',
type: 'notice',
default: '',
},
],
},
},
};
export class NodeTypes implements INodeTypes { export class NodeTypes implements INodeTypes {
nodeTypes: INodeTypeData = { nodeTypes: INodeTypeData = {
'n8n-nodes-base.stickyNote': stickyNode, 'n8n-nodes-base.stickyNote': stickyNode,
@ -628,6 +659,7 @@ export class NodeTypes implements INodeTypes {
}, },
}, },
}, },
'n8n-nodes-base.manualTrigger': manualTriggerNode,
}; };
getByName(nodeType: string): INodeType | IVersionedNodeType { getByName(nodeType: string): INodeType | IVersionedNodeType {

View file

@ -1,3 +1,5 @@
import { mock } from 'jest-mock-extended';
import { NodeConnectionType } from '@/Interfaces';
import type { import type {
IBinaryKeyData, IBinaryKeyData,
IConnections, IConnections,
@ -5,10 +7,14 @@ import type {
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
INodeType,
INodeTypeDescription,
INodeTypes,
IRunExecutionData, IRunExecutionData,
NodeParameterValueType, NodeParameterValueType,
} from '@/Interfaces'; } from '@/Interfaces';
import { Workflow } from '@/Workflow'; import { Workflow, type WorkflowParameters } from '@/Workflow';
import * as NodeHelpers from '@/NodeHelpers';
process.env.TEST_VARIABLE_1 = 'valueEnvVariable1'; process.env.TEST_VARIABLE_1 = 'valueEnvVariable1';
@ -20,303 +26,426 @@ interface StubNode {
} }
describe('Workflow', () => { describe('Workflow', () => {
describe('renameNodeInParameterValue for expressions', () => { describe('checkIfWorkflowCanBeActivated', () => {
const tests = [ const disabledNode = mock<INode>({ type: 'triggerNode', disabled: true });
{ const unknownNode = mock<INode>({ type: 'unknownNode' });
description: 'do nothing if there is no expression', const noTriggersNode = mock<INode>({ type: 'noTriggersNode' });
input: { const pollNode = mock<INode>({ type: 'pollNode' });
currentName: 'Node1', const triggerNode = mock<INode>({ type: 'triggerNode' });
newName: 'Node1New', const webhookNode = mock<INode>({ type: 'webhookNode' });
parameters: {
const nodeTypes = mock<INodeTypes>();
nodeTypes.getByNameAndVersion.mockImplementation((type) => {
// TODO: getByNameAndVersion signature needs to be updated to allow returning undefined
if (type === 'unknownNode') return undefined as unknown as INodeType;
const partial: Partial<INodeType> = {
poll: undefined,
trigger: undefined,
webhook: undefined,
description: mock<INodeTypeDescription>({
properties: [],
}),
};
if (type === 'pollNode') partial.poll = jest.fn();
if (type === 'triggerNode') partial.trigger = jest.fn();
if (type === 'webhookNode') partial.webhook = jest.fn();
return mock(partial);
});
test.each([
['should skip disabled nodes', disabledNode, [], false],
['should skip nodes marked as ignored', triggerNode, ['triggerNode'], false],
['should skip unknown nodes', unknownNode, [], false],
['should skip nodes with no trigger method', noTriggersNode, [], false],
['should activate if poll method exists', pollNode, [], true],
['should activate if trigger method exists', triggerNode, [], true],
['should activate if webhook method exists', webhookNode, [], true],
])('%s', async (_, node, ignoredNodes, expected) => {
const params = mock<WorkflowParameters>({ nodeTypes });
params.nodes = [node];
const workflow = new Workflow(params);
expect(workflow.checkIfWorkflowCanBeActivated(ignoredNodes)).toBe(expected);
});
});
describe('checkReadyForExecution', () => {
const disabledNode = mock<INode>({ name: 'Disabled Node', disabled: true });
const startNode = mock<INode>({ name: 'Start Node' });
const unknownNode = mock<INode>({ name: 'Unknown Node', type: 'unknownNode' });
const nodeParamIssuesSpy = jest.spyOn(NodeHelpers, 'getNodeParametersIssues');
const nodeTypes = mock<INodeTypes>();
nodeTypes.getByNameAndVersion.mockImplementation((type) => {
// TODO: getByNameAndVersion signature needs to be updated to allow returning undefined
if (type === 'unknownNode') return undefined as unknown as INodeType;
return mock<INodeType>({
description: {
properties: [],
},
});
});
beforeEach(() => jest.clearAllMocks());
it('should return null if there are no nodes', () => {
const workflow = new Workflow({
nodes: [],
connections: {},
active: false,
nodeTypes,
});
const issues = workflow.checkReadyForExecution();
expect(issues).toBe(null);
expect(nodeTypes.getByNameAndVersion).not.toHaveBeenCalled();
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
});
it('should return null if there are no enabled nodes', () => {
const workflow = new Workflow({
nodes: [disabledNode],
connections: {},
active: false,
nodeTypes,
});
const issues = workflow.checkReadyForExecution({ startNode: disabledNode.name });
expect(issues).toBe(null);
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(1);
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
});
it('should return typeUnknown for unknown nodes', () => {
const workflow = new Workflow({
nodes: [unknownNode],
connections: {},
active: false,
nodeTypes,
});
const issues = workflow.checkReadyForExecution({ startNode: unknownNode.name });
expect(issues).toEqual({ [unknownNode.name]: { typeUnknown: true } });
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(2);
expect(nodeParamIssuesSpy).not.toHaveBeenCalled();
});
it('should return issues for regular nodes', () => {
const workflow = new Workflow({
nodes: [startNode],
connections: {},
active: false,
nodeTypes,
});
nodeParamIssuesSpy.mockReturnValue({ execution: false });
const issues = workflow.checkReadyForExecution({ startNode: startNode.name });
expect(issues).toEqual({ [startNode.name]: { execution: false } });
expect(nodeTypes.getByNameAndVersion).toHaveBeenCalledTimes(2);
expect(nodeParamIssuesSpy).toHaveBeenCalled();
});
});
describe('renameNodeInParameterValue', () => {
describe('for expressions', () => {
const tests = [
{
description: 'do nothing if there is no expression',
input: {
currentName: 'Node1',
newName: 'Node1New',
parameters: {
value1: 'value1Node1',
value2: 'value2Node1',
},
},
output: {
value1: 'value1Node1', value1: 'value1Node1',
value2: 'value2Node1', value2: 'value2Node1',
}, },
}, },
output: { {
value1: 'value1Node1', description: 'should work with dot notation',
value2: 'value2Node1', input: {
}, currentName: 'Node1',
}, newName: 'NewName',
{ parameters: {
description: 'should work with dot notation', value1: "={{$node.Node1.data.value1 + 'Node1'}}",
input: { value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
currentName: 'Node1', },
newName: 'NewName', },
parameters: { output: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}", value1: "={{$node.NewName.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}", value2: "={{$node.NewName.data.value2 + ' - ' + $node.NewName.data.value2}}",
}, },
}, },
output: { {
value1: "={{$node.NewName.data.value1 + 'Node1'}}", description: 'should work with ["nodeName"]',
value2: "={{$node.NewName.data.value2 + ' - ' + $node.NewName.data.value2}}", input: {
}, currentName: 'Node1',
}, newName: 'NewName',
{ parameters: {
description: 'should work with ["nodeName"]', value1: '={{$node["Node1"]["data"]["value1"] + \'Node1\'}}',
input: { value2:
currentName: 'Node1', '={{$node["Node1"]["data"]["value2"] + \' - \' + $node["Node1"]["data"]["value2"]}}',
newName: 'NewName', },
parameters: { },
value1: '={{$node["Node1"]["data"]["value1"] + \'Node1\'}}', output: {
value1: '={{$node["NewName"]["data"]["value1"] + \'Node1\'}}',
value2: value2:
'={{$node["Node1"]["data"]["value2"] + \' - \' + $node["Node1"]["data"]["value2"]}}', '={{$node["NewName"]["data"]["value2"] + \' - \' + $node["NewName"]["data"]["value2"]}}',
}, },
}, },
output: { {
value1: '={{$node["NewName"]["data"]["value1"] + \'Node1\'}}', description: 'should work with $("Node1")',
value2: input: {
'={{$node["NewName"]["data"]["value2"] + \' - \' + $node["NewName"]["data"]["value2"]}}', currentName: 'Node1',
}, newName: 'NewName',
}, parameters: {
{ value1: '={{$("Node1")["data"]["value1"] + \'Node1\'}}',
description: 'should work with $("Node1")', value2: '={{$("Node1")["data"]["value2"] + \' - \' + $("Node1")["data"]["value2"]}}',
input: { },
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: '={{$("Node1")["data"]["value1"] + \'Node1\'}}',
value2: '={{$("Node1")["data"]["value2"] + \' - \' + $("Node1")["data"]["value2"]}}',
}, },
}, output: {
output: { value1: '={{$("NewName")["data"]["value1"] + \'Node1\'}}',
value1: '={{$("NewName")["data"]["value1"] + \'Node1\'}}',
value2: '={{$("NewName")["data"]["value2"] + \' - \' + $("NewName")["data"]["value2"]}}',
},
},
{
description: 'should work with $items("Node1")',
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: '={{$items("Node1")["data"]["value1"] + \'Node1\'}}',
value2: value2:
'={{$items("Node1")["data"]["value2"] + \' - \' + $items("Node1")["data"]["value2"]}}', '={{$("NewName")["data"]["value2"] + \' - \' + $("NewName")["data"]["value2"]}}',
}, },
}, },
output: { {
value1: '={{$items("NewName")["data"]["value1"] + \'Node1\'}}', description: 'should work with $items("Node1")',
value2: input: {
'={{$items("NewName")["data"]["value2"] + \' - \' + $items("NewName")["data"]["value2"]}}', currentName: 'Node1',
}, newName: 'NewName',
}, parameters: {
{ value1: '={{$items("Node1")["data"]["value1"] + \'Node1\'}}',
description: 'should work with $items("Node1", 0, 1)', value2:
input: { '={{$items("Node1")["data"]["value2"] + \' - \' + $items("Node1")["data"]["value2"]}}',
currentName: 'Node1', },
newName: 'NewName', },
parameters: { output: {
value1: '={{$items("Node1", 0, 1)["data"]["value1"] + \'Node1\'}}', value1: '={{$items("NewName")["data"]["value1"] + \'Node1\'}}',
value2: value2:
'={{$items("Node1", 0, 1)["data"]["value2"] + \' - \' + $items("Node1", 0, 1)["data"]["value2"]}}', '={{$items("NewName")["data"]["value2"] + \' - \' + $items("NewName")["data"]["value2"]}}',
}, },
}, },
output: { {
value1: '={{$items("NewName", 0, 1)["data"]["value1"] + \'Node1\'}}', description: 'should work with $items("Node1", 0, 1)',
value2: input: {
'={{$items("NewName", 0, 1)["data"]["value2"] + \' - \' + $items("NewName", 0, 1)["data"]["value2"]}}', currentName: 'Node1',
}, newName: 'NewName',
}, parameters: {
{ value1: '={{$items("Node1", 0, 1)["data"]["value1"] + \'Node1\'}}',
description: 'should work with dot notation that contains space and special character', value2:
input: { '={{$items("Node1", 0, 1)["data"]["value2"] + \' - \' + $items("Node1", 0, 1)["data"]["value2"]}}',
currentName: 'Node1', },
newName: 'New $ Name',
parameters: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
}, },
}, output: {
output: { value1: '={{$items("NewName", 0, 1)["data"]["value1"] + \'Node1\'}}',
value1: '={{$node["New $ Name"].data.value1 + \'Node1\'}}',
value2:
'={{$node["New $ Name"].data.value2 + \' - \' + $node["New $ Name"].data.value2}}',
},
},
{
description: 'should work with dot notation that contains space and trailing $',
input: {
currentName: 'Node1',
newName: 'NewName$',
parameters: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
},
},
output: {
value1: '={{$node["NewName$"].data.value1 + \'Node1\'}}',
value2: '={{$node["NewName$"].data.value2 + \' - \' + $node["NewName$"].data.value2}}',
},
},
{
description: 'should work with dot notation that contains space and special character',
input: {
currentName: 'Node1',
newName: 'NewName $ $& $` $$$',
parameters: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
},
},
output: {
value1: '={{$node["NewName $ $& $` $$$"].data.value1 + \'Node1\'}}',
value2:
'={{$node["NewName $ $& $` $$$"].data.value2 + \' - \' + $node["NewName $ $& $` $$$"].data.value2}}',
},
},
{
description: 'should work with dot notation without trailing dot',
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: "={{$node.Node1 + 'Node1'}}",
value2: "={{$node.Node1 + ' - ' + $node.Node1}}",
},
},
output: {
value1: "={{$node.NewName + 'Node1'}}",
value2: "={{$node.NewName + ' - ' + $node.NewName}}",
},
},
{
description: "should work with ['nodeName']",
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: "={{$node['Node1']['data']['value1'] + 'Node1'}}",
value2: value2:
"={{$node['Node1']['data']['value2'] + ' - ' + $node['Node1']['data']['value2']}}", '={{$items("NewName", 0, 1)["data"]["value2"] + \' - \' + $items("NewName", 0, 1)["data"]["value2"]}}',
}, },
}, },
output: { {
value1: "={{$node['NewName']['data']['value1'] + 'Node1'}}", description: 'should work with dot notation that contains space and special character',
value2: input: {
"={{$node['NewName']['data']['value2'] + ' - ' + $node['NewName']['data']['value2']}}", currentName: 'Node1',
newName: 'New $ Name',
parameters: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
},
},
output: {
value1: '={{$node["New $ Name"].data.value1 + \'Node1\'}}',
value2:
'={{$node["New $ Name"].data.value2 + \' - \' + $node["New $ Name"].data.value2}}',
},
}, },
}, {
{ description: 'should work with dot notation that contains space and trailing $',
description: 'should work on lower levels', input: {
input: { currentName: 'Node1',
currentName: 'Node1', newName: 'NewName$',
newName: 'NewName', parameters: {
parameters: { value1: "={{$node.Node1.data.value1 + 'Node1'}}",
level1a: "={{$node.Node1.data.value1 + 'Node1'}}", value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
},
},
output: {
value1: '={{$node["NewName$"].data.value1 + \'Node1\'}}',
value2: '={{$node["NewName$"].data.value2 + \' - \' + $node["NewName$"].data.value2}}',
},
},
{
description: 'should work with dot notation that contains space and special character',
input: {
currentName: 'Node1',
newName: 'NewName $ $& $` $$$',
parameters: {
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
},
},
output: {
value1: '={{$node["NewName $ $& $` $$$"].data.value1 + \'Node1\'}}',
value2:
'={{$node["NewName $ $& $` $$$"].data.value2 + \' - \' + $node["NewName $ $& $` $$$"].data.value2}}',
},
},
{
description: 'should work with dot notation without trailing dot',
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: "={{$node.Node1 + 'Node1'}}",
value2: "={{$node.Node1 + ' - ' + $node.Node1}}",
},
},
output: {
value1: "={{$node.NewName + 'Node1'}}",
value2: "={{$node.NewName + ' - ' + $node.NewName}}",
},
},
{
description: "should work with ['nodeName']",
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
value1: "={{$node['Node1']['data']['value1'] + 'Node1'}}",
value2:
"={{$node['Node1']['data']['value2'] + ' - ' + $node['Node1']['data']['value2']}}",
},
},
output: {
value1: "={{$node['NewName']['data']['value1'] + 'Node1'}}",
value2:
"={{$node['NewName']['data']['value2'] + ' - ' + $node['NewName']['data']['value2']}}",
},
},
{
description: 'should work on lower levels',
input: {
currentName: 'Node1',
newName: 'NewName',
parameters: {
level1a: "={{$node.Node1.data.value1 + 'Node1'}}",
level1b: [
{
value2a: "={{$node.Node1.data.value1 + 'Node1'}}",
value2b: "={{$node.Node1.data.value1 + 'Node1'}}",
},
],
level1c: {
value2a: {
value3a: "={{$node.Node1.data.value1 + 'Node1'}}",
value3b: [
{
value4a: "={{$node.Node1.data.value1 + 'Node1'}}",
value4b: {
value5a: "={{$node.Node1.data.value1 + 'Node1'}}",
value5b: "={{$node.Node1.data.value1 + 'Node1'}}",
},
},
],
},
},
} as INodeParameters,
},
output: {
level1a: "={{$node.NewName.data.value1 + 'Node1'}}",
level1b: [ level1b: [
{ {
value2a: "={{$node.Node1.data.value1 + 'Node1'}}", value2a: "={{$node.NewName.data.value1 + 'Node1'}}",
value2b: "={{$node.Node1.data.value1 + 'Node1'}}", value2b: "={{$node.NewName.data.value1 + 'Node1'}}",
}, },
], ],
level1c: { level1c: {
value2a: { value2a: {
value3a: "={{$node.Node1.data.value1 + 'Node1'}}", value3a: "={{$node.NewName.data.value1 + 'Node1'}}",
value3b: [ value3b: [
{ {
value4a: "={{$node.Node1.data.value1 + 'Node1'}}", value4a: "={{$node.NewName.data.value1 + 'Node1'}}",
value4b: { value4b: {
value5a: "={{$node.Node1.data.value1 + 'Node1'}}", value5a: "={{$node.NewName.data.value1 + 'Node1'}}",
value5b: "={{$node.Node1.data.value1 + 'Node1'}}", value5b: "={{$node.NewName.data.value1 + 'Node1'}}",
}, },
}, },
], ],
}, },
}, },
} as INodeParameters,
},
output: {
level1a: "={{$node.NewName.data.value1 + 'Node1'}}",
level1b: [
{
value2a: "={{$node.NewName.data.value1 + 'Node1'}}",
value2b: "={{$node.NewName.data.value1 + 'Node1'}}",
},
],
level1c: {
value2a: {
value3a: "={{$node.NewName.data.value1 + 'Node1'}}",
value3b: [
{
value4a: "={{$node.NewName.data.value1 + 'Node1'}}",
value4b: {
value5a: "={{$node.NewName.data.value1 + 'Node1'}}",
value5b: "={{$node.NewName.data.value1 + 'Node1'}}",
},
},
],
},
}, },
}, },
}, ];
];
const nodeTypes = Helpers.NodeTypes(); const nodeTypes = Helpers.NodeTypes();
const workflow = new Workflow({ nodes: [], connections: {}, active: false, nodeTypes }); const workflow = new Workflow({ nodes: [], connections: {}, active: false, nodeTypes });
for (const testData of tests) { for (const testData of tests) {
test(testData.description, () => { test(testData.description, () => {
const result = workflow.renameNodeInParameterValue( const result = workflow.renameNodeInParameterValue(
testData.input.parameters, testData.input.parameters,
testData.input.currentName, testData.input.currentName,
testData.input.newName, testData.input.newName,
); );
expect(result).toEqual(testData.output); expect(result).toEqual(testData.output);
}); });
} }
});
describe('renameNodeInParameterValue for node with renamable content', () => {
const tests = [
{
description: "should work with $('name')",
input: {
currentName: 'Old',
newName: 'New',
parameters: { jsCode: "$('Old').first();" },
},
output: { jsCode: "$('New').first();" },
},
{
description: "should work with $node['name'] and $node.name",
input: {
currentName: 'Old',
newName: 'New',
parameters: { jsCode: "$node['Old'].first(); $node.Old.first();" },
},
output: { jsCode: "$node['New'].first(); $node.New.first();" },
},
{
description: 'should work with $items()',
input: {
currentName: 'Old',
newName: 'New',
parameters: { jsCode: "$items('Old').first();" },
},
output: { jsCode: "$items('New').first();" },
},
];
const workflow = new Workflow({
nodes: [],
connections: {},
active: false,
nodeTypes: Helpers.NodeTypes(),
}); });
for (const t of tests) { describe('for node with renamable content', () => {
test(t.description, () => { const tests = [
expect( {
workflow.renameNodeInParameterValue( description: "should work with $('name')",
t.input.parameters, input: {
t.input.currentName, currentName: 'Old',
t.input.newName, newName: 'New',
{ hasRenamableContent: true }, parameters: { jsCode: "$('Old').first();" },
), },
).toEqual(t.output); output: { jsCode: "$('New').first();" },
},
{
description: "should work with $node['name'] and $node.name",
input: {
currentName: 'Old',
newName: 'New',
parameters: { jsCode: "$node['Old'].first(); $node.Old.first();" },
},
output: { jsCode: "$node['New'].first(); $node.New.first();" },
},
{
description: 'should work with $items()',
input: {
currentName: 'Old',
newName: 'New',
parameters: { jsCode: "$items('Old').first();" },
},
output: { jsCode: "$items('New').first();" },
},
];
const workflow = new Workflow({
nodes: [],
connections: {},
active: false,
nodeTypes: Helpers.NodeTypes(),
}); });
}
for (const t of tests) {
test(t.description, () => {
expect(
workflow.renameNodeInParameterValue(
t.input.parameters,
t.input.currentName,
t.input.newName,
{ hasRenamableContent: true },
),
).toEqual(t.output);
});
}
});
}); });
describe('renameNode', () => { describe('renameNode', () => {
@ -377,7 +506,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2', node: 'Node2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -408,7 +537,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2', node: 'Node2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -444,7 +573,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2', node: 'Node2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -475,7 +604,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2New', node: 'Node2New',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -532,7 +661,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node3', node: 'Node3',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -543,12 +672,12 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node3', node: 'Node3',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
{ {
node: 'Node5', node: 'Node5',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -559,12 +688,12 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node4', node: 'Node4',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
{ {
node: 'Node5', node: 'Node5',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -616,7 +745,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node3New', node: 'Node3New',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -627,12 +756,12 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node3New', node: 'Node3New',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
{ {
node: 'Node5', node: 'Node5',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -643,12 +772,12 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node4', node: 'Node4',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
{ {
node: 'Node5', node: 'Node5',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1229,7 +1358,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2', node: 'Node2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1240,7 +1369,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node3', node: 'Node3',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1251,7 +1380,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Node2', node: 'Node2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1521,7 +1650,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set', node: 'Set',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1532,7 +1661,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set1', node: 'Set1',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1591,21 +1720,21 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set1', node: 'Set1',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
[ [
{ {
node: 'Set', node: 'Set',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
[ [
{ {
node: 'Set', node: 'Set',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1616,7 +1745,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set2', node: 'Set2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1627,7 +1756,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set2', node: 'Set2',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1691,7 +1820,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set', node: 'Set',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1699,7 +1828,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Switch', node: 'Switch',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1710,7 +1839,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set1', node: 'Set1',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1721,12 +1850,12 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set1', node: 'Set1',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
{ {
node: 'Switch', node: 'Switch',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],
@ -1737,7 +1866,7 @@ describe('Workflow', () => {
[ [
{ {
node: 'Set1', node: 'Set1',
type: 'main', type: NodeConnectionType.Main,
index: 0, index: 0,
}, },
], ],

View file

@ -1,4 +1,11 @@
import type { IExecuteData, INode, IRun, IWorkflowBase } from '@/Interfaces'; import type {
IExecuteData,
INode,
IPinData,
IRun,
IWorkflowBase,
WorkflowExecuteMode,
} from '@/Interfaces';
import { Workflow } from '@/Workflow'; import { Workflow } from '@/Workflow';
import { WorkflowDataProxy } from '@/WorkflowDataProxy'; import { WorkflowDataProxy } from '@/WorkflowDataProxy';
import { ExpressionError } from '@/errors/expression.error'; import { ExpressionError } from '@/errors/expression.error';
@ -13,7 +20,12 @@ const loadFixture = (fixture: string) => {
return { workflow, run }; return { workflow, run };
}; };
const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNode: string) => { const getProxyFromFixture = (
workflow: IWorkflowBase,
run: IRun | null,
activeNode: string,
mode?: WorkflowExecuteMode,
) => {
const taskData = run?.data.resultData.runData[activeNode]?.[0]; const taskData = run?.data.resultData.runData[activeNode]?.[0];
const lastNodeConnectionInputData = taskData?.data?.main[0]; const lastNodeConnectionInputData = taskData?.data?.main[0];
@ -29,6 +41,16 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
}; };
} }
let pinData: IPinData = {};
if (workflow.pinData) {
// json key is stored as part of workflow
// but dropped when copy/pasting
// so adding here to keep updating tests simple
for (let nodeName in workflow.pinData) {
pinData[nodeName] = workflow.pinData[nodeName].map((item) => ({ json: item }));
}
}
const dataProxy = new WorkflowDataProxy( const dataProxy = new WorkflowDataProxy(
new Workflow({ new Workflow({
id: '123', id: '123',
@ -37,6 +59,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
connections: workflow.connections, connections: workflow.connections,
active: false, active: false,
nodeTypes: Helpers.NodeTypes(), nodeTypes: Helpers.NodeTypes(),
pinData,
}), }),
run?.data ?? null, run?.data ?? null,
0, 0,
@ -44,7 +67,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
activeNode, activeNode,
lastNodeConnectionInputData ?? [], lastNodeConnectionInputData ?? [],
{}, {},
'manual', mode ?? 'integrated',
{}, {},
executeData, executeData,
); );
@ -323,4 +346,61 @@ describe('WorkflowDataProxy', () => {
} }
}); });
}); });
describe('Pinned data with manual execution', () => {
const fixture = loadFixture('pindata');
const proxy = getProxyFromFixture(fixture.workflow, null, 'NotPinnedSet1', 'manual');
test('$(PinnedSet).item.json', () => {
expect(proxy.$('PinnedSet').item.json).toEqual({ firstName: 'Joe', lastName: 'Smith' });
});
test('$(PinnedSet).item.json.firstName', () => {
expect(proxy.$('PinnedSet').item.json.firstName).toBe('Joe');
});
test('$(PinnedSet).pairedItem().json.firstName', () => {
expect(proxy.$('PinnedSet').pairedItem().json.firstName).toBe('Joe');
});
test('$(PinnedSet).first().json.firstName', () => {
expect(proxy.$('PinnedSet').first().json.firstName).toBe('Joe');
});
test('$(PinnedSet).first().json.firstName', () => {
expect(proxy.$('PinnedSet').first().json.firstName).toBe('Joe');
});
test('$(PinnedSet).last().json.firstName', () => {
expect(proxy.$('PinnedSet').last().json.firstName).toBe('Joan');
});
test('$(PinnedSet).all()[0].json.firstName', () => {
expect(proxy.$('PinnedSet').all()[0].json.firstName).toBe('Joe');
});
test('$(PinnedSet).all()[1].json.firstName', () => {
expect(proxy.$('PinnedSet').all()[1].json.firstName).toBe('Joan');
});
test('$(PinnedSet).all()[2]', () => {
expect(proxy.$('PinnedSet').all()[2]).toBeUndefined();
});
test('$(PinnedSet).itemMatching(0).json.firstName', () => {
expect(proxy.$('PinnedSet').itemMatching(0).json.firstName).toBe('Joe');
});
test('$(PinnedSet).itemMatching(1).json.firstName', () => {
expect(proxy.$('PinnedSet').itemMatching(1).json.firstName).toBe('Joan');
});
test('$(PinnedSet).itemMatching(2)', () => {
expect(proxy.$('PinnedSet').itemMatching(2)).toBeUndefined();
});
test('$node[PinnedSet].json.firstName', () => {
expect(proxy.$node.PinnedSet.json.firstName).toBe('Joe');
});
});
}); });

View file

@ -0,0 +1,7 @@
{
"data": {
"resultData": {
"runData": {}
}
}
}

View file

@ -0,0 +1,106 @@
{
"meta": {
"instanceId": "a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0"
},
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "3058c300-b377-41b7-9c90-a01372f9b581",
"name": "firstName",
"value": "Joe",
"type": "string"
},
{
"id": "bb871662-c23c-4234-ac0c-b78c279bbf34",
"name": "lastName",
"value": "Smith",
"type": "string"
}
]
},
"options": {}
},
"id": "baee2bf4-5083-4cbe-8e51-4eddcf859ef5",
"name": "PinnedSet",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1120,
380
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a482f1fd-4815-4da4-a733-7beafb43c500",
"name": "test",
"value": "={{ $('PinnedSet').all().json }}\n{{ $('PinnedSet').item.json.firstName }}\n{{ $('PinnedSet').first().json.firstName }}\n{{ $('PinnedSet').itemMatching(0).json.firstName }}\n{{ $('PinnedSet').itemMatching(1).json.firstName }}\n{{ $('PinnedSet').last().json.firstName }}\n{{ $('PinnedSet').all()[0].json.firstName }}\n{{ $('PinnedSet').all()[1].json.firstName }}\n\n{{ $input.first().json.firstName }}\n{{ $input.last().json.firstName }}\n{{ $input.item.json.firstName }}\n\n{{ $json.firstName }}\n{{ $data.firstName }}\n\n{{ $items()[0].json.firstName }}",
"type": "string"
}
]
},
"options": {}
},
"id": "2a543169-e2c1-4764-ac63-09534310b2b9",
"name": "NotPinnedSet1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1360,
380
]
},
{
"parameters": {},
"id": "f36672e5-8c87-480e-a5b8-de9da6b63192",
"name": "Start",
"type": "n8n-nodes-base.manualTrigger",
"position": [
920,
380
],
"typeVersion": 1
}
],
"connections": {
"PinnedSet": {
"main": [
[
{
"node": "NotPinnedSet1",
"type": "main",
"index": 0
}
]
]
},
"Start": {
"main": [
[
{
"node": "PinnedSet",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"PinnedSet": [
{
"firstName": "Joe",
"lastName": "Smith"
},
{
"firstName": "Joan",
"lastName": "Summers"
}
]
}
}

View file

@ -554,8 +554,8 @@ importers:
specifier: 2.13.0 specifier: 2.13.0
version: 2.13.0 version: 2.13.0
'@oclif/core': '@oclif/core':
specifier: 3.26.6 specifier: 4.0.7
version: 3.26.6 version: 4.0.7
'@pinecone-database/pinecone': '@pinecone-database/pinecone':
specifier: 2.1.0 specifier: 2.1.0
version: 2.1.0 version: 2.1.0
@ -1323,8 +1323,8 @@ importers:
packages/node-dev: packages/node-dev:
dependencies: dependencies:
'@oclif/core': '@oclif/core':
specifier: 3.26.6 specifier: 4.0.7
version: 3.26.6 version: 4.0.7
change-case: change-case:
specifier: ^4.1.1 specifier: ^4.1.1
version: 4.1.2 version: 4.1.2
@ -4165,8 +4165,8 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: This functionality has been moved to @npmcli/fs deprecated: This functionality has been moved to @npmcli/fs
'@oclif/core@3.26.6': '@oclif/core@4.0.7':
resolution: {integrity: sha512-+FiTw1IPuJTF9tSAlTsY8bGK4sgthehjz7c2SvYdgQncTkxI2xvUch/8QpjNYGLEmUneNygvYMRBax2KJcLccA==} resolution: {integrity: sha512-sU4Dx+RXCWAkrMw8tQFYAL6VfcHYKLPxVC9iKfgTXr4aDhcCssDwrbgpx0Di1dnNxvQlDGUhuCEInZuIY/nNfw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@one-ini/wasm@0.1.1': '@one-ini/wasm@0.1.1':
@ -5420,9 +5420,6 @@ packages:
'@types/cheerio@0.22.31': '@types/cheerio@0.22.31':
resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==}
'@types/cli-progress@3.11.5':
resolution: {integrity: sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==}
'@types/compression@1.0.1': '@types/compression@1.0.1':
resolution: {integrity: sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==} resolution: {integrity: sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==}
@ -6305,8 +6302,9 @@ packages:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'} engines: {node: '>=12'}
ansicolors@0.3.2: ansis@3.2.0:
resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} resolution: {integrity: sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg==}
engines: {node: '>=15'}
any-promise@1.3.0: any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@ -6789,10 +6787,6 @@ packages:
capital-case@1.0.4: capital-case@1.0.4:
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
cardinal@2.1.1:
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
hasBin: true
caseless@0.12.0: caseless@0.12.0:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
@ -6923,14 +6917,14 @@ packages:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
cli-progress@3.12.0:
resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==}
engines: {node: '>=4'}
cli-spinners@2.9.0: cli-spinners@2.9.0:
resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==} resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==}
engines: {node: '>=6'} engines: {node: '>=6'}
cli-spinners@2.9.2:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
cli-table3@0.6.3: cli-table3@0.6.3:
resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==}
engines: {node: 10.* || >= 12.*} engines: {node: 10.* || >= 12.*}
@ -7002,10 +6996,6 @@ packages:
color@3.2.1: color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
colord@2.9.3: colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@ -7434,6 +7424,15 @@ packages:
supports-color: supports-color:
optional: true optional: true
debug@4.3.5:
resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decamelize@1.2.0: decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -8839,10 +8838,6 @@ packages:
humanize-ms@1.2.1: humanize-ms@1.2.1:
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
hyperlinker@1.0.0:
resolution: {integrity: sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==}
engines: {node: '>=4'}
iconv-lite@0.4.24: iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -9852,6 +9847,10 @@ packages:
resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==}
engines: {node: '>=14'} engines: {node: '>=14'}
lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -10496,9 +10495,6 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
natural-orderby@2.0.3:
resolution: {integrity: sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==}
negotiator@0.6.3: negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -10721,10 +10717,6 @@ packages:
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
object-treeify@1.1.33:
resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==}
engines: {node: '>= 10'}
object.assign@4.1.4: object.assign@4.1.4:
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -10930,9 +10922,6 @@ packages:
pascal-case@3.1.2: pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
password-prompt@1.1.3:
resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==}
path-browserify@1.0.1: path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
@ -11750,9 +11739,6 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'} engines: {node: '>=8'}
redeyed@2.1.1:
resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
redis-errors@1.2.0: redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -12548,10 +12534,6 @@ packages:
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
supports-hyperlinks@2.3.0:
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
engines: {node: '>=8'}
supports-preserve-symlinks-flag@1.0.0: supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -17189,33 +17171,22 @@ snapshots:
rimraf: 3.0.2 rimraf: 3.0.2
optional: true optional: true
'@oclif/core@3.26.6': '@oclif/core@4.0.7':
dependencies: dependencies:
'@types/cli-progress': 3.11.5
ansi-escapes: 4.3.2 ansi-escapes: 4.3.2
ansi-styles: 4.3.0 ansis: 3.2.0
cardinal: 2.1.1
chalk: 4.1.2
clean-stack: 3.0.1 clean-stack: 3.0.1
cli-progress: 3.12.0 cli-spinners: 2.9.2
color: 4.2.3 debug: 4.3.5(supports-color@8.1.1)
debug: 4.3.4(supports-color@8.1.1)
ejs: 3.1.10 ejs: 3.1.10
get-package-type: 0.1.0 get-package-type: 0.1.0
globby: 11.1.0 globby: 11.1.0
hyperlinker: 1.0.0
indent-string: 4.0.0 indent-string: 4.0.0
is-wsl: 2.2.0 is-wsl: 2.2.0
js-yaml: 3.14.1 lilconfig: 3.1.2
minimatch: 9.0.4 minimatch: 9.0.4
natural-orderby: 2.0.3
object-treeify: 1.1.33
password-prompt: 1.1.3
slice-ansi: 4.0.0
string-width: 4.2.3 string-width: 4.2.3
strip-ansi: 6.0.1
supports-color: 8.1.1 supports-color: 8.1.1
supports-hyperlinks: 2.3.0
widest-line: 3.1.0 widest-line: 3.1.0
wordwrap: 1.0.0 wordwrap: 1.0.0
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
@ -19232,10 +19203,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 18.16.16 '@types/node': 18.16.16
'@types/cli-progress@3.11.5':
dependencies:
'@types/node': 18.16.16
'@types/compression@1.0.1': '@types/compression@1.0.1':
dependencies: dependencies:
'@types/express': 4.17.21 '@types/express': 4.17.21
@ -20315,7 +20282,7 @@ snapshots:
ansi-styles@6.2.1: {} ansi-styles@6.2.1: {}
ansicolors@0.3.2: {} ansis@3.2.0: {}
any-promise@1.3.0: {} any-promise@1.3.0: {}
@ -20898,11 +20865,6 @@ snapshots:
tslib: 2.6.2 tslib: 2.6.2
upper-case-first: 2.0.2 upper-case-first: 2.0.2
cardinal@2.1.1:
dependencies:
ansicolors: 0.3.2
redeyed: 2.1.1
caseless@0.12.0: {} caseless@0.12.0: {}
chai@4.3.10: chai@4.3.10:
@ -21058,12 +21020,10 @@ snapshots:
dependencies: dependencies:
restore-cursor: 3.1.0 restore-cursor: 3.1.0
cli-progress@3.12.0:
dependencies:
string-width: 4.2.3
cli-spinners@2.9.0: {} cli-spinners@2.9.0: {}
cli-spinners@2.9.2: {}
cli-table3@0.6.3: cli-table3@0.6.3:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -21156,11 +21116,6 @@ snapshots:
color-convert: 1.9.3 color-convert: 1.9.3
color-string: 1.9.1 color-string: 1.9.1
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
colord@2.9.3: {} colord@2.9.3: {}
colorette@1.4.0: {} colorette@1.4.0: {}
@ -21652,6 +21607,12 @@ snapshots:
optionalDependencies: optionalDependencies:
supports-color: 8.1.1 supports-color: 8.1.1
debug@4.3.5(supports-color@8.1.1):
dependencies:
ms: 2.1.2
optionalDependencies:
supports-color: 8.1.1
decamelize@1.2.0: {} decamelize@1.2.0: {}
decimal.js@10.4.3: {} decimal.js@10.4.3: {}
@ -23463,8 +23424,6 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
hyperlinker@1.0.0: {}
iconv-lite@0.4.24: iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@ -24675,6 +24634,8 @@ snapshots:
lilconfig@3.0.0: {} lilconfig@3.0.0: {}
lilconfig@3.1.2: {}
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
linkify-it@3.0.3: linkify-it@3.0.3:
@ -25334,8 +25295,6 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
natural-orderby@2.0.3: {}
negotiator@0.6.3: {} negotiator@0.6.3: {}
neo-async@2.6.2: {} neo-async@2.6.2: {}
@ -25576,8 +25535,6 @@ snapshots:
object-keys@1.1.1: {} object-keys@1.1.1: {}
object-treeify@1.1.33: {}
object.assign@4.1.4: object.assign@4.1.4:
dependencies: dependencies:
call-bind: 1.0.7 call-bind: 1.0.7
@ -25828,11 +25785,6 @@ snapshots:
no-case: 3.0.4 no-case: 3.0.4
tslib: 2.6.2 tslib: 2.6.2
password-prompt@1.1.3:
dependencies:
ansi-escapes: 4.3.2
cross-spawn: 7.0.3
path-browserify@1.0.1: {} path-browserify@1.0.1: {}
path-case@3.0.4: path-case@3.0.4:
@ -26686,10 +26638,6 @@ snapshots:
indent-string: 4.0.0 indent-string: 4.0.0
strip-indent: 3.0.0 strip-indent: 3.0.0
redeyed@2.1.1:
dependencies:
esprima: 4.0.1
redis-errors@1.2.0: {} redis-errors@1.2.0: {}
redis-parser@3.0.0: redis-parser@3.0.0:
@ -27699,11 +27647,6 @@ snapshots:
dependencies: dependencies:
has-flag: 4.0.0 has-flag: 4.0.0
supports-hyperlinks@2.3.0:
dependencies:
has-flag: 4.0.0
supports-color: 7.2.0
supports-preserve-symlinks-flag@1.0.0: {} supports-preserve-symlinks-flag@1.0.0: {}
svgo@3.1.0: svgo@3.1.0: