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
on:
push:
tags:
- n8n@*
release:
types: [published]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:

View file

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

View file

@ -1,9 +1,8 @@
name: Docker Image CI
on:
push:
tags:
- n8n@*
release:
types: [published]
jobs:
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)

View file

@ -11,15 +11,16 @@ Great that you are here and you want to contribute to n8n
- [Development setup](#development-setup)
- [Requirements](#requirements)
- [Node.js](#nodejs)
- [pnpm](#pnpm)
- [pnpm workspaces](#pnpm-workspaces)
- [corepack](#corepack)
- [Build tools](#build-tools)
- [pnpm workspaces](#pnpm-workspaces)
- [Actual n8n setup](#actual-n8n-setup)
- [Start](#start)
- [Development cycle](#development-cycle)
- [Test suite](#test-suite)
- [Releasing](#releasing)
- [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)
- [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
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
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');
})
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', () => {
cy.intercept('GET', '/types/nodes.json').as('nodesIntercept');
cy.wait('@nodesIntercept').then(() => {

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.216.0",
"version": "0.216.1",
"private": true,
"homepage": "https://n8n.io",
"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'"
},
"dependencies": {
"n8n": "*"
"n8n": "workspace:*"
},
"devDependencies": {
"@n8n_io/eslint-config": "*",
"@n8n_io/eslint-config": "workspace:*",
"@ngneat/falso": "^6.1.0",
"@types/jest": "^29.2.2",
"@types/supertest": "^2.0.12",
@ -79,7 +79,8 @@
"qqjs>globby": "^11.1.0"
},
"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('source-map-support').install();
require('reflect-metadata');
require('@oclif/command')
.run()

View file

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

View file

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

View file

@ -26,7 +26,9 @@ import type {
} from '@/Interfaces';
import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import { Service } from 'typedi';
@Service()
export class ActiveExecutions {
private activeExecutions: {
[index: string]: IExecutingWorkflowData;
@ -34,7 +36,6 @@ export class ActiveExecutions {
/**
* Add a new active execution
*
*/
async add(
executionData: IWorkflowExecutionDataProcess,
@ -253,13 +254,3 @@ export class ActiveExecutions {
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 {
IWebhookData,
WebhookHttpMethod,
@ -6,8 +7,9 @@ import type {
WorkflowExecuteMode,
} from 'n8n-workflow';
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
import * as NodeExecuteFunctions from 'n8n-core';
@Service()
export class ActiveWebhooks {
private workflowWebhooks: {
[key: string]: IWebhookData[];

View file

@ -8,6 +8,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi';
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
import type {
@ -55,7 +57,7 @@ import config from '@/config';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { WebhookEntity } from '@db/entities/WebhookEntity';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import { createErrorExecution } from '@/GenericHelpers';
import { WORKFLOW_REACTIVATE_INITIAL_TIMEOUT, WORKFLOW_REACTIVATE_MAX_TIMEOUT } from '@/constants';
import { NodeTypes } from '@/NodeTypes';
@ -68,8 +70,9 @@ import { START_NODES } from './constants';
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)";
@Service()
export class ActiveWorkflowRunner {
private activeWorkflows: ActiveWorkflows | null = null;
private activeWorkflows = new ActiveWorkflows();
private activationErrors: {
[key: string]: IActivationError;
@ -79,9 +82,7 @@ export class ActiveWorkflowRunner {
[key: string]: IQueuedWorkflowActivations;
} = {};
constructor() {
this.activeWorkflows = new ActiveWorkflows();
}
constructor(private externalHooks: ExternalHooks) {}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async init() {
@ -133,7 +134,7 @@ export class ActiveWorkflowRunner {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
Logger.info(` ${error.message}`);
Logger.error(
`Issue on intital workflow activation try "${workflowData.name}" (startup)`,
`Issue on initial workflow activation try "${workflowData.name}" (startup)`,
{
workflowName: workflowData.name,
workflowId: workflowData.id,
@ -148,21 +149,18 @@ export class ActiveWorkflowRunner {
}
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
*
*/
async removeAll(): Promise<void> {
let activeWorkflowIds: string[] = [];
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();
activeWorkflowIds = [
@ -183,7 +181,6 @@ export class ActiveWorkflowRunner {
/**
* Checks if a webhook for the given method and path exists and executes the workflow.
*
*/
async executeWebhook(
httpMethod: WebhookHttpMethod,
@ -192,11 +189,6 @@ export class ActiveWorkflowRunner {
res: express.Response,
): Promise<IResponseCallbackData> {
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
req.params = {};
@ -279,7 +271,7 @@ export class ActiveWorkflowRunner {
);
}
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({
id: webhook.workflowId,
name: workflowData.name,
@ -482,6 +474,7 @@ export class ActiveWorkflowRunner {
try {
await this.removeWorkflowWebhooks(workflow.id as string);
} catch (error) {
ErrorReporter.error(error);
Logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`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}"`);
}
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({
id: workflowId,
name: workflowData.name,
@ -645,7 +638,7 @@ export class ActiveWorkflowRunner {
if (donePromise) {
executePromise.then((executionId) => {
ActiveExecutions.getInstance()
Container.get(ActiveExecutions)
.getPostExecutePromise(executionId)
.then(donePromise.resolve)
.catch(donePromise.reject);
@ -702,7 +695,7 @@ export class ActiveWorkflowRunner {
if (donePromise) {
executePromise.then((executionId) => {
ActiveExecutions.getInstance()
Container.get(ActiveExecutions)
.getPostExecutePromise(executionId)
.then(donePromise.resolve)
.catch(donePromise.reject);
@ -723,7 +716,7 @@ export class ActiveWorkflowRunner {
// Remove the workflow as "active"
await this.activeWorkflows?.remove(workflowData.id);
await this.activeWorkflows.remove(workflowData.id);
this.activationErrors[workflowData.id] = {
time: new Date().getTime(),
error: {
@ -777,10 +770,6 @@ export class ActiveWorkflowRunner {
activation: WorkflowActivateMode,
workflowData?: IWorkflowDb,
): Promise<void> {
if (this.activeWorkflows === null) {
throw new Error('The "activeWorkflows" instance did not get initialized yet.');
}
let workflowInstance: Workflow;
try {
if (workflowData === undefined) {
@ -793,7 +782,7 @@ export class ActiveWorkflowRunner {
if (!workflowData) {
throw new Error(`Could not find workflow with id "${workflowId}".`);
}
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
workflowInstance = new Workflow({
id: workflowId,
name: workflowData.name,
@ -978,47 +967,31 @@ export class ActiveWorkflowRunner {
*/
// TODO: this should happen in a transaction
async remove(workflowId: string): Promise<void> {
if (this.activeWorkflows !== null) {
// Remove all the webhooks of the workflow
try {
await this.removeWorkflowWebhooks(workflowId);
} catch (error) {
ErrorReporter.error(error);
Logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`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;
// Remove all the webhooks of the workflow
try {
await this.removeWorkflowWebhooks(workflowId);
} catch (error) {
ErrorReporter.error(error);
Logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
);
}
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 type {
ICredentialType,
ICredentialTypes,
INodesAndCredentials,
LoadedClass,
} from 'n8n-workflow';
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
import { Service } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from './constants';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
class CredentialTypesClass implements ICredentialTypes {
constructor(private nodesAndCredentials: INodesAndCredentials) {
@Service()
export class CredentialTypes implements ICredentialTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {
nodesAndCredentials.credentialTypes = this;
}
@ -64,18 +62,3 @@ class CredentialTypesClass implements ICredentialTypes {
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,
INodeTypeData,
INodeTypes,
ICredentialTypes,
} from 'n8n-workflow';
import {
ICredentialsHelper,
@ -54,6 +53,7 @@ import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { whereClause } from './UserManagement/UserManagementHelper';
import { RESPONSE_ERROR_MESSAGES } from './constants';
import { Container } from 'typedi';
const mockNode = {
name: '',
@ -87,8 +87,8 @@ const mockNodeTypes: INodeTypes = {
export class CredentialsHelper extends ICredentialsHelper {
constructor(
encryptionKey: string,
private credentialTypes: ICredentialTypes = CredentialTypes(),
private nodeTypes: INodeTypes = NodeTypes(),
private credentialTypes = Container.get(CredentialTypes),
private nodeTypes = Container.get(NodeTypes),
) {
super(encryptionKey);
}

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable no-restricted-syntax */
import { Service } from 'typedi';
import * as Db from '@/Db';
import type {
IExternalHooksClass,
@ -10,7 +11,8 @@ import type {
import config from '@/config';
class ExternalHooksClass implements IExternalHooksClass {
@Service()
export class ExternalHooks implements IExternalHooksClass {
externalHooks: {
[key: string]: Array<() => {}>;
} = {};
@ -103,14 +105,3 @@ class ExternalHooksClass implements IExternalHooksClass {
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 { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User';
import type { UserUpdatePayload } from '@/requests';
/**
* Returns the base URL n8n is reachable from
@ -99,7 +100,7 @@ export async function generateUniqueName(
}
export async function validateEntity(
entity: WorkflowEntity | CredentialsEntity | TagEntity | User,
entity: WorkflowEntity | CredentialsEntity | TagEntity | User | UserUpdatePayload,
): Promise<void> {
const errors = await validate(entity);

View file

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

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Service } from 'typedi';
import { snakeCase } from 'change-case';
import { BinaryDataManager } from 'n8n-core';
import type {
ExecutionStatus,
INodesGraphResult,
INodeTypes,
IRun,
ITelemetryTrackProperties,
IWorkflowBase,
@ -22,13 +22,14 @@ import type {
IExecutionTrackProperties,
IWorkflowExecutionDataProcess,
} from '@/Interfaces';
import type { Telemetry } from '@/telemetry';
import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { RoleService } from './role/role.service';
import { eventBus } from './eventbus';
import type { User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants';
import * as Db from '@/Db';
import { NodeTypes } from './NodeTypes';
function userToPayload(user: User): {
userId: string;
@ -46,12 +47,17 @@ function userToPayload(user: User): {
};
}
export class InternalHooksClass implements IInternalHooksClass {
constructor(
private telemetry: Telemetry,
private instanceId: string,
private nodeTypes: INodeTypes,
) {}
@Service()
export class InternalHooks implements IInternalHooksClass {
private instanceId: string;
constructor(private telemetry: Telemetry, private nodeTypes: NodeTypes) {}
async init(instanceId: string) {
this.instanceId = instanceId;
this.telemetry.setInstanceId(instanceId);
await this.telemetry.init();
}
async onServerStarted(
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 { Role } from '@db/entities/Role';
import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHistory';
import { InternalHooksManager } from '@/InternalHooksManager';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export class LdapSync {
private intervalId: NodeJS.Timeout | undefined = undefined;
@ -104,7 +105,7 @@ export class LdapSync {
);
if (usersToDisable.length) {
void InternalHooksManager.getInstance().onLdapUsersDisabled({
void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_update',
users: usersToDisable.length,
user_ids: usersToDisable,
@ -144,7 +145,7 @@ export class LdapSync {
error: errorMessage,
});
void InternalHooksManager.getInstance().onLdapSyncFinished({
void Container.get(InternalHooks).onLdapSyncFinished({
type: !this.intervalId ? 'scheduled' : `manual_${mode}`,
succeeded: true,
users_synced: usersToCreate.length + usersToUpdate.length + usersToDisable.length,

View file

@ -22,9 +22,10 @@ import {
LDAP_LOGIN_LABEL,
} from './constants';
import type { ConnectionSecurity, LdapConfig } from './types';
import { InternalHooksManager } from '@/InternalHooksManager';
import { jsonParse, LoggerProxy as Logger } from 'n8n-workflow';
import { getLicense } from '@/License';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
/**
* 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();
if (ldapUsers.length) {
await deleteAllLdapIdentities();
void InternalHooksManager.getInstance().onLdapUsersDisabled({
void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_update',
users: ldapUsers.length,
user_ids: ldapUsers.map((user) => user.id),
@ -185,7 +186,7 @@ export const handleLdapInit = async (): Promise<void> => {
if (!isLdapEnabled()) {
const ldapUsers = await getLdapUsers();
if (ldapUsers.length) {
void InternalHooksManager.getInstance().onLdapUsersDisabled({
void Container.get(InternalHooks).onLdapUsersDisabled({
reason: 'ldap_feature_deactivated',
users: ldapUsers.length,
user_ids: ldapUsers.map((user) => user.id),
@ -238,7 +239,7 @@ export const findAndAuthenticateLdapUser = async (
);
} catch (e) {
if (e instanceof Error) {
void InternalHooksManager.getInstance().onLdapLoginSyncFailed({
void Container.get(InternalHooks).onLdapLoginSyncFailed({
error: 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 { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '../helpers';
import type { LdapConfiguration } from '../types';
import { InternalHooksManager } from '@/InternalHooksManager';
import pick from 'lodash.pick';
import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '../constants';
import { InternalHooks } from '@/InternalHooks';
import { Container } from 'typedi';
export const ldapController = express.Router();
@ -42,7 +43,7 @@ ldapController.put('/config', async (req: LdapConfiguration.Update, res: express
const data = await getLdapConfig();
void InternalHooksManager.getInstance().onUserUpdatedLdapSettings({
void Container.get(InternalHooks).onUserUpdatedLdapSettings({
user_id: req.user.id,
...pick(data, NON_SENSIBLE_LDAP_CONFIG_PROPERTIES),
});

View file

@ -31,13 +31,11 @@ import {
CUSTOM_API_CALL_NAME,
inTest,
} from '@/constants';
import {
persistInstalledPackageData,
removePackageFromDatabase,
} from '@/CommunityNodes/packageModel';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { Service } from 'typedi';
export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
@Service()
export class LoadNodesAndCredentials implements INodesAndCredentials {
known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
@ -202,6 +200,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
if (loader.loadedNodes.length > 0) {
// Save info to DB
try {
const { persistInstalledPackageData } = await import('@/CommunityNodes/packageModel');
const installedPackage = await persistInstalledPackageData(loader);
await this.postProcessLoaders();
await this.generateTypesForFrontend();
@ -229,6 +228,7 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
await executeCommand(command);
const { removePackageFromDatabase } = await import('@/CommunityNodes/packageModel');
await removePackageFromDatabase(installedPackage);
if (packageName in this.loaders) {
@ -264,6 +264,9 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
if (loader.loadedNodes.length > 0) {
// Save info to DB
try {
const { persistInstalledPackageData, removePackageFromDatabase } = await import(
'@/CommunityNodes/packageModel'
);
await removePackageFromDatabase(installedPackage);
const newlyInstalledPackage = await persistInstalledPackageData(loader);
await this.postProcessLoaders();
@ -420,14 +423,3 @@ export class LoadNodesAndCredentialsClass implements INodesAndCredentials {
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 type {
INodesAndCredentials,
INodeType,
INodeTypeDescription,
INodeTypes,
@ -8,10 +7,13 @@ import type {
LoadedClass,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { Service } from 'typedi';
import { RESPONSE_ERROR_MESSAGES } from './constants';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
export class NodeTypesClass implements INodeTypes {
constructor(private nodesAndCredentials: INodesAndCredentials) {
@Service()
export class NodeTypes implements INodeTypes {
constructor(private nodesAndCredentials: LoadNodesAndCredentials) {
// Some nodeTypes need to get special parameters applied like the
// polling nodes the polling times
this.applySpecialNodeParameters();
@ -75,18 +77,3 @@ export class NodeTypesClass implements INodeTypes {
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 * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
async function createApiRouter(
version: string,
@ -100,7 +101,7 @@ async function createApiRouter(
if (!user) return false;
void InternalHooksManager.getInstance().onUserInvokedApi({
void Container.get(InternalHooks).onUserInvokedApi({
user_id: user.id,
path: req.path,
method: req.method,

View file

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

View file

@ -7,6 +7,7 @@ import { CredentialsHelper } from '@/CredentialsHelper';
import { CredentialTypes } from '@/CredentialTypes';
import type { CredentialRequest } from '../../../types';
import { toJsonSchema } from './credentials.service';
import { Container } from 'typedi';
export const validCredentialType = (
req: CredentialRequest.Create,
@ -14,7 +15,7 @@ export const validCredentialType = (
next: express.NextFunction,
): express.Response | void => {
try {
CredentialTypes().getByName(req.body.type);
Container.get(CredentialTypes).getByName(req.body.type);
} catch (_) {
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 type { IDependency, IJsonSchema } from '../../../types';
import type { CredentialRequest } from '@/requests';
import { Container } from 'typedi';
export async function getCredentials(credentialId: string): Promise<ICredentialsDb | null> {
return Db.collections.Credentials.findOneBy({ id: credentialId });
@ -62,7 +63,7 @@ export async function saveCredential(
scope: 'credential',
});
await ExternalHooks().run('credentials.create', [encryptedData]);
await Container.get(ExternalHooks).run('credentials.create', [encryptedData]);
return Db.transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(credential);
@ -84,7 +85,7 @@ export async function saveCredential(
}
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);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import {
LoadNodeParameterOptions,
LoadNodeListSearch,
UserSettings,
FileNotFoundError,
} from 'n8n-core';
import type {
@ -56,8 +57,6 @@ import history from 'connect-history-api-fallback';
import config from '@/config';
import * as Queue from '@/Queue';
import { InternalHooksManager } from '@/InternalHooksManager';
import { getCredentialTranslationPath } from '@/TranslationHelpers';
import { getSharedWorkflowIds } from '@/WorkflowHelpers';
import { nodesController } from '@/api/nodes.api';
@ -67,7 +66,6 @@ import {
GENERATED_STATIC_DIR,
inDevelopment,
N8N_VERSION,
NODES_BASE_DIR,
RESPONSE_ERROR_MESSAGES,
TEMPLATES_DIR,
} from '@/constants';
@ -88,6 +86,7 @@ import {
MeController,
OwnerController,
PasswordResetController,
TranslationController,
UsersController,
} from '@/controllers';
@ -113,7 +112,7 @@ import type {
IExecutionsStopData,
IN8nUISettings,
} from '@/Interfaces';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import {
CredentialsHelper,
getCredentialForUser,
@ -122,11 +121,8 @@ import {
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import type { LoadNodesAndCredentialsClass } from '@/LoadNodesAndCredentials';
import type { NodeTypesClass } from '@/NodeTypes';
import { NodeTypes } from '@/NodeTypes';
import * as ResponseHelper from '@/ResponseHelper';
import type { WaitTrackerClass } from '@/WaitTracker';
import { WaitTracker } from '@/WaitTracker';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
@ -135,8 +131,7 @@ import { eventBusRouter } from '@/eventbus/eventBusRoutes';
import { isLogStreamingEnabled } from '@/eventbus/MessageEventBus/MessageEventBusHelper';
import { getLicense } from '@/License';
import { licenseController } from './license/license.controller';
import type { Push } from '@/push';
import { getPushInstance, setupPushServer, setupPushHandler } from '@/push';
import { Push, setupPushServer, setupPushHandler } from '@/push';
import { setupAuthMiddlewares } from './middlewares';
import { initEvents } from './events';
import { ldapController } from './Ldap/routes/ldap.controller.ee';
@ -145,44 +140,51 @@ import { AbstractServer } from './AbstractServer';
import { configureMetrics } from './metrics';
import { setupBasicAuth } from './middlewares/basicAuth';
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
import { PostHogClient } from './posthog';
import { eventBus } from './eventbus';
import { isSamlEnabled } from './Saml/helpers';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
import { getStatusUsingPreviousExecutionStatusMethod } from './executions/executionHelpers';
const exec = promisify(callbackExec);
class Server extends AbstractServer {
endpointPresetCredentials: string;
waitTracker: WaitTrackerClass;
waitTracker: WaitTracker;
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
activeExecutionsInstance: ActiveExecutions;
frontendSettings: IN8nUISettings;
presetCredentialsLoaded: boolean;
loadNodesAndCredentials: LoadNodesAndCredentialsClass;
loadNodesAndCredentials: LoadNodesAndCredentials;
nodeTypes: NodeTypesClass;
nodeTypes: NodeTypes;
credentialTypes: ICredentialTypes;
postHog: PostHogClient;
push: Push;
constructor() {
super();
this.nodeTypes = NodeTypes();
this.credentialTypes = CredentialTypes();
this.loadNodesAndCredentials = LoadNodesAndCredentials();
this.loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
this.credentialTypes = Container.get(CredentialTypes);
this.nodeTypes = Container.get(NodeTypes);
this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.waitTracker = WaitTracker();
this.activeExecutionsInstance = Container.get(ActiveExecutions);
this.waitTracker = Container.get(WaitTracker);
this.postHog = Container.get(PostHogClient);
this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
this.push = getPushInstance();
this.push = Container.get(Push);
if (process.env.E2E_TESTS === 'true') {
this.app.use('/e2e', require('./api/e2e.api').e2eController);
@ -232,6 +234,16 @@ class Server extends AbstractServer {
},
instanceId: '',
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:
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
@ -343,14 +355,16 @@ class Server extends AbstractServer {
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
const logger = LoggerProxy;
const internalHooks = InternalHooksManager.getInstance();
const internalHooks = Container.get(InternalHooks);
const mailer = getMailerInstance();
const postHog = this.postHog;
const controllers = [
new AuthController({ config, internalHooks, repositories, logger }),
new AuthController({ config, internalHooks, repositories, logger, postHog }),
new OwnerController({ config, internalHooks, repositories, logger }),
new MeController({ externalHooks, internalHooks, repositories, logger }),
new PasswordResetController({ config, externalHooks, internalHooks, repositories, logger }),
new TranslationController(config, this.credentialTypes),
new UsersController({
config,
mailer,
@ -359,6 +373,7 @@ class Server extends AbstractServer {
repositories,
activeWorkflowRunner,
logger,
postHog,
}),
];
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.initLicense();
await this.postHog.init(this.frontendSettings.instanceId);
const publicApiEndpoint = config.getEnv('publicApi.path');
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
// ----------------------------------------
@ -1013,6 +987,9 @@ class Server extends AbstractServer {
if (!executions.length) return [];
return executions.map((execution) => {
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
return {
id: execution.id,
workflowId: execution.workflowId,
@ -1047,6 +1024,7 @@ class Server extends AbstractServer {
mode: data.mode,
retryOf: data.retryOf,
startedAt: new Date(data.startedAt),
status: data.status,
});
}
@ -1099,6 +1077,7 @@ class Server extends AbstractServer {
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
} as IExecutionsStopData;
}
@ -1125,6 +1104,7 @@ class Server extends AbstractServer {
? new Date(fullExecutionData.stoppedAt)
: undefined,
finished: fullExecutionData.finished,
status: fullExecutionData.status,
};
return returnData;
@ -1143,6 +1123,7 @@ class Server extends AbstractServer {
startedAt: new Date(result.startedAt),
stoppedAt: result.stoppedAt ? new Date(result.stoppedAt) : undefined,
finished: result.finished,
status: result.status,
};
}
@ -1173,21 +1154,26 @@ class Server extends AbstractServer {
// TODO UM: check if this needs permission check for UM
const identifier = req.params.path;
const binaryDataManager = BinaryDataManager.getInstance();
const binaryPath = binaryDataManager.getBinaryPath(identifier);
let { mode, fileName, mimeType } = req.query;
if (!fileName || !mimeType) {
try {
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
fileName = metadata.fileName;
mimeType = metadata.mimeType;
res.setHeader('Content-Length', metadata.fileSize);
} catch {}
try {
const binaryPath = binaryDataManager.getBinaryPath(identifier);
let { mode, fileName, mimeType } = req.query;
if (!fileName || !mimeType) {
try {
const metadata = await binaryDataManager.getBinaryMetadata(identifier);
fileName = metadata.fileName;
mimeType = metadata.mimeType;
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`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<IN8nUISettings> => {
void InternalHooksManager.getInstance().onFrontendSettingsAPI(
req.headers.sessionid as string,
);
void Container.get(InternalHooks).onFrontendSettingsAPI(req.headers.sessionid as string);
return this.getSettingsForFrontend();
},
@ -1373,6 +1357,6 @@ export async function start(): Promise<void> {
order: { createdAt: 'ASC' },
where: {},
}).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 no-param-reassign */
import type express from 'express';
import { ActiveWebhooks } from 'n8n-core';
import { Service } from 'typedi';
import type {
IWebhookData,
@ -13,16 +12,18 @@ import type {
WorkflowActivateMode,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { ActiveWebhooks } from '@/ActiveWebhooks';
import type { IResponseCallbackData, IWorkflowDb } from '@/Interfaces';
import type { Push } from '@/push';
import { getPushInstance } from '@/push';
import { Push } from '@/push';
import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
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)";
class TestWebhooks {
@Service()
export class TestWebhooks {
private testWebhookData: {
[key: string]: {
sessionId?: string;
@ -286,13 +287,3 @@ class TestWebhooks {
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 { readdir } from 'fs/promises';
import type { Dirent } from 'fs';
import { NODES_BASE_DIR } from '@/constants';
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, '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 ResponseHelper from '@/ResponseHelper';
import type { PublicUser, WhereClause } from '@/Interfaces';
import type { CurrentUser, PublicUser, WhereClause } from '@/Interfaces';
import type { User } from '@db/entities/User';
import { MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH } from '@db/entities/User';
import type { Role } from '@db/entities/Role';
@ -15,6 +15,7 @@ import config from '@/config';
import { getWebhookBaseUrl } from '@/WebhookHelpers';
import { getLicense } from '@/License';
import { RoleService } from '@/role/role.service';
import type { PostHogClient } from '@/posthog';
export async function getWorkflowOwner(workflowId: string): Promise<User> {
const workflowOwnerRole = await RoleService.get({ name: 'owner', scope: 'workflow' });
@ -162,6 +163,31 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
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 {
if (user.isPending) {
user.inviteAcceptUrl = generateUserInviteUrl(inviterId, user.id);

View file

@ -17,7 +17,7 @@ import { DateUtils } from 'typeorm/util/DateUtils';
import config from '@/config';
import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import type {
IExecutionFlattedDb,
IExecutionsStopData,
@ -25,9 +25,11 @@ import type {
} from '@/Interfaces';
import { WorkflowRunner } from '@/WorkflowRunner';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container, Service } from 'typedi';
export class WaitTrackerClass {
activeExecutionsInstance: ActiveExecutions.ActiveExecutions;
@Service()
export class WaitTracker {
activeExecutionsInstance: ActiveExecutions;
private waitingExecutions: {
[key: string]: {
@ -39,7 +41,7 @@ export class WaitTrackerClass {
mainTimer: NodeJS.Timeout;
constructor() {
this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.activeExecutionsInstance = Container.get(ActiveExecutions);
// Poll every 60 seconds a list of upcoming executions
this.mainTimer = setInterval(() => {
@ -142,6 +144,7 @@ export class WaitTrackerClass {
startedAt: new Date(fullExecutionData.startedAt),
stoppedAt: fullExecutionData.stoppedAt ? new Date(fullExecutionData.stoppedAt) : undefined,
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 * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
export class WaitingWebhooks {
async executeWebhook(
@ -78,7 +79,7 @@ export class WaitingWebhooks {
const { workflowData } = fullExecutionData;
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
const workflow = new Workflow({
id: workflowData.id!.toString(),
name: workflowData.name,

View file

@ -52,10 +52,11 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import type { User } from '@db/entities/User';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
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
const executePromise = ActiveExecutions.getInstance().getPostExecutePromise(
const executePromise = Container.get(ActiveExecutions).getPostExecutePromise(
executionId,
) as Promise<IExecutionDb | undefined>;
executePromise

View file

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

View file

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

View file

@ -33,7 +33,7 @@ import PCancelable from 'p-cancelable';
import { join as pathJoin } from 'path';
import { fork } from 'child_process';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config';
import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks';
@ -49,25 +49,25 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { InternalHooksManager } from '@/InternalHooksManager';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import type { Push } from '@/push';
import { getPushInstance } from '@/push';
import { Push } from '@/push';
import { eventBus } from './eventbus';
import { recoverExecutionDataFromEventLogMessages } from './eventbus/MessageEventBus/recoverEvents';
import { Container } from 'typedi';
import { InternalHooks } from './InternalHooks';
export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions;
activeExecutions: ActiveExecutions;
push: Push;
jobQueue: Queue.JobQueue;
constructor() {
this.push = getPushInstance();
this.activeExecutions = ActiveExecutions.getInstance();
this.push = Container.get(Push);
this.activeExecutions = Container.get(ActiveExecutions);
}
/**
@ -130,7 +130,7 @@ export class WorkflowRunner {
const executionFlattedData = await Db.collections.Execution.findOneBy({ id: executionId });
void InternalHooksManager.getInstance().onWorkflowCrashed(
void Container.get(InternalHooks).onWorkflowCrashed(
executionId,
executionMode,
executionFlattedData?.workflowData,
@ -187,14 +187,14 @@ export class WorkflowRunner {
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 externalHooks = ExternalHooks();
const externalHooks = Container.get(ExternalHooks);
postExecutePromise
.then(async (executionData) => {
void InternalHooksManager.getInstance().onWorkflowPostExecute(
void Container.get(InternalHooks).onWorkflowPostExecute(
executionId!,
data.workflowData,
executionData,
@ -241,7 +241,7 @@ export class WorkflowRunner {
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
// 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/unbound-method */
import 'source-map-support/register';
import 'reflect-metadata';
import { Container } from 'typedi';
import type { IProcessMessage } from 'n8n-core';
import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core';
@ -49,11 +51,12 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
import { getLogger } from '@/Logger';
import config from '@/config';
import { InternalHooksManager } from '@/InternalHooksManager';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { getLicense } from './License';
import { InternalHooks } from './InternalHooks';
import { PostHogClient } from './posthog';
class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined;
@ -103,19 +106,20 @@ class WorkflowRunnerProcess {
const userSettings = await UserSettings.prepareUserSettings();
const loadNodesAndCredentials = LoadNodesAndCredentials();
const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
await loadNodesAndCredentials.init();
const nodeTypes = NodeTypes(loadNodesAndCredentials);
const credentialTypes = CredentialTypes(loadNodesAndCredentials);
const nodeTypes = Container.get(NodeTypes);
const credentialTypes = Container.get(CredentialTypes);
CredentialsOverwrites(credentialTypes);
// Load all external hooks
const externalHooks = ExternalHooks();
const externalHooks = Container.get(ExternalHooks);
await externalHooks.init();
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');
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;
try {
@ -250,7 +254,7 @@ class WorkflowRunnerProcess {
const { workflow } = executeWorkflowFunctionOutput;
result = await workflowExecute.processRunExecutionData(workflow);
await externalHooks.run('workflow.postExecute', [result, workflowData, executionId]);
void InternalHooksManager.getInstance().onWorkflowPostExecute(
void Container.get(InternalHooks).onWorkflowPostExecute(
executionId,
workflowData,
result,
@ -508,6 +512,8 @@ process.on('message', async (message: IProcessMessage) => {
workflowRunner.executionIdCallback(message.data.executionId);
}
} catch (error) {
workflowRunner.logger.error(error.message);
// Catch all uncaught errors and forward them to parent process
const executionError = {
...error,

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import {
} from '@/audit/constants';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { Risk } from '@/audit/types';
import { Container } from 'typedi';
async function getCommunityNodeDetails() {
const installedPackages = await getAllInstalledPackages();
@ -32,7 +33,8 @@ async function getCommunityNodeDetails() {
async function getCustomNodeDetails() {
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 });
for (const nodeFile of customNodeFiles) {

View file

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

View file

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

View file

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

View file

@ -1,10 +1,11 @@
import { flags } from '@oclif/command';
import { audit } from '@/audit';
import { RISK_CATEGORIES } from '@/audit/constants';
import { InternalHooksManager } from '@/InternalHooksManager';
import config from '@/config';
import type { Risk } from '@/audit/types';
import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export class SecurityAudit extends BaseCommand {
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));
}
void InternalHooksManager.getInstance().onAuditGeneratedViaCli();
void Container.get(InternalHooks).onAuditGeneratedViaCli();
}
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 { ExecutionBaseError } from 'n8n-workflow';
import * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import * as Db from '@/Db';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner';
@ -13,6 +13,7 @@ import { getInstanceOwner } from '@/UserManagement/UserManagementHelper';
import { findCliWorkflowStart } from '@/utils';
import { initEvents } from '@/events';
import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
export class Execute extends BaseCommand {
static description = '\nExecutes a given workflow';
@ -117,7 +118,7 @@ export class Execute extends BaseCommand {
const workflowRunner = new WorkflowRunner();
const executionId = await workflowRunner.run(runData);
const activeExecutions = ActiveExecutions.getInstance();
const activeExecutions = Container.get(ActiveExecutions);
const data = await activeExecutions.getPostExecutePromise(executionId);
if (data === undefined) {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import validator from 'validator';
import { plainToInstance } from 'class-transformer';
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import {
compareHash,
@ -7,13 +8,13 @@ import {
validatePassword,
} from '@/UserManagement/UserManagementHelper';
import { BadRequestError } from '@/ResponseHelper';
import { User } from '@db/entities/User';
import type { User } from '@db/entities/User';
import { validateEntity } from '@/GenericHelpers';
import { issueCookie } from '@/auth/jwt';
import { Response } from 'express';
import type { Repository } from 'typeorm';
import type { ILogger } from 'n8n-workflow';
import { AuthenticatedRequest, MeRequest } from '@/requests';
import { AuthenticatedRequest, MeRequest, UserUpdatePayload } from '@/requests';
import type {
PublicUser,
IDatabaseCollections,
@ -53,38 +54,40 @@ export class MeController {
* Update the logged-in user's settings, except password.
*/
@Patch('/')
async updateCurrentUser(req: MeRequest.Settings, res: Response): Promise<PublicUser> {
const { email } = req.body;
async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
const { id: userId, email: currentEmail } = req.user;
const payload = plainToInstance(UserUpdatePayload, req.body);
const { email } = payload;
if (!email) {
this.logger.debug('Request to update user email failed because of missing email in payload', {
userId: req.user.id,
payload: req.body,
userId,
payload,
});
throw new BadRequestError('Email is mandatory');
}
if (!validator.isEmail(email)) {
this.logger.debug('Request to update user email failed because of invalid email in payload', {
userId: req.user.id,
userId,
invalidEmail: email,
});
throw new BadRequestError('Invalid email address');
}
const { email: currentEmail } = req.user;
const newUser = new User();
await validateEntity(payload);
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);
const user = await this.userRepository.save(newUser);
this.logger.info('User updated successfully', { userId: user.id });
this.logger.info('User updated successfully', { userId });
await issueCookie(res, user);
const updatedKeys = Object.keys(req.body);
const updatedKeys = Object.keys(payload);
void this.internalHooks.onUserUpdate({
user,
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,
sanitizeUser,
validatePassword,
withFeatureFlags,
} from '@/UserManagement/UserManagementHelper';
import { issueCookie } from '@/auth/jwt';
import { BadRequestError, InternalServerError, NotFoundError } from '@/ResponseHelper';
@ -33,6 +34,7 @@ import type {
} from '@/Interfaces';
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { PostHogClient } from '@/posthog';
@RestController('/users')
export class UsersController {
@ -56,6 +58,8 @@ export class UsersController {
private mailer: UserManagementMailer;
private postHog?: PostHogClient;
constructor({
config,
logger,
@ -64,6 +68,7 @@ export class UsersController {
repositories,
activeWorkflowRunner,
mailer,
postHog,
}: {
config: Config;
logger: ILogger;
@ -75,6 +80,7 @@ export class UsersController {
>;
activeWorkflowRunner: ActiveWorkflowRunner;
mailer: UserManagementMailer;
postHog?: PostHogClient;
}) {
this.config = config;
this.logger = logger;
@ -86,6 +92,7 @@ export class UsersController {
this.sharedWorkflowRepository = repositories.SharedWorkflow;
this.activeWorkflowRunner = activeWorkflowRunner;
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.password.update', [invitee.email, invitee.password]);
return sanitizeUser(updatedUser);
return withFeatureFlags(this.postHog, sanitizeUser(updatedUser));
}
@Get('/')

View file

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

View file

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

View file

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

View file

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

View file

@ -111,6 +111,9 @@ export class User extends AbstractEntity implements IUser {
@AfterLoad()
@AfterUpdate()
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 { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -21,7 +22,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM \`${tablePrefix}workflow_entity\`
`);
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => {
let connections: IConnections =
@ -57,22 +58,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!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

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 { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106779 } from './1674133106779-CreateExecutionMetadataTable';
export const mysqlMigrations = [
@ -69,6 +70,7 @@ export const mysqlMigrations = [
CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
PurgeInvalidWorkflowConnections1675940580449,
CreateExecutionMetadataTable1674133106779,
];

View file

@ -3,6 +3,7 @@ import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/mi
import { NodeTypes } from '@/NodeTypes';
import { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -17,7 +18,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM "${tablePrefix}workflow_entity"
`);
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => {
let connections: IConnections = workflow.connections;
@ -49,22 +50,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!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

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 { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106778 } from './1674133106778-CreateExecutionMetadataTable';
export const postgresMigrations = [
@ -65,6 +66,7 @@ export const postgresMigrations = [
CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
PurgeInvalidWorkflowConnections1675940580449,
CreateExecutionMetadataTable1674133106778,
];

View file

@ -3,6 +3,7 @@ import { getTablePrefix, logMigrationEnd, logMigrationStart } from '@db/utils/mi
import { NodeTypes } from '@/NodeTypes';
import { IConnections, INode } from 'n8n-workflow';
import { getLogger } from '@/Logger';
import { Container } from 'typedi';
export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationInterface {
name = 'PurgeInvalidWorkflowConnections1675940580449';
@ -18,7 +19,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
FROM "${tablePrefix}workflow_entity"
`);
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
workflows.forEach(async (workflow) => {
let connections: IConnections = JSON.parse(workflow.connections);
@ -50,22 +51,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements MigrationIn
!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

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 { PurgeInvalidWorkflowConnections1675940580449 } from './1675940580449-PurgeInvalidWorkflowConnections';
import { AddStatusToExecutions1674138566000 } from './1674138566000-AddStatusToExecutions';
import { MigrateExecutionStatus1676996103000 } from './1676996103000-MigrateExecutionStatus';
import { CreateExecutionMetadataTable1674133106777 } from './1674133106777-CreateExecutionMetadataTable';
const sqliteMigrations = [
@ -63,6 +64,7 @@ const sqliteMigrations = [
CreateLdapEntities1674509946020,
PurgeInvalidWorkflowConnections1675940580449,
AddStatusToExecutions1674138566000,
MigrateExecutionStatus1676996103000,
CreateExecutionMetadataTable1674133106777,
];

View file

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

View file

@ -1,9 +1,10 @@
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { QueryFailedError } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export async function workflowExecutionCompleted(
workflowData: IWorkflowBase,
@ -46,7 +47,7 @@ export async function workflowExecutionCompleted(
};
// Send the metrics
await InternalHooksManager.getInstance().onFirstProductionWorkflowSuccess(metrics);
await Container.get(InternalHooks).onFirstProductionWorkflowSuccess(metrics);
} catch (error) {
if (!(error instanceof QueryFailedError)) {
throw error;
@ -101,5 +102,5 @@ export async function nodeFetchedData(
}
// 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,
} 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 * as ActiveExecutions from '@/ActiveExecutions';
import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config';
import type { User } from '@db/entities/User';
import type { ExecutionEntity } from '@db/entities/ExecutionEntity';
@ -34,6 +34,8 @@ import { WorkflowRunner } from '@/WorkflowRunner';
import * as Db from '@/Db';
import * as GenericHelpers from '@/GenericHelpers';
import { parse } from 'flatted';
import { Container } from 'typedi';
import { getStatusUsingPreviousExecutionStatusMethod } from './executionHelpers';
import { ExecutionMetadata } from '@/databases/entities/ExecutionMetadata';
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.
executingWorkflowIds.push(
...ActiveExecutions.getInstance()
...Container.get(ActiveExecutions)
.getActiveExecutions()
.map(({ id }) => id),
);
@ -262,7 +264,6 @@ export class ExecutionsService {
};
if (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) {
Object.assign(findWhere, { finished: filter.finished });
@ -308,6 +309,7 @@ export class ExecutionsService {
'execution.startedAt',
'execution.stoppedAt',
'execution.workflowData',
'execution.status',
])
.orderBy('execution.id', 'DESC')
.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) {
this.massageFilters(filter as IDataObject);
query = query.andWhere(filter);
@ -352,6 +360,10 @@ export class ExecutionsService {
const nodeExecutionStatus = {};
let lastNodeExecuted;
let executionError;
// fill execution status for old executions that will return null
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
try {
const data = parse(execution.data) as IRunExecutionData;
lastNodeExecuted = data?.resultData?.lastNodeExecuted ?? '';
@ -429,6 +441,10 @@ export class ExecutionsService {
return undefined;
}
if (!execution.status) {
execution.status = getStatusUsingPreviousExecutionStatusMethod(execution);
}
if (req.query.unflattedResponse === 'true') {
return ResponseHelper.unflattenExecutionData(execution);
}
@ -513,7 +529,7 @@ export class ExecutionsService {
}
data.workflowData = workflowData;
const nodeTypes = NodeTypes();
const nodeTypes = Container.get(NodeTypes);
const workflowInstance = new Workflow({
id: workflowData.id as string,
name: workflowData.name,
@ -548,7 +564,7 @@ export class ExecutionsService {
const workflowRunner = new WorkflowRunner();
const retriedExecutionId = await workflowRunner.run(data);
const executionData = await ActiveExecutions.getInstance().getPostExecutePromise(
const executionData = await Container.get(ActiveExecutions).getPostExecutePromise(
retriedExecutionId,
);

View file

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

View file

@ -5,12 +5,13 @@ import { LoggerProxy } from 'n8n-workflow';
import { getLogger } from '@/Logger';
import * as ResponseHelper from '@/ResponseHelper';
import { InternalHooksManager } from '@/InternalHooksManager';
import type { ILicensePostResponse, ILicenseReadResponse } from '@/Interfaces';
import { LicenseService } from './License.service';
import { getLicense } from '@/License';
import type { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { isInstanceOwner } from '@/PublicApi/v1/handlers/users/users.service';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
export const licenseController = express.Router();
@ -115,14 +116,14 @@ licenseController.post(
await license.renew();
} catch (e) {
// 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) {
throw new ResponseHelper.BadRequestError(e.message);
}
}
// 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 {

View file

@ -3,11 +3,12 @@ import jwt from 'jsonwebtoken';
import cookieParser from 'cookie-parser';
import passport from 'passport';
import { Strategy } from 'passport-jwt';
import { sync as globSync } from 'fast-glob';
import { LoggerProxy as Logger } from 'n8n-workflow';
import type { JwtPayload } from '@/Interfaces';
import type { AuthenticatedRequest } from '@/requests';
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 {
isAuthenticatedRequest,
@ -61,6 +62,10 @@ const refreshExpiringCookie: RequestHandler = async (req: AuthenticatedRequest,
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
*/
@ -79,12 +84,7 @@ export const setupAuthMiddlewares = (
// TODO: refactor me!!!
// skip authentication for preflight requests
req.method === 'OPTIONS' ||
req.url === '/index.html' ||
req.url === '/favicon.ico' ||
req.url.startsWith('/css/') ||
req.url.startsWith('/js/') ||
req.url.startsWith('/fonts/') ||
req.url.includes('.svg') ||
staticAssets.includes(req.url.slice(1)) ||
req.url.startsWith(`/${restEndpoint}/settings`) ||
req.url.startsWith(`/${restEndpoint}/login`) ||
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;
if (sessionId !== undefined && connections[sessionId] === undefined) {
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 { Server as WSServer } from 'ws';
import { parse as parseUrl } from 'url';
import { Container, Service } from 'typedi';
import config from '@/config';
import { resolveJwt } from '@/auth/jwt';
import { AUTH_COOKIE_NAME } from '@/constants';
import { SSEPush } from './sse.push';
import { WebSocketPush } from './websocket.push';
import type { Push, PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
export type { Push } from './types';
import type { PushResponse, SSEPushRequest, WebSocketPushRequest } from './types';
import type { IPushDataType } from '@/Interfaces';
const useWebSockets = config.getEnv('push.backend') === 'websocket';
let pushInstance: Push;
export const getPushInstance = () => {
if (!pushInstance) pushInstance = useWebSockets ? new WebSocketPush() : new SSEPush();
return pushInstance;
};
@Service()
export class Push {
private backend = useWebSockets ? new WebSocketPush() : new SSEPush();
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) => {
if (useWebSockets) {
@ -48,7 +62,6 @@ export const setupPushHandler = (
app: Application,
isUserManagementEnabled: boolean,
) => {
const push = getPushInstance();
const endpoint = `/${restEndpoint}/push`;
const pushValidationMiddleware: RequestHandler = async (
@ -89,17 +102,10 @@ export const setupPushHandler = (
next();
};
const push = Container.get(Push);
app.use(
endpoint,
pushValidationMiddleware,
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => {
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');
}
},
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res),
);
};

View file

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

View file

@ -10,11 +10,28 @@ import type {
IWorkflowSettings,
} 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 { Role } from '@db/entities/Role';
import type { User } from '@db/entities/User';
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<
RouteParams = {},
ResponseBody = {},
@ -144,11 +161,7 @@ export declare namespace ExecutionRequest {
// ----------------------------------
export declare namespace MeRequest {
export type Settings = AuthenticatedRequest<
{},
{},
Pick<PublicUser, 'email' | 'firstName' | 'lastName'>
>;
export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>;
export type Password = AuthenticatedRequest<
{},
{},

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
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 { LoggerProxy } from 'n8n-workflow';
import config from '@/config';
@ -10,6 +10,7 @@ import { getLogger } from '@/Logger';
import { getLicense } from '@/License';
import { LicenseService } from '@/license/License.service';
import { N8N_VERSION } from '@/constants';
import { Service } from 'typedi';
type ExecutionTrackDataKey = 'manual_error' | 'manual_success' | 'prod_error' | 'prod_success';
@ -28,16 +29,21 @@ interface IExecutionsBuffer {
};
}
@Service()
export class Telemetry {
private rudderStack?: RudderStack;
private instanceId: string;
private postHog?: PostHog;
private rudderStack?: RudderStack;
private pulseIntervalReference: NodeJS.Timeout;
private executionCountsBuffer: IExecutionsBuffer = {};
constructor(private instanceId: string) {}
constructor(private postHog: PostHogClient) {}
setInstanceId(instanceId: string) {
this.instanceId = instanceId;
}
async init() {
const enabled = config.getEnv('diagnostics.enabled');
@ -58,12 +64,6 @@ export class Telemetry {
const { default: RudderStack } = await import('@rudderstack/rudder-sdk-node');
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();
}
}
@ -137,10 +137,8 @@ export class Telemetry {
async trackN8nStop(): Promise<void> {
clearInterval(this.pulseIntervalReference);
void this.track('User instance stopped');
return new Promise<void>((resolve) => {
if (this.postHog) {
this.postHog.shutdown();
}
return new Promise<void>(async (resolve) => {
await this.postHog.stop();
if (this.rudderStack) {
this.rudderStack.flush(resolve);
@ -192,11 +190,7 @@ export class Telemetry {
};
if (withPostHog) {
this.postHog?.capture({
distinctId: payload.userId,
sendFeatureFlags: true,
...payload,
});
this.postHog?.track(payload);
}
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
getCountsBuffer(): IExecutionsBuffer {
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 { v4 as uuid } from 'uuid';
import * as Db from '@/Db';
import { InternalHooksManager } from '@/InternalHooksManager';
import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import config from '@/config';
@ -18,6 +17,8 @@ import { EECredentialsService as EECredentials } from '../credentials/credential
import type { IExecutionPushResponse } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers';
import { In } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
// eslint-disable-next-line @typescript-eslint/naming-convention
export const EEWorkflowController = express.Router();
@ -75,7 +76,7 @@ EEWorkflowController.put(
}
});
void InternalHooksManager.getInstance().onWorkflowSharingUpdate(
void Container.get(InternalHooks).onWorkflowSharingUpdate(
workflowId,
req.user.id,
shareWithIds,
@ -126,7 +127,7 @@ EEWorkflowController.post(
await validateEntity(newWorkflow);
await ExternalHooks().run('workflow.create', [newWorkflow]);
await Container.get(ExternalHooks).run('workflow.create', [newWorkflow]);
const { tags: tagIds } = req.body;
@ -190,8 +191,8 @@ EEWorkflowController.post(
});
}
await ExternalHooks().run('workflow.afterCreate', [savedWorkflow]);
void InternalHooksManager.getInstance().onWorkflowCreated(req.user, newWorkflow, false);
await Container.get(ExternalHooks).run('workflow.afterCreate', [savedWorkflow]);
void Container.get(InternalHooks).onWorkflowCreated(req.user, newWorkflow, false);
return savedWorkflow;
}),

View file

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

View file

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

View file

@ -2,10 +2,17 @@ import * as Db from '@/Db';
import { Reset } from '@/commands/user-management/reset';
import type { Role } from '@db/entities/Role';
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;
beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
mockInstance(NodeTypes);
await testDb.init();
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 { InstalledNodes } from '@db/entities/InstalledNodes';
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', () => {
return {
@ -213,7 +219,7 @@ test('POST /nodes should allow installing packages that could not be loaded', as
mocked(hasPackageLoaded).mockReturnValueOnce(false);
mocked(checkNpmPackageStatus).mockResolvedValueOnce({ status: 'OK' });
jest.spyOn(LoadNodesAndCredentials(), 'loadNpmModule').mockImplementationOnce(mockedEmptyPackage);
mockLoadNodesAndCredentials.loadNpmModule.mockImplementationOnce(mockedEmptyPackage);
const { statusCode } = await authAgent(ownerShell).post('/nodes').send({
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 () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const removeSpy = jest
.spyOn(LoadNodesAndCredentials(), 'removeNpmModule')
.mockImplementationOnce(jest.fn());
const removeSpy = mockLoadNodesAndCredentials.removeNpmModule.mockImplementationOnce(jest.fn());
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 () => {
const ownerShell = await testDb.createUserShell(globalOwnerRole);
const updateSpy = jest
.spyOn(LoadNodesAndCredentials(), 'updateNpmModule')
.mockImplementationOnce(mockedEmptyPackage);
const updateSpy =
mockLoadNodesAndCredentials.updateNpmModule.mockImplementationOnce(mockedEmptyPackage);
mocked(findInstalledPackage).mockImplementationOnce(mockedEmptyPackage);

View file

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

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