mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 16:44:07 -08:00
Merge remote-tracking branch 'origin/master' into pay-2003-add-executions-tab-to-home-and-projects-view
This commit is contained in:
commit
acb6700fc9
|
@ -15,3 +15,4 @@
|
|||
# refactor: Move test files alongside tested files (#11504)
|
||||
|
||||
7e58fc4fec468aca0b45d5bfe6150e1af632acbc
|
||||
f32b13c6ed078be042a735bc8621f27e00dc3116
|
||||
|
|
39
.github/workflows/release-publish.yml
vendored
39
.github/workflows/release-publish.yml
vendored
|
@ -38,6 +38,12 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache build artifacts
|
||||
uses: actions/cache/save@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}-base:build
|
||||
|
||||
- name: Dry-run publishing
|
||||
run: pnpm publish -r --no-git-checks --dry-run
|
||||
|
||||
|
@ -119,6 +125,39 @@ jobs:
|
|||
makeLatest: false
|
||||
body: ${{github.event.pull_request.body}}
|
||||
|
||||
create-sentry-release:
|
||||
name: Create release on Sentry
|
||||
needs: [publish-to-npm, publish-to-docker-hub]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
|
||||
steps:
|
||||
- name: Restore cached build artifacts
|
||||
uses: actions/cache/restore@v4.0.0
|
||||
with:
|
||||
path: ./packages/**/dist
|
||||
key: ${{ github.sha }}:db-tests
|
||||
|
||||
- name: Create a frontend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_FRONTEND_PROJECT }}
|
||||
version: {{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/editor-ui/dist
|
||||
|
||||
- name: Create a backend release
|
||||
uses: getsentry/action-release@v1.7.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
projects: ${{ secrets.SENTRY_BACKEND_PROJECT }}
|
||||
version: {{ needs.publish-to-npm.outputs.release }}
|
||||
sourcemaps: packages/cli/dist packages/core/dist packages/nodes-base/dist packages/@n8n/n8n-nodes-langchain/dist
|
||||
|
||||
trigger-release-note:
|
||||
name: Trigger a release note
|
||||
needs: [publish-to-npm, create-github-release]
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} from '../composables/workflow';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
|
@ -232,95 +233,96 @@ describe('Langchain Integration', () => {
|
|||
|
||||
const inputMessage = 'Hello!';
|
||||
const outputMessage = 'Hi there! How can I assist you today?';
|
||||
const runData = [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_languageModel: {
|
||||
response: {
|
||||
generations: [
|
||||
{
|
||||
text: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
message: {
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'AIMessage'],
|
||||
kwargs: {
|
||||
content: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
generationInfo: { finish_reason: 'stop' },
|
||||
},
|
||||
],
|
||||
llmOutput: {
|
||||
tokenUsage: {
|
||||
completionTokens: 26,
|
||||
promptTokens: 519,
|
||||
totalTokens: 545,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
messages: [
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'SystemMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'HumanMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { stop: ['Observation:'], promptIndex: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => {
|
||||
sendManualChatMessage(inputMessage);
|
||||
},
|
||||
runData: [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_languageModel: {
|
||||
response: {
|
||||
generations: [
|
||||
{
|
||||
text: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
message: {
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'AIMessage'],
|
||||
kwargs: {
|
||||
content: `{
|
||||
"action": "Final Answer",
|
||||
"action_input": "${outputMessage}"
|
||||
}`,
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
generationInfo: { finish_reason: 'stop' },
|
||||
},
|
||||
],
|
||||
llmOutput: {
|
||||
tokenUsage: {
|
||||
completionTokens: 26,
|
||||
promptTokens: 519,
|
||||
totalTokens: 545,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
|
||||
},
|
||||
inputOverride: {
|
||||
ai_languageModel: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
messages: [
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'SystemMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
lc: 1,
|
||||
type: 'constructor',
|
||||
id: ['langchain', 'schema', 'HumanMessage'],
|
||||
kwargs: {
|
||||
content:
|
||||
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
|
||||
additional_kwargs: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { stop: ['Observation:'], promptIndex: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { output: 'Hi there! How can I assist you today?' },
|
||||
},
|
||||
}),
|
||||
],
|
||||
runData,
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
|
@ -357,4 +359,56 @@ describe('Langchain Integration', () => {
|
|||
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
|
||||
getNodes().should('have.length', 3);
|
||||
});
|
||||
it('should render runItems for sub-nodes and allow switching between them', () => {
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
||||
cy.visit(workflowPage.url);
|
||||
cy.createFixtureWorkflow('In_memory_vector_store_fake_embeddings.json');
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
workflowPage.actions.executeNode('Populate VS');
|
||||
cy.get('[data-label="25 items"]').should('exist');
|
||||
|
||||
const assertInputOutputText = (text: string, assertion: 'exist' | 'not.exist') => {
|
||||
ndv.getters.outputPanel().contains(text).should(assertion);
|
||||
ndv.getters.inputPanel().contains(text).should(assertion);
|
||||
};
|
||||
|
||||
workflowPage.actions.openNode('Character Text Splitter');
|
||||
ndv.getters.outputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().should('exist');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '3 of 3');
|
||||
assertInputOutputText('Kyiv', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
assertInputOutputText('Berlin', 'exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
assertInputOutputText('Prague', 'not.exist');
|
||||
|
||||
ndv.actions.changeOutputRunSelector('1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.actions.changeOutputRunSelector('2 of 3');
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '2 of 3');
|
||||
ndv.getters.inputPanel().contains('Prague').should('exist');
|
||||
ndv.getters.inputPanel().contains('Berlin').should('not.exist');
|
||||
|
||||
ndv.getters.outputPanel().contains('Berlin').should('exist');
|
||||
ndv.getters.outputPanel().contains('Prague').should('not.exist');
|
||||
|
||||
ndv.actions.toggleInputRunLinking();
|
||||
ndv.getters.inputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
ndv.getters.outputRunSelector().find('input').should('include.value', '1 of 3');
|
||||
assertInputOutputText('Prague', 'exist');
|
||||
assertInputOutputText('Berlin', 'not.exist');
|
||||
assertInputOutputText('Kyiv', 'not.exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -441,7 +441,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('contain.text', 'Notion account personal project');
|
||||
});
|
||||
|
||||
it('should move resources between projects', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should move resources between projects', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
@ -684,7 +686,9 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||
.should('have.length', 1);
|
||||
});
|
||||
|
||||
it('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
// Skip flaky test
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
|
||||
it.skip('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
|
||||
cy.signinAsOwner();
|
||||
cy.visit(workflowsPage.url);
|
||||
|
||||
|
|
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
347
cypress/fixtures/In_memory_vector_store_fake_embeddings.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -16,7 +16,7 @@ export function createMockNodeExecutionData(
|
|||
return {
|
||||
[name]: {
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: 0,
|
||||
executionTime: 1,
|
||||
executionStatus,
|
||||
data: jsonData
|
||||
? Object.keys(jsonData).reduce((acc, key) => {
|
||||
|
@ -33,6 +33,7 @@ export function createMockNodeExecutionData(
|
|||
}, {} as ITaskDataConnections)
|
||||
: data,
|
||||
source: [null],
|
||||
inputOverride,
|
||||
...rest,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
},
|
||||
"patchedDependencies": {
|
||||
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
|
||||
"@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch",
|
||||
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
|
||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||
|
|
|
@ -7,6 +7,7 @@ export const LOG_SCOPES = [
|
|||
'external-secrets',
|
||||
'license',
|
||||
'multi-main-setup',
|
||||
'pruning',
|
||||
'pubsub',
|
||||
'redis',
|
||||
'scaling',
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
/* eslint-disable n8n-nodes-base/node-dirname-against-convention */
|
||||
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import type { LLMResult } from '@langchain/core/outputs';
|
||||
import {
|
||||
NodeConnectionType,
|
||||
type INodePropertyOptions,
|
||||
|
@ -9,8 +12,6 @@ import {
|
|||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import type { LLMResult } from '@langchain/core/outputs';
|
||||
import { getConnectionHintNoticeField } from '../../../utils/sharedFields';
|
||||
import { N8nLlmTracing } from '../N8nLlmTracing';
|
||||
|
||||
|
@ -36,6 +37,10 @@ const modelField: INodeProperties = {
|
|||
name: 'Claude 3 Sonnet(20240229)',
|
||||
value: 'claude-3-sonnet-20240229',
|
||||
},
|
||||
{
|
||||
name: 'Claude 3.5 Haiku(20241022)',
|
||||
value: 'claude-3-5-haiku-20241022',
|
||||
},
|
||||
{
|
||||
name: 'Claude 3 Haiku(20240307)',
|
||||
value: 'claude-3-haiku-20240307',
|
||||
|
|
|
@ -17,10 +17,22 @@
|
|||
},
|
||||
"main": "dist/start.js",
|
||||
"module": "src/start.ts",
|
||||
"types": "dist/start.d.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"exports": {
|
||||
"./start": {
|
||||
"require": "./dist/start.js",
|
||||
"import": "./src/start.ts",
|
||||
"types": "./dist/start.d.ts"
|
||||
},
|
||||
".": {
|
||||
"require": "./dist/index.js",
|
||||
"import": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/config": "workspace:*",
|
||||
"acorn": "8.14.0",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './task-runner';
|
||||
export * from './runner-types';
|
||||
export * from './message-types';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { N8nMessage } from '../../runner-types';
|
||||
import type { BrokerMessage } from '@/message-types';
|
||||
|
||||
/**
|
||||
* Class to keep track of which built-in variables are accessed in the code
|
||||
|
@ -53,7 +53,7 @@ export class BuiltInsParserState {
|
|||
this.needs$prevNode = true;
|
||||
}
|
||||
|
||||
toDataRequestParams(): N8nMessage.ToRequester.TaskDataRequest['requestParams'] {
|
||||
toDataRequestParams(): BrokerMessage.ToRequester.TaskDataRequest['requestParams'] {
|
||||
return {
|
||||
dataOfNodes: this.needsAllNodes ? 'all' : Array.from(this.neededNodeNames),
|
||||
env: this.needs$env,
|
||||
|
|
204
packages/@n8n/task-runner/src/message-types.ts
Normal file
204
packages/@n8n/task-runner/src/message-types.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
import type { INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
|
||||
import type { RPC_ALLOW_LIST, TaskDataRequestParams, TaskResultData } from './runner-types';
|
||||
|
||||
export namespace BrokerMessage {
|
||||
export namespace ToRunner {
|
||||
export interface InfoRequest {
|
||||
type: 'broker:inforequest';
|
||||
}
|
||||
|
||||
export interface RunnerRegistered {
|
||||
type: 'broker:runnerregistered';
|
||||
}
|
||||
|
||||
export interface TaskOfferAccept {
|
||||
type: 'broker:taskofferaccept';
|
||||
taskId: string;
|
||||
offerId: string;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'broker:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskSettings {
|
||||
type: 'broker:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'broker:rpcresponse';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'broker:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface NodeTypes {
|
||||
type: 'broker:nodetypes';
|
||||
nodeTypes: INodeTypeBaseDescription[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| InfoRequest
|
||||
| TaskOfferAccept
|
||||
| TaskCancel
|
||||
| TaskSettings
|
||||
| RunnerRegistered
|
||||
| RPCResponse
|
||||
| TaskDataResponse
|
||||
| NodeTypes;
|
||||
}
|
||||
|
||||
export namespace ToRequester {
|
||||
export interface TaskReady {
|
||||
type: 'broker:taskready';
|
||||
requestId: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'broker:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'broker:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'broker:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'broker:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All = TaskReady | TaskDone | TaskError | TaskDataRequest | RPC;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RequesterMessage {
|
||||
export namespace ToBroker {
|
||||
export interface TaskSettings {
|
||||
type: 'requester:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'requester:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'requester:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'requester:rpcresponse';
|
||||
taskId: string;
|
||||
callId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskRequest {
|
||||
type: 'requester:taskrequest';
|
||||
requestId: string;
|
||||
taskType: string;
|
||||
}
|
||||
|
||||
export type All = TaskSettings | TaskCancel | RPCResponse | TaskDataResponse | TaskRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RunnerMessage {
|
||||
export namespace ToBroker {
|
||||
export interface Info {
|
||||
type: 'runner:info';
|
||||
name: string;
|
||||
types: string[];
|
||||
}
|
||||
|
||||
export interface TaskAccepted {
|
||||
type: 'runner:taskaccepted';
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskRejected {
|
||||
type: 'runner:taskrejected';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'runner:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'runner:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskOffer {
|
||||
type: 'runner:taskoffer';
|
||||
offerId: string;
|
||||
taskType: string;
|
||||
validFor: number;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'runner:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'runner:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| Info
|
||||
| TaskDone
|
||||
| TaskError
|
||||
| TaskAccepted
|
||||
| TaskRejected
|
||||
| TaskOffer
|
||||
| RPC
|
||||
| TaskDataRequest;
|
||||
}
|
||||
}
|
|
@ -1,216 +1,90 @@
|
|||
import type { INodeExecutionData, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
import type {
|
||||
EnvProviderState,
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
IRunExecutionData,
|
||||
ITaskDataConnections,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
WorkflowParameters,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
/**
|
||||
* Specifies what data should be included for a task data request.
|
||||
*/
|
||||
export interface TaskDataRequestParams {
|
||||
dataOfNodes: string[] | 'all';
|
||||
prevNode: boolean;
|
||||
/** Whether input data for the node should be included */
|
||||
input: boolean;
|
||||
/** Whether env provider's state should be included */
|
||||
env: boolean;
|
||||
}
|
||||
|
||||
export interface DataRequestResponse {
|
||||
workflow: Omit<WorkflowParameters, 'nodeTypes'>;
|
||||
inputData: ITaskDataConnections;
|
||||
node: INode;
|
||||
|
||||
runExecutionData: IRunExecutionData;
|
||||
runIndex: number;
|
||||
itemIndex: number;
|
||||
activeNodeName: string;
|
||||
connectionInputData: INodeExecutionData[];
|
||||
siblingParameters: INodeParameters;
|
||||
mode: WorkflowExecuteMode;
|
||||
envProviderState: EnvProviderState;
|
||||
executeData?: IExecuteData;
|
||||
defaultReturnRunIndex: number;
|
||||
selfData: IDataObject;
|
||||
contextNodeName: string;
|
||||
additionalData: PartialAdditionalData;
|
||||
}
|
||||
|
||||
export interface TaskResultData {
|
||||
result: INodeExecutionData[];
|
||||
customData?: Record<string, string>;
|
||||
}
|
||||
|
||||
export namespace N8nMessage {
|
||||
export namespace ToRunner {
|
||||
export interface InfoRequest {
|
||||
type: 'broker:inforequest';
|
||||
}
|
||||
export interface TaskData {
|
||||
executeFunctions: IExecuteFunctions;
|
||||
inputData: ITaskDataConnections;
|
||||
node: INode;
|
||||
|
||||
export interface RunnerRegistered {
|
||||
type: 'broker:runnerregistered';
|
||||
}
|
||||
|
||||
export interface TaskOfferAccept {
|
||||
type: 'broker:taskofferaccept';
|
||||
taskId: string;
|
||||
offerId: string;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'broker:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskSettings {
|
||||
type: 'broker:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'broker:rpcresponse';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'broker:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface NodeTypes {
|
||||
type: 'broker:nodetypes';
|
||||
nodeTypes: INodeTypeBaseDescription[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| InfoRequest
|
||||
| TaskOfferAccept
|
||||
| TaskCancel
|
||||
| TaskSettings
|
||||
| RunnerRegistered
|
||||
| RPCResponse
|
||||
| TaskDataResponse
|
||||
| NodeTypes;
|
||||
}
|
||||
|
||||
export namespace ToRequester {
|
||||
export interface TaskReady {
|
||||
type: 'broker:taskready';
|
||||
requestId: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'broker:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'broker:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'broker:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'broker:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All = TaskReady | TaskDone | TaskError | TaskDataRequest | RPC;
|
||||
}
|
||||
workflow: Workflow;
|
||||
runExecutionData: IRunExecutionData;
|
||||
runIndex: number;
|
||||
itemIndex: number;
|
||||
activeNodeName: string;
|
||||
connectionInputData: INodeExecutionData[];
|
||||
siblingParameters: INodeParameters;
|
||||
mode: WorkflowExecuteMode;
|
||||
envProviderState: EnvProviderState;
|
||||
executeData?: IExecuteData;
|
||||
defaultReturnRunIndex: number;
|
||||
selfData: IDataObject;
|
||||
contextNodeName: string;
|
||||
additionalData: IWorkflowExecuteAdditionalData;
|
||||
}
|
||||
|
||||
export namespace RequesterMessage {
|
||||
export namespace ToN8n {
|
||||
export interface TaskSettings {
|
||||
type: 'requester:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'requester:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'requester:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'requester:rpcresponse';
|
||||
taskId: string;
|
||||
callId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskRequest {
|
||||
type: 'requester:taskrequest';
|
||||
requestId: string;
|
||||
taskType: string;
|
||||
}
|
||||
|
||||
export type All = TaskSettings | TaskCancel | RPCResponse | TaskDataResponse | TaskRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RunnerMessage {
|
||||
export namespace ToN8n {
|
||||
export interface Info {
|
||||
type: 'runner:info';
|
||||
name: string;
|
||||
types: string[];
|
||||
}
|
||||
|
||||
export interface TaskAccepted {
|
||||
type: 'runner:taskaccepted';
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskRejected {
|
||||
type: 'runner:taskrejected';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'runner:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'runner:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskOffer {
|
||||
type: 'runner:taskoffer';
|
||||
offerId: string;
|
||||
taskType: string;
|
||||
validFor: number;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'runner:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'runner:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| Info
|
||||
| TaskDone
|
||||
| TaskError
|
||||
| TaskAccepted
|
||||
| TaskRejected
|
||||
| TaskOffer
|
||||
| RPC
|
||||
| TaskDataRequest;
|
||||
}
|
||||
export interface PartialAdditionalData {
|
||||
executionId?: string;
|
||||
restartExecutionId?: string;
|
||||
restApiUrl: string;
|
||||
instanceBaseUrl: string;
|
||||
formWaitingBaseUrl: string;
|
||||
webhookBaseUrl: string;
|
||||
webhookWaitingBaseUrl: string;
|
||||
webhookTestBaseUrl: string;
|
||||
currentNodeParameters?: INodeParameters;
|
||||
executionTimeoutTimestamp?: number;
|
||||
userId?: string;
|
||||
variables: IDataObject;
|
||||
}
|
||||
|
||||
export const RPC_ALLOW_LIST = [
|
||||
|
|
|
@ -2,14 +2,10 @@ import { ApplicationError, type INodeTypeDescription } from 'n8n-workflow';
|
|||
import { nanoid } from 'nanoid';
|
||||
import { type MessageEvent, WebSocket } from 'ws';
|
||||
|
||||
import type { BaseRunnerConfig } from './config/base-runner-config';
|
||||
import { TaskRunnerNodeTypes } from './node-types';
|
||||
import {
|
||||
RPC_ALLOW_LIST,
|
||||
type RunnerMessage,
|
||||
type N8nMessage,
|
||||
type TaskResultData,
|
||||
} from './runner-types';
|
||||
import type { BaseRunnerConfig } from '@/config/base-runner-config';
|
||||
import type { BrokerMessage, RunnerMessage } from '@/message-types';
|
||||
import { TaskRunnerNodeTypes } from '@/node-types';
|
||||
import { RPC_ALLOW_LIST, type TaskResultData } from '@/runner-types';
|
||||
|
||||
export interface Task<T = unknown> {
|
||||
taskId: string;
|
||||
|
@ -90,7 +86,7 @@ export abstract class TaskRunner {
|
|||
|
||||
private receiveMessage = (message: MessageEvent) => {
|
||||
// eslint-disable-next-line n8n-local-rules/no-uncaught-json-parse
|
||||
const data = JSON.parse(message.data as string) as N8nMessage.ToRunner.All;
|
||||
const data = JSON.parse(message.data as string) as BrokerMessage.ToRunner.All;
|
||||
void this.onMessage(data);
|
||||
};
|
||||
|
||||
|
@ -140,11 +136,11 @@ export abstract class TaskRunner {
|
|||
}
|
||||
}
|
||||
|
||||
send(message: RunnerMessage.ToN8n.All) {
|
||||
send(message: RunnerMessage.ToBroker.All) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
onMessage(message: N8nMessage.ToRunner.All) {
|
||||
onMessage(message: BrokerMessage.ToRunner.All) {
|
||||
switch (message.type) {
|
||||
case 'broker:inforequest':
|
||||
this.send({
|
||||
|
@ -252,7 +248,7 @@ export abstract class TaskRunner {
|
|||
this.sendOffers();
|
||||
}
|
||||
|
||||
taskDone(taskId: string, data: RunnerMessage.ToN8n.TaskDone['data']) {
|
||||
taskDone(taskId: string, data: RunnerMessage.ToBroker.TaskDone['data']) {
|
||||
this.send({
|
||||
type: 'runner:taskdone',
|
||||
taskId,
|
||||
|
@ -288,7 +284,7 @@ export abstract class TaskRunner {
|
|||
|
||||
async requestData<T = unknown>(
|
||||
taskId: Task['taskId'],
|
||||
requestParams: RunnerMessage.ToN8n.TaskDataRequest['requestParams'],
|
||||
requestParams: RunnerMessage.ToBroker.TaskDataRequest['requestParams'],
|
||||
): Promise<T> {
|
||||
const requestId = nanoid();
|
||||
|
||||
|
@ -314,7 +310,7 @@ export abstract class TaskRunner {
|
|||
}
|
||||
}
|
||||
|
||||
async makeRpcCall(taskId: string, name: RunnerMessage.ToN8n.RPC['name'], params: unknown[]) {
|
||||
async makeRpcCall(taskId: string, name: RunnerMessage.ToBroker.RPC['name'], params: unknown[]) {
|
||||
const callId = nanoid();
|
||||
|
||||
const dataPromise = new Promise((resolve, reject) => {
|
||||
|
@ -342,7 +338,7 @@ export abstract class TaskRunner {
|
|||
|
||||
handleRpcResponse(
|
||||
callId: string,
|
||||
status: N8nMessage.ToRunner.RPCResponse['status'],
|
||||
status: BrokerMessage.ToRunner.RPCResponse['status'],
|
||||
data: unknown,
|
||||
) {
|
||||
const call = this.rpcCalls.get(callId);
|
||||
|
|
|
@ -27,7 +27,7 @@ import { Subscriber } from '@/scaling/pubsub/subscriber.service';
|
|||
import { Server } from '@/server';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { PruningService } from '@/services/pruning.service';
|
||||
import { PruningService } from '@/services/pruning/pruning.service';
|
||||
import { UrlService } from '@/services/url.service';
|
||||
import { WaitTracker } from '@/wait-tracker';
|
||||
import { WorkflowRunner } from '@/workflow-runner';
|
||||
|
|
|
@ -513,7 +513,7 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
|
|||
.execute();
|
||||
}
|
||||
|
||||
async hardDeleteSoftDeletedExecutions() {
|
||||
async findSoftDeletedExecutions() {
|
||||
const date = new Date();
|
||||
date.setHours(date.getHours() - this.globalConfig.pruning.hardDeleteBuffer);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
deepCopy,
|
||||
ErrorReporterProxy,
|
||||
type IRunExecutionData,
|
||||
type ITaskData,
|
||||
|
@ -57,7 +58,7 @@ test('should ignore on leftover async call', async () => {
|
|||
expect(executionRepository.updateExistingExecution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should update execution', async () => {
|
||||
test('should update execution when saving progress is enabled', async () => {
|
||||
jest.spyOn(fnModule, 'toSaveSettings').mockReturnValue({
|
||||
...commonSettings,
|
||||
progress: true,
|
||||
|
@ -86,6 +87,37 @@ test('should update execution', async () => {
|
|||
expect(reporterSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should update execution when saving progress is disabled, but waitTill is defined', async () => {
|
||||
jest.spyOn(fnModule, 'toSaveSettings').mockReturnValue({
|
||||
...commonSettings,
|
||||
progress: false,
|
||||
});
|
||||
|
||||
const reporterSpy = jest.spyOn(ErrorReporterProxy, 'error');
|
||||
|
||||
executionRepository.findSingleExecution.mockResolvedValue({} as IExecutionResponse);
|
||||
|
||||
const args = deepCopy(commonArgs);
|
||||
args[4].waitTill = new Date();
|
||||
await saveExecutionProgress(...args);
|
||||
|
||||
expect(executionRepository.updateExistingExecution).toHaveBeenCalledWith('some-execution-id', {
|
||||
data: {
|
||||
executionData: undefined,
|
||||
resultData: {
|
||||
lastNodeExecuted: 'My Node',
|
||||
runData: {
|
||||
'My Node': [{}],
|
||||
},
|
||||
},
|
||||
startData: {},
|
||||
},
|
||||
status: 'running',
|
||||
});
|
||||
|
||||
expect(reporterSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should report error on failure', async () => {
|
||||
jest.spyOn(fnModule, 'toSaveSettings').mockReturnValue({
|
||||
...commonSettings,
|
||||
|
|
|
@ -16,7 +16,7 @@ export async function saveExecutionProgress(
|
|||
) {
|
||||
const saveSettings = toSaveSettings(workflowData.settings);
|
||||
|
||||
if (!saveSettings.progress) return;
|
||||
if (!saveSettings.progress && !executionData.waitTill) return;
|
||||
|
||||
const logger = Container.get(Logger);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { RunnerMessage, TaskResultData } from '@n8n/task-runner';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
|
||||
import { TaskRejectError } from '../errors';
|
||||
import type { RunnerMessage, TaskResultData } from '../runner-types';
|
||||
import { TaskBroker } from '../task-broker.service';
|
||||
import type { TaskOffer, TaskRequest, TaskRunner } from '../task-broker.service';
|
||||
|
||||
|
@ -381,7 +381,7 @@ describe('TaskBroker', () => {
|
|||
const runnerId = 'runner1';
|
||||
const taskId = 'task1';
|
||||
|
||||
const message: RunnerMessage.ToN8n.TaskAccepted = {
|
||||
const message: RunnerMessage.ToBroker.TaskAccepted = {
|
||||
type: 'runner:taskaccepted',
|
||||
taskId,
|
||||
};
|
||||
|
@ -406,7 +406,7 @@ describe('TaskBroker', () => {
|
|||
const taskId = 'task1';
|
||||
const rejectionReason = 'Task execution failed';
|
||||
|
||||
const message: RunnerMessage.ToN8n.TaskRejected = {
|
||||
const message: RunnerMessage.ToBroker.TaskRejected = {
|
||||
type: 'runner:taskrejected',
|
||||
taskId,
|
||||
reason: rejectionReason,
|
||||
|
@ -433,7 +433,7 @@ describe('TaskBroker', () => {
|
|||
const requesterId = 'requester1';
|
||||
const data = mock<TaskResultData>();
|
||||
|
||||
const message: RunnerMessage.ToN8n.TaskDone = {
|
||||
const message: RunnerMessage.ToBroker.TaskDone = {
|
||||
type: 'runner:taskdone',
|
||||
taskId,
|
||||
data,
|
||||
|
@ -464,7 +464,7 @@ describe('TaskBroker', () => {
|
|||
const requesterId = 'requester1';
|
||||
const errorMessage = 'Task execution failed';
|
||||
|
||||
const message: RunnerMessage.ToN8n.TaskError = {
|
||||
const message: RunnerMessage.ToBroker.TaskError = {
|
||||
type: 'runner:taskerror',
|
||||
taskId,
|
||||
error: errorMessage,
|
||||
|
@ -494,14 +494,14 @@ describe('TaskBroker', () => {
|
|||
const taskId = 'task1';
|
||||
const requesterId = 'requester1';
|
||||
const requestId = 'request1';
|
||||
const requestParams: RunnerMessage.ToN8n.TaskDataRequest['requestParams'] = {
|
||||
const requestParams: RunnerMessage.ToBroker.TaskDataRequest['requestParams'] = {
|
||||
dataOfNodes: 'all',
|
||||
env: true,
|
||||
input: true,
|
||||
prevNode: true,
|
||||
};
|
||||
|
||||
const message: RunnerMessage.ToN8n.TaskDataRequest = {
|
||||
const message: RunnerMessage.ToBroker.TaskDataRequest = {
|
||||
type: 'runner:taskdatarequest',
|
||||
taskId,
|
||||
requestId,
|
||||
|
@ -534,7 +534,7 @@ describe('TaskBroker', () => {
|
|||
const rpcName = 'helpers.httpRequestWithAuthentication';
|
||||
const rpcParams = ['param1', 'param2'];
|
||||
|
||||
const message: RunnerMessage.ToN8n.RPC = {
|
||||
const message: RunnerMessage.ToBroker.RPC = {
|
||||
type: 'runner:rpc',
|
||||
taskId,
|
||||
callId,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Response } from 'express';
|
||||
import type { INodeExecutionData, INodeTypeBaseDescription } from 'n8n-workflow';
|
||||
import type { INodeExecutionData } from 'n8n-workflow';
|
||||
import type WebSocket from 'ws';
|
||||
|
||||
import type { TaskRunner } from './task-broker.service';
|
||||
|
@ -34,230 +34,3 @@ export interface TaskRunnerServerInitRequest
|
|||
}
|
||||
|
||||
export type TaskRunnerServerInitResponse = Response & { req: TaskRunnerServerInitRequest };
|
||||
|
||||
export namespace N8nMessage {
|
||||
export namespace ToRunner {
|
||||
export interface InfoRequest {
|
||||
type: 'broker:inforequest';
|
||||
}
|
||||
|
||||
export interface RunnerRegistered {
|
||||
type: 'broker:runnerregistered';
|
||||
}
|
||||
|
||||
export interface TaskOfferAccept {
|
||||
type: 'broker:taskofferaccept';
|
||||
taskId: string;
|
||||
offerId: string;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'broker:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskSettings {
|
||||
type: 'broker:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'broker:rpcresponse';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'broker:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface NodeTypes {
|
||||
type: 'broker:nodetypes';
|
||||
nodeTypes: INodeTypeBaseDescription[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| InfoRequest
|
||||
| TaskOfferAccept
|
||||
| TaskCancel
|
||||
| TaskSettings
|
||||
| RunnerRegistered
|
||||
| RPCResponse
|
||||
| TaskDataResponse
|
||||
| NodeTypes;
|
||||
}
|
||||
|
||||
export namespace ToRequester {
|
||||
export interface TaskReady {
|
||||
type: 'broker:taskready';
|
||||
requestId: string;
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'broker:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'broker:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'broker:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'broker:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All = TaskReady | TaskDone | TaskError | TaskDataRequest | RPC;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RequesterMessage {
|
||||
export namespace ToN8n {
|
||||
export interface TaskSettings {
|
||||
type: 'requester:tasksettings';
|
||||
taskId: string;
|
||||
settings: unknown;
|
||||
}
|
||||
|
||||
export interface TaskCancel {
|
||||
type: 'requester:taskcancel';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDataResponse {
|
||||
type: 'requester:taskdataresponse';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface RPCResponse {
|
||||
type: 'requester:rpcresponse';
|
||||
taskId: string;
|
||||
callId: string;
|
||||
status: 'success' | 'error';
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TaskRequest {
|
||||
type: 'requester:taskrequest';
|
||||
requestId: string;
|
||||
taskType: string;
|
||||
}
|
||||
|
||||
export type All = TaskSettings | TaskCancel | RPCResponse | TaskDataResponse | TaskRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace RunnerMessage {
|
||||
export namespace ToN8n {
|
||||
export interface Info {
|
||||
type: 'runner:info';
|
||||
name: string;
|
||||
types: string[];
|
||||
}
|
||||
|
||||
export interface TaskAccepted {
|
||||
type: 'runner:taskaccepted';
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface TaskRejected {
|
||||
type: 'runner:taskrejected';
|
||||
taskId: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface TaskDone {
|
||||
type: 'runner:taskdone';
|
||||
taskId: string;
|
||||
data: TaskResultData;
|
||||
}
|
||||
|
||||
export interface TaskError {
|
||||
type: 'runner:taskerror';
|
||||
taskId: string;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export interface TaskOffer {
|
||||
type: 'runner:taskoffer';
|
||||
offerId: string;
|
||||
taskType: string;
|
||||
validFor: number;
|
||||
}
|
||||
|
||||
export interface TaskDataRequest {
|
||||
type: 'runner:taskdatarequest';
|
||||
taskId: string;
|
||||
requestId: string;
|
||||
requestParams: TaskDataRequestParams;
|
||||
}
|
||||
|
||||
export interface RPC {
|
||||
type: 'runner:rpc';
|
||||
callId: string;
|
||||
taskId: string;
|
||||
name: (typeof RPC_ALLOW_LIST)[number];
|
||||
params: unknown[];
|
||||
}
|
||||
|
||||
export type All =
|
||||
| Info
|
||||
| TaskDone
|
||||
| TaskError
|
||||
| TaskAccepted
|
||||
| TaskRejected
|
||||
| TaskOffer
|
||||
| RPC
|
||||
| TaskDataRequest;
|
||||
}
|
||||
}
|
||||
|
||||
export const RPC_ALLOW_LIST = [
|
||||
'logNodeOutput',
|
||||
'helpers.httpRequestWithAuthentication',
|
||||
'helpers.requestWithAuthenticationPaginated',
|
||||
// "helpers.normalizeItems"
|
||||
// "helpers.constructExecutionMetaData"
|
||||
// "helpers.assertBinaryData"
|
||||
'helpers.getBinaryDataBuffer',
|
||||
// "helpers.copyInputItems"
|
||||
// "helpers.returnJsonArray"
|
||||
'helpers.getSSHClient',
|
||||
'helpers.createReadStream',
|
||||
// "helpers.getStoragePath"
|
||||
'helpers.writeContentToFile',
|
||||
'helpers.prepareBinaryData',
|
||||
'helpers.setBinaryDataBuffer',
|
||||
'helpers.copyBinaryFile',
|
||||
'helpers.binaryToBuffer',
|
||||
// "helpers.binaryToString"
|
||||
// "helpers.getBinaryPath"
|
||||
'helpers.getBinaryStream',
|
||||
'helpers.getBinaryMetadata',
|
||||
'helpers.createDeferredPromise',
|
||||
'helpers.httpRequest',
|
||||
] as const;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { BrokerMessage, RunnerMessage } from '@n8n/task-runner';
|
||||
import { Service } from 'typedi';
|
||||
import type WebSocket from 'ws';
|
||||
|
||||
|
@ -5,11 +6,9 @@ import { Logger } from '@/logging/logger.service';
|
|||
|
||||
import { DefaultTaskRunnerDisconnectAnalyzer } from './default-task-runner-disconnect-analyzer';
|
||||
import type {
|
||||
RunnerMessage,
|
||||
N8nMessage,
|
||||
DisconnectAnalyzer,
|
||||
TaskRunnerServerInitRequest,
|
||||
TaskRunnerServerInitResponse,
|
||||
DisconnectAnalyzer,
|
||||
} from './runner-types';
|
||||
import { TaskBroker, type MessageCallback, type TaskRunner } from './task-broker.service';
|
||||
|
||||
|
@ -35,7 +34,7 @@ export class TaskRunnerWsServer {
|
|||
return this.disconnectAnalyzer;
|
||||
}
|
||||
|
||||
sendMessage(id: TaskRunner['id'], message: N8nMessage.ToRunner.All) {
|
||||
sendMessage(id: TaskRunner['id'], message: BrokerMessage.ToRunner.All) {
|
||||
this.runnerConnections.get(id)?.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
|
@ -49,9 +48,9 @@ export class TaskRunnerWsServer {
|
|||
try {
|
||||
const buffer = Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
|
||||
|
||||
const message: RunnerMessage.ToN8n.All = JSON.parse(
|
||||
const message: RunnerMessage.ToBroker.All = JSON.parse(
|
||||
buffer.toString('utf8'),
|
||||
) as RunnerMessage.ToN8n.All;
|
||||
) as RunnerMessage.ToBroker.All;
|
||||
|
||||
if (!isConnected && message.type !== 'runner:info') {
|
||||
return;
|
||||
|
@ -94,7 +93,7 @@ export class TaskRunnerWsServer {
|
|||
|
||||
connection.on('message', onMessage);
|
||||
connection.send(
|
||||
JSON.stringify({ type: 'broker:inforequest' } as N8nMessage.ToRunner.InfoRequest),
|
||||
JSON.stringify({ type: 'broker:inforequest' } as BrokerMessage.ToRunner.InfoRequest),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import type {
|
||||
BrokerMessage,
|
||||
RequesterMessage,
|
||||
RunnerMessage,
|
||||
TaskResultData,
|
||||
} from '@n8n/task-runner';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Service } from 'typedi';
|
||||
|
@ -6,7 +12,6 @@ import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
|
|||
import { Logger } from '@/logging/logger.service';
|
||||
|
||||
import { TaskRejectError } from './errors';
|
||||
import type { N8nMessage, RunnerMessage, RequesterMessage, TaskResultData } from './runner-types';
|
||||
|
||||
export interface TaskRunner {
|
||||
id: string;
|
||||
|
@ -38,13 +43,15 @@ export interface TaskRequest {
|
|||
acceptInProgress?: boolean;
|
||||
}
|
||||
|
||||
export type MessageCallback = (message: N8nMessage.ToRunner.All) => Promise<void> | void;
|
||||
export type MessageCallback = (message: BrokerMessage.ToRunner.All) => Promise<void> | void;
|
||||
export type RequesterMessageCallback = (
|
||||
message: N8nMessage.ToRequester.All,
|
||||
message: BrokerMessage.ToRequester.All,
|
||||
) => Promise<void> | void;
|
||||
|
||||
type RunnerAcceptCallback = () => void;
|
||||
type RequesterAcceptCallback = (settings: RequesterMessage.ToN8n.TaskSettings['settings']) => void;
|
||||
type RequesterAcceptCallback = (
|
||||
settings: RequesterMessage.ToBroker.TaskSettings['settings'],
|
||||
) => void;
|
||||
type TaskRejectCallback = (reason: TaskRejectError) => void;
|
||||
|
||||
@Service()
|
||||
|
@ -134,11 +141,11 @@ export class TaskBroker {
|
|||
this.requesters.delete(requesterId);
|
||||
}
|
||||
|
||||
private async messageRunner(runnerId: TaskRunner['id'], message: N8nMessage.ToRunner.All) {
|
||||
private async messageRunner(runnerId: TaskRunner['id'], message: BrokerMessage.ToRunner.All) {
|
||||
await this.knownRunners.get(runnerId)?.messageCallback(message);
|
||||
}
|
||||
|
||||
private async messageAllRunners(message: N8nMessage.ToRunner.All) {
|
||||
private async messageAllRunners(message: BrokerMessage.ToRunner.All) {
|
||||
await Promise.allSettled(
|
||||
[...this.knownRunners.values()].map(async (runner) => {
|
||||
await runner.messageCallback(message);
|
||||
|
@ -146,11 +153,11 @@ export class TaskBroker {
|
|||
);
|
||||
}
|
||||
|
||||
private async messageRequester(requesterId: string, message: N8nMessage.ToRequester.All) {
|
||||
private async messageRequester(requesterId: string, message: BrokerMessage.ToRequester.All) {
|
||||
await this.requesters.get(requesterId)?.(message);
|
||||
}
|
||||
|
||||
async onRunnerMessage(runnerId: TaskRunner['id'], message: RunnerMessage.ToN8n.All) {
|
||||
async onRunnerMessage(runnerId: TaskRunner['id'], message: RunnerMessage.ToBroker.All) {
|
||||
const runner = this.knownRunners.get(runnerId);
|
||||
if (!runner) {
|
||||
return;
|
||||
|
@ -193,7 +200,7 @@ export class TaskBroker {
|
|||
async handleRpcRequest(
|
||||
taskId: Task['id'],
|
||||
callId: string,
|
||||
name: RunnerMessage.ToN8n.RPC['name'],
|
||||
name: RunnerMessage.ToBroker.RPC['name'],
|
||||
params: unknown[],
|
||||
) {
|
||||
const task = this.tasks.get(taskId);
|
||||
|
@ -227,8 +234,8 @@ export class TaskBroker {
|
|||
|
||||
async handleDataRequest(
|
||||
taskId: Task['id'],
|
||||
requestId: RunnerMessage.ToN8n.TaskDataRequest['requestId'],
|
||||
requestParams: RunnerMessage.ToN8n.TaskDataRequest['requestParams'],
|
||||
requestId: RunnerMessage.ToBroker.TaskDataRequest['requestId'],
|
||||
requestParams: RunnerMessage.ToBroker.TaskDataRequest['requestParams'],
|
||||
) {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
|
@ -244,7 +251,7 @@ export class TaskBroker {
|
|||
|
||||
async handleResponse(
|
||||
taskId: Task['id'],
|
||||
requestId: RunnerMessage.ToN8n.TaskDataRequest['requestId'],
|
||||
requestId: RunnerMessage.ToBroker.TaskDataRequest['requestId'],
|
||||
data: unknown,
|
||||
) {
|
||||
const task = this.tasks.get(taskId);
|
||||
|
@ -259,7 +266,7 @@ export class TaskBroker {
|
|||
});
|
||||
}
|
||||
|
||||
async onRequesterMessage(requesterId: string, message: RequesterMessage.ToN8n.All) {
|
||||
async onRequesterMessage(requesterId: string, message: RequesterMessage.ToBroker.All) {
|
||||
switch (message.type) {
|
||||
case 'requester:tasksettings':
|
||||
this.handleRequesterAccept(message.taskId, message.settings);
|
||||
|
@ -291,7 +298,7 @@ export class TaskBroker {
|
|||
async handleRequesterRpcResponse(
|
||||
taskId: string,
|
||||
callId: string,
|
||||
status: RequesterMessage.ToN8n.RPCResponse['status'],
|
||||
status: RequesterMessage.ToBroker.RPCResponse['status'],
|
||||
data: unknown,
|
||||
) {
|
||||
const runner = await this.getRunnerOrFailTask(taskId);
|
||||
|
@ -317,7 +324,7 @@ export class TaskBroker {
|
|||
|
||||
handleRequesterAccept(
|
||||
taskId: Task['id'],
|
||||
settings: RequesterMessage.ToN8n.TaskSettings['settings'],
|
||||
settings: RequesterMessage.ToBroker.TaskSettings['settings'],
|
||||
) {
|
||||
const acceptReject = this.requesterAcceptRejects.get(taskId);
|
||||
if (acceptReject) {
|
||||
|
@ -467,10 +474,12 @@ export class TaskBroker {
|
|||
this.pendingTaskRequests.splice(requestIndex, 1);
|
||||
|
||||
try {
|
||||
const acceptPromise = new Promise<RequesterMessage.ToN8n.TaskSettings['settings']>(
|
||||
const acceptPromise = new Promise<RequesterMessage.ToBroker.TaskSettings['settings']>(
|
||||
(resolve, reject) => {
|
||||
this.requesterAcceptRejects.set(taskId, {
|
||||
accept: resolve as (settings: RequesterMessage.ToN8n.TaskSettings['settings']) => void,
|
||||
accept: resolve as (
|
||||
settings: RequesterMessage.ToBroker.TaskSettings['settings'],
|
||||
) => void,
|
||||
reject,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type { TaskData } from '@n8n/task-runner';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
|
||||
import { type INode, type INodeExecutionData, type Workflow } from 'n8n-workflow';
|
||||
|
||||
import { DataRequestResponseBuilder } from '../data-request-response-builder';
|
||||
import type { TaskData } from '../task-manager';
|
||||
|
||||
const triggerNode: INode = mock<INode>({
|
||||
name: 'Trigger',
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import type {
|
||||
DataRequestResponse,
|
||||
BrokerMessage,
|
||||
PartialAdditionalData,
|
||||
TaskData,
|
||||
} from '@n8n/task-runner';
|
||||
import type {
|
||||
EnvProviderState,
|
||||
IExecuteData,
|
||||
|
@ -11,9 +17,6 @@ import type {
|
|||
WorkflowParameters,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { DataRequestResponse, PartialAdditionalData, TaskData } from './task-manager';
|
||||
import type { N8nMessage } from '../runner-types';
|
||||
|
||||
/**
|
||||
* Builds the response to a data request coming from a Task Runner. Tries to minimize
|
||||
* the amount of data that is sent to the runner by only providing what is requested.
|
||||
|
@ -23,7 +26,7 @@ export class DataRequestResponseBuilder {
|
|||
|
||||
constructor(
|
||||
private readonly taskData: TaskData,
|
||||
private readonly requestParams: N8nMessage.ToRequester.TaskDataRequest['requestParams'],
|
||||
private readonly requestParams: BrokerMessage.ToRequester.TaskDataRequest['requestParams'],
|
||||
) {
|
||||
this.requestedNodeNames = new Set(requestParams.dataOfNodes);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { RequesterMessage } from '@n8n/task-runner';
|
||||
import Container from 'typedi';
|
||||
|
||||
import { TaskManager } from './task-manager';
|
||||
import type { RequesterMessage } from '../runner-types';
|
||||
import type { RequesterMessageCallback } from '../task-broker.service';
|
||||
import { TaskBroker } from '../task-broker.service';
|
||||
|
||||
|
@ -24,7 +24,7 @@ export class LocalTaskManager extends TaskManager {
|
|||
);
|
||||
}
|
||||
|
||||
sendMessage(message: RequesterMessage.ToN8n.All) {
|
||||
sendMessage(message: RequesterMessage.ToBroker.All) {
|
||||
void this.taskBroker.onRequesterMessage(this.id, message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,24 @@
|
|||
import {
|
||||
type EnvProviderState,
|
||||
type IExecuteFunctions,
|
||||
type Workflow,
|
||||
type IRunExecutionData,
|
||||
type INodeExecutionData,
|
||||
type ITaskDataConnections,
|
||||
type INode,
|
||||
type WorkflowParameters,
|
||||
type INodeParameters,
|
||||
type WorkflowExecuteMode,
|
||||
type IExecuteData,
|
||||
type IDataObject,
|
||||
type IWorkflowExecuteAdditionalData,
|
||||
type Result,
|
||||
createResultOk,
|
||||
createResultError,
|
||||
import type { TaskResultData, RequesterMessage, BrokerMessage, TaskData } from '@n8n/task-runner';
|
||||
import { RPC_ALLOW_LIST } from '@n8n/task-runner';
|
||||
import type {
|
||||
EnvProviderState,
|
||||
IExecuteFunctions,
|
||||
Workflow,
|
||||
IRunExecutionData,
|
||||
INodeExecutionData,
|
||||
ITaskDataConnections,
|
||||
INode,
|
||||
INodeParameters,
|
||||
WorkflowExecuteMode,
|
||||
IExecuteData,
|
||||
IDataObject,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
Result,
|
||||
} from 'n8n-workflow';
|
||||
import { createResultOk, createResultError } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { DataRequestResponseBuilder } from './data-request-response-builder';
|
||||
import {
|
||||
RPC_ALLOW_LIST,
|
||||
type TaskResultData,
|
||||
type N8nMessage,
|
||||
type RequesterMessage,
|
||||
} from '../runner-types';
|
||||
|
||||
export type RequestAccept = (jobId: string) => void;
|
||||
export type RequestReject = (reason: string) => void;
|
||||
|
@ -32,62 +26,6 @@ export type RequestReject = (reason: string) => void;
|
|||
export type TaskAccept = (data: TaskResultData) => void;
|
||||
export type TaskReject = (error: unknown) => void;
|
||||
|
||||
export interface TaskData {
|
||||
executeFunctions: IExecuteFunctions;
|
||||
inputData: ITaskDataConnections;
|
||||
node: INode;
|
||||
|
||||
workflow: Workflow;
|
||||
runExecutionData: IRunExecutionData;
|
||||
runIndex: number;
|
||||
itemIndex: number;
|
||||
activeNodeName: string;
|
||||
connectionInputData: INodeExecutionData[];
|
||||
siblingParameters: INodeParameters;
|
||||
mode: WorkflowExecuteMode;
|
||||
envProviderState: EnvProviderState;
|
||||
executeData?: IExecuteData;
|
||||
defaultReturnRunIndex: number;
|
||||
selfData: IDataObject;
|
||||
contextNodeName: string;
|
||||
additionalData: IWorkflowExecuteAdditionalData;
|
||||
}
|
||||
|
||||
export interface PartialAdditionalData {
|
||||
executionId?: string;
|
||||
restartExecutionId?: string;
|
||||
restApiUrl: string;
|
||||
instanceBaseUrl: string;
|
||||
formWaitingBaseUrl: string;
|
||||
webhookBaseUrl: string;
|
||||
webhookWaitingBaseUrl: string;
|
||||
webhookTestBaseUrl: string;
|
||||
currentNodeParameters?: INodeParameters;
|
||||
executionTimeoutTimestamp?: number;
|
||||
userId?: string;
|
||||
variables: IDataObject;
|
||||
}
|
||||
|
||||
export interface DataRequestResponse {
|
||||
workflow: Omit<WorkflowParameters, 'nodeTypes'>;
|
||||
inputData: ITaskDataConnections;
|
||||
node: INode;
|
||||
|
||||
runExecutionData: IRunExecutionData;
|
||||
runIndex: number;
|
||||
itemIndex: number;
|
||||
activeNodeName: string;
|
||||
connectionInputData: INodeExecutionData[];
|
||||
siblingParameters: INodeParameters;
|
||||
mode: WorkflowExecuteMode;
|
||||
envProviderState: EnvProviderState;
|
||||
executeData?: IExecuteData;
|
||||
defaultReturnRunIndex: number;
|
||||
selfData: IDataObject;
|
||||
contextNodeName: string;
|
||||
additionalData: PartialAdditionalData;
|
||||
}
|
||||
|
||||
export interface TaskRequest {
|
||||
requestId: string;
|
||||
taskType: string;
|
||||
|
@ -219,9 +157,9 @@ export class TaskManager {
|
|||
}
|
||||
}
|
||||
|
||||
sendMessage(_message: RequesterMessage.ToN8n.All) {}
|
||||
sendMessage(_message: RequesterMessage.ToBroker.All) {}
|
||||
|
||||
onMessage(message: N8nMessage.ToRequester.All) {
|
||||
onMessage(message: BrokerMessage.ToRequester.All) {
|
||||
switch (message.type) {
|
||||
case 'broker:taskready':
|
||||
this.taskReady(message.requestId, message.taskId);
|
||||
|
@ -282,7 +220,7 @@ export class TaskManager {
|
|||
sendTaskData(
|
||||
taskId: string,
|
||||
requestId: string,
|
||||
requestParams: N8nMessage.ToRequester.TaskDataRequest['requestParams'],
|
||||
requestParams: BrokerMessage.ToRequester.TaskDataRequest['requestParams'],
|
||||
) {
|
||||
const job = this.tasks.get(taskId);
|
||||
if (!job) {
|
||||
|
@ -304,7 +242,7 @@ export class TaskManager {
|
|||
async handleRpc(
|
||||
taskId: string,
|
||||
callId: string,
|
||||
name: N8nMessage.ToRequester.RPC['name'],
|
||||
name: BrokerMessage.ToRequester.RPC['name'],
|
||||
params: unknown[],
|
||||
) {
|
||||
const job = this.tasks.get(taskId);
|
||||
|
|
|
@ -92,7 +92,7 @@ export class TaskRunnerProcess extends TypedEmitter<TaskRunnerProcessEventMap> {
|
|||
}
|
||||
|
||||
startNode(grantToken: string, n8nUri: string) {
|
||||
const startScript = require.resolve('@n8n/task-runner');
|
||||
const startScript = require.resolve('@n8n/task-runner/start');
|
||||
|
||||
return spawn('node', [startScript], {
|
||||
env: this.getProcessEnvVars(grantToken, n8nUri),
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
import type { GlobalConfig } from '@n8n/config';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { InstanceSettings } from 'n8n-core';
|
||||
|
||||
import type { MultiMainSetup } from '@/scaling/multi-main-setup.ee';
|
||||
import type { OrchestrationService } from '@/services/orchestration.service';
|
||||
import { mockLogger } from '@test/mocking';
|
||||
|
||||
import { PruningService } from '../pruning.service';
|
||||
|
||||
describe('PruningService', () => {
|
||||
describe('init', () => {
|
||||
it('should start pruning if leader', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock(),
|
||||
);
|
||||
const startPruningSpy = jest.spyOn(pruningService, 'startPruning');
|
||||
|
||||
pruningService.init();
|
||||
|
||||
expect(startPruningSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not start pruning if follower', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: false }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock(),
|
||||
);
|
||||
const startPruningSpy = jest.spyOn(pruningService, 'startPruning');
|
||||
|
||||
pruningService.init();
|
||||
|
||||
expect(startPruningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should register leadership events if multi-main setup is enabled', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>({ on: jest.fn() }),
|
||||
}),
|
||||
mock(),
|
||||
);
|
||||
|
||||
pruningService.init();
|
||||
|
||||
// @ts-expect-error Private method
|
||||
expect(pruningService.orchestrationService.multiMainSetup.on).toHaveBeenCalledWith(
|
||||
'leader-takeover',
|
||||
expect.any(Function),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
expect(pruningService.orchestrationService.multiMainSetup.on).toHaveBeenCalledWith(
|
||||
'leader-stepdown',
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEnabled', () => {
|
||||
it('should return `true` based on config if leader main', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true, instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: true } }),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const isEnabled = pruningService.isEnabled();
|
||||
|
||||
expect(isEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return `false` based on config if leader main', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true, instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: false } }),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const isEnabled = pruningService.isEnabled();
|
||||
|
||||
expect(isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return `false` if non-main even if enabled', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: false, instanceType: 'worker' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: true } }),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const isEnabled = pruningService.isEnabled();
|
||||
|
||||
expect(isEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should return `false` if follower main even if enabled', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: false, isFollower: true, instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: true }, multiMainSetup: { enabled: true } }),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const isEnabled = pruningService.isEnabled();
|
||||
|
||||
expect(isEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('startPruning', () => {
|
||||
it('should not start pruning if service is disabled', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true, instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: false } }),
|
||||
);
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const setSoftDeletionInterval = jest.spyOn(pruningService, 'setSoftDeletionInterval');
|
||||
|
||||
// @ts-expect-error Private method
|
||||
const scheduleHardDeletion = jest.spyOn(pruningService, 'scheduleHardDeletion');
|
||||
|
||||
pruningService.startPruning();
|
||||
|
||||
expect(setSoftDeletionInterval).not.toHaveBeenCalled();
|
||||
expect(scheduleHardDeletion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should start pruning if service is enabled', () => {
|
||||
const pruningService = new PruningService(
|
||||
mockLogger(),
|
||||
mock<InstanceSettings>({ isLeader: true, instanceType: 'main' }),
|
||||
mock(),
|
||||
mock(),
|
||||
mock<OrchestrationService>({
|
||||
isMultiMainSetupEnabled: true,
|
||||
multiMainSetup: mock<MultiMainSetup>(),
|
||||
}),
|
||||
mock<GlobalConfig>({ pruning: { isEnabled: true } }),
|
||||
);
|
||||
|
||||
const setSoftDeletionInterval = jest
|
||||
// @ts-expect-error Private method
|
||||
.spyOn(pruningService, 'setSoftDeletionInterval')
|
||||
.mockImplementation();
|
||||
|
||||
const scheduleHardDeletion = jest
|
||||
// @ts-expect-error Private method
|
||||
.spyOn(pruningService, 'scheduleHardDeletion')
|
||||
.mockImplementation();
|
||||
|
||||
pruningService.startPruning();
|
||||
|
||||
expect(setSoftDeletionInterval).toHaveBeenCalled();
|
||||
expect(scheduleHardDeletion).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,12 +3,12 @@ import { BinaryDataService, InstanceSettings } from 'n8n-core';
|
|||
import { jsonStringify } from 'n8n-workflow';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { inTest, TIME } from '@/constants';
|
||||
import { TIME } from '@/constants';
|
||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
import { OnShutdown } from '@/decorators/on-shutdown';
|
||||
import { Logger } from '@/logging/logger.service';
|
||||
|
||||
import { OrchestrationService } from './orchestration.service';
|
||||
import { OrchestrationService } from '../orchestration.service';
|
||||
|
||||
@Service()
|
||||
export class PruningService {
|
||||
|
@ -32,7 +32,9 @@ export class PruningService {
|
|||
private readonly binaryDataService: BinaryDataService,
|
||||
private readonly orchestrationService: OrchestrationService,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
) {}
|
||||
) {
|
||||
this.logger = this.logger.scoped('pruning');
|
||||
}
|
||||
|
||||
/**
|
||||
* @important Requires `OrchestrationService` to be initialized.
|
||||
|
@ -49,9 +51,9 @@ export class PruningService {
|
|||
}
|
||||
}
|
||||
|
||||
private isPruningEnabled() {
|
||||
private isEnabled() {
|
||||
const { instanceType, isFollower } = this.instanceSettings;
|
||||
if (!this.globalConfig.pruning.isEnabled || inTest || instanceType !== 'main') {
|
||||
if (!this.globalConfig.pruning.isEnabled || instanceType !== 'main') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -66,23 +68,23 @@ export class PruningService {
|
|||
* @important Call this method only after DB migrations have completed.
|
||||
*/
|
||||
startPruning() {
|
||||
if (!this.isPruningEnabled()) return;
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
if (this.isShuttingDown) {
|
||||
this.logger.warn('[Pruning] Cannot start pruning while shutting down');
|
||||
this.logger.warn('Cannot start pruning while shutting down');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug('[Pruning] Starting soft-deletion and hard-deletion timers');
|
||||
this.logger.debug('Starting soft-deletion and hard-deletion timers');
|
||||
|
||||
this.setSoftDeletionInterval();
|
||||
this.scheduleHardDeletion();
|
||||
}
|
||||
|
||||
stopPruning() {
|
||||
if (!this.isPruningEnabled()) return;
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
this.logger.debug('[Pruning] Removing soft-deletion and hard-deletion timers');
|
||||
this.logger.debug('Removing soft-deletion and hard-deletion timers');
|
||||
|
||||
clearInterval(this.softDeletionInterval);
|
||||
clearTimeout(this.hardDeletionTimeout);
|
||||
|
@ -96,7 +98,7 @@ export class PruningService {
|
|||
this.rates.softDeletion,
|
||||
);
|
||||
|
||||
this.logger.debug(`[Pruning] Soft-deletion scheduled every ${when}`);
|
||||
this.logger.debug(`Soft-deletion scheduled every ${when}`);
|
||||
}
|
||||
|
||||
private scheduleHardDeletion(rateMs = this.rates.hardDeletion) {
|
||||
|
@ -113,27 +115,27 @@ export class PruningService {
|
|||
? error.message
|
||||
: jsonStringify(error, { replaceCircularRefs: true });
|
||||
|
||||
this.logger.error('[Pruning] Failed to hard-delete executions', { errorMessage });
|
||||
this.logger.error('Failed to hard-delete executions', { errorMessage });
|
||||
});
|
||||
}, rateMs);
|
||||
|
||||
this.logger.debug(`[Pruning] Hard-deletion scheduled for next ${when}`);
|
||||
this.logger.debug(`Hard-deletion scheduled for next ${when}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark executions as deleted based on age and count, in a pruning cycle.
|
||||
*/
|
||||
async softDeleteOnPruningCycle() {
|
||||
this.logger.debug('[Pruning] Starting soft-deletion of executions');
|
||||
this.logger.debug('Starting soft-deletion of executions');
|
||||
|
||||
const result = await this.executionRepository.softDeletePrunableExecutions();
|
||||
|
||||
if (result.affected === 0) {
|
||||
this.logger.debug('[Pruning] Found no executions to soft-delete');
|
||||
this.logger.debug('Found no executions to soft-delete');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug('[Pruning] Soft-deleted executions', { count: result.affected });
|
||||
this.logger.debug('Soft-deleted executions', { count: result.affected });
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
|
@ -147,26 +149,26 @@ export class PruningService {
|
|||
* @return Delay in ms after which the next cycle should be started
|
||||
*/
|
||||
private async hardDeleteOnPruningCycle() {
|
||||
const ids = await this.executionRepository.hardDeleteSoftDeletedExecutions();
|
||||
const ids = await this.executionRepository.findSoftDeletedExecutions();
|
||||
|
||||
const executionIds = ids.map((o) => o.executionId);
|
||||
|
||||
if (executionIds.length === 0) {
|
||||
this.logger.debug('[Pruning] Found no executions to hard-delete');
|
||||
this.logger.debug('Found no executions to hard-delete');
|
||||
|
||||
return this.rates.hardDeletion;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.debug('[Pruning] Starting hard-deletion of executions', { executionIds });
|
||||
this.logger.debug('Starting hard-deletion of executions', { executionIds });
|
||||
|
||||
await this.binaryDataService.deleteMany(ids);
|
||||
|
||||
await this.executionRepository.deleteByIds(executionIds);
|
||||
|
||||
this.logger.debug('[Pruning] Hard-deleted executions', { executionIds });
|
||||
this.logger.debug('Hard-deleted executions', { executionIds });
|
||||
} catch (error) {
|
||||
this.logger.error('[Pruning] Failed to hard-delete executions', {
|
||||
this.logger.error('Failed to hard-delete executions', {
|
||||
executionIds,
|
||||
error: error instanceof Error ? error.message : `${error}`,
|
||||
});
|
|
@ -8,8 +8,7 @@ import { TIME } from '@/constants';
|
|||
import type { ExecutionEntity } from '@/databases/entities/execution-entity';
|
||||
import type { WorkflowEntity } from '@/databases/entities/workflow-entity';
|
||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
import { Logger } from '@/logging/logger.service';
|
||||
import { PruningService } from '@/services/pruning.service';
|
||||
import { PruningService } from '@/services/pruning/pruning.service';
|
||||
|
||||
import {
|
||||
annotateExecution,
|
||||
|
@ -18,7 +17,7 @@ import {
|
|||
} from './shared/db/executions';
|
||||
import { createWorkflow } from './shared/db/workflows';
|
||||
import * as testDb from './shared/test-db';
|
||||
import { mockInstance } from '../shared/mocking';
|
||||
import { mockInstance, mockLogger } from '../shared/mocking';
|
||||
|
||||
describe('softDeleteOnPruningCycle()', () => {
|
||||
let pruningService: PruningService;
|
||||
|
@ -35,7 +34,7 @@ describe('softDeleteOnPruningCycle()', () => {
|
|||
|
||||
globalConfig = Container.get(GlobalConfig);
|
||||
pruningService = new PruningService(
|
||||
mockInstance(Logger),
|
||||
mockLogger(),
|
||||
instanceSettings,
|
||||
Container.get(ExecutionRepository),
|
||||
mockInstance(BinaryDataService),
|
||||
|
|
|
@ -8,6 +8,7 @@ import { TaskRunnerWsServer } from '../../../src/runners/runner-ws-server';
|
|||
|
||||
describe('TaskRunnerModule in internal_childprocess mode', () => {
|
||||
const runnerConfig = Container.get(TaskRunnersConfig);
|
||||
runnerConfig.port = 0; // Random port
|
||||
runnerConfig.mode = 'internal_childprocess';
|
||||
const module = Container.get(TaskRunnerModule);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import AssistantAvatar from '../AssistantAvatar.vue';
|
||||
import AssistantAvatar from './AssistantAvatar.vue';
|
||||
|
||||
describe('AskAssistantAvatar', () => {
|
||||
it('renders small avatar correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import AskAssistantButton from '../AskAssistantButton.vue';
|
||||
import AskAssistantButton from './AskAssistantButton.vue';
|
||||
|
||||
describe('AskAssistantButton', () => {
|
||||
it('renders default button correctly', () => {
|
|
@ -2,7 +2,7 @@ import { render } from '@testing-library/vue';
|
|||
|
||||
import { n8nHtml } from 'n8n-design-system/directives';
|
||||
|
||||
import AskAssistantChat from '../AskAssistantChat.vue';
|
||||
import AskAssistantChat from './AskAssistantChat.vue';
|
||||
|
||||
const stubs = ['n8n-avatar', 'n8n-button', 'n8n-icon', 'n8n-icon-button'];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import AssistantIcon from '../AssistantIcon.vue';
|
||||
import AssistantIcon from './AssistantIcon.vue';
|
||||
|
||||
describe('AssistantIcon', () => {
|
||||
it('renders default icon correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import AssistantLoadingMessage from '../AssistantLoadingMessage.vue';
|
||||
import AssistantLoadingMessage from './AssistantLoadingMessage.vue';
|
||||
|
||||
describe('AssistantLoadingMessage', () => {
|
||||
it('renders loading message correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import AssistantText from '../AssistantText.vue';
|
||||
import AssistantText from './AssistantText.vue';
|
||||
|
||||
describe('AssistantText', () => {
|
||||
it('renders default text correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import BetaTag from '../BetaTag.vue';
|
||||
import BetaTag from './BetaTag.vue';
|
||||
|
||||
describe('BetaTag', () => {
|
||||
it('renders beta tag correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import BlinkingCursor from '../BlinkingCursor.vue';
|
||||
import BlinkingCursor from './BlinkingCursor.vue';
|
||||
|
||||
describe('BlinkingCursor', () => {
|
||||
it('renders blinking cursor correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import CodeDiff from '../CodeDiff.vue';
|
||||
import CodeDiff from './CodeDiff.vue';
|
||||
|
||||
const stubs = ['n8n-button', 'n8n-icon'];
|
||||
|
|
@ -2,7 +2,7 @@ import { render } from '@testing-library/vue';
|
|||
import { beforeAll, describe } from 'vitest';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import CondtionalRouterLink from '../CondtionalRouterLink.vue';
|
||||
import CondtionalRouterLink from './CondtionalRouterLink.vue';
|
||||
|
||||
const slots = {
|
||||
default: 'Button',
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8NActionBox from '../ActionBox.vue';
|
||||
import N8NActionBox from './ActionBox.vue';
|
||||
|
||||
describe('N8NActionBox', () => {
|
||||
it('should render correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nActionDropdown from '../ActionDropdown.vue';
|
||||
import N8nActionDropdown from './ActionDropdown.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nActionDropdown', () => {
|
|
@ -1,7 +1,7 @@
|
|||
import { render, screen } from '@testing-library/vue';
|
||||
|
||||
import N8nIcon from '../../N8nIcon';
|
||||
import N8nAlert from '../Alert.vue';
|
||||
import N8nAlert from './Alert.vue';
|
||||
import N8nIcon from '../N8nIcon';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nAlert', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nAvatar from '../Avatar.vue';
|
||||
import N8nAvatar from './Avatar.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nAlert', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nBadge from '../Badge.vue';
|
||||
import N8nBadge from './Badge.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nBadge', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render, screen } from '@testing-library/vue';
|
||||
|
||||
import N8nBlockUi from '../BlockUi.vue';
|
||||
import N8nBlockUi from './BlockUi.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nBlockUi', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nButton from '../Button.vue';
|
||||
import N8nButton from './Button.vue';
|
||||
|
||||
const slots = {
|
||||
default: 'Button',
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nCallout from '../Callout.vue';
|
||||
import N8nCallout from './Callout.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nCallout', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nCard from '../Card.vue';
|
||||
import N8nCard from './Card.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nCard', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nCheckbox from '../Checkbox.vue';
|
||||
import N8nCheckbox from './Checkbox.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nCheckbox', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8NCircleLoader from '../CircleLoader.vue';
|
||||
import N8NCircleLoader from './CircleLoader.vue';
|
||||
|
||||
describe('N8NCircleLoader', () => {
|
||||
it('should render correctly', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nColorPicker from '../ColorPicker.vue';
|
||||
import N8nColorPicker from './ColorPicker.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nColorPicker', () => {
|
|
@ -2,8 +2,8 @@ import { render } from '@testing-library/vue';
|
|||
|
||||
import { removeDynamicAttributes } from 'n8n-design-system/utils';
|
||||
|
||||
import { rows, columns } from './data';
|
||||
import N8nDatatable from '../Datatable.vue';
|
||||
import { rows, columns } from './__tests__/data';
|
||||
import N8nDatatable from './Datatable.vue';
|
||||
|
||||
const stubs = [
|
||||
'n8n-option',
|
|
@ -1,5 +1,5 @@
|
|||
import { createComponentRenderer } from '../../../__tests__/render';
|
||||
import FormBox from '../FormBox.vue';
|
||||
import FormBox from './FormBox.vue';
|
||||
import { createComponentRenderer } from '../../__tests__/render';
|
||||
|
||||
const render = createComponentRenderer(FormBox);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nInfoTip from '../InfoTip.vue';
|
||||
import N8nInfoTip from './InfoTip.vue';
|
||||
|
||||
const slots = {
|
||||
default: ['Need help doing something?', '<a href="/docs" target="_blank">Open docs</a>'],
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nInput from '../Input.vue';
|
||||
import N8nInput from './Input.vue';
|
||||
|
||||
describe('N8nInput', () => {
|
||||
it('should render correctly', () => {
|
|
@ -32,7 +32,7 @@ const props = withDefaults(defineProps<InputProps>(), {
|
|||
readonly: false,
|
||||
clearable: false,
|
||||
rows: 2,
|
||||
maxlength: Infinity,
|
||||
maxlength: undefined,
|
||||
title: '',
|
||||
name: () => uid('input'),
|
||||
autocomplete: 'off',
|
||||
|
@ -81,6 +81,7 @@ defineExpose({ focus, blur, select });
|
|||
:clearable="clearable"
|
||||
:rows="rows"
|
||||
:title="title"
|
||||
:maxlength="maxlength"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<template v-if="$slots.prepend" #prepend>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import InputLabel from '../InputLabel.vue';
|
||||
import InputLabel from './InputLabel.vue';
|
||||
|
||||
describe('component', () => {
|
||||
describe('Text overflow behavior', () => {
|
|
@ -2,7 +2,7 @@ import { render, fireEvent } from '@testing-library/vue';
|
|||
|
||||
import { n8nHtml } from 'n8n-design-system/directives';
|
||||
|
||||
import N8nMarkdown from '../Markdown.vue';
|
||||
import N8nMarkdown from './Markdown.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nMarkdown', () => {
|
|
@ -3,7 +3,7 @@ import { configure, render, waitFor } from '@testing-library/vue';
|
|||
import { h } from 'vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
import NavigationDropdown from '../NavigationDropdown.vue';
|
||||
import NavigationDropdown from './NavigationDropdown.vue';
|
||||
|
||||
configure({ testIdAttribute: 'data-test-id' });
|
||||
|
|
@ -3,7 +3,7 @@ import { render } from '@testing-library/vue';
|
|||
import { N8nText } from 'n8n-design-system/components';
|
||||
import { n8nHtml } from 'n8n-design-system/directives';
|
||||
|
||||
import N8nNotice from '../Notice.vue';
|
||||
import N8nNotice from './Notice.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nNotice', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nRecycleScroller from '../RecycleScroller.vue';
|
||||
import N8nRecycleScroller from './RecycleScroller.vue';
|
||||
|
||||
const itemSize = 100;
|
||||
const itemKey = 'id';
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nRoute from '../Route.vue';
|
||||
import N8nRoute from './Route.vue';
|
||||
|
||||
describe('N8nRoute', () => {
|
||||
it('should render internal router links', () => {
|
|
@ -4,8 +4,8 @@ import { defineComponent, ref } from 'vue';
|
|||
|
||||
import { removeDynamicAttributes } from 'n8n-design-system/utils';
|
||||
|
||||
import N8nOption from '../../N8nOption/Option.vue';
|
||||
import N8nSelect from '../Select.vue';
|
||||
import N8nSelect from './Select.vue';
|
||||
import N8nOption from '../N8nOption/Option.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nSelect', () => {
|
|
@ -1,7 +1,7 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nTooltip from '../Tooltip.vue';
|
||||
import N8nTooltip from './Tooltip.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nTooltip', () => {
|
|
@ -1,6 +1,6 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
|
||||
import N8nTree from '../Tree.vue';
|
||||
import N8nTree from './Tree.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nTree', () => {
|
|
@ -2,7 +2,7 @@ import { render } from '@testing-library/vue';
|
|||
|
||||
import { N8nAvatar, N8nUserInfo } from 'n8n-design-system/main';
|
||||
|
||||
import UserStack from '../UserStack.vue';
|
||||
import UserStack from './UserStack.vue';
|
||||
|
||||
describe('UserStack', () => {
|
||||
it('should render flat user list', () => {
|
|
@ -1,7 +1,7 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import { mock, mockClear } from 'vitest-mock-extended';
|
||||
|
||||
import ColorCircles from '../ColorCircles.vue';
|
||||
import ColorCircles from './ColorCircles.vue';
|
||||
|
||||
describe('ColorCircles', () => {
|
||||
const mockCssDeclaration = mock<CSSStyleDeclaration>();
|
|
@ -1,7 +1,7 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import { mock, mockClear } from 'vitest-mock-extended';
|
||||
|
||||
import Sizes from '../Sizes.vue';
|
||||
import Sizes from './Sizes.vue';
|
||||
|
||||
describe('Sizes', () => {
|
||||
const mockCssDeclaration = mock<CSSStyleDeclaration>();
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue