mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'n8n-io:master' into MongoDB_vector_store
This commit is contained in:
commit
afe56adc0e
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.81.0",
|
||||
"version": "1.82.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "0.22.0",
|
||||
"version": "0.23.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/config",
|
||||
"version": "1.30.0",
|
||||
"version": "1.31.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/di",
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/imap",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/n8n-nodes-langchain",
|
||||
"version": "1.81.0",
|
||||
"version": "1.82.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/permissions",
|
||||
"version": "0.18.0",
|
||||
"version": "0.19.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/typescript-config",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"tsconfig.backend.json",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@n8n/utils",
|
||||
"type": "module",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/vitest-config",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"vite": "catalog:frontend",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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?');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@n8n/composables",
|
||||
"type": "module",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue