Merge branch 'master' of github.com:seatable/n8n into seatable_node_rework

This commit is contained in:
Christoph Dyllick-Brenzinger 2024-06-25 10:44:16 +02:00
commit 80fc892192
808 changed files with 17681 additions and 10103 deletions

View file

@ -153,7 +153,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Cypress run
uses: cypress-io/github-action@v5.8.3
uses: cypress-io/github-action@v6.6.1
with:
install: false
start: pnpm start
@ -172,6 +172,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
E2E_TESTS: true
COMMIT_INFO_MESSAGE: 🌳 ${{ inputs.branch }} 🖥️ ${{ inputs.run-env }} 🤖 ${{ inputs.user }} 🗃️ ${{ inputs.spec }}
SHELL: /bin/sh
# Check if all tests passed and set the output variable
check_testing_matrix:

1
.gitignore vendored
View file

@ -22,4 +22,3 @@ cypress/screenshots/*
cypress/downloads/*
*.swp
CHANGELOG-*.md
packages/cli/oclif.manifest.json

View file

@ -1,3 +1,44 @@
# [1.25.0](https://github.com/n8n-io/n8n/compare/n8n@1.24.0...n8n@1.25.0) (2024-01-17)
### Bug Fixes
* Add fallback resolver for langchain modules ([#8308](https://github.com/n8n-io/n8n/issues/8308)) ([851060d](https://github.com/n8n-io/n8n/commit/851060dd3f38245da6e09c04ec0b12b24b63dca4))
* **API:** Fix manual chat trigger execution ([#8300](https://github.com/n8n-io/n8n/issues/8300)) ([884396e](https://github.com/n8n-io/n8n/commit/884396ea0d9f4a8d7987daf2b674f080056dd1d1))
* **AwsS3 Node:** Return confirmation of success after upload ([#8312](https://github.com/n8n-io/n8n/issues/8312)) ([c921665](https://github.com/n8n-io/n8n/commit/c921665f9abe19d9e8831062c1e7673d4d1ea694))
* **core:** Account for immediate confirmation request during test webhook creation ([#8329](https://github.com/n8n-io/n8n/issues/8329)) ([5fbd797](https://github.com/n8n-io/n8n/commit/5fbd7971e04640be3f877b3aa22d4aee61c1d40a))
* **core:** Ensure waiting executions account for workflow timezone ([#8340](https://github.com/n8n-io/n8n/issues/8340)) ([3734c89](https://github.com/n8n-io/n8n/commit/3734c89cf64514489831b5339d722c89b300cc54))
* **core:** Parse any readable stream response instead of only IncomingMessage ([#8359](https://github.com/n8n-io/n8n/issues/8359)) ([eb1320f](https://github.com/n8n-io/n8n/commit/eb1320fd7a4a67cd16de10c4174c7bcf2c177b06))
* **core:** Prevent invalid compressed responses from making executions stuck forever ([#8315](https://github.com/n8n-io/n8n/issues/8315)) ([0776814](https://github.com/n8n-io/n8n/commit/0776814ed8c520326a6447dcd7b6c53fda933054))
* **core:** Prevent issues with missing or mismatching encryption key ([#8332](https://github.com/n8n-io/n8n/issues/8332)) ([d4c93b1](https://github.com/n8n-io/n8n/commit/d4c93b16071081002b4bd316be0921bc7867dd82))
* **core:** Prevent NodeErrors from being wrapped multiple times ([#8301](https://github.com/n8n-io/n8n/issues/8301)) ([b267bf0](https://github.com/n8n-io/n8n/commit/b267bf07e365d8bb82a9847fb3c490437dc1010e))
* **core:** Replace all `moment` imports with `moment-timezone` ([#8337](https://github.com/n8n-io/n8n/issues/8337)) ([52a2e25](https://github.com/n8n-io/n8n/commit/52a2e25a25e9a009a536d8a371d9404e75d756f4))
* **core:** Report when waitTill is invalid and handle it ([#8356](https://github.com/n8n-io/n8n/issues/8356)) ([d5455d7](https://github.com/n8n-io/n8n/commit/d5455d7accb193078b05a0f52386cf9303b6a00f))
* **editor:** Add read only mode to filter component ([#8285](https://github.com/n8n-io/n8n/issues/8285)) ([dcc76f3](https://github.com/n8n-io/n8n/commit/dcc76f348075b6e05e3f38bb9694d25ac9a5646b))
* **editor:** Capture indexed access expressions when building completions ([#8331](https://github.com/n8n-io/n8n/issues/8331)) ([159b328](https://github.com/n8n-io/n8n/commit/159b328587f3c57c73ae77c2a0c5d5c6ecc330aa))
* **editor:** Fix issue with synchronization table on LDAP not loading data ([#8327](https://github.com/n8n-io/n8n/issues/8327)) ([6b92d49](https://github.com/n8n-io/n8n/commit/6b92d49ea58b8e5797e4e938444b161a63137638))
* **editor:** Properly set colors for connections and labels on nodes with pinned data ([#8209](https://github.com/n8n-io/n8n/issues/8209)) ([3b8ccb9](https://github.com/n8n-io/n8n/commit/3b8ccb9fb903036a7d6e4b33f6b5a8933576e9e6))
* Fix node graph telemetry with default values ([#8297](https://github.com/n8n-io/n8n/issues/8297)) ([93b969a](https://github.com/n8n-io/n8n/commit/93b969a327e0770d9a0e81a95a5185b0fc12ebc6))
* **Google Drive Node:** Fix issue preventing service account from downloading files ([#7642](https://github.com/n8n-io/n8n/issues/7642)) ([cf7131d](https://github.com/n8n-io/n8n/commit/cf7131d766dfc7aec2c973525653ffec1ced03c1))
* **HTTP Request Node:** Delete `response.request` only when it's a valid circular references ([#8293](https://github.com/n8n-io/n8n/issues/8293)) ([05c43fa](https://github.com/n8n-io/n8n/commit/05c43faa2d7582a8ce58b9bb3338c00253ad3281))
* **Microsoft SQL Node:** Fix "Maximum call stack size exceeded" error on too many rows ([#8334](https://github.com/n8n-io/n8n/issues/8334)) ([bb2be8d](https://github.com/n8n-io/n8n/commit/bb2be8d70580896321641a49a3044165763eb9e1))
* **Ollama Model Node:** Use a simpler credentials test ([#8318](https://github.com/n8n-io/n8n/issues/8318)) ([63b738a](https://github.com/n8n-io/n8n/commit/63b738a542429934b3838bfc814ea2a4c51675c7))
* **OpenAI Node:** Load correct models for operation ([#8313](https://github.com/n8n-io/n8n/issues/8313)) ([a6a5372](https://github.com/n8n-io/n8n/commit/a6a5372b5f8e48e98788c4e3750ac4b63e91a96f))
* Properly output saml validation errors ([#8284](https://github.com/n8n-io/n8n/issues/8284)) ([8c7f399](https://github.com/n8n-io/n8n/commit/8c7f39907fa82fa37af4436511d4a2daaff13015))
* **Salesforce Node:** Upgrade to API version 59 ([#8346](https://github.com/n8n-io/n8n/issues/8346)) ([b51cbb3](https://github.com/n8n-io/n8n/commit/b51cbb325e03fd42be6dca99819d4cc7c4c1574b))
* **Supabase Node:** Pagination for get all rows ([#8311](https://github.com/n8n-io/n8n/issues/8311)) ([e080476](https://github.com/n8n-io/n8n/commit/e0804768e84aefe9d66ab683080f67bb15a1cb58))
* **Venafi TLS Protect Cloud Node:** Remove parameter `Application Server Type` ([#8325](https://github.com/n8n-io/n8n/issues/8325)) ([e3cedf7](https://github.com/n8n-io/n8n/commit/e3cedf7db038a70c9d48bb7c665b1be4beb872a9))
* **Venafi TLS Protect Cloud Trigger Node:** Handle new webhook payload format ([#8326](https://github.com/n8n-io/n8n/issues/8326)) ([057d7d0](https://github.com/n8n-io/n8n/commit/057d7d031828ea8b6e779ca535ccd50d91bfa0cc))
### Features
* **core:** Implement inter-main communication for test webhooks in multi-main setup ([#8267](https://github.com/n8n-io/n8n/issues/8267)) ([1a0e285](https://github.com/n8n-io/n8n/commit/1a0e28555385f682aa335115c4d72e671c0bdc85))
* **editor:** Add new `/templates/search` endpoint ([#8227](https://github.com/n8n-io/n8n/issues/8227)) ([4277e92](https://github.com/n8n-io/n8n/commit/4277e92ec07671a679b0d9ab6e691ef9208585bd))
* Implement Chat Memory Manager node ([#8127](https://github.com/n8n-io/n8n/issues/8127)) ([464be93](https://github.com/n8n-io/n8n/commit/464be9332354620b2f1890136abf95dfdb71fd2e))
# [1.24.0](https://github.com/n8n-io/n8n/compare/n8n@1.23.0...n8n@1.24.0) (2024-01-10)

View file

@ -0,0 +1,18 @@
//#region Getters
export const getBecomeTemplateCreatorCta = () => cy.getByTestId('become-template-creator-cta');
export const getCloseBecomeTemplateCreatorCtaButton = () =>
cy.getByTestId('close-become-template-creator-cta');
//#endregion
//#region Actions
export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
return cy.intercept('GET', `/rest/cta/become-creator`, {
body: becomeCreator,
});
};
//#endregion

View file

@ -35,7 +35,7 @@ export const INSTANCE_MEMBERS = [
];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test Workflow"';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test workflow"';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';

View file

@ -1,5 +1,6 @@
import { v4 as uuid } from 'uuid';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
@ -409,5 +410,83 @@ describe('Execution', () => {
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
it('when connecting pinned node by output drag and drop', () => {
cy.drag(
workflowPage.getters.getEndpointSelector('output', SCHEDULE_TRIGGER_NODE_NAME),
[-200, -300],
);
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [150, 200], {
clickToFinish: true,
});
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.actions.executeWorkflow();
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
cy.drag(workflowPage.getters.getEndpointSelector('output', 'Edit Fields2'), [-200, -300]);
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [150, 200], {
clickToFinish: true,
});
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields2', 'Edit Fields11')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
it('when connecting pinned node after adding an unconnected node', () => {
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', SCHEDULE_TRIGGER_NODE_NAME),
workflowPage.getters.getEndpointSelector('input', 'Edit Fields8'),
);
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.actions.executeWorkflow();
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields8')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.actions.deselectAll();
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.getters.zoomToFitButton().click();
cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', 'Edit Fields7'),
workflowPage.getters.getEndpointSelector('input', 'Edit Fields11'),
);
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields7', 'Edit Fields11')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
});
});

View file

@ -16,11 +16,11 @@ describe('Current Workflow Executions', () => {
it('should render executions tab correctly', () => {
createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionListItems().should('have.length', 11);
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
@ -34,7 +34,7 @@ describe('Current Workflow Executions', () => {
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
cy.intercept('GET', '/rest/executions?filter=*');
cy.intercept('GET', '/rest/executions-current?filter=*');
cy.intercept('GET', '/rest/executions/active?filter=*');
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab();
@ -63,7 +63,7 @@ describe('Current Workflow Executions', () => {
};
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
cy.intercept('GET', '/rest/executions-current?filter=*', throttleResponse);
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab();

View file

@ -19,7 +19,7 @@ describe('Debug', () => {
it('should be able to debug executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
@ -41,7 +41,7 @@ describe('Debug', () => {
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click();
cy.url().should('include', '/debug');
@ -66,7 +66,7 @@ describe('Debug', () => {
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
cy.wait(['@getExecution']);
@ -77,7 +77,7 @@ describe('Debug', () => {
confirmDialog.find('li').should('have.length', 2);
confirmDialog.get('.btn--cancel').click();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionListItems().should('have.length', 2).first().click();
cy.wait(['@getExecution']);
@ -108,7 +108,7 @@ describe('Debug', () => {
cy.url().should('not.include', '/debug');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible');
@ -130,7 +130,7 @@ describe('Debug', () => {
workflowPage.actions.deleteNode(IF_NODE_NAME);
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionListItems().should('have.length', 3).first().click();
cy.wait(['@getExecution']);
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();

View file

@ -136,10 +136,10 @@ describe('Editor actions should work', () => {
it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
cy.wait(500);
executionsTab.actions.switchToEditorTab();
editWorkflowAndDeactivate();
@ -149,7 +149,7 @@ describe('Editor actions should work', () => {
it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
editWorkflowAndDeactivate();
@ -157,7 +157,7 @@ describe('Editor actions should work', () => {
cy.wait(['@postWorkflowRun']);
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
cy.wait(['@getExecution']);

View file

@ -49,7 +49,7 @@ describe('Suggested templates - Should render', () => {
it('should render suggested templates when there are workflows in the list', () => {
WorkflowsListPage.getters.suggestedTemplatesNewWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Test Workflow');
cy.createFixtureWorkflow('Test_workflow_1.json', 'Test workflow');
cy.visit(WorkflowsListPage.url);
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('exist');
cy.contains(`Explore ${fixtureSections.sections[0].name.toLocaleLowerCase()} workflow templates`).should('exist');

View file

@ -0,0 +1,32 @@
import {
getBecomeTemplateCreatorCta,
getCloseBecomeTemplateCreatorCtaButton,
interceptCtaRequestWithResponse,
} from '../composables/becomeTemplateCreatorCta';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
const WorkflowsPage = new WorkflowsPageClass();
describe('Become creator CTA', () => {
it('should not show the CTA if user is not eligible', () => {
interceptCtaRequestWithResponse(false).as('cta');
cy.visit(WorkflowsPage.url);
cy.wait('@cta');
getBecomeTemplateCreatorCta().should('not.exist');
});
it('should show the CTA if the user is eligible', () => {
interceptCtaRequestWithResponse(true).as('cta');
cy.visit(WorkflowsPage.url);
cy.wait('@cta');
getBecomeTemplateCreatorCta().should('be.visible');
getCloseBecomeTemplateCreatorCtaButton().click();
getBecomeTemplateCreatorCta().should('not.exist');
});
});

View file

@ -308,7 +308,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Test Workflow"');
WorkflowPage.actions.deleteNode('When clicking "Test workflow"');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();

View file

@ -579,7 +579,7 @@ describe('NDV', () => {
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();
// Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking "Test Workflow"');
workflowPage.actions.openNode('When clicking "Test workflow"');
ndv.getters.nodeRunSuccessIndicator().should('exist');
// Code node should show error
ndv.getters.backToCanvas().click();

View file

@ -259,7 +259,7 @@ describe('Workflow Actions', () => {
it('should keep endpoint click working when switching between execution and editor tab', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('GET', '/rest/executions/active?filter=*').as('getActiveExecutions');
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
@ -270,7 +270,7 @@ describe('Workflow Actions', () => {
cy.get('body').type('{esc}');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(['@getExecutions', '@getActiveExecutions']);
cy.wait(500);
executionsTab.actions.switchToEditorTab();

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -91,7 +91,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "369fe424-dd3b-4399-9de3-50bd4ce1f75b",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -570,7 +570,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{
@ -1048,4 +1048,4 @@
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"tags": []
}
}

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "46770685-44d1-4aad-9107-1d790cf26b50",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -74,7 +74,7 @@
}
],
"pinData": {
"When clicking \"Test Workflow\"": [
"When clicking \"Test workflow\"": [
{
"json": {
"id": "654cfa05fa51480dcb543b1a",
@ -599,7 +599,7 @@
]
},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -42,7 +42,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -92,7 +92,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{
@ -191,7 +191,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -241,7 +241,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{
@ -374,7 +374,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -424,7 +424,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{
@ -524,7 +524,7 @@
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -574,7 +574,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -40,7 +40,7 @@
{
"parameters": {},
"id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -199,7 +199,7 @@
]
]
},
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -99,7 +99,7 @@
],
"pinData": {},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "aadaed66-84ed-4cf8-bf21-082e9a65db76",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [

View file

@ -47,7 +47,7 @@
{
"parameters": {},
"id": "58512a93-dabf-4584-817f-27c608c1bdd5",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -69,7 +69,7 @@
]
]
},
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -47,7 +47,7 @@
{
"parameters": {},
"id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -552,7 +552,7 @@
]
]
},
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -4,7 +4,7 @@
{
"parameters": {},
"id": "0a60e507-7f34-41c0-a0f9-697d852033b6",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -93,7 +93,7 @@
]
},
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -37,7 +37,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -90,7 +90,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -27,7 +27,7 @@
{
"parameters": {},
"id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -37,7 +37,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -6,7 +6,7 @@
{
"parameters": {},
"id": "40720511-19b6-4421-bdb0-3fb6efef4bc5",
"name": "When clicking \"Test Workflow\"",
"name": "When clicking \"Test workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
@ -64,7 +64,7 @@
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"When clicking \"Test workflow\"": {
"main": [
[
{

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "1.24.0",
"version": "1.25.0",
"private": true,
"homepage": "https://n8n.io",
"engines": {
@ -29,7 +29,7 @@
"test:backend": "pnpm --filter=!@n8n/chat --filter=!n8n-design-system --filter=!n8n-editor-ui --filter=!n8n-nodes-base test",
"test:nodes": "pnpm --filter=n8n-nodes-base test",
"test:frontend": "pnpm --filter=@n8n/chat --filter=n8n-design-system --filter=n8n-editor-ui test",
"watch": "turbo run watch",
"watch": "turbo run watch --parallel",
"webhook": "./packages/cli/bin/n8n webhook",
"worker": "./packages/cli/bin/n8n worker",
"cypress:install": "cypress install",

View file

@ -3,7 +3,7 @@ import type { LoadPreviousSessionResponse, SendMessageResponse } from '@n8n/chat
export function createFetchResponse<T>(data: T) {
return async () =>
({
json: async () => new Promise<T>((resolve) => resolve(data)),
json: async () => await new Promise<T>((resolve) => resolve(data)),
}) as Response;
}

View file

@ -16,7 +16,7 @@ export async function authenticatedFetch<T>(...args: Parameters<typeof fetch>):
},
});
return (await response.json()) as Promise<T>;
return (await response.json()) as T;
}
export async function get<T>(url: string, query: object = {}, options: RequestInit = {}) {
@ -27,11 +27,11 @@ export async function get<T>(url: string, query: object = {}, options: RequestIn
).toString()}`;
}
return authenticatedFetch<T>(resolvedUrl, { ...options, method: 'GET' });
return await authenticatedFetch<T>(resolvedUrl, { ...options, method: 'GET' });
}
export async function post<T>(url: string, body: object = {}, options: RequestInit = {}) {
return authenticatedFetch<T>(url, {
return await authenticatedFetch<T>(url, {
...options,
method: 'POST',
body: JSON.stringify(body),
@ -39,7 +39,7 @@ export async function post<T>(url: string, body: object = {}, options: RequestIn
}
export async function put<T>(url: string, body: object = {}, options: RequestInit = {}) {
return authenticatedFetch<T>(url, {
return await authenticatedFetch<T>(url, {
...options,
method: 'PUT',
body: JSON.stringify(body),
@ -47,7 +47,7 @@ export async function put<T>(url: string, body: object = {}, options: RequestIni
}
export async function patch<T>(url: string, body: object = {}, options: RequestInit = {}) {
return authenticatedFetch<T>(url, {
return await authenticatedFetch<T>(url, {
...options,
method: 'PATCH',
body: JSON.stringify(body),
@ -55,7 +55,7 @@ export async function patch<T>(url: string, body: object = {}, options: RequestI
}
export async function del<T>(url: string, body: object = {}, options: RequestInit = {}) {
return authenticatedFetch<T>(url, {
return await authenticatedFetch<T>(url, {
...options,
method: 'DELETE',
body: JSON.stringify(body),

View file

@ -7,7 +7,7 @@ import type {
export async function loadPreviousSession(sessionId: string, options: ChatOptions) {
const method = options.webhookConfig?.method === 'POST' ? post : get;
return method<LoadPreviousSessionResponse>(
return await method<LoadPreviousSessionResponse>(
`${options.webhookUrl}`,
{
action: 'loadPreviousSession',
@ -22,7 +22,7 @@ export async function loadPreviousSession(sessionId: string, options: ChatOption
export async function sendMessage(message: string, sessionId: string, options: ChatOptions) {
const method = options.webhookConfig?.method === 'POST' ? post : get;
return method<SendMessageResponse>(
return await method<SendMessageResponse>(
`${options.webhookUrl}`,
{
action: 'sendMessage',

View file

@ -20,6 +20,6 @@
"dist/**/*"
],
"dependencies": {
"axios": "1.6.2"
"axios": "1.6.5"
}
}

View file

@ -42,7 +42,7 @@ describe('CredentialsFlow', () => {
refresh_token: config.refreshToken,
scope: requestedScope,
});
return new Promise<{ headers: Headers; body: unknown }>((resolve) => {
return await new Promise<{ headers: Headers; body: unknown }>((resolve) => {
nockScope.once('request', (req) => {
resolve({
headers: req.headers,

View file

@ -252,15 +252,15 @@ export class Agent implements INodeType {
const agentType = this.getNodeParameter('agent', 0, '') as string;
if (agentType === 'conversationalAgent') {
return conversationalAgentExecute.call(this);
return await conversationalAgentExecute.call(this);
} else if (agentType === 'openAiFunctionsAgent') {
return openAiFunctionsAgentExecute.call(this);
return await openAiFunctionsAgentExecute.call(this);
} else if (agentType === 'reActAgent') {
return reActAgentAgentExecute.call(this);
return await reActAgentAgentExecute.call(this);
} else if (agentType === 'sqlAgent') {
return sqlAgentAgentExecute.call(this);
return await sqlAgentAgentExecute.call(this);
} else if (agentType === 'planAndExecuteAgent') {
return planAndExecuteAgentExecute.call(this);
return await planAndExecuteAgentExecute.call(this);
}
throw new NodeOperationError(this.getNode(), `The agent type "${agentType}" is not supported`);

View file

@ -102,5 +102,5 @@ export async function conversationalAgentExecute(
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}

View file

@ -101,5 +101,5 @@ export async function openAiFunctionsAgentExecute(
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}

View file

@ -76,5 +76,5 @@ export async function planAndExecuteAgentExecute(
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}

View file

@ -94,5 +94,5 @@ export async function reActAgentAgentExecute(
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}

View file

@ -101,5 +101,5 @@ export async function sqlAgentAgentExecute(
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}

View file

@ -380,6 +380,6 @@ export class OpenAiAssistant implements INodeType {
returnData.push({ json: response });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}
}

View file

@ -166,7 +166,7 @@ async function getChain(
// If there are no output parsers, create a simple LLM chain and execute the query
if (!outputParsers.length) {
return createSimpleLLMChain(context, llm, query, chatTemplate);
return await createSimpleLLMChain(context, llm, query, chatTemplate);
}
// If there's only one output parser, use it; otherwise, create a combined output parser

View file

@ -126,6 +126,6 @@ export class ChainRetrievalQa implements INodeType {
const response = await chain.call({ query });
returnData.push({ json: { response } });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}
}

View file

@ -258,6 +258,6 @@ export class ChainSummarizationV1 implements INodeType {
returnData.push({ json: { response } });
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}
}

View file

@ -415,6 +415,6 @@ export class ChainSummarizationV2 implements INodeType {
}
}
return this.prepareOutputData(returnData);
return await this.prepareOutputData(returnData);
}
}

View file

@ -98,7 +98,7 @@ export class MemoryChatRetriever implements INodeType {
const messages = await memory?.chatHistory.getMessages();
if (simplifyOutput && messages) {
return this.prepareOutputData(simplifyMessages(messages));
return await this.prepareOutputData(simplifyMessages(messages));
}
const serializedMessages =
@ -107,6 +107,6 @@ export class MemoryChatRetriever implements INodeType {
return { json: serializedMessage as unknown as IDataObject };
}) ?? [];
return this.prepareOutputData(serializedMessages);
return await this.prepareOutputData(serializedMessages);
}
}

View file

@ -324,6 +324,6 @@ export class MemoryManager implements INodeType {
result.push(...executionData);
}
return this.prepareOutputData(result);
return await this.prepareOutputData(result);
}
}

View file

@ -163,7 +163,7 @@ export class ToolCode implements INodeType {
const runFunction = async (query: string): Promise<string> => {
const sandbox = getSandbox(query, itemIndex);
return sandbox.runCode() as Promise<string>;
return await (sandbox.runCode() as Promise<string>);
};
return {

View file

@ -46,7 +46,7 @@ export const VectorStoreInMemory = createVectorStoreNode({
const memoryKey = context.getNodeParameter('memoryKey', itemIndex) as string;
const vectorStoreSingleton = MemoryVectorStoreManager.getInstance(embeddings);
return vectorStoreSingleton.getVectorStore(`${workflowId}__${memoryKey}`);
return await vectorStoreSingleton.getVectorStore(`${workflowId}__${memoryKey}`);
},
async populateVectorStore(context, embeddings, documents, itemIndex) {
const memoryKey = context.getNodeParameter('memoryKey', itemIndex) as string;

View file

@ -108,6 +108,6 @@ export class VectorStoreInMemoryInsert implements INodeType {
clearStore,
);
return this.prepareOutputData(serializedDocuments);
return await this.prepareOutputData(serializedDocuments);
}
}

View file

@ -97,7 +97,7 @@ export const VectorStorePinecone = createVectorStoreNode({
filter,
};
return PineconeStore.fromExistingIndex(embeddings, config);
return await PineconeStore.fromExistingIndex(embeddings, config);
},
async populateVectorStore(context, embeddings, documents, itemIndex) {
const index = context.getNodeParameter('pineconeIndex', itemIndex, '', {

View file

@ -134,6 +134,6 @@ export class VectorStorePineconeInsert implements INodeType {
pineconeIndex,
});
return this.prepareOutputData(serializedDocuments);
return await this.prepareOutputData(serializedDocuments);
}
}

View file

@ -59,7 +59,7 @@ export const VectorStoreQdrant = createVectorStoreNode({
collectionName: collection,
};
return QdrantVectorStore.fromExistingCollection(embeddings, config);
return await QdrantVectorStore.fromExistingCollection(embeddings, config);
},
async populateVectorStore(context, embeddings, documents, itemIndex) {
const collectionName = context.getNodeParameter('qdrantCollection', itemIndex, '', {

View file

@ -76,7 +76,7 @@ export const VectorStoreSupabase = createVectorStoreNode({
const credentials = await context.getCredentials('supabaseApi');
const client = createClient(credentials.host as string, credentials.serviceRole as string);
return SupabaseVectorStore.fromExistingIndex(embeddings, {
return await SupabaseVectorStore.fromExistingIndex(embeddings, {
client,
tableName,
queryName: options.queryName ?? 'match_documents',

View file

@ -122,6 +122,6 @@ export class VectorStoreSupabaseInsert implements INodeType {
queryName,
});
return this.prepareOutputData(serializedDocuments);
return await this.prepareOutputData(serializedDocuments);
}
}

View file

@ -139,6 +139,6 @@ export class VectorStoreZepInsert implements INodeType {
await ZepVectorStore.fromDocuments(processedDocuments, embeddings, zepConfig);
return this.prepareOutputData(serializedDocuments);
return await this.prepareOutputData(serializedDocuments);
}
}

View file

@ -239,7 +239,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
resultData.push(...serializedDocs);
}
return this.prepareOutputData(resultData);
return await this.prepareOutputData(resultData);
}
if (mode === 'insert') {
@ -267,7 +267,7 @@ export const createVectorStoreNode = (args: VectorStoreNodeConstructorArgs) =>
}
}
return this.prepareOutputData(resultData);
return await this.prepareOutputData(resultData);
}
throw new NodeOperationError(

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-nodes-langchain",
"version": "0.9.0",
"version": "0.10.0",
"description": "",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -148,7 +148,7 @@
"openai": "4.20.0",
"pdf-parse": "1.1.1",
"pg": "8.11.3",
"redis": "4.6.11",
"redis": "4.6.12",
"sqlite3": "5.1.6",
"temp": "0.9.4",
"typeorm": "0.3.17",

View file

@ -336,6 +336,11 @@ const config = (module.exports = {
},
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/return-await.md
*/
'@typescript-eslint/return-await': ['error', 'always'],
// ----------------------------------
// eslint-plugin-import
// ----------------------------------

View file

@ -44,7 +44,7 @@ if (process.env.NODEJS_PREFER_IPV4 === 'true') {
require('dns').setDefaultResultOrder('ipv4first');
}
require('@oclif/command')
.run()
.then(require('@oclif/command/flush'))
.catch(require('@oclif/errors/handle'));
(async () => {
const oclif = await import('@oclif/core');
await oclif.execute({});
})();

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "1.24.0",
"version": "1.25.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -29,12 +29,9 @@
"format": "prettier --write . --ignore-path ../../.prettierignore",
"lint": "eslint . --quiet",
"lintfix": "eslint . --fix",
"postpack": "rm -f oclif.manifest.json",
"prepack": "OCLIF_TS_NODE=0 oclif-dev manifest",
"start": "run-script-os",
"start:default": "cd bin && ./n8n",
"start:windows": "cd bin && n8n",
"swagger": "swagger-cli",
"test": "pnpm test:sqlite",
"test:sqlite": "N8N_LOG_LEVEL=silent DB_TYPE=sqlite jest",
"test:postgres": "N8N_LOG_LEVEL=silent DB_TYPE=postgresdb DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ jest --no-coverage",
@ -60,12 +57,10 @@
"bin",
"templates",
"dist",
"oclif.manifest.json",
"!dist/**/e2e.*"
],
"devDependencies": {
"@apidevtools/swagger-cli": "4.0.0",
"@oclif/dev-cli": "^1.22.2",
"@redocly/cli": "^1.6.0",
"@types/basic-auth": "^1.1.3",
"@types/bcryptjs": "^2.4.2",
"@types/compression": "1.0.1",
@ -84,7 +79,7 @@
"@types/shelljs": "^0.8.11",
"@types/sshpk": "^1.17.1",
"@types/superagent": "4.1.13",
"@types/swagger-ui-express": "^4.1.3",
"@types/swagger-ui-express": "^4.1.6",
"@types/syslog-client": "^1.1.2",
"@types/uuid": "^8.3.2",
"@types/validator": "^13.7.0",
@ -99,19 +94,17 @@
"dependencies": {
"@n8n/client-oauth2": "workspace:*",
"@n8n/localtunnel": "2.1.0",
"@n8n/n8n-nodes-langchain": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n_io/license-sdk": "2.7.2",
"@oclif/command": "1.8.18",
"@oclif/config": "1.18.17",
"@oclif/core": "1.16.6",
"@oclif/errors": "1.3.6",
"@n8n_io/license-sdk": "2.9.1",
"@oclif/core": "3.18.1",
"@rudderstack/rudder-sdk-node": "1.0.6",
"@sentry/integrations": "7.87.0",
"@sentry/node": "7.87.0",
"axios": "1.6.2",
"axios": "1.6.5",
"basic-auth": "2.0.1",
"bcryptjs": "2.4.3",
"bull": "4.10.2",
"bull": "4.12.1",
"cache-manager": "5.2.3",
"callsites": "3.1.0",
"change-case": "4.1.2",
@ -137,7 +130,7 @@
"handlebars": "4.7.7",
"infisical-node": "1.3.0",
"inquirer": "7.3.3",
"ioredis": "5.2.4",
"ioredis": "5.3.2",
"isbot": "3.6.13",
"json-diff": "1.0.6",
"jsonschema": "1.4.1",
@ -150,7 +143,6 @@
"n8n-core": "workspace:*",
"n8n-editor-ui": "workspace:*",
"n8n-nodes-base": "workspace:*",
"@n8n/n8n-nodes-langchain": "workspace:*",
"n8n-workflow": "workspace:*",
"nanoid": "3.3.6",
"nodemailer": "6.8.0",
@ -180,7 +172,7 @@
"sqlite3": "5.1.6",
"sse-channel": "4.0.0",
"sshpk": "1.17.0",
"swagger-ui-express": "4.5.0",
"swagger-ui-express": "5.0.0",
"syslog-client": "1.1.1",
"typedi": "0.10.0",
"typeorm": "0.3.17",

View file

@ -46,7 +46,7 @@ function bundleOpenApiSpecs(rootDir = ROOT_DIR, specFileName = SPEC_FILENAME) {
}, [])
.forEach((specPath) => {
const distSpecPath = path.resolve(rootDir, 'dist', specPath);
const command = `npm run swagger -- bundle src/${specPath} --type yaml --outfile ${distSpecPath}`;
const command = `pnpm openapi bundle src/${specPath} --output ${distSpecPath}`;
shell.exec(command, { silent: true });
});
}

View file

@ -208,7 +208,7 @@ export abstract class AbstractServer {
// TODO UM: check if this needs validation with user management.
this.app.delete(
`/${this.restEndpoint}/test-webhook/:id`,
send(async (req) => testWebhooks.cancelWebhook(req.params.id)),
send(async (req) => await testWebhooks.cancelWebhook(req.params.id)),
);
}

View file

@ -178,7 +178,7 @@ export class ActiveExecutions {
this.activeExecutions[executionId].workflowExecution!.cancel();
}
return this.getPostExecutePromise(executionId);
return await this.getPostExecutePromise(executionId);
}
/**
@ -197,7 +197,7 @@ export class ActiveExecutions {
this.activeExecutions[executionId].postExecutePromises.push(waitPromise);
return waitPromise.promise();
return await waitPromise.promise();
}
/**

View file

@ -30,7 +30,7 @@ export class ActiveWebhooks implements IWebhookManager {
) {}
async getWebhookMethods(path: string) {
return this.webhookService.getWebhookMethods(path);
return await this.webhookService.getWebhookMethods(path);
}
async findAccessControlOptions(path: string, httpMethod: IHttpRequestMethods) {
@ -84,7 +84,7 @@ export class ActiveWebhooks implements IWebhookManager {
const workflowData = await this.workflowRepository.findOne({
where: { id: webhook.workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
relations: ['shared', 'shared.user'],
});
if (workflowData === null) {
@ -120,7 +120,7 @@ export class ActiveWebhooks implements IWebhookManager {
throw new NotFoundError('Could not find node to process webhook.');
}
return new Promise((resolve, reject) => {
return await new Promise((resolve, reject) => {
const executionMode = 'webhook';
void WebhookHelpers.executeWebhook(
workflow,

View file

@ -35,7 +35,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { ActiveExecutions } from '@/ActiveExecutions';
import { ExecutionsService } from './executions/executions.service';
import { ExecutionService } from './executions/execution.service';
import {
STARTING_NODES,
WORKFLOW_REACTIVATE_INITIAL_TIMEOUT,
@ -47,7 +47,7 @@ import { ExternalHooks } from '@/ExternalHooks';
import { WebhookService } from './services/webhook.service';
import { Logger } from './Logger';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { OrchestrationService } from '@/services/orchestration.service';
import { ActivationErrorsService } from '@/ActivationErrors.service';
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
import { WorkflowStaticDataService } from '@/workflows/workflowStaticData.service';
@ -72,15 +72,15 @@ export class ActiveWorkflowRunner {
private readonly nodeTypes: NodeTypes,
private readonly webhookService: WebhookService,
private readonly workflowRepository: WorkflowRepository,
private readonly multiMainSetup: MultiMainSetup,
private readonly orchestrationService: OrchestrationService,
private readonly activationErrorsService: ActivationErrorsService,
private readonly executionService: ExecutionsService,
private readonly executionService: ExecutionService,
private readonly workflowStaticDataService: WorkflowStaticDataService,
private readonly activeWorkflowsService: ActiveWorkflowsService,
) {}
async init() {
await this.multiMainSetup.init();
await this.orchestrationService.init();
await this.addActiveWorkflows('init');
@ -89,7 +89,7 @@ export class ActiveWorkflowRunner {
}
async getAllWorkflowActivationErrors() {
return this.activationErrorsService.getAll();
return await this.activationErrorsService.getAll();
}
/**
@ -229,7 +229,7 @@ export class ActiveWorkflowRunner {
async clearWebhooks(workflowId: string) {
const workflowData = await this.workflowRepository.findOne({
where: { id: workflowId },
relations: ['shared', 'shared.user', 'shared.user.globalRole'],
relations: ['shared', 'shared.user'],
});
if (workflowData === null) {
@ -305,7 +305,7 @@ export class ActiveWorkflowRunner {
};
const workflowRunner = new WorkflowRunner();
return workflowRunner.run(runData, true, undefined, undefined, responsePromise);
return await workflowRunner.run(runData, true, undefined, undefined, responsePromise);
}
/**
@ -470,25 +470,23 @@ export class ActiveWorkflowRunner {
if (dbWorkflows.length === 0) return;
this.logger.info(' ================================');
this.logger.info(' Start Active Workflows:');
this.logger.info(' ================================');
if (this.orchestrationService.isLeader) {
this.logger.info(' ================================');
this.logger.info(' Start Active Workflows:');
this.logger.info(' ================================');
}
for (const dbWorkflow of dbWorkflows) {
this.logger.info(` - ${dbWorkflow.display()}`);
this.logger.debug(`Initializing active workflow ${dbWorkflow.display()} (startup)`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
try {
await this.add(dbWorkflow.id, activationMode, dbWorkflow);
const wasActivated = await this.add(dbWorkflow.id, activationMode, dbWorkflow);
this.logger.verbose(`Successfully started workflow ${dbWorkflow.display()}`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
this.logger.info(' => Started');
if (wasActivated) {
this.logger.verbose(`Successfully started workflow ${dbWorkflow.display()}`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
this.logger.info(' => Started');
}
} catch (error) {
ErrorReporter.error(error);
this.logger.info(
@ -571,16 +569,18 @@ export class ActiveWorkflowRunner {
* again, and the new leader should take over the triggers and pollers that stopped
* running when the former leader became unresponsive.
*/
if (this.multiMainSetup.isEnabled) {
if (this.orchestrationService.isMultiMainSetupEnabled) {
if (activationMode !== 'leadershipChange') {
shouldAddWebhooks = this.multiMainSetup.isLeader;
shouldAddTriggersAndPollers = this.multiMainSetup.isLeader;
shouldAddWebhooks = this.orchestrationService.isLeader;
shouldAddTriggersAndPollers = this.orchestrationService.isLeader;
} else {
shouldAddWebhooks = false;
shouldAddTriggersAndPollers = this.multiMainSetup.isLeader;
shouldAddTriggersAndPollers = this.orchestrationService.isLeader;
}
}
const shouldActivate = shouldAddWebhooks || shouldAddTriggersAndPollers;
try {
const dbWorkflow = existingWorkflow ?? (await this.workflowRepository.findById(workflowId));
@ -588,6 +588,14 @@ export class ActiveWorkflowRunner {
throw new WorkflowActivationError(`Failed to find workflow with ID "${workflowId}"`);
}
if (shouldActivate) {
this.logger.info(` - ${dbWorkflow.display()}`);
this.logger.debug(`Initializing active workflow ${dbWorkflow.display()} (startup)`, {
workflowName: dbWorkflow.name,
workflowId: dbWorkflow.id,
});
}
workflow = new Workflow({
id: dbWorkflow.id,
name: dbWorkflow.name,
@ -607,7 +615,7 @@ export class ActiveWorkflowRunner {
);
}
const sharing = dbWorkflow.shared.find((shared) => shared.role.name === 'owner');
const sharing = dbWorkflow.shared.find((shared) => shared.role === 'workflow:owner');
if (!sharing) {
throw new WorkflowActivationError(`Workflow ${dbWorkflow.display()} has no owner`);
@ -644,6 +652,8 @@ export class ActiveWorkflowRunner {
// If for example webhooks get created it sometimes has to save the
// id of them in the static data. So make sure that data gets persisted.
await this.workflowStaticDataService.saveStaticData(workflow);
return shouldActivate;
}
/**
@ -804,7 +814,7 @@ export class ActiveWorkflowRunner {
);
if (workflow.getTriggerNodes().length !== 0 || workflow.getPollNodes().length !== 0) {
this.logger.debug(`Adding triggers and pollers for workflow "${dbWorkflow.display()}"`);
this.logger.debug(`Adding triggers and pollers for workflow ${dbWorkflow.display()}`);
await this.activeWorkflows.add(
workflow.id,

View file

@ -121,7 +121,10 @@ export class CredentialsHelper extends ICredentialsHelper {
if (typeof credentialType.authenticate === 'function') {
// Special authentication function is defined
return credentialType.authenticate(credentials, requestOptions as IHttpRequestOptions);
return await credentialType.authenticate(
credentials,
requestOptions as IHttpRequestOptions,
);
}
if (typeof credentialType.authenticate === 'object') {
@ -783,15 +786,9 @@ export class CredentialsHelper extends ICredentialsHelper {
const credential = await this.sharedCredentialsRepository.findOne({
where: {
role: {
scope: 'credential',
name: 'owner',
},
role: 'credential:owner',
user: {
globalRole: {
scope: 'global',
name: 'owner',
},
role: 'global:owner',
},
credentials: {
id: nodeCredential.id,

View file

@ -54,7 +54,7 @@ if (!inTest) {
}
export async function transaction<T>(fn: (entityManager: EntityManager) => Promise<T>): Promise<T> {
return connection.transaction(fn);
return await connection.transaction(fn);
}
export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions {
@ -97,6 +97,16 @@ export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions {
}
}
export async function setSchema(conn: Connection) {
const schema = config.getEnv('database.postgresdb.schema');
const searchPath = ['public'];
if (schema !== 'public') {
await conn.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
searchPath.unshift(schema);
}
await conn.query(`SET search_path TO ${searchPath.join(',')};`);
}
export async function init(testConnectionOptions?: ConnectionOptions): Promise<void> {
if (connectionState.connected) return;
@ -130,13 +140,7 @@ export async function init(testConnectionOptions?: ConnectionOptions): Promise<v
await connection.initialize();
if (dbType === 'postgresdb') {
const schema = config.getEnv('database.postgresdb.schema');
const searchPath = ['public'];
if (schema !== 'public') {
await connection.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
searchPath.unshift(schema);
}
await connection.query(`SET search_path TO ${searchPath.join(',')};`);
await setSchema(connection);
}
connectionState.connected = true;

View file

@ -13,7 +13,7 @@ export class ExternalSecretsController {
@Get('/providers')
@RequireGlobalScope('externalSecretsProvider:list')
async getProviders() {
return this.secretsService.getProviders();
return await this.secretsService.getProviders();
}
@Get('/providers/:provider')

View file

@ -134,7 +134,7 @@ export class ExternalSecretsService {
}
const { settings } = providerAndSettings;
const newData = this.unredact(data, settings.settings);
return Container.get(ExternalSecretsManager).testProviderSettings(providerName, newData);
return await Container.get(ExternalSecretsManager).testProviderSettings(providerName, newData);
}
async updateProvider(providerName: string) {
@ -143,6 +143,6 @@ export class ExternalSecretsService {
if (!providerAndSettings) {
throw new ExternalSecretsProviderNotFoundError(providerName);
}
return Container.get(ExternalSecretsManager).updateProvider(providerName);
return await Container.get(ExternalSecretsManager).updateProvider(providerName);
}
}

View file

@ -16,7 +16,7 @@ import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks';
import { updateIntervalTime } from './externalSecretsHelper.ee';
import { ExternalSecretsProviders } from './ExternalSecretsProviders.ee';
import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { OrchestrationService } from '@/services/orchestration.service';
@Service()
export class ExternalSecretsManager {
@ -48,10 +48,13 @@ export class ExternalSecretsManager {
this.initialized = true;
resolve();
this.initializingPromise = undefined;
this.updateInterval = setInterval(async () => this.updateSecrets(), updateIntervalTime());
this.updateInterval = setInterval(
async () => await this.updateSecrets(),
updateIntervalTime(),
);
});
}
return this.initializingPromise;
return await this.initializingPromise;
}
}
@ -76,7 +79,7 @@ export class ExternalSecretsManager {
}
async broadcastReloadExternalSecretsProviders() {
await Container.get(SingleMainSetup).broadcastReloadExternalSecretsProviders();
await Container.get(OrchestrationService).publish('reloadExternalSecretsProviders');
}
private decryptSecretsSettings(value: string): ExternalSecretsSettings {
@ -107,8 +110,8 @@ export class ExternalSecretsManager {
}
const providers: Array<SecretsProvider | null> = (
await Promise.allSettled(
Object.entries(settings).map(async ([name, providerSettings]) =>
this.initProvider(name, providerSettings),
Object.entries(settings).map(
async ([name, providerSettings]) => await this.initProvider(name, providerSettings),
),
)
).map((i) => (i.status === 'rejected' ? null : i.value));

View file

@ -2,4 +2,4 @@ export const EXTERNAL_SECRETS_DB_KEY = 'feature.externalSecrets';
export const EXTERNAL_SECRETS_INITIAL_BACKOFF = 10 * 1000;
export const EXTERNAL_SECRETS_MAX_BACKOFF = 5 * 60 * 1000;
export const EXTERNAL_SECRETS_NAME_REGEX = /^[a-zA-Z0-9\_\/]+$/;
export const EXTERNAL_SECRETS_NAME_REGEX = /^[a-zA-Z0-9\-\_\/]+$/;

View file

@ -436,7 +436,7 @@ export class VaultProvider extends SecretsProvider {
await Promise.allSettled(
listResp.data.data.keys.map(async (key): Promise<[string, IDataObject] | null> => {
if (key.endsWith('/')) {
return this.getKVSecrets(mountPath, kvVersion, path + key);
return await this.getKVSecrets(mountPath, kvVersion, path + key);
}
let secretPath = mountPath;
if (kvVersion === '2') {

View file

@ -18,7 +18,7 @@ import type {
Workflow,
WorkflowExecuteMode,
ExecutionStatus,
IExecutionsSummary,
ExecutionSummary,
FeatureFlags,
INodeProperties,
IUserSettings,
@ -35,10 +35,9 @@ import type { ChildProcess } from 'child_process';
import type { DatabaseType } from '@db/types';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { Role } from '@db/entities/Role';
import type { SharedCredentials } from '@db/entities/SharedCredentials';
import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import type { GlobalRole, User } from '@db/entities/User';
import type { CredentialsRepository } from '@db/repositories/credentials.repository';
import type { SettingsRepository } from '@db/repositories/settings.repository';
import type { UserRepository } from '@db/repositories/user.repository';
@ -170,8 +169,7 @@ export interface IExecutionFlattedResponse extends IExecutionFlatted {
export interface IExecutionsListResponse {
count: number;
// results: IExecutionShortResponse[];
results: IExecutionsSummary[];
results: ExecutionSummary[];
estimated: boolean;
}
@ -192,12 +190,6 @@ export interface IExecutionsCurrentSummary {
status?: ExecutionStatus;
}
export interface IExecutionDeleteFilter {
deleteBefore?: Date;
filters?: IDataObject;
ids?: string[];
}
export interface IExecutingWorkflowData {
executionData: IWorkflowExecutionDataProcess;
process?: ChildProcess;
@ -667,6 +659,7 @@ export interface ILicensePostResponse extends ILicenseReadResponse {
export interface JwtToken {
token: string;
/** The amount of seconds after which the JWT will expire. **/
expiresIn: number;
}
@ -687,7 +680,7 @@ export interface PublicUser {
createdAt: Date;
isPending: boolean;
hasRecoveryCodesLeft: boolean;
globalRole?: Role;
role?: GlobalRole;
globalScopes?: Scope[];
signInType: AuthProviderType;
disabled: boolean;

View file

@ -22,11 +22,11 @@ import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { eventBus } from './eventbus';
import { EventsService } from '@/services/events.service';
import type { User } from '@db/entities/User';
import type { GlobalRole, User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants';
import { NodeTypes } from './NodeTypes';
import { NodeTypes } from '@/NodeTypes';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { RoleService } from './services/role.service';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core';
@ -36,14 +36,14 @@ function userToPayload(user: User): {
_email: string;
_firstName: string;
_lastName: string;
globalRole?: string;
globalRole: GlobalRole;
} {
return {
userId: user.id,
_email: user.email,
_firstName: user.firstName,
_lastName: user.lastName,
globalRole: user.globalRole?.name,
globalRole: user.role,
};
}
@ -52,15 +52,17 @@ export class InternalHooks {
constructor(
private telemetry: Telemetry,
private nodeTypes: NodeTypes,
private roleService: RoleService,
private sharedWorkflowRepository: SharedWorkflowRepository,
eventsService: EventsService,
private readonly instanceSettings: InstanceSettings,
) {
eventsService.on('telemetry.onFirstProductionWorkflowSuccess', async (metrics) =>
this.onFirstProductionWorkflowSuccess(metrics),
eventsService.on(
'telemetry.onFirstProductionWorkflowSuccess',
async (metrics) => await this.onFirstProductionWorkflowSuccess(metrics),
);
eventsService.on('telemetry.onFirstWorkflowDataLoad', async (metrics) =>
this.onFirstWorkflowDataLoad(metrics),
eventsService.on(
'telemetry.onFirstWorkflowDataLoad',
async (metrics) => await this.onFirstWorkflowDataLoad(metrics),
);
}
@ -88,7 +90,7 @@ export class InternalHooks {
license_tenant_id: diagnosticInfo.licenseTenantId,
};
return Promise.all([
return await Promise.all([
this.telemetry.identify(info),
this.telemetry.track('Instance started', {
...info,
@ -98,7 +100,7 @@ export class InternalHooks {
}
async onFrontendSettingsAPI(sessionId?: string): Promise<void> {
return this.telemetry.track('Session started', { session_id: sessionId });
return await this.telemetry.track('Session started', { session_id: sessionId });
}
async onPersonalizationSurveySubmitted(
@ -111,7 +113,7 @@ export class InternalHooks {
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey];
});
return this.telemetry.track(
return await this.telemetry.track(
'User responded to personalization questions',
personalizationSurveyData,
);
@ -164,9 +166,9 @@ export class InternalHooks {
let userRole: 'owner' | 'sharee' | undefined = undefined;
if (user.id && workflow.id) {
const role = await this.roleService.findRoleByUserAndWorkflow(user.id, workflow.id);
const role = await this.sharedWorkflowRepository.findSharingRole(user.id, workflow.id);
if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee';
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}
@ -369,9 +371,9 @@ export class InternalHooks {
let userRole: 'owner' | 'sharee' | undefined = undefined;
if (userId) {
const role = await this.roleService.findRoleByUserAndWorkflow(userId, workflow.id);
const role = await this.sharedWorkflowRepository.findSharingRole(userId, workflow.id);
if (role) {
userRole = role.name === 'owner' ? 'owner' : 'sharee';
userRole = role === 'workflow:owner' ? 'owner' : 'sharee';
}
}
@ -459,7 +461,7 @@ export class InternalHooks {
user_id_list: userList,
};
return this.telemetry.track('User updated workflow sharing', properties);
return await this.telemetry.track('User updated workflow sharing', properties);
}
async onN8nStop(): Promise<void> {
@ -469,7 +471,7 @@ export class InternalHooks {
}, 3000);
});
return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
return await Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
}
async onUserDeletion(userDeletionData: {
@ -554,42 +556,42 @@ export class InternalHooks {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved user', userRetrievedData);
return await this.telemetry.track('User retrieved user', userRetrievedData);
}
async onUserRetrievedAllUsers(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved all users', userRetrievedData);
return await this.telemetry.track('User retrieved all users', userRetrievedData);
}
async onUserRetrievedExecution(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved execution', userRetrievedData);
return await this.telemetry.track('User retrieved execution', userRetrievedData);
}
async onUserRetrievedAllExecutions(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved all executions', userRetrievedData);
return await this.telemetry.track('User retrieved all executions', userRetrievedData);
}
async onUserRetrievedWorkflow(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved workflow', userRetrievedData);
return await this.telemetry.track('User retrieved workflow', userRetrievedData);
}
async onUserRetrievedAllWorkflows(userRetrievedData: {
user_id: string;
public_api: boolean;
}): Promise<void> {
return this.telemetry.track('User retrieved all workflows', userRetrievedData);
return await this.telemetry.track('User retrieved all workflows', userRetrievedData);
}
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> {
@ -646,10 +648,15 @@ export class InternalHooks {
async onUserTransactionalEmail(userTransactionalEmailData: {
user_id: string;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
message_type:
| 'Reset password'
| 'New user invite'
| 'Resend invite'
| 'Workflow shared'
| 'Credentials shared';
public_api: boolean;
}): Promise<void> {
return this.telemetry.track(
return await this.telemetry.track(
'Instance sent transactional email to user',
userTransactionalEmailData,
);
@ -661,7 +668,7 @@ export class InternalHooks {
method: string;
api_version: string;
}): Promise<void> {
return this.telemetry.track('User invoked API', userInvokedApiData);
return await this.telemetry.track('User invoked API', userInvokedApiData);
}
async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void> {
@ -709,7 +716,7 @@ export class InternalHooks {
}
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
return this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
return await this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
}
async onUserSignup(
@ -735,7 +742,12 @@ export class InternalHooks {
async onEmailFailed(failedEmailData: {
user: User;
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
message_type:
| 'Reset password'
| 'New user invite'
| 'Resend invite'
| 'Workflow shared'
| 'Credentials shared';
public_api: boolean;
}): Promise<void> {
void Promise.all([
@ -963,7 +975,7 @@ export class InternalHooks {
users_synced: number;
error: string;
}): Promise<void> {
return this.telemetry.track('Ldap general sync finished', data);
return await this.telemetry.track('Ldap general sync finished', data);
}
async onUserUpdatedLdapSettings(data: {
@ -980,15 +992,15 @@ export class InternalHooks {
loginLabel: string;
loginEnabled: boolean;
}): Promise<void> {
return this.telemetry.track('Ldap general sync finished', data);
return await this.telemetry.track('Ldap general sync finished', data);
}
async onLdapLoginSyncFailed(data: { error: string }): Promise<void> {
return this.telemetry.track('Ldap login sync failed', data);
return await this.telemetry.track('Ldap login sync failed', data);
}
async userLoginFailedDueToLdapDisabled(data: { user_id: string }): Promise<void> {
return this.telemetry.track('User login failed since ldap disabled', data);
return await this.telemetry.track('User login failed since ldap disabled', data);
}
/*
@ -998,7 +1010,7 @@ export class InternalHooks {
user_id: string;
workflow_id: string;
}): Promise<void> {
return this.telemetry.track('Workflow first prod success', data);
return await this.telemetry.track('Workflow first prod success', data);
}
async onFirstWorkflowDataLoad(data: {
@ -1009,7 +1021,7 @@ export class InternalHooks {
credential_type?: string;
credential_id?: string;
}): Promise<void> {
return this.telemetry.track('Workflow first data fetched', data);
return await this.telemetry.track('Workflow first data fetched', data);
}
/**
@ -1023,11 +1035,11 @@ export class InternalHooks {
* Audit
*/
async onAuditGeneratedViaCli() {
return this.telemetry.track('Instance generated security audit via CLI command');
return await this.telemetry.track('Instance generated security audit via CLI command');
}
async onVariableCreated(createData: { variable_type: string }): Promise<void> {
return this.telemetry.track('User created variable', createData);
return await this.telemetry.track('User created variable', createData);
}
async onSourceControlSettingsUpdated(data: {
@ -1036,7 +1048,7 @@ export class InternalHooks {
repo_type: 'github' | 'gitlab' | 'other';
connected: boolean;
}): Promise<void> {
return this.telemetry.track('User updated source control settings', data);
return await this.telemetry.track('User updated source control settings', data);
}
async onSourceControlUserStartedPullUI(data: {
@ -1044,11 +1056,11 @@ export class InternalHooks {
workflow_conflicts: number;
cred_conflicts: number;
}): Promise<void> {
return this.telemetry.track('User started pull via UI', data);
return await this.telemetry.track('User started pull via UI', data);
}
async onSourceControlUserFinishedPullUI(data: { workflow_updates: number }): Promise<void> {
return this.telemetry.track('User finished pull via UI', {
return await this.telemetry.track('User finished pull via UI', {
workflow_updates: data.workflow_updates,
});
}
@ -1057,7 +1069,7 @@ export class InternalHooks {
workflow_updates: number;
forced: boolean;
}): Promise<void> {
return this.telemetry.track('User pulled via API', data);
return await this.telemetry.track('User pulled via API', data);
}
async onSourceControlUserStartedPushUI(data: {
@ -1067,7 +1079,7 @@ export class InternalHooks {
creds_eligible_with_conflicts: number;
variables_eligible: number;
}): Promise<void> {
return this.telemetry.track('User started push via UI', data);
return await this.telemetry.track('User started push via UI', data);
}
async onSourceControlUserFinishedPushUI(data: {
@ -1076,7 +1088,7 @@ export class InternalHooks {
creds_pushed: number;
variables_pushed: number;
}): Promise<void> {
return this.telemetry.track('User finished push via UI', data);
return await this.telemetry.track('User finished push via UI', data);
}
async onExternalSecretsProviderSettingsSaved(saveData: {
@ -1086,6 +1098,6 @@ export class InternalHooks {
is_new: boolean;
error_message?: string | undefined;
}): Promise<void> {
return this.telemetry.track('User updated external secrets settings', saveData);
return await this.telemetry.track('User updated external secrets settings', saveData);
}
}

View file

@ -5,7 +5,6 @@ import { Container } from 'typedi';
import { validate } from 'jsonschema';
import * as Db from '@/Db';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import { User } from '@db/entities/User';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
@ -18,7 +17,6 @@ import {
} from './constants';
import type { ConnectionSecurity, LdapConfig } from './types';
import { License } from '@/License';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { AuthProviderSyncHistoryRepository } from '@db/repositories/authProviderSyncHistory.repository';
import { AuthIdentityRepository } from '@db/repositories/authIdentity.repository';
@ -47,13 +45,6 @@ export const randomPassword = (): string => {
return Math.random().toString(36).slice(-8);
};
/**
* Return the user role to be assigned to LDAP users
*/
export const getLdapUserRole = async (): Promise<Role> => {
return Container.get(RoleService).findGlobalMemberRole();
};
/**
* Validate the structure of the LDAP configuration schema
*/
@ -101,8 +92,8 @@ export const escapeFilter = (filter: string): string => {
export const getAuthIdentityByLdapId = async (
idAttributeValue: string,
): Promise<AuthIdentity | null> => {
return Container.get(AuthIdentityRepository).findOne({
relations: ['user', 'user.globalRole'],
return await Container.get(AuthIdentityRepository).findOne({
relations: ['user'],
where: {
providerId: idAttributeValue,
providerType: 'ldap',
@ -111,9 +102,8 @@ export const getAuthIdentityByLdapId = async (
};
export const getUserByEmail = async (email: string): Promise<User | null> => {
return Container.get(UserRepository).findOne({
return await Container.get(UserRepository).findOne({
where: { email },
relations: ['globalRole'],
});
};
@ -164,13 +154,13 @@ export const getLdapUsers = async (): Promise<User[]> => {
export const mapLdapUserToDbUser = (
ldapUser: LdapUser,
ldapConfig: LdapConfig,
role?: Role,
toCreate = false,
): [string, User] => {
const user = new User();
const [ldapId, data] = mapLdapAttributesToUser(ldapUser, ldapConfig);
Object.assign(user, data);
if (role) {
user.globalRole = role;
if (toCreate) {
user.role = 'global:member';
user.password = randomPassword();
user.disabled = false;
} else {
@ -190,10 +180,10 @@ export const processUsers = async (
toDisableUsers: string[],
): Promise<void> => {
await Db.transaction(async (transactionManager) => {
return Promise.all([
return await Promise.all([
...toCreateUsers.map(async ([ldapId, user]) => {
const authIdentity = AuthIdentity.create(await transactionManager.save(user), ldapId);
return transactionManager.save(authIdentity);
return await transactionManager.save(authIdentity);
}),
...toUpdateUsers.map(async ([ldapId, user]) => {
const authIdentity = await transactionManager.findOneBy(AuthIdentity, {
@ -240,7 +230,7 @@ export const getLdapSynchronizations = async (
perPage: number,
): Promise<AuthProviderSyncHistory[]> => {
const _page = Math.abs(page);
return Container.get(AuthProviderSyncHistoryRepository).find({
return await Container.get(AuthProviderSyncHistoryRepository).find({
where: { providerType: 'ldap' },
order: { id: 'DESC' },
take: perPage,
@ -267,13 +257,13 @@ export const getMappingAttributes = (ldapConfig: LdapConfig): string[] => {
};
export const createLdapAuthIdentity = async (user: User, ldapId: string) => {
return Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId));
return await Container.get(AuthIdentityRepository).save(AuthIdentity.create(user, ldapId));
};
export const createLdapUserOnLocalDb = async (role: Role, data: Partial<User>, ldapId: string) => {
export const createLdapUserOnLocalDb = async (data: Partial<User>, ldapId: string) => {
const user = await Container.get(UserRepository).save({
password: randomPassword(),
globalRole: role,
role: 'global:member',
...data,
});
await createLdapAuthIdentity(user, ldapId);
@ -288,5 +278,5 @@ export const updateLdapUserOnLocalDb = async (identity: AuthIdentity, data: Part
};
export const deleteAllLdapIdentities = async () => {
return Container.get(AuthIdentityRepository).delete({ providerType: 'ldap' });
return await Container.get(AuthIdentityRepository).delete({ providerType: 'ldap' });
};

View file

@ -19,7 +19,7 @@ export class LdapController {
@Get('/config')
@RequireGlobalScope('ldap:manage')
async getConfig() {
return this.ldapService.loadConfig();
return await this.ldapService.loadConfig();
}
@Post('/test-connection')
@ -55,7 +55,7 @@ export class LdapController {
@RequireGlobalScope('ldap:sync')
async getLdapSync(req: LdapConfiguration.GetSync) {
const { page = '0', perPage = '20' } = req.query;
return getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
return await getLdapSynchronizations(parseInt(page, 10), parseInt(perPage, 10));
}
@Post('/sync')

View file

@ -7,7 +7,6 @@ import { ApplicationError, jsonParse } from 'n8n-workflow';
import { Cipher } from 'n8n-core';
import config from '@/config';
import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
import { SettingsRepository } from '@db/repositories/settings.repository';
@ -30,7 +29,6 @@ import {
escapeFilter,
formatUrl,
getLdapIds,
getLdapUserRole,
getLdapUsers,
getMappingAttributes,
mapLdapUserToDbUser,
@ -346,12 +344,9 @@ export class LdapService {
const localAdUsers = await getLdapIds();
const role = await getLdapUserRole();
const { usersToCreate, usersToUpdate, usersToDisable } = this.getUsersToProcess(
adUsers,
localAdUsers,
role,
);
this.logger.debug('LDAP - Users processed', {
@ -407,14 +402,13 @@ export class LdapService {
private getUsersToProcess(
adUsers: LdapUser[],
localAdUsers: string[],
role: Role,
): {
usersToCreate: Array<[string, User]>;
usersToUpdate: Array<[string, User]>;
usersToDisable: string[];
} {
return {
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers, role),
usersToCreate: this.getUsersToCreate(adUsers, localAdUsers),
usersToUpdate: this.getUsersToUpdate(adUsers, localAdUsers),
usersToDisable: this.getUsersToDisable(adUsers, localAdUsers),
};
@ -424,11 +418,10 @@ export class LdapService {
private getUsersToCreate(
remoteAdUsers: LdapUser[],
localLdapIds: string[],
role: Role,
): Array<[string, User]> {
return remoteAdUsers
.filter((adUser) => !localLdapIds.includes(adUser[this.config.ldapIdAttribute] as string))
.map((adUser) => mapLdapUserToDbUser(adUser, this.config, role));
.map((adUser) => mapLdapUserToDbUser(adUser, this.config, true));
}
/** Get users in LDAP that are already in the database */

View file

@ -12,12 +12,12 @@ import {
UNLIMITED_LICENSE_QUOTA,
} from './constants';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { BooleanLicenseFeature, N8nInstanceType, NumericLicenseFeature } from './Interfaces';
import type { RedisServicePubSubPublisher } from './services/redis/RedisServicePubSubPublisher';
import { RedisService } from './services/redis.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { OrchestrationService } from '@/services/orchestration.service';
import { OnShutdown } from '@/decorators/OnShutdown';
import { UsageMetricsService } from './services/usageMetrics.service';
type FeatureReturnType = Partial<
{
@ -36,9 +36,9 @@ export class License {
constructor(
private readonly logger: Logger,
private readonly instanceSettings: InstanceSettings,
private readonly multiMainSetup: MultiMainSetup,
private readonly orchestrationService: OrchestrationService,
private readonly settingsRepository: SettingsRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly usageMetricsService: UsageMetricsService,
) {}
async init(instanceType: N8nInstanceType = 'main') {
@ -51,21 +51,19 @@ export class License {
return;
}
await this.multiMainSetup.init();
const isMainInstance = instanceType === 'main';
const server = config.getEnv('license.serverUrl');
const autoRenewEnabled = isMainInstance && config.getEnv('license.autoRenewEnabled');
const offlineMode = !isMainInstance;
const autoRenewOffset = config.getEnv('license.autoRenewOffset');
const saveCertStr = isMainInstance
? async (value: TLicenseBlock) => this.saveCertStr(value)
? async (value: TLicenseBlock) => await this.saveCertStr(value)
: async () => {};
const onFeatureChange = isMainInstance
? async (features: TFeatures) => this.onFeatureChange(features)
? async (features: TFeatures) => await this.onFeatureChange(features)
: async () => {};
const collectUsageMetrics = isMainInstance
? async () => this.collectUsageMetrics()
? async () => await this.usageMetricsService.collectUsageMetrics()
: async () => [];
try {
@ -78,7 +76,7 @@ export class License {
autoRenewOffset,
offlineMode,
logger: this.logger,
loadCertStr: async () => this.loadCertStr(),
loadCertStr: async () => await this.loadCertStr(),
saveCertStr,
deviceFingerprint: () => this.instanceSettings.instanceId,
collectUsageMetrics,
@ -93,15 +91,6 @@ export class License {
}
}
async collectUsageMetrics() {
return [
{
name: 'activeWorkflows',
value: await this.workflowRepository.count({ where: { active: true } }),
},
];
}
async loadCertStr(): Promise<TLicenseBlock> {
// if we have an ephemeral license, we don't want to load it from the database
const ephemeralLicense = config.get('license.cert');
@ -123,16 +112,19 @@ export class License {
| boolean
| undefined;
this.multiMainSetup.setLicensed(isMultiMainLicensed ?? false);
this.orchestrationService.setMultiMainSetupLicensed(isMultiMainLicensed ?? false);
if (this.multiMainSetup.isEnabled && this.multiMainSetup.isFollower) {
if (
this.orchestrationService.isMultiMainSetupEnabled &&
this.orchestrationService.isFollower
) {
this.logger.debug(
'[Multi-main setup] Instance is follower, skipping sending of "reloadLicense" command...',
);
return;
}
if (this.multiMainSetup.isEnabled && !isMultiMainLicensed) {
if (this.orchestrationService.isMultiMainSetupEnabled && !isMultiMainLicensed) {
this.logger.debug(
'[Multi-main setup] License changed with no support for multi-main setup - no new followers will be allowed to init. To restore multi-main setup, please upgrade to a license that supporst this feature.',
);

View file

@ -182,7 +182,7 @@ export class LoadNodesAndCredentials {
'node_modules',
packageName,
);
return this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
return await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
}
async unloadPackage(packageName: string) {

View file

@ -8,7 +8,7 @@ export const isMfaFeatureEnabled = () => config.get(MFA_FEATURE_ENABLED);
const isMfaFeatureDisabled = () => !isMfaFeatureEnabled();
const getUsersWithMfaEnabled = async () =>
Container.get(UserRepository).count({ where: { mfaEnabled: true } });
await Container.get(UserRepository).count({ where: { mfaEnabled: true } });
export const handleMfaDisable = async () => {
if (isMfaFeatureDisabled()) {

View file

@ -25,7 +25,7 @@ export class MfaService {
secret,
recoveryCodes,
);
return this.userRepository.update(userId, {
return await this.userRepository.update(userId, {
mfaSecret: encryptedSecret,
mfaRecoveryCodes: encryptedRecoveryCodes,
});

View file

@ -98,7 +98,6 @@ async function createApiRouter(
const apiKey = req.headers[schema.name.toLowerCase()] as string;
const user = await Container.get(UserRepository).findOne({
where: { apiKey },
relations: ['globalRole'],
});
if (!user) return false;
@ -144,7 +143,7 @@ export const loadPublicApiVersions = async (
const apiRouters = await Promise.all(
versions.map(async (version) => {
const openApiPath = path.join(__dirname, version, 'openapi.yml');
return createApiRouter(version, openApiPath, __dirname, publicApiEndpoint);
return await createApiRouter(version, openApiPath, __dirname, publicApiEndpoint);
}),
);

View file

@ -3,8 +3,6 @@ import type { IDataObject, ExecutionStatus } from 'n8n-workflow';
import type { User } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { UserManagementMailer } from '@/UserManagement/email';
@ -25,7 +23,6 @@ export type AuthenticatedRequest<
RequestQuery = {},
> = express.Request<RouteParams, ResponseBody, RequestBody, RequestQuery> & {
user: User;
globalMemberRole?: Role;
mailer?: UserManagementMailer;
};

View file

@ -5,7 +5,7 @@ import Container from 'typedi';
export = {
generateAudit: [
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: AuditRequest.Generate, res: Response): Promise<Response> => {
try {
const { SecurityAuditService } = await import('@/security-audit/SecurityAudit.service');

View file

@ -23,7 +23,7 @@ import { Container } from 'typedi';
export = {
createCredential: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCredentialType,
validCredentialsProperties,
async (
@ -47,7 +47,7 @@ export = {
},
],
deleteCredential: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (
req: CredentialRequest.Delete,
res: express.Response,
@ -55,13 +55,10 @@ export = {
const { id: credentialId } = req.params;
let credential: CredentialsEntity | undefined;
if (!['owner', 'admin'].includes(req.user.globalRole.name)) {
const shared = await getSharedCredentials(req.user.id, credentialId, [
'credentials',
'role',
]);
if (!['global:owner', 'global:admin'].includes(req.user.role)) {
const shared = await getSharedCredentials(req.user.id, credentialId);
if (shared?.role.name === 'owner') {
if (shared?.role === 'credential:owner') {
credential = shared.credentials;
}
} else {
@ -78,7 +75,7 @@ export = {
],
getCredentialType: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: CredentialTypeRequest.Get, res: express.Response): Promise<express.Response> => {
const { credentialTypeName } = req.params;

View file

@ -1,5 +1,10 @@
import { Credentials } from 'n8n-core';
import type { IDataObject, INodeProperties, INodePropertyOptions } from 'n8n-workflow';
import type {
DisplayCondition,
IDataObject,
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
import * as Db from '@/Db';
import type { ICredentialsDb } from '@/Interfaces';
import { CredentialsEntity } from '@db/entities/CredentialsEntity';
@ -9,25 +14,23 @@ import { ExternalHooks } from '@/ExternalHooks';
import type { IDependency, IJsonSchema } from '../../../types';
import type { CredentialRequest } from '@/requests';
import { Container } from 'typedi';
import { RoleService } from '@/services/role.service';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
return Container.get(CredentialsRepository).findOneBy({ id: credentialId });
return await Container.get(CredentialsRepository).findOneBy({ id: credentialId });
}
export async function getSharedCredentials(
userId: string,
credentialId: string,
relations?: string[],
): Promise<SharedCredentials | null> {
return Container.get(SharedCredentialsRepository).findOne({
return await Container.get(SharedCredentialsRepository).findOne({
where: {
userId,
credentialsId: credentialId,
},
relations,
relations: ['credentials'],
});
}
@ -60,11 +63,9 @@ export async function saveCredential(
user: User,
encryptedData: ICredentialsDb,
): Promise<CredentialsEntity> {
const role = await Container.get(RoleService).findCredentialOwnerRole();
await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
return Db.transaction(async (transactionManager) => {
return await Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
savedCredential.data = credential.data;
@ -72,7 +73,7 @@ export async function saveCredential(
const newSharedCredential = new SharedCredentials();
Object.assign(newSharedCredential, {
role,
role: 'credential:owner',
user,
credentials: savedCredential,
});
@ -85,7 +86,7 @@ export async function saveCredential(
export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> {
await Container.get(ExternalHooks).run('credentials.delete', [credentials.id]);
return Container.get(CredentialsRepository).remove(credentials);
return await Container.get(CredentialsRepository).remove(credentials);
}
export async function encryptCredential(credential: CredentialsEntity): Promise<ICredentialsDb> {
@ -186,7 +187,7 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject {
if (property.displayOptions?.show) {
const dependantName = Object.keys(property.displayOptions?.show)[0] || '';
const displayOptionsValues = property.displayOptions.show[dependantName];
let dependantValue: string | number | boolean = '';
let dependantValue: DisplayCondition | string | number | boolean = '';
if (displayOptionsValues && Array.isArray(displayOptionsValues) && displayOptionsValues[0]) {
dependantValue = displayOptionsValues[0];
@ -197,12 +198,75 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject {
}
if (!resolveProperties.includes(dependantName)) {
let conditionalValue;
if (typeof dependantValue === 'object' && dependantValue._cnd) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const [key, targetValue] = Object.entries(dependantValue._cnd)[0];
if (key === 'eq') {
conditionalValue = {
const: [targetValue],
};
} else if (key === 'not') {
conditionalValue = {
not: {
const: [targetValue],
},
};
} else if (key === 'gt') {
conditionalValue = {
type: 'number',
exclusiveMinimum: [targetValue],
};
} else if (key === 'gte') {
conditionalValue = {
type: 'number',
minimum: [targetValue],
};
} else if (key === 'lt') {
conditionalValue = {
type: 'number',
exclusiveMaximum: [targetValue],
};
} else if (key === 'lte') {
conditionalValue = {
type: 'number',
maximum: [targetValue],
};
} else if (key === 'startsWith') {
conditionalValue = {
type: 'string',
pattern: `^${targetValue}`,
};
} else if (key === 'endsWith') {
conditionalValue = {
type: 'string',
pattern: `${targetValue}$`,
};
} else if (key === 'includes') {
conditionalValue = {
type: 'string',
pattern: `${targetValue}`,
};
} else if (key === 'regex') {
conditionalValue = {
type: 'string',
pattern: `${targetValue}`,
};
} else {
conditionalValue = {
enum: [dependantValue],
};
}
} else {
conditionalValue = {
enum: [dependantValue],
};
}
propertyRequiredDependencies[dependantName] = {
if: {
properties: {
[dependantName]: {
enum: [dependantValue],
},
[dependantName]: conditionalValue,
},
},
then: {

View file

@ -2,7 +2,6 @@ import type express from 'express';
import { Container } from 'typedi';
import { replaceCircularReferences } from 'n8n-workflow';
import { getExecutions, getExecutionInWorkflows, getExecutionsCount } from './executions.service';
import { ActiveExecutions } from '@/ActiveExecutions';
import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
import type { ExecutionRequest } from '../../../types';
@ -13,7 +12,7 @@ import { ExecutionRepository } from '@db/repositories/execution.repository';
export = {
deleteExecution: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: ExecutionRequest.Delete, res: express.Response): Promise<express.Response> => {
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
@ -26,7 +25,9 @@ export = {
const { id } = req.params;
// look for the execution on the workflow the user owns
const execution = await getExecutionInWorkflows(id, sharedWorkflowsIds, false);
const execution = await Container.get(
ExecutionRepository,
).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, false);
if (!execution) {
return res.status(404).json({ message: 'Not Found' });
@ -43,7 +44,7 @@ export = {
},
],
getExecution: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: ExecutionRequest.Get, res: express.Response): Promise<express.Response> => {
const sharedWorkflowsIds = await getSharedWorkflowIds(req.user);
@ -57,7 +58,9 @@ export = {
const { includeData = false } = req.query;
// look for the execution on the workflow the user owns
const execution = await getExecutionInWorkflows(id, sharedWorkflowsIds, includeData);
const execution = await Container.get(
ExecutionRepository,
).getExecutionInWorkflowsForPublicApi(id, sharedWorkflowsIds, includeData);
if (!execution) {
return res.status(404).json({ message: 'Not Found' });
@ -72,7 +75,7 @@ export = {
},
],
getExecutions: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCursor,
async (req: ExecutionRequest.GetAll, res: express.Response): Promise<express.Response> => {
const {
@ -105,13 +108,15 @@ export = {
excludedExecutionsIds: runningExecutionsIds,
};
const executions = await getExecutions(filters);
const executions =
await Container.get(ExecutionRepository).getExecutionsForPublicApi(filters);
const newLastId = !executions.length ? '0' : executions.slice(-1)[0].id;
filters.lastId = newLastId;
const count = await getExecutionsCount(filters);
const count =
await Container.get(ExecutionRepository).getExecutionsCountForPublicApi(filters);
void Container.get(InternalHooks).onUserRetrievedAllExecutions({
user_id: req.user.id,

View file

@ -1,110 +0,0 @@
import type { FindOptionsWhere } from 'typeorm';
import { In, Not, Raw, LessThan } from 'typeorm';
import { Container } from 'typedi';
import type { ExecutionStatus } from 'n8n-workflow';
import type { IExecutionBase, IExecutionFlattedDb } from '@/Interfaces';
import { ExecutionRepository } from '@db/repositories/execution.repository';
function getStatusCondition(status: ExecutionStatus) {
const condition: Pick<FindOptionsWhere<IExecutionFlattedDb>, 'status'> = {};
if (status === 'success') {
condition.status = 'success';
} else if (status === 'waiting') {
condition.status = 'waiting';
} else if (status === 'error') {
condition.status = In(['error', 'crashed', 'failed']);
}
return condition;
}
export async function getExecutions(params: {
limit: number;
includeData?: boolean;
lastId?: string;
workflowIds?: string[];
status?: ExecutionStatus;
excludedExecutionsIds?: string[];
}): Promise<IExecutionBase[]> {
let where: FindOptionsWhere<IExecutionFlattedDb> = {};
if (params.lastId && params.excludedExecutionsIds?.length) {
where.id = Raw((id) => `${id} < :lastId AND ${id} NOT IN (:...excludedExecutionsIds)`, {
lastId: params.lastId,
excludedExecutionsIds: params.excludedExecutionsIds,
});
} else if (params.lastId) {
where.id = LessThan(params.lastId);
} else if (params.excludedExecutionsIds?.length) {
where.id = Not(In(params.excludedExecutionsIds));
}
if (params.status) {
where = { ...where, ...getStatusCondition(params.status) };
}
if (params.workflowIds) {
where = { ...where, workflowId: In(params.workflowIds) };
}
return Container.get(ExecutionRepository).findMultipleExecutions(
{
select: [
'id',
'mode',
'retryOf',
'retrySuccessId',
'startedAt',
'stoppedAt',
'workflowId',
'waitTill',
'finished',
],
where,
order: { id: 'DESC' },
take: params.limit,
relations: ['executionData'],
},
{
includeData: params.includeData,
unflattenData: true,
},
);
}
export async function getExecutionsCount(data: {
limit: number;
lastId?: string;
workflowIds?: string[];
status?: ExecutionStatus;
excludedWorkflowIds?: string[];
}): Promise<number> {
// TODO: Consider moving this to the repository as well
const executions = await Container.get(ExecutionRepository).count({
where: {
...(data.lastId && { id: LessThan(data.lastId) }),
...(data.status && { ...getStatusCondition(data.status) }),
...(data.workflowIds && { workflowId: In(data.workflowIds) }),
...(data.excludedWorkflowIds && { workflowId: Not(In(data.excludedWorkflowIds)) }),
},
take: data.limit,
});
return executions;
}
export async function getExecutionInWorkflows(
id: string,
workflowIds: string[],
includeData?: boolean,
): Promise<IExecutionBase | undefined> {
return Container.get(ExecutionRepository).findSingleExecution(id, {
where: {
workflowId: In(workflowIds),
},
includeData,
unflattenData: true,
});
}

View file

@ -14,7 +14,7 @@ import { InternalHooks } from '@/InternalHooks';
export = {
pull: [
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (
req: PublicSourceControlRequest.Pull,
res: express.Response,

View file

@ -36,5 +36,7 @@ properties:
description: Last time the user was updated.
format: date-time
readOnly: true
globalRole:
$ref: './role.yml'
role:
type: string
example: owner
readOnly: true

View file

@ -15,7 +15,7 @@ import { InternalHooks } from '@/InternalHooks';
export = {
getUser: [
validLicenseWithUserQuota,
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => {
const { includeRole = false } = req.query;
const { id } = req.params;
@ -41,7 +41,7 @@ export = {
getUsers: [
validLicenseWithUserQuota,
validCursor,
authorize(['owner', 'admin']),
authorize(['global:owner', 'global:admin']),
async (req: UserRequest.Get, res: express.Response) => {
const { offset = 0, limit = 100, includeRole = false } = req.query;

View file

@ -4,24 +4,21 @@ import type { User } from '@db/entities/User';
import pick from 'lodash/pick';
import { validate as uuidValidate } from 'uuid';
export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
return {
user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt', 'isPending'],
role: ['id', 'name', 'scope', 'createdAt', 'updatedAt'],
}[table];
};
export async function getUser(data: {
withIdentifier: string;
includeRole?: boolean;
}): Promise<User | null> {
return Container.get(UserRepository).findOne({
where: {
...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }),
...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }),
},
relations: data?.includeRole ? ['globalRole'] : undefined,
});
return await Container.get(UserRepository)
.findOne({
where: {
...(uuidValidate(data.withIdentifier) && { id: data.withIdentifier }),
...(!uuidValidate(data.withIdentifier) && { email: data.withIdentifier }),
},
})
.then((user) => {
if (user && !data?.includeRole) delete (user as Partial<User>).role;
return user;
});
}
export async function getAllUsersAndCount(data: {
@ -31,19 +28,29 @@ export async function getAllUsersAndCount(data: {
}): Promise<[User[], number]> {
const users = await Container.get(UserRepository).find({
where: {},
relations: data?.includeRole ? ['globalRole'] : undefined,
skip: data.offset,
take: data.limit,
});
if (!data?.includeRole) {
users.forEach((user) => {
delete (user as Partial<User>).role;
});
}
const count = await Container.get(UserRepository).count();
return [users, count];
}
const userProperties = [
'id',
'email',
'firstName',
'lastName',
'createdAt',
'updatedAt',
'isPending',
];
function pickUserSelectableProperties(user: User, options?: { includeRole: boolean }) {
return pick(
user,
getSelectableProperties('user').concat(options?.includeRole ? ['globalRole'] : []),
);
return pick(user, userProperties.concat(options?.includeRole ? ['role'] : []));
}
export function clean(user: User, options?: { includeRole: boolean }): Partial<User>;

View file

@ -23,7 +23,6 @@ import {
} from './workflows.service';
import { WorkflowService } from '@/workflows/workflow.service';
import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service';
import { WorkflowHistoryService } from '@/workflows/workflowHistory/workflowHistory.service.ee';
import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflow.repository';
import { TagRepository } from '@/databases/repositories/tag.repository';
@ -31,7 +30,7 @@ import { WorkflowRepository } from '@/databases/repositories/workflow.repository
export = {
createWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Create, res: express.Response): Promise<express.Response> => {
const workflow = req.body;
@ -42,9 +41,7 @@ export = {
addNodeIds(workflow);
const role = await Container.get(RoleService).findWorkflowOwnerRole();
const createdWorkflow = await createWorkflow(workflow, req.user, role);
const createdWorkflow = await createWorkflow(workflow, req.user, 'workflow:owner');
await Container.get(WorkflowHistoryService).saveVersion(
req.user,
@ -59,7 +56,7 @@ export = {
},
],
deleteWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id: workflowId } = req.params;
@ -74,7 +71,7 @@ export = {
},
],
getWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Get, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
@ -95,7 +92,7 @@ export = {
},
],
getWorkflows: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
validCursor,
async (req: WorkflowRequest.GetAll, res: express.Response): Promise<express.Response> => {
const { offset = 0, limit = 100, active = undefined, tags = undefined } = req.query;
@ -104,7 +101,7 @@ export = {
...(active !== undefined && { active }),
};
if (['owner', 'admin'].includes(req.user.globalRole.name)) {
if (['global:owner', 'global:admin'].includes(req.user.role)) {
if (tags) {
const workflowIds = await Container.get(TagRepository).getWorkflowIdsViaTags(
parseTagNames(tags),
@ -159,7 +156,7 @@ export = {
},
],
updateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Update, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
const updateData = new WorkflowEntity();
@ -221,7 +218,7 @@ export = {
},
],
activateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params;
@ -255,7 +252,7 @@ export = {
},
],
deactivateWorkflow: [
authorize(['owner', 'admin', 'member']),
authorize(['global:owner', 'global:admin', 'global:member']),
async (req: WorkflowRequest.Activate, res: express.Response): Promise<express.Response> => {
const { id } = req.params;

View file

@ -1,10 +1,9 @@
import { Container } from 'typedi';
import * as Db from '@/Db';
import type { User } from '@db/entities/User';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import type { Role } from '@db/entities/Role';
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import config from '@/config';
import Container from 'typedi';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -13,7 +12,7 @@ function insertIf(condition: boolean, elements: string[]): string[] {
}
export async function getSharedWorkflowIds(user: User): Promise<string[]> {
const where = ['owner', 'admin'].includes(user.globalRole.name) ? {} : { userId: user.id };
const where = ['global:owner', 'global:admin'].includes(user.role) ? {} : { userId: user.id };
const sharedWorkflows = await Container.get(SharedWorkflowRepository).find({
where,
select: ['workflowId'],
@ -25,9 +24,9 @@ export async function getSharedWorkflow(
user: User,
workflowId?: string | undefined,
): Promise<SharedWorkflow | null> {
return Container.get(SharedWorkflowRepository).findOne({
return await Container.get(SharedWorkflowRepository).findOne({
where: {
...(!['owner', 'admin'].includes(user.globalRole.name) && { userId: user.id }),
...(!['global:owner', 'global:admin'].includes(user.role) && { userId: user.id }),
...(workflowId && { workflowId }),
},
relations: [...insertIf(!config.getEnv('workflowTagsDisabled'), ['workflow.tags']), 'workflow'],
@ -35,7 +34,7 @@ export async function getSharedWorkflow(
}
export async function getWorkflowById(id: string): Promise<WorkflowEntity | null> {
return Container.get(WorkflowRepository).findOne({
return await Container.get(WorkflowRepository).findOne({
where: { id },
});
}
@ -43,9 +42,9 @@ export async function getWorkflowById(id: string): Promise<WorkflowEntity | null
export async function createWorkflow(
workflow: WorkflowEntity,
user: User,
role: Role,
role: WorkflowSharingRole,
): Promise<WorkflowEntity> {
return Db.transaction(async (transactionManager) => {
return await Db.transaction(async (transactionManager) => {
const newWorkflow = new WorkflowEntity();
Object.assign(newWorkflow, workflow);
const savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
@ -70,18 +69,18 @@ export async function setWorkflowAsActive(workflow: WorkflowEntity) {
}
export async function setWorkflowAsInactive(workflow: WorkflowEntity) {
return Container.get(WorkflowRepository).update(workflow.id, {
return await Container.get(WorkflowRepository).update(workflow.id, {
active: false,
updatedAt: new Date(),
});
}
export async function deleteWorkflow(workflow: WorkflowEntity): Promise<WorkflowEntity> {
return Container.get(WorkflowRepository).remove(workflow);
return await Container.get(WorkflowRepository).remove(workflow);
}
export async function updateWorkflow(workflowId: string, updateData: WorkflowEntity) {
return Container.get(WorkflowRepository).update(workflowId, updateData);
return await Container.get(WorkflowRepository).update(workflowId, updateData);
}
export function parseTagNames(tags: string): string[] {

View file

@ -6,20 +6,18 @@ import { Container } from 'typedi';
import type { AuthenticatedRequest, PaginatedRequest } from '../../../types';
import { decodeCursor } from '../services/pagination.service';
import { License } from '@/License';
import type { RoleNames } from '@/databases/entities/Role';
import type { GlobalRole } from '@db/entities/User';
const UNLIMITED_USERS_QUOTA = -1;
export const authorize =
(authorizedRoles: readonly RoleNames[]) =>
(authorizedRoles: readonly GlobalRole[]) =>
(
req: AuthenticatedRequest,
res: express.Response,
next: express.NextFunction,
): express.Response | void => {
const { name } = req.user.globalRole;
if (!authorizedRoles.includes(name)) {
if (!authorizedRoles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}

View file

@ -74,27 +74,27 @@ export class Queue {
}
async add(jobData: JobData, jobOptions: object): Promise<Job> {
return this.jobQueue.add(jobData, jobOptions);
return await this.jobQueue.add(jobData, jobOptions);
}
async getJob(jobId: JobId): Promise<Job | null> {
return this.jobQueue.getJob(jobId);
return await this.jobQueue.getJob(jobId);
}
async getJobs(jobTypes: Bull.JobStatus[]): Promise<Job[]> {
return this.jobQueue.getJobs(jobTypes);
return await this.jobQueue.getJobs(jobTypes);
}
async process(concurrency: number, fn: Bull.ProcessCallbackFunction<JobData>): Promise<void> {
return this.jobQueue.process(concurrency, fn);
return await this.jobQueue.process(concurrency, fn);
}
async ping(): Promise<string> {
return this.jobQueue.client.ping();
return await this.jobQueue.client.ping();
}
async pause(isLocal?: boolean): Promise<void> {
return this.jobQueue.pause(isLocal);
return await this.jobQueue.pause(isLocal);
}
getBullObjectInstance(): JobQueue {

View file

@ -14,13 +14,10 @@ import cookieParser from 'cookie-parser';
import express from 'express';
import { engine as expressHandlebars } from 'express-handlebars';
import type { ServeStaticOptions } from 'serve-static';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { Not, In } from 'typeorm';
import { type Class, InstanceSettings } from 'n8n-core';
import type { ExecutionStatus, IExecutionsSummary, IN8nUISettings } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import type { IN8nUISettings } from 'n8n-workflow';
// @ts-ignore
import timezones from 'google-timezones-json';
@ -28,7 +25,6 @@ import history from 'connect-history-api-fallback';
import config from '@/config';
import { Queue } from '@/Queue';
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { WorkflowsController } from '@/workflows/workflows.controller';
import {
@ -39,7 +35,7 @@ import {
TEMPLATES_DIR,
} from '@/constants';
import { credentialsController } from '@/credentials/credentials.controller';
import type { CurlHelper, ExecutionRequest } from '@/requests';
import type { CurlHelper } from '@/requests';
import { registerController } from '@/decorators';
import { AuthController } from '@/controllers/auth.controller';
import { BinaryDataController } from '@/controllers/binaryData.controller';
@ -56,14 +52,12 @@ import { TranslationController } from '@/controllers/translation.controller';
import { UsersController } from '@/controllers/users.controller';
import { WorkflowStatisticsController } from '@/controllers/workflowStatistics.controller';
import { ExternalSecretsController } from '@/ExternalSecrets/ExternalSecrets.controller.ee';
import { executionsController } from '@/executions/executions.controller';
import { ExecutionsController } from '@/executions/executions.controller';
import { isApiEnabled, loadPublicApiVersions } from '@/PublicApi';
import type { ICredentialsOverwrite, IDiagnosticInfo, IExecutionsStopData } from '@/Interfaces';
import { ActiveExecutions } from '@/ActiveExecutions';
import type { ICredentialsOverwrite, IDiagnosticInfo } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as ResponseHelper from '@/ResponseHelper';
import { WaitTracker } from '@/WaitTracker';
import { toHttpNodeParameters } from '@/CurlConverterHelper';
import { EventBusController } from '@/eventbus/eventBus.controller';
import { EventBusControllerEE } from '@/eventbus/eventBus.controller.ee';
@ -76,7 +70,6 @@ import { PostHogClient } from './posthog';
import { eventBus } from './eventbus';
import { InternalHooks } from './InternalHooks';
import { License } from './License';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
import { SamlController } from './sso/saml/routes/saml.controller.ee';
import { SamlService } from './sso/saml/saml.service.ee';
import { VariablesController } from './environments/variables/variables.controller.ee';
@ -87,8 +80,6 @@ import {
import { SourceControlService } from '@/environments/sourceControl/sourceControl.service.ee';
import { SourceControlController } from '@/environments/sourceControl/sourceControl.controller.ee';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { handleMfaDisable, isMfaFeatureEnabled } from './Mfa/helpers';
@ -98,10 +89,8 @@ import { OrchestrationController } from './controllers/orchestration.controller'
import { WorkflowHistoryController } from './workflows/workflowHistory/workflowHistory.controller.ee';
import { InvitationController } from './controllers/invitation.controller';
import { CollaborationService } from './collaboration/collaboration.service';
import { RoleController } from './controllers/role.controller';
import { BadRequestError } from './errors/response-errors/bad-request.error';
import { NotFoundError } from './errors/response-errors/not-found.error';
import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee';
import { OrchestrationService } from '@/services/orchestration.service';
const exec = promisify(callbackExec);
@ -109,10 +98,6 @@ const exec = promisify(callbackExec);
export class Server extends AbstractServer {
private endpointPresetCredentials: string;
private waitTracker: WaitTracker;
private activeExecutionsInstance: ActiveExecutions;
private presetCredentialsLoaded: boolean;
private loadNodesAndCredentials: LoadNodesAndCredentials;
@ -138,9 +123,6 @@ export class Server extends AbstractServer {
this.frontendService = Container.get(require('@/services/frontend.service').FrontendService);
}
this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = Container.get(WaitTracker);
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
@ -208,8 +190,9 @@ export class Server extends AbstractServer {
order: { createdAt: 'ASC' },
where: {},
})
.then(async (workflow) =>
Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
.then(
async (workflow) =>
await Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
);
Container.get(CollaborationService);
@ -244,12 +227,15 @@ export class Server extends AbstractServer {
VariablesController,
InvitationController,
VariablesController,
RoleController,
ActiveWorkflowsController,
WorkflowsController,
ExecutionsController,
];
if (process.env.NODE_ENV !== 'production' && Container.get(MultiMainSetup).isEnabled) {
if (
process.env.NODE_ENV !== 'production' &&
Container.get(OrchestrationService).isMultiMainSetupEnabled
) {
const { DebugController } = await import('@/controllers/debug.controller');
controllers.push(DebugController);
}
@ -277,6 +263,11 @@ export class Server extends AbstractServer {
controllers.push(MFAController);
}
if (!config.getEnv('endpoints.disableUi')) {
const { CtaController } = await import('@/controllers/cta.controller');
controllers.push(CtaController);
}
controllers.forEach((controller) => registerController(app, controller));
}
@ -397,219 +388,6 @@ export class Server extends AbstractServer {
}),
);
// ----------------------------------------
// Executions
// ----------------------------------------
this.app.use(`/${this.restEndpoint}/executions`, executionsController);
// ----------------------------------------
// Executing Workflows
// ----------------------------------------
// Returns all the currently working executions
this.app.get(
`/${this.restEndpoint}/executions-current`,
ResponseHelper.send(
async (req: ExecutionRequest.GetAllCurrent): Promise<IExecutionsSummary[]> => {
if (config.getEnv('executions.mode') === 'queue') {
const queue = Container.get(Queue);
const currentJobs = await queue.getJobs(['active', 'waiting']);
const currentlyRunningQueueIds = currentJobs.map((job) => job.data.executionId);
const currentlyRunningManualExecutions =
this.activeExecutionsInstance.getActiveExecutions();
const manualExecutionIds = currentlyRunningManualExecutions.map(
(execution) => execution.id,
);
const currentlyRunningExecutionIds =
currentlyRunningQueueIds.concat(manualExecutionIds);
if (!currentlyRunningExecutionIds.length) return [];
const findOptions: FindManyOptions<ExecutionEntity> & {
where: FindOptionsWhere<ExecutionEntity>;
} = {
select: ['id', 'workflowId', 'mode', 'retryOf', 'startedAt', 'stoppedAt', 'status'],
order: { id: 'DESC' },
where: {
id: In(currentlyRunningExecutionIds),
status: Not(In(['finished', 'stopped', 'failed', 'crashed'] as ExecutionStatus[])),
},
};
const sharedWorkflowIds = await getSharedWorkflowIds(req.user);
if (!sharedWorkflowIds.length) return [];
if (req.query.filter) {
const { workflowId, status, finished } = jsonParse<any>(req.query.filter);
if (workflowId && sharedWorkflowIds.includes(workflowId)) {
Object.assign(findOptions.where, { workflowId });
} else {
Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
}
if (status) {
Object.assign(findOptions.where, { status: In(status) });
}
if (finished) {
Object.assign(findOptions.where, { finished });
}
} else {
Object.assign(findOptions.where, { workflowId: In(sharedWorkflowIds) });
}
const executions =
await Container.get(ExecutionRepository).findMultipleExecutions(findOptions);
if (!executions.length) return [];
return executions.map((execution) => {
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
return {
id: execution.id,
workflowId: execution.workflowId,
mode: execution.mode,
retryOf: execution.retryOf !== null ? execution.retryOf : undefined,
startedAt: new Date(execution.startedAt),
status: execution.status ?? null,
stoppedAt: execution.stoppedAt ?? null,
} as IExecutionsSummary;
});
}
const executingWorkflows = this.activeExecutionsInstance.getActiveExecutions();
const returnData: IExecutionsSummary[] = [];
const filter = req.query.filter ? jsonParse<any>(req.query.filter) : {};
const sharedWorkflowIds = await getSharedWorkflowIds(req.user);
for (const data of executingWorkflows) {
if (
(filter.workflowId !== undefined && filter.workflowId !== data.workflowId) ||
(data.workflowId !== undefined && !sharedWorkflowIds.includes(data.workflowId))
) {
continue;
}
returnData.push({
id: data.id,
workflowId: data.workflowId === undefined ? '' : data.workflowId,
mode: data.mode,
retryOf: data.retryOf,
startedAt: new Date(data.startedAt),
status: data.status,
});
}
returnData.sort((a, b) => Number(b.id) - Number(a.id));
return returnData;
},
),
);
// Forces the execution to stop
this.app.post(
`/${this.restEndpoint}/executions-current/:id/stop`,
ResponseHelper.send(async (req: ExecutionRequest.Stop): Promise<IExecutionsStopData> => {
const { id: executionId } = req.params;
const sharedWorkflowIds = await getSharedWorkflowIds(req.user);
if (!sharedWorkflowIds.length) {
throw new NotFoundError('Execution not found');
}
const fullExecutionData = await Container.get(ExecutionRepository).findSingleExecution(
executionId,
{
where: {
workflowId: In(sharedWorkflowIds),
},
},
);
if (!fullExecutionData) {
throw new NotFoundError('Execution not found');
}
if (config.getEnv('executions.mode') === 'queue') {
// Manual executions should still be stoppable, so
// try notifying the `activeExecutions` to stop it.
const result = await this.activeExecutionsInstance.stopExecution(req.params.id);
if (result === undefined) {
// If active execution could not be found check if it is a waiting one
try {
return await this.waitTracker.stopExecution(req.params.id);
} catch (error) {
// Ignore, if it errors as then it is probably a currently running
// execution
}
} else {
return {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
} as IExecutionsStopData;
}
const queue = Container.get(Queue);
const currentJobs = await queue.getJobs(['active', 'waiting']);
const job = currentJobs.find((job) => job.data.executionId === req.params.id);
if (!job) {
this.logger.debug('Could not stop job because it is no longer in queue', {
jobId: req.params.id,
});
} else {
await queue.stopJob(job);
}
const returnData: IExecutionsStopData = {
mode: fullExecutionData.mode,
startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt
? new Date(fullExecutionData.stoppedAt)
: undefined,
finished: fullExecutionData.finished,
status: fullExecutionData.status,
};
return returnData;
}
// Stop the execution and wait till it is done and we got the data
const result = await this.activeExecutionsInstance.stopExecution(executionId);
let returnData: IExecutionsStopData;
if (result === undefined) {
// If active execution could not be found check if it is a waiting one
returnData = await this.waitTracker.stopExecution(executionId);
} else {
returnData = {
mode: result.mode,
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
};
}
return returnData;
}),
);
// ----------------------------------------
// Options
// ----------------------------------------

View file

@ -25,7 +25,7 @@ import * as NodeExecuteFunctions from 'n8n-core';
import { removeTrailingSlash } from './utils';
import type { TestWebhookRegistration } from '@/services/test-webhook-registrations.service';
import { TestWebhookRegistrationsService } from '@/services/test-webhook-registrations.service';
import { MultiMainSetup } from './services/orchestration/main/MultiMainSetup.ee';
import { OrchestrationService } from '@/services/orchestration.service';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@Service()
@ -34,7 +34,7 @@ export class TestWebhooks implements IWebhookManager {
private readonly push: Push,
private readonly nodeTypes: NodeTypes,
private readonly registrations: TestWebhookRegistrationsService,
private readonly multiMainSetup: MultiMainSetup,
private readonly orchestrationService: OrchestrationService,
) {}
private timeouts: { [webhookKey: string]: NodeJS.Timeout } = {};
@ -101,7 +101,7 @@ export class TestWebhooks implements IWebhookManager {
throw new NotFoundError('Could not find node to process webhook.');
}
return new Promise(async (resolve, reject) => {
return await new Promise(async (resolve, reject) => {
try {
const executionMode = 'manual';
const executionId = await WebhookHelpers.executeWebhook(
@ -144,12 +144,12 @@ export class TestWebhooks implements IWebhookManager {
* the handler process commands the creator process to clear its test webhooks.
*/
if (
this.multiMainSetup.isEnabled &&
this.orchestrationService.isMultiMainSetupEnabled &&
sessionId &&
!this.push.getBackend().hasSessionId(sessionId)
) {
const payload = { webhookKey: key, workflowEntity, sessionId };
void this.multiMainSetup.publish('clear-test-webhooks', payload);
void this.orchestrationService.publish('clear-test-webhooks', payload);
return;
}
@ -229,7 +229,10 @@ export class TestWebhooks implements IWebhookManager {
return false; // no webhooks found to start a workflow
}
const timeout = setTimeout(async () => this.cancelWebhook(workflow.id), TEST_WEBHOOK_TIMEOUT);
const timeout = setTimeout(
async () => await this.cancelWebhook(workflow.id),
TEST_WEBHOOK_TIMEOUT,
);
for (const webhook of webhooks) {
const key = this.registrations.toKey(webhook);

View file

@ -1,22 +1,31 @@
import { Service } from 'typedi';
import type { INode, Workflow } from 'n8n-workflow';
import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import config from '@/config';
import { isSharingEnabled } from './UserManagementHelper';
import { License } from '@/License';
import { OwnershipService } from '@/services/ownership.service';
import Container from 'typedi';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@Service()
export class PermissionChecker {
constructor(
private readonly userRepository: UserRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly ownershipService: OwnershipService,
private readonly license: License,
) {}
/**
* Check if a user is permitted to execute a workflow.
*/
static async check(workflow: Workflow, userId: string) {
async check(workflow: Workflow, userId: string) {
// allow if no nodes in this workflow use creds
const credIdsToNodes = PermissionChecker.mapCredIdsToNodes(workflow);
const credIdsToNodes = this.mapCredIdsToNodes(workflow);
const workflowCredIds = Object.keys(credIdsToNodes);
@ -24,9 +33,8 @@ export class PermissionChecker {
// allow if requesting user is instance owner
const user = await Container.get(UserRepository).findOneOrFail({
const user = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['globalRole'],
});
if (user.hasGlobalScope('workflow:execute')) return;
@ -36,8 +44,8 @@ export class PermissionChecker {
let workflowUserIds = [userId];
if (workflow.id && isSharingEnabled()) {
const workflowSharings = await Container.get(SharedWorkflowRepository).find({
if (workflow.id && this.license.isSharingEnabled()) {
const workflowSharings = await this.sharedWorkflowRepository.find({
relations: ['workflow'],
where: { workflowId: workflow.id },
select: ['userId'],
@ -45,12 +53,8 @@ export class PermissionChecker {
workflowUserIds = workflowSharings.map((s) => s.userId);
}
const roleId = await Container.get(RoleService).findCredentialOwnerRoleId();
const credentialSharings = await Container.get(SharedCredentialsRepository).findSharings(
workflowUserIds,
roleId,
);
const credentialSharings =
await this.sharedCredentialsRepository.findOwnedSharings(workflowUserIds);
const accessibleCredIds = credentialSharings.map((s) => s.credentialsId);
@ -68,7 +72,7 @@ export class PermissionChecker {
});
}
static async checkSubworkflowExecutePolicy(
async checkSubworkflowExecutePolicy(
subworkflow: Workflow,
parentWorkflowId: string,
node?: INode,
@ -88,17 +92,15 @@ export class PermissionChecker {
let policy =
subworkflow.settings?.callerPolicy ?? config.getEnv('workflows.callerPolicyDefaultOption');
if (!isSharingEnabled()) {
if (!this.license.isSharingEnabled()) {
// Community version allows only same owner workflows
policy = 'workflowsFromSameOwner';
}
const parentWorkflowOwner =
await Container.get(OwnershipService).getWorkflowOwnerCached(parentWorkflowId);
await this.ownershipService.getWorkflowOwnerCached(parentWorkflowId);
const subworkflowOwner = await Container.get(OwnershipService).getWorkflowOwnerCached(
subworkflow.id,
);
const subworkflowOwner = await this.ownershipService.getWorkflowOwnerCached(subworkflow.id);
const description =
subworkflowOwner.id === parentWorkflowOwner.id
@ -134,7 +136,7 @@ export class PermissionChecker {
}
}
private static mapCredIdsToNodes(workflow: Workflow) {
private mapCredIdsToNodes(workflow: Workflow) {
return Object.values(workflow.nodes).reduce<{ [credentialId: string]: INode[] }>(
(map, node) => {
if (node.disabled || !node.credentials) return map;

Some files were not shown because too many files have changed in this diff Show more