mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' of github.com:seatable/n8n into seatable_node_rework
This commit is contained in:
commit
80fc892192
3
.github/workflows/e2e-reusable.yml
vendored
3
.github/workflows/e2e-reusable.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -22,4 +22,3 @@ cypress/screenshots/*
|
|||
cypress/downloads/*
|
||||
*.swp
|
||||
CHANGELOG-*.md
|
||||
packages/cli/oclif.manifest.json
|
||||
|
|
41
CHANGELOG.md
41
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
||||
|
|
18
cypress/composables/becomeTemplateCreatorCta.ts
Normal file
18
cypress/composables/becomeTemplateCreatorCta.ts
Normal 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
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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');
|
||||
|
|
32
cypress/e2e/37-become-creator-cta.cy.ts
Normal file
32
cypress/e2e/37-become-creator-cta.cy.ts
Normal 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');
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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": [
|
||||
[
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -20,6 +20,6 @@
|
|||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "1.6.2"
|
||||
"axios": "1.6.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`);
|
||||
|
|
|
@ -102,5 +102,5 @@ export async function conversationalAgentExecute(
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
|
|
|
@ -101,5 +101,5 @@ export async function openAiFunctionsAgentExecute(
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
|
|
|
@ -76,5 +76,5 @@ export async function planAndExecuteAgentExecute(
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
|
|
|
@ -94,5 +94,5 @@ export async function reActAgentAgentExecute(
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
|
|
|
@ -101,5 +101,5 @@ export async function sqlAgentAgentExecute(
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
|
|
|
@ -380,6 +380,6 @@ export class OpenAiAssistant implements INodeType {
|
|||
returnData.push({ json: response });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -258,6 +258,6 @@ export class ChainSummarizationV1 implements INodeType {
|
|||
returnData.push({ json: { response } });
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -415,6 +415,6 @@ export class ChainSummarizationV2 implements INodeType {
|
|||
}
|
||||
}
|
||||
|
||||
return this.prepareOutputData(returnData);
|
||||
return await this.prepareOutputData(returnData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -324,6 +324,6 @@ export class MemoryManager implements INodeType {
|
|||
result.push(...executionData);
|
||||
}
|
||||
|
||||
return this.prepareOutputData(result);
|
||||
return await this.prepareOutputData(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -108,6 +108,6 @@ export class VectorStoreInMemoryInsert implements INodeType {
|
|||
clearStore,
|
||||
);
|
||||
|
||||
return this.prepareOutputData(serializedDocuments);
|
||||
return await this.prepareOutputData(serializedDocuments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, '', {
|
||||
|
|
|
@ -134,6 +134,6 @@ export class VectorStorePineconeInsert implements INodeType {
|
|||
pineconeIndex,
|
||||
});
|
||||
|
||||
return this.prepareOutputData(serializedDocuments);
|
||||
return await this.prepareOutputData(serializedDocuments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, '', {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -122,6 +122,6 @@ export class VectorStoreSupabaseInsert implements INodeType {
|
|||
queryName,
|
||||
});
|
||||
|
||||
return this.prepareOutputData(serializedDocuments);
|
||||
return await this.prepareOutputData(serializedDocuments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,6 @@ export class VectorStoreZepInsert implements INodeType {
|
|||
|
||||
await ZepVectorStore.fromDocuments(processedDocuments, embeddings, zepConfig);
|
||||
|
||||
return this.prepareOutputData(serializedDocuments);
|
||||
return await this.prepareOutputData(serializedDocuments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
// ----------------------------------
|
||||
|
|
|
@ -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({});
|
||||
})();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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\-\_\/]+$/;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
};
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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.',
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -25,7 +25,7 @@ export class MfaService {
|
|||
secret,
|
||||
recoveryCodes,
|
||||
);
|
||||
return this.userRepository.update(userId, {
|
||||
return await this.userRepository.update(userId, {
|
||||
mfaSecret: encryptedSecret,
|
||||
mfaRecoveryCodes: encryptedRecoveryCodes,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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[] {
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
// ----------------------------------------
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue