Merge remote-tracking branch 'origin/master' into pay-36-store-execution-metadata

# Conflicts:
#	packages/cli/src/databases/migrations/mysqldb/index.ts
#	packages/cli/src/databases/migrations/postgresdb/index.ts
#	packages/cli/src/databases/migrations/sqlite/index.ts
#	packages/cli/src/executions/executions.service.ts
This commit is contained in:
Csaba Tuncsik 2023-02-23 14:10:49 +01:00
commit 6455ef2bfa
217 changed files with 2658 additions and 1847 deletions

52
.github/scripts/bump-versions.mjs vendored Normal file
View file

@ -0,0 +1,52 @@
import semver from 'semver';
import { writeFile, readFile } from 'fs/promises';
import { resolve } from 'path';
import child_process from 'child_process';
import { promisify } from 'util';
import assert from 'assert';
const exec = promisify(child_process.exec);
const rootDir = process.cwd();
const releaseType = process.env.RELEASE_TYPE;
assert.match(releaseType, /^(patch|minor|major)$/, 'Invalid RELEASE_TYPE');
// TODO: if releaseType is `auto` determine release type based on the changelog
const lastTag = (await exec('git describe --tags --match "n8n@*" --abbrev=0')).stdout.trim();
const packages = JSON.parse((await exec('pnpm ls -r --only-projects --json')).stdout);
const packageMap = {};
for (let { name, path, version, private: isPrivate, dependencies } of packages) {
if (isPrivate && path !== rootDir) continue;
if (path === rootDir) name = 'monorepo-root';
const isDirty = await exec(`git diff --quiet HEAD ${lastTag} -- ${path}`)
.then(() => false)
.catch((error) => true);
packageMap[name] = { path, isDirty, version };
}
assert.ok(packageMap['n8n'].isDirty, 'No changes found since the last release');
// Keep the monorepo version up to date with the released version
packageMap['monorepo-root'].version = packageMap['n8n'].version;
for (const packageName in packageMap) {
const { path, version, isDirty } = packageMap[packageName];
const packageFile = resolve(path, 'package.json');
const packageJson = JSON.parse(await readFile(packageFile, 'utf-8'));
packageJson.version = packageMap[packageName].nextVersion =
isDirty ||
Object.keys(packageJson.dependencies).some(
(dependencyName) => packageMap[dependencyName]?.isDirty,
)
? semver.inc(version, releaseType)
: version;
await writeFile(packageFile, JSON.stringify(packageJson, null, 2) + '\n');
}
console.log(packageMap['n8n'].nextVersion);

6
.github/scripts/package.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"dependencies": {
"semver": "^7.3.8",
"conventional-changelog-cli": "^2.2.2"
}
}

View file

@ -1,9 +1,8 @@
name: Check Documentation URLs name: Check Documentation URLs
on: on:
push: release:
tags: types: [published]
- n8n@*
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
workflow_dispatch: workflow_dispatch:

View file

@ -6,6 +6,9 @@ on:
- opened - opened
- edited - edited
- synchronize - synchronize
branches:
- '**'
- '!release/*'
jobs: jobs:
check-pr-title: check-pr-title:

View file

@ -1,9 +1,8 @@
name: Docker Image CI name: Docker Image CI
on: on:
push: release:
tags: types: [published]
- n8n@*
jobs: jobs:
build: build:

70
.github/workflows/release-create-pr.yml vendored Normal file
View file

@ -0,0 +1,70 @@
name: 'Release: Create Pull Request'
on:
workflow_dispatch:
inputs:
base-branch:
description: 'The branch, tag, or commit to create this release PR from.'
required: true
default: 'master'
release-type:
description: 'A SemVer release type.'
required: true
type: choice
default: 'minor'
options:
- patch
- minor
jobs:
create-release-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.inputs.base-branch }}
- name: Push the base branch
run: |
git checkout -b "release/${{ github.event.inputs.release-type }}"
git push -f origin "release/${{ github.event.inputs.release-type }}"
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm install --prefix=.github/scripts --no-package-lock
- name: Bump package versions
run: |
echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> $GITHUB_ENV
pnpm i --lockfile-only
env:
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
- name: Generate Changelog
run: npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -t n8n@
- name: Push the release branch, and Create the PR
uses: peter-evans/create-pull-request@v4
with:
base: 'release/${{ github.event.inputs.release-type }}'
branch: 'release/${{ env.NEXT_RELEASE }}'
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
delete-branch: true
labels: 'release'
title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
# 'TODO: add generated changelog to the body. create a script to generate custom changelog'
body: ''
# TODO: post PR link to slack

57
.github/workflows/release-publish.yml vendored Normal file
View file

@ -0,0 +1,57 @@
name: 'Release: Publish'
on:
pull_request:
types:
- closed
branches:
- 'release/patch'
- 'release/minor'
jobs:
publish-release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public
echo "RELEASE=$(node -e 'console.log(require("./package.json").version)')" >> $GITHUB_ENV
- name: Create Release
uses: ncipollo/release-action@v1
with:
commit: ${{github.event.pull_request.base.ref}}
tag: 'n8n@${{env.RELEASE}}'
- name: Merge Release into 'master'
run: |
git fetch origin
git checkout --track origin/master
git config user.name "Jan Oberhauser"
git config user.email jan.oberhauser@gmail.com
git merge --ff n8n@${{env.RELEASE}}
git push origin master
git push origin :${{github.event.pull_request.base.ref}}

View file

@ -1,3 +1,15 @@
## [0.216.1](https://github.com/n8n-io/n8n/compare/n8n@0.216.0...n8n@0.216.1) (2023-02-21)
### Bug Fixes
* **core:** Do not allow arbitrary path traversal in BinaryDataManager ([#5523](https://github.com/n8n-io/n8n/issues/5523)) ([40b9784](https://github.com/n8n-io/n8n/commit/40b97846483fe7c58229c156acb66f43a5a79dc3))
* **core:** Do not allow arbitrary path traversal in the credential-translation endpoint ([#5522](https://github.com/n8n-io/n8n/issues/5522)) ([fb07d77](https://github.com/n8n-io/n8n/commit/fb07d77106bb4933758c63bbfb87f591bf4a27dd))
* **core:** Do not explicitly bypass auth on urls containing `.svg` ([#5525](https://github.com/n8n-io/n8n/issues/5525)) ([27adea7](https://github.com/n8n-io/n8n/commit/27adea70459329fc0dddabee69e10c9d1453835f))
* **core:** User update endpoint should only allow updating email, firstName, and lastName ([#5526](https://github.com/n8n-io/n8n/issues/5526)) ([5599221](https://github.com/n8n-io/n8n/commit/5599221007cb09cb81f0623874fafc6cd481384c))
# [0.216.0](https://github.com/n8n-io/n8n/compare/n8n@0.215.2...n8n@0.216.0) (2023-02-16) # [0.216.0](https://github.com/n8n-io/n8n/compare/n8n@0.215.2...n8n@0.216.0) (2023-02-16)

View file

@ -11,15 +11,16 @@ Great that you are here and you want to contribute to n8n
- [Development setup](#development-setup) - [Development setup](#development-setup)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Node.js](#nodejs) - [Node.js](#nodejs)
- [pnpm](#pnpm)
- [pnpm workspaces](#pnpm-workspaces)
- [corepack](#corepack)
- [Build tools](#build-tools) - [Build tools](#build-tools)
- [pnpm workspaces](#pnpm-workspaces)
- [Actual n8n setup](#actual-n8n-setup) - [Actual n8n setup](#actual-n8n-setup)
- [Start](#start) - [Start](#start)
- [Development cycle](#development-cycle) - [Development cycle](#development-cycle)
- [Test suite](#test-suite) - [Test suite](#test-suite)
- [Releasing](#releasing)
- [Create custom nodes](#create-custom-nodes) - [Create custom nodes](#create-custom-nodes)
- [Create a new node to contribute to n8n](#create-a-new-node-to-contribute-to-n8n)
- [Checklist before submitting a new node](#checklist-before-submitting-a-new-node)
- [Extend documentation](#extend-documentation) - [Extend documentation](#extend-documentation)
- [Contributor License Agreement](#contributor-license-agreement) - [Contributor License Agreement](#contributor-license-agreement)
@ -195,6 +196,22 @@ If that gets executed in one of the package folders it will only run the tests
of this package. If it gets executed in the n8n-root folder it will run all of this package. If it gets executed in the n8n-root folder it will run all
tests of all packages. tests of all packages.
## Releasing
To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then
1. Bump versions of packages that have changed or have dependencies that have changed
2. Update the Changelog
3. Create a new branch called `release/${VERSION}`, and
4. Create a new pull-request to track any further changes that need to be included in this release
Once ready to release, simply merge the pull-request.
This triggers [another workflow](https://github.com/n8n-io/n8n/actions/workflows/release-publish.yml), that will
1. Build and publish the packages that have a new version in this release
2. Create a new tag, and GitHub release from squashed release commit
3. Merge the squashed release commit back into `master`
## Create custom nodes ## Create custom nodes
Learn about [building nodes](https://docs.n8n.io/integrations/creating-nodes/) to create custom nodes for n8n. You can create community nodes and make them available using [npm](https://www.npmjs.com/). Learn about [building nodes](https://docs.n8n.io/integrations/creating-nodes/) to create custom nodes for n8n. You can create community nodes and make them available using [npm](https://www.npmjs.com/).

View file

@ -112,6 +112,20 @@ describe('Node Creator', () => {
NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop'); NDVModal.getters.parameterInput('operation').should('contain.text', 'Crop');
}) })
it('should search through actions and confirm added action', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('ftp');
nodeCreatorFeature.getters.searchBar().find('input').realPress('{rightarrow}');
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('file');
// Navigate to rename action which should be the 4th item
nodeCreatorFeature.getters.searchBar().find('input').realPress('{downarrow}');
nodeCreatorFeature.getters.searchBar().find('input').realPress('{downarrow}');
nodeCreatorFeature.getters.searchBar().find('input').realPress('{downarrow}');
nodeCreatorFeature.getters.searchBar().find('input').realPress('{rightarrow}');
NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename');
})
it('should render and select community node', () => { it('should render and select community node', () => {
cy.intercept('GET', '/types/nodes.json').as('nodesIntercept'); cy.intercept('GET', '/types/nodes.json').as('nodesIntercept');
cy.wait('@nodesIntercept').then(() => { cy.wait('@nodesIntercept').then(() => {

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.216.0", "version": "0.216.1",
"private": true, "private": true,
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"engines": { "engines": {
@ -34,10 +34,10 @@
"test:e2e:all": "cross-env E2E_TESTS=true start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'" "test:e2e:all": "cross-env E2E_TESTS=true start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'"
}, },
"dependencies": { "dependencies": {
"n8n": "*" "n8n": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@n8n_io/eslint-config": "*", "@n8n_io/eslint-config": "workspace:*",
"@ngneat/falso": "^6.1.0", "@ngneat/falso": "^6.1.0",
"@types/jest": "^29.2.2", "@types/jest": "^29.2.2",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
@ -79,7 +79,8 @@
"qqjs>globby": "^11.1.0" "qqjs>globby": "^11.1.0"
}, },
"patchedDependencies": { "patchedDependencies": {
"element-ui@2.15.12": "patches/element-ui@2.15.12.patch" "element-ui@2.15.12": "patches/element-ui@2.15.12.patch",
"typedi@0.10.0": "patches/typedi@0.10.0.patch"
} }
} }
} }

View file

@ -34,6 +34,7 @@ process.env.OCLIF_TS_NODE = '0';
require('express-async-errors'); require('express-async-errors');
require('source-map-support').install(); require('source-map-support').install();
require('reflect-metadata');
require('@oclif/command') require('@oclif/command')
.run() .run()

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.216.0", "version": "0.216.1",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -109,6 +109,7 @@
"mock-jwks": "^1.0.9", "mock-jwks": "^1.0.9",
"nodemon": "^2.0.2", "nodemon": "^2.0.2",
"run-script-os": "^1.0.7", "run-script-os": "^1.0.7",
"ts-essentials": "^7.0.3",
"tsc-alias": "^1.8.2", "tsc-alias": "^1.8.2",
"tsconfig-paths": "^4.1.2" "tsconfig-paths": "^4.1.2"
}, },
@ -129,6 +130,7 @@
"callsites": "^3.1.0", "callsites": "^3.1.0",
"change-case": "^4.1.1", "change-case": "^4.1.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"client-oauth2": "^4.2.5", "client-oauth2": "^4.2.5",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "^1.6.0",
@ -169,10 +171,10 @@
"lodash.unset": "^4.5.2", "lodash.unset": "^4.5.2",
"luxon": "^3.1.0", "luxon": "^3.1.0",
"mysql2": "~2.3.3", "mysql2": "~2.3.3",
"n8n-core": "~0.155.0", "n8n-core": "workspace:*",
"n8n-editor-ui": "~0.182.0", "n8n-editor-ui": "workspace:*",
"n8n-nodes-base": "~0.214.0", "n8n-nodes-base": "workspace:*",
"n8n-workflow": "~0.137.0", "n8n-workflow": "workspace:*",
"nodemailer": "^6.7.1", "nodemailer": "^6.7.1",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
@ -187,6 +189,7 @@
"posthog-node": "^2.2.2", "posthog-node": "^2.2.2",
"prom-client": "^13.1.0", "prom-client": "^13.1.0",
"psl": "^1.8.0", "psl": "^1.8.0",
"reflect-metadata": "^0.1.13",
"replacestream": "^4.0.3", "replacestream": "^4.0.3",
"semver": "^7.3.8", "semver": "^7.3.8",
"shelljs": "^0.8.5", "shelljs": "^0.8.5",
@ -195,6 +198,7 @@
"sse-channel": "^4.0.0", "sse-channel": "^4.0.0",
"swagger-ui-express": "^4.3.0", "swagger-ui-express": "^4.3.0",
"syslog-client": "^1.1.1", "syslog-client": "^1.1.1",
"typedi": "^0.10.0",
"typeorm": "^0.3.12", "typeorm": "^0.3.12",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"validator": "13.7.0", "validator": "13.7.0",

View file

@ -1,3 +1,4 @@
import { Container } from 'typedi';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import type { Server } from 'http'; import type { Server } from 'http';
import type { Url } from 'url'; import type { Url } from 'url';
@ -12,7 +13,7 @@ import type { WebhookHttpMethod } from 'n8n-workflow';
import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow'; import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { N8N_VERSION, inDevelopment } from '@/constants'; import { N8N_VERSION, inDevelopment } from '@/constants';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { IExternalHooksClass } from '@/Interfaces'; import type { IExternalHooksClass } from '@/Interfaces';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
@ -23,7 +24,7 @@ import {
ServiceUnavailableError, ServiceUnavailableError,
} from '@/ResponseHelper'; } from '@/ResponseHelper';
import { corsMiddleware } from '@/middlewares'; import { corsMiddleware } from '@/middlewares';
import * as TestWebhooks from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
import { WaitingWebhooks } from '@/WaitingWebhooks'; import { WaitingWebhooks } from '@/WaitingWebhooks';
import { WEBHOOK_METHODS } from '@/WebhookHelpers'; import { WEBHOOK_METHODS } from '@/WebhookHelpers';
@ -36,7 +37,7 @@ export abstract class AbstractServer {
protected externalHooks: IExternalHooksClass; protected externalHooks: IExternalHooksClass;
protected activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner; protected activeWorkflowRunner: ActiveWorkflowRunner;
protected protocol: string; protected protocol: string;
@ -71,8 +72,8 @@ export abstract class AbstractServer {
this.endpointWebhookTest = config.getEnv('endpoints.webhookTest'); this.endpointWebhookTest = config.getEnv('endpoints.webhookTest');
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting'); this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting');
this.externalHooks = ExternalHooks(); this.externalHooks = Container.get(ExternalHooks);
this.activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); this.activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
} }
private async setupErrorHandlers() { private async setupErrorHandlers() {
@ -338,7 +339,7 @@ export abstract class AbstractServer {
// ---------------------------------------- // ----------------------------------------
protected setupTestWebhookEndpoint() { protected setupTestWebhookEndpoint() {
const endpoint = this.endpointWebhookTest; const endpoint = this.endpointWebhookTest;
const testWebhooks = TestWebhooks.getInstance(); const testWebhooks = Container.get(TestWebhooks);
// Register all test webhook requests (for testing via the UI) // Register all test webhook requests (for testing via the UI)
this.app.all(`/${endpoint}/*`, async (req, res) => { this.app.all(`/${endpoint}/*`, async (req, res) => {

View file

@ -26,7 +26,9 @@ import type {
} from '@/Interfaces'; } from '@/Interfaces';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { Service } from 'typedi';
@Service()
export class ActiveExecutions { export class ActiveExecutions {
private activeExecutions: { private activeExecutions: {
[index: string]: IExecutingWorkflowData; [index: string]: IExecutingWorkflowData;
@ -34,7 +36,6 @@ export class ActiveExecutions {
/** /**
* Add a new active execution * Add a new active execution
*
*/ */
async add( async add(
executionData: IWorkflowExecutionDataProcess, executionData: IWorkflowExecutionDataProcess,
@ -253,13 +254,3 @@ export class ActiveExecutions {
return this.activeExecutions[executionId].status; return this.activeExecutions[executionId].status;
} }
} }
let activeExecutionsInstance: ActiveExecutions | undefined;
export function getInstance(): ActiveExecutions {
if (activeExecutionsInstance === undefined) {
activeExecutionsInstance = new ActiveExecutions();
}
return activeExecutionsInstance;
}

View file

@ -1,3 +1,4 @@
import { Service } from 'typedi';
import type { import type {
IWebhookData, IWebhookData,
WebhookHttpMethod, WebhookHttpMethod,
@ -6,8 +7,9 @@ import type {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as NodeExecuteFunctions from './NodeExecuteFunctions'; import * as NodeExecuteFunctions from 'n8n-core';
@Service()
export class ActiveWebhooks { export class ActiveWebhooks {
private workflowWebhooks: { private workflowWebhooks: {
[key: string]: IWebhookData[]; [key: string]: IWebhookData[];

View file

@ -8,6 +8,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi';
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core'; import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
import type { import type {
@ -55,7 +57,7 @@ import config from '@/config';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WebhookEntity } from '@db/entities/WebhookEntity'; import type { WebhookEntity } from '@db/entities/WebhookEntity';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { createErrorExecution } from '@/GenericHelpers'; import { createErrorExecution } from '@/GenericHelpers';
import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from '@/constants'; import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from '@/constants';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@ -68,8 +70,9 @@ import { START_NODES } from './constants';
const WEBHOOK_PROD_UNREGISTERED_HINT = const WEBHOOK_PROD_UNREGISTERED_HINT =
"The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)"; "The workflow must be active for a production URL to run successfully. You can activate the workflow using the toggle in the top-right of the editor. Note that unlike test URL calls, production URL calls aren't shown on the canvas (only in the executions list)";
@Service()
export class ActiveWorkflowRunner { export class ActiveWorkflowRunner {
private activeWorkflows: ActiveWorkflows | null = null; private activeWorkflows = new ActiveWorkflows();
private activationErrors: { private activationErrors: {
[key: string]: IActivationError; [key: string]: IActivationError;
@ -79,9 +82,7 @@ export class ActiveWorkflowRunner {
[key: string]: IQueuedWorkflowActivations; [key: string]: IQueuedWorkflowActivations;
} = {}; } = {};
constructor() { constructor(private externalHooks: ExternalHooks) {}
this.activeWorkflows = new ActiveWorkflows();
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async init() { async init() {
@ -133,7 +134,7 @@ export class ActiveWorkflowRunner {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.info(` ${error.message}`); Logger.info(` ${error.message}`);
Logger.error( Logger.error(
`Issue on intital workflow activation try "${workflowData.name}" (startup)`, `Issue on initial workflow activation try "${workflowData.name}" (startup)`,
{ {
workflowName: workflowData.name, workflowName: workflowData.name,
workflowId: workflowData.id, workflowId: workflowData.id,
@ -148,21 +149,18 @@ export class ActiveWorkflowRunner {
} }
Logger.verbose('Finished initializing active workflows (startup)'); Logger.verbose('Finished initializing active workflows (startup)');
} }
const externalHooks = ExternalHooks();
await externalHooks.run('activeWorkflows.initialized', []); await this.externalHooks.run('activeWorkflows.initialized', []);
} }
/** /**
* Removes all the currently active workflows * Removes all the currently active workflows
*
*/ */
async removeAll(): Promise<void> { async removeAll(): Promise<void> {
let activeWorkflowIds: string[] = []; let activeWorkflowIds: string[] = [];
Logger.verbose('Call to remove all active workflows received (removeAll)'); Logger.verbose('Call to remove all active workflows received (removeAll)');
if (this.activeWorkflows !== null) { activeWorkflowIds.push.apply(activeWorkflowIds, this.activeWorkflows.allActiveWorkflows());
activeWorkflowIds.push.apply(activeWorkflowIds, this.activeWorkflows.allActiveWorkflows());
}
const activeWorkflows = await this.getActiveWorkflows(); const activeWorkflows = await this.getActiveWorkflows();
activeWorkflowIds = [ activeWorkflowIds = [
@ -183,7 +181,6 @@ export class ActiveWorkflowRunner {
/** /**
* Checks if a webhook for the given method and path exists and executes the workflow. * Checks if a webhook for the given method and path exists and executes the workflow.
*
*/ */
async executeWebhook( async executeWebhook(
httpMethod: WebhookHttpMethod, httpMethod: WebhookHttpMethod,
@ -192,11 +189,6 @@ export class ActiveWorkflowRunner {
res: express.Response, res: express.Response,
): Promise<IResponseCallbackData> { ): Promise<IResponseCallbackData> {
Logger.debug(`Received webhook "${httpMethod}" for path "${path}"`); Logger.debug(`Received webhook "${httpMethod}" for path "${path}"`);
if (this.activeWorkflows === null) {
throw new ResponseHelper.NotFoundError(
'The "activeWorkflows" instance did not get initialized yet.',
);
}
// Reset request parameters // Reset request parameters
req.params = {}; req.params = {};
@ -279,7 +271,7 @@ export class ActiveWorkflowRunner {
); );
} }
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({ const workflow = new Workflow({
id: webhook.workflowId, id: webhook.workflowId,
name: workflowData.name, name: workflowData.name,
@ -482,6 +474,7 @@ export class ActiveWorkflowRunner {
try { try {
await this.removeWorkflowWebhooks(workflow.id as string); await this.removeWorkflowWebhooks(workflow.id as string);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
Logger.error( Logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`, `Could not remove webhooks of workflow "${workflow.id}" because of error: "${error.message}"`,
@ -521,7 +514,7 @@ export class ActiveWorkflowRunner {
throw new Error(`Could not find workflow with id "${workflowId}"`); throw new Error(`Could not find workflow with id "${workflowId}"`);
} }
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({ const workflow = new Workflow({
id: workflowId, id: workflowId,
name: workflowData.name, name: workflowData.name,
@ -645,7 +638,7 @@ export class ActiveWorkflowRunner {
if (donePromise) { if (donePromise) {
executePromise.then((executionId) => { executePromise.then((executionId) => {
ActiveExecutions.getInstance() Container.get(ActiveExecutions)
.getPostExecutePromise(executionId) .getPostExecutePromise(executionId)
.then(donePromise.resolve) .then(donePromise.resolve)
.catch(donePromise.reject); .catch(donePromise.reject);
@ -702,7 +695,7 @@ export class ActiveWorkflowRunner {
if (donePromise) { if (donePromise) {
executePromise.then((executionId) => { executePromise.then((executionId) => {
ActiveExecutions.getInstance() Container.get(ActiveExecutions)
.getPostExecutePromise(executionId) .getPostExecutePromise(executionId)
.then(donePromise.resolve) .then(donePromise.resolve)
.catch(donePromise.reject); .catch(donePromise.reject);
@ -723,7 +716,7 @@ export class ActiveWorkflowRunner {
// Remove the workflow as "active" // Remove the workflow as "active"
await this.activeWorkflows?.remove(workflowData.id); await this.activeWorkflows.remove(workflowData.id);
this.activationErrors[workflowData.id] = { this.activationErrors[workflowData.id] = {
time: new Date().getTime(), time: new Date().getTime(),
error: { error: {
@ -777,10 +770,6 @@ export class ActiveWorkflowRunner {
activation: WorkflowActivateMode, activation: WorkflowActivateMode,
workflowData?: IWorkflowDb, workflowData?: IWorkflowDb,
): Promise<void> { ): Promise<void> {
if (this.activeWorkflows === null) {
throw new Error('The "activeWorkflows" instance did not get initialized yet.');
}
let workflowInstance: Workflow; let workflowInstance: Workflow;
try { try {
if (workflowData === undefined) { if (workflowData === undefined) {
@ -793,7 +782,7 @@ export class ActiveWorkflowRunner {
if (!workflowData) { if (!workflowData) {
throw new Error(`Could not find workflow with id "${workflowId}".`); throw new Error(`Could not find workflow with id "${workflowId}".`);
} }
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
workflowInstance = new Workflow({ workflowInstance = new Workflow({
id: workflowId, id: workflowId,
name: workflowData.name, name: workflowData.name,
@ -978,47 +967,31 @@ export class ActiveWorkflowRunner {
*/ */
// TODO: this should happen in a transaction // TODO: this should happen in a transaction
async remove(workflowId: string): Promise<void> { async remove(workflowId: string): Promise<void> {
if (this.activeWorkflows !== null) { // Remove all the webhooks of the workflow
// Remove all the webhooks of the workflow try {
try { await this.removeWorkflowWebhooks(workflowId);
await this.removeWorkflowWebhooks(workflowId); } catch (error) {
} catch (error) { ErrorReporter.error(error);
ErrorReporter.error(error); Logger.error(
Logger.error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`, );
);
}
if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them
delete this.activationErrors[workflowId];
}
if (this.queuedWorkflowActivations[workflowId] !== undefined) {
this.removeQueuedWorkflowActivation(workflowId);
}
// if it's active in memory then it's a trigger
// so remove from list of actives workflows
if (this.activeWorkflows.isActive(workflowId)) {
await this.activeWorkflows.remove(workflowId);
Logger.verbose(`Successfully deactivated workflow "${workflowId}"`, { workflowId });
}
return;
} }
throw new Error('The "activeWorkflows" instance did not get initialized yet.'); if (this.activationErrors[workflowId] !== undefined) {
// If there were any activation errors delete them
delete this.activationErrors[workflowId];
}
if (this.queuedWorkflowActivations[workflowId] !== undefined) {
this.removeQueuedWorkflowActivation(workflowId);
}
// if it's active in memory then it's a trigger
// so remove from list of actives workflows
if (this.activeWorkflows.isActive(workflowId)) {
await this.activeWorkflows.remove(workflowId);
Logger.verbose(`Successfully deactivated workflow "${workflowId}"`, { workflowId });
}
} }
} }
let workflowRunnerInstance: ActiveWorkflowRunner | undefined;
export function getInstance(): ActiveWorkflowRunner {
if (workflowRunnerInstance === undefined) {
workflowRunnerInstance = new ActiveWorkflowRunner();
}
return workflowRunnerInstance;
}

View file

@ -1,14 +1,12 @@
import { loadClassInIsolation } from 'n8n-core'; import { loadClassInIsolation } from 'n8n-core';
import type { import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
ICredentialType, import { Service } from 'typedi';
ICredentialTypes,
INodesAndCredentials,
LoadedClass,
} from 'n8n-workflow';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { RESPONSE_ERROR_MESSAGES } from './constants';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
class CredentialTypesClass implements ICredentialTypes { @Service()
constructor(private nodesAndCredentials: INodesAndCredentials) { export class CredentialTypes implements ICredentialTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {
nodesAndCredentials.credentialTypes = this; nodesAndCredentials.credentialTypes = this;
} }
@ -64,18 +62,3 @@ class CredentialTypesClass implements ICredentialTypes {
return this.nodesAndCredentials.known.credentials; return this.nodesAndCredentials.known.credentials;
} }
} }
let credentialTypesInstance: CredentialTypesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function CredentialTypes(nodesAndCredentials?: INodesAndCredentials): CredentialTypesClass {
if (!credentialTypesInstance) {
if (nodesAndCredentials) {
credentialTypesInstance = new CredentialTypesClass(nodesAndCredentials);
} else {
throw new Error('CredentialTypes not initialized yet');
}
}
return credentialTypesInstance;
}

View file

@ -32,7 +32,6 @@ import type {
IHttpRequestHelper, IHttpRequestHelper,
INodeTypeData, INodeTypeData,
INodeTypes, INodeTypes,
ICredentialTypes,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ICredentialsHelper, ICredentialsHelper,
@ -54,6 +53,7 @@ import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { whereClause } from './UserManagement/UserManagementHelper'; import { whereClause } from './UserManagement/UserManagementHelper';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { RESPONSE_ERROR_MESSAGES } from './constants';
import { Container } from 'typedi';
const mockNode = { const mockNode = {
name: '', name: '',
@ -87,8 +87,8 @@ const mockNodeTypes: INodeTypes = {
export class CredentialsHelper extends ICredentialsHelper { export class CredentialsHelper extends ICredentialsHelper {
constructor( constructor(
encryptionKey: string, encryptionKey: string,
private credentialTypes: ICredentialTypes = CredentialTypes(), private credentialTypes = Container.get(CredentialTypes),
private nodeTypes: INodeTypes = NodeTypes(), private nodeTypes = Container.get(NodeTypes),
) { ) {
super(encryptionKey); super(encryptionKey);
} }

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable import/no-dynamic-require */ /* eslint-disable import/no-dynamic-require */
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
import { Service } from 'typedi';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { import type {
IExternalHooksClass, IExternalHooksClass,
@ -10,7 +11,8 @@ import type {
import config from '@/config'; import config from '@/config';
class ExternalHooksClass implements IExternalHooksClass { @Service()
export class ExternalHooks implements IExternalHooksClass {
externalHooks: { externalHooks: {
[key: string]: Array<() => {}>; [key: string]: Array<() => {}>;
} = {}; } = {};
@ -103,14 +105,3 @@ class ExternalHooksClass implements IExternalHooksClass {
return !!this.externalHooks[hookName]; return !!this.externalHooks[hookName];
} }
} }
let externalHooksInstance: ExternalHooksClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function ExternalHooks(): ExternalHooksClass {
if (externalHooksInstance === undefined) {
externalHooksInstance = new ExternalHooksClass();
}
return externalHooksInstance;
}

View file

@ -22,6 +22,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { UserUpdatePayload } from '@/requests';
/** /**
* Returns the base URL n8n is reachable from * Returns the base URL n8n is reachable from
@ -99,7 +100,7 @@ export async function generateUniqueName(
} }
export async function validateEntity( export async function validateEntity(
entity: WorkflowEntity | CredentialsEntity | TagEntity | User, entity: WorkflowEntity | CredentialsEntity | TagEntity | User | UserUpdatePayload,
): Promise<void> { ): Promise<void> {
const errors = await validate(entity); const errors = await validate(entity);

View file

@ -22,6 +22,7 @@ import type {
WorkflowExecuteMode, WorkflowExecuteMode,
ExecutionStatus, ExecutionStatus,
IExecutionsSummary, IExecutionsSummary,
FeatureFlags,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
@ -226,6 +227,7 @@ export interface IExecutionsStopData {
mode: WorkflowExecuteMode; mode: WorkflowExecuteMode;
startedAt: Date; startedAt: Date;
stoppedAt?: Date; stoppedAt?: Date;
status: ExecutionStatus;
} }
export interface IExecutionsCurrentSummary { export interface IExecutionsCurrentSummary {
@ -477,6 +479,14 @@ export interface IN8nUISettings {
versionNotifications: IVersionNotificationSettings; versionNotifications: IVersionNotificationSettings;
instanceId: string; instanceId: string;
telemetry: ITelemetrySettings; telemetry: ITelemetrySettings;
posthog: {
enabled: boolean;
apiHost: string;
apiKey: string;
autocapture: boolean;
disableSessionRecording: boolean;
debug: boolean;
};
personalizationSurveyEnabled: boolean; personalizationSurveyEnabled: boolean;
defaultLocale: string; defaultLocale: string;
userManagement: IUserManagementSettings; userManagement: IUserManagementSettings;
@ -838,6 +848,10 @@ export interface PublicUser {
inviteAcceptUrl?: string; inviteAcceptUrl?: string;
} }
export interface CurrentUser extends PublicUser {
featureFlags?: FeatureFlags;
}
export interface N8nApp { export interface N8nApp {
app: Application; app: Application;
restEndpoint: string; restEndpoint: string;

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Service } from 'typedi';
import { snakeCase } from 'change-case'; import { snakeCase } from 'change-case';
import { BinaryDataManager } from 'n8n-core'; import { BinaryDataManager } from 'n8n-core';
import type { import type {
ExecutionStatus, ExecutionStatus,
INodesGraphResult, INodesGraphResult,
INodeTypes,
IRun, IRun,
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase, IWorkflowBase,
@ -22,13 +22,14 @@ import type {
IExecutionTrackProperties, IExecutionTrackProperties,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
import type { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity'; import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { RoleService } from './role/role.service'; import { RoleService } from './role/role.service';
import { eventBus } from './eventbus'; import { eventBus } from './eventbus';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { NodeTypes } from './NodeTypes';
function userToPayload(user: User): { function userToPayload(user: User): {
userId: string; userId: string;
@ -46,12 +47,17 @@ function userToPayload(user: User): {
}; };
} }
export class InternalHooksClass implements IInternalHooksClass { @Service()
constructor( export class InternalHooks implements IInternalHooksClass {
private telemetry: Telemetry, private instanceId: string;
private instanceId: string,
private nodeTypes: INodeTypes, constructor(private telemetry: Telemetry, private nodeTypes: NodeTypes) {}
) {}
async init(instanceId: string) {
this.instanceId = instanceId;
this.telemetry.setInstanceId(instanceId);
await this.telemetry.init();
}
async onServerStarted( async onServerStarted(
diagnosticInfo: IDiagnosticInfo, diagnosticInfo: IDiagnosticInfo,

View file

@ -1,25 +0,0 @@
import type { INodeTypes } from 'n8n-workflow';
import { InternalHooksClass } from '@/InternalHooks';
import { Telemetry } from '@/telemetry';
export class InternalHooksManager {
private static internalHooksInstance: InternalHooksClass;
static getInstance(): InternalHooksClass {
if (this.internalHooksInstance) {
return this.internalHooksInstance;
}
throw new Error('InternalHooks not initialized');
}
static async init(instanceId: string, nodeTypes: INodeTypes): Promise<InternalHooksClass> {
if (!this.internalHooksInstance) {
const telemetry = new Telemetry(instanceId);
await telemetry.init();
this.internalHooksInstance = new InternalHooksClass(telemetry, instanceId, nodeTypes);
}
return this.internalHooksInstance;
}
}

View file

@ -15,7 +15,8 @@ import {
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory'; import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
import { InternalHooksManager } from '@/InternalHooksManager'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export class LdapSync { export class LdapSync {
private intervalId: NodeJS.Timeout | undefined = undefined; private intervalId: NodeJS.Timeout | undefined = undefined;
@ -104,7 +105,7 @@ export class LdapSync {
); );
if (usersToDisable.length) { if (usersToDisable.length) {
void InternalHooksManager.getInstance().onLdapUsersDisabled({ void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_update', reason: 'ldap_update',
users: usersToDisable.length, users: usersToDisable.length,
user_ids: usersToDisable, user_ids: usersToDisable,
@ -144,7 +145,7 @@ export class LdapSync {
error: errorMessage, error: errorMessage,
}); });
void InternalHooksManager.getInstance().onLdapSyncFinished({ void Container.get(InternalHooks).onLdapSyncFinished({
type: !this.intervalId ? 'scheduled' : `manual_${mode}`, type: !this.intervalId ? 'scheduled' : `manual_${mode}`,
succeeded: true, succeeded: true,
users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length, users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length,

View file

@ -22,9 +22,10 @@ import {
LDAP_LOGIN_LABEL, LDAP_LOGIN_LABEL,
} from './constants'; } from './constants';
import type { ConnectionSecurity, LdapConfig } from './types'; import type { ConnectionSecurity, LdapConfig } from './types';
import { InternalHooksManager } from '@/InternalHooksManager';
import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow'; import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow';
import { getLicense } from '@/License'; import { getLicense } from '@/License';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
/** /**
* Check whether the LDAP feature is disabled in the instance * Check whether the LDAP feature is disabled in the instance
@ -162,7 +163,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
const ldapUsers = await getLdapUsers(); const ldapUsers = await getLdapUsers();
if (ldapUsers.length) { if (ldapUsers.length) {
await deleteAllLdapIdentities(); await deleteAllLdapIdentities();
void InternalHooksManager.getInstance().onLdapUsersDisabled({ void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_update', reason: 'ldap_update',
users: ldapUsers.length, users: ldapUsers.length,
user_ids: ldapUsers.map((user) => user.id), user_ids: ldapUsers.map((user) => user.id),
@ -185,7 +186,7 @@ export const handleLdapInit = async (): Promise<void> => {
if (!isLdapEnabled()) { if (!isLdapEnabled()) {
const ldapUsers = await getLdapUsers(); const ldapUsers = await getLdapUsers();
if (ldapUsers.length) { if (ldapUsers.length) {
void InternalHooksManager.getInstance().onLdapUsersDisabled({ void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_feature_deactivated', reason: 'ldap_feature_deactivated',
users: ldapUsers.length, users: ldapUsers.length,
user_ids: ldapUsers.map((user) => user.id), user_ids: ldapUsers.map((user) => user.id),
@ -238,7 +239,7 @@ export const findAndAuthenticateLdapUser = async (
); );
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
void InternalHooksManager.getInstance().onLdapLoginSyncFailed({ void Container.get(InternalHooks).onLdapLoginSyncFailed({
error: e.message, error: e.message,
}); });
Logger.error('LDAP - Error during search', { message: e.message }); Logger.error('LDAP - Error during search', { message: e.message });

View file

@ -2,9 +2,10 @@ import express from 'express';
import { LdapManager } from '../LdapManager.ee'; import { LdapManager } from '../LdapManager.ee';
import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers'; import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers';
import type { LdapConfiguration } from '../types'; import type { LdapConfiguration } from '../types';
import { InternalHooksManager } from '@/InternalHooksManager';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants'; import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants';
import { InternalHooks } from '@/InternalHooks';
import { Container } from 'typedi';
export const ldapController = express.Router(); export const ldapController = express.Router();
@ -42,7 +43,7 @@ ldapController.put('/config', async (req: LdapConfiguration.Update, res: express
const data = await getLdapConfig(); const data = await getLdapConfig();
void InternalHooksManager.getInstance().onUserUpdatedLdapSettings({ void Container.get(InternalHooks).onUserUpdatedLdapSettings({
user_id: req.user.id, user_id: req.user.id,
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES), ...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
}); });

View file

@ -31,13 +31,11 @@ import {
CUSTOM_API_CALL_NAME, CUSTOM_API_CALL_NAME,
inTest, inTest,
} from '@/constants'; } from '@/constants';
import {
persistInstalledPackageData,
removePackageFromDatabase,
} from '@/CommunityNodes/packageModel';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { Service } from 'typedi';
export class LoadNodesAndCredentialsClass implements INodesAndCredentials { @Service()
export class LoadNodesAndCredentials implements INodesAndCredentials {
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} }; known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} }; loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
@ -202,6 +200,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
if (loader.loadedNodes.length > 0) { if (loader.loadedNodes.length > 0) {
// Save info to DB // Save info to DB
try { try {
const { persistInstalledPackageData } = await import('@/CommunityNodes/packageModel');
const installedPackage = await persistInstalledPackageData(loader); const installedPackage = await persistInstalledPackageData(loader);
await this.postProcessLoaders(); await this.postProcessLoaders();
await this.generateTypesForFrontend(); await this.generateTypesForFrontend();
@ -229,6 +228,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
await executeCommand(command); await executeCommand(command);
const { removePackageFromDatabase } = await import('@/CommunityNodes/packageModel');
await removePackageFromDatabase(installedPackage); await removePackageFromDatabase(installedPackage);
if (packageName in this.loaders) { if (packageName in this.loaders) {
@ -264,6 +264,9 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
if (loader.loadedNodes.length > 0) { if (loader.loadedNodes.length > 0) {
// Save info to DB // Save info to DB
try { try {
const { persistInstalledPackageData, removePackageFromDatabase } = await import(
'@/CommunityNodes/packageModel'
);
await removePackageFromDatabase(installedPackage); await removePackageFromDatabase(installedPackage);
const newlyInstalledPackage = await persistInstalledPackageData(loader); const newlyInstalledPackage = await persistInstalledPackageData(loader);
await this.postProcessLoaders(); await this.postProcessLoaders();
@ -420,14 +423,3 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
throw new Error('Could not find "node_modules" folder!'); throw new Error('Could not find "node_modules" folder!');
} }
} }
let packagesInformationInstance: LoadNodesAndCredentialsClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function LoadNodesAndCredentials(): LoadNodesAndCredentialsClass {
if (packagesInformationInstance === undefined) {
packagesInformationInstance = new LoadNodesAndCredentialsClass();
}
return packagesInformationInstance;
}

View file

@ -1,6 +1,5 @@
import { loadClassInIsolation } from 'n8n-core'; import { loadClassInIsolation } from 'n8n-core';
import type { import type {
INodesAndCredentials,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
INodeTypes, INodeTypes,
@ -8,10 +7,13 @@ import type {
LoadedClass, LoadedClass,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow';
import { Service } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from './constants'; import { RESPONSE_ERROR_MESSAGES } from './constants';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
export class NodeTypesClass implements INodeTypes { @Service()
constructor(private nodesAndCredentials: INodesAndCredentials) { export class NodeTypes implements INodeTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {
// Some nodeTypes need to get special parameters applied like the // Some nodeTypes need to get special parameters applied like the
// polling nodes the polling times // polling nodes the polling times
this.applySpecialNodeParameters(); this.applySpecialNodeParameters();
@ -75,18 +77,3 @@ export class NodeTypesClass implements INodeTypes {
return this.nodesAndCredentials.known.nodes; return this.nodesAndCredentials.known.nodes;
} }
} }
let nodeTypesInstance: NodeTypesClass | undefined;
// eslint-disable-next-line @typescript-eslint/naming-convention
export function NodeTypes(nodesAndCredentials?: INodesAndCredentials): NodeTypesClass {
if (!nodeTypesInstance) {
if (nodesAndCredentials) {
nodeTypesInstance = new NodeTypesClass(nodesAndCredentials);
} else {
throw new Error('NodeTypes not initialized yet');
}
}
return nodeTypesInstance;
}

View file

@ -13,8 +13,9 @@ import type { JsonObject } from 'swagger-ui-express';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
async function createApiRouter( async function createApiRouter(
version: string, version: string,
@ -100,7 +101,7 @@ async function createApiRouter(
if (!user) return false; if (!user) return false;
void InternalHooksManager.getInstance().onUserInvokedApi({ void Container.get(InternalHooks).onUserInvokedApi({
user_id: user.id, user_id: user.id,
path: req.path, path: req.path,
method: req.method, method: req.method,

View file

@ -19,6 +19,7 @@ import {
saveCredential, saveCredential,
toJsonSchema, toJsonSchema,
} from './credentials.service'; } from './credentials.service';
import { Container } from 'typedi';
export = { export = {
createCredential: [ createCredential: [
@ -87,7 +88,7 @@ export = {
const { credentialTypeName } = req.params; const { credentialTypeName } = req.params;
try { try {
CredentialTypes().getByName(credentialTypeName); Container.get(CredentialTypes).getByName(credentialTypeName);
} catch (error) { } catch (error) {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }

View file

@ -7,6 +7,7 @@ import { CredentialsHelper } from '@/CredentialsHelper';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import type { CredentialRequest } from '../../../types'; import type { CredentialRequest } from '../../../types';
import { toJsonSchema } from './credentials.service'; import { toJsonSchema } from './credentials.service';
import { Container } from 'typedi';
export const validCredentialType = ( export const validCredentialType = (
req: CredentialRequest.Create, req: CredentialRequest.Create,
@ -14,7 +15,7 @@ export const validCredentialType = (
next: express.NextFunction, next: express.NextFunction,
): express.Response | void => { ): express.Response | void => {
try { try {
CredentialTypes().getByName(req.body.type); Container.get(CredentialTypes).getByName(req.body.type);
} catch (_) { } catch (_) {
return res.status(400).json({ message: 'req.body.type is not a known type' }); return res.status(400).json({ message: 'req.body.type is not a known type' });
} }

View file

@ -8,6 +8,7 @@ import type { User } from '@db/entities/User';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import type { IDependency, IJsonSchema } from '../../../types'; import type { IDependency, IJsonSchema } from '../../../types';
import type { CredentialRequest } from '@/requests'; import type { CredentialRequest } from '@/requests';
import { Container } from 'typedi';
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> { export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
return Db.collections.Credentials.findOneBy({ id: credentialId }); return Db.collections.Credentials.findOneBy({ id: credentialId });
@ -62,7 +63,7 @@ export async function saveCredential(
scope: 'credential', scope: 'credential',
}); });
await ExternalHooks().run('credentials.create', [encryptedData]); await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
return Db.transaction(async (transactionManager) => { return Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(credential); const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
@ -84,7 +85,7 @@ export async function saveCredential(
} }
export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> { export async function removeCredential(credentials: CredentialsEntity): Promise<ICredentialsDb> {
await ExternalHooks().run('credentials.delete', [credentials.id]); await Container.get(ExternalHooks).run('credentials.delete', [credentials.id]);
return Db.collections.Credentials.remove(credentials); return Db.collections.Credentials.remove(credentials);
} }

View file

@ -8,12 +8,13 @@ import {
deleteExecution, deleteExecution,
getExecutionsCount, getExecutionsCount,
} from './executions.service'; } from './executions.service';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { authorize, validCursor } from '../../shared/middlewares/global.middleware'; import { authorize, validCursor } from '../../shared/middlewares/global.middleware';
import type { ExecutionRequest } from '../../../types'; import type { ExecutionRequest } from '../../../types';
import { getSharedWorkflowIds } from '../workflows/workflows.service'; import { getSharedWorkflowIds } from '../workflows/workflows.service';
import { encodeNextCursor } from '../../shared/services/pagination.service'; import { encodeNextCursor } from '../../shared/services/pagination.service';
import { InternalHooksManager } from '@/InternalHooksManager'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export = { export = {
deleteExecution: [ deleteExecution: [
@ -66,7 +67,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
void InternalHooksManager.getInstance().onUserRetrievedExecution({ void Container.get(InternalHooks).onUserRetrievedExecution({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });
@ -95,7 +96,7 @@ export = {
} }
// get running workflows so we exclude them from the result // get running workflows so we exclude them from the result
const runningExecutionsIds = ActiveExecutions.getInstance() const runningExecutionsIds = Container.get(ActiveExecutions)
.getActiveExecutions() .getActiveExecutions()
.map(({ id }) => id); .map(({ id }) => id);
@ -116,7 +117,7 @@ export = {
const count = await getExecutionsCount(filters); const count = await getExecutionsCount(filters);
void InternalHooksManager.getInstance().onUserRetrievedAllExecutions({ void Container.get(InternalHooks).onUserRetrievedAllExecutions({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });

View file

@ -1,12 +1,11 @@
import type express from 'express'; import type express from 'express';
import { Container } from 'typedi';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm'; import type { FindManyOptions, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm'; import { In } from 'typeorm';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import config from '@/config'; import config from '@/config';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { InternalHooksManager } from '@/InternalHooksManager';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { addNodeIds, replaceInvalidCredentials } from '@/WorkflowHelpers'; import { addNodeIds, replaceInvalidCredentials } from '@/WorkflowHelpers';
import type { WorkflowRequest } from '../../../types'; import type { WorkflowRequest } from '../../../types';
@ -29,6 +28,7 @@ import {
parseTagNames, parseTagNames,
} from './workflows.service'; } from './workflows.service';
import { WorkflowsService } from '@/workflows/workflows.services'; import { WorkflowsService } from '@/workflows/workflows.services';
import { InternalHooks } from '@/InternalHooks';
export = { export = {
createWorkflow: [ createWorkflow: [
@ -50,8 +50,8 @@ export = {
const createdWorkflow = await createWorkflow(workflow, req.user, role); const createdWorkflow = await createWorkflow(workflow, req.user, role);
await ExternalHooks().run('workflow.afterCreate', [createdWorkflow]); await Container.get(ExternalHooks).run('workflow.afterCreate', [createdWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user, createdWorkflow, true); void Container.get(InternalHooks).onWorkflowCreated(req.user, createdWorkflow, true);
return res.json(createdWorkflow); return res.json(createdWorkflow);
}, },
@ -84,7 +84,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
void InternalHooksManager.getInstance().onUserRetrievedWorkflow({ void Container.get(InternalHooks).onUserRetrievedWorkflow({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });
@ -145,7 +145,7 @@ export = {
count = await getWorkflowsCount(query); count = await getWorkflowsCount(query);
} }
void InternalHooksManager.getInstance().onUserRetrievedAllWorkflows({ void Container.get(InternalHooks).onUserRetrievedAllWorkflows({
user_id: req.user.id, user_id: req.user.id,
public_api: true, public_api: true,
}); });
@ -182,7 +182,7 @@ export = {
await replaceInvalidCredentials(updateData); await replaceInvalidCredentials(updateData);
addNodeIds(updateData); addNodeIds(updateData);
const workflowRunner = ActiveWorkflowRunner.getInstance(); const workflowRunner = Container.get(ActiveWorkflowRunner);
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
// When workflow gets saved always remove it as the triggers could have been // When workflow gets saved always remove it as the triggers could have been
@ -210,8 +210,8 @@ export = {
const updatedWorkflow = await getWorkflowById(sharedWorkflow.workflowId); const updatedWorkflow = await getWorkflowById(sharedWorkflow.workflowId);
await ExternalHooks().run('workflow.afterUpdate', [updateData]); await Container.get(ExternalHooks).run('workflow.afterUpdate', [updateData]);
void InternalHooksManager.getInstance().onWorkflowSaved(req.user, updateData, true); void Container.get(InternalHooks).onWorkflowSaved(req.user, updateData, true);
return res.json(updatedWorkflow); return res.json(updatedWorkflow);
}, },
@ -231,7 +231,7 @@ export = {
if (!sharedWorkflow.workflow.active) { if (!sharedWorkflow.workflow.active) {
try { try {
await ActiveWorkflowRunner.getInstance().add(sharedWorkflow.workflowId, 'activate'); await Container.get(ActiveWorkflowRunner).add(sharedWorkflow.workflowId, 'activate');
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
return res.status(400).json({ message: error.message }); return res.status(400).json({ message: error.message });
@ -263,7 +263,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
const workflowRunner = ActiveWorkflowRunner.getInstance(); const workflowRunner = Container.get(ActiveWorkflowRunner);
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
await workflowRunner.remove(sharedWorkflow.workflowId); await workflowRunner.remove(sharedWorkflow.workflowId);

View file

@ -2,8 +2,9 @@ import type Bull from 'bull';
import type { RedisOptions } from 'ioredis'; import type { RedisOptions } from 'ioredis';
import type { IExecuteResponsePromiseData } from 'n8n-workflow'; import type { IExecuteResponsePromiseData } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import { Container } from 'typedi';
export type JobId = Bull.JobId; export type JobId = Bull.JobId;
export type Job = Bull.Job<JobData>; export type Job = Bull.Job<JobData>;
@ -26,7 +27,7 @@ export interface WebhookResponse {
export class Queue { export class Queue {
private jobQueue: JobQueue; private jobQueue: JobQueue;
constructor(private activeExecutions: ActiveExecutions.ActiveExecutions) {} constructor(private activeExecutions: ActiveExecutions) {}
async init() { async init() {
const prefix = config.getEnv('queue.bull.prefix'); const prefix = config.getEnv('queue.bull.prefix');
@ -95,7 +96,7 @@ let activeQueueInstance: Queue | undefined;
export async function getInstance(): Promise<Queue> { export async function getInstance(): Promise<Queue> {
if (activeQueueInstance === undefined) { if (activeQueueInstance === undefined) {
activeQueueInstance = new Queue(ActiveExecutions.getInstance()); activeQueueInstance = new Queue(Container.get(ActiveExecutions));
await activeQueueInstance.init(); await activeQueueInstance.init();
} }

View file

@ -1,13 +1,13 @@
import path from 'path'; import path from 'path';
import { realpath, access } from 'fs/promises'; import { realpath, access } from 'fs/promises';
import type { LoadNodesAndCredentialsClass } from '@/LoadNodesAndCredentials'; import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { NodeTypesClass } from '@/NodeTypes'; import type { NodeTypes } from '@/NodeTypes';
import type { Push } from '@/push'; import type { Push } from '@/push';
export const reloadNodesAndCredentials = async ( export const reloadNodesAndCredentials = async (
loadNodesAndCredentials: LoadNodesAndCredentialsClass, loadNodesAndCredentials: LoadNodesAndCredentials,
nodeTypes: NodeTypesClass, nodeTypes: NodeTypes,
push: Push, push: Push,
) => { ) => {
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies

View file

@ -33,6 +33,7 @@ import {
LoadNodeParameterOptions, LoadNodeParameterOptions,
LoadNodeListSearch, LoadNodeListSearch,
UserSettings, UserSettings,
FileNotFoundError,
} from 'n8n-core'; } from 'n8n-core';
import type { import type {
@ -56,8 +57,6 @@ import history from 'connect-history-api-fallback';
import config from '@/config'; import config from '@/config';
import * as Queue from '@/Queue'; import * as Queue from '@/Queue';
import { InternalHooksManager } from '@/InternalHooksManager';
import { getCredentialTranslationPath } from '@/TranslationHelpers';
import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { nodesController } from '@/api/nodes.api'; import { nodesController } from '@/api/nodes.api';
@ -67,7 +66,6 @@ import {
GENERATED_STATIC_DIR, GENERATED_STATIC_DIR,
inDevelopment, inDevelopment,
N8N_VERSION, N8N_VERSION,
NODES_BASE_DIR,
RESPONSE_ERROR_MESSAGES, RESPONSE_ERROR_MESSAGES,
TEMPLATES_DIR, TEMPLATES_DIR,
} from '@/constants'; } from '@/constants';
@ -88,6 +86,7 @@ import {
MeController, MeController,
OwnerController, OwnerController,
PasswordResetController, PasswordResetController,
TranslationController,
UsersController, UsersController,
} from '@/controllers'; } from '@/controllers';
@ -113,7 +112,7 @@ import type {
IExecutionsStopData, IExecutionsStopData,
IN8nUISettings, IN8nUISettings,
} from '@/Interfaces'; } from '@/Interfaces';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { import {
CredentialsHelper, CredentialsHelper,
getCredentialForUser, getCredentialForUser,
@ -122,11 +121,8 @@ import {
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { LoadNodesAndCredentialsClass } from '@/LoadNodesAndCredentials';
import type { NodeTypesClass } from '@/NodeTypes';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import type { WaitTrackerClass } from '@/WaitTracker';
import { WaitTracker } from '@/WaitTracker'; import { WaitTracker } from '@/WaitTracker';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@ -135,8 +131,7 @@ import { eventBusRouter } from '@/eventbus/eventBusRoutes';
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper'; import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
import { getLicense } from '@/License'; import { getLicense } from '@/License';
import { licenseController } from './license/license.controller'; import { licenseController } from './license/license.controller';
import type { Push } from '@/push'; import { Push, setupPushServer, setupPushHandler } from '@/push';
import { getPushInstance, setupPushServer, setupPushHandler } from '@/push';
import { setupAuthMiddlewares } from './middlewares'; import { setupAuthMiddlewares } from './middlewares';
import { initEvents } from './events'; import { initEvents } from './events';
import { ldapController } from './Ldap/routes/ldap.controller.ee'; import { ldapController } from './Ldap/routes/ldap.controller.ee';
@ -145,44 +140,51 @@ import { AbstractServer } from './AbstractServer';
import { configureMetrics } from './metrics'; import { configureMetrics } from './metrics';
import { setupBasicAuth } from './middlewares/basicAuth'; import { setupBasicAuth } from './middlewares/basicAuth';
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth'; import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
import { PostHogClient } from './posthog';
import { eventBus } from './eventbus'; import { eventBus } from './eventbus';
import { isSamlEnabled } from './Saml/helpers'; import { isSamlEnabled } from './Saml/helpers';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
const exec = promisify(callbackExec); const exec = promisify(callbackExec);
class Server extends AbstractServer { class Server extends AbstractServer {
endpointPresetCredentials: string; endpointPresetCredentials: string;
waitTracker: WaitTrackerClass; waitTracker: WaitTracker;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; activeExecutionsInstance: ActiveExecutions;
frontendSettings: IN8nUISettings; frontendSettings: IN8nUISettings;
presetCredentialsLoaded: boolean; presetCredentialsLoaded: boolean;
loadNodesAndCredentials: LoadNodesAndCredentialsClass; loadNodesAndCredentials: LoadNodesAndCredentials;
nodeTypes: NodeTypesClass; nodeTypes: NodeTypes;
credentialTypes: ICredentialTypes; credentialTypes: ICredentialTypes;
postHog: PostHogClient;
push: Push; push: Push;
constructor() { constructor() {
super(); super();
this.nodeTypes = NodeTypes(); this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
this.credentialTypes = CredentialTypes(); this.credentialTypes = Container.get(CredentialTypes);
this.loadNodesAndCredentials = LoadNodesAndCredentials(); this.nodeTypes = Container.get(NodeTypes);
this.activeExecutionsInstance = ActiveExecutions.getInstance(); this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = WaitTracker(); this.waitTracker = Container.get(WaitTracker);
this.postHog = Container.get(PostHogClient);
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint'); this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
this.push = getPushInstance(); this.push = Container.get(Push);
if (process.env.E2E_TESTS === 'true') { if (process.env.E2E_TESTS === 'true') {
this.app.use('/e2e', require('./api/e2e.api').e2eController); this.app.use('/e2e', require('./api/e2e.api').e2eController);
@ -232,6 +234,16 @@ class Server extends AbstractServer {
}, },
instanceId: '', instanceId: '',
telemetry: telemetrySettings, telemetry: telemetrySettings,
posthog: {
enabled: config.getEnv('diagnostics.enabled'),
apiHost: config.getEnv('diagnostics.config.posthog.apiHost'),
apiKey: config.getEnv('diagnostics.config.posthog.apiKey'),
autocapture: false,
disableSessionRecording: config.getEnv(
'diagnostics.config.posthog.disableSessionRecording',
),
debug: config.getEnv('logs.level') === 'debug',
},
personalizationSurveyEnabled: personalizationSurveyEnabled:
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'), config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'), defaultLocale: config.getEnv('defaultLocale'),
@ -343,14 +355,16 @@ class Server extends AbstractServer {
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User); setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
const logger = LoggerProxy; const logger = LoggerProxy;
const internalHooks = InternalHooksManager.getInstance(); const internalHooks = Container.get(InternalHooks);
const mailer = getMailerInstance(); const mailer = getMailerInstance();
const postHog = this.postHog;
const controllers = [ const controllers = [
new AuthController({ config, internalHooks, repositories, logger }), new AuthController({ config, internalHooks, repositories, logger, postHog }),
new OwnerController({ config, internalHooks, repositories, logger }), new OwnerController({ config, internalHooks, repositories, logger }),
new MeController({ externalHooks, internalHooks, repositories, logger }), new MeController({ externalHooks, internalHooks, repositories, logger }),
new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }), new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }),
new TranslationController(config, this.credentialTypes),
new UsersController({ new UsersController({
config, config,
mailer, mailer,
@ -359,6 +373,7 @@ class Server extends AbstractServer {
repositories, repositories,
activeWorkflowRunner, activeWorkflowRunner,
logger, logger,
postHog,
}), }),
]; ];
controllers.forEach((controller) => registerController(app, config, controller)); controllers.forEach((controller) => registerController(app, config, controller));
@ -378,6 +393,7 @@ class Server extends AbstractServer {
await this.externalHooks.run('frontend.settings', [this.frontendSettings]); await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
await this.initLicense(); await this.initLicense();
await this.postHog.init(this.frontendSettings.instanceId);
const publicApiEndpoint = config.getEnv('publicApi.path'); const publicApiEndpoint = config.getEnv('publicApi.path');
const excludeEndpoints = config.getEnv('security.excludeEndpoints'); const excludeEndpoints = config.getEnv('security.excludeEndpoints');
@ -589,48 +605,6 @@ class Server extends AbstractServer {
), ),
); );
this.app.get(
`/${this.restEndpoint}/credential-translation`,
ResponseHelper.send(
async (
req: express.Request & { query: { credentialType: string } },
res: express.Response,
): Promise<object | null> => {
const translationPath = getCredentialTranslationPath({
locale: this.frontendSettings.defaultLocale,
credentialType: req.query.credentialType,
});
try {
return require(translationPath);
} catch (error) {
return null;
}
},
),
);
// Returns node information based on node names and versions
const headersPath = pathJoin(NODES_BASE_DIR, 'dist', 'nodes', 'headers');
this.app.get(
`/${this.restEndpoint}/node-translation-headers`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<object | void> => {
try {
await fsAccess(`${headersPath}.js`);
} catch (_) {
return; // no headers available
}
try {
return require(headersPath);
} catch (error) {
res.status(500).send('Failed to load headers file');
}
},
),
);
// ---------------------------------------- // ----------------------------------------
// Node-Types // Node-Types
// ---------------------------------------- // ----------------------------------------
@ -1013,6 +987,9 @@ class Server extends AbstractServer {
if (!executions.length) return []; if (!executions.length) return [];
return executions.map((execution) => { return executions.map((execution) => {
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
return { return {
id: execution.id, id: execution.id,
workflowId: execution.workflowId, workflowId: execution.workflowId,
@ -1047,6 +1024,7 @@ class Server extends AbstractServer {
mode: data.mode, mode: data.mode,
retryOf: data.retryOf, retryOf: data.retryOf,
startedAt: new Date(data.startedAt), startedAt: new Date(data.startedAt),
status: data.status,
}); });
} }
@ -1099,6 +1077,7 @@ class Server extends AbstractServer {
startedAt: new Date(result.startedAt), startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished, finished: result.finished,
status: result.status,
} as IExecutionsStopData; } as IExecutionsStopData;
} }
@ -1125,6 +1104,7 @@ class Server extends AbstractServer {
? new Date(fullExecutionData.stoppedAt) ? new Date(fullExecutionData.stoppedAt)
: undefined, : undefined,
finished: fullExecutionData.finished, finished: fullExecutionData.finished,
status: fullExecutionData.status,
}; };
return returnData; return returnData;
@ -1143,6 +1123,7 @@ class Server extends AbstractServer {
startedAt: new Date(result.startedAt), startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined, stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished, finished: result.finished,
status: result.status,
}; };
} }
@ -1173,21 +1154,26 @@ class Server extends AbstractServer {
// TODO UM: check if this needs permission check for UM // TODO UM: check if this needs permission check for UM
const identifier = req.params.path; const identifier = req.params.path;
const binaryDataManager = BinaryDataManager.getInstance(); const binaryDataManager = BinaryDataManager.getInstance();
const binaryPath = binaryDataManager.getBinaryPath(identifier); try {
let { mode, fileName, mimeType } = req.query; const binaryPath = binaryDataManager.getBinaryPath(identifier);
if (!fileName || !mimeType) { let { mode, fileName, mimeType } = req.query;
try { if (!fileName || !mimeType) {
const metadata = await binaryDataManager.getBinaryMetadata(identifier); try {
fileName = metadata.fileName; const metadata = await binaryDataManager.getBinaryMetadata(identifier);
mimeType = metadata.mimeType; fileName = metadata.fileName;
res.setHeader('Content-Length', metadata.fileSize); mimeType = metadata.mimeType;
} catch {} res.setHeader('Content-Length', metadata.fileSize);
} catch {}
}
if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
res.sendFile(binaryPath);
} catch (error) {
if (error instanceof FileNotFoundError) res.writeHead(404).end();
else throw error;
} }
if (mimeType) res.setHeader('Content-Type', mimeType);
if (mode === 'download') {
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
}
res.sendFile(binaryPath);
}, },
); );
@ -1200,9 +1186,7 @@ class Server extends AbstractServer {
`/${this.restEndpoint}/settings`, `/${this.restEndpoint}/settings`,
ResponseHelper.send( ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => { async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
void InternalHooksManager.getInstance().onFrontendSettingsAPI( void Container.get(InternalHooks).onFrontendSettingsAPI(req.headers.sessionid as string);
req.headers.sessionid as string,
);
return this.getSettingsForFrontend(); return this.getSettingsForFrontend();
}, },
@ -1373,6 +1357,6 @@ export async function start(): Promise<void> {
order: { createdAt: 'ASC' }, order: { createdAt: 'ASC' },
where: {}, where: {},
}).then(async (workflow) => }).then(async (workflow) =>
InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt), Container.get(InternalHooks).onServerStarted(diagnosticInfo, workflow?.createdAt),
); );
} }

View file

@ -2,8 +2,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type express from 'express'; import type express from 'express';
import { Service } from 'typedi';
import { ActiveWebhooks } from 'n8n-core';
import type { import type {
IWebhookData, IWebhookData,
@ -13,16 +12,18 @@ import type {
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { ActiveWebhooks } from '@/ActiveWebhooks';
import type { IResponseCallbackData, IWorkflowDb } from '@/Interfaces'; import type { IResponseCallbackData, IWorkflowDb } from '@/Interfaces';
import type { Push } from '@/push'; import { Push } from '@/push';
import { getPushInstance } from '@/push';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
const WEBHOOK_TEST_UNREGISTERED_HINT = const WEBHOOK_TEST_UNREGISTERED_HINT =
"Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)"; "Click the 'Execute workflow' button on the canvas, then try again. (In test mode, the webhook only works for one call after you click this button)";
class TestWebhooks { @Service()
export class TestWebhooks {
private testWebhookData: { private testWebhookData: {
[key: string]: { [key: string]: {
sessionId?: string; sessionId?: string;
@ -286,13 +287,3 @@ class TestWebhooks {
return this.activeWebhooks.removeAll(workflows); return this.activeWebhooks.removeAll(workflows);
} }
} }
let testWebhooksInstance: TestWebhooks | undefined;
export function getInstance(): TestWebhooks {
if (testWebhooksInstance === undefined) {
testWebhooksInstance = new TestWebhooks(new ActiveWebhooks(), getPushInstance());
}
return testWebhooksInstance;
}

View file

@ -1,7 +1,6 @@
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { readdir } from 'fs/promises'; import { readdir } from 'fs/promises';
import type { Dirent } from 'fs'; import type { Dirent } from 'fs';
import { NODES_BASE_DIR } from '@/constants';
const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // e.g. v1, v10 const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // e.g. v1, v10
@ -47,18 +46,3 @@ export async function getNodeTranslationPath({
? join(nodeDir, `v${maxVersion}`, 'translations', locale, `${nodeType}.json`) ? join(nodeDir, `v${maxVersion}`, 'translations', locale, `${nodeType}.json`)
: join(nodeDir, 'translations', locale, `${nodeType}.json`); : join(nodeDir, 'translations', locale, `${nodeType}.json`);
} }
/**
* Get the full path to a credential translation file in `/dist`.
*/
export function getCredentialTranslationPath({
locale,
credentialType,
}: {
locale: string;
credentialType: string;
}): string {
const credsPath = join(NODES_BASE_DIR, 'dist', 'credentials');
return join(credsPath, 'translations', locale, `${credentialType}.json`);
}

View file

@ -6,7 +6,7 @@ import { compare, genSaltSync, hash } from 'bcryptjs';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import type { PublicUser, WhereClause } from '@/Interfaces'; import type { CurrentUser, PublicUser, WhereClause } from '@/Interfaces';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User'; import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
@ -15,6 +15,7 @@ import config from '@/config';
import { getWebhookBaseUrl } from '@/WebhookHelpers'; import { getWebhookBaseUrl } from '@/WebhookHelpers';
import { getLicense } from '@/License'; import { getLicense } from '@/License';
import { RoleService } from '@/role/role.service'; import { RoleService } from '@/role/role.service';
import type { PostHogClient } from '@/posthog';
export async function getWorkflowOwner(workflowId: string): Promise<User> { export async function getWorkflowOwner(workflowId: string): Promise<User> {
const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' }); const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' });
@ -162,6 +163,31 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
return sanitizedUser; return sanitizedUser;
} }
export async function withFeatureFlags(
postHog: PostHogClient | undefined,
user: CurrentUser,
): Promise<CurrentUser> {
if (!postHog) {
return user;
}
// native PostHog implementation has default 10s timeout and 3 retries.. which cannot be updated without affecting other functionality
// https://github.com/PostHog/posthog-js-lite/blob/a182de80a433fb0ffa6859c10fb28084d0f825c2/posthog-core/src/index.ts#L67
const timeoutPromise = new Promise<CurrentUser>((resolve) => {
setTimeout(() => {
resolve(user);
}, 1500);
});
const fetchPromise = new Promise<CurrentUser>(async (resolve) => {
user.featureFlags = await postHog.getFeatureFlags(user);
resolve(user);
});
return Promise.race([fetchPromise, timeoutPromise]);
}
export function addInviteLinkToUser(user: PublicUser, inviterId: string): PublicUser { export function addInviteLinkToUser(user: PublicUser, inviterId: string): PublicUser {
if (user.isPending) { if (user.isPending) {
user.inviteAcceptUrl = generateUserInviteUrl(inviterId, user.id); user.inviteAcceptUrl = generateUserInviteUrl(inviterId, user.id);

View file

@ -17,7 +17,7 @@ import { DateUtils } from 'typeorm/util/DateUtils';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import type { import type {
IExecutionFlattedDb, IExecutionFlattedDb,
IExecutionsStopData, IExecutionsStopData,
@ -25,9 +25,11 @@ import type {
} from '@/Interfaces'; } from '@/Interfaces';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container, Service } from 'typedi';
export class WaitTrackerClass { @Service()
activeExecutionsInstance: ActiveExecutions.ActiveExecutions; export class WaitTracker {
activeExecutionsInstance: ActiveExecutions;
private waitingExecutions: { private waitingExecutions: {
[key: string]: { [key: string]: {
@ -39,7 +41,7 @@ export class WaitTrackerClass {
mainTimer: NodeJS.Timeout; mainTimer: NodeJS.Timeout;
constructor() { constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance(); this.activeExecutionsInstance = Container.get(ActiveExecutions);
// Poll every 60 seconds a list of upcoming executions // Poll every 60 seconds a list of upcoming executions
this.mainTimer = setInterval(() => { this.mainTimer = setInterval(() => {
@ -142,6 +144,7 @@ export class WaitTrackerClass {
startedAt: new Date(fullExecutionData.startedAt), startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined, stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined,
finished: fullExecutionData.finished, finished: fullExecutionData.finished,
status: fullExecutionData.status,
}; };
} }
@ -189,13 +192,3 @@ export class WaitTrackerClass {
}); });
} }
} }
let waitTrackerInstance: WaitTrackerClass | undefined;
export function WaitTracker(): WaitTrackerClass {
if (waitTrackerInstance === undefined) {
waitTrackerInstance = new WaitTrackerClass();
}
return waitTrackerInstance;
}

View file

@ -13,6 +13,7 @@ import { NodeTypes } from '@/NodeTypes';
import type { IExecutionResponse, IResponseCallbackData, IWorkflowDb } from '@/Interfaces'; import type { IExecutionResponse, IResponseCallbackData, IWorkflowDb } from '@/Interfaces';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
export class WaitingWebhooks { export class WaitingWebhooks {
async executeWebhook( async executeWebhook(
@ -78,7 +79,7 @@ export class WaitingWebhooks {
const { workflowData } = fullExecutionData; const { workflowData } = fullExecutionData;
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({ const workflow = new Workflow({
id: workflowData.id!.toString(), id: workflowData.id!.toString(),
name: workflowData.name, name: workflowData.name,

View file

@ -52,10 +52,11 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
export const WEBHOOK_METHODS = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; export const WEBHOOK_METHODS = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'];
@ -460,7 +461,7 @@ export async function executeWebhook(
); );
// Get a promise which resolves when the workflow did execute and send then response // Get a promise which resolves when the workflow did execute and send then response
const executePromise = ActiveExecutions.getInstance().getPostExecutePromise( const executePromise = Container.get(ActiveExecutions).getPostExecutePromise(
executionId, executionId,
) as Promise<IExecutionDb | undefined>; ) as Promise<IExecutionDb | undefined>;
executePromise executePromise

View file

@ -48,7 +48,7 @@ import { LessThanOrEqual } from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils'; import { DateUtils } from 'typeorm/util/DateUtils';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialsHelper } from '@/CredentialsHelper';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import type { import type {
@ -60,9 +60,8 @@ import type {
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
IWorkflowErrorData, IWorkflowErrorData,
} from '@/Interfaces'; } from '@/Interfaces';
import { InternalHooksManager } from '@/InternalHooksManager';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { getPushInstance } from '@/push'; import { Push } from '@/push';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
@ -70,6 +69,8 @@ import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { findSubworkflowStart } from '@/utils'; import { findSubworkflowStart } from '@/utils';
import { PermissionChecker } from './UserManagement/PermissionChecker'; import { PermissionChecker } from './UserManagement/PermissionChecker';
import { WorkflowsService } from './workflows/workflows.services'; import { WorkflowsService } from './workflows/workflows.services';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
@ -280,7 +281,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
workflowId: this.workflowData.id, workflowId: this.workflowData.id,
}); });
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send('nodeExecuteBefore', { executionId, nodeName }, sessionId); pushInstance.send('nodeExecuteBefore', { executionId, nodeName }, sessionId);
}, },
], ],
@ -298,7 +299,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
workflowId: this.workflowData.id, workflowId: this.workflowData.id,
}); });
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send('nodeExecuteAfter', { executionId, nodeName, data }, sessionId); pushInstance.send('nodeExecuteAfter', { executionId, nodeName, data }, sessionId);
}, },
], ],
@ -315,7 +316,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
if (sessionId === undefined) { if (sessionId === undefined) {
return; return;
} }
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send( pushInstance.send(
'executionStarted', 'executionStarted',
{ {
@ -381,7 +382,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
retryOf, retryOf,
}; };
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send('executionFinished', sendData, sessionId); pushInstance.send('executionFinished', sendData, sessionId);
}, },
], ],
@ -389,7 +390,7 @@ function hookFunctionsPush(): IWorkflowExecuteHooks {
} }
export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks { export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks {
const externalHooks = ExternalHooks(); const externalHooks = Container.get(ExternalHooks);
return { return {
workflowExecuteBefore: [ workflowExecuteBefore: [
@ -938,10 +939,10 @@ async function executeWorkflow(
parentWorkflowSettings?: IWorkflowSettings; parentWorkflowSettings?: IWorkflowSettings;
}, },
): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> { ): Promise<Array<INodeExecutionData[] | null> | IWorkflowExecuteProcess> {
const externalHooks = ExternalHooks(); const externalHooks = Container.get(ExternalHooks);
await externalHooks.init(); await externalHooks.init();
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflowData = const workflowData =
options.loadedWorkflowData ?? options.loadedWorkflowData ??
@ -971,10 +972,10 @@ async function executeWorkflow(
executionId = executionId =
options.parentExecutionId !== undefined options.parentExecutionId !== undefined
? options.parentExecutionId ? options.parentExecutionId
: await ActiveExecutions.getInstance().add(runData); : await Container.get(ActiveExecutions).add(runData);
} }
void InternalHooksManager.getInstance().onWorkflowBeforeExecute(executionId || '', runData); void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId || '', runData);
let data; let data;
try { try {
@ -1077,7 +1078,7 @@ async function executeWorkflow(
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]); await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
void InternalHooksManager.getInstance().onWorkflowPostExecute( void Container.get(InternalHooks).onWorkflowPostExecute(
executionId, executionId,
workflowData, workflowData,
data, data,
@ -1087,11 +1088,11 @@ async function executeWorkflow(
if (data.finished === true) { if (data.finished === true) {
// Workflow did finish successfully // Workflow did finish successfully
ActiveExecutions.getInstance().remove(executionId, data); Container.get(ActiveExecutions).remove(executionId, data);
const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data);
return returnData!.data!.main; return returnData!.data!.main;
} }
ActiveExecutions.getInstance().remove(executionId, data); Container.get(ActiveExecutions).remove(executionId, data);
// Workflow did fail // Workflow did fail
const { error } = data.data.resultData; const { error } = data.data.resultData;
// eslint-disable-next-line @typescript-eslint/no-throw-literal // eslint-disable-next-line @typescript-eslint/no-throw-literal
@ -1107,7 +1108,7 @@ export function setExecutionStatus(status: ExecutionStatus) {
return; return;
} }
Logger.debug(`Setting execution status for ${this.executionId} to "${status}"`); Logger.debug(`Setting execution status for ${this.executionId} to "${status}"`);
ActiveExecutions.getInstance() Container.get(ActiveExecutions)
.setStatus(this.executionId, status) .setStatus(this.executionId, status)
.catch((error) => { .catch((error) => {
Logger.debug(`Setting execution status "${status}" failed: ${error.message}`); Logger.debug(`Setting execution status "${status}" failed: ${error.message}`);
@ -1123,7 +1124,7 @@ export function sendMessageToUI(source: string, messages: any[]) {
// Push data to session which started workflow // Push data to session which started workflow
try { try {
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send( pushInstance.send(
'sendConsoleMessage', 'sendConsoleMessage',
{ {
@ -1244,7 +1245,7 @@ export function getWorkflowHooksWorkerMain(
this: WorkflowHooks, this: WorkflowHooks,
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
void InternalHooksManager.getInstance().onNodeBeforeExecute( void Container.get(InternalHooks).onNodeBeforeExecute(
this.executionId, this.executionId,
this.workflowData, this.workflowData,
nodeName, nodeName,
@ -1254,7 +1255,7 @@ export function getWorkflowHooksWorkerMain(
this: WorkflowHooks, this: WorkflowHooks,
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
void InternalHooksManager.getInstance().onNodePostExecute( void Container.get(InternalHooks).onNodePostExecute(
this.executionId, this.executionId,
this.workflowData, this.workflowData,
nodeName, nodeName,
@ -1296,7 +1297,7 @@ export function getWorkflowHooksMain(
this: WorkflowHooks, this: WorkflowHooks,
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
void InternalHooksManager.getInstance().onNodeBeforeExecute( void Container.get(InternalHooks).onNodeBeforeExecute(
this.executionId, this.executionId,
this.workflowData, this.workflowData,
nodeName, nodeName,
@ -1307,7 +1308,7 @@ export function getWorkflowHooksMain(
this: WorkflowHooks, this: WorkflowHooks,
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
void InternalHooksManager.getInstance().onNodePostExecute( void Container.get(InternalHooks).onNodePostExecute(
this.executionId, this.executionId,
this.workflowData, this.workflowData,
nodeName, nodeName,

View file

@ -32,6 +32,7 @@ import type { User } from '@db/entities/User';
import { whereClause } from '@/UserManagement/UserManagementHelper'; import { whereClause } from '@/UserManagement/UserManagementHelper';
import omit from 'lodash.omit'; import omit from 'lodash.omit';
import { PermissionChecker } from './UserManagement/PermissionChecker'; import { PermissionChecker } from './UserManagement/PermissionChecker';
import { Container } from 'typedi';
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
@ -108,7 +109,7 @@ export async function executeErrorWorkflow(
} }
const executionMode = 'error'; const executionMode = 'error';
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflowInstance = new Workflow({ const workflowInstance = new Workflow({
id: workflowId, id: workflowId,

View file

@ -33,7 +33,7 @@ import PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path'; import { join as pathJoin } from 'path';
import { fork } from 'child_process'; import { fork } from 'child_process';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
@ -49,25 +49,25 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers'; import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { InternalHooksManager } from '@/InternalHooksManager';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers'; import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting'; import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import type { Push } from '@/push'; import { Push } from '@/push';
import { getPushInstance } from '@/push';
import { eventBus } from './eventbus'; import { eventBus } from './eventbus';
import { recoverExecutionDataFromEventLogMessages } from './eventbus/MessageEventBus/recoverEvents'; import { recoverExecutionDataFromEventLogMessages } from './eventbus/MessageEventBus/recoverEvents';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
export class WorkflowRunner { export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions; activeExecutions: ActiveExecutions;
push: Push; push: Push;
jobQueue: Queue.JobQueue; jobQueue: Queue.JobQueue;
constructor() { constructor() {
this.push = getPushInstance(); this.push = Container.get(Push);
this.activeExecutions = ActiveExecutions.getInstance(); this.activeExecutions = Container.get(ActiveExecutions);
} }
/** /**
@ -130,7 +130,7 @@ export class WorkflowRunner {
const executionFlattedData = await Db.collections.Execution.findOneBy({ id: executionId }); const executionFlattedData = await Db.collections.Execution.findOneBy({ id: executionId });
void InternalHooksManager.getInstance().onWorkflowCrashed( void Container.get(InternalHooks).onWorkflowCrashed(
executionId, executionId,
executionMode, executionMode,
executionFlattedData?.workflowData, executionFlattedData?.workflowData,
@ -187,14 +187,14 @@ export class WorkflowRunner {
executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise); executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise);
} }
void InternalHooksManager.getInstance().onWorkflowBeforeExecute(executionId, data); void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId, data);
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId); const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
const externalHooks = ExternalHooks(); const externalHooks = Container.get(ExternalHooks);
postExecutePromise postExecutePromise
.then(async (executionData) => { .then(async (executionData) => {
void InternalHooksManager.getInstance().onWorkflowPostExecute( void Container.get(InternalHooks).onWorkflowPostExecute(
executionId!, executionId!,
data.workflowData, data.workflowData,
executionData, executionData,
@ -241,7 +241,7 @@ export class WorkflowRunner {
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId); data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(workflowId);
} }
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
// Soft timeout to stop workflow execution after current running node // Soft timeout to stop workflow execution after current running node
// Changes were made by adding the `workflowTimeout` to the `additionalData` // Changes were made by adding the `workflowTimeout` to the `additionalData`

View file

@ -7,6 +7,8 @@
/* eslint-disable @typescript-eslint/no-use-before-define */ /* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
import 'source-map-support/register'; import 'source-map-support/register';
import 'reflect-metadata';
import { Container } from 'typedi';
import type { IProcessMessage } from 'n8n-core'; import type { IProcessMessage } from 'n8n-core';
import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core'; import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core';
@ -49,11 +51,12 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import config from '@/config'; import config from '@/config';
import { InternalHooksManager } from '@/InternalHooksManager';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers'; import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting'; import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker'; import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { getLicense } from './License'; import { getLicense } from './License';
import { InternalHooks } from './InternalHooks';
import { PostHogClient } from './posthog';
class WorkflowRunnerProcess { class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined; data: IWorkflowExecutionDataProcessWithExecution | undefined;
@ -103,19 +106,20 @@ class WorkflowRunnerProcess {
const userSettings = await UserSettings.prepareUserSettings(); const userSettings = await UserSettings.prepareUserSettings();
const loadNodesAndCredentials = LoadNodesAndCredentials(); const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
await loadNodesAndCredentials.init(); await loadNodesAndCredentials.init();
const nodeTypes = NodeTypes(loadNodesAndCredentials); const nodeTypes = Container.get(NodeTypes);
const credentialTypes = CredentialTypes(loadNodesAndCredentials); const credentialTypes = Container.get(CredentialTypes);
CredentialsOverwrites(credentialTypes); CredentialsOverwrites(credentialTypes);
// Load all external hooks // Load all external hooks
const externalHooks = ExternalHooks(); const externalHooks = Container.get(ExternalHooks);
await externalHooks.init(); await externalHooks.init();
const instanceId = userSettings.instanceId ?? ''; const instanceId = userSettings.instanceId ?? '';
await InternalHooksManager.init(instanceId, nodeTypes); await Container.get(PostHogClient).init(instanceId);
await Container.get(InternalHooks).init(instanceId);
const binaryDataConfig = config.getEnv('binaryDataManager'); const binaryDataConfig = config.getEnv('binaryDataManager');
await BinaryDataManager.init(binaryDataConfig); await BinaryDataManager.init(binaryDataConfig);
@ -229,7 +233,7 @@ class WorkflowRunnerProcess {
}; };
}); });
void InternalHooksManager.getInstance().onWorkflowBeforeExecute(executionId || '', runData); void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId || '', runData);
let result: IRun; let result: IRun;
try { try {
@ -250,7 +254,7 @@ class WorkflowRunnerProcess {
const { workflow } = executeWorkflowFunctionOutput; const { workflow } = executeWorkflowFunctionOutput;
result = await workflowExecute.processRunExecutionData(workflow); result = await workflowExecute.processRunExecutionData(workflow);
await externalHooks.run('workflow.postExecute', [result, workflowData, executionId]); await externalHooks.run('workflow.postExecute', [result, workflowData, executionId]);
void InternalHooksManager.getInstance().onWorkflowPostExecute( void Container.get(InternalHooks).onWorkflowPostExecute(
executionId, executionId,
workflowData, workflowData,
result, result,
@ -508,6 +512,8 @@ process.on('message', async (message: IProcessMessage) => {
workflowRunner.executionIdCallback(message.data.executionId); workflowRunner.executionIdCallback(message.data.executionId);
} }
} catch (error) { } catch (error) {
workflowRunner.logger.error(error.message);
// Catch all uncaught errors and forward them to parent process // Catch all uncaught errors and forward them to parent process
const executionError = { const executionError = {
...error, ...error,

View file

@ -8,6 +8,7 @@ import config from '@/config';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import { getNodeTranslationPath } from '@/TranslationHelpers'; import { getNodeTranslationPath } from '@/TranslationHelpers';
import { Container } from 'typedi';
export const nodeTypesController = express.Router(); export const nodeTypesController = express.Router();
@ -21,7 +22,7 @@ nodeTypesController.post(
if (defaultLocale === 'en') { if (defaultLocale === 'en') {
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => { return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
const { description } = NodeTypes().getByNameAndVersion(name, version); const { description } = Container.get(NodeTypes).getByNameAndVersion(name, version);
acc.push(description); acc.push(description);
return acc; return acc;
}, []); }, []);
@ -32,7 +33,7 @@ nodeTypesController.post(
version: number, version: number,
nodeTypes: INodeTypeDescription[], nodeTypes: INodeTypeDescription[],
) { ) {
const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version); const { description, sourcePath } = Container.get(NodeTypes).getWithSourcePath(name, version);
const translationPath = await getNodeTranslationPath({ const translationPath = await getNodeTranslationPath({
nodeSourcePath: sourcePath, nodeSourcePath: sourcePath,
longNodeType: description.name, longNodeType: description.name,

View file

@ -2,7 +2,6 @@ import express from 'express';
import type { PublicInstalledPackage } from 'n8n-workflow'; import type { PublicInstalledPackage } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import { InternalHooksManager } from '@/InternalHooksManager';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
@ -33,7 +32,9 @@ import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper';
import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces'; import type { CommunityPackages } from '@/Interfaces';
import type { NodeRequest } from '@/requests'; import type { NodeRequest } from '@/requests';
import { getPushInstance } from '@/push'; import { Push } from '@/push';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES; const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES;
@ -116,14 +117,14 @@ nodesController.post(
let installedPackage: InstalledPackages; let installedPackage: InstalledPackages;
try { try {
installedPackage = await LoadNodesAndCredentials().loadNpmModule( installedPackage = await Container.get(LoadNodesAndCredentials).loadNpmModule(
parsed.packageName, parsed.packageName,
parsed.version, parsed.version,
); );
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON; const errorMessage = error instanceof Error ? error.message : UNKNOWN_FAILURE_REASON;
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({ void Container.get(InternalHooks).onCommunityPackageInstallFinished({
user: req.user, user: req.user,
input_string: name, input_string: name,
package_name: parsed.packageName, package_name: parsed.packageName,
@ -141,7 +142,7 @@ nodesController.post(
if (!hasLoaded) removePackageFromMissingList(name); if (!hasLoaded) removePackageFromMissingList(name);
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
// broadcast to connected frontends that node list has been updated // broadcast to connected frontends that node list has been updated
installedPackage.installedNodes.forEach((node) => { installedPackage.installedNodes.forEach((node) => {
@ -151,7 +152,7 @@ nodesController.post(
}); });
}); });
void InternalHooksManager.getInstance().onCommunityPackageInstallFinished({ void Container.get(InternalHooks).onCommunityPackageInstallFinished({
user: req.user, user: req.user,
input_string: name, input_string: name,
package_name: parsed.packageName, package_name: parsed.packageName,
@ -238,7 +239,7 @@ nodesController.delete(
} }
try { try {
await LoadNodesAndCredentials().removeNpmModule(name, installedPackage); await Container.get(LoadNodesAndCredentials).removeNpmModule(name, installedPackage);
} catch (error) { } catch (error) {
const message = [ const message = [
`Error removing package "${name}"`, `Error removing package "${name}"`,
@ -248,7 +249,7 @@ nodesController.delete(
throw new ResponseHelper.InternalServerError(message); throw new ResponseHelper.InternalServerError(message);
} }
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
// broadcast to connected frontends that node list has been updated // broadcast to connected frontends that node list has been updated
installedPackage.installedNodes.forEach((node) => { installedPackage.installedNodes.forEach((node) => {
@ -258,7 +259,7 @@ nodesController.delete(
}); });
}); });
void InternalHooksManager.getInstance().onCommunityPackageDeleteFinished({ void Container.get(InternalHooks).onCommunityPackageDeleteFinished({
user: req.user, user: req.user,
package_name: name, package_name: name,
package_version: installedPackage.installedVersion, package_version: installedPackage.installedVersion,
@ -290,12 +291,12 @@ nodesController.patch(
} }
try { try {
const newInstalledPackage = await LoadNodesAndCredentials().updateNpmModule( const newInstalledPackage = await Container.get(LoadNodesAndCredentials).updateNpmModule(
parseNpmPackageName(name).packageName, parseNpmPackageName(name).packageName,
previouslyInstalledPackage, previouslyInstalledPackage,
); );
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
// broadcast to connected frontends that node list has been updated // broadcast to connected frontends that node list has been updated
previouslyInstalledPackage.installedNodes.forEach((node) => { previouslyInstalledPackage.installedNodes.forEach((node) => {
@ -312,7 +313,7 @@ nodesController.patch(
}); });
}); });
void InternalHooksManager.getInstance().onCommunityPackageUpdateFinished({ void Container.get(InternalHooks).onCommunityPackageUpdateFinished({
user: req.user, user: req.user,
package_name: name, package_name: name,
package_version_current: previouslyInstalledPackage.installedVersion, package_version_current: previouslyInstalledPackage.installedVersion,
@ -325,7 +326,7 @@ nodesController.patch(
return newInstalledPackage; return newInstalledPackage;
} catch (error) { } catch (error) {
previouslyInstalledPackage.installedNodes.forEach((node) => { previouslyInstalledPackage.installedNodes.forEach((node) => {
const pushInstance = getPushInstance(); const pushInstance = Container.get(Push);
pushInstance.send('removeNodeType', { pushInstance.send('removeNodeType', {
name: node.type, name: node.type,
version: node.latestVersion, version: node.latestVersion,

View file

@ -9,15 +9,14 @@ import express from 'express';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import type { IExternalHooksClass, ITagWithCountDb } from '@/Interfaces'; import type { ITagWithCountDb } from '@/Interfaces';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import config from '@/config'; import config from '@/config';
import * as TagHelpers from '@/TagHelpers'; import * as TagHelpers from '@/TagHelpers';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { TagEntity } from '@db/entities/TagEntity'; import { TagEntity } from '@db/entities/TagEntity';
import type { TagsRequest } from '@/requests'; import type { TagsRequest } from '@/requests';
import { Container } from 'typedi';
export const externalHooks: IExternalHooksClass = ExternalHooks();
export const tagsController = express.Router(); export const tagsController = express.Router();
@ -50,12 +49,12 @@ tagsController.post(
const newTag = new TagEntity(); const newTag = new TagEntity();
newTag.name = req.body.name.trim(); newTag.name = req.body.name.trim();
await externalHooks.run('tag.beforeCreate', [newTag]); await Container.get(ExternalHooks).run('tag.beforeCreate', [newTag]);
await validateEntity(newTag); await validateEntity(newTag);
const tag = await Db.collections.Tag.save(newTag); const tag = await Db.collections.Tag.save(newTag);
await externalHooks.run('tag.afterCreate', [tag]); await Container.get(ExternalHooks).run('tag.afterCreate', [tag]);
return tag; return tag;
}), }),
@ -74,12 +73,12 @@ tagsController.patch(
newTag.id = id; newTag.id = id;
newTag.name = name.trim(); newTag.name = name.trim();
await externalHooks.run('tag.beforeUpdate', [newTag]); await Container.get(ExternalHooks).run('tag.beforeUpdate', [newTag]);
await validateEntity(newTag); await validateEntity(newTag);
const tag = await Db.collections.Tag.save(newTag); const tag = await Db.collections.Tag.save(newTag);
await externalHooks.run('tag.afterUpdate', [tag]); await Container.get(ExternalHooks).run('tag.afterUpdate', [tag]);
return tag; return tag;
}), }),
@ -100,11 +99,11 @@ tagsController.delete(
} }
const id = req.params.id; const id = req.params.id;
await externalHooks.run('tag.beforeDelete', [id]); await Container.get(ExternalHooks).run('tag.beforeDelete', [id]);
await Db.collections.Tag.delete({ id }); await Db.collections.Tag.delete({ id });
await externalHooks.run('tag.afterDelete', [id]); await Container.get(ExternalHooks).run('tag.afterDelete', [id]);
return true; return true;
}), }),

View file

@ -12,6 +12,7 @@ import {
} from '@/audit/constants'; } from '@/audit/constants';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { Risk } from '@/audit/types'; import type { Risk } from '@/audit/types';
import { Container } from 'typedi';
async function getCommunityNodeDetails() { async function getCommunityNodeDetails() {
const installedPackages = await getAllInstalledPackages(); const installedPackages = await getAllInstalledPackages();
@ -32,7 +33,8 @@ async function getCommunityNodeDetails() {
async function getCustomNodeDetails() { async function getCustomNodeDetails() {
const customNodeTypes: Risk.CustomNodeDetails[] = []; const customNodeTypes: Risk.CustomNodeDetails[] = [];
for (const customDir of LoadNodesAndCredentials().getCustomDirectories()) { const nodesAndCredentials = Container.get(LoadNodesAndCredentials);
for (const customDir of nodesAndCredentials.getCustomDirectories()) {
const customNodeFiles = await glob('**/*.node.js', { cwd: customDir, absolute: true }); const customNodeFiles = await glob('**/*.node.js', { cwd: customDir, absolute: true });
for (const nodeFile of customNodeFiles) { for (const nodeFile of customNodeFiles) {

View file

@ -1,8 +1,9 @@
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { compareHash } from '@/UserManagement/UserManagementHelper'; import { compareHash } from '@/UserManagement/UserManagementHelper';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export const handleEmailLogin = async ( export const handleEmailLogin = async (
email: string, email: string,
@ -21,7 +22,7 @@ export const handleEmailLogin = async (
// so suggest to reset the password to gain access to the instance. // so suggest to reset the password to gain access to the instance.
const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap'); const ldapIdentity = user?.authIdentities?.find((i) => i.providerType === 'ldap');
if (user && ldapIdentity) { if (user && ldapIdentity) {
void InternalHooksManager.getInstance().userLoginFailedDueToLdapDisabled({ void Container.get(InternalHooks).userLoginFailedDueToLdapDisabled({
user_id: user.id, user_id: user.id,
}); });

View file

@ -1,4 +1,4 @@
import { InternalHooksManager } from '@/InternalHooksManager'; import { InternalHooks } from '@/InternalHooks';
import { import {
createLdapUserOnLocalDb, createLdapUserOnLocalDb,
findAndAuthenticateLdapUser, findAndAuthenticateLdapUser,
@ -12,6 +12,7 @@ import {
updateLdapUserOnLocalDb, updateLdapUserOnLocalDb,
} from '@/Ldap/helpers'; } from '@/Ldap/helpers';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { Container } from 'typedi';
export const handleLdapLogin = async ( export const handleLdapLogin = async (
loginId: string, loginId: string,
@ -51,7 +52,7 @@ export const handleLdapLogin = async (
} else { } else {
const role = await getLdapUserRole(); const role = await getLdapUserRole();
const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId); const user = await createLdapUserOnLocalDb(role, ldapAttributesValues, ldapId);
void InternalHooksManager.getInstance().onUserSignup(user, { void Container.get(InternalHooks).onUserSignup(user, {
user_type: 'ldap', user_type: 'ldap',
was_disabled_ldap_user: false, was_disabled_ldap_user: false,
}); });

View file

@ -1,5 +1,6 @@
import { Command } from '@oclif/command'; import { Command } from '@oclif/command';
import { ExitError } from '@oclif/errors'; import { ExitError } from '@oclif/errors';
import { Container } from 'typedi';
import type { INodeTypes } from 'n8n-workflow'; import type { INodeTypes } from 'n8n-workflow';
import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow'; import { LoggerProxy, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import type { IUserSettings } from 'n8n-core'; import type { IUserSettings } from 'n8n-core';
@ -11,13 +12,13 @@ import * as CrashJournal from '@/CrashJournal';
import { inTest } from '@/constants'; import { inTest } from '@/constants';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { InternalHooksManager } from '@/InternalHooksManager';
import { initErrorHandling } from '@/ErrorReporting'; import { initErrorHandling } from '@/ErrorReporting';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import type { LoadNodesAndCredentialsClass } from '@/LoadNodesAndCredentials';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { IExternalHooksClass } from '@/Interfaces'; import type { IExternalHooksClass } from '@/Interfaces';
import { InternalHooks } from '@/InternalHooks';
import { PostHogClient } from '@/posthog';
export const UM_FIX_INSTRUCTION = export const UM_FIX_INSTRUCTION =
'Please fix the database by running ./packages/cli/bin/n8n user-management:reset'; 'Please fix the database by running ./packages/cli/bin/n8n user-management:reset';
@ -27,7 +28,7 @@ export abstract class BaseCommand extends Command {
protected externalHooks: IExternalHooksClass; protected externalHooks: IExternalHooksClass;
protected loadNodesAndCredentials: LoadNodesAndCredentialsClass; protected loadNodesAndCredentials: LoadNodesAndCredentials;
protected nodeTypes: INodeTypes; protected nodeTypes: INodeTypes;
@ -42,13 +43,15 @@ export abstract class BaseCommand extends Command {
// Make sure the settings exist // Make sure the settings exist
this.userSettings = await UserSettings.prepareUserSettings(); this.userSettings = await UserSettings.prepareUserSettings();
this.loadNodesAndCredentials = LoadNodesAndCredentials(); this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
await this.loadNodesAndCredentials.init(); await this.loadNodesAndCredentials.init();
this.nodeTypes = NodeTypes(this.loadNodesAndCredentials); this.nodeTypes = Container.get(NodeTypes);
const credentialTypes = CredentialTypes(this.loadNodesAndCredentials); const credentialTypes = Container.get(CredentialTypes);
CredentialsOverwrites(credentialTypes); CredentialsOverwrites(credentialTypes);
await InternalHooksManager.init(this.userSettings.instanceId ?? '', this.nodeTypes); const instanceId = this.userSettings.instanceId ?? '';
await Container.get(PostHogClient).init(instanceId);
await Container.get(InternalHooks).init(instanceId);
await Db.init().catch(async (error: Error) => await Db.init().catch(async (error: Error) =>
this.exitWithCrash('There was an error initializing DB', error), this.exitWithCrash('There was an error initializing DB', error),
@ -83,7 +86,7 @@ export abstract class BaseCommand extends Command {
} }
protected async initExternalHooks() { protected async initExternalHooks() {
this.externalHooks = ExternalHooks(); this.externalHooks = Container.get(ExternalHooks);
await this.externalHooks.init(); await this.externalHooks.init();
} }

View file

@ -1,10 +1,11 @@
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import { audit } from '@/audit'; import { audit } from '@/audit';
import { RISK_CATEGORIES } from '@/audit/constants'; import { RISK_CATEGORIES } from '@/audit/constants';
import { InternalHooksManager } from '@/InternalHooksManager';
import config from '@/config'; import config from '@/config';
import type { Risk } from '@/audit/types'; import type { Risk } from '@/audit/types';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export class SecurityAudit extends BaseCommand { export class SecurityAudit extends BaseCommand {
static description = 'Generate a security audit report for this n8n instance'; static description = 'Generate a security audit report for this n8n instance';
@ -56,7 +57,7 @@ export class SecurityAudit extends BaseCommand {
process.stdout.write(JSON.stringify(result, null, 2)); process.stdout.write(JSON.stringify(result, null, 2));
} }
void InternalHooksManager.getInstance().onAuditGeneratedViaCli(); void Container.get(InternalHooks).onAuditGeneratedViaCli();
} }
async catch(error: Error) { async catch(error: Error) {

View file

@ -4,7 +4,7 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
import type { IWorkflowBase } from 'n8n-workflow'; import type { IWorkflowBase } from 'n8n-workflow';
import { ExecutionBaseError } from 'n8n-workflow'; import { ExecutionBaseError } from 'n8n-workflow';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
@ -13,6 +13,7 @@ import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
import { findCliWorkflowStart } from '@/utils'; import { findCliWorkflowStart } from '@/utils';
import { initEvents } from '@/events'; import { initEvents } from '@/events';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
export class Execute extends BaseCommand { export class Execute extends BaseCommand {
static description = '\nExecutes a given workflow'; static description = '\nExecutes a given workflow';
@ -117,7 +118,7 @@ export class Execute extends BaseCommand {
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData); const executionId = await workflowRunner.run(runData);
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = Container.get(ActiveExecutions);
const data = await activeExecutions.getPostExecutePromise(executionId); const data = await activeExecutions.getPostExecutePromise(executionId);
if (data === undefined) { if (data === undefined) {

View file

@ -7,7 +7,7 @@ import { sep } from 'path';
import { diff } from 'json-diff'; import { diff } from 'json-diff';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces'; import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
@ -16,6 +16,7 @@ import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
import { findCliWorkflowStart } from '@/utils'; import { findCliWorkflowStart } from '@/utils';
import { initEvents } from '@/events'; import { initEvents } from '@/events';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
const re = /\d+/; const re = /\d+/;
@ -101,7 +102,7 @@ export class ExecuteBatch extends BaseCommand {
} }
ExecuteBatch.cancelled = true; ExecuteBatch.cancelled = true;
const activeExecutionsInstance = ActiveExecutions.getInstance(); const activeExecutionsInstance = Container.get(ActiveExecutions);
const stopPromises = activeExecutionsInstance const stopPromises = activeExecutionsInstance
.getActiveExecutions() .getActiveExecutions()
.map(async (execution) => activeExecutionsInstance.stopExecution(execution.id)); .map(async (execution) => activeExecutionsInstance.stopExecution(execution.id));
@ -597,7 +598,7 @@ export class ExecuteBatch extends BaseCommand {
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData); const executionId = await workflowRunner.run(runData);
const activeExecutions = ActiveExecutions.getInstance(); const activeExecutions = Container.get(ActiveExecutions);
const data = await activeExecutions.getPostExecutePromise(executionId); const data = await activeExecutions.getPostExecutePromise(executionId);
if (gotCancel || ExecuteBatch.cancelled) { if (gotCancel || ExecuteBatch.cancelled) {
clearTimeout(timeoutTimer); clearTimeout(timeoutTimer);

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/await-thenable */ /* eslint-disable @typescript-eslint/await-thenable */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Container } from 'typedi';
import path from 'path'; import path from 'path';
import { mkdir } from 'fs/promises'; import { mkdir } from 'fs/promises';
import { createReadStream, createWriteStream, existsSync } from 'fs'; import { createReadStream, createWriteStream, existsSync } from 'fs';
@ -16,27 +17,23 @@ import { LoggerProxy, sleep, jsonParse } from 'n8n-workflow';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import config from '@/config'; import config from '@/config';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as Server from '@/Server'; import * as Server from '@/Server';
import * as TestWebhooks from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
import { WaitTracker } from '@/WaitTracker';
import { getAllInstalledPackages } from '@/CommunityNodes/packageModel'; import { getAllInstalledPackages } from '@/CommunityNodes/packageModel';
import { handleLdapInit } from '@/Ldap/helpers'; import { handleLdapInit } from '@/Ldap/helpers';
import { createPostHogLoadingScript } from '@/telemetry/scripts';
import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants'; import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR } from '@/constants';
import { eventBus } from '@/eventbus'; import { eventBus } from '@/eventbus';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { InternalHooks } from '@/InternalHooks';
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires
const open = require('open'); const open = require('open');
const pipeline = promisify(stream.pipeline); const pipeline = promisify(stream.pipeline);
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
export class Start extends BaseCommand { export class Start extends BaseCommand {
static description = 'Starts n8n. Makes Web-UI available and starts active workflows'; static description = 'Starts n8n. Makes Web-UI available and starts active workflows';
@ -63,6 +60,8 @@ export class Start extends BaseCommand {
}), }),
}; };
protected activeWorkflowRunner = Container.get(ActiveWorkflowRunner);
/** /**
* Opens the UI in browser * Opens the UI in browser
*/ */
@ -87,7 +86,7 @@ export class Start extends BaseCommand {
try { try {
// Stop with trying to activate workflows that could not be activated // Stop with trying to activate workflows that could not be activated
activeWorkflowRunner?.removeAllQueuedWorkflowActivations(); this.activeWorkflowRunner.removeAllQueuedWorkflowActivations();
await this.externalHooks.run('n8n.stop', []); await this.externalHooks.run('n8n.stop', []);
@ -98,25 +97,25 @@ export class Start extends BaseCommand {
await this.exitSuccessFully(); await this.exitSuccessFully();
}, 30000); }, 30000);
await InternalHooksManager.getInstance().onN8nStop(); await Container.get(InternalHooks).onN8nStop();
const skipWebhookDeregistration = config.getEnv( const skipWebhookDeregistration = config.getEnv(
'endpoints.skipWebhooksDeregistrationOnShutdown', 'endpoints.skipWebhooksDeregistrationOnShutdown',
); );
const removePromises = []; const removePromises = [];
if (activeWorkflowRunner !== undefined && !skipWebhookDeregistration) { if (!skipWebhookDeregistration) {
removePromises.push(activeWorkflowRunner.removeAll()); removePromises.push(this.activeWorkflowRunner.removeAll());
} }
// Remove all test webhooks // Remove all test webhooks
const testWebhooks = TestWebhooks.getInstance(); const testWebhooks = Container.get(TestWebhooks);
removePromises.push(testWebhooks.removeAll()); removePromises.push(testWebhooks.removeAll());
await Promise.all(removePromises); await Promise.all(removePromises);
// Wait for active workflow executions to finish // Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions.getInstance(); const activeExecutionsInstance = Container.get(ActiveExecutions);
let executingWorkflows = activeExecutionsInstance.getActiveExecutions(); let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
let count = 0; let count = 0;
@ -154,20 +153,6 @@ export class Start extends BaseCommand {
}, ''); }, '');
} }
if (config.getEnv('diagnostics.enabled')) {
const phLoadingScript = createPostHogLoadingScript({
apiKey: config.getEnv('diagnostics.config.posthog.apiKey'),
apiHost: config.getEnv('diagnostics.config.posthog.apiHost'),
autocapture: false,
disableSessionRecording: config.getEnv(
'diagnostics.config.posthog.disableSessionRecording',
),
debug: config.getEnv('logs.level') === 'debug',
});
scriptsString += phLoadingScript;
}
const closingTitleTag = '</title>'; const closingTitleTag = '</title>';
const compileFile = async (fileName: string) => { const compileFile = async (fileName: string) => {
const filePath = path.join(EDITOR_UI_DIST_DIR, fileName); const filePath = path.join(EDITOR_UI_DIST_DIR, fileName);
@ -344,10 +329,7 @@ export class Start extends BaseCommand {
await Server.start(); await Server.start();
// Start to get active workflows and run their triggers // Start to get active workflows and run their triggers
activeWorkflowRunner = ActiveWorkflowRunner.getInstance(); await this.activeWorkflowRunner.init();
await activeWorkflowRunner.init();
WaitTracker();
await handleLdapInit(); await handleLdapInit();
@ -393,6 +375,7 @@ export class Start extends BaseCommand {
} }
async catch(error: Error) { async catch(error: Error) {
console.log(error.stack);
await this.exitWithCrash('Exiting due to an error.', error); await this.exitWithCrash('Exiting due to an error.', error);
} }
} }

View file

@ -1,9 +1,10 @@
import { flags } from '@oclif/command'; import { flags } from '@oclif/command';
import { LoggerProxy, sleep } from 'n8n-workflow'; import { LoggerProxy, sleep } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { WebhookServer } from '@/WebhookServer'; import { WebhookServer } from '@/WebhookServer';
import { BaseCommand } from './BaseCommand'; import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
export class Webhook extends BaseCommand { export class Webhook extends BaseCommand {
static description = 'Starts n8n webhook process. Intercepts only production URLs.'; static description = 'Starts n8n webhook process. Intercepts only production URLs.';
@ -32,7 +33,7 @@ export class Webhook extends BaseCommand {
}, 30000); }, 30000);
// Wait for active workflow executions to finish // Wait for active workflow executions to finish
const activeExecutionsInstance = ActiveExecutions.getInstance(); const activeExecutionsInstance = Container.get(ActiveExecutions);
let executingWorkflows = activeExecutionsInstance.getActiveExecutions(); let executingWorkflows = activeExecutionsInstance.getActiveExecutions();
let count = 0; let count = 0;

View file

@ -1,7 +1,7 @@
import validator from 'validator'; import validator from 'validator';
import { Get, Post, RestController } from '@/decorators'; import { Get, Post, RestController } from '@/decorators';
import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper'; import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper';
import { sanitizeUser } from '@/UserManagement/UserManagementHelper'; import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper';
import { issueCookie, resolveJwt } from '@/auth/jwt'; import { issueCookie, resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
@ -11,8 +11,14 @@ import { LoginRequest, UserRequest } from '@/requests';
import type { Repository } from 'typeorm'; import type { Repository } from 'typeorm';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Config } from '@/config'; import type { Config } from '@/config';
import type { PublicUser, IDatabaseCollections, IInternalHooksClass } from '@/Interfaces'; import type {
PublicUser,
IDatabaseCollections,
IInternalHooksClass,
CurrentUser,
} from '@/Interfaces';
import { handleEmailLogin, handleLdapLogin } from '@/auth'; import { handleEmailLogin, handleLdapLogin } from '@/auth';
import type { PostHogClient } from '@/posthog';
@RestController() @RestController()
export class AuthController { export class AuthController {
@ -24,21 +30,26 @@ export class AuthController {
private readonly userRepository: Repository<User>; private readonly userRepository: Repository<User>;
private readonly postHog?: PostHogClient;
constructor({ constructor({
config, config,
logger, logger,
internalHooks, internalHooks,
repositories, repositories,
postHog,
}: { }: {
config: Config; config: Config;
logger: ILogger; logger: ILogger;
internalHooks: IInternalHooksClass; internalHooks: IInternalHooksClass;
repositories: Pick<IDatabaseCollections, 'User'>; repositories: Pick<IDatabaseCollections, 'User'>;
postHog?: PostHogClient;
}) { }) {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
this.internalHooks = internalHooks; this.internalHooks = internalHooks;
this.userRepository = repositories.User; this.userRepository = repositories.User;
this.postHog = postHog;
} }
/** /**
@ -56,7 +67,7 @@ export class AuthController {
if (user) { if (user) {
await issueCookie(res, user); await issueCookie(res, user);
return sanitizeUser(user); return withFeatureFlags(this.postHog, sanitizeUser(user));
} }
throw new AuthError('Wrong username or password. Do you have caps lock on?'); throw new AuthError('Wrong username or password. Do you have caps lock on?');
@ -66,7 +77,7 @@ export class AuthController {
* Manually check the `n8n-auth` cookie. * Manually check the `n8n-auth` cookie.
*/ */
@Get('/login') @Get('/login')
async currentUser(req: Request, res: Response): Promise<PublicUser> { async currentUser(req: Request, res: Response): Promise<CurrentUser> {
// Manually check the existing cookie. // Manually check the existing cookie.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const cookieContents = req.cookies?.[AUTH_COOKIE_NAME] as string | undefined; const cookieContents = req.cookies?.[AUTH_COOKIE_NAME] as string | undefined;
@ -76,7 +87,7 @@ export class AuthController {
// If logged in, return user // If logged in, return user
try { try {
user = await resolveJwt(cookieContents); user = await resolveJwt(cookieContents);
return sanitizeUser(user); return await withFeatureFlags(this.postHog, sanitizeUser(user));
} catch (error) { } catch (error) {
res.clearCookie(AUTH_COOKIE_NAME); res.clearCookie(AUTH_COOKIE_NAME);
} }
@ -102,7 +113,7 @@ export class AuthController {
} }
await issueCookie(res, user); await issueCookie(res, user);
return sanitizeUser(user); return withFeatureFlags(this.postHog, sanitizeUser(user));
} }
/** /**

View file

@ -2,4 +2,5 @@ export { AuthController } from './auth.controller';
export { MeController } from './me.controller'; export { MeController } from './me.controller';
export { OwnerController } from './owner.controller'; export { OwnerController } from './owner.controller';
export { PasswordResetController } from './passwordReset.controller'; export { PasswordResetController } from './passwordReset.controller';
export { TranslationController } from './translation.controller';
export { UsersController } from './users.controller'; export { UsersController } from './users.controller';

View file

@ -1,4 +1,5 @@
import validator from 'validator'; import validator from 'validator';
import { plainToInstance } from 'class-transformer';
import { Delete, Get, Patch, Post, RestController } from '@/decorators'; import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import { import {
compareHash, compareHash,
@ -7,13 +8,13 @@ import {
validatePassword, validatePassword,
} from '@/UserManagement/UserManagementHelper'; } from '@/UserManagement/UserManagementHelper';
import { BadRequestError } from '@/ResponseHelper'; import { BadRequestError } from '@/ResponseHelper';
import { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { Response } from 'express'; import { Response } from 'express';
import type { Repository } from 'typeorm'; import type { Repository } from 'typeorm';
import type { ILogger } from 'n8n-workflow'; import type { ILogger } from 'n8n-workflow';
import { AuthenticatedRequest, MeRequest } from '@/requests'; import { AuthenticatedRequest, MeRequest, UserUpdatePayload } from '@/requests';
import type { import type {
PublicUser, PublicUser,
IDatabaseCollections, IDatabaseCollections,
@ -53,38 +54,40 @@ export class MeController {
* Update the logged-in user's settings, except password. * Update the logged-in user's settings, except password.
*/ */
@Patch('/') @Patch('/')
async updateCurrentUser(req: MeRequest.Settings, res: Response): Promise<PublicUser> { async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
const { email } = req.body; const { id: userId, email: currentEmail } = req.user;
const payload = plainToInstance(UserUpdatePayload, req.body);
const { email } = payload;
if (!email) { if (!email) {
this.logger.debug('Request to update user email failed because of missing email in payload', { this.logger.debug('Request to update user email failed because of missing email in payload', {
userId: req.user.id, userId,
payload: req.body, payload,
}); });
throw new BadRequestError('Email is mandatory'); throw new BadRequestError('Email is mandatory');
} }
if (!validator.isEmail(email)) { if (!validator.isEmail(email)) {
this.logger.debug('Request to update user email failed because of invalid email in payload', { this.logger.debug('Request to update user email failed because of invalid email in payload', {
userId: req.user.id, userId,
invalidEmail: email, invalidEmail: email,
}); });
throw new BadRequestError('Invalid email address'); throw new BadRequestError('Invalid email address');
} }
const { email: currentEmail } = req.user; await validateEntity(payload);
const newUser = new User();
Object.assign(newUser, req.user, req.body); await this.userRepository.update(userId, payload);
const user = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: { globalRole: true },
});
await validateEntity(newUser); this.logger.info('User updated successfully', { userId });
const user = await this.userRepository.save(newUser);
this.logger.info('User updated successfully', { userId: user.id });
await issueCookie(res, user); await issueCookie(res, user);
const updatedKeys = Object.keys(req.body); const updatedKeys = Object.keys(payload);
void this.internalHooks.onUserUpdate({ void this.internalHooks.onUserUpdate({
user, user,
fields_changed: updatedKeys, fields_changed: updatedKeys,

View file

@ -0,0 +1,58 @@
import type { Request } from 'express';
import { ICredentialTypes } from 'n8n-workflow';
import { join } from 'path';
import { access } from 'fs/promises';
import { Get, RestController } from '@/decorators';
import { BadRequestError, InternalServerError } from '@/ResponseHelper';
import { Config } from '@/config';
import { NODES_BASE_DIR } from '@/constants';
export const CREDENTIAL_TRANSLATIONS_DIR = 'n8n-nodes-base/dist/credentials/translations';
export const NODE_HEADERS_PATH = join(NODES_BASE_DIR, 'dist/nodes/headers');
export declare namespace TranslationRequest {
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
}
@RestController('/')
export class TranslationController {
constructor(private config: Config, private credentialTypes: ICredentialTypes) {}
@Get('/credential-translation')
async getCredentialTranslation(req: TranslationRequest.Credential) {
const { credentialType } = req.query;
if (!this.credentialTypes.recognizes(credentialType))
throw new BadRequestError(`Invalid Credential type: "${credentialType}"`);
const defaultLocale = this.config.getEnv('defaultLocale');
const translationPath = join(
CREDENTIAL_TRANSLATIONS_DIR,
defaultLocale,
`${credentialType}.json`,
);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return require(translationPath);
} catch (error) {
return null;
}
}
@Get('/node-translation-headers')
async getNodeTranslationHeaders() {
try {
await access(`${NODE_HEADERS_PATH}.js`);
} catch (_) {
return; // no headers available
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return require(NODE_HEADERS_PATH);
} catch (error) {
throw new InternalServerError('Failed to load headers file');
}
}
}

View file

@ -16,6 +16,7 @@ import {
isUserManagementEnabled, isUserManagementEnabled,
sanitizeUser, sanitizeUser,
validatePassword, validatePassword,
withFeatureFlags,
} from '@/UserManagement/UserManagementHelper'; } from '@/UserManagement/UserManagementHelper';
import { issueCookie } from '@/auth/jwt'; import { issueCookie } from '@/auth/jwt';
import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper'; import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper';
@ -33,6 +34,7 @@ import type {
} from '@/Interfaces'; } from '@/Interfaces';
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { AuthIdentity } from '@db/entities/AuthIdentity'; import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { PostHogClient } from '@/posthog';
@RestController('/users') @RestController('/users')
export class UsersController { export class UsersController {
@ -56,6 +58,8 @@ export class UsersController {
private mailer: UserManagementMailer; private mailer: UserManagementMailer;
private postHog?: PostHogClient;
constructor({ constructor({
config, config,
logger, logger,
@ -64,6 +68,7 @@ export class UsersController {
repositories, repositories,
activeWorkflowRunner, activeWorkflowRunner,
mailer, mailer,
postHog,
}: { }: {
config: Config; config: Config;
logger: ILogger; logger: ILogger;
@ -75,6 +80,7 @@ export class UsersController {
>; >;
activeWorkflowRunner: ActiveWorkflowRunner; activeWorkflowRunner: ActiveWorkflowRunner;
mailer: UserManagementMailer; mailer: UserManagementMailer;
postHog?: PostHogClient;
}) { }) {
this.config = config; this.config = config;
this.logger = logger; this.logger = logger;
@ -86,6 +92,7 @@ export class UsersController {
this.sharedWorkflowRepository = repositories.SharedWorkflow; this.sharedWorkflowRepository = repositories.SharedWorkflow;
this.activeWorkflowRunner = activeWorkflowRunner; this.activeWorkflowRunner = activeWorkflowRunner;
this.mailer = mailer; this.mailer = mailer;
this.postHog = postHog;
} }
/** /**
@ -327,7 +334,7 @@ export class UsersController {
await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]); await this.externalHooks.run('user.profile.update', [invitee.email, sanitizeUser(invitee)]);
await this.externalHooks.run('user.password.update', [invitee.email, invitee.password]); await this.externalHooks.run('user.password.update', [invitee.email, invitee.password]);
return sanitizeUser(updatedUser); return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
} }
@Get('/') @Get('/')

View file

@ -2,7 +2,6 @@ import express from 'express';
import type { INodeCredentialTestResult } from 'n8n-workflow'; import type { INodeCredentialTestResult } from 'n8n-workflow';
import { deepCopy, LoggerProxy } from 'n8n-workflow'; import { deepCopy, LoggerProxy } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
@ -10,6 +9,8 @@ import type { CredentialRequest } from '@/requests';
import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper'; import { isSharingEnabled, rightDiff } from '@/UserManagement/UserManagementHelper';
import { EECredentialsService as EECredentials } from './credentials.service.ee'; import { EECredentialsService as EECredentials } from './credentials.service.ee';
import type { CredentialWithSharings } from './credentials.types'; import type { CredentialWithSharings } from './credentials.types';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export const EECredentialsController = express.Router(); export const EECredentialsController = express.Router();
@ -174,7 +175,7 @@ EECredentialsController.put(
} }
}); });
void InternalHooksManager.getInstance().onUserSharedCredentials({ void Container.get(InternalHooks).onUserSharedCredentials({
user: req.user, user: req.user,
credential_name: credential.name, credential_name: credential.name,
credential_type: credential.type, credential_type: credential.type,

View file

@ -5,7 +5,6 @@ import type { INodeCredentialTestResult } from 'n8n-workflow';
import { deepCopy, LoggerProxy } from 'n8n-workflow'; import { deepCopy, LoggerProxy } from 'n8n-workflow';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import config from '@/config'; import config from '@/config';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
@ -14,6 +13,8 @@ import { CredentialsService } from './credentials.service';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
import type { CredentialRequest } from '@/requests'; import type { CredentialRequest } from '@/requests';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export const credentialsController = express.Router(); export const credentialsController = express.Router();
@ -130,7 +131,7 @@ credentialsController.post(
const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential); const encryptedData = CredentialsService.createEncryptedData(key, null, newCredential);
const credential = await CredentialsService.save(newCredential, encryptedData, req.user); const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
void InternalHooksManager.getInstance().onUserCreatedCredentials({ void Container.get(InternalHooks).onUserCreatedCredentials({
user: req.user, user: req.user,
credential_name: newCredential.name, credential_name: newCredential.name,
credential_type: credential.type, credential_type: credential.type,

View file

@ -24,6 +24,7 @@ import { ExternalHooks } from '@/ExternalHooks';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { CredentialRequest } from '@/requests'; import type { CredentialRequest } from '@/requests';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { Container } from 'typedi';
export class CredentialsService { export class CredentialsService {
static async get( static async get(
@ -205,7 +206,7 @@ export class CredentialsService {
credentialId: string, credentialId: string,
newCredentialData: ICredentialsDb, newCredentialData: ICredentialsDb,
): Promise<ICredentialsDb | null> { ): Promise<ICredentialsDb | null> {
await ExternalHooks().run('credentials.update', [newCredentialData]); await Container.get(ExternalHooks).run('credentials.update', [newCredentialData]);
// Update the credentials in DB // Update the credentials in DB
await Db.collections.Credentials.update(credentialId, newCredentialData); await Db.collections.Credentials.update(credentialId, newCredentialData);
@ -224,7 +225,7 @@ export class CredentialsService {
const newCredential = new CredentialsEntity(); const newCredential = new CredentialsEntity();
Object.assign(newCredential, credential, encryptedData); Object.assign(newCredential, credential, encryptedData);
await ExternalHooks().run('credentials.create', [encryptedData]); await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
const role = await Db.collections.Role.findOneByOrFail({ const role = await Db.collections.Role.findOneByOrFail({
name: 'owner', name: 'owner',
@ -256,7 +257,7 @@ export class CredentialsService {
} }
static async delete(credentials: CredentialsEntity): Promise<void> { static async delete(credentials: CredentialsEntity): Promise<void> {
await ExternalHooks().run('credentials.delete', [credentials.id]); await Container.get(ExternalHooks).run('credentials.delete', [credentials.id]);
await Db.collections.Credentials.remove(credentials); await Db.collections.Credentials.remove(credentials);
} }
@ -279,7 +280,7 @@ export class CredentialsService {
): ICredentialDataDecryptedObject { ): ICredentialDataDecryptedObject {
const copiedData = deepCopy(data); const copiedData = deepCopy(data);
const credTypes = CredentialTypes(); const credTypes = Container.get(CredentialTypes);
let credType: ICredentialType; let credType: ICredentialType;
try { try {
credType = credTypes.getByName(credential.type); credType = credTypes.getByName(credential.type);

View file

@ -30,6 +30,7 @@ import type { OAuthRequest } from '@/requests';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import config from '@/config'; import config from '@/config';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
export const oauth2CredentialController = express.Router(); export const oauth2CredentialController = express.Router();
@ -129,7 +130,7 @@ oauth2CredentialController.get(
state: stateEncodedStr, state: stateEncodedStr,
}; };
await ExternalHooks().run('oauth2.authenticate', [oAuthOptions]); await Container.get(ExternalHooks).run('oauth2.authenticate', [oAuthOptions]);
const oAuthObj = new ClientOAuth2(oAuthOptions); const oAuthObj = new ClientOAuth2(oAuthOptions);
@ -281,7 +282,7 @@ oauth2CredentialController.get(
delete oAuth2Parameters.clientSecret; delete oAuth2Parameters.clientSecret;
} }
await ExternalHooks().run('oauth2.callback', [oAuth2Parameters]); await Container.get(ExternalHooks).run('oauth2.callback', [oAuth2Parameters]);
const oAuthObj = new ClientOAuth2(oAuth2Parameters); const oAuthObj = new ClientOAuth2(oAuth2Parameters);

View file

@ -111,6 +111,9 @@ export class User extends AbstractEntity implements IUser {
@AfterLoad() @AfterLoad()
@AfterUpdate() @AfterUpdate()
computeIsPending(): void { computeIsPending(): void {
this.isPending = this.password === null; this.isPending =
this.globalRole?.name === 'owner' && this.globalRole.scope === 'global'
? false
: this.password === null;
} }
} }

View file

@ -3,6 +3,7 @@ import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/mi
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { IConnections, INode } from 'n8n-workflow'; import { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface { export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449'; name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -21,7 +22,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM \`${tablePrefix}workflow_entity\` FROM \`${tablePrefix}workflow_entity\`
`); `);
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => { workflows.forEach(async (workflow) => {
let connections: IConnections = let connections: IConnections =
@ -57,22 +58,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!nodesThatCannotReceiveInput.includes(outgoingConnections.node), !nodesThatCannotReceiveInput.includes(outgoingConnections.node),
); );
}); });
// Filter out output connection items that are empty
connection[outputConnectionName] = connection[outputConnectionName].filter(
(item) => item.length > 0,
);
// Delete the output connection container if it is empty
if (connection[outputConnectionName].length === 0) {
delete connection[outputConnectionName];
}
}); });
// Finally delete the source node if it has no output connections
if (Object.keys(connection).length === 0) {
delete connections[sourceNodeName];
}
}); });
// Update database with new connections // Update database with new connections

View file

@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
export class MigrateExecutionStatus1676996103000 implements MigrationInterface {
name = 'MigrateExecutionStatus1676996103000';
public async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);
const tablePrefix = config.getEnv('database.tablePrefix');
await queryRunner.query(
`UPDATE \`${tablePrefix}execution_entity\` SET status='waiting' WHERE status IS NULL AND \`waitTill\` IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE \`${tablePrefix}execution_entity\` SET status='failed' WHERE status IS NULL AND finished=0 AND \`stoppedAt\` IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE \`${tablePrefix}execution_entity\` SET status='success' WHERE status IS NULL AND finished=1 AND \`stoppedAt\` IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE \`${tablePrefix}execution_entity\` SET status='crashed' WHERE status IS NULL;`,
);
logMigrationEnd(this.name);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -32,6 +32,7 @@ import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-Dele
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities'; import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections'; import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable'; import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable';
export const mysqlMigrations = [ export const mysqlMigrations = [
@ -69,6 +70,7 @@ export const mysqlMigrations = [
CreateLdapEntities1674509946020, CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449, PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000, AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
PurgeInvalidWorkflowConnections1675940580449, PurgeInvalidWorkflowConnections1675940580449,
CreateExecutionMetadataTable1674133106779, CreateExecutionMetadataTable1674133106779,
]; ];

View file

@ -3,6 +3,7 @@ import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/mi
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { IConnections, INode } from 'n8n-workflow'; import { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface { export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449'; name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -17,7 +18,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM "${tablePrefix}workflow_entity" FROM "${tablePrefix}workflow_entity"
`); `);
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => { workflows.forEach(async (workflow) => {
let connections: IConnections = workflow.connections; let connections: IConnections = workflow.connections;
@ -49,22 +50,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!nodesThatCannotReceiveInput.includes(outgoingConnections.node), !nodesThatCannotReceiveInput.includes(outgoingConnections.node),
); );
}); });
// Filter out output connection items that are empty
connection[outputConnectionName] = connection[outputConnectionName].filter(
(item) => item.length > 0,
);
// Delete the output connection container if it is empty
if (connection[outputConnectionName].length === 0) {
delete connection[outputConnectionName];
}
}); });
// Finally delete the source node if it has no output connections
if (Object.keys(connection).length === 0) {
delete connections[sourceNodeName];
}
}); });
// Update database with new connections // Update database with new connections

View file

@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
export class MigrateExecutionStatus1676996103000 implements MigrationInterface {
name = 'MigrateExecutionStatus1676996103000';
public async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);
const tablePrefix = config.getEnv('database.tablePrefix');
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'waiting' WHERE "status" IS NULL AND "waitTill" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'failed' WHERE "status" IS NULL AND "finished"=false AND "stoppedAt" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'success' WHERE "status" IS NULL AND "finished"=true AND "stoppedAt" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'crashed' WHERE "status" IS NULL;`,
);
logMigrationEnd(this.name);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -30,6 +30,7 @@ import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-Dele
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities'; import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections'; import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable'; import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable';
export const postgresMigrations = [ export const postgresMigrations = [
@ -65,6 +66,7 @@ export const postgresMigrations = [
CreateLdapEntities1674509946020, CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449, PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000, AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
PurgeInvalidWorkflowConnections1675940580449, PurgeInvalidWorkflowConnections1675940580449,
CreateExecutionMetadataTable1674133106778, CreateExecutionMetadataTable1674133106778,
]; ];

View file

@ -3,6 +3,7 @@ import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/mi
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { IConnections, INode } from 'n8n-workflow'; import { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface { export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449'; name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -18,7 +19,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM "${tablePrefix}workflow_entity" FROM "${tablePrefix}workflow_entity"
`); `);
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => { workflows.forEach(async (workflow) => {
let connections: IConnections = JSON.parse(workflow.connections); let connections: IConnections = JSON.parse(workflow.connections);
@ -50,22 +51,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!nodesThatCannotReceiveInput.includes(outgoingConnections.node), !nodesThatCannotReceiveInput.includes(outgoingConnections.node),
); );
}); });
// Filter out output connection items that are empty
connection[outputConnectionName] = connection[outputConnectionName].filter(
(item) => item.length > 0,
);
// Delete the output connection container if it is empty
if (connection[outputConnectionName].length === 0) {
delete connection[outputConnectionName];
}
}); });
// Finally delete the source node if it has no output connections
if (Object.keys(connection).length === 0) {
delete connections[sourceNodeName];
}
}); });
// Update database with new connections // Update database with new connections

View file

@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { logMigrationEnd, logMigrationStart } from '@db/utils/migrationHelpers';
import config from '@/config';
export class MigrateExecutionStatus1676996103000 implements MigrationInterface {
name = 'MigrateExecutionStatus1676996103000';
public async up(queryRunner: QueryRunner): Promise<void> {
logMigrationStart(this.name);
const tablePrefix = config.getEnv('database.tablePrefix');
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'waiting' WHERE "status" IS NULL AND "waitTill" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'failed' WHERE "status" IS NULL AND "finished"=0 AND "stoppedAt" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'success' WHERE "status" IS NULL AND "finished"=1 AND "stoppedAt" IS NOT NULL;`,
);
await queryRunner.query(
`UPDATE "${tablePrefix}execution_entity" SET "status" = 'crashed' WHERE "status" IS NULL;`,
);
logMigrationEnd(this.name);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -29,6 +29,7 @@ import { DeleteExecutionsWithWorkflows1673268682475 } from './1673268682475-Dele
import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities'; import { CreateLdapEntities1674509946020 } from './1674509946020-CreateLdapEntities';
import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections'; import { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions'; import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable'; import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable';
const sqliteMigrations = [ const sqliteMigrations = [
@ -63,6 +64,7 @@ const sqliteMigrations = [
CreateLdapEntities1674509946020, CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449, PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000, AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
CreateExecutionMetadataTable1674133106777, CreateExecutionMetadataTable1674133106777,
]; ];

View file

@ -4,11 +4,12 @@ import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import type { EventMessageTypes, EventNamesTypes } from '../EventMessageClasses'; import type { EventMessageTypes, EventNamesTypes } from '../EventMessageClasses';
import type { DateTime } from 'luxon'; import type { DateTime } from 'luxon';
import { InternalHooksManager } from '../../InternalHooksManager'; import { Push } from '@/push';
import { getPushInstance } from '@/push';
import type { IPushDataExecutionRecovered } from '../../Interfaces'; import type { IPushDataExecutionRecovered } from '../../Interfaces';
import { workflowExecutionCompleted } from '../../events/WorkflowStatistics'; import { workflowExecutionCompleted } from '../../events/WorkflowStatistics';
import { eventBus } from './MessageEventBus'; import { eventBus } from './MessageEventBus';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export async function recoverExecutionDataFromEventLogMessages( export async function recoverExecutionDataFromEventLogMessages(
executionId: string, executionId: string,
@ -151,16 +152,19 @@ export async function recoverExecutionDataFromEventLogMessages(
status: 'crashed', status: 'crashed',
stoppedAt: lastNodeRunTimestamp?.toJSDate(), stoppedAt: lastNodeRunTimestamp?.toJSDate(),
}); });
const internalHooks = InternalHooksManager.getInstance(); await Container.get(InternalHooks).onWorkflowPostExecute(
await internalHooks.onWorkflowPostExecute(executionId, executionEntry.workflowData, { executionId,
data: executionData, executionEntry.workflowData,
finished: false, {
mode: executionEntry.mode, data: executionData,
waitTill: executionEntry.waitTill ?? undefined, finished: false,
startedAt: executionEntry.startedAt, mode: executionEntry.mode,
stoppedAt: lastNodeRunTimestamp?.toJSDate(), waitTill: executionEntry.waitTill ?? undefined,
status: 'crashed', startedAt: executionEntry.startedAt,
}); stoppedAt: lastNodeRunTimestamp?.toJSDate(),
status: 'crashed',
},
);
const iRunData: IRun = { const iRunData: IRun = {
data: executionData, data: executionData,
finished: false, finished: false,
@ -178,7 +182,7 @@ export async function recoverExecutionDataFromEventLogMessages(
eventBus.once('editorUiConnected', function handleUiBackUp() { eventBus.once('editorUiConnected', function handleUiBackUp() {
// add a small timeout to make sure the UI is back up // add a small timeout to make sure the UI is back up
setTimeout(() => { setTimeout(() => {
getPushInstance().send('executionRecovered', { Container.get(Push).send('executionRecovered', {
executionId, executionId,
} as IPushDataExecutionRecovered); } as IPushDataExecutionRecovered);
}, 1000); }, 1000);

View file

@ -1,9 +1,10 @@
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow'; import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import { StatisticsNames } from '@db/entities/WorkflowStatistics'; import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper'; import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { QueryFailedError } from 'typeorm'; import { QueryFailedError } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export async function workflowExecutionCompleted( export async function workflowExecutionCompleted(
workflowData: IWorkflowBase, workflowData: IWorkflowBase,
@ -46,7 +47,7 @@ export async function workflowExecutionCompleted(
}; };
// Send the metrics // Send the metrics
await InternalHooksManager.getInstance().onFirstProductionWorkflowSuccess(metrics); await Container.get(InternalHooks).onFirstProductionWorkflowSuccess(metrics);
} catch (error) { } catch (error) {
if (!(error instanceof QueryFailedError)) { if (!(error instanceof QueryFailedError)) {
throw error; throw error;
@ -101,5 +102,5 @@ export async function nodeFetchedData(
} }
// Send metrics to posthog // Send metrics to posthog
await InternalHooksManager.getInstance().onFirstWorkflowDataLoad(metrics); await Container.get(InternalHooks).onFirstWorkflowDataLoad(metrics);
} }

View file

@ -0,0 +1,18 @@
import type { IExecutionFlattedDb } from '../Interfaces';
import type { ExecutionStatus } from 'n8n-workflow';
export function getStatusUsingPreviousExecutionStatusMethod(
execution: IExecutionFlattedDb,
): ExecutionStatus {
if (execution.waitTill) {
return 'waiting';
} else if (execution.stoppedAt === undefined) {
return 'running';
} else if (execution.finished) {
return 'success';
} else if (execution.stoppedAt !== null) {
return 'failed';
} else {
return 'unknown';
}
}

View file

@ -13,9 +13,9 @@ import type {
IExecutionsSummary, IExecutionsSummary,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { deepCopy, LoggerProxy, jsonParse, Workflow } from 'n8n-workflow'; import { deepCopy, LoggerProxy, jsonParse, Workflow } from 'n8n-workflow';
import type { FindOptionsWhere, FindOperator } from 'typeorm'; import type { FindOperator, FindOptionsWhere } from 'typeorm';
import { In, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, Raw } from 'typeorm'; import { In, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, Raw } from 'typeorm';
import * as ActiveExecutions from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config'; import config from '@/config';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity'; import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
@ -34,6 +34,8 @@ import { WorkflowRunner } from '@/WorkflowRunner';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { parse } from 'flatted'; import { parse } from 'flatted';
import { Container } from 'typedi';
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
import { ExecutionMetadata } from '@/databases/entities/ExecutionMetadata'; import { ExecutionMetadata } from '@/databases/entities/ExecutionMetadata';
import { DateUtils } from 'typeorm/util/DateUtils'; import { DateUtils } from 'typeorm/util/DateUtils';
@ -252,7 +254,7 @@ export class ExecutionsService {
// We may have manual executions even with queue so we must account for these. // We may have manual executions even with queue so we must account for these.
executingWorkflowIds.push( executingWorkflowIds.push(
...ActiveExecutions.getInstance() ...Container.get(ActiveExecutions)
.getActiveExecutions() .getActiveExecutions()
.map(({ id }) => id), .map(({ id }) => id),
); );
@ -262,7 +264,6 @@ export class ExecutionsService {
}; };
if (filter?.status) { if (filter?.status) {
Object.assign(findWhere, { status: In(filter.status) }); Object.assign(findWhere, { status: In(filter.status) });
delete filter.status; // remove status from filter so it does not get applied twice
} }
if (filter?.finished) { if (filter?.finished) {
Object.assign(findWhere, { finished: filter.finished }); Object.assign(findWhere, { finished: filter.finished });
@ -308,6 +309,7 @@ export class ExecutionsService {
'execution.startedAt', 'execution.startedAt',
'execution.stoppedAt', 'execution.stoppedAt',
'execution.workflowData', 'execution.workflowData',
'execution.status',
]) ])
.orderBy('execution.id', 'DESC') .orderBy('execution.id', 'DESC')
.take(limit) .take(limit)
@ -331,6 +333,12 @@ export class ExecutionsService {
}); });
} }
// deepcopy breaks the In operator so we need to reapply it
if (filter?.status) {
Object.assign(filter, { status: In(filter.status) });
Object.assign(countFilter, { status: In(filter.status) });
}
if (filter) { if (filter) {
this.massageFilters(filter as IDataObject); this.massageFilters(filter as IDataObject);
query = query.andWhere(filter); query = query.andWhere(filter);
@ -352,6 +360,10 @@ export class ExecutionsService {
const nodeExecutionStatus = {}; const nodeExecutionStatus = {};
let lastNodeExecuted; let lastNodeExecuted;
let executionError; let executionError;
// fill execution status for old executions that will return null
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
try { try {
const data = parse(execution.data) as IRunExecutionData; const data = parse(execution.data) as IRunExecutionData;
lastNodeExecuted = data?.resultData?.lastNodeExecuted ?? ''; lastNodeExecuted = data?.resultData?.lastNodeExecuted ?? '';
@ -429,6 +441,10 @@ export class ExecutionsService {
return undefined; return undefined;
} }
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
if (req.query.unflattedResponse === 'true') { if (req.query.unflattedResponse === 'true') {
return ResponseHelper.unflattenExecutionData(execution); return ResponseHelper.unflattenExecutionData(execution);
} }
@ -513,7 +529,7 @@ export class ExecutionsService {
} }
data.workflowData = workflowData; data.workflowData = workflowData;
const nodeTypes = NodeTypes(); const nodeTypes = Container.get(NodeTypes);
const workflowInstance = new Workflow({ const workflowInstance = new Workflow({
id: workflowData.id as string, id: workflowData.id as string,
name: workflowData.name, name: workflowData.name,
@ -548,7 +564,7 @@ export class ExecutionsService {
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
const retriedExecutionId = await workflowRunner.run(data); const retriedExecutionId = await workflowRunner.run(data);
const executionData = await ActiveExecutions.getInstance().getPostExecutePromise( const executionData = await Container.get(ActiveExecutions).getPostExecutePromise(
retriedExecutionId, retriedExecutionId,
); );

View file

@ -2,18 +2,13 @@
export * from './CredentialsHelper'; export * from './CredentialsHelper';
export * from './CredentialTypes'; export * from './CredentialTypes';
export * from './CredentialsOverwrites'; export * from './CredentialsOverwrites';
export * from './ExternalHooks';
export * from './Interfaces'; export * from './Interfaces';
export * from './InternalHooksManager';
export * from './LoadNodesAndCredentials';
export * from './NodeTypes'; export * from './NodeTypes';
export * from './WaitTracker';
export * from './WaitingWebhooks'; export * from './WaitingWebhooks';
export * from './WorkflowCredentials'; export * from './WorkflowCredentials';
export * from './WorkflowRunner'; export * from './WorkflowRunner';
import * as ActiveExecutions from './ActiveExecutions'; import { ActiveExecutions } from './ActiveExecutions';
import * as ActiveWorkflowRunner from './ActiveWorkflowRunner';
import * as Db from './Db'; import * as Db from './Db';
import * as GenericHelpers from './GenericHelpers'; import * as GenericHelpers from './GenericHelpers';
import * as ResponseHelper from './ResponseHelper'; import * as ResponseHelper from './ResponseHelper';
@ -26,7 +21,6 @@ import * as WorkflowHelpers from './WorkflowHelpers';
export { export {
ActiveExecutions, ActiveExecutions,
ActiveWorkflowRunner,
Db, Db,
GenericHelpers, GenericHelpers,
ResponseHelper, ResponseHelper,

View file

@ -5,12 +5,13 @@ import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import { InternalHooksManager } from '@/InternalHooksManager';
import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces'; import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { LicenseService } from './License.service'; import { LicenseService } from './License.service';
import { getLicense } from '@/License'; import { getLicense } from '@/License';
import type { AuthenticatedRequest, LicenseRequest } from '@/requests'; import type { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { isInstanceOwner } from '@/PublicApi/v1/handlers/users/users.service'; import { isInstanceOwner } from '@/PublicApi/v1/handlers/users/users.service';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export const licenseController = express.Router(); export const licenseController = express.Router();
@ -115,14 +116,14 @@ licenseController.post(
await license.renew(); await license.renew();
} catch (e) { } catch (e) {
// not awaiting so as not to make the endpoint hang // not awaiting so as not to make the endpoint hang
void InternalHooksManager.getInstance().onLicenseRenewAttempt({ success: false }); void Container.get(InternalHooks).onLicenseRenewAttempt({ success: false });
if (e instanceof Error) { if (e instanceof Error) {
throw new ResponseHelper.BadRequestError(e.message); throw new ResponseHelper.BadRequestError(e.message);
} }
} }
// not awaiting so as not to make the endpoint hang // not awaiting so as not to make the endpoint hang
void InternalHooksManager.getInstance().onLicenseRenewAttempt({ success: true }); void Container.get(InternalHooks).onLicenseRenewAttempt({ success: true });
// Return the read data, plus the management JWT // Return the read data, plus the management JWT
return { return {

View file

@ -3,11 +3,12 @@ import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import passport from 'passport'; import passport from 'passport';
import { Strategy } from 'passport-jwt'; import { Strategy } from 'passport-jwt';
import { sync as globSync } from 'fast-glob';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow';
import type { JwtPayload } from '@/Interfaces'; import type { JwtPayload } from '@/Interfaces';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import config from '@/config'; import config from '@/config';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
import { issueCookie, resolveJwtContent } from '@/auth/jwt'; import { issueCookie, resolveJwtContent } from '@/auth/jwt';
import { import {
isAuthenticatedRequest, isAuthenticatedRequest,
@ -61,6 +62,10 @@ const refreshExpiringCookie: RequestHandler = async (req: AuthenticatedRequest,
const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler; const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler;
const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'], {
cwd: EDITOR_UI_DIST_DIR,
});
/** /**
* This sets up the auth middlewares in the correct order * This sets up the auth middlewares in the correct order
*/ */
@ -79,12 +84,7 @@ export const setupAuthMiddlewares = (
// TODO: refactor me!!! // TODO: refactor me!!!
// skip authentication for preflight requests // skip authentication for preflight requests
req.method === 'OPTIONS' || req.method === 'OPTIONS' ||
req.url === '/index.html' || staticAssets.includes(req.url.slice(1)) ||
req.url === '/favicon.ico' ||
req.url.startsWith('/css/') ||
req.url.startsWith('/js/') ||
req.url.startsWith('/fonts/') ||
req.url.includes('.svg') ||
req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/settings`) ||
req.url.startsWith(`/${restEndpoint}/login`) || req.url.startsWith(`/${restEndpoint}/login`) ||
req.url.startsWith(`/${restEndpoint}/logout`) || req.url.startsWith(`/${restEndpoint}/logout`) ||

View file

@ -0,0 +1,59 @@
import { Service } from 'typedi';
import type { PostHog } from 'posthog-node';
import type { FeatureFlags, ITelemetryTrackProperties } from 'n8n-workflow';
import config from '@/config';
import type { PublicUser } from '@/Interfaces';
@Service()
export class PostHogClient {
private postHog?: PostHog;
private instanceId?: string;
async init(instanceId: string) {
this.instanceId = instanceId;
const enabled = config.getEnv('diagnostics.enabled');
if (!enabled) {
return;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
const { PostHog } = await import('posthog-node');
this.postHog = new PostHog(config.getEnv('diagnostics.config.posthog.apiKey'), {
host: config.getEnv('diagnostics.config.posthog.apiHost'),
});
const logLevel = config.getEnv('logs.level');
if (logLevel === 'debug') {
this.postHog.debug(true);
}
}
async stop(): Promise<void> {
if (this.postHog) {
return this.postHog.shutdown();
}
}
track(payload: { userId: string; event: string; properties: ITelemetryTrackProperties }): void {
this.postHog?.capture({
distinctId: payload.userId,
sendFeatureFlags: true,
...payload,
});
}
async getFeatureFlags(user: Pick<PublicUser, 'id' | 'createdAt'>): Promise<FeatureFlags> {
if (!this.postHog) return Promise.resolve({});
const fullId = [this.instanceId, user.id].join('#');
// cannot use local evaluation because that requires PostHog personal api key with org-wide
// https://github.com/PostHog/posthog/issues/4849
return this.postHog.getAllFlags(fullId, {
personProperties: {
created_at_timestamp: user.createdAt.getTime().toString(),
},
});
}
}

View file

@ -29,7 +29,7 @@ export abstract class AbstractPush<T> {
} }
} }
send<D>(type: IPushDataType, data: D, sessionId: string | undefined = undefined) { send<D>(type: IPushDataType, data: D, sessionId: string | undefined) {
const { connections } = this; const { connections } = this;
if (sessionId !== undefined && connections[sessionId] === undefined) { if (sessionId !== undefined && connections[sessionId] === undefined) {
Logger.error(`The session "${sessionId}" is not registered.`, { sessionId }); Logger.error(`The session "${sessionId}" is not registered.`, { sessionId });

View file

@ -4,21 +4,35 @@ import type { Socket } from 'net';
import type { Application, RequestHandler } from 'express'; import type { Application, RequestHandler } from 'express';
import { Server as WSServer } from 'ws'; import { Server as WSServer } from 'ws';
import { parse as parseUrl } from 'url'; import { parse as parseUrl } from 'url';
import { Container, Service } from 'typedi';
import config from '@/config'; import config from '@/config';
import { resolveJwt } from '@/auth/jwt'; import { resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME } from '@/constants'; import { AUTH_COOKIE_NAME } from '@/constants';
import { SSEPush } from './sse.push'; import { SSEPush } from './sse.push';
import { WebSocketPush } from './websocket.push'; import { WebSocketPush } from './websocket.push';
import type { Push, PushResponse, SSEPushRequest, WebSocketPushRequest } from './types'; import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
export type { Push } from './types'; import type { IPushDataType } from '@/Interfaces';
const useWebSockets = config.getEnv('push.backend') === 'websocket'; const useWebSockets = config.getEnv('push.backend') === 'websocket';
let pushInstance: Push; @Service()
export const getPushInstance = () => { export class Push {
if (!pushInstance) pushInstance = useWebSockets ? new WebSocketPush() : new SSEPush(); private backend = useWebSockets ? new WebSocketPush() : new SSEPush();
return pushInstance;
}; handleRequest(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) {
if (req.ws) {
(this.backend as WebSocketPush).add(req.query.sessionId, req.ws);
} else if (!useWebSockets) {
(this.backend as SSEPush).add(req.query.sessionId, { req, res });
} else {
res.status(401).send('Unauthorized');
}
}
send<D>(type: IPushDataType, data: D, sessionId: string | undefined = undefined) {
this.backend.send(type, data, sessionId);
}
}
export const setupPushServer = (restEndpoint: string, server: Server, app: Application) => { export const setupPushServer = (restEndpoint: string, server: Server, app: Application) => {
if (useWebSockets) { if (useWebSockets) {
@ -48,7 +62,6 @@ export const setupPushHandler = (
app: Application, app: Application,
isUserManagementEnabled: boolean, isUserManagementEnabled: boolean,
) => { ) => {
const push = getPushInstance();
const endpoint = `/${restEndpoint}/push`; const endpoint = `/${restEndpoint}/push`;
const pushValidationMiddleware: RequestHandler = async ( const pushValidationMiddleware: RequestHandler = async (
@ -89,17 +102,10 @@ export const setupPushHandler = (
next(); next();
}; };
const push = Container.get(Push);
app.use( app.use(
endpoint, endpoint,
pushValidationMiddleware, pushValidationMiddleware,
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => { (req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res),
if (req.ws) {
(push as WebSocketPush).add(req.query.sessionId, req.ws);
} else if (!useWebSockets) {
(push as SSEPush).add(req.query.sessionId, { req, res });
} else {
res.status(401).send('Unauthorized');
}
},
); );
}; };

View file

@ -1,12 +1,8 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import type { WebSocket } from 'ws'; import type { WebSocket } from 'ws';
import type { SSEPush } from './sse.push';
import type { WebSocketPush } from './websocket.push';
// TODO: move all push related types here // TODO: move all push related types here
export type Push = SSEPush | WebSocketPush;
export type PushRequest = Request<{}, {}, {}, { sessionId: string }>; export type PushRequest = Request<{}, {}, {}, { sessionId: string }>;
export type SSEPushRequest = PushRequest & { ws: undefined }; export type SSEPushRequest = PushRequest & { ws: undefined };

View file

@ -10,11 +10,28 @@ import type {
IWorkflowSettings, IWorkflowSettings,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { IsEmail, IsString, Length } from 'class-validator';
import { NoXss } from '@db/utils/customValidators';
import type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces'; import type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type * as UserManagementMailer from '@/UserManagement/email/UserManagementMailer'; import type * as UserManagementMailer from '@/UserManagement/email/UserManagementMailer';
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
@IsEmail()
email: string;
@NoXss()
@IsString({ message: 'First name must be of type string.' })
@Length(1, 32, { message: 'First name must be $constraint1 to $constraint2 characters long.' })
firstName: string;
@NoXss()
@IsString({ message: 'Last name must be of type string.' })
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
lastName: string;
}
export type AuthlessRequest< export type AuthlessRequest<
RouteParams = {}, RouteParams = {},
ResponseBody = {}, ResponseBody = {},
@ -144,11 +161,7 @@ export declare namespace ExecutionRequest {
// ---------------------------------- // ----------------------------------
export declare namespace MeRequest { export declare namespace MeRequest {
export type Settings = AuthenticatedRequest< export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>;
{},
{},
Pick<PublicUser, 'email' | 'firstName' | 'lastName'>
>;
export type Password = AuthenticatedRequest< export type Password = AuthenticatedRequest<
{}, {},
{}, {},

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type RudderStack from '@rudderstack/rudder-sdk-node'; import type RudderStack from '@rudderstack/rudder-sdk-node';
import type { PostHog } from 'posthog-node'; import { PostHogClient } from '@/posthog';
import type { ITelemetryTrackProperties } from 'n8n-workflow'; import type { ITelemetryTrackProperties } from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow'; import { LoggerProxy } from 'n8n-workflow';
import config from '@/config'; import config from '@/config';
@ -10,6 +10,7 @@ import { getLogger } from '@/Logger';
import { getLicense } from '@/License'; import { getLicense } from '@/License';
import { LicenseService } from '@/license/License.service'; import { LicenseService } from '@/license/License.service';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { Service } from 'typedi';
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success'; type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
@ -28,16 +29,21 @@ interface IExecutionsBuffer {
}; };
} }
@Service()
export class Telemetry { export class Telemetry {
private rudderStack?: RudderStack; private instanceId: string;
private postHog?: PostHog; private rudderStack?: RudderStack;
private pulseIntervalReference: NodeJS.Timeout; private pulseIntervalReference: NodeJS.Timeout;
private executionCountsBuffer: IExecutionsBuffer = {}; private executionCountsBuffer: IExecutionsBuffer = {};
constructor(private instanceId: string) {} constructor(private postHog: PostHogClient) {}
setInstanceId(instanceId: string) {
this.instanceId = instanceId;
}
async init() { async init() {
const enabled = config.getEnv('diagnostics.enabled'); const enabled = config.getEnv('diagnostics.enabled');
@ -58,12 +64,6 @@ export class Telemetry {
const { default: RudderStack } = await import('@rudderstack/rudder-sdk-node'); const { default: RudderStack } = await import('@rudderstack/rudder-sdk-node');
this.rudderStack = new RudderStack(key, url, { logLevel }); this.rudderStack = new RudderStack(key, url, { logLevel });
// eslint-disable-next-line @typescript-eslint/naming-convention
const { PostHog } = await import('posthog-node');
this.postHog = new PostHog(config.getEnv('diagnostics.config.posthog.apiKey'), {
host: config.getEnv('diagnostics.config.posthog.apiHost'),
});
this.startPulse(); this.startPulse();
} }
} }
@ -137,10 +137,8 @@ export class Telemetry {
async trackN8nStop(): Promise<void> { async trackN8nStop(): Promise<void> {
clearInterval(this.pulseIntervalReference); clearInterval(this.pulseIntervalReference);
void this.track('User instance stopped'); void this.track('User instance stopped');
return new Promise<void>((resolve) => { return new Promise<void>(async (resolve) => {
if (this.postHog) { await this.postHog.stop();
this.postHog.shutdown();
}
if (this.rudderStack) { if (this.rudderStack) {
this.rudderStack.flush(resolve); this.rudderStack.flush(resolve);
@ -192,11 +190,7 @@ export class Telemetry {
}; };
if (withPostHog) { if (withPostHog) {
this.postHog?.capture({ this.postHog?.track(payload);
distinctId: payload.userId,
sendFeatureFlags: true,
...payload,
});
} }
return this.rudderStack.track(payload, resolve); return this.rudderStack.track(payload, resolve);
@ -206,19 +200,7 @@ export class Telemetry {
}); });
} }
async isFeatureFlagEnabled(
featureFlagName: string,
{ user_id: userId }: ITelemetryTrackProperties = {},
): Promise<boolean | undefined> {
if (!this.postHog) return Promise.resolve(false);
const fullId = [this.instanceId, userId].join('#');
return this.postHog.isFeatureEnabled(featureFlagName, fullId);
}
// test helpers // test helpers
getCountsBuffer(): IExecutionsBuffer { getCountsBuffer(): IExecutionsBuffer {
return this.executionCountsBuffer; return this.executionCountsBuffer;
} }

View file

@ -1,17 +0,0 @@
/**
* Create a script to init PostHog, for embedding before the Vue bundle in `<head>` in `index.html`.
*/
export const createPostHogLoadingScript = ({
apiKey,
apiHost,
autocapture,
disableSessionRecording,
debug,
}: {
apiKey: string;
apiHost: string;
autocapture: boolean;
disableSessionRecording: boolean;
debug: boolean;
}) =>
`<script>!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);posthog.init('${apiKey}',{api_host:'${apiHost}', autocapture: ${autocapture.toString()}, disable_session_recording: ${disableSessionRecording.toString()}, debug:${debug.toString()}})</script>`;

View file

@ -1,7 +1,6 @@
import express from 'express'; import express from 'express';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config'; import config from '@/config';
@ -18,6 +17,8 @@ import { EECredentialsService as EECredentials } from '../credentials/credential
import type { IExecutionPushResponse } from '@/Interfaces'; import type { IExecutionPushResponse } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers'; import * as GenericHelpers from '@/GenericHelpers';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
export const EEWorkflowController = express.Router(); export const EEWorkflowController = express.Router();
@ -75,7 +76,7 @@ EEWorkflowController.put(
} }
}); });
void InternalHooksManager.getInstance().onWorkflowSharingUpdate( void Container.get(InternalHooks).onWorkflowSharingUpdate(
workflowId, workflowId,
req.user.id, req.user.id,
shareWithIds, shareWithIds,
@ -126,7 +127,7 @@ EEWorkflowController.post(
await validateEntity(newWorkflow); await validateEntity(newWorkflow);
await ExternalHooks().run('workflow.create', [newWorkflow]); await Container.get(ExternalHooks).run('workflow.create', [newWorkflow]);
const { tags: tagIds } = req.body; const { tags: tagIds } = req.body;
@ -190,8 +191,8 @@ EEWorkflowController.post(
}); });
} }
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]); await Container.get(ExternalHooks).run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user, newWorkflow, false); void Container.get(InternalHooks).onWorkflowCreated(req.user, newWorkflow, false);
return savedWorkflow; return savedWorkflow;
}), }),

View file

@ -15,7 +15,6 @@ import * as TagHelpers from '@/TagHelpers';
import { SharedWorkflow } from '@db/entities/SharedWorkflow'; import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import { InternalHooksManager } from '@/InternalHooksManager';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { getLogger } from '@/Logger'; import { getLogger } from '@/Logger';
import type { WorkflowRequest } from '@/requests'; import type { WorkflowRequest } from '@/requests';
@ -24,6 +23,8 @@ import { EEWorkflowController } from './workflows.controller.ee';
import { WorkflowsService } from './workflows.services'; import { WorkflowsService } from './workflows.services';
import { whereClause } from '@/UserManagement/UserManagementHelper'; import { whereClause } from '@/UserManagement/UserManagementHelper';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export const workflowsController = express.Router(); export const workflowsController = express.Router();
@ -57,7 +58,7 @@ workflowsController.post(
await validateEntity(newWorkflow); await validateEntity(newWorkflow);
await ExternalHooks().run('workflow.create', [newWorkflow]); await Container.get(ExternalHooks).run('workflow.create', [newWorkflow]);
const { tags: tagIds } = req.body; const { tags: tagIds } = req.body;
@ -106,8 +107,8 @@ workflowsController.post(
}); });
} }
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]); await Container.get(ExternalHooks).run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user, newWorkflow, false); void Container.get(InternalHooks).onWorkflowCreated(req.user, newWorkflow, false);
return savedWorkflow; return savedWorkflow;
}), }),

View file

@ -1,3 +1,4 @@
import { Container } from 'typedi';
import { validate as jsonSchemaValidate } from 'jsonschema'; import { validate as jsonSchemaValidate } from 'jsonschema';
import type { INode, IPinData, JsonObject } from 'n8n-workflow'; import type { INode, IPinData, JsonObject } from 'n8n-workflow';
import { NodeApiError, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow'; import { NodeApiError, jsonParse, LoggerProxy, Workflow } from 'n8n-workflow';
@ -5,9 +6,8 @@ import type { FindOptionsSelect, FindOptionsWhere, UpdateResult } from 'typeorm'
import { In } from 'typeorm'; import { In } from 'typeorm';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config'; import config from '@/config';
@ -22,10 +22,11 @@ import type { IWorkflowDb, IWorkflowExecutionDataProcess } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import * as TestWebhooks from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
import { getSharedWorkflowIds } from '@/WorkflowHelpers'; import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper'; import { isSharingEnabled, whereClause } from '@/UserManagement/UserManagementHelper';
import type { WorkflowForList } from '@/workflows/workflows.types'; import type { WorkflowForList } from '@/workflows/workflows.types';
import { InternalHooks } from '@/InternalHooks';
export type IGetWorkflowsQueryFilter = Pick< export type IGetWorkflowsQueryFilter = Pick<
FindOptionsWhere<WorkflowEntity>, FindOptionsWhere<WorkflowEntity>,
@ -91,7 +92,7 @@ export class WorkflowsService {
nodes: workflow.nodes, nodes: workflow.nodes,
connections: workflow.connections, connections: workflow.connections,
active: workflow.active, active: workflow.active,
nodeTypes: NodeTypes(), nodeTypes: Container.get(NodeTypes),
}).getParentNodes(startNodeName); }).getParentNodes(startNodeName);
let checkNodeName = ''; let checkNodeName = '';
@ -236,12 +237,12 @@ export class WorkflowsService {
WorkflowHelpers.addNodeIds(workflow); WorkflowHelpers.addNodeIds(workflow);
await ExternalHooks().run('workflow.update', [workflow]); await Container.get(ExternalHooks).run('workflow.update', [workflow]);
if (shared.workflow.active) { if (shared.workflow.active) {
// When workflow gets saved always remove it as the triggers could have been // When workflow gets saved always remove it as the triggers could have been
// changed and so the changes would not take effect // changed and so the changes would not take effect
await ActiveWorkflowRunner.getInstance().remove(workflowId); await Container.get(ActiveWorkflowRunner).remove(workflowId);
} }
if (workflow.settings) { if (workflow.settings) {
@ -319,20 +320,24 @@ export class WorkflowsService {
}); });
} }
await ExternalHooks().run('workflow.afterUpdate', [updatedWorkflow]); await Container.get(ExternalHooks).run('workflow.afterUpdate', [updatedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowSaved(user, updatedWorkflow, false); void Container.get(InternalHooks).onWorkflowSaved(user, updatedWorkflow, false);
if (updatedWorkflow.active) { if (updatedWorkflow.active) {
// When the workflow is supposed to be active add it again // When the workflow is supposed to be active add it again
try { try {
await ExternalHooks().run('workflow.activate', [updatedWorkflow]); await Container.get(ExternalHooks).run('workflow.activate', [updatedWorkflow]);
await ActiveWorkflowRunner.getInstance().add( await Container.get(ActiveWorkflowRunner).add(
workflowId, workflowId,
shared.workflow.active ? 'update' : 'activate', shared.workflow.active ? 'update' : 'activate',
); );
} catch (error) { } catch (error) {
// If workflow could not be activated set it again to inactive // If workflow could not be activated set it again to inactive
await Db.collections.Workflow.update(workflowId, { active: false }); // and revert the versionId change so UI remains consistent
await Db.collections.Workflow.update(workflowId, {
active: false,
versionId: shared.workflow.versionId,
});
// Also set it in the returned data // Also set it in the returned data
updatedWorkflow.active = false; updatedWorkflow.active = false;
@ -379,14 +384,14 @@ export class WorkflowsService {
nodes: workflowData.nodes, nodes: workflowData.nodes,
connections: workflowData.connections, connections: workflowData.connections,
active: false, active: false,
nodeTypes: NodeTypes(), nodeTypes: Container.get(NodeTypes),
staticData: undefined, staticData: undefined,
settings: workflowData.settings, settings: workflowData.settings,
}); });
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id); const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
const needsWebhook = await TestWebhooks.getInstance().needsWebhookData( const needsWebhook = await Container.get(TestWebhooks).needsWebhookData(
workflowData, workflowData,
workflow, workflow,
additionalData, additionalData,
@ -432,7 +437,7 @@ export class WorkflowsService {
} }
static async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> { static async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> {
await ExternalHooks().run('workflow.delete', [workflowId]); await Container.get(ExternalHooks).run('workflow.delete', [workflowId]);
const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({ const sharedWorkflow = await Db.collections.SharedWorkflow.findOne({
relations: ['workflow', 'role'], relations: ['workflow', 'role'],
@ -450,13 +455,13 @@ export class WorkflowsService {
if (sharedWorkflow.workflow.active) { if (sharedWorkflow.workflow.active) {
// deactivate before deleting // deactivate before deleting
await ActiveWorkflowRunner.getInstance().remove(workflowId); await Container.get(ActiveWorkflowRunner).remove(workflowId);
} }
await Db.collections.Workflow.delete(workflowId); await Db.collections.Workflow.delete(workflowId);
void InternalHooksManager.getInstance().onWorkflowDeleted(user, workflowId, false); void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
await ExternalHooks().run('workflow.afterDelete', [workflowId]); await Container.get(ExternalHooks).run('workflow.afterDelete', [workflowId]);
return sharedWorkflow.workflow; return sharedWorkflow.workflow;
} }

View file

@ -6,6 +6,13 @@ import { OFFICIAL_RISKY_NODE_TYPES, NODES_REPORT } from '@/audit/constants';
import { getRiskSection, MOCK_PACKAGE, saveManualTriggerWorkflow } from './utils'; import { getRiskSection, MOCK_PACKAGE, saveManualTriggerWorkflow } from './utils';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import { toReportTitle } from '@/audit/utils'; import { toReportTitle } from '@/audit/utils';
import { mockInstance } from '../shared/utils';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
const nodesAndCredentials = mockInstance(LoadNodesAndCredentials);
nodesAndCredentials.getCustomDirectories.mockReturnValue([]);
mockInstance(NodeTypes);
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();

View file

@ -2,10 +2,17 @@ import * as Db from '@/Db';
import { Reset } from '@/commands/user-management/reset'; import { Reset } from '@/commands/user-management/reset';
import type { Role } from '@db/entities/Role'; import type { Role } from '@db/entities/Role';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import { mockInstance } from '../shared/utils';
import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { NodeTypes } from '@/NodeTypes';
let globalOwnerRole: Role; let globalOwnerRole: Role;
beforeAll(async () => { beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
mockInstance(NodeTypes);
await testDb.init(); await testDb.init();
globalOwnerRole = await testDb.getGlobalOwnerRole(); globalOwnerRole = await testDb.getGlobalOwnerRole();

View file

@ -20,6 +20,12 @@ import type { Role } from '@db/entities/Role';
import type { AuthAgent } from './shared/types'; import type { AuthAgent } from './shared/types';
import type { InstalledNodes } from '@db/entities/InstalledNodes'; import type { InstalledNodes } from '@db/entities/InstalledNodes';
import { COMMUNITY_PACKAGE_VERSION } from './shared/constants'; import { COMMUNITY_PACKAGE_VERSION } from './shared/constants';
import { NodeTypes } from '@/NodeTypes';
import { Push } from '@/push';
const mockLoadNodesAndCredentials = utils.mockInstance(LoadNodesAndCredentials);
utils.mockInstance(NodeTypes);
utils.mockInstance(Push);
jest.mock('@/CommunityNodes/helpers', () => { jest.mock('@/CommunityNodes/helpers', () => {
return { return {
@ -213,7 +219,7 @@ test('POST /nodes should allow installing packages that could not be loaded', as
mocked(hasPackageLoaded).mockReturnValueOnce(false); mocked(hasPackageLoaded).mockReturnValueOnce(false);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' }); mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
jest.spyOn(LoadNodesAndCredentials(), 'loadNpmModule').mockImplementationOnce(mockedEmptyPackage); mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({ const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
name: utils.installedPackagePayload().packageName, name: utils.installedPackagePayload().packageName,
@ -267,9 +273,7 @@ test('DELETE /nodes should reject if package is not installed', async () => {
test('DELETE /nodes should uninstall package', async () => { test('DELETE /nodes should uninstall package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const removeSpy = jest const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
.spyOn(LoadNodesAndCredentials(), 'removeNpmModule')
.mockImplementationOnce(jest.fn());
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);
@ -310,9 +314,8 @@ test('PATCH /nodes reject if package is not installed', async () => {
test('PATCH /nodes should update a package', async () => { test('PATCH /nodes should update a package', async () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole); const ownerShell = await testDb.createUserShell(globalOwnerRole);
const updateSpy = jest const updateSpy =
.spyOn(LoadNodesAndCredentials(), 'updateNpmModule') mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
.mockImplementationOnce(mockedEmptyPackage);
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage); mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);

View file

@ -1,3 +1,4 @@
import { Container } from 'typedi';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
@ -25,14 +26,15 @@ import {
import superagent from 'superagent'; import superagent from 'superagent';
import request from 'supertest'; import request from 'supertest';
import { URL } from 'url'; import { URL } from 'url';
import { mock } from 'jest-mock-extended';
import { DeepPartial } from 'ts-essentials';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooksManager } from '@/InternalHooksManager';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import * as ActiveWorkflowRunner from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { nodesController } from '@/api/nodes.api'; import { nodesController } from '@/api/nodes.api';
import { workflowsController } from '@/workflows/workflows.controller'; import { workflowsController } from '@/workflows/workflows.controller';
import { AUTH_COOKIE_NAME, NODE_PACKAGE_PREFIX } from '@/constants'; import { AUTH_COOKIE_NAME, NODE_PACKAGE_PREFIX } from '@/constants';
@ -74,15 +76,25 @@ import * as testDb from '../shared/testDb';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { handleLdapInit } from '@/Ldap/helpers'; import { handleLdapInit } from '@/Ldap/helpers';
import { ldapController } from '@/Ldap/routes/ldap.controller.ee'; import { ldapController } from '@/Ldap/routes/ldap.controller.ee';
import { InternalHooks } from '@/InternalHooks';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { PostHogClient } from '@/posthog';
export const mockInstance = <T>(
ctor: new (...args: any[]) => T,
data: DeepPartial<T> | undefined = undefined,
) => {
const instance = mock<T>(data);
Container.set(ctor, instance);
return instance;
};
const loadNodesAndCredentials: INodesAndCredentials = { const loadNodesAndCredentials: INodesAndCredentials = {
loaded: { nodes: {}, credentials: {} }, loaded: { nodes: {}, credentials: {} },
known: { nodes: {}, credentials: {} }, known: { nodes: {}, credentials: {} },
credentialTypes: {} as ICredentialTypes, credentialTypes: {} as ICredentialTypes,
}; };
Container.set(LoadNodesAndCredentials, loadNodesAndCredentials);
const mockNodeTypes = NodeTypes(loadNodesAndCredentials);
CredentialTypes(loadNodesAndCredentials);
/** /**
* Initialize a test server. * Initialize a test server.
@ -107,8 +119,9 @@ export async function initTestServer({
const logger = getLogger(); const logger = getLogger();
LoggerProxy.init(logger); LoggerProxy.init(logger);
// Pre-requisite: Mock the telemetry module before calling. // Mock all telemetry.
await InternalHooksManager.init('test-instance-id', mockNodeTypes); mockInstance(InternalHooks);
mockInstance(PostHogClient);
testServer.app.use(bodyParser.json()); testServer.app.use(bodyParser.json());
testServer.app.use(bodyParser.urlencoded({ extended: true })); testServer.app.use(bodyParser.urlencoded({ extended: true }));
@ -133,7 +146,7 @@ export async function initTestServer({
endpointGroups.includes('users') || endpointGroups.includes('users') ||
endpointGroups.includes('passwordReset') endpointGroups.includes('passwordReset')
) { ) {
testServer.externalHooks = ExternalHooks(); testServer.externalHooks = Container.get(ExternalHooks);
} }
const [routerEndpoints, functionEndpoints] = classifyEndpointGroups(endpointGroups); const [routerEndpoints, functionEndpoints] = classifyEndpointGroups(endpointGroups);
@ -163,8 +176,8 @@ export async function initTestServer({
} }
if (functionEndpoints.length) { if (functionEndpoints.length) {
const externalHooks = ExternalHooks(); const externalHooks = Container.get(ExternalHooks);
const internalHooks = InternalHooksManager.getInstance(); const internalHooks = Container.get(InternalHooks);
const mailer = UserManagementMailer.getInstance(); const mailer = UserManagementMailer.getInstance();
const repositories = Db.collections; const repositories = Db.collections;
@ -214,7 +227,7 @@ export async function initTestServer({
externalHooks, externalHooks,
internalHooks, internalHooks,
repositories, repositories,
activeWorkflowRunner: ActiveWorkflowRunner.getInstance(), activeWorkflowRunner: Container.get(ActiveWorkflowRunner),
logger, logger,
}), }),
); );
@ -257,8 +270,8 @@ const classifyEndpointGroups = (endpointGroups: EndpointGroup[]) => {
/** /**
* Initialize node types. * Initialize node types.
*/ */
export async function initActiveWorkflowRunner(): Promise<ActiveWorkflowRunner.ActiveWorkflowRunner> { export async function initActiveWorkflowRunner(): Promise<ActiveWorkflowRunner> {
const workflowRunner = ActiveWorkflowRunner.getInstance(); const workflowRunner = Container.get(ActiveWorkflowRunner);
workflowRunner.init(); workflowRunner.init();
return workflowRunner; return workflowRunner;
} }
@ -299,7 +312,7 @@ export function gitHubCredentialType(): ICredentialType {
* Initialize node types. * Initialize node types.
*/ */
export async function initCredentialsTypes(): Promise<void> { export async function initCredentialsTypes(): Promise<void> {
loadNodesAndCredentials.loaded.credentials = { Container.get(LoadNodesAndCredentials).loaded.credentials = {
githubApi: { githubApi: {
type: gitHubCredentialType(), type: gitHubCredentialType(),
sourcePath: '', sourcePath: '',
@ -318,7 +331,7 @@ export async function initLdapManager(): Promise<void> {
* Initialize node types. * Initialize node types.
*/ */
export async function initNodeTypes() { export async function initNodeTypes() {
loadNodesAndCredentials.loaded.nodes = { Container.get(LoadNodesAndCredentials).loaded.nodes = {
'n8n-nodes-base.start': { 'n8n-nodes-base.start': {
sourcePath: '', sourcePath: '',
type: { type: {

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