Merge branch 'n8n-io:master' into MongoDB_vector_store

This commit is contained in:
Pash10g 2025-03-04 08:16:46 +02:00 committed by GitHub
commit afe56adc0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 616 additions and 138 deletions

View file

@ -1,3 +1,55 @@
# [1.82.0](https://github.com/n8n-io/n8n/compare/n8n@1.81.0...n8n@1.82.0) (2025-03-03)
### Bug Fixes
* **Call n8n Workflow Tool Node:** Support concurrent invocations of the tool ([#13526](https://github.com/n8n-io/n8n/issues/13526)) ([5334661](https://github.com/n8n-io/n8n/commit/5334661b76909f48aa4e45af889e6180c025eed6))
* **core:** Gracefully handle missing tasks metadata ([#13632](https://github.com/n8n-io/n8n/issues/13632)) ([999fb81](https://github.com/n8n-io/n8n/commit/999fb8174ae6bb34354cb8c6f85f769cb64e8ae4))
* **core:** Remove `index.html` caching entirely ([#13563](https://github.com/n8n-io/n8n/issues/13563)) ([afba8f9](https://github.com/n8n-io/n8n/commit/afba8f9ff89054d54e1cf70070ae5710bc9ddd37))
* **editor:** Add workflows to the store when fetching current page ([#13583](https://github.com/n8n-io/n8n/issues/13583)) ([c4f3293](https://github.com/n8n-io/n8n/commit/c4f329377828d80a54b71f5733ea7d9b4ee91f48))
* **editor:** Ai 672 minor UI fixes on evaluation creation ([#13461](https://github.com/n8n-io/n8n/issues/13461)) ([b791677](https://github.com/n8n-io/n8n/commit/b791677ffa8c82161c4c40b65bc62d93f2e7bc9e))
* **editor:** Ai 675 minor tweaks to tests list ([#13467](https://github.com/n8n-io/n8n/issues/13467)) ([5ad950f](https://github.com/n8n-io/n8n/commit/5ad950f60371546414ff17eb31171f2259e70f57))
* **editor:** Don't show duplicate logs when tree is deeply nested ([#13537](https://github.com/n8n-io/n8n/issues/13537)) ([d550382](https://github.com/n8n-io/n8n/commit/d550382a4a43c54cae47e9071236aa18efe38a5d))
* **editor:** Fix browser crash with large execution result ([#13580](https://github.com/n8n-io/n8n/issues/13580)) ([1c8c7e3](https://github.com/n8n-io/n8n/commit/1c8c7e34f9d2c8363c441aeb8c562ac91088a687))
* **editor:** Fix github star button layout ([#13630](https://github.com/n8n-io/n8n/issues/13630)) ([139b5b3](https://github.com/n8n-io/n8n/commit/139b5b378daba6df18639eeb4f326edce7752e11))
* **editor:** Fix icon color on 'Call n8n Workflow Tool' node ([#13568](https://github.com/n8n-io/n8n/issues/13568)) ([90d0943](https://github.com/n8n-io/n8n/commit/90d09431af97570a3a6adfb0470a18681af28001))
* **editor:** Fix icon spacing in accordion title ([#13539](https://github.com/n8n-io/n8n/issues/13539)) ([ebaaf0e](https://github.com/n8n-io/n8n/commit/ebaaf0e3d9602052f76f61b90fb073e390896cea))
* **editor:** Fix keyboard shortcuts no longer working after editing sticky note ([#13502](https://github.com/n8n-io/n8n/issues/13502)) ([ab41fc3](https://github.com/n8n-io/n8n/commit/ab41fc3fb5f15e9c7ce7279b46cec90a511d0e0d))
* **editor:** Fix workflows list status filter ([#13621](https://github.com/n8n-io/n8n/issues/13621)) ([4067fb0](https://github.com/n8n-io/n8n/commit/4067fb0b12d242c795c6598df6c4090d48cec7b1))
* **editor:** Hide fromAI button in old workflow tool ([#13552](https://github.com/n8n-io/n8n/issues/13552)) ([6ef8d34](https://github.com/n8n-io/n8n/commit/6ef8d34f969ddb9e80b82dc50b38698249089af2))
* **editor:** Parse out nodeType ([#13474](https://github.com/n8n-io/n8n/issues/13474)) ([1cd13b6](https://github.com/n8n-io/n8n/commit/1cd13b639efcfabf183740bb6634023c66d5ce99))
* **editor:** Show dropdown scrollbars only when appropriate ([#13562](https://github.com/n8n-io/n8n/issues/13562)) ([615a42a](https://github.com/n8n-io/n8n/commit/615a42afd52d0d95dd30ed9aa231b9921e0708fe))
* **editor:** Show JSON full-screen Editor Window in Full Height ([#13350](https://github.com/n8n-io/n8n/issues/13350)) ([46dcce3](https://github.com/n8n-io/n8n/commit/46dcce341fbfa1c2a44a08f3dc93f1f8f16808c8))
* **editor:** Show scrollbar in Element UI popup ([#13259](https://github.com/n8n-io/n8n/issues/13259)) ([c021a7e](https://github.com/n8n-io/n8n/commit/c021a7e4b2daccc59541bab25c1447339dd68c09))
* **editor:** Undo keybinding changes related to window focus/blur events ([#13559](https://github.com/n8n-io/n8n/issues/13559)) ([6ddcc1f](https://github.com/n8n-io/n8n/commit/6ddcc1f8c93f86b0d111cae1b24518d621d8fe84))
* **Odoo Node:** Model and fields dynamic fetching errors ([#13511](https://github.com/n8n-io/n8n/issues/13511)) ([294f019](https://github.com/n8n-io/n8n/commit/294f0194145ca4139d9d9cea0729bf83d0871c94))
* **Postgres Node:** Accommodate null values in query parameters for expressions ([#13544](https://github.com/n8n-io/n8n/issues/13544)) ([6c266ac](https://github.com/n8n-io/n8n/commit/6c266acced95500148532b4fc015fe5d9587db76))
* **QuickBooks Online Node:** Add qty to quickbooks invoice line details ([#13602](https://github.com/n8n-io/n8n/issues/13602)) ([7c4e2f0](https://github.com/n8n-io/n8n/commit/7c4e2f014c0b38935a4d661646e773ad26fc97e1))
* **seven Node:** Remove obsolete options and fix typos ([#13122](https://github.com/n8n-io/n8n/issues/13122)) ([d02c8b0](https://github.com/n8n-io/n8n/commit/d02c8b0d7dbd4144c954a66aa0e78e43122b6e9a))
* **Switch Node:** Fix an issue in ordering rules in Switch Node ([#13476](https://github.com/n8n-io/n8n/issues/13476)) ([0fb6607](https://github.com/n8n-io/n8n/commit/0fb66076ba6120a7cb2401102ff8d1d6220ae106))
### Features
* **Anthropic Chat Model Node:** Fetch models dynamically & support thinking ([#13543](https://github.com/n8n-io/n8n/issues/13543)) ([461df37](https://github.com/n8n-io/n8n/commit/461df371f76b9dee9916a985a2bd2197facbcf6b))
* **Azure Storage Node:** New node ([#12536](https://github.com/n8n-io/n8n/issues/12536)) ([727f6f3](https://github.com/n8n-io/n8n/commit/727f6f3c0e5cef2d0cd4cd1ef1c6fa8f4d3f69ec))
* **core:** Add metric for active workflow count ([#13420](https://github.com/n8n-io/n8n/issues/13420)) ([3aa679e](https://github.com/n8n-io/n8n/commit/3aa679e4ac411d0d34e039fa6c43bc98f2e3670f))
* **core:** Fix partial workflow execution with specific trigger data ([#13505](https://github.com/n8n-io/n8n/issues/13505)) ([9029dac](https://github.com/n8n-io/n8n/commit/9029dace5c682e4b5df4f18f2f51098dce6436e5))
* **core:** Make Tools Agent the default Agent type, deprecate other agent types ([#13459](https://github.com/n8n-io/n8n/issues/13459)) ([a60d106](https://github.com/n8n-io/n8n/commit/a60d106ebb4fb71e80f90a17965d7fb79d7806c6))
* **core:** Support executing single nodes not part of a graph as a partial execution ([#13529](https://github.com/n8n-io/n8n/issues/13529)) ([8a34f02](https://github.com/n8n-io/n8n/commit/8a34f027c531f0d37fc8088c13d7e289cd8897ce))
* **editor:** Add functionality to create folders ([#13473](https://github.com/n8n-io/n8n/issues/13473)) ([2cb9d9e](https://github.com/n8n-io/n8n/commit/2cb9d9e29fc961a417d06c1449b79d4a0a66658e))
* **editor:** Automatically tidy up workflows ([#13471](https://github.com/n8n-io/n8n/issues/13471)) ([f381a24](https://github.com/n8n-io/n8n/commit/f381a24145271f4df4fa5c9345bb12c984f6e1fc))
* **editor:** Indicate dirty nodes with yellow borders/connectors on canvas ([#13040](https://github.com/n8n-io/n8n/issues/13040)) ([75493ef](https://github.com/n8n-io/n8n/commit/75493ef6ef4ee47d0ccf217cd5c2e58754f60c12))
* **editor:** Rename 'In-Memory Vector Store' to 'Simple Vector Store' ([#13472](https://github.com/n8n-io/n8n/issues/13472)) ([35c00d0](https://github.com/n8n-io/n8n/commit/35c00d0c846e8a1e214aea3690ea60ff80d03eed))
* **editor:** Rename 'Window Buffer Memory' to 'Simple Memory' ([#13477](https://github.com/n8n-io/n8n/issues/13477)) ([819fc2d](https://github.com/n8n-io/n8n/commit/819fc2da63ce7f06d4702bce698d382eb64c45a3))
* Hackmation - automatically switch to expression mode ([#13213](https://github.com/n8n-io/n8n/issues/13213)) ([6953b0d](https://github.com/n8n-io/n8n/commit/6953b0d53a28448022c9de0a2f6294c9390a3b48))
* **n8n Form Trigger Node, Chat Trigger Node:** Allow to customize form and chat css ([#13506](https://github.com/n8n-io/n8n/issues/13506)) ([289041e](https://github.com/n8n-io/n8n/commit/289041e997eedb660356cdbd259660b7c3117194))
* **n8n Vertica credentials only Node:** New node ([#12256](https://github.com/n8n-io/n8n/issues/12256)) ([d3fe3de](https://github.com/n8n-io/n8n/commit/d3fe3dea32207dfdb2a43db0def96466a31daa66))
* Update AWS credential to support more regions ([#13524](https://github.com/n8n-io/n8n/issues/13524)) ([b50658c](https://github.com/n8n-io/n8n/commit/b50658cbc64c0a6fc000b11dca0cca49cc707471))
* WhatsApp Business Cloud Node - new operation sendAndWait ([#12941](https://github.com/n8n-io/n8n/issues/12941)) ([97defb3](https://github.com/n8n-io/n8n/commit/97defb3a833bb269a4a3fc573a8e250a0d0e0deb))
# [1.81.0](https://github.com/n8n-io/n8n/compare/n8n@1.80.0...n8n@1.81.0) (2025-02-24)

View file

@ -36,7 +36,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
it('should login and logout', () => {
cy.visit('/');
cy.get('input[name="email"]').type(INSTANCE_OWNER.email);
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_OWNER.email);
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.logo().should('be.visible');
@ -47,7 +47,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.logo().should('be.visible');

View file

@ -506,7 +506,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();

View file

@ -15,7 +15,7 @@ export class SigninPage extends BasePage {
getters = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
email: () => cy.getByTestId('emailOrLdapLoginId'),
password: () => cy.getByTestId('password'),
submit: () => cy.get('button'),
};

View file

@ -69,7 +69,7 @@ Cypress.Commands.add('signin', ({ email, password }) => {
.request({
method: 'POST',
url: `${BACKEND_BASE_URL}/rest/login`,
body: { email, password },
body: { emailOrLdapLoginId: email, password },
failOnStatusCode: false,
})
.then((response) => {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-monorepo",
"version": "1.81.0",
"version": "1.82.0",
"private": true,
"engines": {
"node": ">=20.15",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/api-types",
"version": "0.16.0",
"version": "0.17.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -6,7 +6,7 @@ describe('LoginRequestDto', () => {
{
name: 'complete valid login request',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
password: 'securePassword123',
mfaCode: '123456',
},
@ -14,14 +14,14 @@ describe('LoginRequestDto', () => {
{
name: 'login request without optional MFA',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
password: 'securePassword123',
},
},
{
name: 'login request with both mfaCode and mfaRecoveryCode',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
password: 'securePassword123',
mfaCode: '123456',
mfaRecoveryCode: 'recovery-code-123',
@ -30,7 +30,7 @@ describe('LoginRequestDto', () => {
{
name: 'login request with only mfaRecoveryCode',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
password: 'securePassword123',
mfaRecoveryCode: 'recovery-code-123',
},
@ -44,43 +44,35 @@ describe('LoginRequestDto', () => {
describe('Invalid requests', () => {
test.each([
{
name: 'invalid email',
name: 'invalid emailOrLdapLoginId',
request: {
email: 'invalid-email',
emailOrLdapLoginId: 0,
password: 'securePassword123',
},
expectedErrorPath: ['email'],
expectedErrorPath: ['emailOrLdapLoginId'],
},
{
name: 'empty password',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
password: '',
},
expectedErrorPath: ['password'],
},
{
name: 'missing email',
name: 'missing emailOrLdapLoginId',
request: {
password: 'securePassword123',
},
expectedErrorPath: ['email'],
expectedErrorPath: ['emailOrLdapLoginId'],
},
{
name: 'missing password',
request: {
email: 'test@example.com',
emailOrLdapLoginId: 'test@example.com',
},
expectedErrorPath: ['password'],
},
{
name: 'whitespace in email and password',
request: {
email: ' test@example.com ',
password: ' securePassword123 ',
},
expectedErrorPath: ['email'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = LoginRequestDto.safeParse(request);
expect(result.success).toBe(false);

View file

@ -2,7 +2,12 @@ import { z } from 'zod';
import { Z } from 'zod-class';
export class LoginRequestDto extends Z.class({
email: z.string().email(),
/*
* The LDAP username does not need to be an email, so email validation
* is not enforced here. The controller determines whether this is an
* email and validates when LDAP is disabled
*/
emailOrLdapLoginId: z.string().trim(),
password: z.string().min(1),
mfaCode: z.string().optional(),
mfaRecoveryCode: z.string().optional(),

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-benchmark",
"version": "1.11.0",
"version": "1.12.0",
"description": "Cli for running benchmark tests for n8n",
"main": "dist/index",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/client-oauth2",
"version": "0.22.0",
"version": "0.23.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/config",
"version": "1.30.0",
"version": "1.31.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/di",
"version": "0.3.0",
"version": "0.4.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/imap",
"version": "0.8.0",
"version": "0.9.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/json-schema-to-zod",
"version": "1.2.0",
"version": "1.3.0",
"description": "Converts JSON schema objects into Zod schemas",
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/index.js",

View file

@ -28,7 +28,7 @@ export class ToolWorkflow extends VersionedNodeType {
],
},
},
defaultVersion: 2,
defaultVersion: 2.1,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
@ -37,6 +37,7 @@ export class ToolWorkflow extends VersionedNodeType {
1.2: new ToolWorkflowV1(baseDescription),
1.3: new ToolWorkflowV1(baseDescription),
2: new ToolWorkflowV2(baseDescription),
2.1: new ToolWorkflowV2(baseDescription),
};
super(nodeVersions, baseDescription);
}

View file

@ -25,7 +25,9 @@ export class ToolWorkflowV2 implements INodeType {
};
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
const workflowToolService = new WorkflowToolService(this);
const returnAllItems = this.getNode().typeVersion > 2;
const workflowToolService = new WorkflowToolService(this, { returnAllItems });
const name = this.getNodeParameter('name', itemIndex) as string;
const description = this.getNodeParameter('description', itemIndex) as string;

View file

@ -187,6 +187,66 @@ describe('WorkflowTool::WorkflowToolService', () => {
expect(result.subExecutionId).toBe('test-execution');
});
it('should successfully execute workflow and return first item of many', async () => {
const workflowInfo = { id: 'test-workflow' };
const items: INodeExecutionData[] = [];
const workflowProxyMock = {
$execution: { id: 'exec-id' },
$workflow: { id: 'workflow-id' },
} as unknown as IWorkflowDataProxyData;
const TEST_RESPONSE_1 = { msg: 'test response 1' };
const TEST_RESPONSE_2 = { msg: 'test response 2' };
const mockResponse: ExecuteWorkflowData = {
data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]],
executionId: 'test-execution',
};
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
const result = await service['executeSubWorkflow'](
context,
workflowInfo,
items,
workflowProxyMock,
);
expect(result.response).toBe(TEST_RESPONSE_1);
expect(result.subExecutionId).toBe('test-execution');
});
it('should successfully execute workflow and return all items', async () => {
const serviceWithReturnAllItems = new WorkflowToolService(context, { returnAllItems: true });
const workflowInfo = { id: 'test-workflow' };
const items: INodeExecutionData[] = [];
const workflowProxyMock = {
$execution: { id: 'exec-id' },
$workflow: { id: 'workflow-id' },
} as unknown as IWorkflowDataProxyData;
const TEST_RESPONSE_1 = { msg: 'test response 1' };
const TEST_RESPONSE_2 = { msg: 'test response 2' };
const mockResponse: ExecuteWorkflowData = {
data: [[{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]],
executionId: 'test-execution',
};
jest.spyOn(context, 'executeWorkflow').mockResolvedValueOnce(mockResponse);
const result = await serviceWithReturnAllItems['executeSubWorkflow'](
context,
workflowInfo,
items,
workflowProxyMock,
undefined,
);
expect(result.response).toEqual([{ json: TEST_RESPONSE_1 }, { json: TEST_RESPONSE_2 }]);
expect(result.subExecutionId).toBe('test-execution');
});
it('should throw error when workflow execution fails', async () => {
jest.spyOn(context, 'executeWorkflow').mockRejectedValueOnce(new Error('Execution failed'));

View file

@ -1,7 +1,6 @@
import type { CallbackManagerForToolRun } from '@langchain/core/callbacks/manager';
import { DynamicStructuredTool, DynamicTool } from '@langchain/core/tools';
import get from 'lodash/get';
import isObject from 'lodash/isObject';
import { isArray, isObject } from 'lodash';
import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces';
import * as manual from 'n8n-nodes-base/dist/nodes/Set/v2/manual.mode';
import { getCurrentWorkflowInputData } from 'n8n-nodes-base/dist/utils/workflowInputsResourceMapping/GenericFunctions';
@ -29,6 +28,10 @@ import {
} from 'n8n-workflow';
import { z } from 'zod';
function isNodeExecutionData(data: unknown): data is INodeExecutionData[] {
return isArray(data) && Boolean(data.length) && isObject(data[0]) && 'json' in data[0];
}
/**
Main class for creating the Workflow tool
Processes the node parameters and creates AI Agent tool capable of executing n8n workflows
@ -43,10 +46,16 @@ export class WorkflowToolService {
// Sub-workflow execution id, will be set after the sub-workflow is executed
private subExecutionId: string | undefined;
constructor(private baseContext: ISupplyDataFunctions) {
private returnAllItems: boolean = false;
constructor(
private baseContext: ISupplyDataFunctions,
options?: { returnAllItems: boolean },
) {
const subWorkflowInputs = this.baseContext.getNode().parameters
.workflowInputs as ResourceMapperValue;
this.useSchema = (subWorkflowInputs?.schema ?? []).length > 0;
this.returnAllItems = options?.returnAllItems ?? false;
}
// Creates the tool based on the provided parameters
@ -65,7 +74,7 @@ export class WorkflowToolService {
const toolHandler = async (
query: string | IDataObject,
runManager?: CallbackManagerForToolRun,
): Promise<string> => {
): Promise<IDataObject | IDataObject[] | string> => {
const localRunIndex = runIndex++;
// We need to clone the context here to handle runIndex correctly
// Otherwise the runIndex will be shared between different executions
@ -74,10 +83,23 @@ export class WorkflowToolService {
runIndex: localRunIndex,
inputData: [[{ json: { query } }]],
});
try {
const response = await this.runFunction(context, query, itemIndex, runManager);
const processedResponse = this.handleToolResponse(response);
let responseData: INodeExecutionData[];
if (isNodeExecutionData(response)) {
responseData = response;
} else {
const reParsedData = jsonParse<IDataObject>(processedResponse, {
fallbackValue: { response: processedResponse },
});
responseData = [{ json: reParsedData }];
}
// Once the sub-workflow is executed, add the output data to the context
// This will be used to link the sub-workflow execution in the parent workflow
let metadata: ITaskMetadata | undefined;
@ -89,13 +111,11 @@ export class WorkflowToolService {
},
};
}
const json = jsonParse<IDataObject>(processedResponse, {
fallbackValue: { response: processedResponse },
});
void context.addOutputData(
NodeConnectionType.AiTool,
localRunIndex,
[[{ json }]],
[responseData],
metadata,
);
@ -126,6 +146,14 @@ export class WorkflowToolService {
return response.toString();
}
if (isNodeExecutionData(response)) {
return JSON.stringify(
response.map((item) => item.json),
null,
2,
);
}
if (isObject(response)) {
return JSON.stringify(response, null, 2);
}
@ -148,7 +176,7 @@ export class WorkflowToolService {
items: INodeExecutionData[],
workflowProxy: IWorkflowDataProxyData,
runManager?: CallbackManagerForToolRun,
): Promise<{ response: string; subExecutionId: string }> {
): Promise<{ response: string | IDataObject | INodeExecutionData[]; subExecutionId: string }> {
let receivedData: ExecuteWorkflowData;
try {
receivedData = await context.executeWorkflow(workflowInfo, items, runManager?.getChild(), {
@ -163,7 +191,12 @@ export class WorkflowToolService {
throw new NodeOperationError(context.getNode(), error as Error);
}
const response: string | undefined = get(receivedData, 'data[0][0].json') as string | undefined;
let response: IDataObject | INodeExecutionData[] | undefined;
if (this.returnAllItems) {
response = receivedData?.data?.[0]?.length ? receivedData.data[0] : undefined;
} else {
response = receivedData?.data?.[0]?.[0]?.json;
}
if (response === undefined) {
throw new NodeOperationError(
context.getNode(),
@ -183,7 +216,7 @@ export class WorkflowToolService {
query: string | IDataObject,
itemIndex: number,
runManager?: CallbackManagerForToolRun,
): Promise<string> {
): Promise<string | IDataObject | INodeExecutionData[]> {
const source = context.getNodeParameter('source', itemIndex) as string;
const workflowProxy = context.getWorkflowDataProxy(0);
@ -304,7 +337,10 @@ export class WorkflowToolService {
private async createStructuredTool(
name: string,
description: string,
func: (query: string | IDataObject, runManager?: CallbackManagerForToolRun) => Promise<string>,
func: (
query: string | IDataObject,
runManager?: CallbackManagerForToolRun,
) => Promise<string | IDataObject | IDataObject[]>,
): Promise<DynamicStructuredTool | DynamicTool> {
const collectedArguments = await this.extractFromAIParameters();

View file

@ -12,7 +12,7 @@ export const versionDescription: INodeTypeDescription = {
defaults: {
name: 'Call n8n Workflow Tool',
},
version: [2],
version: [2, 2.1],
inputs: [],
outputs: [NodeConnectionType.AiTool],
outputNames: ['Tool'],

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-nodes-langchain",
"version": "1.81.0",
"version": "1.82.0",
"description": "",
"main": "index.js",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/permissions",
"version": "0.18.0",
"version": "0.19.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/task-runner",
"version": "1.18.0",
"version": "1.19.0",
"scripts": {
"clean": "rimraf dist .turbo",
"start": "node dist/start.js",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/typescript-config",
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"files": [
"tsconfig.backend.json",

View file

@ -1,7 +1,7 @@
{
"name": "@n8n/utils",
"type": "module",
"version": "1.2.0",
"version": "1.3.0",
"files": [
"dist"
],

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/vitest-config",
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"peerDependencies": {
"vite": "catalog:frontend",

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "1.81.0",
"version": "1.82.0",
"description": "n8n Workflow Automation Tool",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -0,0 +1,99 @@
import type { LoginRequestDto } from '@n8n/api-types';
import { Container } from '@n8n/di';
import type { Response } from 'express';
import { mock } from 'jest-mock-extended';
import { Logger } from 'n8n-core';
import * as auth from '@/auth';
import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import type { User } from '@/databases/entities/user';
import { UserRepository } from '@/databases/repositories/user.repository';
import { EventService } from '@/events/event.service';
import { License } from '@/license';
import { MfaService } from '@/mfa/mfa.service';
import { PostHogClient } from '@/posthog';
import type { AuthenticatedRequest } from '@/requests';
import { UserService } from '@/services/user.service';
import { mockInstance } from '@test/mocking';
import { AuthController } from '../auth.controller';
jest.mock('@/auth');
const mockedAuth = auth as jest.Mocked<typeof auth>;
describe('AuthController', () => {
mockInstance(Logger);
mockInstance(EventService);
mockInstance(AuthService);
mockInstance(MfaService);
mockInstance(UserService);
mockInstance(UserRepository);
mockInstance(PostHogClient);
mockInstance(License);
const controller = Container.get(AuthController);
const userService = Container.get(UserService);
const authService = Container.get(AuthService);
const eventsService = Container.get(EventService);
const postHog = Container.get(PostHogClient);
describe('login', () => {
it('should not validate email in "emailOrLdapLoginId" if LDAP is enabled', async () => {
// Arrange
const browserId = '1';
const member = mock<User>({
id: '123',
role: 'global:member',
mfaEnabled: false,
});
const body = mock<LoginRequestDto>({
emailOrLdapLoginId: 'non email',
password: 'password',
});
const req = mock<AuthenticatedRequest>({
user: member,
body,
browserId,
});
const res = mock<Response>();
mockedAuth.handleEmailLogin.mockResolvedValue(member);
mockedAuth.handleLdapLogin.mockResolvedValue(member);
config.set('userManagement.authenticationMethod', 'ldap');
// Act
await controller.login(req, res, body);
// Assert
expect(mockedAuth.handleEmailLogin).toHaveBeenCalledWith(
body.emailOrLdapLoginId,
body.password,
);
expect(mockedAuth.handleLdapLogin).toHaveBeenCalledWith(
body.emailOrLdapLoginId,
body.password,
);
expect(authService.issueCookie).toHaveBeenCalledWith(res, member, browserId);
expect(eventsService.emit).toHaveBeenCalledWith('user-logged-in', {
user: member,
authenticationMethod: 'ldap',
});
expect(userService.toPublic).toHaveBeenCalledWith(member, {
posthog: postHog,
withScopes: true,
});
});
});
});

View file

@ -1,4 +1,5 @@
import { LoginRequestDto, ResolveSignupTokenQueryDto } from '@n8n/api-types';
import { isEmail } from 'class-validator';
import { Response } from 'express';
import { Logger } from 'n8n-core';
@ -44,14 +45,19 @@ export class AuthController {
res: Response,
@Body payload: LoginRequestDto,
): Promise<PublicUser | undefined> {
const { email, password, mfaCode, mfaRecoveryCode } = payload;
const { emailOrLdapLoginId, password, mfaCode, mfaRecoveryCode } = payload;
let user: User | undefined;
let usedAuthenticationMethod = getCurrentAuthenticationMethod();
if (usedAuthenticationMethod === 'email' && !isEmail(emailOrLdapLoginId)) {
throw new BadRequestError('Invalid email address');
}
if (isSamlCurrentAuthenticationMethod()) {
// attempt to fetch user data with the credentials, but don't log in yet
const preliminaryUser = await handleEmailLogin(email, password);
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
// if the user is an owner, continue with the login
if (
preliminaryUser?.role === 'global:owner' ||
@ -63,15 +69,15 @@ export class AuthController {
throw new AuthError('SSO is enabled, please log in with SSO');
}
} else if (isLdapCurrentAuthenticationMethod()) {
const preliminaryUser = await handleEmailLogin(email, password);
const preliminaryUser = await handleEmailLogin(emailOrLdapLoginId, password);
if (preliminaryUser?.role === 'global:owner') {
user = preliminaryUser;
usedAuthenticationMethod = 'email';
} else {
user = await handleLdapLogin(email, password);
user = await handleLdapLogin(emailOrLdapLoginId, password);
}
} else {
user = await handleEmailLogin(email, password);
user = await handleEmailLogin(emailOrLdapLoginId, password);
}
if (user) {
@ -101,7 +107,7 @@ export class AuthController {
}
this.eventService.emit('user-login-failed', {
authenticationMethod: usedAuthenticationMethod,
userEmail: email,
userEmail: emailOrLdapLoginId,
reason: 'wrong credentials',
});
throw new AuthError('Wrong username or password. Do you have caps lock on?');

View file

@ -43,7 +43,7 @@ describe('POST /login', () => {
test('should log user in', async () => {
const response = await testServer.authlessAgent.post('/login').send({
email: owner.email,
emailOrLdapLoginId: owner.email,
password: ownerPassword,
});
@ -87,7 +87,7 @@ describe('POST /login', () => {
await mfaService.enableMfa(owner.id);
const response = await testServer.authlessAgent.post('/login').send({
email: owner.email,
emailOrLdapLoginId: owner.email,
password: ownerPassword,
mfaCode: mfaService.totp.generateTOTP(secret),
});
@ -131,7 +131,7 @@ describe('POST /login', () => {
});
const response = await testServer.authlessAgent.post('/login').send({
email: member.email,
emailOrLdapLoginId: member.email,
password,
});
expect(response.statusCode).toBe(403);
@ -148,19 +148,16 @@ describe('POST /login', () => {
expect(response.statusCode).toBe(200);
});
test('should fail on invalid email in the payload', async () => {
test('should fail with invalid email in the payload is the current authentication method is "email"', async () => {
config.set('userManagement.authenticationMethod', 'email');
const response = await testServer.authlessAgent.post('/login').send({
email: 'invalid-email',
emailOrLdapLoginId: 'invalid-email',
password: ownerPassword,
});
expect(response.statusCode).toBe(400);
expect(response.body).toEqual({
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: ['email'],
});
expect(response.body.message).toBe('Invalid email address');
});
});

View file

@ -470,7 +470,7 @@ describe('POST /login', () => {
const response = await testServer.authlessAgent
.post('/login')
.send({ email: ldapUser.mail, password: 'password' });
.send({ emailOrLdapLoginId: ldapUser.mail, password: 'password' });
expect(response.statusCode).toBe(200);
expect(response.headers['set-cookie']).toBeDefined();
@ -529,7 +529,7 @@ describe('POST /login', () => {
const response = await testServer.authlessAgent
.post('/login')
.send({ email: owner.email, password: 'password' });
.send({ emailOrLdapLoginId: owner.email, password: 'password' });
expect(response.status).toBe(200);
expect(response.body.data?.signInType).toBeDefined();

View file

@ -268,7 +268,7 @@ describe('Change password with MFA enabled', () => {
.authAgentFor(user)
.post('/login')
.send({
email: user.email,
emailOrLdapLoginId: user.email,
password: newPassword,
mfaCode: new TOTPService().generateTOTP(rawSecret),
})
@ -306,7 +306,10 @@ describe('Login', () => {
const user = await createUser({ password });
await testServer.authlessAgent.post('/login').send({ email: user.email, password }).expect(200);
await testServer.authlessAgent
.post('/login')
.send({ emailOrLdapLoginId: user.email, password })
.expect(200);
});
test('GET /login should not include mfaSecret and mfaRecoveryCodes property in response', async () => {
@ -323,7 +326,7 @@ describe('Login', () => {
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword })
.send({ emailOrLdapLoginId: user.email, password: rawPassword })
.expect(401);
});
@ -333,7 +336,7 @@ describe('Login', () => {
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaCode: 'wrongvalue' })
.send({ emailOrLdapLoginId: user.email, password: rawPassword, mfaCode: 'wrongvalue' })
.expect(401);
});
@ -342,7 +345,7 @@ describe('Login', () => {
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword })
.send({ emailOrLdapLoginId: user.email, password: rawPassword })
.expect(401);
expect(response.body.code).toBe(998);
@ -355,7 +358,7 @@ describe('Login', () => {
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaCode: token })
.send({ emailOrLdapLoginId: user.email, password: rawPassword, mfaCode: token })
.expect(200);
const data = response.body.data;
@ -370,7 +373,11 @@ describe('Login', () => {
await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: 'wrongvalue' })
.send({
emailOrLdapLoginId: user.email,
password: rawPassword,
mfaRecoveryCode: 'wrongvalue',
})
.expect(401);
});
@ -379,7 +386,11 @@ describe('Login', () => {
const response = await testServer.authlessAgent
.post('/login')
.send({ email: user.email, password: rawPassword, mfaRecoveryCode: rawRecoveryCodes[0] })
.send({
emailOrLdapLoginId: user.email,
password: rawPassword,
mfaRecoveryCode: rawRecoveryCodes[0],
})
.expect(200);
const data = response.body.data;

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "1.80.0",
"version": "1.81.0",
"description": "Core functionality of n8n",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/chat",
"version": "0.34.0",
"version": "0.35.0",
"scripts": {
"dev": "pnpm run storybook",
"build": "pnpm build:vite && pnpm build:bundle",

View file

@ -1,7 +1,7 @@
{
"name": "@n8n/composables",
"type": "module",
"version": "1.2.0",
"version": "1.3.0",
"files": [
"dist"
],

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/design-system",
"version": "1.69.0",
"version": "1.70.0",
"main": "src/index.ts",
"import": "src/index.ts",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "1.81.0",
"version": "1.82.0",
"description": "Workflow Editor UI for n8n",
"main": "index.js",
"scripts": {

View file

@ -1,4 +1,5 @@
import type {
LoginRequestDto,
PasswordUpdateRequestDto,
SettingsUpdateRequestDto,
UserUpdateRequestDto,
@ -21,7 +22,7 @@ export async function loginCurrentUser(
export async function login(
context: IRestApiContext,
params: { email: string; password: string; mfaCode?: string; mfaRecoveryToken?: string },
params: LoginRequestDto,
): Promise<CurrentUserResponse> {
return await makeRestApiRequest(context, 'POST', '/login', params);
}

View file

@ -166,14 +166,14 @@ describe('RunData', () => {
expect(queryByTestId('ndv-pin-data')).not.toBeInTheDocument();
});
it('should disable pin data button when data is pinned', async () => {
it('should not disable pin data button when data is pinned [ADO-3143]', async () => {
const { getByTestId } = render({
defaultRunItems: [],
displayMode: 'table',
pinnedData: [{ json: { name: 'Test' } }],
});
const pinDataButton = getByTestId('ndv-pin-data');
expect(pinDataButton).toBeDisabled();
expect(pinDataButton).not.toBeDisabled();
});
it('should render callout when data is pinned in output panel', async () => {

View file

@ -528,8 +528,7 @@ const showPinButton = computed(() => {
const pinButtonDisabled = computed(
() =>
pinnedData.hasData.value ||
!rawInputData.value.length ||
(!rawInputData.value.length && !pinnedData.hasData.value) ||
!!binaryData.value?.length ||
isReadOnlyRoute.value ||
readOnlyEnv.value,

View file

@ -30,7 +30,7 @@ const renderComponent = createComponentRenderer(RunDataPinButton, {
},
dataPinningDocsUrl: '',
pinnedData: {
hasData: false,
hasData: { value: false },
},
disabled: false,
},
@ -121,4 +121,30 @@ describe('RunDataPinButton.vue', () => {
expect(getByRole('tooltip')).toBeVisible();
expect(getByRole('tooltip')).toHaveTextContent('disabled');
});
it('pins data on button click', async () => {
const { getByTestId, getByRole, emitted } = renderComponent({});
// Should show 'Pin data' tooltip and emit togglePinData event
await userEvent.hover(getByTestId('ndv-pin-data'));
expect(getByRole('tooltip')).toBeVisible();
expect(getByRole('tooltip').textContent).toContain('Pin data');
await userEvent.click(getByTestId('ndv-pin-data'));
expect(emitted().togglePinData).toBeDefined();
});
it('should show correct tooltip and unpin data on button click', async () => {
const { getByTestId, getByRole, emitted } = renderComponent({
props: {
pinnedData: {
hasData: { value: true },
},
},
});
// Should show 'Unpin data' tooltip and emit togglePinData event
await userEvent.hover(getByTestId('ndv-pin-data'));
expect(getByRole('tooltip')).toBeVisible();
expect(getByRole('tooltip').textContent).toContain('Unpin data');
await userEvent.click(getByTestId('ndv-pin-data'));
expect(emitted().togglePinData).toBeDefined();
});
});

View file

@ -37,14 +37,19 @@ const visible = computed(() =>
{{ locale.baseText('node.discovery.pinData.ndv') }}
</div>
<div v-else>
<strong>{{ locale.baseText('ndv.pinData.pin.title') }}</strong>
<N8nText size="small" tag="p">
{{ locale.baseText('ndv.pinData.pin.description') }}
<div v-if="pinnedData.hasData.value">
<strong>{{ locale.baseText('ndv.pinData.unpin.title') }}</strong>
</div>
<div v-else>
<strong>{{ locale.baseText('ndv.pinData.pin.title') }}</strong>
<N8nText size="small" tag="p">
{{ locale.baseText('ndv.pinData.pin.description') }}
<N8nLink :to="props.dataPinningDocsUrl" size="small">
{{ locale.baseText('ndv.pinData.pin.link') }}
</N8nLink>
</N8nText>
<N8nLink :to="props.dataPinningDocsUrl" size="small">
{{ locale.baseText('ndv.pinData.pin.link') }}
</N8nLink>
</N8nText>
</div>
</div>
</template>
<N8nIconButton

View file

@ -1042,9 +1042,10 @@
"ndv.title.rename": "Rename",
"ndv.title.renameNode": "Rename node",
"ndv.pinData.pin.title": "Pin data",
"ndv.pinData.pin.description": "Node will always output this data instead of executing.",
"ndv.pinData.pin.description": "Node will always output current data instead of executing. Doesn't apply to production executions.",
"ndv.pinData.pin.binary": "Pin Data is disabled as this node's output contains binary data.",
"ndv.pinData.pin.link": "More info",
"ndv.pinData.unpin.title": "Unpin data",
"ndv.pinData.pin.multipleRuns.title": "Run #{index} was pinned",
"ndv.pinData.pin.multipleRuns.description": "This run will be outputted each time the node is run.",
"ndv.pinData.unpinAndExecute.title": "Unpin output data?",

View file

@ -1,4 +1,5 @@
import type {
LoginRequestDto,
PasswordUpdateRequestDto,
SettingsUpdateRequestDto,
UserUpdateRequestDto,
@ -181,12 +182,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
};
};
const loginWithCreds = async (params: {
email: string;
password: string;
mfaCode?: string;
mfaRecoveryCode?: string;
}) => {
const loginWithCreds = async (params: LoginRequestDto) => {
const user = await usersApi.login(rootStore.restApiContext, params);
if (!user) {
return;

View file

@ -3,6 +3,7 @@ import Logo from '@/components/Logo/Logo.vue';
import SSOLogin from '@/components/SSOLogin.vue';
import type { IFormBoxConfig } from '@/Interface';
import { useSettingsStore } from '@/stores/settings.store';
import type { EmailOrLdapLoginIdAndPassword } from './SigninView.vue';
withDefaults(
defineProps<{
@ -19,7 +20,7 @@ withDefaults(
const emit = defineEmits<{
update: [{ name: string; value: string }];
submit: [values: { [key: string]: string }];
submit: [values: EmailOrLdapLoginIdAndPassword];
secondaryClick: [];
}>();
@ -27,7 +28,7 @@ const onUpdate = (e: { name: string; value: string }) => {
emit('update', e);
};
const onSubmit = (values: { [key: string]: string }) => {
const onSubmit = (values: EmailOrLdapLoginIdAndPassword) => {
emit('submit', values);
};

View file

@ -85,7 +85,7 @@ describe('SigninView', () => {
await userEvent.click(submitButton);
expect(usersStore.loginWithCreds).toHaveBeenCalledWith({
email: 'test@n8n.io',
emailOrLdapLoginId: 'test@n8n.io',
password: 'password',
mfaCode: undefined,
mfaRecoveryCode: undefined,

View file

@ -15,6 +15,14 @@ import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import type { IFormBoxConfig } from '@/Interface';
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS, MFA_FORM } from '@/constants';
import type { LoginRequestDto } from '@n8n/api-types';
export type EmailOrLdapLoginIdAndPassword = Pick<
LoginRequestDto,
'emailOrLdapLoginId' | 'password'
>;
export type MfaCodeOrMfaRecoveryCode = Pick<LoginRequestDto, 'mfaCode' | 'mfaRecoveryCode'>;
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
@ -29,7 +37,7 @@ const telemetry = useTelemetry();
const loading = ref(false);
const showMfaView = ref(false);
const email = ref('');
const emailOrLdapLoginId = ref('');
const password = ref('');
const reportError = ref(false);
@ -50,7 +58,7 @@ const formConfig: IFormBoxConfig = reactive({
redirectLink: '/forgot-password',
inputs: [
{
name: 'email',
name: 'emailOrLdapLoginId',
properties: {
label: emailLabel.value,
type: 'email',
@ -78,23 +86,16 @@ const formConfig: IFormBoxConfig = reactive({
],
});
const onMFASubmitted = async (form: { mfaCode?: string; mfaRecoveryCode?: string }) => {
const onMFASubmitted = async (form: MfaCodeOrMfaRecoveryCode) => {
await login({
email: email.value,
emailOrLdapLoginId: emailOrLdapLoginId.value,
password: password.value,
mfaCode: form.mfaCode,
mfaRecoveryCode: form.mfaRecoveryCode,
});
};
const isFormWithEmailAndPassword = (values: {
[key: string]: string;
}): values is { email: string; password: string } => {
return 'email' in values && 'password' in values;
};
const onEmailPasswordSubmitted = async (form: { [key: string]: string }) => {
if (!isFormWithEmailAndPassword(form)) return;
const onEmailPasswordSubmitted = async (form: EmailOrLdapLoginIdAndPassword) => {
await login(form);
};
@ -111,16 +112,11 @@ const getRedirectQueryParameter = () => {
return redirect;
};
const login = async (form: {
email: string;
password: string;
mfaCode?: string;
mfaRecoveryCode?: string;
}) => {
const login = async (form: LoginRequestDto) => {
try {
loading.value = true;
await usersStore.loginWithCreds({
email: form.email,
emailOrLdapLoginId: form.emailOrLdapLoginId,
password: form.password,
mfaCode: form.mfaCode,
mfaRecoveryCode: form.mfaRecoveryCode,
@ -185,8 +181,8 @@ const onFormChanged = (toForm: string) => {
reportError.value = false;
}
};
const cacheCredentials = (form: { email: string; password: string }) => {
email.value = form.email;
const cacheCredentials = (form: EmailOrLdapLoginIdAndPassword) => {
emailOrLdapLoginId.value = form.emailOrLdapLoginId;
password.value = form.password;
};
</script>

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "1.80.0",
"version": "1.81.0",
"description": "CLI to simplify n8n credentials/node development",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",

View file

@ -457,14 +457,12 @@ export class Github implements INodeType {
required: true,
modes: [
{
displayName: 'Workflow',
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a workflow...',
typeOptions: {
searchListMethod: 'getWorkflows',
searchable: true,
searchFilterRequired: true,
},
},
{
@ -482,6 +480,21 @@ export class Github implements INodeType {
},
],
},
{
displayName: 'By File Name',
name: 'filename',
type: 'string',
placeholder: 'e.g. main.yaml or main.yml',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9_-]+.(yaml|yml)',
errorMessage: 'Not a valid Github Workflow File Name',
},
},
],
},
],
displayOptions: {
show: {
@ -2501,7 +2514,9 @@ export class Github implements INodeType {
requestMethod = 'POST';
const workflowId = this.getNodeParameter('workflowId', i) as string;
const workflowId = this.getNodeParameter('workflowId', i, undefined, {
extractValue: true,
}) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
body.ref = this.getNodeParameter('ref', i) as string;

View file

@ -0,0 +1,91 @@
import nock from 'nock';
import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
const workflows = getWorkflowFilenames(__dirname);
describe('Test Github Node', () => {
describe('Workflow Dispatch', () => {
const now = 1683028800000;
const owner = 'testOwner';
const repository = 'testRepository';
const workflowId = 147025216;
const usersResponse = {
total_count: 12,
items: [
{
login: 'testOwner',
id: 1,
},
],
};
const repositoriesResponse = {
total_count: 40,
items: [
{
id: 3081286,
name: 'testRepository',
},
],
};
const workflowsResponse = {
total_count: 2,
workflows: [
{
id: workflowId,
node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
name: 'CI',
path: '.github/workflows/blank.yaml',
state: 'active',
created_at: '2020-01-08T23:48:37.000-08:00',
updated_at: '2020-01-08T23:50:21.000-08:00',
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335',
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/161335',
badge_url: 'https://github.com/octo-org/octo-repo/workflows/CI/badge.svg',
},
{
id: 269289,
node_id: 'MDE4OldvcmtmbG93IFNlY29uZGFyeTI2OTI4OQ==',
name: 'Linter',
path: '.github/workflows/linter.yaml',
state: 'active',
created_at: '2020-01-08T23:48:37.000-08:00',
updated_at: '2020-01-08T23:50:21.000-08:00',
url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/269289',
html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/269289',
badge_url: 'https://github.com/octo-org/octo-repo/workflows/Linter/badge.svg',
},
],
};
beforeAll(async () => {
jest.useFakeTimers({ doNotFake: ['nextTick'], now });
await initBinaryDataService();
});
beforeEach(async () => {
const baseUrl = 'https://api.github.com';
nock.cleanAll();
nock(baseUrl)
.persist()
.defaultReplyHeaders({ 'Content-Type': 'application/json' })
.get('/search/users')
.query(true)
.reply(200, usersResponse)
.get('/search/repositories')
.query(true)
.reply(200, repositoriesResponse)
.get(`/repos/${owner}/${repository}/actions/workflows`)
.reply(200, workflowsResponse)
.post(`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`, {
ref: 'main',
inputs: {},
})
.reply(200, {});
});
afterEach(() => {
nock.cleanAll();
});
testWorkflows(workflows);
});
});

View file

@ -0,0 +1,86 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-300, 260],
"id": "b14bf20f-78b0-490a-bbc6-d02b1af4c03c",
"name": "When clicking Test workflow"
},
{
"parameters": {
"resource": "workflow",
"workflowId": {
"__rl": true,
"value": 147025216,
"mode": "list",
"cachedResultName": "CI"
},
"owner": {
"__rl": true,
"value": "testOwner",
"mode": "name"
},
"repository": {
"__rl": true,
"value": "testRepository",
"mode": "name"
}
},
"type": "n8n-nodes-base.github",
"typeVersion": 1,
"position": [-80, 260],
"id": "061752c9-507c-4b27-ba18-47b21d487aed",
"name": "GitHub",
"credentials": {
"githubApi": {
"id": "1",
"name": "GitHub account"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [120, 260],
"id": "3bc54e8f-eeba-496d-a95f-bb8927eff671",
"name": "No Operation, do nothing"
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "GitHub",
"type": "main",
"index": 0
}
]
]
},
"GitHub": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {
"No Operation, do nothing": [
{
"json": {}
}
]
},
"meta": {
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "1.80.0",
"version": "1.81.0",
"description": "Base nodes of n8n",
"main": "index.js",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "1.79.0",
"version": "1.80.0",
"description": "Workflow base code of n8n",
"main": "dist/index.js",
"module": "src/index.ts",