mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into seatable_node_rework2
This commit is contained in:
commit
9ae72e8e8e
26
.github/workflows/notify-pr-status.yml
vendored
Normal file
26
.github/workflows/notify-pr-status.yml
vendored
Normal 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) }} }'
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ n8n is split up in different modules which are all in a single mono repository.
|
|||
The most important directories:
|
||||
|
||||
- [/docker/image](/docker/images) - Dockerfiles to create n8n containers
|
||||
- [/docker/compose](/docker/compose) - Examples Docker Setups
|
||||
- [/packages](/packages) - The different n8n modules
|
||||
- [/packages/cli](/packages/cli) - CLI code to run front- & backend
|
||||
- [/packages/core](/packages/core) - Core code which handles workflow
|
||||
|
|
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal file
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal 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]',
|
||||
);
|
||||
});
|
||||
});
|
|
@ -329,7 +329,15 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
// Go to the first project and create a workflow
|
||||
projects.getMenuItems().first().click();
|
||||
workflowsPage.getters.workflowCards().should('not.have.length');
|
||||
|
||||
cy.intercept('GET', '/rest/credentials/for-workflow*').as('getCredentialsForWorkflow');
|
||||
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(NOTION_NODE_NAME, true, true);
|
||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||
|
@ -342,6 +350,10 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||
|
||||
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.nodeCredentialsSelect().first().click();
|
||||
getVisibleSelect()
|
||||
|
|
112
cypress/fixtures/Test_workflow_pinned_data_in_expressions.json
Normal file
112
cypress/fixtures/Test_workflow_pinned_data_in_expressions.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ export class NDV extends BasePage {
|
|||
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
||||
savePinnedDataButton: () =>
|
||||
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||
inputLabel: () => cy.getByTestId('input-label'),
|
||||
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
||||
outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'),
|
||||
outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"private": true,
|
||||
"homepage": "https://n8n.io",
|
||||
"engines": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm build:vite && pnpm run build:individual && npm run build:prepare",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
|
|
|
@ -120,6 +120,7 @@ export class Code implements INodeType {
|
|||
displayName: 'LangChain Code',
|
||||
name: 'code',
|
||||
icon: 'fa:code',
|
||||
iconColor: 'black',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'LangChain Code Node',
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-nodes-langchain",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -100,6 +100,7 @@
|
|||
"dist/nodes/tools/ToolCode/ToolCode.node.js",
|
||||
"dist/nodes/tools/ToolHttpRequest/ToolHttpRequest.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/ToolWolframAlpha/ToolWolframAlpha.node.js",
|
||||
"dist/nodes/tools/ToolWorkflow/ToolWorkflow.node.js",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"@n8n/permissions": "workspace:*",
|
||||
"@n8n/typeorm": "0.3.20-10",
|
||||
"@n8n_io/license-sdk": "2.13.0",
|
||||
"@oclif/core": "3.26.6",
|
||||
"@oclif/core": "4.0.7",
|
||||
"@pinecone-database/pinecone": "2.1.0",
|
||||
"@rudderstack/rudder-sdk-node": "2.0.7",
|
||||
"@sentry/integrations": "7.87.0",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'reflect-metadata';
|
||||
import { Container } from 'typedi';
|
||||
import { Command } from '@oclif/core';
|
||||
import { ExitError } from '@oclif/core/lib/errors';
|
||||
import { Command, Errors } from '@oclif/core';
|
||||
import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
|
||||
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
|
||||
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 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
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 { InvalidConcurrencyLimitError } from '@/errors/invalid-concurrency-limit.error';
|
||||
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,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { sleep } from 'n8n-workflow';
|
||||
import { ConcurrencyQueue } from '../concurrency-queue';
|
||||
|
||||
describe('ConcurrencyQueue', () => {
|
||||
|
@ -10,12 +11,12 @@ describe('ConcurrencyQueue', () => {
|
|||
const state: Record<string, 'started' | 'finished'> = {};
|
||||
|
||||
// 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 }) => {
|
||||
await queue.enqueue(item.executionId);
|
||||
state[item.executionId] = 'started';
|
||||
await sleep();
|
||||
await sleepSpy();
|
||||
queue.dequeue();
|
||||
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
|
||||
expect(sleep).toHaveBeenCalledTimes(0);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(0);
|
||||
expect(state).toEqual({});
|
||||
|
||||
// At T+0.4 seconds the first `testFn` has been called, but hasn't resolved
|
||||
await jest.advanceTimersByTimeAsync(400);
|
||||
expect(sleep).toHaveBeenCalledTimes(1);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(1);
|
||||
expect(state).toEqual({ 1: 'started' });
|
||||
|
||||
// At T+0.5 seconds the first promise has resolved, and the second one has stared
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
expect(sleep).toHaveBeenCalledTimes(2);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(2);
|
||||
expect(state).toEqual({ 1: 'finished', 2: 'started' });
|
||||
|
||||
// At T+1 seconds the first two promises have resolved, and the third one has stared
|
||||
await jest.advanceTimersByTimeAsync(500);
|
||||
expect(sleep).toHaveBeenCalledTimes(3);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(3);
|
||||
expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started' });
|
||||
|
||||
// If the fourth promise is removed, the fifth one is started in the next tick
|
||||
queue.remove('4');
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(sleep).toHaveBeenCalledTimes(4);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(4);
|
||||
expect(state).toEqual({ 1: 'finished', 2: 'finished', 3: 'started', 5: 'started' });
|
||||
|
||||
// at T+5 seconds, all but the fourth promise should be resolved
|
||||
await jest.advanceTimersByTimeAsync(4000);
|
||||
expect(sleep).toHaveBeenCalledTimes(4);
|
||||
expect(sleepSpy).toHaveBeenCalledTimes(4);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,9 @@ import type { WorkflowExecuteMode as ExecutionMode } from 'n8n-workflow';
|
|||
import type { IExecutingWorkflowData } from '@/Interfaces';
|
||||
import { Telemetry } from '@/telemetry';
|
||||
|
||||
export const CLOUD_TEMP_PRODUCTION_LIMIT = 999;
|
||||
export const CLOUD_TEMP_REPORTABLE_THRESHOLDS = [5, 10, 20, 50, 100, 200];
|
||||
|
||||
@Service()
|
||||
export class ConcurrencyControlService {
|
||||
private readonly isEnabled: boolean;
|
||||
|
@ -17,7 +20,9 @@ export class ConcurrencyControlService {
|
|||
|
||||
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(
|
||||
private readonly logger: Logger,
|
||||
|
@ -46,19 +51,17 @@ export class ConcurrencyControlService {
|
|||
|
||||
this.isEnabled = true;
|
||||
|
||||
this.productionQueue.on(
|
||||
'execution-throttled',
|
||||
async ({ executionId, capacity }: { executionId: string; capacity: number }) => {
|
||||
this.log('Execution throttled', { executionId });
|
||||
this.productionQueue.on('concurrency-check', ({ capacity }: { capacity: number }) => {
|
||||
if (this.shouldReport(capacity)) {
|
||||
void this.telemetry.track('User hit concurrency limit', {
|
||||
threshold: CLOUD_TEMP_PRODUCTION_LIMIT - capacity,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Temporary until base data for cloud plans is collected.
|
||||
*/
|
||||
if (this.shouldReport(capacity)) {
|
||||
await this.telemetry.track('User hit concurrency limit', { threshold: capacity });
|
||||
}
|
||||
},
|
||||
);
|
||||
this.productionQueue.on('execution-throttled', ({ executionId }: { executionId: string }) => {
|
||||
this.log('Execution throttled', { executionId });
|
||||
});
|
||||
|
||||
this.productionQueue.on('execution-released', async (executionId: string) => {
|
||||
this.log('Execution released', { executionId });
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Service } from 'typedi';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
@Service()
|
||||
export class ConcurrencyQueue extends EventEmitter {
|
||||
|
@ -15,14 +16,20 @@ export class ConcurrencyQueue extends EventEmitter {
|
|||
async enqueue(executionId: string) {
|
||||
this.capacity--;
|
||||
|
||||
this.debouncedEmit('concurrency-check', { capacity: this.capacity });
|
||||
|
||||
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
|
||||
return new Promise<void>((resolve) => this.queue.push({ executionId, resolve }));
|
||||
}
|
||||
}
|
||||
|
||||
get currentCapacity() {
|
||||
return this.capacity;
|
||||
}
|
||||
|
||||
dequeue() {
|
||||
this.capacity++;
|
||||
|
||||
|
@ -56,4 +63,9 @@ export class ConcurrencyQueue extends EventEmitter {
|
|||
|
||||
resolve();
|
||||
}
|
||||
|
||||
private debouncedEmit = debounce(
|
||||
(event: string, payload: object) => this.emit(event, payload),
|
||||
300,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "1.37.0",
|
||||
"version": "1.38.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
|
|
|
@ -79,16 +79,19 @@
|
|||
--color-sticky-border-7: var(--prim-gray-670);
|
||||
|
||||
// NodeIcon
|
||||
--color-node-icon-gray: var(--prim-gray-200);
|
||||
--color-node-icon-black: var(--prim-gray-70);
|
||||
--color-node-icon-blue: #766dfb;
|
||||
--color-node-icon-dark-blue: #6275ad;
|
||||
--color-node-icon-gray: var(--prim-gray-120);
|
||||
--color-node-icon-black: var(--prim-gray-10);
|
||||
--color-node-icon-blue: #898fff;
|
||||
--color-node-icon-light-blue: #58abff;
|
||||
--color-node-icon-dark-blue: #7ba7ff;
|
||||
--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-light-green: #20b69e;
|
||||
--color-node-icon-green: #38cb7a;
|
||||
--color-node-icon-dark-green: #86decc;
|
||||
--color-node-icon-purple: #9b6dd5;
|
||||
--color-node-icon-crimson: #d05876;
|
||||
--color-node-icon-crimson: #f188a2;
|
||||
|
||||
// Expressions, autocomplete, infobox
|
||||
--color-valid-resolvable-foreground: var(--prim-color-alt-a-tint-300);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -6,16 +6,20 @@ export function createCanvasNodeData({
|
|||
id = 'node',
|
||||
type = 'test',
|
||||
typeVersion = 1,
|
||||
disabled = false,
|
||||
inputs = [],
|
||||
outputs = [],
|
||||
connections = { input: {}, output: {} },
|
||||
renderType = 'default',
|
||||
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
typeVersion,
|
||||
disabled,
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
renderType,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -29,12 +29,14 @@ export const mockNode = ({
|
|||
name,
|
||||
type,
|
||||
position = [0, 0],
|
||||
disabled = false,
|
||||
}: {
|
||||
id?: INode['id'];
|
||||
name: INode['name'];
|
||||
type: INode['type'];
|
||||
position?: INode['position'];
|
||||
}) => mock<INode>({ id, name, type, position });
|
||||
disabled?: INode['disabled'];
|
||||
}) => mock<INode>({ id, name, type, position, disabled });
|
||||
|
||||
export const mockNodeTypeDescription = ({
|
||||
name,
|
||||
|
|
|
@ -53,7 +53,7 @@ export const codeNodeEditorTheme = ({
|
|||
},
|
||||
'.cm-content': {
|
||||
fontFamily: BASE_STYLING.fontFamily,
|
||||
caretColor: 'var(--color-code-caret)',
|
||||
caretColor: isReadOnly ? 'transparent' : 'var(--color-code-caret)',
|
||||
},
|
||||
'.cm-cursor, .cm-dropCursor': {
|
||||
borderLeftColor: 'var(--color-code-caret)',
|
||||
|
|
|
@ -613,6 +613,7 @@ function copySuccess() {
|
|||
}
|
||||
|
||||
&__header-description {
|
||||
overflow: hidden;
|
||||
padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s);
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
|
|
|
@ -97,7 +97,6 @@ onMounted(() => {
|
|||
extensions: [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.editable.of(false),
|
||||
EditorView.domEventHandlers({ scroll: forceParse }),
|
||||
...props.extensions,
|
||||
],
|
||||
|
|
|
@ -53,7 +53,7 @@ const extensions = computed(() => [
|
|||
),
|
||||
n8nLang(),
|
||||
n8nAutocompletion(),
|
||||
inputTheme({ rows: props.rows }),
|
||||
inputTheme({ isReadOnly: props.isReadOnly, rows: props.rows }),
|
||||
history(),
|
||||
expressionInputHandler(),
|
||||
EditorView.lineWrapping,
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import { EditorView } from '@codemirror/view';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
|
||||
const commonThemeProps = {
|
||||
const commonThemeProps = (isReadOnly = false) => ({
|
||||
'&.cm-focused': {
|
||||
outline: '0 !important',
|
||||
},
|
||||
'.cm-content': {
|
||||
fontFamily: 'var(--font-family-monospace)',
|
||||
color: 'var(--input-font-color, var(--color-text-dark))',
|
||||
caretColor: 'var(--color-code-caret)',
|
||||
caretColor: isReadOnly ? 'transparent' : 'var(--color-code-caret)',
|
||||
},
|
||||
'.cm-line': {
|
||||
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 theme = EditorView.theme({
|
||||
...commonThemeProps,
|
||||
...commonThemeProps(isReadOnly),
|
||||
'&': {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
minHeight: '30px',
|
||||
|
@ -54,7 +54,7 @@ export const inputTheme = ({ rows } = { rows: 5 }) => {
|
|||
|
||||
export const outputTheme = () => {
|
||||
const theme = EditorView.theme({
|
||||
...commonThemeProps,
|
||||
...commonThemeProps(true),
|
||||
'&': {
|
||||
maxHeight: '95px',
|
||||
width: '100%',
|
||||
|
|
|
@ -61,7 +61,6 @@ const extensions = computed(() => {
|
|||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(props.isReadOnly),
|
||||
EditorView.editable.of(!props.isReadOnly),
|
||||
codeNodeEditorTheme({
|
||||
isReadOnly: props.isReadOnly,
|
||||
maxHeight: props.fillParent ? '100%' : '40vh',
|
||||
|
|
|
@ -54,7 +54,6 @@ const extensions = computed(() => {
|
|||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(props.isReadOnly),
|
||||
EditorView.editable.of(!props.isReadOnly),
|
||||
codeNodeEditorTheme({
|
||||
isReadOnly: props.isReadOnly,
|
||||
maxHeight: props.fillParent ? '100%' : '40vh',
|
||||
|
|
|
@ -61,7 +61,6 @@ describe('ExpressionParameterInput', () => {
|
|||
await waitFor(() => {
|
||||
const editor = container.querySelector('.cm-content') as HTMLDivElement;
|
||||
expect(editor).toBeInTheDocument();
|
||||
expect(editor.getAttribute('contenteditable')).toEqual('false');
|
||||
expect(editor.getAttribute('aria-readonly')).toEqual('true');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ const emit = defineEmits<{
|
|||
'update:modelValue': [elements: CanvasElement[]];
|
||||
'update:node:position': [id: string, position: XYPosition];
|
||||
'update:node:active': [id: string];
|
||||
'update:node:enabled': [id: string];
|
||||
'update:node:selected': [id?: string];
|
||||
'delete:node': [id: string];
|
||||
'delete:connection': [connection: Connection];
|
||||
|
@ -66,6 +67,10 @@ function onSelectNode() {
|
|||
emit('update:node:selected', selectedNodeId);
|
||||
}
|
||||
|
||||
function onToggleNodeEnabled(id: string) {
|
||||
emit('update:node:enabled', id);
|
||||
}
|
||||
|
||||
function onDeleteNode(id: string) {
|
||||
emit('delete:node', id);
|
||||
}
|
||||
|
@ -126,6 +131,7 @@ function onClickPane(event: MouseEvent) {
|
|||
v-bind="canvasNodeProps"
|
||||
@delete="onDeleteNode"
|
||||
@select="onSelectNode"
|
||||
@toggle="onToggleNodeEnabled"
|
||||
@activate="onSetNodeActive"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -18,21 +18,24 @@ import type { NodeProps } from '@vue-flow/core';
|
|||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
select: [id: string, selected: boolean];
|
||||
toggle: [id: string];
|
||||
activate: [id: string];
|
||||
}>();
|
||||
|
||||
const props = defineProps<NodeProps<CanvasElementData>>();
|
||||
|
||||
const inputs = computed(() => props.data.inputs);
|
||||
const outputs = computed(() => props.data.outputs);
|
||||
|
||||
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({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => props.data.disabled);
|
||||
|
||||
const nodeType = computed(() => {
|
||||
return nodeTypesStore.getNodeType(props.data.type, props.data.typeVersion);
|
||||
});
|
||||
|
@ -107,6 +110,10 @@ function onDelete() {
|
|||
emit('delete', props.id);
|
||||
}
|
||||
|
||||
function onDisabledToggle() {
|
||||
emit('toggle', props.id);
|
||||
}
|
||||
|
||||
function onActivate() {
|
||||
emit('activate', props.id);
|
||||
}
|
||||
|
@ -143,12 +150,12 @@ function onActivate() {
|
|||
data-test-id="canvas-node-toolbar"
|
||||
:class="$style.canvasNodeToolbar"
|
||||
@delete="onDelete"
|
||||
@toggle="onDisabledToggle"
|
||||
/>
|
||||
|
||||
<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"-->
|
||||
<!-- :disabled="data.disabled"-->
|
||||
</CanvasNodeRenderer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -53,39 +53,39 @@ describe('CanvasNodeToolbar', () => {
|
|||
});
|
||||
|
||||
it('should call toggleDisableNode function when disable node button is clicked', async () => {
|
||||
const toggleDisableNode = vi.fn();
|
||||
const onToggleNode = vi.fn();
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
mocks: {
|
||||
toggleDisableNode,
|
||||
onToggleNode,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const deleteNode = vi.fn();
|
||||
const onDeleteNode = vi.fn();
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
mocks: {
|
||||
deleteNode,
|
||||
onDeleteNode,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
|
|
@ -3,12 +3,14 @@ import { computed, inject, useCssModule } from 'vue';
|
|||
import { CanvasNodeKey } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const emit = defineEmits(['delete']);
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
toggle: [];
|
||||
}>();
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const i18n = useI18n();
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const data = computed(() => node?.data.value);
|
||||
|
||||
|
@ -21,10 +23,11 @@ const nodeDisabledTitle = 'Test';
|
|||
// @TODO
|
||||
function executeNode() {}
|
||||
|
||||
// @TODO
|
||||
function toggleDisableNode() {}
|
||||
function onToggleNode() {
|
||||
emit('toggle');
|
||||
}
|
||||
|
||||
function deleteNode() {
|
||||
function onDeleteNode() {
|
||||
emit('delete');
|
||||
}
|
||||
|
||||
|
@ -53,7 +56,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
size="small"
|
||||
icon="power-off"
|
||||
:title="nodeDisabledTitle"
|
||||
@click="toggleDisableNode"
|
||||
@click="onToggleNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
data-test-id="delete-node-button"
|
||||
|
@ -62,7 +65,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
text
|
||||
icon="trash"
|
||||
:title="i18n.baseText('node.delete')"
|
||||
@click="deleteNode"
|
||||
@click="onDeleteNode"
|
||||
/>
|
||||
<N8nIconButton
|
||||
data-test-id="overflow-node-button"
|
||||
|
|
|
@ -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', () => {
|
||||
it('should adjust width css variable based on the number of non-main inputs', () => {
|
||||
const { getByText } = renderComponent({
|
||||
|
|
|
@ -2,24 +2,32 @@
|
|||
import { computed, inject, useCssModule } from 'vue';
|
||||
import { CanvasNodeKey, NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const label = computed(() => node?.label.value ?? '');
|
||||
|
||||
const inputs = computed(() => node?.data.value.inputs ?? []);
|
||||
const outputs = computed(() => node?.data.value.outputs ?? []);
|
||||
|
||||
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
|
||||
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: node?.selected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -46,7 +54,11 @@ const styles = computed(() => {
|
|||
<template>
|
||||
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
@ -54,12 +66,14 @@ const styles = computed(() => {
|
|||
.node {
|
||||
--configurable-node-min-input-count: 4;
|
||||
--configurable-node-input-width: 65px;
|
||||
|
||||
width: calc(
|
||||
--canvas-node--height: 100px;
|
||||
--canvas-node--width: calc(
|
||||
max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) *
|
||||
var(--configurable-node-input-width)
|
||||
);
|
||||
height: 100px;
|
||||
|
||||
width: var(--canvas-node--width);
|
||||
height: var(--canvas-node--height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -82,4 +96,8 @@ const styles = computed(() => {
|
|||
.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>
|
||||
|
|
|
@ -40,4 +40,34 @@ describe('CanvasNodeConfiguration', () => {
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, inject, useCssModule } from 'vue';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const label = computed(() => node?.label.value ?? '');
|
||||
|
||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: node?.selected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -19,14 +24,20 @@ const classes = computed(() => {
|
|||
<template>
|
||||
<div :class="classes" data-test-id="canvas-node-configuration">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.node {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
--canvas-node--width: 75px;
|
||||
--canvas-node--height: 75px;
|
||||
|
||||
width: var(--canvas-node--width);
|
||||
height: var(--canvas-node--height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -35,10 +46,6 @@ const classes = computed(() => {
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
||||
}
|
||||
|
||||
.label {
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
|
@ -48,4 +55,12 @@ const classes = computed(() => {
|
|||
min-width: 200px;
|
||||
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>
|
||||
|
|
|
@ -81,4 +81,34 @@ describe('CanvasNodeDefault', () => {
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,24 +2,32 @@
|
|||
import { computed, inject, useCssModule } from 'vue';
|
||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const label = computed(() => node?.label.value ?? '');
|
||||
|
||||
const inputs = computed(() => node?.data.value.inputs ?? []);
|
||||
const outputs = computed(() => node?.data.value.outputs ?? []);
|
||||
|
||||
const connections = computed(() => node?.data.value.connections ?? { input: {}, output: {} });
|
||||
const { mainOutputs } = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
|
||||
const isDisabled = computed(() => node?.data.value.disabled ?? false);
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: node?.selected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -33,14 +41,21 @@ const styles = computed(() => {
|
|||
<template>
|
||||
<div v-if="node" :class="classes" :style="styles" data-test-id="canvas-node-default">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.node {
|
||||
height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px);
|
||||
width: 100px;
|
||||
--canvas-node--height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px);
|
||||
--canvas-node--width: 100px;
|
||||
|
||||
height: var(--canvas-node--height);
|
||||
width: var(--canvas-node--width);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -62,4 +77,8 @@ const styles = computed(() => {
|
|||
.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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -7,7 +7,8 @@ import { mock } from 'vitest-mock-extended';
|
|||
|
||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||
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', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
|
@ -48,7 +49,11 @@ describe('useCanvasMapping', () => {
|
|||
|
||||
describe('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>({
|
||||
nodes: [manualTriggerNode],
|
||||
});
|
||||
|
@ -69,13 +74,75 @@ describe('useCanvasMapping', () => {
|
|||
id: manualTriggerNode.id,
|
||||
type: manualTriggerNode.type,
|
||||
typeVersion: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
connections: {
|
||||
input: {},
|
||||
output: {},
|
||||
},
|
||||
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', () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useNodeConnections } from '@/composables/useNodeConnections';
|
|||
import type { CanvasElementData } from '@/types';
|
||||
|
||||
describe('useNodeConnections', () => {
|
||||
const defaultConnections = { input: {}, output: {} };
|
||||
describe('mainInputs', () => {
|
||||
it('should return main inputs when provided with main inputs', () => {
|
||||
const inputs = ref<CanvasElementData['inputs']>([
|
||||
|
@ -14,7 +15,11 @@ describe('useNodeConnections', () => {
|
|||
]);
|
||||
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).toEqual(inputs.value.slice(0, 3));
|
||||
|
@ -30,7 +35,11 @@ describe('useNodeConnections', () => {
|
|||
]);
|
||||
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).toEqual(inputs.value.slice(1));
|
||||
|
@ -46,13 +55,42 @@ describe('useNodeConnections', () => {
|
|||
]);
|
||||
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).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', () => {
|
||||
it('should return main outputs when provided with main outputs', () => {
|
||||
const inputs = ref<CanvasElementData['inputs']>([]);
|
||||
|
@ -63,7 +101,11 @@ describe('useNodeConnections', () => {
|
|||
{ 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).toEqual(outputs.value.slice(0, 3));
|
||||
|
@ -79,10 +121,41 @@ describe('useNodeConnections', () => {
|
|||
{ 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).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],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -89,12 +89,20 @@ export function useCanvasMapping({
|
|||
|
||||
const elements = computed<CanvasElement[]>(() => [
|
||||
...workflow.value.nodes.map<CanvasElement>((node) => {
|
||||
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};
|
||||
const outputConnections = workflowObject.value.connectionsBySourceNode[node.name] ?? {};
|
||||
|
||||
const data: CanvasElementData = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
typeVersion: node.typeVersion,
|
||||
disabled: !!node.disabled,
|
||||
inputs: nodeInputsById.value[node.id] ?? [],
|
||||
outputs: nodeOutputsById.value[node.id] ?? [],
|
||||
connections: {
|
||||
input: inputConnections,
|
||||
output: outputConnections,
|
||||
},
|
||||
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
||||
};
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ import { useCredentialsStore } from '@/stores/credentials.store';
|
|||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
type AddNodeData = {
|
||||
name?: string;
|
||||
|
@ -78,6 +79,7 @@ export function useCanvasOperations({
|
|||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const telemetry = useTelemetry();
|
||||
const externalHooks = useExternalHooks();
|
||||
|
||||
|
@ -235,6 +237,18 @@ export function useCanvasOperations({
|
|||
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(
|
||||
nodes: AddedNodesAndConnections['nodes'],
|
||||
{
|
||||
|
@ -886,6 +900,7 @@ export function useCanvasOperations({
|
|||
setNodeActive,
|
||||
setNodeActiveByName,
|
||||
setNodeSelected,
|
||||
toggleNodeDisabled,
|
||||
renameNode,
|
||||
revertRenameNode,
|
||||
deleteNode,
|
||||
|
|
|
@ -185,10 +185,7 @@ export const useExpressionEditor = ({
|
|||
doc: toValue(editorValue),
|
||||
extensions: [
|
||||
customExtensions.value.of(toValue(extensions)),
|
||||
readOnlyExtensions.value.of([
|
||||
EditorState.readOnly.of(toValue(isReadOnly)),
|
||||
EditorView.editable.of(!toValue(isReadOnly)),
|
||||
]),
|
||||
readOnlyExtensions.value.of([EditorState.readOnly.of(toValue(isReadOnly))]),
|
||||
telemetryExtensions.value.of([]),
|
||||
EditorView.updateListener.of(onEditorUpdate),
|
||||
EditorView.focusChangeEffect.of((_, newHasFocus) => {
|
||||
|
@ -229,7 +226,6 @@ export const useExpressionEditor = ({
|
|||
editor.value.dispatch({
|
||||
effects: readOnlyExtensions.value.reconfigure([
|
||||
EditorState.readOnly.of(toValue(isReadOnly)),
|
||||
EditorView.editable.of(!toValue(isReadOnly)),
|
||||
]),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,9 +6,11 @@ import { NodeConnectionType } from 'n8n-workflow';
|
|||
export function useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
}: {
|
||||
inputs: MaybeRef<CanvasElementData['inputs']>;
|
||||
outputs: MaybeRef<CanvasElementData['outputs']>;
|
||||
connections: MaybeRef<CanvasElementData['connections']>;
|
||||
}) {
|
||||
/**
|
||||
* Inputs
|
||||
|
@ -26,6 +28,10 @@ export function useNodeConnections({
|
|||
nonMainInputs.value.filter((input) => input.required),
|
||||
);
|
||||
|
||||
const mainInputConnections = computed(
|
||||
() => unref(connections).input[NodeConnectionType.Main] ?? [],
|
||||
);
|
||||
|
||||
/**
|
||||
* Outputs
|
||||
*/
|
||||
|
@ -33,15 +39,22 @@ export function useNodeConnections({
|
|||
const mainOutputs = computed(() =>
|
||||
unref(outputs).filter((output) => output.type === NodeConnectionType.Main),
|
||||
);
|
||||
|
||||
const nonMainOutputs = computed(() =>
|
||||
unref(outputs).filter((output) => output.type !== NodeConnectionType.Main),
|
||||
);
|
||||
|
||||
const mainOutputConnections = computed(
|
||||
() => unref(connections).output[NodeConnectionType.Main] ?? [],
|
||||
);
|
||||
|
||||
return {
|
||||
mainInputs,
|
||||
nonMainInputs,
|
||||
requiredNonMainInputs,
|
||||
mainInputConnections,
|
||||
mainOutputs,
|
||||
nonMainOutputs,
|
||||
mainOutputConnections,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -218,7 +218,6 @@ export function resolveParameter<T = IDataObject>(
|
|||
ExpressionEvaluatorProxy.setEvaluator(
|
||||
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
|
||||
);
|
||||
|
||||
return workflow.expression.getParameterValue(
|
||||
parameter,
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -362,7 +362,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
function getCurrentWorkflow(copyData?: boolean): Workflow {
|
||||
const nodes = getNodes();
|
||||
const connections = allConnections.value;
|
||||
const cacheKey = JSON.stringify({ nodes, connections });
|
||||
const cacheKey = JSON.stringify({ nodes, connections, pinData: pinnedWorkflowData.value });
|
||||
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
|
||||
return cachedWorkflow;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* 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 { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
@ -25,8 +25,13 @@ export interface CanvasElementData {
|
|||
id: INodeUi['id'];
|
||||
type: INodeUi['type'];
|
||||
typeVersion: INodeUi['typeVersion'];
|
||||
disabled: INodeUi['disabled'];
|
||||
inputs: CanvasConnectionPort[];
|
||||
outputs: CanvasConnectionPort[];
|
||||
connections: {
|
||||
input: INodeConnections;
|
||||
output: INodeConnections;
|
||||
};
|
||||
renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
|
||||
}
|
||||
|
||||
|
|
|
@ -95,6 +95,7 @@ const {
|
|||
revertRenameNode,
|
||||
setNodeActive,
|
||||
setNodeSelected,
|
||||
toggleNodeDisabled,
|
||||
deleteNode,
|
||||
revertDeleteNode,
|
||||
addNodes,
|
||||
|
@ -363,6 +364,14 @@ function onRevertDeleteNode({ node }: { node: INodeUi }) {
|
|||
revertDeleteNode(node);
|
||||
}
|
||||
|
||||
function onToggleNodeDisabled(id: string) {
|
||||
if (!checkIfEditingIsAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleNodeDisabled(id);
|
||||
}
|
||||
|
||||
function onSetNodeActive(id: string) {
|
||||
setNodeActive(id);
|
||||
}
|
||||
|
@ -680,6 +689,7 @@ onBeforeUnmount(() => {
|
|||
@update:node:position="onUpdateNodePosition"
|
||||
@update:node:active="onSetNodeActive"
|
||||
@update:node:selected="onSetNodeSelected"
|
||||
@update:node:enabled="onToggleNodeDisabled"
|
||||
@delete:node="onDeleteNode"
|
||||
@create:connection="onCreateConnection"
|
||||
@delete:connection="onDeleteConnection"
|
||||
|
|
|
@ -3743,6 +3743,8 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
async initView(): Promise<void> {
|
||||
await this.loadCredentialsForWorkflow();
|
||||
|
||||
if (this.$route.params.action === 'workflowSave') {
|
||||
// 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
|
||||
|
@ -3820,7 +3822,7 @@ export default defineComponent({
|
|||
await this.newWorkflow();
|
||||
}
|
||||
}
|
||||
await this.loadCredentials();
|
||||
|
||||
this.historyStore.reset();
|
||||
this.uiStore.nodeViewInitialized = true;
|
||||
document.addEventListener('keydown', this.keyDown);
|
||||
|
@ -4468,12 +4470,13 @@ export default defineComponent({
|
|||
async loadCredentialTypes(): Promise<void> {
|
||||
await this.credentialsStore.fetchCredentialTypes(true);
|
||||
},
|
||||
async loadCredentials(): Promise<void> {
|
||||
async loadCredentialsForWorkflow(): Promise<void> {
|
||||
const workflow = this.workflowsStore.getWorkflowById(this.currentWorkflow);
|
||||
const workflowId = workflow?.id ?? this.$route.params.name;
|
||||
let options: { workflowId: string } | { projectId: string };
|
||||
|
||||
if (workflow) {
|
||||
options = { workflowId: workflow.id };
|
||||
if (workflowId) {
|
||||
options = { workflowId };
|
||||
} else {
|
||||
const queryParam =
|
||||
typeof this.$route.query?.projectId === 'string'
|
||||
|
@ -4801,7 +4804,7 @@ export default defineComponent({
|
|||
await Promise.all([
|
||||
this.loadVariables(),
|
||||
this.tagsStore.fetchAll(),
|
||||
this.loadCredentials(),
|
||||
this.loadCredentialsForWorkflow(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-node-dev",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -48,7 +48,7 @@
|
|||
"@types/inquirer": "^6.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/core": "3.26.6",
|
||||
"@oclif/core": "4.0.7",
|
||||
"change-case": "^4.1.1",
|
||||
"fast-glob": "^3.2.5",
|
||||
"inquirer": "^7.0.1",
|
||||
|
|
|
@ -37,7 +37,6 @@ export async function calApiRequest(
|
|||
try {
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, 'calApi', options);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError) throw error;
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -435,7 +435,6 @@ export class Coda implements INodeType {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
if (error instanceof NodeApiError) throw error;
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
@ -803,7 +802,6 @@ export class Coda implements INodeType {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
if (error instanceof NodeApiError) throw error;
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ const versionDescription: INodeTypeDescription = {
|
|||
displayName: 'Email Trigger (IMAP)',
|
||||
name: 'emailReadImap',
|
||||
icon: 'fa:inbox',
|
||||
iconColor: 'green',
|
||||
group: ['trigger'],
|
||||
version: 2,
|
||||
description: 'Triggers the workflow when a new email is received',
|
||||
|
|
|
@ -276,6 +276,11 @@ export class GetResponse implements INodeType {
|
|||
if (updateFields.customFieldsUi) {
|
||||
const customFieldValues = (updateFields.customFieldsUi as IDataObject)
|
||||
.customFieldValues as IDataObject[];
|
||||
customFieldValues.forEach((entry) => {
|
||||
if (typeof entry.value === 'string') {
|
||||
entry.value = entry.value.split(',').map((value) => value.trim());
|
||||
}
|
||||
});
|
||||
if (customFieldValues) {
|
||||
body.customFieldValues = customFieldValues;
|
||||
delete body.customFieldsUi;
|
||||
|
|
|
@ -57,8 +57,6 @@ export async function googleApiRequest(
|
|||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError) throw error;
|
||||
|
||||
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
|
||||
error.statusCode = '401';
|
||||
}
|
||||
|
|
|
@ -1869,8 +1869,20 @@ export class HttpRequestV3 implements INodeType {
|
|||
if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) {
|
||||
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]);
|
||||
|
||||
throw error;
|
||||
} else {
|
||||
removeCircularRefs(responseData.reason as JsonObject);
|
||||
|
|
|
@ -27,7 +27,6 @@ export async function uprocApiRequest(
|
|||
try {
|
||||
return await this.helpers.httpRequestWithAuthentication.call(this, 'uprocApi', options);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError) throw error;
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-base",
|
||||
"version": "1.47.0",
|
||||
"version": "1.48.0",
|
||||
"description": "Base nodes of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-workflow",
|
||||
"version": "1.46.0",
|
||||
"version": "1.47.0",
|
||||
"description": "Workflow base code of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -310,7 +310,7 @@ type ICredentialHttpRequestNode = {
|
|||
export interface ICredentialType {
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon?: Themed<Icon>;
|
||||
icon?: Icon;
|
||||
iconUrl?: Themed<string>;
|
||||
extends?: string[];
|
||||
properties: INodeProperties[];
|
||||
|
@ -1571,13 +1571,15 @@ export type NodeIconColor =
|
|||
| 'azure'
|
||||
| 'purple'
|
||||
| 'crimson';
|
||||
export type Icon = `fa:${string}` | `file:${string}` | `node:${string}`;
|
||||
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 {
|
||||
displayName: string;
|
||||
name: string;
|
||||
icon?: Themed<Icon>;
|
||||
icon?: Icon;
|
||||
iconColor?: NodeIconColor;
|
||||
iconUrl?: Themed<string>;
|
||||
badgeIconUrl?: Themed<string>;
|
||||
|
|
|
@ -63,6 +63,18 @@ function dedupe<T>(arr: T[]): T[] {
|
|||
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 {
|
||||
id: string;
|
||||
|
||||
|
@ -90,18 +102,7 @@ export class Workflow {
|
|||
|
||||
pinData?: IPinData;
|
||||
|
||||
// constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
|
||||
constructor(parameters: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
active: boolean;
|
||||
nodeTypes: INodeTypes;
|
||||
staticData?: IDataObject;
|
||||
settings?: IWorkflowSettings;
|
||||
pinData?: IPinData;
|
||||
}) {
|
||||
constructor(parameters: WorkflowParameters) {
|
||||
this.id = parameters.id as string; // @tech_debt Ensure this is not optional
|
||||
this.name = parameters.name;
|
||||
this.nodeTypes = parameters.nodeTypes;
|
||||
|
@ -251,16 +252,14 @@ export class Workflow {
|
|||
* is fine. If there are issues it returns the issues
|
||||
* which have been found for the different nodes.
|
||||
* TODO: Does currently not check for credential issues!
|
||||
*
|
||||
*/
|
||||
checkReadyForExecution(inputData: {
|
||||
startNode?: string;
|
||||
destinationNode?: string;
|
||||
pinDataNodeNames?: string[];
|
||||
}): IWorkflowIssues | null {
|
||||
let node: INode;
|
||||
let nodeType: INodeType | undefined;
|
||||
let nodeIssues: INodeIssues | null = null;
|
||||
checkReadyForExecution(
|
||||
inputData: {
|
||||
startNode?: string;
|
||||
destinationNode?: string;
|
||||
pinDataNodeNames?: string[];
|
||||
} = {},
|
||||
): IWorkflowIssues | null {
|
||||
const workflowIssues: IWorkflowIssues = {};
|
||||
|
||||
let checkNodes: string[] = [];
|
||||
|
@ -277,14 +276,14 @@ export class Workflow {
|
|||
}
|
||||
|
||||
for (const nodeName of checkNodes) {
|
||||
nodeIssues = null;
|
||||
node = this.nodes[nodeName];
|
||||
let nodeIssues: INodeIssues | null = null;
|
||||
const node = this.nodes[nodeName];
|
||||
|
||||
if (node.disabled === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
if (nodeType === undefined) {
|
||||
// Node type is not known
|
||||
|
@ -415,7 +414,7 @@ export class Workflow {
|
|||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import { deepCopy } from './utils';
|
|||
import { getGlobalState } from './GlobalState';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
import { SCRIPTING_NODE_TYPES } from './Constants';
|
||||
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
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
|
||||
*
|
||||
|
@ -283,7 +307,7 @@ export class WorkflowDataProxy {
|
|||
|
||||
if (
|
||||
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||
) {
|
||||
throw new ExpressionError('Referenced node is unexecuted', {
|
||||
runIndex: that.runIndex,
|
||||
|
@ -383,7 +407,10 @@ export class WorkflowDataProxy {
|
|||
}
|
||||
|
||||
if (['binary', 'data', 'json'].includes(name)) {
|
||||
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
|
||||
const executionData = that.getNodeExecutionOrPinnedData({
|
||||
nodeName,
|
||||
shortSyntax,
|
||||
});
|
||||
|
||||
if (executionData.length === 0) {
|
||||
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
||||
|
@ -619,11 +646,6 @@ export class WorkflowDataProxy {
|
|||
getDataProxy(): IWorkflowDataProxyData {
|
||||
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.
|
||||
const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => {
|
||||
if (typeof data !== 'object' || typeof query !== 'string') {
|
||||
|
@ -662,7 +684,7 @@ export class WorkflowDataProxy {
|
|||
|
||||
if (context?.nodeCause) {
|
||||
const nodeName = context.nodeCause;
|
||||
const pinData = this.workflow.getPinDataOfNode(nodeName);
|
||||
const pinData = getPinDataIfManualExecution(that.workflow, nodeName, that.mode);
|
||||
|
||||
if (pinData) {
|
||||
if (!context) {
|
||||
|
@ -776,7 +798,8 @@ export class WorkflowDataProxy {
|
|||
|
||||
const previousNodeOutputData =
|
||||
taskData?.data?.main?.[previousNodeOutput] ??
|
||||
(that.workflow.getPinDataOfNode(sourceData.previousNode) as INodeExecutionData[]);
|
||||
getPinDataIfManualExecution(that.workflow, sourceData.previousNode, that.mode) ??
|
||||
[];
|
||||
const source = taskData?.source ?? [];
|
||||
|
||||
if (pairedItem.item >= previousNodeOutputData.length) {
|
||||
|
@ -897,10 +920,22 @@ export class WorkflowDataProxy {
|
|||
}
|
||||
|
||||
taskData =
|
||||
that.runExecutionData!.resultData.runData[sourceData.previousNode][
|
||||
that.runExecutionData!.resultData.runData[sourceData.previousNode]?.[
|
||||
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;
|
||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
|
@ -944,7 +979,7 @@ export class WorkflowDataProxy {
|
|||
const ensureNodeExecutionData = () => {
|
||||
if (
|
||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||
) {
|
||||
throw createExpressionError('Referenced node is unexecuted', {
|
||||
runIndex: that.runIndex,
|
||||
|
@ -1009,8 +1044,20 @@ export class WorkflowDataProxy {
|
|||
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 input = executionData[itemIndex];
|
||||
const input = executionData?.[itemIndex];
|
||||
if (!input) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
|
@ -1061,6 +1108,7 @@ export class WorkflowDataProxy {
|
|||
}
|
||||
return pairedItemMethod;
|
||||
}
|
||||
|
||||
if (property === 'first') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
|
@ -1070,7 +1118,11 @@ export class WorkflowDataProxy {
|
|||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||
?.sourceIndex ??
|
||||
0;
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
const executionData = that.getNodeExecutionOrPinnedData({
|
||||
nodeName,
|
||||
branchIndex,
|
||||
runIndex,
|
||||
});
|
||||
if (executionData[0]) return executionData[0];
|
||||
return undefined;
|
||||
};
|
||||
|
@ -1084,7 +1136,11 @@ export class WorkflowDataProxy {
|
|||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||
?.sourceIndex ??
|
||||
0;
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
const executionData = that.getNodeExecutionOrPinnedData({
|
||||
nodeName,
|
||||
branchIndex,
|
||||
runIndex,
|
||||
});
|
||||
if (!executionData.length) return undefined;
|
||||
if (executionData[executionData.length - 1]) {
|
||||
return executionData[executionData.length - 1];
|
||||
|
@ -1101,7 +1157,7 @@ export class WorkflowDataProxy {
|
|||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||
?.sourceIndex ??
|
||||
0;
|
||||
return getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
return that.getNodeExecutionOrPinnedData({ nodeName, branchIndex, runIndex });
|
||||
};
|
||||
}
|
||||
if (property === 'context') {
|
||||
|
|
12
packages/workflow/src/WorkflowDataProxyHelpers.ts
Normal file
12
packages/workflow/src/WorkflowDataProxyHelpers.ts
Normal 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);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { isTraversableObject } from '../../utils';
|
||||
import { isTraversableObject, jsonParse } from '../../utils';
|
||||
import type { IDataObject, INode, JsonObject } from '@/Interfaces';
|
||||
import { ExecutionBaseError } from './execution-base.error';
|
||||
|
||||
|
@ -81,9 +81,16 @@ export abstract class NodeError extends ExecutionBaseError {
|
|||
traversalKeys: string[] = [],
|
||||
): string | null {
|
||||
for (const key of potentialKeys) {
|
||||
const value = jsonError[key];
|
||||
let value = jsonError[key];
|
||||
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 (Array.isArray(value)) {
|
||||
const resolvedErrors: string[] = value
|
||||
|
|
|
@ -39,8 +39,9 @@ interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
|||
|
||||
/**
|
||||
* 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',
|
||||
'error',
|
||||
'message',
|
||||
|
@ -60,6 +61,7 @@ const ERROR_MESSAGE_PROPERTIES = [
|
|||
'errorDescription',
|
||||
'error_description',
|
||||
'error_summary',
|
||||
'error_info',
|
||||
'title',
|
||||
'text',
|
||||
'field',
|
||||
|
@ -67,10 +69,15 @@ const ERROR_MESSAGE_PROPERTIES = [
|
|||
'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.
|
||||
*/
|
||||
const ERROR_STATUS_PROPERTIES = [
|
||||
const POSSIBLE_ERROR_STATUS_KEYS = [
|
||||
'statusCode',
|
||||
'status',
|
||||
'code',
|
||||
|
@ -79,11 +86,6 @@ const ERROR_STATUS_PROPERTIES = [
|
|||
'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
|
||||
* this is used by NodeApiError class
|
||||
|
@ -187,7 +189,11 @@ export class NodeApiError extends NodeError {
|
|||
this.httpCode = errorResponse.httpCode as string;
|
||||
} else {
|
||||
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';
|
||||
|
@ -222,8 +228,8 @@ export class NodeApiError extends NodeError {
|
|||
} else {
|
||||
this.description = this.findProperty(
|
||||
errorResponse,
|
||||
ERROR_MESSAGE_PROPERTIES,
|
||||
ERROR_NESTING_PROPERTIES,
|
||||
POSSIBLE_ERROR_MESSAGE_KEYS,
|
||||
POSSIBLE_NESTED_ERROR_OBJECT_KEYS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -266,8 +272,8 @@ export class NodeApiError extends NodeError {
|
|||
const topLevelKey = Object.keys(result)[0];
|
||||
this.description = this.findProperty(
|
||||
result[topLevelKey],
|
||||
ERROR_MESSAGE_PROPERTIES,
|
||||
['Error'].concat(ERROR_NESTING_PROPERTIES),
|
||||
POSSIBLE_ERROR_MESSAGE_KEYS,
|
||||
POSSIBLE_NESTED_ERROR_OBJECT_KEYS,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.stickyNote': stickyNode,
|
||||
|
@ -628,6 +659,7 @@ export class NodeTypes implements INodeTypes {
|
|||
},
|
||||
},
|
||||
},
|
||||
'n8n-nodes-base.manualTrigger': manualTriggerNode,
|
||||
};
|
||||
|
||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { mock } from 'jest-mock-extended';
|
||||
import { NodeConnectionType } from '@/Interfaces';
|
||||
import type {
|
||||
IBinaryKeyData,
|
||||
IConnections,
|
||||
|
@ -5,10 +7,14 @@ import type {
|
|||
INode,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypes,
|
||||
IRunExecutionData,
|
||||
NodeParameterValueType,
|
||||
} from '@/Interfaces';
|
||||
import { Workflow } from '@/Workflow';
|
||||
import { Workflow, type WorkflowParameters } from '@/Workflow';
|
||||
import * as NodeHelpers from '@/NodeHelpers';
|
||||
|
||||
process.env.TEST_VARIABLE_1 = 'valueEnvVariable1';
|
||||
|
||||
|
@ -20,303 +26,426 @@ interface StubNode {
|
|||
}
|
||||
|
||||
describe('Workflow', () => {
|
||||
describe('renameNodeInParameterValue for expressions', () => {
|
||||
const tests = [
|
||||
{
|
||||
description: 'do nothing if there is no expression',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'Node1New',
|
||||
parameters: {
|
||||
describe('checkIfWorkflowCanBeActivated', () => {
|
||||
const disabledNode = mock<INode>({ type: 'triggerNode', disabled: true });
|
||||
const unknownNode = mock<INode>({ type: 'unknownNode' });
|
||||
const noTriggersNode = mock<INode>({ type: 'noTriggersNode' });
|
||||
const pollNode = mock<INode>({ type: 'pollNode' });
|
||||
const triggerNode = mock<INode>({ type: 'triggerNode' });
|
||||
const webhookNode = mock<INode>({ type: 'webhookNode' });
|
||||
|
||||
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',
|
||||
value2: 'value2Node1',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
value1: 'value1Node1',
|
||||
value2: 'value2Node1',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should work with dot notation',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
|
||||
{
|
||||
description: 'should work with dot notation',
|
||||
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}}",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
value1: "={{$node.NewName.data.value1 + 'Node1'}}",
|
||||
value2: "={{$node.NewName.data.value2 + ' - ' + $node.NewName.data.value2}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should work with ["nodeName"]',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$node["Node1"]["data"]["value1"] + \'Node1\'}}',
|
||||
{
|
||||
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["Node1"]["data"]["value2"] + \' - \' + $node["Node1"]["data"]["value2"]}}',
|
||||
'={{$node["NewName"]["data"]["value2"] + \' - \' + $node["NewName"]["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
value1: '={{$node["NewName"]["data"]["value1"] + \'Node1\'}}',
|
||||
value2:
|
||||
'={{$node["NewName"]["data"]["value2"] + \' - \' + $node["NewName"]["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should work with $("Node1")',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$("Node1")["data"]["value1"] + \'Node1\'}}',
|
||||
value2: '={{$("Node1")["data"]["value2"] + \' - \' + $("Node1")["data"]["value2"]}}',
|
||||
{
|
||||
description: 'should work with $("Node1")',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$("Node1")["data"]["value1"] + \'Node1\'}}',
|
||||
value2: '={{$("Node1")["data"]["value2"] + \' - \' + $("Node1")["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
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\'}}',
|
||||
output: {
|
||||
value1: '={{$("NewName")["data"]["value1"] + \'Node1\'}}',
|
||||
value2:
|
||||
'={{$items("Node1")["data"]["value2"] + \' - \' + $items("Node1")["data"]["value2"]}}',
|
||||
'={{$("NewName")["data"]["value2"] + \' - \' + $("NewName")["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
value1: '={{$items("NewName")["data"]["value1"] + \'Node1\'}}',
|
||||
value2:
|
||||
'={{$items("NewName")["data"]["value2"] + \' - \' + $items("NewName")["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should work with $items("Node1", 0, 1)',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$items("Node1", 0, 1)["data"]["value1"] + \'Node1\'}}',
|
||||
{
|
||||
description: 'should work with $items("Node1")',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$items("Node1")["data"]["value1"] + \'Node1\'}}',
|
||||
value2:
|
||||
'={{$items("Node1")["data"]["value2"] + \' - \' + $items("Node1")["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
value1: '={{$items("NewName")["data"]["value1"] + \'Node1\'}}',
|
||||
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\'}}',
|
||||
value2:
|
||||
'={{$items("NewName", 0, 1)["data"]["value2"] + \' - \' + $items("NewName", 0, 1)["data"]["value2"]}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'should work with dot notation that contains space and special character',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'New $ Name',
|
||||
parameters: {
|
||||
value1: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value2: "={{$node.Node1.data.value2 + ' - ' + $node.Node1.data.value2}}",
|
||||
{
|
||||
description: 'should work with $items("Node1", 0, 1)',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
value1: '={{$items("Node1", 0, 1)["data"]["value1"] + \'Node1\'}}',
|
||||
value2:
|
||||
'={{$items("Node1", 0, 1)["data"]["value2"] + \' - \' + $items("Node1", 0, 1)["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 $',
|
||||
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'}}",
|
||||
output: {
|
||||
value1: '={{$items("NewName", 0, 1)["data"]["value1"] + \'Node1\'}}',
|
||||
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'}}",
|
||||
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: '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 on lower levels',
|
||||
input: {
|
||||
currentName: 'Node1',
|
||||
newName: 'NewName',
|
||||
parameters: {
|
||||
level1a: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
{
|
||||
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:
|
||||
"={{$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: [
|
||||
{
|
||||
value2a: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value2b: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value2a: "={{$node.NewName.data.value1 + 'Node1'}}",
|
||||
value2b: "={{$node.NewName.data.value1 + 'Node1'}}",
|
||||
},
|
||||
],
|
||||
level1c: {
|
||||
value2a: {
|
||||
value3a: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value3a: "={{$node.NewName.data.value1 + 'Node1'}}",
|
||||
value3b: [
|
||||
{
|
||||
value4a: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value4a: "={{$node.NewName.data.value1 + 'Node1'}}",
|
||||
value4b: {
|
||||
value5a: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value5b: "={{$node.Node1.data.value1 + 'Node1'}}",
|
||||
value5a: "={{$node.NewName.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 workflow = new Workflow({ nodes: [], connections: {}, active: false, nodeTypes });
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({ nodes: [], connections: {}, active: false, nodeTypes });
|
||||
|
||||
for (const testData of tests) {
|
||||
test(testData.description, () => {
|
||||
const result = workflow.renameNodeInParameterValue(
|
||||
testData.input.parameters,
|
||||
testData.input.currentName,
|
||||
testData.input.newName,
|
||||
);
|
||||
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 testData of tests) {
|
||||
test(testData.description, () => {
|
||||
const result = workflow.renameNodeInParameterValue(
|
||||
testData.input.parameters,
|
||||
testData.input.currentName,
|
||||
testData.input.newName,
|
||||
);
|
||||
expect(result).toEqual(testData.output);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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('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) {
|
||||
test(t.description, () => {
|
||||
expect(
|
||||
workflow.renameNodeInParameterValue(
|
||||
t.input.parameters,
|
||||
t.input.currentName,
|
||||
t.input.newName,
|
||||
{ hasRenamableContent: true },
|
||||
),
|
||||
).toEqual(t.output);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameNode', () => {
|
||||
|
@ -377,7 +506,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -408,7 +537,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -444,7 +573,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -475,7 +604,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2New',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -532,7 +661,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node3',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -543,12 +672,12 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node3',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
node: 'Node5',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -559,12 +688,12 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node4',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
node: 'Node5',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -616,7 +745,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node3New',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -627,12 +756,12 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node3New',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
node: 'Node5',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -643,12 +772,12 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node4',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
node: 'Node5',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1229,7 +1358,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1240,7 +1369,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node3',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1251,7 +1380,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Node2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1521,7 +1650,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1532,7 +1661,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set1',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1591,21 +1720,21 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set1',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
node: 'Set',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
node: 'Set',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1616,7 +1745,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1627,7 +1756,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set2',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1691,7 +1820,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1699,7 +1828,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Switch',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1710,7 +1839,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set1',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1721,12 +1850,12 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set1',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
{
|
||||
node: 'Switch',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
@ -1737,7 +1866,7 @@ describe('Workflow', () => {
|
|||
[
|
||||
{
|
||||
node: 'Set1',
|
||||
type: 'main',
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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 { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
|
@ -13,7 +20,12 @@ const loadFixture = (fixture: string) => {
|
|||
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 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(
|
||||
new Workflow({
|
||||
id: '123',
|
||||
|
@ -37,6 +59,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
|
|||
connections: workflow.connections,
|
||||
active: false,
|
||||
nodeTypes: Helpers.NodeTypes(),
|
||||
pinData,
|
||||
}),
|
||||
run?.data ?? null,
|
||||
0,
|
||||
|
@ -44,7 +67,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
|
|||
activeNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
{},
|
||||
'manual',
|
||||
mode ?? 'integrated',
|
||||
{},
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
7
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_run.json
vendored
Normal file
7
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_run.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"data": {
|
||||
"resultData": {
|
||||
"runData": {}
|
||||
}
|
||||
}
|
||||
}
|
106
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_workflow.json
vendored
Normal file
106
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_workflow.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
141
pnpm-lock.yaml
141
pnpm-lock.yaml
|
@ -554,8 +554,8 @@ importers:
|
|||
specifier: 2.13.0
|
||||
version: 2.13.0
|
||||
'@oclif/core':
|
||||
specifier: 3.26.6
|
||||
version: 3.26.6
|
||||
specifier: 4.0.7
|
||||
version: 4.0.7
|
||||
'@pinecone-database/pinecone':
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
|
@ -1323,8 +1323,8 @@ importers:
|
|||
packages/node-dev:
|
||||
dependencies:
|
||||
'@oclif/core':
|
||||
specifier: 3.26.6
|
||||
version: 3.26.6
|
||||
specifier: 4.0.7
|
||||
version: 4.0.7
|
||||
change-case:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.2
|
||||
|
@ -4165,8 +4165,8 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
deprecated: This functionality has been moved to @npmcli/fs
|
||||
|
||||
'@oclif/core@3.26.6':
|
||||
resolution: {integrity: sha512-+FiTw1IPuJTF9tSAlTsY8bGK4sgthehjz7c2SvYdgQncTkxI2xvUch/8QpjNYGLEmUneNygvYMRBax2KJcLccA==}
|
||||
'@oclif/core@4.0.7':
|
||||
resolution: {integrity: sha512-sU4Dx+RXCWAkrMw8tQFYAL6VfcHYKLPxVC9iKfgTXr4aDhcCssDwrbgpx0Di1dnNxvQlDGUhuCEInZuIY/nNfw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@one-ini/wasm@0.1.1':
|
||||
|
@ -5420,9 +5420,6 @@ packages:
|
|||
'@types/cheerio@0.22.31':
|
||||
resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==}
|
||||
|
||||
'@types/cli-progress@3.11.5':
|
||||
resolution: {integrity: sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==}
|
||||
|
||||
'@types/compression@1.0.1':
|
||||
resolution: {integrity: sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==}
|
||||
|
||||
|
@ -6305,8 +6302,9 @@ packages:
|
|||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansicolors@0.3.2:
|
||||
resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==}
|
||||
ansis@3.2.0:
|
||||
resolution: {integrity: sha512-Yk3BkHH9U7oPyCN3gL5Tc7CpahG/+UFv/6UG03C311Vy9lzRmA5uoxDTpU9CO3rGHL6KzJz/pdDeXZCZ5Mu/Sg==}
|
||||
engines: {node: '>=15'}
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
@ -6789,10 +6787,6 @@ packages:
|
|||
capital-case@1.0.4:
|
||||
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||
|
||||
cardinal@2.1.1:
|
||||
resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==}
|
||||
hasBin: true
|
||||
|
||||
caseless@0.12.0:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
|
||||
|
@ -6923,14 +6917,14 @@ packages:
|
|||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
cli-progress@3.12.0:
|
||||
resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
cli-spinners@2.9.0:
|
||||
resolution: {integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cli-spinners@2.9.2:
|
||||
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cli-table3@0.6.3:
|
||||
resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==}
|
||||
engines: {node: 10.* || >= 12.*}
|
||||
|
@ -7002,10 +6996,6 @@ packages:
|
|||
color@3.2.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||
|
||||
|
@ -7434,6 +7424,15 @@ packages:
|
|||
supports-color:
|
||||
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:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -8839,10 +8838,6 @@ packages:
|
|||
humanize-ms@1.2.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -9852,6 +9847,10 @@ packages:
|
|||
resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lilconfig@3.1.2:
|
||||
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
|
@ -10496,9 +10495,6 @@ packages:
|
|||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
natural-orderby@2.0.3:
|
||||
resolution: {integrity: sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==}
|
||||
|
||||
negotiator@0.6.3:
|
||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -10721,10 +10717,6 @@ packages:
|
|||
resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
object-treeify@1.1.33:
|
||||
resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
object.assign@4.1.4:
|
||||
resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -10930,9 +10922,6 @@ packages:
|
|||
pascal-case@3.1.2:
|
||||
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
|
||||
|
||||
password-prompt@1.1.3:
|
||||
resolution: {integrity: sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
|
@ -11750,9 +11739,6 @@ packages:
|
|||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redeyed@2.1.1:
|
||||
resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -12548,10 +12534,6 @@ packages:
|
|||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
supports-hyperlinks@2.3.0:
|
||||
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -17189,33 +17171,22 @@ snapshots:
|
|||
rimraf: 3.0.2
|
||||
optional: true
|
||||
|
||||
'@oclif/core@3.26.6':
|
||||
'@oclif/core@4.0.7':
|
||||
dependencies:
|
||||
'@types/cli-progress': 3.11.5
|
||||
ansi-escapes: 4.3.2
|
||||
ansi-styles: 4.3.0
|
||||
cardinal: 2.1.1
|
||||
chalk: 4.1.2
|
||||
ansis: 3.2.0
|
||||
clean-stack: 3.0.1
|
||||
cli-progress: 3.12.0
|
||||
color: 4.2.3
|
||||
debug: 4.3.4(supports-color@8.1.1)
|
||||
cli-spinners: 2.9.2
|
||||
debug: 4.3.5(supports-color@8.1.1)
|
||||
ejs: 3.1.10
|
||||
get-package-type: 0.1.0
|
||||
globby: 11.1.0
|
||||
hyperlinker: 1.0.0
|
||||
indent-string: 4.0.0
|
||||
is-wsl: 2.2.0
|
||||
js-yaml: 3.14.1
|
||||
lilconfig: 3.1.2
|
||||
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
|
||||
strip-ansi: 6.0.1
|
||||
supports-color: 8.1.1
|
||||
supports-hyperlinks: 2.3.0
|
||||
widest-line: 3.1.0
|
||||
wordwrap: 1.0.0
|
||||
wrap-ansi: 7.0.0
|
||||
|
@ -19232,10 +19203,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 18.16.16
|
||||
|
||||
'@types/cli-progress@3.11.5':
|
||||
dependencies:
|
||||
'@types/node': 18.16.16
|
||||
|
||||
'@types/compression@1.0.1':
|
||||
dependencies:
|
||||
'@types/express': 4.17.21
|
||||
|
@ -20315,7 +20282,7 @@ snapshots:
|
|||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
ansicolors@0.3.2: {}
|
||||
ansis@3.2.0: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
|
@ -20898,11 +20865,6 @@ snapshots:
|
|||
tslib: 2.6.2
|
||||
upper-case-first: 2.0.2
|
||||
|
||||
cardinal@2.1.1:
|
||||
dependencies:
|
||||
ansicolors: 0.3.2
|
||||
redeyed: 2.1.1
|
||||
|
||||
caseless@0.12.0: {}
|
||||
|
||||
chai@4.3.10:
|
||||
|
@ -21058,12 +21020,10 @@ snapshots:
|
|||
dependencies:
|
||||
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.2: {}
|
||||
|
||||
cli-table3@0.6.3:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
@ -21156,11 +21116,6 @@ snapshots:
|
|||
color-convert: 1.9.3
|
||||
color-string: 1.9.1
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
|
||||
colord@2.9.3: {}
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
@ -21652,6 +21607,12 @@ snapshots:
|
|||
optionalDependencies:
|
||||
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: {}
|
||||
|
||||
decimal.js@10.4.3: {}
|
||||
|
@ -23463,8 +23424,6 @@ snapshots:
|
|||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
hyperlinker@1.0.0: {}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
@ -24675,6 +24634,8 @@ snapshots:
|
|||
|
||||
lilconfig@3.0.0: {}
|
||||
|
||||
lilconfig@3.1.2: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
linkify-it@3.0.3:
|
||||
|
@ -25334,8 +25295,6 @@ snapshots:
|
|||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
natural-orderby@2.0.3: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
@ -25576,8 +25535,6 @@ snapshots:
|
|||
|
||||
object-keys@1.1.1: {}
|
||||
|
||||
object-treeify@1.1.33: {}
|
||||
|
||||
object.assign@4.1.4:
|
||||
dependencies:
|
||||
call-bind: 1.0.7
|
||||
|
@ -25828,11 +25785,6 @@ snapshots:
|
|||
no-case: 3.0.4
|
||||
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-case@3.0.4:
|
||||
|
@ -26686,10 +26638,6 @@ snapshots:
|
|||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redeyed@2.1.1:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
|
@ -27699,11 +27647,6 @@ snapshots:
|
|||
dependencies:
|
||||
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: {}
|
||||
|
||||
svgo@3.1.0:
|
||||
|
|
Loading…
Reference in a new issue