mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-12 15:44:06 -08:00
Compare commits
9 commits
6105bfeb4b
...
59c5ff6135
Author | SHA1 | Date | |
---|---|---|---|
59c5ff6135 | |||
948edd1a04 | |||
3c7556542c | |||
86069321a1 | |||
49c71469f4 | |||
4546649c61 | |||
8d9eb162ae | |||
9c43fb301d | |||
74fa259b37 |
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -1,3 +1,55 @@
|
|||
## [1.62.1](https://github.com/n8n-io/n8n/compare/n8n@1.61.0...n8n@1.62.1) (2024-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **AI Agent Node:** Fix output parsing and empty tool input handling in AI Agent node ([#10970](https://github.com/n8n-io/n8n/issues/10970)) ([3a65bdc](https://github.com/n8n-io/n8n/commit/3a65bdc1f522932d463b4da0e67d29076887d06c))
|
||||
* **API:** Fix workflow project transfer ([#10651](https://github.com/n8n-io/n8n/issues/10651)) ([5f89e3a](https://github.com/n8n-io/n8n/commit/5f89e3a01c1bbb3589ff0464fd5bc991426f55dc))
|
||||
* **AwsS3 Node:** Fix search only using first input parameters ([#10998](https://github.com/n8n-io/n8n/issues/10998)) ([846cfde](https://github.com/n8n-io/n8n/commit/846cfde8dcaf7bf80f0a4ca7d65fc2a7b61d0e23))
|
||||
* **Chat Trigger Node:** Fix Allowed Origins paramter ([#11011](https://github.com/n8n-io/n8n/issues/11011)) ([b5f4afe](https://github.com/n8n-io/n8n/commit/b5f4afe12ec77f527080a4b7f812e12f9f73f8df))
|
||||
* **core:** Fix ownerless project case in statistics service ([#11051](https://github.com/n8n-io/n8n/issues/11051)) ([bdaadf1](https://github.com/n8n-io/n8n/commit/bdaadf10e058e2c0b1141289189d6526c030a2ca))
|
||||
* **core:** Handle Redis disconnects gracefully ([#11007](https://github.com/n8n-io/n8n/issues/11007)) ([cd91648](https://github.com/n8n-io/n8n/commit/cd916480c2d2b55f2215c72309dc432340fc3f30))
|
||||
* **core:** Prevent backend from loading duplicate copies of nodes packages ([#10979](https://github.com/n8n-io/n8n/issues/10979)) ([4584f22](https://github.com/n8n-io/n8n/commit/4584f22a9b16883779d8555cda309fd8bd113f6c))
|
||||
* **core:** Upgrade @n8n/typeorm to address a rare mutex release issue ([#10993](https://github.com/n8n-io/n8n/issues/10993)) ([2af0fbf](https://github.com/n8n-io/n8n/commit/2af0fbf52f0b404697f5148f81ad0035c9ffb6b9))
|
||||
* **editor:** Allow resources to move between personal and team projects ([#10683](https://github.com/n8n-io/n8n/issues/10683)) ([136d491](https://github.com/n8n-io/n8n/commit/136d49132567558b7d27069c857c0e0bfee70ce2))
|
||||
* **editor:** Color scheme for a markdown code blocks in dark mode ([#11008](https://github.com/n8n-io/n8n/issues/11008)) ([b20d2eb](https://github.com/n8n-io/n8n/commit/b20d2eb403f71fe1dc21c92df118adcebef51ffe))
|
||||
* **editor:** Fix filter execution by "Queued" ([#10987](https://github.com/n8n-io/n8n/issues/10987)) ([819d20f](https://github.com/n8n-io/n8n/commit/819d20fa2eee314b88a7ce1c4db632afac514704))
|
||||
* **editor:** Fix performance issue in credentials list ([#10988](https://github.com/n8n-io/n8n/issues/10988)) ([7073ec6](https://github.com/n8n-io/n8n/commit/7073ec6fe5384cc8c50dcb242212999a1fbc9041))
|
||||
* **editor:** Fix schema view pill highlighting ([#10936](https://github.com/n8n-io/n8n/issues/10936)) ([1b973dc](https://github.com/n8n-io/n8n/commit/1b973dcd8dbce598e6ada490fd48fad52f7b4f3a))
|
||||
* **editor:** Fix workflow executions list page redirection ([#10981](https://github.com/n8n-io/n8n/issues/10981)) ([fe7d060](https://github.com/n8n-io/n8n/commit/fe7d0605681dc963f5e5d1607f9d40c5173e0f9f))
|
||||
* **editor:** Format action names properly when action is not defined ([#11030](https://github.com/n8n-io/n8n/issues/11030)) ([9c43fb3](https://github.com/n8n-io/n8n/commit/9c43fb301d1ccb82e42f46833e19587289803cd3))
|
||||
* **Elasticsearch Node:** Fix issue with self signed certificates not working ([#10954](https://github.com/n8n-io/n8n/issues/10954)) ([79622b5](https://github.com/n8n-io/n8n/commit/79622b5f267f2a4a53f3eb48e228939d6e3a9caa))
|
||||
* **Facebook Lead Ads Trigger Node:** Pagination fix in RLC ([#10956](https://github.com/n8n-io/n8n/issues/10956)) ([6322372](https://github.com/n8n-io/n8n/commit/632237261087ada0177b67922f9f48ca02ef1d9e))
|
||||
* **Github Document Loader Node:** Pass through apiUrl from credentials & fix log output ([#11049](https://github.com/n8n-io/n8n/issues/11049)) ([a7af981](https://github.com/n8n-io/n8n/commit/a7af98183c47a5e215869c8269729b0fb2f318b5))
|
||||
* **Google Sheets Node:** Updating on row_number using automatic matching ([#10940](https://github.com/n8n-io/n8n/issues/10940)) ([ed91495](https://github.com/n8n-io/n8n/commit/ed91495ebc1e09b89533ffef4b775eaa0139f365))
|
||||
* **HTTP Request Tool Node:** Remove default user agent header ([#10971](https://github.com/n8n-io/n8n/issues/10971)) ([5a99e93](https://github.com/n8n-io/n8n/commit/5a99e93f8d2c66d7dbcef382478badd63bc4a0b5))
|
||||
* **Postgres Node:** Falsy query parameters ignored ([#10960](https://github.com/n8n-io/n8n/issues/10960)) ([4a63cff](https://github.com/n8n-io/n8n/commit/4a63cff5ec722c810e3ff2bd7b0bb1e32f7f403b))
|
||||
* **Respond to Webhook Node:** Node does not work with Wait node ([#10992](https://github.com/n8n-io/n8n/issues/10992)) ([2df5a5b](https://github.com/n8n-io/n8n/commit/2df5a5b649f8ba3b747782d6d5045820aa74955d))
|
||||
* **RSS Feed Trigger Node:** Fix regression on missing timestamps ([#10991](https://github.com/n8n-io/n8n/issues/10991)) ([d2bc076](https://github.com/n8n-io/n8n/commit/d2bc0760e2b5c977fcc683f0a0281f099a9c538d))
|
||||
* **Supabase Node:** Fix issue with delete not always working ([#10952](https://github.com/n8n-io/n8n/issues/10952)) ([1944b46](https://github.com/n8n-io/n8n/commit/1944b46fd472bb59552b5fbf7783168a622a2bd2))
|
||||
* **Text Classifier Node:** Default system prompt template ([#11018](https://github.com/n8n-io/n8n/issues/11018)) ([77fec19](https://github.com/n8n-io/n8n/commit/77fec195d92e0fe23c60552a72e8c030cf7e5e5c))
|
||||
* **Todoist Node:** Fix listSearch filter bug in Todoist Node ([#10989](https://github.com/n8n-io/n8n/issues/10989)) ([c4b3272](https://github.com/n8n-io/n8n/commit/c4b327248d7aa1352e8d6acec5627ff406aea3d4))
|
||||
* **Todoist Node:** Make Section Name optional in Move Task operation ([#10732](https://github.com/n8n-io/n8n/issues/10732)) ([799006a](https://github.com/n8n-io/n8n/commit/799006a3cce6abe210469c839ae392d0c1aec486))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add more context to support chat ([#11014](https://github.com/n8n-io/n8n/issues/11014)) ([8a30f92](https://github.com/n8n-io/n8n/commit/8a30f92156d6a4fe73113bd3cdfb751b8c9ce4b4))
|
||||
* Add Sysdig API credentials for SecOps ([#7033](https://github.com/n8n-io/n8n/issues/7033)) ([a8d1a1e](https://github.com/n8n-io/n8n/commit/a8d1a1ea854fb2c69643b0a5738440b389121ca3))
|
||||
* **core:** Filter executions by project ID in internal API ([#10976](https://github.com/n8n-io/n8n/issues/10976)) ([06d749f](https://github.com/n8n-io/n8n/commit/06d749ffa7ced503141d8b07e22c47d971eb1623))
|
||||
* **core:** Implement Dynamic Parameters within regular nodes used as AI Tools ([#10862](https://github.com/n8n-io/n8n/issues/10862)) ([ef5b7cf](https://github.com/n8n-io/n8n/commit/ef5b7cf9b77b653111eb5b1d9de8116c9f6b9f92))
|
||||
* **editor:** Do not show error for remote options when credentials aren't specified ([#10944](https://github.com/n8n-io/n8n/issues/10944)) ([9fc3699](https://github.com/n8n-io/n8n/commit/9fc3699beb0c150909889ed17740a5cd9e0461c3))
|
||||
* **editor:** Enable drag and drop in code editors (Code/SQL/HTML) ([#10888](https://github.com/n8n-io/n8n/issues/10888)) ([af9e227](https://github.com/n8n-io/n8n/commit/af9e227ad4848995b9d82c72f814dbf9d1de506f))
|
||||
* **editor:** Overhaul document title management ([#10999](https://github.com/n8n-io/n8n/issues/10999)) ([bb28956](https://github.com/n8n-io/n8n/commit/bb2895689fb006897bc244271aca6f0bfa1839b9))
|
||||
* **editor:** Remove execution annotation feature flag ([#11020](https://github.com/n8n-io/n8n/issues/11020)) ([e7199db](https://github.com/n8n-io/n8n/commit/e7199dbfccdbdf1c4273f916e3006ca610c230e9))
|
||||
* **editor:** Support node-creator actions for vector store nodes ([#11032](https://github.com/n8n-io/n8n/issues/11032)) ([72b70d9](https://github.com/n8n-io/n8n/commit/72b70d9d98daeba654baf6785ff1ae234c73c977))
|
||||
* **Google BigQuery Node:** Return numeric values as integers ([#10943](https://github.com/n8n-io/n8n/issues/10943)) ([d7c1d24](https://github.com/n8n-io/n8n/commit/d7c1d24f74648740b2f425640909037ba06c5030))
|
||||
* **Invoice Ninja Node:** Add more query params to getAll requests ([#9238](https://github.com/n8n-io/n8n/issues/9238)) ([50b7238](https://github.com/n8n-io/n8n/commit/50b723836e70bbe405594f690b73057f9c33fbe4))
|
||||
* **Iterable Node:** Add support for EDC and USDC selection ([#10908](https://github.com/n8n-io/n8n/issues/10908)) ([0ca9c07](https://github.com/n8n-io/n8n/commit/0ca9c076ca51d313392e45c3b013f2e83aaea843))
|
||||
* **Question and Answer Chain Node:** Customize question and answer system prompt ([#10385](https://github.com/n8n-io/n8n/issues/10385)) ([08a27b3](https://github.com/n8n-io/n8n/commit/08a27b3148aac2282f64339ddc33ac7c90835d84))
|
||||
|
||||
|
||||
|
||||
# [1.61.0](https://github.com/n8n-io/n8n/compare/n8n@1.60.0...n8n@1.61.0) (2024-09-25)
|
||||
|
||||
|
||||
|
|
|
@ -73,4 +73,28 @@ describe('Workflows', () => {
|
|||
|
||||
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
|
||||
});
|
||||
|
||||
it('should respect tag querystring filter when listing workflows', () => {
|
||||
WorkflowsPage.getters.newWorkflowButtonCard().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_2.json', getUniqueWorkflowName('My New Workflow'));
|
||||
|
||||
cy.visit(WorkflowsPage.url);
|
||||
|
||||
WorkflowsPage.getters.createWorkflowButton().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
|
||||
|
||||
cy.visit(WorkflowsPage.url);
|
||||
|
||||
WorkflowsPage.getters.workflowFilterButton().click();
|
||||
|
||||
WorkflowsPage.getters.workflowTagsDropdown().click();
|
||||
|
||||
WorkflowsPage.getters.workflowTagItem('some-tag-1').click();
|
||||
|
||||
cy.reload();
|
||||
|
||||
WorkflowsPage.getters.workflowCards().should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('Resource Locator', () => {
|
|||
});
|
||||
|
||||
it('should show appropriate errors when search filter is required', () => {
|
||||
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On Pull Request');
|
||||
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On pull request');
|
||||
ndv.getters.resourceLocator('owner').should('be.visible');
|
||||
ndv.getters.resourceLocatorInput('owner').click();
|
||||
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-benchmark",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.1",
|
||||
"description": "Cli for running benchmark tests for n8n",
|
||||
"main": "dist/index",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/chat",
|
||||
"version": "0.26.0",
|
||||
"version": "0.27.1",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm build:vite && pnpm build:bundle",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -11,4 +11,12 @@ export class TaskRunnersConfig {
|
|||
|
||||
@Env('N8N_RUNNERS_AUTH_TOKEN')
|
||||
authToken: string = '';
|
||||
|
||||
/** IP address task runners server should listen on */
|
||||
@Env('N8N_RUNNERS_SERVER_PORT')
|
||||
port: number = 5679;
|
||||
|
||||
/** IP address task runners server should listen on */
|
||||
@Env('N8N_RUNNERS_SERVER_LISTEN_ADDRESS')
|
||||
listen_address: string = '127.0.0.1';
|
||||
}
|
||||
|
|
|
@ -225,6 +225,8 @@ describe('GlobalConfig', () => {
|
|||
disabled: true,
|
||||
path: '/runners',
|
||||
authToken: '',
|
||||
listen_address: '127.0.0.1',
|
||||
port: 5679,
|
||||
},
|
||||
sentry: {
|
||||
backendDsn: '',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-nodes-langchain",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"name": "@n8n/task-runner",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/start.js",
|
||||
"dev": "pnpm build && pnpm start",
|
||||
"build": "tsc -p ./tsconfig.build.json",
|
||||
"test": "jest",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -w -p ./tsconfig.build.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
"pnpm": ">=9.5"
|
||||
},
|
||||
"files": [
|
||||
"src/",
|
||||
"dist/",
|
||||
"package.json",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"devDependencies": {
|
||||
"@n8n_io/eslint-config": "^0.0.2",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^18.13.0",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-n8n-local-rules": "^1.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^48.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"prettier": "^3.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.7",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jmespath": "^0.16.0",
|
||||
"luxon": "^3.5.0",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"n8n-core": "workspace:*",
|
||||
"nanoid": "^3.3.6",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import * as a from 'node:assert/strict';
|
||||
|
||||
import { JsTaskRunner } from './code';
|
||||
import { authenticate } from './authenticator';
|
||||
|
||||
let _runner: JsTaskRunner;
|
||||
|
||||
type Config = {
|
||||
n8nUri: string;
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
function readAndParseConfig(): Config {
|
||||
const authToken = process.env.N8N_RUNNERS_AUTH_TOKEN;
|
||||
a.ok(authToken, 'Missing task runner auth token. Use N8N_RUNNERS_AUTH_TOKEN to configure it');
|
||||
|
||||
return {
|
||||
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? 'localhost:5678',
|
||||
authToken,
|
||||
};
|
||||
}
|
||||
|
||||
void (async function start() {
|
||||
const config = readAndParseConfig();
|
||||
|
||||
const grantToken = await authenticate({
|
||||
authToken: config.authToken,
|
||||
n8nUri: config.n8nUri,
|
||||
});
|
||||
|
||||
const wsUrl = `ws://${config.n8nUri}/rest/runners/_ws`;
|
||||
|
||||
_runner = new JsTaskRunner('javascript', wsUrl, grantToken, 5);
|
||||
})();
|
29
packages/@n8n/task-runner/package.json
Normal file
29
packages/@n8n/task-runner/package.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@n8n/task-runner",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"start": "node dist/start.js",
|
||||
"dev": "pnpm build && pnpm start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p ./tsconfig.build.json",
|
||||
"format": "biome format --write src",
|
||||
"format:check": "biome ci src",
|
||||
"test": "echo \"Error: no tests in this package\" && exit 0",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch"
|
||||
},
|
||||
"main": "dist/start.js",
|
||||
"module": "src/start.ts",
|
||||
"types": "dist/start.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"n8n-workflow": "workspace:*",
|
||||
"n8n-core": "workspace:*",
|
||||
"nanoid": "^3.3.6",
|
||||
"ws": "^8.18.0"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { ApplicationError } from 'n8n-workflow';
|
||||
import * as a from 'node:assert/strict';
|
||||
|
||||
export type AuthOpts = {
|
||||
|
@ -23,7 +24,9 @@ export async function authenticate(opts: AuthOpts) {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Invalid response status ${response.status}: ${await response.text()}`);
|
||||
throw new ApplicationError(
|
||||
`Invalid response status ${response.status}: ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { data } = (await response.json()) as { data: { token: string } };
|
||||
|
@ -34,9 +37,11 @@ export async function authenticate(opts: AuthOpts) {
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
const error = e as Error;
|
||||
|
||||
throw new Error(`Could not connect to n8n message broker ${opts.n8nUri}: ${error.message}`, {
|
||||
cause: error,
|
||||
});
|
||||
throw new ApplicationError(
|
||||
`Could not connect to n8n message broker ${opts.n8nUri}: ${error.message}`,
|
||||
{
|
||||
cause: error,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { runInNewContext, type Context } from 'node:vm';
|
||||
import * as a from 'node:assert';
|
||||
|
||||
import { getAdditionalKeys } from 'n8n-core';
|
||||
import {
|
||||
type INode,
|
||||
type INodeType,
|
||||
|
@ -8,8 +6,6 @@ import {
|
|||
type IWorkflowExecuteAdditionalData,
|
||||
WorkflowDataProxy,
|
||||
type WorkflowParameters,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
type IDataObject,
|
||||
type IExecuteData,
|
||||
type INodeExecutionData,
|
||||
|
@ -19,7 +15,8 @@ import {
|
|||
Workflow,
|
||||
type WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import { getAdditionalKeys } from 'n8n-core';
|
||||
import * as a from 'node:assert';
|
||||
import { runInNewContext, type Context } from 'node:vm';
|
||||
|
||||
import type { TaskResultData } from './runner-types';
|
||||
import { type Task, TaskRunner } from './task-runner';
|
48
packages/@n8n/task-runner/src/start.ts
Normal file
48
packages/@n8n/task-runner/src/start.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { ApplicationError, ensureError } from 'n8n-workflow';
|
||||
import * as a from 'node:assert/strict';
|
||||
|
||||
import { authenticate } from './authenticator';
|
||||
import { JsTaskRunner } from './code';
|
||||
|
||||
type Config = {
|
||||
n8nUri: string;
|
||||
authToken?: string;
|
||||
grantToken?: string;
|
||||
};
|
||||
|
||||
function readAndParseConfig(): Config {
|
||||
const authToken = process.env.N8N_RUNNERS_AUTH_TOKEN;
|
||||
const grantToken = process.env.N8N_RUNNERS_GRANT_TOKEN;
|
||||
if (!authToken && !grantToken) {
|
||||
throw new ApplicationError(
|
||||
'Missing task runner authentication. Use either N8N_RUNNERS_AUTH_TOKEN or N8N_RUNNERS_GRANT_TOKEN to configure it',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
n8nUri: process.env.N8N_RUNNERS_N8N_URI ?? 'localhost:5678',
|
||||
authToken,
|
||||
grantToken,
|
||||
};
|
||||
}
|
||||
|
||||
void (async function start() {
|
||||
const config = readAndParseConfig();
|
||||
|
||||
let grantToken = config.grantToken;
|
||||
if (!grantToken) {
|
||||
a.ok(config.authToken);
|
||||
|
||||
grantToken = await authenticate({
|
||||
authToken: config.authToken,
|
||||
n8nUri: config.n8nUri,
|
||||
});
|
||||
}
|
||||
|
||||
const wsUrl = `ws://${config.n8nUri}/runners/_ws`;
|
||||
new JsTaskRunner('javascript', wsUrl, grantToken, 5);
|
||||
})().catch((e) => {
|
||||
const error = ensureError(e);
|
||||
console.error('Task runner failed to start', { error });
|
||||
process.exit(1);
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import { URL } from 'node:url';
|
||||
import { ApplicationError, ensureError } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { URL } from 'node:url';
|
||||
import { type MessageEvent, WebSocket } from 'ws';
|
||||
import { ensureError } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
RPC_ALLOW_LIST,
|
||||
|
@ -267,7 +267,7 @@ export abstract class TaskRunner {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
async executeTask(_task: Task): Promise<TaskResultData> {
|
||||
throw new Error('Unimplemented');
|
||||
throw new ApplicationError('Unimplemented');
|
||||
}
|
||||
|
||||
async requestData<T = unknown>(
|
||||
|
@ -354,7 +354,7 @@ export abstract class TaskRunner {
|
|||
obj = obj[s];
|
||||
return;
|
||||
}
|
||||
obj[s] = async (...args: unknown[]) => this.makeRpcCall(taskId, r, args);
|
||||
obj[s] = async (...args: unknown[]) => await this.makeRpcCall(taskId, r, args);
|
||||
});
|
||||
}
|
||||
return rpcObject;
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"extends": ["./tsconfig.json", "../../../tsconfig.build.json"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["test/**", "src/**/__tests__/**"]
|
||||
"exclude": ["src/**/__tests__/**"]
|
||||
}
|
|
@ -2,13 +2,11 @@
|
|||
"extends": ["../../../tsconfig.json", "../../../tsconfig.backend.json"],
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"tsBuildInfoFile": "dist/typecheck.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts", "test/**/*.ts"]
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
|
@ -2,6 +2,16 @@
|
|||
|
||||
This list shows all the versions which include breaking changes and how to upgrade.
|
||||
|
||||
## 1.57.0
|
||||
|
||||
### What changed?
|
||||
|
||||
The `verbose` log level was merged into the `debug` log level.
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
If you are setting the env var `N8N_LOG_LEVEL=verbose`, please update your log level to `N8N_LOG_LEVEL=debug`.
|
||||
|
||||
## 1.55.0
|
||||
|
||||
### What changed?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -119,8 +119,6 @@ export abstract class AbstractServer {
|
|||
|
||||
protected setupPushServer() {}
|
||||
|
||||
protected setupRunnerServer() {}
|
||||
|
||||
private async setupHealthCheck() {
|
||||
// main health check should not care about DB connections
|
||||
this.app.get('/healthz', async (_req, res) => {
|
||||
|
@ -184,10 +182,6 @@ export abstract class AbstractServer {
|
|||
if (!inTest) {
|
||||
await this.setupErrorHandlers();
|
||||
this.setupPushServer();
|
||||
|
||||
if (!this.globalConfig.taskRunners.disabled) {
|
||||
this.setupRunnerServer();
|
||||
}
|
||||
}
|
||||
|
||||
this.setupCommonMiddlewares();
|
||||
|
|
|
@ -225,6 +225,13 @@ export class Start extends BaseCommand {
|
|||
|
||||
if (!this.globalConfig.taskRunners.disabled) {
|
||||
Container.set(TaskManager, new SingleMainTaskManager());
|
||||
const { TaskRunnerServer } = await import('@/runners/task-runner-server');
|
||||
const taskRunnerServer = Container.get(TaskRunnerServer);
|
||||
await taskRunnerServer.start();
|
||||
|
||||
const { TaskRunnerProcess } = await import('@/runners/task-runner-process');
|
||||
const runnerProcess = Container.get(TaskRunnerProcess);
|
||||
await runnerProcess.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,6 +168,8 @@ export const ARTIFICIAL_TASK_DATA = {
|
|||
],
|
||||
};
|
||||
|
||||
/** Lowest priority, meaning shut down happens after other groups */
|
||||
export const LOWEST_SHUTDOWN_PRIORITY = 0;
|
||||
export const DEFAULT_SHUTDOWN_PRIORITY = 100;
|
||||
/** Highest priority, meaning shut down happens before all other groups */
|
||||
export const HIGHEST_SHUTDOWN_PRIORITY = 200;
|
||||
|
|
|
@ -44,7 +44,7 @@ function getWsEndpoint(restEndpoint: string) {
|
|||
|
||||
@Service()
|
||||
export class TaskRunnerService {
|
||||
runnerConnections: Record<TaskRunner['id'], WebSocket> = {};
|
||||
runnerConnections: Map<TaskRunner['id'], WebSocket> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
|
@ -52,7 +52,7 @@ export class TaskRunnerService {
|
|||
) {}
|
||||
|
||||
sendMessage(id: TaskRunner['id'], message: N8nMessage.ToRunner.All) {
|
||||
this.runnerConnections[id]?.send(JSON.stringify(message));
|
||||
this.runnerConnections.get(id)?.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
add(id: TaskRunner['id'], connection: WebSocket) {
|
||||
|
@ -75,7 +75,7 @@ export class TaskRunnerService {
|
|||
this.removeConnection(id);
|
||||
isConnected = true;
|
||||
|
||||
this.runnerConnections[id] = connection;
|
||||
this.runnerConnections.set(id, connection);
|
||||
|
||||
this.taskBroker.registerRunner(
|
||||
{
|
||||
|
@ -117,10 +117,11 @@ export class TaskRunnerService {
|
|||
}
|
||||
|
||||
removeConnection(id: TaskRunner['id']) {
|
||||
if (id in this.runnerConnections) {
|
||||
const connection = this.runnerConnections.get(id);
|
||||
if (connection) {
|
||||
this.taskBroker.deregisterRunner(id);
|
||||
this.runnerConnections[id].close();
|
||||
delete this.runnerConnections[id];
|
||||
connection.close();
|
||||
this.runnerConnections.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
89
packages/cli/src/runners/task-runner-process.ts
Normal file
89
packages/cli/src/runners/task-runner-process.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import * as a from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
import { TaskRunnerAuthService } from './auth/task-runner-auth.service';
|
||||
import { OnShutdown } from '../decorators/on-shutdown';
|
||||
|
||||
type ChildProcess = ReturnType<typeof spawn>;
|
||||
|
||||
/**
|
||||
* Manages the JS task runner process as a child process
|
||||
*/
|
||||
@Service()
|
||||
export class TaskRunnerProcess {
|
||||
public get isRunning() {
|
||||
return this.process !== null;
|
||||
}
|
||||
|
||||
/** The process ID of the task runner process */
|
||||
public get pid() {
|
||||
return this.process?.pid;
|
||||
}
|
||||
|
||||
private process: ChildProcess | null = null;
|
||||
|
||||
/** Promise that resolves after the process has exited */
|
||||
private runPromise: Promise<void> | null = null;
|
||||
|
||||
private isShuttingDown = false;
|
||||
|
||||
constructor(
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly authService: TaskRunnerAuthService,
|
||||
) {}
|
||||
|
||||
async start() {
|
||||
a.ok(!this.process, 'Task Runner Process already running');
|
||||
|
||||
const grantToken = await this.authService.createGrantToken();
|
||||
const startScript = require.resolve('@n8n/task-runner');
|
||||
|
||||
this.process = spawn('node', [startScript], {
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
N8N_RUNNERS_GRANT_TOKEN: grantToken,
|
||||
N8N_RUNNERS_N8N_URI: `localhost:${this.globalConfig.taskRunners.port}`,
|
||||
},
|
||||
});
|
||||
|
||||
this.process.stdout?.pipe(process.stdout);
|
||||
this.process.stderr?.pipe(process.stderr);
|
||||
|
||||
this.monitorProcess(this.process);
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
async stop() {
|
||||
if (!this.process) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// TODO: Timeout & force kill
|
||||
this.process.kill();
|
||||
await this.runPromise;
|
||||
|
||||
this.isShuttingDown = false;
|
||||
}
|
||||
|
||||
private monitorProcess(process: ChildProcess) {
|
||||
this.runPromise = new Promise((resolve) => {
|
||||
process.on('exit', (code) => {
|
||||
this.onProcessExit(code, resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private onProcessExit(_code: number | null, resolveFn: () => void) {
|
||||
this.process = null;
|
||||
resolveFn();
|
||||
|
||||
// If we are not shutting down, restart the process
|
||||
if (!this.isShuttingDown) {
|
||||
setImmediate(async () => await this.start());
|
||||
}
|
||||
}
|
||||
}
|
201
packages/cli/src/runners/task-runner-server.ts
Normal file
201
packages/cli/src/runners/task-runner-server.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
import * as a from 'node:assert/strict';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { ServerResponse, type Server, createServer as createHttpServer } from 'node:http';
|
||||
import type { AddressInfo, Socket } from 'node:net';
|
||||
import { parse as parseUrl } from 'node:url';
|
||||
import { Service } from 'typedi';
|
||||
import { Server as WSServer } from 'ws';
|
||||
|
||||
import { inTest, LOWEST_SHUTDOWN_PRIORITY } from '@/constants';
|
||||
import { OnShutdown } from '@/decorators/on-shutdown';
|
||||
import { Logger } from '@/logging/logger.service';
|
||||
import { bodyParser, rawBodyReader } from '@/middlewares';
|
||||
import { send } from '@/response-helper';
|
||||
import { TaskRunnerAuthController } from '@/runners/auth/task-runner-auth.controller';
|
||||
import type {
|
||||
TaskRunnerServerInitRequest,
|
||||
TaskRunnerServerInitResponse,
|
||||
} from '@/runners/runner-types';
|
||||
import { TaskRunnerService } from '@/runners/runner-ws-server';
|
||||
|
||||
/**
|
||||
* Task Runner HTTP & WS server
|
||||
*/
|
||||
@Service()
|
||||
export class TaskRunnerServer {
|
||||
private server: Server | undefined;
|
||||
|
||||
private wsServer: WSServer | undefined;
|
||||
|
||||
readonly app: express.Application;
|
||||
|
||||
public get port() {
|
||||
return (this.server?.address() as AddressInfo)?.port;
|
||||
}
|
||||
|
||||
private get upgradeEndpoint() {
|
||||
return `${this.getEndpointBasePath()}/_ws`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly globalConfig: GlobalConfig,
|
||||
private readonly taskRunnerAuthController: TaskRunnerAuthController,
|
||||
private readonly taskRunnerService: TaskRunnerService,
|
||||
) {
|
||||
this.app = express();
|
||||
this.app.disable('x-powered-by');
|
||||
|
||||
if (!this.globalConfig.taskRunners.authToken) {
|
||||
// Generate an auth token if one is not set
|
||||
this.globalConfig.taskRunners.authToken = randomBytes(32).toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.setupHttpServer();
|
||||
|
||||
this.setupWsServer();
|
||||
|
||||
if (!inTest) {
|
||||
await this.setupErrorHandlers();
|
||||
}
|
||||
|
||||
this.setupCommonMiddlewares();
|
||||
|
||||
this.configureRoutes();
|
||||
}
|
||||
|
||||
@OnShutdown(LOWEST_SHUTDOWN_PRIORITY)
|
||||
async stop(): Promise<void> {
|
||||
if (this.wsServer) {
|
||||
this.wsServer.close();
|
||||
this.wsServer = undefined;
|
||||
}
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve) => this.server?.close(() => resolve()));
|
||||
this.server = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an HTTP server and listens to the configured port */
|
||||
private async setupHttpServer() {
|
||||
const { app } = this;
|
||||
|
||||
this.server = createHttpServer(app);
|
||||
|
||||
const {
|
||||
taskRunners: { port, listen_address: address },
|
||||
} = this.globalConfig;
|
||||
|
||||
this.server.on('error', (error: Error & { code: string }) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
this.logger.info(
|
||||
`n8n Task Runner's port ${port} is already in use. Do you have another instance of n8n running already?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
a.ok(this.server);
|
||||
this.server.listen(port, address, () => resolve());
|
||||
});
|
||||
|
||||
this.logger.info(`n8n Task Runner server ready on ${address}, port ${port}`);
|
||||
}
|
||||
|
||||
/** Creates WebSocket server for handling upgrade requests */
|
||||
private setupWsServer() {
|
||||
const { authToken } = this.globalConfig.taskRunners;
|
||||
a.ok(authToken);
|
||||
a.ok(this.server);
|
||||
|
||||
this.wsServer = new WSServer({ noServer: true });
|
||||
this.server.on('upgrade', this.handleUpgradeRequest);
|
||||
}
|
||||
|
||||
private async setupErrorHandlers() {
|
||||
const { app } = this;
|
||||
|
||||
// Augment errors sent to Sentry
|
||||
const {
|
||||
Handlers: { requestHandler, errorHandler },
|
||||
} = await import('@sentry/node');
|
||||
app.use(requestHandler());
|
||||
app.use(errorHandler());
|
||||
}
|
||||
|
||||
private setupCommonMiddlewares() {
|
||||
// Compress the response data
|
||||
this.app.use(compression());
|
||||
|
||||
this.app.use(rawBodyReader);
|
||||
this.app.use(bodyParser);
|
||||
}
|
||||
|
||||
private configureRoutes() {
|
||||
this.app.use(
|
||||
this.upgradeEndpoint,
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.taskRunnerAuthController.authMiddleware,
|
||||
(req: TaskRunnerServerInitRequest, res: TaskRunnerServerInitResponse) =>
|
||||
this.taskRunnerService.handleRequest(req, res),
|
||||
);
|
||||
|
||||
const authEndpoint = `${this.getEndpointBasePath()}/auth`;
|
||||
this.app.post(
|
||||
authEndpoint,
|
||||
send(async (req) => await this.taskRunnerAuthController.createGrantToken(req)),
|
||||
);
|
||||
}
|
||||
|
||||
private handleUpgradeRequest = (
|
||||
request: TaskRunnerServerInitRequest,
|
||||
socket: Socket,
|
||||
head: Buffer,
|
||||
) => {
|
||||
if (parseUrl(request.url).pathname !== this.upgradeEndpoint) {
|
||||
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.wsServer) {
|
||||
// This might happen if the server is shutting down and we receive an upgrade request
|
||||
socket.write('HTTP/1.1 503 Service Unavailable\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
request.ws = ws;
|
||||
|
||||
const response = new ServerResponse(request);
|
||||
response.writeHead = (statusCode) => {
|
||||
if (statusCode > 200) ws.close(100);
|
||||
return response;
|
||||
};
|
||||
|
||||
// @ts-expect-error Delegate the request to the express app. This function is not exposed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
this.app.handle(request, response);
|
||||
});
|
||||
};
|
||||
|
||||
/** Returns the normalized base path for the task runner endpoints */
|
||||
private getEndpointBasePath() {
|
||||
let path = this.globalConfig.taskRunners.path;
|
||||
if (!path.startsWith('/')) {
|
||||
path = `/${path}`;
|
||||
}
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(-1);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
|
@ -31,7 +31,6 @@ import { isApiEnabled, loadPublicApiVersions } from '@/public-api';
|
|||
import { setupPushServer, setupPushHandler, Push } from '@/push';
|
||||
import type { APIRequest } from '@/requests';
|
||||
import * as ResponseHelper from '@/response-helper';
|
||||
import { setupRunnerServer, setupRunnerHandler } from '@/runners/runner-ws-server';
|
||||
import type { FrontendService } from '@/services/frontend.service';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
|
||||
|
@ -202,10 +201,6 @@ export class Server extends AbstractServer {
|
|||
const { restEndpoint, app } = this;
|
||||
setupPushHandler(restEndpoint, app);
|
||||
|
||||
if (!this.globalConfig.taskRunners.disabled) {
|
||||
setupRunnerHandler(restEndpoint, app);
|
||||
}
|
||||
|
||||
const push = Container.get(Push);
|
||||
if (push.isBidirectional) {
|
||||
const { CollaborationService } = await import('@/collaboration/collaboration.service');
|
||||
|
@ -405,9 +400,4 @@ export class Server extends AbstractServer {
|
|||
const { restEndpoint, server, app } = this;
|
||||
setupPushServer(restEndpoint, server, app);
|
||||
}
|
||||
|
||||
protected setupRunnerServer(): void {
|
||||
const { restEndpoint, server, app } = this;
|
||||
setupRunnerServer(restEndpoint, server, app);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import { GlobalConfig } from '@n8n/config';
|
||||
import Container from 'typedi';
|
||||
|
||||
import { TaskRunnerService } from '@/runners/runner-ws-server';
|
||||
import { TaskBroker } from '@/runners/task-broker.service';
|
||||
import { TaskRunnerProcess } from '@/runners/task-runner-process';
|
||||
import { TaskRunnerServer } from '@/runners/task-runner-server';
|
||||
import { retryUntil } from '@test-integration/retry-until';
|
||||
|
||||
describe('TaskRunnerProcess', () => {
|
||||
const authToken = 'token';
|
||||
const globalConfig = Container.get(GlobalConfig);
|
||||
globalConfig.taskRunners.authToken = authToken;
|
||||
globalConfig.taskRunners.port = 0; // Use any port
|
||||
const taskRunnerServer = Container.get(TaskRunnerServer);
|
||||
|
||||
const runnerProcess = Container.get(TaskRunnerProcess);
|
||||
const taskBroker = Container.get(TaskBroker);
|
||||
const taskRunnerService = Container.get(TaskRunnerService);
|
||||
|
||||
beforeAll(async () => {
|
||||
await taskRunnerServer.start();
|
||||
// Set the port to the actually used port
|
||||
globalConfig.taskRunners.port = taskRunnerServer.port;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await taskRunnerServer.stop();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await runnerProcess.stop();
|
||||
});
|
||||
|
||||
const getNumConnectedRunners = () => taskRunnerService.runnerConnections.size;
|
||||
const getNumRegisteredRunners = () => taskBroker.getKnownRunners().size;
|
||||
|
||||
it('should start and connect the task runner', async () => {
|
||||
// Act
|
||||
await runnerProcess.start();
|
||||
|
||||
// Assert
|
||||
expect(runnerProcess.isRunning).toBeTruthy();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
});
|
||||
|
||||
it('should stop an disconnect the task runner', async () => {
|
||||
// Arrange
|
||||
await runnerProcess.start();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
|
||||
// Act
|
||||
await runnerProcess.stop();
|
||||
|
||||
// Assert
|
||||
// Wait until the runner has disconnected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(0));
|
||||
|
||||
expect(runnerProcess.isRunning).toBeFalsy();
|
||||
expect(getNumRegisteredRunners()).toBe(0);
|
||||
});
|
||||
|
||||
it('should restart the task runner if it exits', async () => {
|
||||
// Arrange
|
||||
await runnerProcess.start();
|
||||
|
||||
// Wait until the runner has connected
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
const processId = runnerProcess.pid;
|
||||
|
||||
// Act
|
||||
// @ts-expect-error private property
|
||||
runnerProcess.process?.kill('SIGKILL');
|
||||
|
||||
// Assert
|
||||
// Wait until the runner is running again
|
||||
await retryUntil(() => expect(runnerProcess.isRunning).toBeTruthy());
|
||||
expect(runnerProcess.pid).not.toBe(processId);
|
||||
|
||||
// Wait until the runner has connected again
|
||||
await retryUntil(() => expect(getNumConnectedRunners()).toBe(1));
|
||||
expect(getNumConnectedRunners()).toBe(1);
|
||||
expect(getNumRegisteredRunners()).toBe(1);
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "Core functionality of n8n",
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "1.51.0",
|
||||
"version": "1.52.1",
|
||||
"main": "src/main.ts",
|
||||
"import": "src/main.ts",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -48,6 +48,7 @@
|
|||
"@vueuse/core": "^10.11.0",
|
||||
"axios": "catalog:",
|
||||
"bowser": "2.11.0",
|
||||
"change-case": "^5.4.4",
|
||||
"chart.js": "^4.4.0",
|
||||
"codemirror-lang-html-n8n": "^1.0.0",
|
||||
"dateformat": "^3.0.3",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { SectionCreateElement } from '@/Interface';
|
||||
import { groupItemsInSections, sortNodeCreateElements } from '../utils';
|
||||
import { formatTriggerActionName, groupItemsInSections, sortNodeCreateElements } from '../utils';
|
||||
import { mockActionCreateElement, mockNodeCreateElement, mockSectionCreateElement } from './utils';
|
||||
|
||||
describe('NodeCreator - utils', () => {
|
||||
|
@ -62,4 +62,15 @@ describe('NodeCreator - utils', () => {
|
|||
expect(sortNodeCreateElements([node1, node2, node3])).toEqual([node1, node2, node3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTriggerActionName', () => {
|
||||
test.each([
|
||||
['project.created', 'project created'],
|
||||
['Project Created', 'project created'],
|
||||
['field.value.created', 'field value created'],
|
||||
['attendee.checked_in', 'attendee checked in'],
|
||||
])('Action name %i should become as %i', (actionName, expected) => {
|
||||
expect(formatTriggerActionName(actionName)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import { i18n } from '@/plugins/i18n';
|
||||
|
||||
import { getCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||
import { formatTriggerActionName } from '../utils';
|
||||
|
||||
const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended';
|
||||
|
||||
|
@ -168,7 +169,7 @@ function triggersCategory(nodeTypeDescription: INodeTypeDescription): ActionType
|
|||
displayName:
|
||||
categoryItem.action ??
|
||||
cachedBaseText('nodeCreator.actionsCategory.onEvent', {
|
||||
interpolate: { event: startCase(categoryItem.name) },
|
||||
interpolate: { event: formatTriggerActionName(categoryItem.name) },
|
||||
}),
|
||||
description: categoryItem.description ?? '',
|
||||
displayOptions: matchedProperty.displayOptions,
|
||||
|
|
|
@ -19,6 +19,7 @@ import { sublimeSearch } from '@/utils/sortUtils';
|
|||
import type { NodeViewItemSection } from './viewsData';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { sortBy } from 'lodash-es';
|
||||
import * as changeCase from 'change-case';
|
||||
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
|
||||
|
@ -177,3 +178,11 @@ export function groupItemsInSections(
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const formatTriggerActionName = (actionPropertyName: string) => {
|
||||
let name = actionPropertyName;
|
||||
if (actionPropertyName.includes('.')) {
|
||||
name = actionPropertyName.split('.').join(' ');
|
||||
}
|
||||
return changeCase.noCase(name);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('WorkflowsView', () => {
|
|||
let projectsStore: ReturnType<typeof useProjectsStore>;
|
||||
|
||||
const routerReplaceMock = vi.fn();
|
||||
const routerPushMock = vi.fn();
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowsView, {
|
||||
global: {
|
||||
|
@ -32,6 +33,7 @@ describe('WorkflowsView', () => {
|
|||
},
|
||||
$router: {
|
||||
replace: routerReplaceMock,
|
||||
push: routerPushMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -26,6 +26,8 @@ interface Filters {
|
|||
tags: string[];
|
||||
}
|
||||
|
||||
type QueryFilters = Partial<Filters>;
|
||||
|
||||
const StatusFilter = {
|
||||
ACTIVE: true,
|
||||
DEACTIVATED: false,
|
||||
|
@ -152,6 +154,9 @@ const WorkflowsView = defineComponent({
|
|||
},
|
||||
async mounted() {
|
||||
this.documentTitle.set(this.$locale.baseText('workflows.heading'));
|
||||
|
||||
await this.tagsStore.fetchAll();
|
||||
|
||||
await this.setFiltersFromQueryString();
|
||||
|
||||
void this.usersStore.showPersonalizationSurvey();
|
||||
|
@ -263,10 +268,20 @@ const WorkflowsView = defineComponent({
|
|||
isValidProjectId(projectId: string) {
|
||||
return this.projectsStore.availableProjects.some((project) => project.id === projectId);
|
||||
},
|
||||
async removeInvalidQueryFiltersFromUrl(filtersToApply: QueryFilters) {
|
||||
await this.$router.push({
|
||||
query: {
|
||||
...(filtersToApply.tags && { tags: filtersToApply.tags?.join(',') }),
|
||||
...(filtersToApply.status && { status: filtersToApply.status?.toString() }),
|
||||
...(filtersToApply.search && { search: filtersToApply.search }),
|
||||
...(filtersToApply.homeProject && { homeProject: filtersToApply.homeProject }),
|
||||
},
|
||||
});
|
||||
},
|
||||
async setFiltersFromQueryString() {
|
||||
const { tags, status, search, homeProject } = this.$route.query;
|
||||
|
||||
const filtersToApply: { [key: string]: string | string[] | boolean } = {};
|
||||
const filtersToApply: QueryFilters = {};
|
||||
|
||||
if (homeProject && typeof homeProject === 'string') {
|
||||
await this.projectsStore.getAvailableProjects();
|
||||
|
@ -295,6 +310,8 @@ const WorkflowsView = defineComponent({
|
|||
filtersToApply.status = status === 'true';
|
||||
}
|
||||
|
||||
await this.removeInvalidQueryFiltersFromUrl(filtersToApply);
|
||||
|
||||
if (Object.keys(filtersToApply).length) {
|
||||
this.filters = {
|
||||
...this.filters,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-node-dev",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
|
|
@ -211,9 +211,51 @@ describe('FormTrigger, prepareFormData', () => {
|
|||
|
||||
expect(result.formFields[0].isMultiSelect).toBe(true);
|
||||
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||
{ id: 'option0', label: 'Red' },
|
||||
{ id: 'option1', label: 'Blue' },
|
||||
{ id: 'option2', label: 'Green' },
|
||||
{ id: 'option0_field-0', label: 'Red' },
|
||||
{ id: 'option1_field-0', label: 'Blue' },
|
||||
{ id: 'option2_field-0', label: 'Green' },
|
||||
]);
|
||||
});
|
||||
it('should correctly handle multiselect fields with unique ids', () => {
|
||||
const formFields: FormField[] = [
|
||||
{
|
||||
fieldLabel: 'Favorite Colors',
|
||||
fieldType: 'text',
|
||||
requiredField: true,
|
||||
multiselect: true,
|
||||
fieldOptions: { values: [{ option: 'Red' }, { option: 'Blue' }, { option: 'Green' }] },
|
||||
},
|
||||
{
|
||||
fieldLabel: 'Favorite Colors',
|
||||
fieldType: 'text',
|
||||
requiredField: true,
|
||||
multiselect: true,
|
||||
fieldOptions: { values: [{ option: 'Red' }, { option: 'Blue' }, { option: 'Green' }] },
|
||||
},
|
||||
];
|
||||
|
||||
const query = { 'Favorite Colors': 'Red,Blue' };
|
||||
|
||||
const result = prepareFormData({
|
||||
formTitle: 'Test Form',
|
||||
formDescription: 'This is a test form',
|
||||
formSubmittedText: 'Thank you',
|
||||
redirectUrl: 'example.com',
|
||||
formFields,
|
||||
testRun: false,
|
||||
query,
|
||||
});
|
||||
|
||||
expect(result.formFields[0].isMultiSelect).toBe(true);
|
||||
expect(result.formFields[0].multiSelectOptions).toEqual([
|
||||
{ id: 'option0_field-0', label: 'Red' },
|
||||
{ id: 'option1_field-0', label: 'Blue' },
|
||||
{ id: 'option2_field-0', label: 'Green' },
|
||||
]);
|
||||
expect(result.formFields[1].multiSelectOptions).toEqual([
|
||||
{ id: 'option0_field-1', label: 'Red' },
|
||||
{ id: 'option1_field-1', label: 'Blue' },
|
||||
{ id: 'option2_field-1', label: 'Green' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -83,7 +83,7 @@ export function prepareFormData({
|
|||
input.isMultiSelect = true;
|
||||
input.multiSelectOptions =
|
||||
field.fieldOptions?.values.map((e, i) => ({
|
||||
id: `option${i}`,
|
||||
id: `option${i}_${input.id}`,
|
||||
label: e.option,
|
||||
})) ?? [];
|
||||
} else if (fieldType === 'file') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-base",
|
||||
"version": "1.61.0",
|
||||
"version": "1.62.1",
|
||||
"description": "Base nodes of n8n",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-workflow",
|
||||
"version": "1.60.0",
|
||||
"version": "1.61.1",
|
||||
"description": "Workflow base code of n8n",
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
|
|
596
pnpm-lock.yaml
596
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue