chore: merge master

This commit is contained in:
Alex Grozav 2023-07-18 12:15:46 +03:00
commit b86cc334cd
734 changed files with 34301 additions and 10890 deletions

View file

@ -10,3 +10,5 @@ packages/**/.turbo
.git .git
.github .github
*.tsbuildinfo *.tsbuildinfo
packages/cli/dist/**/e2e.*
packages/cli/dist/ReloadNodesAndCredentials.*

View file

@ -23,10 +23,10 @@ A clear and concise description of what you expected to happen.
**Environment (please complete the following information):** **Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux 22.04] - OS: [e.g. Ubuntu Linux 22.04]
- n8n Version [e.g. 0.200.1] - n8n Version [e.g. 1.0.1]
- Node.js Version [e.g. 16.17.0] - Node.js Version [e.g. 18.16.0]
- Database system [e.g. SQLite; n8n uses SQLite as default otherwise changed] - Database system [e.g. SQLite; n8n uses SQLite as default otherwise changed]
- Operation mode [e.g. own; operation modes are `own`, `main` and `queue`. Default is `own`] - Operation mode [e.g. own; operation modes are `own`, `main` and `queue`. Default is `main`]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -17,6 +17,18 @@ const filterAsync = async (asyncPredicate, arr) => {
return filterResults.filter(({shouldKeep}) => shouldKeep).map(({item}) => item); return filterResults.filter(({shouldKeep}) => shouldKeep).map(({item}) => item);
} }
const isAbstractClass = (node) => {
if (ts.isClassDeclaration(node)) {
return node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword) || false;
}
return false;
}
const isAbstractMethod = (node) => {
return ts.isMethodDeclaration(node) && Boolean(node.modifiers?.find((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword));
}
// Function to check if a file has a function declaration, function expression, object method or class // Function to check if a file has a function declaration, function expression, object method or class
const hasFunctionOrClass = async filePath => { const hasFunctionOrClass = async filePath => {
const fileContent = await readFileAsync(filePath, 'utf-8'); const fileContent = await readFileAsync(filePath, 'utf-8');
@ -24,7 +36,13 @@ const hasFunctionOrClass = async filePath => {
let hasFunctionOrClass = false; let hasFunctionOrClass = false;
const visit = node => { const visit = node => {
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node) || ts.isClassDeclaration(node)) { if (
ts.isFunctionDeclaration(node)
|| ts.isFunctionExpression(node)
|| ts.isArrowFunction(node)
|| (ts.isMethodDeclaration(node) && !isAbstractMethod(node))
|| (ts.isClassDeclaration(node) && !isAbstractClass(node))
) {
hasFunctionOrClass = true; hasFunctionOrClass = true;
} }
node.forEachChild(visit); node.forEachChild(visit);

View file

@ -1,8 +1,10 @@
{ {
"dependencies": { "dependencies": {
"conventional-changelog-cli": "^2.2.2", "add-stream": "^1.0.0",
"glob": "^10.2.7", "conventional-changelog": "^4.0.0",
"semver": "^7.3.8", "glob": "^10.3.0",
"semver": "^7.5.2",
"tempfile": "^5.0.0",
"typescript": "*" "typescript": "*"
} }
} }

42
.github/scripts/update-changelog.mjs vendored Normal file
View file

@ -0,0 +1,42 @@
import addStream from 'add-stream';
import createTempFile from 'tempfile';
import conventionalChangelog from 'conventional-changelog';
import { resolve } from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import stream from 'stream';
import { promisify } from 'util';
import packageJson from '../../package.json' assert { type: 'json' };
const pipeline = promisify(stream.pipeline);
const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const fullChangelogFile = resolve(baseDir, 'CHANGELOG.md');
const versionChangelogFile = resolve(baseDir, `CHANGELOG-${packageJson.version}.md`);
const changelogStream = conventionalChangelog({
preset: 'angular',
releaseCount: 1,
tagPrefix: 'n8n@',
transform: (commit, callback) => {
callback(null, commit.header.includes('(no-changelog)') ? undefined : commit);
},
}).on('error', (err) => {
console.error(err.stack);
process.exit(1);
});
// We need to duplicate the stream here to pipe the changelog into two separate files
const stream1 = new stream.PassThrough();
const stream2 = new stream.PassThrough();
changelogStream.pipe(stream1);
changelogStream.pipe(stream2);
await pipeline(stream1, createWriteStream(versionChangelogFile));
// Since we can't read and write from the same file at the same time,
// we use a temporary file to output the updated changelog to.
const tmpFile = createTempFile();
await pipeline(stream2, addStream(createReadStream(fullChangelogFile)), createWriteStream(tmpFile)),
await pipeline(createReadStream(tmpFile), createWriteStream(fullChangelogFile));

View file

@ -0,0 +1,18 @@
name: Check Issue Template
on:
issues:
types: [opened, edited]
jobs:
check-issue:
name: Check Issue Template
runs-on: ubuntu-latest
steps:
- name: Run Check Issue Template
uses: n8n-io/GH-actions-playground@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

22
.github/workflows/checklist.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: PR Checklist
on:
pull_request_target:
types:
- opened
- synchronize
branches:
- master
jobs:
checklist_job:
runs-on: ubuntu-latest
name: Checklist job
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Checklist
uses: wyozi/contextual-qa-checklist-action@master
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}
comment-footer: Make sure to check off this list before asking for review.

View file

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x, 18.x] node-version: [18.x, 20.x]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View file

@ -1,4 +1,4 @@
name: Build, unit/smoke test and lint branch name: Build, unit test and lint branch
on: [pull_request] on: [pull_request]
@ -107,29 +107,3 @@ jobs:
env: env:
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event.pull_request.base.ref }} ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event.pull_request.base.ref }}
run: pnpm lint run: pnpm lint
smoke-test:
name: E2E [Electron/Node 18]
uses: ./.github/workflows/e2e-reusable.yml
with:
branch: ${{ github.event.pull_request.base.ref }}
user: ${{ github.event.inputs.user || 'PR User' }}
spec: ${{ github.event.inputs.spec || 'e2e/0-smoke.cy.ts' }}
record: false
parallel: false
pr_number: ${{ github.event.number }}
containers: '[1]'
secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
checklist_job:
runs-on: ubuntu-latest
name: Checklist job
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Checklist
uses: wyozi/contextual-qa-checklist-action@master
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}
comment-footer: Make sure to check off this list before asking for review.

View file

@ -7,10 +7,11 @@ on:
description: 'Node.js version to build this image with.' description: 'Node.js version to build this image with.'
type: choice type: choice
required: true required: true
default: '16' default: '18'
options: options:
- '16' - '16'
- '18' - '18'
- '20'
jobs: jobs:
build: build:

View file

@ -1,46 +0,0 @@
name: Docker Image - V1 RC
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: release-v1
- uses: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- run: npm install --prefix=.github/scripts --no-package-lock
- name: Bump package versions to 1.0.0
run: |
RELEASE_TYPE=major node .github/scripts/bump-versions.mjs
pnpm i --lockfile-only
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./docker/images/n8n-custom/Dockerfile
platforms: linux/amd64
provenance: false
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:1.0.0-rc
no-cache: true

View file

@ -8,10 +8,6 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
docker-context: ['', '-debian']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -41,12 +37,12 @@ jobs:
- name: Build - name: Build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: ./docker/images/n8n${{ matrix.docker-context }} context: ./docker/images/n8n
build-args: | build-args: |
N8N_VERSION=${{ steps.vars.outputs.tag }} N8N_VERSION=${{ steps.vars.outputs.tag }}
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
provenance: false provenance: false
push: true push: true
tags: | tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}${{ matrix.docker-context }} ${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}${{ matrix.docker-context }} ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}

View file

@ -35,37 +35,33 @@ jobs:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.inputs.base-branch }} 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: pnpm/action-setup@v2.2.4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
- run: npm install --prefix=.github/scripts --no-package-lock - run: npm install --prefix=.github/scripts --no-package-lock
- name: Bump package versions - name: Bump package versions
run: | run: |
echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> $GITHUB_ENV echo "NEXT_RELEASE=$(node .github/scripts/bump-versions.mjs)" >> $GITHUB_ENV
pnpm i --lockfile-only
env: env:
RELEASE_TYPE: ${{ github.event.inputs.release-type }} RELEASE_TYPE: ${{ github.event.inputs.release-type }}
- name: Generate Changelog - name: Update Changelog
run: npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -t n8n@ run: node .github/scripts/update-changelog.mjs
- name: Push the base branch
run: |
git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }}
- name: Push the release branch, and Create the PR - name: Push the release branch, and Create the PR
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
base: 'release/${{ github.event.inputs.release-type }}' base: 'release/${{ env.NEXT_RELEASE }}'
branch: 'release/${{ env.NEXT_RELEASE }}' branch: '${{ env.NEXT_RELEASE }}-pr'
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}' commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
delete-branch: true delete-branch: true
labels: 'release' labels: 'release'
title: ':rocket: Release ${{ env.NEXT_RELEASE }}' title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
# 'TODO: add generated changelog to the body. create a script to generate custom changelog' body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md'
body: ''
# TODO: post PR link to slack

View file

@ -5,8 +5,7 @@ on:
types: types:
- closed - closed
branches: branches:
- 'release/patch' - 'release/*'
- 'release/minor'
jobs: jobs:
publish-release: publish-release:
@ -50,6 +49,7 @@ jobs:
tag: 'n8n@${{env.RELEASE}}' tag: 'n8n@${{env.RELEASE}}'
prerelease: true prerelease: true
makeLatest: false makeLatest: false
body: ${{github.event.pull_request.body}}
- name: Trigger a release note - name: Trigger a release note
continue-on-error: true continue-on-error: true

1
.gitignore vendored
View file

@ -20,3 +20,4 @@ packages/**/.turbo
cypress/videos/* cypress/videos/*
cypress/screenshots/* cypress/screenshots/*
*.swp *.swp
CHANGELOG-*.md

View file

@ -1,3 +1,122 @@
## [1.0.1](https://github.com/n8n-io/n8n/compare/n8n@1.0.0...n8n@1.0.1) (2023-07-05)
### Bug Fixes
* **core:** Fix credentials test ([#6569](https://github.com/n8n-io/n8n/issues/6569)) ([8f244df](https://github.com/n8n-io/n8n/commit/8f244df0f9efcb087a78dd8d9481489c484c77b7))
* **core:** Fix migrations for MySQL/MariaDB ([#6591](https://github.com/n8n-io/n8n/issues/6591)) ([b9da67b](https://github.com/n8n-io/n8n/commit/b9da67b653bf19f39d0d1506d3140c71432efaed))
* **core:** Make node execution order configurable, and backward-compatible ([#6507](https://github.com/n8n-io/n8n/issues/6507)) ([d97edbc](https://github.com/n8n-io/n8n/commit/d97edbcffa966a693548eed033ac41d4a404fc23))
* **core:** Update pruning related config defaults for v1 ([#6577](https://github.com/n8n-io/n8n/issues/6577)) ([ffb4e47](https://github.com/n8n-io/n8n/commit/ffb4e470b56222ae11891d478e96ea9c31675afe))
* **editor:** Restore expression completions ([#6566](https://github.com/n8n-io/n8n/issues/6566)) ([516e572](https://github.com/n8n-io/n8n/commit/516e5728f73da6393defe7633533cc142c531c7a))
* **editor:** Show retry information in execution list only when it exists ([#6587](https://github.com/n8n-io/n8n/issues/6587)) ([2580286](https://github.com/n8n-io/n8n/commit/2580286a198e53c3bf3db6e56faed301b606db07))
* **Sendy Node:** Fix issue with brand id not being sent ([#6530](https://github.com/n8n-io/n8n/issues/6530)) ([b9e5211](https://github.com/n8n-io/n8n/commit/b9e52117355d939e77a2e3c59a7f67ac21e31b22))
* **Strapi Node:** Fix issue with pagination ([#4991](https://github.com/n8n-io/n8n/issues/4991)) ([4253b48](https://github.com/n8n-io/n8n/commit/4253b48b26d1625cd2fb7f38159f9528cea45f34))
* **XML Node:** Fix issue with not returning valid data ([#6565](https://github.com/n8n-io/n8n/issues/6565)) ([c2b9d5a](https://github.com/n8n-io/n8n/commit/c2b9d5ac506375ecc316e8c79a3ce0bf143e9406))
### Features
* Add missing input panels to some trigger nodes ([#6518](https://github.com/n8n-io/n8n/issues/6518)) ([3b12864](https://github.com/n8n-io/n8n/commit/3b12864460a458f23b57a6f3f4b40d0d364ef6e6))
# [1.0.0](https://github.com/n8n-io/n8n/compare/n8n@0.234.0...n8n@1.0.0) (2023-06-27)
### ⚠️ BREAKING CHANGES
* **core** Docker containers now run as the user `node` instead of `root` ([#6365](https://github.com/n8n-io/n8n/pull/6365)) ([f636616](https://github.com/n8n-io/n8n/commit/f6366160a476f42cb0612d10c5777a154d8665dd))
* **core** Drop `debian` and `rhel7` images ([#6365](https://github.com/n8n-io/n8n/pull/6365)) ([f636616](https://github.com/n8n-io/n8n/commit/f6366160a476f42cb0612d10c5777a154d8665dd))
* **core** Drop support for deprecated `WEBHOOK_TUNNEL_URL` env variable ([#6363](https://github.com/n8n-io/n8n/pull/6363))
* **core** Execution mode defaults to `main` now, instead of `own` ([#6363](https://github.com/n8n-io/n8n/pull/6363))
* **core** Default push backend is `websocket` now, instead of `sse` ([#6363](https://github.com/n8n-io/n8n/pull/6363))
* **core** Stop loading custom/community nodes from n8n's `node_modules` folder ([#6396](https://github.com/n8n-io/n8n/pull/6396)) ([a45a2c8](https://github.com/n8n-io/n8n/commit/a45a2c8c41eb7ffb2d62d5a8877c34eb45799fa9))
* **core** User management is mandatory now. basic-auth, external-jwt-auth, and no-auth options are removed ([#6362](https://github.com/n8n-io/n8n/pull/6362)) ([8c008f5](https://github.com/n8n-io/n8n/commit/8c008f5d2217030e93d79e2baca0f2965d4d643e))
* **core** Allow syntax errors and expression errors to fail executions ([#6352](https://github.com/n8n-io/n8n/pull/6352)) ([1197811](https://github.com/n8n-io/n8n/commit/1197811a1e3bc4ad7464d53d7e4860d0e62335a3))
* **core** Drop support for `request` library and `N8N_USE_DEPRECATED_REQUEST_LIB` env variable ([#6413](https://github.com/n8n-io/n8n/pull/6413)) ([632ea27](https://github.com/n8n-io/n8n/commit/632ea275b7fa352d4af23339208bed66bb948da8))
* **core** Make date extensions outputs match inputs ([#6435](https://github.com/n8n-io/n8n/pull/6435)) ([85372aa](https://github.com/n8n-io/n8n/commit/85372aabdfc52493504d4723ee1829e2ea15151d))
* **core** Drop support for `executeSingle` method on nodes ([#4853](https://github.com/n8n-io/n8n/pull/4853)) ([9194d8b](https://github.com/n8n-io/n8n/commit/9194d8bb0ecf81e52d47ddfc4b75dc4e0efd492d))
* **core** Change data processing for multi-input-nodes ([#4238](https://github.com/n8n-io/n8n/pull/4238)) ([b8458a5](https://github.com/n8n-io/n8n/commit/b8458a53f66b79903f0fdb168f6febdefb36d13a))
### Bug Fixes
* **core:** All migrations should run in a transaction ([#6519](https://github.com/n8n-io/n8n/issues/6519)) ([e152cfe](https://github.com/n8n-io/n8n/commit/e152cfe27cf3396f4b278614f1d46d9dd723f36e))
* **Edit Image Node:** Fix transparent operation ([#6513](https://github.com/n8n-io/n8n/issues/6513)) ([4a4bcbc](https://github.com/n8n-io/n8n/commit/4a4bcbca298bf90c54d3597103e6a231855abbd2))
* **Google Drive Node:** URL parsing ([#6527](https://github.com/n8n-io/n8n/issues/6527)) ([18aa9f3](https://github.com/n8n-io/n8n/commit/18aa9f3c62149cd603c560c2944c3146cd31e9e7))
* **Google Sheets Node:** Incorrect read of 0 and false ([#6525](https://github.com/n8n-io/n8n/issues/6525)) ([b6202b5](https://github.com/n8n-io/n8n/commit/b6202b5585f864d97dc114e1e49a6a7dae5c674a))
* **Merge Node:** Enrich input 2 fix ([#6526](https://github.com/n8n-io/n8n/issues/6526)) ([70822ce](https://github.com/n8n-io/n8n/commit/70822ce988543476719089c132e1d10af0d03e78))
* **Notion Node:** Version fix ([#6531](https://github.com/n8n-io/n8n/issues/6531)) ([d3d8522](https://github.com/n8n-io/n8n/commit/d3d8522e8f0c702f56997667a252892296540450))
* Show error when referencing node that exist but has not been executed ([#6496](https://github.com/n8n-io/n8n/issues/6496)) ([3db2707](https://github.com/n8n-io/n8n/commit/3db2707b8e47ea539f4f6c40497a928b51b40274))
### Features
* **core:** Change node execution order (most top-left one first) ([#6246](https://github.com/n8n-io/n8n/issues/6246)) ([0287d5b](https://github.com/n8n-io/n8n/commit/0287d5becdce30a9c0de2a0d6ad4a0db50e198d7))
* **core:** Remove conditional defaults in V1 release ([#6363](https://github.com/n8n-io/n8n/issues/6363)) ([f636616](https://github.com/n8n-io/n8n/commit/f6366160a476f42cb0612d10c5777a154d8665dd))
* **editor:** Add v1 banner ([#6443](https://github.com/n8n-io/n8n/issues/6443)) ([0fe415a](https://github.com/n8n-io/n8n/commit/0fe415add2baa8e70e29087f7a90312bd1ab38af))
* **editor:** SQL editor overhaul ([#6282](https://github.com/n8n-io/n8n/issues/6282)) ([beedfb6](https://github.com/n8n-io/n8n/commit/beedfb609ccde2ef202e08566580a2e1a6b6eafa))
* **HTTP Request Node:** Notice about dev console ([#6516](https://github.com/n8n-io/n8n/issues/6516)) ([d431117](https://github.com/n8n-io/n8n/commit/d431117c9e5db9ff0ec6a1e7371bbf58698957c9))
# [0.236.0](https://github.com/n8n-io/n8n/compare/n8n@0.235.0...n8n@0.236.0) (2023-07-05)
### Bug Fixes
* **Brevo Node:** Rename SendInBlue node to Brevo node ([#6521](https://github.com/n8n-io/n8n/issues/6521)) ([e63b398](https://github.com/n8n-io/n8n/commit/e63b3982d200ade34461b9159eb1e988f494c025))
* **core:** Fix credentials test ([#6569](https://github.com/n8n-io/n8n/issues/6569)) ([1abd172](https://github.com/n8n-io/n8n/commit/1abd172f73e171e37c4cc3ccfaa395c6a46bdf48))
* **core:** Fix migrations for MySQL/MariaDB ([#6591](https://github.com/n8n-io/n8n/issues/6591)) ([29882a6](https://github.com/n8n-io/n8n/commit/29882a6f39dddcd1c8c107c20a548ce8dc665cba))
* **core:** Improve the performance of last 2 sqlite migrations ([#6522](https://github.com/n8n-io/n8n/issues/6522)) ([31cba87](https://github.com/n8n-io/n8n/commit/31cba87d307183d613890c7e6d627636b5280b52))
* **core:** Remove typeorm patches, but still enforce transactions on every migration ([#6594](https://github.com/n8n-io/n8n/issues/6594)) ([9def7a7](https://github.com/n8n-io/n8n/commit/9def7a729b52cd6b4698c47e190e9e2bd7894da5)), closes [#6519](https://github.com/n8n-io/n8n/issues/6519)
* **core:** Use owners file to export wf owners ([#6547](https://github.com/n8n-io/n8n/issues/6547)) ([4b755fb](https://github.com/n8n-io/n8n/commit/4b755fb0b441a37eb804c9e70d4b071a341f7155))
* **editor:** Show retry information in execution list only when it exists ([#6587](https://github.com/n8n-io/n8n/issues/6587)) ([3ca66be](https://github.com/n8n-io/n8n/commit/3ca66be38082e7a3866d53d07328be58e913067f))
* **Salesforce Node:** Fix typo for adding a contact to a campaign ([#6598](https://github.com/n8n-io/n8n/issues/6598)) ([7ffe3cb](https://github.com/n8n-io/n8n/commit/7ffe3cb36adeecaca6cc6ddf067a701ee55c18d1))
* **Strapi Node:** Fix issue with pagination ([#4991](https://github.com/n8n-io/n8n/issues/4991)) ([54444fa](https://github.com/n8n-io/n8n/commit/54444fa388da12d75553e66e53a8cf6f8a99b6fc))
* **XML Node:** Fix issue with not returning valid data ([#6565](https://github.com/n8n-io/n8n/issues/6565)) ([cdd215f](https://github.com/n8n-io/n8n/commit/cdd215f642b47413c05f229e641074d0d4048f68))
### Features
* Add crowd.dev node and trigger node ([#6082](https://github.com/n8n-io/n8n/issues/6082)) ([238a78f](https://github.com/n8n-io/n8n/commit/238a78f0582dbf439a9799de0edcb2e9bef29978))
* Add various source control improvements ([#6533](https://github.com/n8n-io/n8n/issues/6533)) ([68fdc20](https://github.com/n8n-io/n8n/commit/68fdc2078928be478a286774f2889feba1c3f5fe))
* **HTTP Request Node:** New http request generic custom auth credential ([#5798](https://github.com/n8n-io/n8n/issues/5798)) ([b17b458](https://github.com/n8n-io/n8n/commit/b17b4582a059104665888a2369c3e2256db4c1ed))
* **Microsoft To Do Node:** Add an option to set a reminder when creating a task ([#5757](https://github.com/n8n-io/n8n/issues/5757)) ([b19833d](https://github.com/n8n-io/n8n/commit/b19833d673bd554ba86c0b234e8d13633912563a))
* **Notion Node:** Add option to update icon when updating a page ([#5670](https://github.com/n8n-io/n8n/issues/5670)) ([225e849](https://github.com/n8n-io/n8n/commit/225e849960ce65d7f85b482f05fb3d7ffb4f9427))
* **Strava Node:** Add hide_from_home field in Activity Update ([#5883](https://github.com/n8n-io/n8n/issues/5883)) ([7495e31](https://github.com/n8n-io/n8n/commit/7495e31a5b25e97683c7ea38225ba253d8fae8b7))
* **Twitter Node:** Node overhaul ([#4788](https://github.com/n8n-io/n8n/issues/4788)) ([42721db](https://github.com/n8n-io/n8n/commit/42721dba80077fb796086a2bf0ecce256bf3a50f))
# [0.235.0](https://github.com/n8n-io/n8n/compare/n8n@0.234.0...n8n@0.235.0) (2023-06-28)
### Bug Fixes
* **core:** Add empty credential value marker to show empty pw field ([#6532](https://github.com/n8n-io/n8n/issues/6532)) ([9294e2d](https://github.com/n8n-io/n8n/commit/9294e2da3c7c99c2099f5865e610fa7217bf06be))
* **core:** All migrations should run in a transaction ([#6519](https://github.com/n8n-io/n8n/issues/6519)) ([e152cfe](https://github.com/n8n-io/n8n/commit/e152cfe27cf3396f4b278614f1d46d9dd723f36e))
* **core:** Rename to credential_stubs and variable_stubs.json ([#6528](https://github.com/n8n-io/n8n/issues/6528)) ([b06462f](https://github.com/n8n-io/n8n/commit/b06462f4415bd1143a00b4a66e6e626da8c52196))
* **Edit Image Node:** Fix transparent operation ([#6513](https://github.com/n8n-io/n8n/issues/6513)) ([4a4bcbc](https://github.com/n8n-io/n8n/commit/4a4bcbca298bf90c54d3597103e6a231855abbd2))
* **editor:** Add default author name and email to source control settings ([#6543](https://github.com/n8n-io/n8n/issues/6543)) ([e1a02c7](https://github.com/n8n-io/n8n/commit/e1a02c76257de30e08878279dea33d7854d46938))
* **editor:** Change default branchColor and remove label ([#6541](https://github.com/n8n-io/n8n/issues/6541)) ([186271e](https://github.com/n8n-io/n8n/commit/186271e939bca19ec9c94d9455e9430d8b8cf9d7))
* **Google Drive Node:** URL parsing ([#6527](https://github.com/n8n-io/n8n/issues/6527)) ([d9ed0b3](https://github.com/n8n-io/n8n/commit/d9ed0b31b538320a67ee4e5c0cae34656c9f4334))
* **Google Sheets Node:** Incorrect read of 0 and false ([#6525](https://github.com/n8n-io/n8n/issues/6525)) ([806d134](https://github.com/n8n-io/n8n/commit/806d13460240abe94843e569b1820cd8d0d8edd1))
* **Merge Node:** Enrich input 2 fix ([#6526](https://github.com/n8n-io/n8n/issues/6526)) ([c82c7f1](https://github.com/n8n-io/n8n/commit/c82c7f19128df3a11d6d0f18e8d8dab57e6a3b8f))
* **Notion Node:** Version fix ([#6531](https://github.com/n8n-io/n8n/issues/6531)) ([38dc784](https://github.com/n8n-io/n8n/commit/38dc784d2eed25aae777c5c3c3fda1a35e20bd24))
* **Sendy Node:** Fix issue with brand id not being sent ([#6530](https://github.com/n8n-io/n8n/issues/6530)) ([2e8dfb8](https://github.com/n8n-io/n8n/commit/2e8dfb86d4636781b319d6190e8be12e7661ee16))
### Features
* Add missing input panels to some trigger nodes ([#6518](https://github.com/n8n-io/n8n/issues/6518)) ([fdf8a42](https://github.com/n8n-io/n8n/commit/fdf8a428ed38bb3ceb2bc0e50b002b34843d8fc4))
* **editor:** Prevent saving of workflow when canvas is loading ([#6497](https://github.com/n8n-io/n8n/issues/6497)) ([f89ef83](https://github.com/n8n-io/n8n/commit/f89ef83c766fafb1d0497ed91a74b93e8d2af1ec))
* **editor:** SQL editor overhaul ([#6282](https://github.com/n8n-io/n8n/issues/6282)) ([beedfb6](https://github.com/n8n-io/n8n/commit/beedfb609ccde2ef202e08566580a2e1a6b6eafa))
* **Google Drive Node:** Overhaul ([#5941](https://github.com/n8n-io/n8n/issues/5941)) ([d70a1cb](https://github.com/n8n-io/n8n/commit/d70a1cb0c82ee0a4b92776684c6c9079020d028f))
* **HTTP Request Node:** Notice about dev console ([#6516](https://github.com/n8n-io/n8n/issues/6516)) ([d431117](https://github.com/n8n-io/n8n/commit/d431117c9e5db9ff0ec6a1e7371bbf58698957c9))
* **Matrix Node:** Allow setting filename if the binary data has none ([#6536](https://github.com/n8n-io/n8n/issues/6536)) ([8b76e98](https://github.com/n8n-io/n8n/commit/8b76e980852062b192a95593035697c43d6f808e))
# [0.234.0](https://github.com/n8n-io/n8n/compare/n8n@0.233.0...n8n@0.234.0) (2023-06-22) # [0.234.0](https://github.com/n8n-io/n8n/compare/n8n@0.233.0...n8n@0.234.0) (2023-06-22)

View file

@ -54,8 +54,8 @@ The most important directories:
## Development setup ## Development setup
If you want to change or extend n8n you have to make sure that all needed If you want to change or extend n8n you have to make sure that all the needed
dependencies are installed and the packages get linked correctly. Here a short guide on how that can be done: dependencies are installed and the packages get linked correctly. Here's a short guide on how that can be done:
### Requirements ### Requirements
@ -69,7 +69,7 @@ dependencies are installed and the packages get linked correctly. Here a short g
##### pnpm workspaces ##### pnpm workspaces
n8n is split up in different modules which are all in a single mono repository. n8n is split up into different modules which are all in a single mono repository.
To facilitate the module management, [pnpm workspaces](https://pnpm.io/workspaces) are used. To facilitate the module management, [pnpm workspaces](https://pnpm.io/workspaces) are used.
This automatically sets up file-links between modules which depend on each other. This automatically sets up file-links between modules which depend on each other.
@ -113,24 +113,24 @@ No additional packages required.
> **IMPORTANT**: All the steps below have to get executed at least once to get the development setup up and running! > **IMPORTANT**: All the steps below have to get executed at least once to get the development setup up and running!
Now that everything n8n requires to run is installed the actual n8n code can be Now that everything n8n requires to run is installed, the actual n8n code can be
checked out and set up: checked out and set up:
1. [Fork](https://guides.github.com/activities/forking/#fork) the n8n repository 1. [Fork](https://guides.github.com/activities/forking/#fork) the n8n repository.
2. Clone your forked repository 2. Clone your forked repository:
``` ```
git clone https://github.com/<your_github_username>/n8n.git git clone https://github.com/<your_github_username>/n8n.git
``` ```
3. Go into repository folder 3. Go into repository folder:
``` ```
cd n8n cd n8n
``` ```
4. Add the original n8n repository as `upstream` to your forked repository 4. Add the original n8n repository as `upstream` to your forked repository:
``` ```
git remote add upstream https://github.com/n8n-io/n8n.git git remote add upstream https://github.com/n8n-io/n8n.git
@ -172,13 +172,13 @@ automatically build your code, restart the backend and refresh the frontend
pnpm dev pnpm dev
``` ```
1. Hack, hack, hack 1. Hack, hack, hack
1. Check if everything still runs in production mode 1. Check if everything still runs in production mode:
``` ```
pnpm build pnpm build
pnpm start pnpm start
``` ```
1. Create tests 1. Create tests
1. Run all [tests](#test-suite) 1. Run all [tests](#test-suite):
``` ```
pnpm test pnpm test
``` ```
@ -198,7 +198,7 @@ tests of all packages.
## Releasing ## 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 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 1. Bump versions of packages that have changed or have dependencies that have changed
2. Update the Changelog 2. Update the Changelog
@ -206,7 +206,7 @@ To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/action
4. Create a new pull-request to track any further changes that need to be included in this release 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. 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 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 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 2. Create a new tag, and GitHub release from squashed release commit
@ -226,4 +226,4 @@ That we do not have any potential problems later it is sadly necessary to sign a
We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long. We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long.
A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. Once a pull request is opened, an automated bot will promptly leave a comment requesting the agreement to be signed. The pull request can only be merged once the signature is obtained.

View file

@ -1,10 +1,9 @@
const fetch = require('node-fetch');
const { defineConfig } = require('cypress'); const { defineConfig } = require('cypress');
const BASE_URL = 'http://localhost:5678'; const BASE_URL = 'http://localhost:5678';
module.exports = defineConfig({ module.exports = defineConfig({
projectId: "5hbsdn", projectId: '5hbsdn',
retries: { retries: {
openMode: 0, openMode: 0,
runMode: 2, runMode: 2,
@ -19,31 +18,5 @@ module.exports = defineConfig({
screenshotOnRunFailure: true, screenshotOnRunFailure: true,
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,
experimentalSessionAndOrigin: true, experimentalSessionAndOrigin: true,
setupNodeEvents(on, config) {
on('task', {
reset: () => fetch(BASE_URL + '/e2e/db/reset', { method: 'POST' }),
'setup-owner': (payload) => {
try {
return fetch(BASE_URL + '/e2e/db/setup-owner', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
console.error("setup-owner failed with: ", error)
return null
}
},
'set-feature': ({ feature, enabled }) => {
return fetch(BASE_URL + `/e2e/feature/${feature}`, {
method: 'PATCH',
body: JSON.stringify({ enabled }),
headers: { 'Content-Type': 'application/json' }
})
},
});
},
}, },
}); });

View file

@ -1,9 +1,32 @@
export const BACKEND_BASE_URL = 'http://localhost:5678'; import { randFirstName, randLastName } from '@ngneat/falso';
export const BASE_URL = 'http://localhost:5678';
export const BACKEND_BASE_URL = 'http://localhost:5678';
export const N8N_AUTH_COOKIE = 'n8n-auth'; export const N8N_AUTH_COOKIE = 'n8n-auth';
export const DEFAULT_USER_EMAIL = 'nathan@n8n.io'; const DEFAULT_USER_PASSWORD = 'CypressTest123';
export const DEFAULT_USER_PASSWORD = 'CypressTest123';
export const INSTANCE_OWNER = {
email: 'nathan@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
};
export const INSTANCE_MEMBERS = [
{
email: 'rebecca@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
},
{
email: 'mustafa@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
},
];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"';

View file

@ -1,28 +0,0 @@
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { randFirstName, randLastName } from '@ngneat/falso';
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
describe('Authentication', () => {
beforeEach(() => {
cy.resetAll();
});
it('should setup owner', () => {
cy.setup({ email, firstName, lastName, password });
});
it('should sign user in', () => {
cy.setupOwner({ email, password, firstName, lastName });
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');
return false;
});
cy.signin({ email, password });
});
});

View file

@ -8,10 +8,6 @@ const WorkflowPage = new WorkflowPageClass();
const multipleWorkflowsCount = 5; const multipleWorkflowsCount = 5;
describe('Workflows', () => { describe('Workflows', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
cy.visit(WorkflowsPage.url); cy.visit(WorkflowsPage.url);
}); });

View file

@ -1,22 +1,8 @@
import { randFirstName, randLastName } from '@ngneat/falso';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { SettingsLogStreamingPage } from '../pages'; import { SettingsLogStreamingPage } from '../pages';
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
const settingsLogStreamingPage = new SettingsLogStreamingPage(); const settingsLogStreamingPage = new SettingsLogStreamingPage();
describe('Log Streaming Settings', () => { describe('Log Streaming Settings', () => {
before(() => {
cy.setup({ email, firstName, lastName, password });
});
beforeEach(() => {
cy.signin({ email, password });
});
it('should show the unlicensed view when the feature is disabled', () => { it('should show the unlicensed view when the feature is disabled', () => {
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.getters.getActionBoxUnlicensed().should('be.visible'); settingsLogStreamingPage.getters.getActionBoxUnlicensed().should('be.visible');
@ -25,7 +11,7 @@ describe('Log Streaming Settings', () => {
}); });
it('should show the licensed view when the feature is enabled', () => { it('should show the licensed view when the feature is enabled', () => {
cy.enableFeature('feat:logStreaming'); cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming'); cy.visit('/settings/log-streaming');
settingsLogStreamingPage.getters.getActionBoxLicensed().should('be.visible'); settingsLogStreamingPage.getters.getActionBoxLicensed().should('be.visible');
settingsLogStreamingPage.getters.getAddFirstDestinationButton().should('be.visible'); settingsLogStreamingPage.getters.getAddFirstDestinationButton().should('be.visible');

View file

@ -10,10 +10,6 @@ const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
describe('Undo/Redo', () => { describe('Undo/Redo', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
@ -125,17 +121,17 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.attr', 'style', 'left: 740px; top: 360px;'); .should('have.attr', 'style', 'left: 740px; top: 320px;');
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.attr', 'style', 'left: 640px; top: 260px;'); .should('have.attr', 'style', 'left: 640px; top: 220px;');
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.attr', 'style', 'left: 740px; top: 360px;'); .should('have.attr', 'style', 'left: 740px; top: 320px;');
}); });
it('should undo/redo deleting a connection by pressing delete button', () => { it('should undo/redo deleting a connection by pressing delete button', () => {
@ -285,7 +281,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.first() .first()
.should('have.attr', 'style', 'left: 420px; top: 260px;'); .should('have.attr', 'style', 'left: 420px; top: 220px;');
// Third undo: Should enable last node // Third undo: Should enable last node
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0); WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -298,7 +294,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.first() .first()
.should('have.attr', 'style', 'left: 540px; top: 400px;'); .should('have.attr', 'style', 'left: 540px; top: 360px;');
// Third redo: Should delete the Set node // Third redo: Should delete the Set node
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);

View file

@ -3,16 +3,14 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Inline expression editor', () => { describe('Inline expression editor', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.openNode('Hacker News'); WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openInlineExpressionEditor(); WorkflowPage.actions.openInlineExpressionEditor();
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
}); });
it('should resolve primitive resolvables', () => { it('should resolve primitive resolvables', () => {

View file

@ -11,10 +11,6 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => { describe('Canvas Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
@ -103,7 +99,7 @@ describe('Canvas Actions', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.attr', 'style', 'left: 860px; top: 260px;'); .should('have.attr', 'style', 'left: 860px; top: 220px;');
}); });
it('should delete connections by pressing the delete button', () => { it('should delete connections by pressing the delete button', () => {

View file

@ -5,9 +5,7 @@ import {
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
SET_NODE_NAME, SET_NODE_NAME,
SWITCH_NODE_NAME, SWITCH_NODE_NAME,
IF_NODE_NAME,
MERGE_NODE_NAME, MERGE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
@ -21,10 +19,6 @@ const ZOOM_OUT_X2_FACTOR = 0.64;
const RENAME_NODE_NAME = 'Something else'; const RENAME_NODE_NAME = 'Something else';
describe('Canvas Node Manipulation and Navigation', () => { describe('Canvas Node Manipulation and Navigation', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
@ -168,7 +162,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.attr', 'style', 'left: 740px; top: 360px;'); .should('have.attr', 'style', 'left: 740px; top: 320px;');
}); });
it('should zoom in', () => { it('should zoom in', () => {

View file

@ -10,10 +10,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Data pinning', () => { describe('Data pinning', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });

View file

@ -4,10 +4,6 @@ const wf = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Data transformation expressions', () => { describe('Data transformation expressions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
wf.actions.visit(); wf.actions.visit();

View file

@ -9,10 +9,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Data mapping', () => { describe('Data mapping', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
@ -192,7 +188,11 @@ describe('Data mapping', () => {
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`); .should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
ndv.getters.parameterExpressionPreview('value').should('not.exist'); ndv.getters
.parameterExpressionPreview('value')
.invoke('text')
.invoke('replace', /\u00a0/g, ' ')
.should('equal', '[ERROR: no data, execute "Schedule Trigger" node first]');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.actions.mapDataFromHeader(1, 'value'); ndv.actions.mapDataFromHeader(1, 'value');

View file

@ -6,10 +6,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Schedule Trigger node', async () => { describe('Schedule Trigger node', async () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });

View file

@ -92,10 +92,6 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
}; };
describe('Webhook Trigger node', async () => { describe('Webhook Trigger node', async () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();

View file

@ -1,4 +1,4 @@
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { import {
CredentialsModal, CredentialsModal,
CredentialsPage, CredentialsPage,
@ -28,47 +28,12 @@ const workflowPage = new WorkflowPage();
const workflowSharingModal = new WorkflowSharingModal(); const workflowSharingModal = new WorkflowSharingModal();
const ndv = new NDV(); const ndv = new NDV();
const instanceOwner = { describe('Sharing', { disableAutoLogin: true }, () => {
email: `${DEFAULT_USER_EMAIL}one`, before(() => cy.enableFeature('sharing', true));
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'U1',
};
const users = [
{
email: `${DEFAULT_USER_EMAIL}two`,
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'U2',
},
{
email: `${DEFAULT_USER_EMAIL}three`,
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'U3',
},
];
describe('Sharing', () => {
before(() => {
cy.setupOwner(instanceOwner);
});
beforeEach(() => {
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');
return false;
});
});
it('should invite User U2 and User U3 to instance', () => {
cy.inviteUsers({ instanceOwner, users });
});
let workflowW2Url = ''; let workflowW2Url = '';
it('should create C1, W1, W2, share W1 with U3, as U2', () => { it('should create C1, W1, W2, share W1 with U3, as U2', () => {
cy.signin(users[0]); cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -87,7 +52,7 @@ describe('Sharing', () => {
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openShareModal(); workflowPage.actions.openShareModal();
workflowSharingModal.actions.addUser(users[1].email); workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[1].email);
workflowSharingModal.actions.save(); workflowSharingModal.actions.save();
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
@ -100,23 +65,23 @@ describe('Sharing', () => {
}); });
it('should create C2, share C2 with U1 and U2, as U3', () => { it('should create C2, share C2 with U1 and U2, as U3', () => {
cy.signin(users[1]); cy.signin(INSTANCE_MEMBERS[1]);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Airtable API').click(); credentialsModal.getters.newCredentialTypeOption('Airtable Personal Access Token API').click();
credentialsModal.getters.newCredentialTypeButton().click(); credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('API Key').type('1234567890'); credentialsModal.getters.connectionParameter('Access Token').type('1234567890');
credentialsModal.actions.setName('Credential C2'); credentialsModal.actions.setName('Credential C2');
credentialsModal.actions.changeTab('Sharing'); credentialsModal.actions.changeTab('Sharing');
credentialsModal.actions.addUser(instanceOwner.email); credentialsModal.actions.addUser(INSTANCE_OWNER.email);
credentialsModal.actions.addUser(users[0].email); credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
credentialsModal.actions.save(); credentialsModal.actions.save();
credentialsModal.actions.close(); credentialsModal.actions.close();
}); });
it('should open W1, add node using C2 as U3', () => { it('should open W1, add node using C2 as U3', () => {
cy.signin(users[1]); cy.signin(INSTANCE_MEMBERS[1]);
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1); workflowsPage.getters.workflowCards().should('have.length', 1);
@ -136,7 +101,7 @@ describe('Sharing', () => {
}); });
it('should not have access to W2, as U3', () => { it('should not have access to W2, as U3', () => {
cy.signin(users[1]); cy.signin(INSTANCE_MEMBERS[1]);
cy.visit(workflowW2Url); cy.visit(workflowW2Url);
cy.waitForLoad(); cy.waitForLoad();
@ -145,7 +110,7 @@ describe('Sharing', () => {
}); });
it('should have access to W1, W2, as U1', () => { it('should have access to W1, W2, as U1', () => {
cy.signin(instanceOwner); cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
@ -165,7 +130,7 @@ describe('Sharing', () => {
}); });
it('should automatically test C2 when opened by U2 sharee', () => { it('should automatically test C2 when opened by U2 sharee', () => {
cy.signin(users[0]); cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C2').click(); credentialsPage.getters.credentialCard('Credential C2').click();

View file

@ -5,10 +5,6 @@ const wf = new WorkflowPage();
const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4', 'Tag 5']; const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4', 'Tag 5'];
describe('Workflow tags', () => { describe('Workflow tags', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
wf.actions.visit(); wf.actions.visit();
}); });

View file

@ -1,6 +1,5 @@
import { MainSidebar } from './../pages/sidebar/main-sidebar'; import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { SettingsUsersPage, WorkflowPage } from '../pages';
import { SettingsSidebar, SettingsUsersPage, WorkflowPage, WorkflowsPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
/** /**
@ -15,28 +14,6 @@ import { PersonalSettingsPage } from '../pages/settings-personal';
* C2 - Credential owned by User C, shared with User A and User B * C2 - Credential owned by User C, shared with User A and User B
*/ */
const instanceOwner = {
email: `${DEFAULT_USER_EMAIL}A`,
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'A',
};
const users = [
{
email: `${DEFAULT_USER_EMAIL}B`,
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'B',
},
{
email: `${DEFAULT_USER_EMAIL}C`,
password: DEFAULT_USER_PASSWORD,
firstName: 'User',
lastName: 'C',
},
];
const updatedPersonalData = { const updatedPersonalData = {
newFirstName: 'Something', newFirstName: 'Something',
newLastName: 'Else', newLastName: 'Else',
@ -49,47 +26,38 @@ const usersSettingsPage = new SettingsUsersPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const personalSettingsPage = new PersonalSettingsPage(); const personalSettingsPage = new PersonalSettingsPage();
describe('User Management', () => { describe('User Management', { disableAutoLogin: true }, () => {
before(() => { before(() => cy.enableFeature('sharing'));
cy.setupOwner(instanceOwner);
});
beforeEach(() => {
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in');
return false;
});
});
it(`should invite User B and User C to instance`, () => {
cy.inviteUsers({ instanceOwner, users });
});
it('should prevent non-owners to access UM settings', () => { it('should prevent non-owners to access UM settings', () => {
usersSettingsPage.actions.loginAndVisit(users[0].email, users[0].password, false); usersSettingsPage.actions.loginAndVisit(
INSTANCE_MEMBERS[0].email,
INSTANCE_MEMBERS[0].password,
false,
);
}); });
it('should allow instance owner to access UM settings', () => { it('should allow instance owner to access UM settings', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
}); });
it('should properly render UM settings page for instance owners', () => { it('should properly render UM settings page for instance owners', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
// All items in user list should be there // All items in user list should be there
usersSettingsPage.getters.userListItems().should('have.length', 3); usersSettingsPage.getters.userListItems().should('have.length', 3);
// List item for current user should have the `Owner` badge // List item for current user should have the `Owner` badge
usersSettingsPage.getters usersSettingsPage.getters
.userItem(instanceOwner.email) .userItem(INSTANCE_OWNER.email)
.find('.n8n-badge:contains("Owner")') .find('.n8n-badge:contains("Owner")')
.should('exist'); .should('exist');
// Other users list items should contain action pop-up list // Other users list items should contain action pop-up list
usersSettingsPage.getters.userActionsToggle(users[0].email).should('exist'); usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist');
usersSettingsPage.getters.userActionsToggle(users[1].email).should('exist'); usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist');
}); });
it('should delete user and their data', () => { it('should delete user and their data', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(users[0].email); usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email);
usersSettingsPage.getters.deleteDataRadioButton().realClick(); usersSettingsPage.getters.deleteDataRadioButton().realClick();
usersSettingsPage.getters.deleteDataInput().type('delete all data'); usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().realClick(); usersSettingsPage.getters.deleteUserButton().realClick();
@ -97,8 +65,8 @@ describe('User Management', () => {
}); });
it('should delete user and transfer their data', () => { it('should delete user and transfer their data', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(users[1].email); usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email);
usersSettingsPage.getters.transferDataRadioButton().realClick(); usersSettingsPage.getters.transferDataRadioButton().realClick();
usersSettingsPage.getters.userSelectDropDown().realClick(); usersSettingsPage.getters.userSelectDropDown().realClick();
usersSettingsPage.getters.userSelectOptions().first().realClick(); usersSettingsPage.getters.userSelectOptions().first().realClick();
@ -107,7 +75,7 @@ describe('User Management', () => {
}); });
it(`should allow user to change their personal data`, () => { it(`should allow user to change their personal data`, () => {
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updateFirstAndLastName( personalSettingsPage.actions.updateFirstAndLastName(
updatedPersonalData.newFirstName, updatedPersonalData.newFirstName,
updatedPersonalData.newLastName, updatedPersonalData.newLastName,
@ -119,14 +87,14 @@ describe('User Management', () => {
}); });
it(`shouldn't allow user to set weak password`, () => { it(`shouldn't allow user to set weak password`, () => {
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
for (let weakPass of updatedPersonalData.invalidPasswords) { for (let weakPass of updatedPersonalData.invalidPasswords) {
personalSettingsPage.actions.tryToSetWeakPassword(instanceOwner.password, weakPass); personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
} }
}); });
it(`shouldn't allow user to change password if old password is wrong`, () => { it(`shouldn't allow user to change password if old password is wrong`, () => {
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword); personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
workflowPage.getters workflowPage.getters
.errorToast() .errorToast()
@ -135,21 +103,21 @@ describe('User Management', () => {
}); });
it(`should change current user password`, () => { it(`should change current user password`, () => {
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updatePassword( personalSettingsPage.actions.updatePassword(
instanceOwner.password, INSTANCE_OWNER.password,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
workflowPage.getters.successToast().should('contain', 'Password updated'); workflowPage.getters.successToast().should('contain', 'Password updated');
personalSettingsPage.actions.loginWithNewData( personalSettingsPage.actions.loginWithNewData(
instanceOwner.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
}); });
it(`shouldn't allow users to set invalid email`, () => { it(`shouldn't allow users to set invalid email`, () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
instanceOwner.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
// try without @ part // try without @ part
@ -160,7 +128,7 @@ describe('User Management', () => {
it(`should change user email`, () => { it(`should change user email`, () => {
personalSettingsPage.actions.loginAndVisit( personalSettingsPage.actions.loginAndVisit(
instanceOwner.email, INSTANCE_OWNER.email,
updatedPersonalData.newPassword, updatedPersonalData.newPassword,
); );
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail); personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);

View file

@ -1,15 +1,10 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { NDV, WorkflowPage as WorkflowPageClass, WorkflowsPage } from '../pages'; import { NDV, WorkflowPage as WorkflowPageClass, WorkflowsPage } from '../pages';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
describe('Execution', () => { describe('Execution', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });

View file

@ -6,24 +6,14 @@ import {
NEW_QUERY_AUTH_ACCOUNT_NAME, NEW_QUERY_AUTH_ACCOUNT_NAME,
} from './../constants'; } from './../constants';
import { import {
DEFAULT_USER_EMAIL,
DEFAULT_USER_PASSWORD,
GMAIL_NODE_NAME, GMAIL_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME, NEW_GOOGLE_ACCOUNT_NAME,
NEW_TRELLO_ACCOUNT_NAME, NEW_TRELLO_ACCOUNT_NAME,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
TRELLO_NODE_NAME, TRELLO_NODE_NAME,
} from '../constants'; } from '../constants';
import { randFirstName, randLastName } from '@ngneat/falso';
import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages'; import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages';
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json';
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
const credentialsPage = new CredentialsPage(); const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -32,10 +22,6 @@ const nodeDetailsView = new NDV();
const NEW_CREDENTIAL_NAME = 'Something else'; const NEW_CREDENTIAL_NAME = 'Something else';
describe('Credentials', () => { describe('Credentials', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);
}); });

View file

@ -6,10 +6,6 @@ const executionsTab = new WorkflowExecutionsTab();
// Test suite for executions tab // Test suite for executions tab
describe('Current Workflow Executions', () => { describe('Current Workflow Executions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
@ -36,7 +32,6 @@ describe('Current Workflow Executions', () => {
}); });
const createMockExecutions = () => { const createMockExecutions = () => {
workflowPage.actions.turnOnManualExecutionSaving();
executionsTab.actions.createManualExecutions(5); executionsTab.actions.createManualExecutions(5);
// Make some failed executions by enabling Code node with syntax error // Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');

View file

@ -13,9 +13,6 @@ const workflowPage = new WorkflowPage();
// so the /nodes and /credentials endpoints are intercepted and non-cached. // so the /nodes and /credentials endpoints are intercepted and non-cached.
// We want to keep the other tests as fast as possible so we don't want to break the cache in those. // We want to keep the other tests as fast as possible so we don't want to break the cache in those.
describe('Community Nodes', () => { describe('Community Nodes', () => {
before(() => {
cy.skipSetup();
})
beforeEach(() => { beforeEach(() => {
cy.intercept('/types/nodes.json', { middleware: true }, (req) => { cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
@ -36,6 +33,7 @@ describe('Community Nodes', () => {
credentials.push(CustomCredential); credentials.push(CustomCredential);
}) })
}) })
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });

View file

@ -1,22 +1,10 @@
import { VariablesPage } from '../pages/variables'; import { VariablesPage } from '../pages/variables';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { randFirstName, randLastName } from '@ngneat/falso';
const variablesPage = new VariablesPage(); const variablesPage = new VariablesPage();
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
describe('Variables', () => { describe('Variables', () => {
before(() => {
cy.setup({ email, firstName, lastName, password });
});
it('should show the unlicensed action box when the feature is disabled', () => { it('should show the unlicensed action box when the feature is disabled', () => {
cy.disableFeature('feat:variables'); cy.disableFeature('variables', false);
cy.signin({ email, password });
cy.visit(variablesPage.url); cy.visit(variablesPage.url);
variablesPage.getters.unavailableResourcesList().should('be.visible'); variablesPage.getters.unavailableResourcesList().should('be.visible');
@ -25,11 +13,10 @@ describe('Variables', () => {
describe('licensed', () => { describe('licensed', () => {
before(() => { before(() => {
cy.enableFeature('feat:variables'); cy.enableFeature('variables');
}); });
beforeEach(() => { beforeEach(() => {
cy.signin({ email, password });
cy.intercept('GET', '/rest/variables').as('loadVariables'); cy.intercept('GET', '/rest/variables').as('loadVariables');
cy.visit(variablesPage.url); cy.visit(variablesPage.url);

View file

@ -5,10 +5,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('NDV', () => { describe('NDV', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.renameWorkflow(uuid());
@ -277,7 +273,11 @@ describe('NDV', () => {
.should('equal', 'hovering-item'); .should('equal', 'hovering-item');
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.openNode('Set5'); workflowPage.actions.openNode('Set5');
ndv.actions.switchInputBranch('True Branch');
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)')
ndv.getters.outputTableRow(1) ndv.getters.outputTableRow(1)
.should('have.text', '8888') .should('have.text', '8888')
.realHover(); .realHover();
@ -288,16 +288,21 @@ describe('NDV', () => {
.realHover(); .realHover();
ndv.getters.outputHoveringItem().should('not.exist'); ndv.getters.outputHoveringItem().should('not.exist');
ndv.actions.switchIntputBranch('False Branch'); ndv.actions.switchInputBranch('False Branch');
ndv.getters.inputTableRow(1)
.should('have.text', '8888')
.realHover();
ndv.actions.changeOutputRunSelector('2 of 2 (4 items)')
ndv.getters.outputTableRow(1)
.should('have.text', '1111')
.realHover();
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)')
ndv.getters.inputTableRow(1) ndv.getters.inputTableRow(1)
.should('have.text', '8888') .should('have.text', '8888')
.realHover(); .realHover();
ndv.getters.outputHoveringItem().should('have.text', '8888'); ndv.getters.outputHoveringItem().should('have.text', '8888');
ndv.actions.changeOutputRunSelector('1 of 2 (4 items)')
ndv.getters.outputTableRow(1)
.should('have.text', '1111')
.realHover();
// todo there's a bug here need to fix ADO-534 // todo there's a bug here need to fix ADO-534
// ndv.getters.outputHoveringItem().should('not.exist'); // ndv.getters.outputHoveringItem().should('not.exist');
}); });

View file

@ -15,10 +15,6 @@ function checkStickiesStyle( top: number, left: number, height: number, width: n
} }
describe('Canvas Actions', () => { describe('Canvas Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
@ -94,66 +90,66 @@ describe('Canvas Actions', () => {
moveSticky({ left: 600, top: 200 }); moveSticky({ left: 600, top: 200 });
cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]);
checkStickiesStyle(140, 510, 160, 150); checkStickiesStyle(100, 510, 160, 150);
cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]); cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]);
checkStickiesStyle(140, 466, 160, 194); checkStickiesStyle(100, 466, 160, 194);
}); });
it('expands/shrinks sticky from the top edge', () => { it('expands/shrinks sticky from the top edge', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button
checkStickiesStyle(360, 620, 160, 240); checkStickiesStyle(300, 620, 160, 240);
cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]);
checkStickiesStyle(440, 620, 80, 240); checkStickiesStyle(380, 620, 80, 240);
cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]); cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]);
checkStickiesStyle(384, 620, 136, 240); checkStickiesStyle(324, 620, 136, 240);
}); });
it('expands/shrinks sticky from the bottom edge', () => { it('expands/shrinks sticky from the bottom edge', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button
checkStickiesStyle(360, 620, 160, 240); checkStickiesStyle(300, 620, 160, 240);
cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]);
checkStickiesStyle(360, 620, 254, 240); checkStickiesStyle(300, 620, 254, 240);
cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]); cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]);
checkStickiesStyle(360, 620, 198, 240); checkStickiesStyle(300, 620, 198, 240);
}); });
it('expands/shrinks sticky from the bottom right edge', () => { it('expands/shrinks sticky from the bottom right edge', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button
checkStickiesStyle(160, 420, 160, 240); checkStickiesStyle(100, 420, 160, 240);
cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]);
checkStickiesStyle(160, 420, 254, 346); checkStickiesStyle(100, 420, 254, 346);
cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]); cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]);
checkStickiesStyle(160, 420, 198, 302); checkStickiesStyle(100, 420, 198, 302);
}); });
it('expands/shrinks sticky from the top right edge', () => { it('expands/shrinks sticky from the top right edge', () => {
addDefaultSticky(); addDefaultSticky();
cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]);
checkStickiesStyle(420, 400, 80, 346); checkStickiesStyle(360, 400, 80, 346);
cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]); cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]);
checkStickiesStyle(364, 400, 136, 302); checkStickiesStyle(304, 400, 136, 302);
}); });
it('expands/shrinks sticky from the top left edge, and reach min height/width', () => { it('expands/shrinks sticky from the top left edge, and reach min height/width', () => {
addDefaultSticky(); addDefaultSticky();
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]);
checkStickiesStyle(420, 490, 80, 150); checkStickiesStyle(360, 490, 80, 150);
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]);
checkStickiesStyle(264, 346, 236, 294); checkStickiesStyle(204, 346, 236, 294);
}); });
it('sets sticky behind node', () => { it('sets sticky behind node', () => {
@ -161,7 +157,7 @@ describe('Canvas Actions', () => {
addDefaultSticky(); addDefaultSticky();
cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]);
checkStickiesStyle(184, 256, 316, 384, -121); checkStickiesStyle(124, 256, 316, 384, -121);
workflowPage.getters.canvasNodes().eq(0) workflowPage.getters.canvasNodes().eq(0)
.should(($el) => { .should(($el) => {
@ -239,7 +235,7 @@ function addDefaultSticky() {
} }
function stickyShouldBePositionedCorrectly(position: Position) { function stickyShouldBePositionedCorrectly(position: Position) {
const yOffset = -60; const yOffset = -100;
const xOffset = -180; const xOffset = -180;
workflowPage.getters.stickies() workflowPage.getters.stickies()
.should(($el) => { .should(($el) => {

View file

@ -8,10 +8,6 @@ const NO_CREDENTIALS_MESSAGE = 'Please add your credential';
const INVALID_CREDENTIALS_MESSAGE = 'Please check your credential'; const INVALID_CREDENTIALS_MESSAGE = 'Please check your credential';
describe('Resource Locator', () => { describe('Resource Locator', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
}); });

View file

@ -1,110 +0,0 @@
import { randFirstName, randLastName } from '@ngneat/falso';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import {
SettingsUsersPage,
SignupPage,
WorkflowsPage,
WorkflowPage,
CredentialsPage,
CredentialsModal,
MessageBox,
} from '../pages';
import { SettingsUsagePage } from '../pages/settings-usage';
import { MainSidebar, SettingsSidebar } from '../pages/sidebar';
const mainSidebar = new MainSidebar();
const settingsSidebar = new SettingsSidebar();
const workflowsPage = new WorkflowsPage();
const signupPage = new SignupPage();
const workflowPage = new WorkflowPage();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const settingsUsersPage = new SettingsUsersPage();
const settingsUsagePage = new SettingsUsagePage();
const messageBox = new MessageBox();
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
describe('Default owner', () => {
it('should be able to create workflows', () => {
cy.skipSetup();
cy.createFixtureWorkflow('Test_workflow_1.json', `Test workflow`);
// reload page, ensure owner still has access
cy.reload();
cy.waitForLoad();
workflowPage.getters.workflowNameInput().should('contain.value', 'Test workflow');
});
it('should be able to add new credentials', () => {
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('API Key').type('1234567890');
credentialsModal.actions.setName('My awesome Notion account');
credentialsModal.actions.save();
credentialsModal.actions.close();
credentialsModal.getters.newCredentialModal().should('not.exist');
credentialsModal.getters.editCredentialModal().should('not.exist');
credentialsPage.getters.credentialCards().should('have.length', 1);
});
it('should be able to setup UM from settings', () => {
cy.visit('/');
mainSidebar.getters.settings().should('be.visible');
mainSidebar.actions.goToSettings();
cy.url().should('include', settingsUsagePage.url);
settingsSidebar.actions.goToUsers();
cy.url().should('include', settingsUsersPage.url);
settingsUsersPage.actions.goToOwnerSetup();
cy.url().should('include', signupPage.url);
});
it('should be able to setup instance and migrate workflows and credentials', () => {
cy.setup({ email, firstName, lastName, password }, true);
messageBox.getters.content().should('contain.text', '1 existing workflow and 1 credential');
messageBox.actions.confirm();
cy.wait('@setupRequest');
cy.url().should('include', settingsUsersPage.url);
settingsSidebar.actions.back();
cy.url().should('include', workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
});
it('can click back to main menu and have migrated credential after setup', () => {
cy.signin({ email, password });
cy.visit(workflowsPage.url);
mainSidebar.actions.goToCredentials();
cy.url().should('include', credentialsPage.url);
credentialsPage.getters.credentialCards().should('have.length', 1);
});
});

View file

@ -7,10 +7,6 @@ const WorkflowPage = new WorkflowPageClass();
const NDVModal = new NDV(); const NDVModal = new NDV();
describe('Node Creator', () => { describe('Node Creator', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
@ -271,7 +267,7 @@ describe('Node Creator', () => {
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists') WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists', 'Summarize')
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);
}) })
}); });

View file

@ -5,10 +5,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('NDV', () => { describe('NDV', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.renameWorkflow(uuid());
@ -68,15 +64,15 @@ describe('NDV', () => {
it('should show validation errors only after blur or re-opening of NDV', () => { it('should show validation errors only after blur or re-opening of NDV', () => {
workflowPage.actions.addNodeToCanvas('Manual'); workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Read data from a table'); workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records');
ndv.getters.container().should('be.visible'); ndv.getters.container().should('be.visible');
cy.get('.has-issues').should('have.length', 0); // cy.get('.has-issues').should('have.length', 0);
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur(); ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
ndv.getters.parameterInput('application').find('input').eq(1).focus().blur(); ndv.getters.parameterInput('base').find('input').eq(1).focus().blur();
cy.get('.has-issues').should('have.length', 2); cy.get('.has-issues').should('have.length', 0);
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Airtable'); workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 3); cy.get('.has-issues').should('have.length', 2);
cy.get('[class*=hasIssues]').should('have.length', 1); cy.get('[class*=hasIssues]').should('have.length', 1);
}); });

View file

@ -5,8 +5,8 @@ const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
describe('Code node', () => { describe('Code node', () => {
before(() => { beforeEach(() => {
cy.skipSetup(); WorkflowPage.actions.visit();
}); });
it('should execute the placeholder in all-items mode successfully', () => { it('should execute the placeholder in all-items mode successfully', () => {
@ -20,7 +20,6 @@ describe('Code node', () => {
}); });
it('should execute the placeholder in each-item mode successfully', () => { it('should execute the placeholder in each-item mode successfully', () => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code'); WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code'); WorkflowPage.actions.openNode('Code');

View file

@ -5,6 +5,7 @@ import {
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
const NEW_WORKFLOW_NAME = 'Something else'; const NEW_WORKFLOW_NAME = 'Something else';
const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json'; const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd3f45c8a94f8eb7012e663a8b671/raw/3afea1aec15573cc168d9af7e79395bd76082906/test-workflow.json';
@ -12,12 +13,9 @@ const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
const DUPLICATE_WORKFLOW_TAG = 'Duplicate'; const DUPLICATE_WORKFLOW_TAG = 'Duplicate';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const WorkflowPages = new WorkflowsPageClass();
describe('Workflow Actions', () => { describe('Workflow Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
@ -66,6 +64,42 @@ describe('Workflow Actions', () => {
.should('eq', NEW_WORKFLOW_NAME); .should('eq', NEW_WORKFLOW_NAME);
}); });
it('should not save workflow if canvas is loading', () => {
let interceptCalledCount = 0;
// There's no way in Cypress to check if intercept was not called
// so we'll count the number of times it was called
cy.intercept('PATCH', '/rest/workflows/*', () => {
interceptCalledCount++;
}).as('saveWorkflow');
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.intercept(
{
url: '/rest/workflows/*',
method: 'GET',
middleware: true,
},
(req) => {
// Delay the response to give time for the save to be triggered
req.on('response', async (res) => {
await new Promise((resolve) => setTimeout(resolve, 2000))
res.send();
})
}
)
cy.reload();
cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
})
it('should copy nodes', () => { it('should copy nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -110,64 +144,70 @@ describe('Workflow Actions', () => {
}); });
it('should update workflow settings', () => { it('should update workflow settings', () => {
WorkflowPage.actions.visit(); cy.visit(WorkflowPages.url);
// Open settings dialog WorkflowPages.getters.workflowCards().then((cards) => {
WorkflowPage.actions.saveWorkflowOnButtonClick(); const totalWorkflows = cards.length;
WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowMenu().click(); WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible'); // Open settings dialog
WorkflowPage.getters.workflowMenuItemSettings().click(); WorkflowPage.actions.saveWorkflowOnButtonClick();
// Change all settings WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', 7); WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
.workflowSettingsErrorWorkflowSelect() WorkflowPage.getters.workflowMenuItemSettings().click();
.find('li') // Change all settings
.last() // totalWorkflows + 1 (current workflow) + 1 (no workflow option)
.click({ force: true }); WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2);
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist'); WorkflowPage.getters
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true }); .workflowSettingsErrorWorkflowSelect()
WorkflowPage.getters .find('li')
.workflowSettingsSaveFiledExecutionsSelect() .last()
.find('li') .click({ force: true });
.should('have.length', 3); WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist');
WorkflowPage.getters WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true });
.workflowSettingsSaveFiledExecutionsSelect() WorkflowPage.getters
.find('li') .workflowSettingsSaveFiledExecutionsSelect()
.last() .find('li')
.click({ force: true }); .should('have.length', 3);
WorkflowPage.getters WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect() .workflowSettingsSaveFiledExecutionsSelect()
.find('li') .find('li')
.should('have.length', 3); .last()
WorkflowPage.getters .click({ force: true });
.workflowSettingsSaveSuccessExecutionsSelect() WorkflowPage.getters
.find('li') .workflowSettingsSaveSuccessExecutionsSelect()
.last() .find('li')
.click({ force: true }); .should('have.length', 3);
WorkflowPage.getters WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect() .workflowSettingsSaveSuccessExecutionsSelect()
.find('li') .find('li')
.should('have.length', 3); .last()
WorkflowPage.getters .click({ force: true });
.workflowSettingsSaveManualExecutionsSelect() WorkflowPage.getters
.find('li') .workflowSettingsSaveManualExecutionsSelect()
.last() .find('li')
.click({ force: true }); .should('have.length', 3);
WorkflowPage.getters WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect() .workflowSettingsSaveManualExecutionsSelect()
.find('li') .find('li')
.should('have.length', 3); .last()
WorkflowPage.getters .click({ force: true });
.workflowSettingsSaveExecutionProgressSelect() WorkflowPage.getters
.find('li') .workflowSettingsSaveExecutionProgressSelect()
.last() .find('li')
.click({ force: true }); .should('have.length', 3);
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click(); WorkflowPage.getters
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1'); .workflowSettingsSaveExecutionProgressSelect()
// Save settings .find('li')
WorkflowPage.getters.workflowSettingsSaveButton().click(); .last()
WorkflowPage.getters.workflowSettingsModal().should('not.exist'); .click({ force: true });
WorkflowPage.getters.successToast().should('exist'); WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist');
})
}); });
it('should not be able to delete unsaved workflow', () => { it('should not be able to delete unsaved workflow', () => {

View file

@ -4,8 +4,8 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('HTTP Request node', () => { describe('HTTP Request node', () => {
before(() => { beforeEach(() => {
cy.skipSetup(); workflowPage.actions.visit();
}); });
it('should make a request with a URL and receive a response', () => { it('should make a request with a URL and receive a response', () => {

View file

@ -3,16 +3,14 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Expression editor modal', () => { describe('Expression editor modal', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');
WorkflowPage.actions.openNode('Hacker News'); WorkflowPage.actions.openNode('Hacker News');
WorkflowPage.actions.openExpressionEditorModal(); WorkflowPage.actions.openExpressionEditorModal();
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
}); });
it('should resolve primitive resolvables', () => { it('should resolve primitive resolvables', () => {

View file

@ -1,7 +1,5 @@
export * from './base'; export * from './base';
export * from './credentials'; export * from './credentials';
export * from './signin';
export * from './signup';
export * from './workflows'; export * from './workflows';
export * from './workflow'; export * from './workflow';
export * from './modals'; export * from './modals';

View file

@ -154,7 +154,7 @@ export class NDV extends BasePage {
switchOutputBranch: (name: string) => { switchOutputBranch: (name: string) => {
this.getters.outputBranches().get('span').contains(name).click(); this.getters.outputBranches().get('span').contains(name).click();
}, },
switchIntputBranch: (name: string) => { switchInputBranch: (name: string) => {
this.getters.inputBranches().get('span').contains(name).click(); this.getters.inputBranches().get('span').contains(name).click();
}, },
setRLCValue: (paramName: string, value: string) => { setRLCValue: (paramName: string, value: string) => {

View file

@ -26,9 +26,5 @@ export class MainSidebar extends BasePage {
openUserMenu: () => { openUserMenu: () => {
this.getters.userMenu().find('[role="button"]').last().click(); this.getters.userMenu().find('[role="button"]').last().click();
}, },
signout: () => {
this.actions.openUserMenu();
cy.getByTestId('workflow-menu-item-logout').click();
},
}; };
} }

View file

@ -1,11 +0,0 @@
import { BasePage } from './base';
export class SigninPage extends BasePage {
url = '/signin';
getters = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
password: () => cy.getByTestId('password'),
submit: () => cy.get('button'),
};
}

View file

@ -1,15 +0,0 @@
import { BasePage } from './base';
// todo rename to setup
export class SignupPage extends BasePage {
url = '/setup';
getters = {
form: () => cy.getByTestId('auth-form'),
email: () => cy.getByTestId('email'),
firstName: () => cy.getByTestId('firstName'),
lastName: () => cy.getByTestId('lastName'),
password: () => cy.getByTestId('password'),
submit: () => cy.get('button'),
skip: () => cy.get('a'),
};
}

View file

@ -178,7 +178,7 @@ export class WorkflowPage extends BasePage {
}, },
saveWorkflowUsingKeyboardShortcut: () => { saveWorkflowUsingKeyboardShortcut: () => {
cy.intercept('POST', '/rest/workflows').as('createWorkflow'); cy.intercept('POST', '/rest/workflows').as('createWorkflow');
cy.get('body').type('{meta}', { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
}, },
deleteNode: (name: string) => { deleteNode: (name: string) => {
this.getters.canvasNodeByName(name).first().click(); this.getters.canvasNodeByName(name).first().click();
@ -242,14 +242,15 @@ export class WorkflowPage extends BasePage {
executeWorkflow: () => { executeWorkflow: () => {
this.getters.executeWorkflowButton().click(); this.getters.executeWorkflowButton().click();
}, },
addNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string, newNodeName: string) => { addNodeBetweenNodes: (sourceNodeName: string, targetNodeName: string, newNodeName: string, action?: string) => {
this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover(); this.getters.getConnectionBetweenNodes(sourceNodeName, targetNodeName).first().realHover();
this.getters this.getters
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName) .getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.add') .find('.add')
.first() .first()
.click({ force: true }); .click({ force: true });
this.actions.addNodeToCanvas(newNodeName, false);
this.actions.addNodeToCanvas(newNodeName, false, false, action);
}, },
deleteNodeBetweenNodes: ( deleteNodeBetweenNodes: (
sourceNodeName: string, sourceNodeName: string,
@ -281,23 +282,5 @@ export class WorkflowPage extends BasePage {
.type(content) .type(content)
.type('{esc}'); .type('{esc}');
}, },
turnOnManualExecutionSaving: () => {
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
cy.get('.el-loading-mask').should('not.be.visible');
this.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li:contains("Yes")')
.click({ force: true });
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
this.getters.successToast().should('exist');
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
},
}; };
} }

View file

@ -36,8 +36,10 @@ export class WorkflowsPage extends BasePage {
cy.visit(this.url); cy.visit(this.url);
this.getters.workflowCardActions(name).click(); this.getters.workflowCardActions(name).click();
this.getters.workflowDeleteButton().click(); this.getters.workflowDeleteButton().click();
cy.intercept('DELETE', '/rest/workflows/*').as('deleteWorkflow');
cy.get('button').contains('delete').click(); cy.get('button').contains('delete').click();
cy.wait('@deleteWorkflow');
}, },
}; };
} }

View file

@ -1,32 +1,6 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import 'cypress-real-events'; import 'cypress-real-events';
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { N8N_AUTH_COOKIE } from '../constants'; import { BASE_URL, N8N_AUTH_COOKIE } from '../constants';
import { MessageBox } from '../pages/modals/message-box';
Cypress.Commands.add('getByTestId', (selector, ...args) => { Cypress.Commands.add('getByTestId', (selector, ...args) => {
return cy.get(`[data-test-id="${selector}"]`, ...args); return cy.get(`[data-test-id="${selector}"]`, ...args);
@ -59,164 +33,35 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => {
// we can't set them up here because at this point it would be too late // we can't set them up here because at this point it would be too late
// and the requests would already have been made // and the requests would already have been made
if (waitForIntercepts) { if (waitForIntercepts) {
cy.wait(['@loadSettings', '@loadLogin']); cy.wait(['@loadSettings']);
} }
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist'); cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist'); cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
}); });
Cypress.Commands.add('signin', ({ email, password }) => { Cypress.Commands.add('signin', ({ email, password }) => {
const signinPage = new SigninPage(); Cypress.session.clearAllSavedSessions();
const workflowsPage = new WorkflowsPage(); cy.session([email, password], () => cy.request('POST', '/rest/login', { email, password }), {
validate() {
cy.session( cy.getCookie(N8N_AUTH_COOKIE).should('exist');
[email, password],
() => {
cy.visit(signinPage.url);
signinPage.getters.form().within(() => {
signinPage.getters.email().type(email);
signinPage.getters.password().type(password);
signinPage.getters.submit().click();
});
// we should be redirected to /workflows
cy.url().should('include', workflowsPage.url);
}, },
{ });
validate() {
cy.getCookie(N8N_AUTH_COOKIE).should('exist');
},
},
);
}); });
Cypress.Commands.add('signout', () => { Cypress.Commands.add('signout', () => {
cy.visit('/signout'); cy.request('POST', '/rest/logout');
cy.waitForLoad();
cy.url().should('include', '/signin');
cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist');
}); });
Cypress.Commands.add('signup', ({ firstName, lastName, password, url }) => {
const signupPage = new SignupPage();
cy.visit(url);
signupPage.getters.form().within(() => {
cy.url().then((url) => {
cy.intercept('/rest/users/*').as('userSignup')
signupPage.getters.firstName().type(firstName);
signupPage.getters.lastName().type(lastName);
signupPage.getters.password().type(password);
signupPage.getters.submit().click();
cy.wait('@userSignup');
});
});
});
Cypress.Commands.add('setup', ({ email, firstName, lastName, password }, skipIntercept = false) => {
const signupPage = new SignupPage();
cy.intercept('GET', signupPage.url).as('setupPage');
cy.visit(signupPage.url);
cy.wait('@setupPage');
signupPage.getters.form().within(() => {
cy.url().then((url) => {
if (url.includes(signupPage.url)) {
signupPage.getters.email().type(email);
signupPage.getters.firstName().type(firstName);
signupPage.getters.lastName().type(lastName);
signupPage.getters.password().type(password);
cy.intercept('POST', '/rest/owner/setup').as('setupRequest');
signupPage.getters.submit().click();
if(!skipIntercept) {
cy.wait('@setupRequest');
}
} else {
cy.log('User already signed up');
}
});
});
});
Cypress.Commands.add('interceptREST', (method, url) => { Cypress.Commands.add('interceptREST', (method, url) => {
cy.intercept(method, `http://localhost:5678/rest${url}`); cy.intercept(method, `http://localhost:5678/rest${url}`);
}); });
Cypress.Commands.add('inviteUsers', ({ instanceOwner, users }) => { const setFeature = (feature: string, enabled: boolean) =>
const settingsUsersPage = new SettingsUsersPage(); cy.request('PATCH', `${BASE_URL}/rest/e2e/feature`, { feature: `feat:${feature}`, enabled });
cy.signin(instanceOwner); Cypress.Commands.add('enableFeature', (feature: string) => setFeature(feature, true));
Cypress.Commands.add('disableFeature', (feature): string => setFeature(feature, false));
users.forEach((user) => {
cy.signin(instanceOwner);
cy.visit(settingsUsersPage.url);
cy.interceptREST('POST', '/users').as('inviteUser');
settingsUsersPage.getters.inviteButton().click();
settingsUsersPage.getters.inviteUsersModal().within((modal) => {
settingsUsersPage.getters.inviteUsersModalEmailsInput().type(user.email).type('{enter}');
});
cy.wait('@inviteUser').then((interception) => {
const inviteLink = interception.response!.body.data[0].user.inviteAcceptUrl;
cy.log(JSON.stringify(interception.response!.body.data[0].user));
cy.log(inviteLink);
cy.signout();
cy.signup({ ...user, url: inviteLink });
});
});
});
Cypress.Commands.add('skipSetup', () => {
const signupPage = new SignupPage();
const workflowPage = new WorkflowPage();
const Confirmation = new MessageBox();
cy.intercept('GET', signupPage.url).as('setupPage');
cy.visit(signupPage.url);
cy.wait('@setupPage');
signupPage.getters.form().within(() => {
cy.url().then((url) => {
if (url.endsWith(signupPage.url)) {
signupPage.getters.skip().click();
Confirmation.getters.header().should('contain.text', 'Skip owner account setup?');
Confirmation.actions.confirm();
// we should be redirected to empty canvas
cy.intercept('GET', '/rest/workflows/new').as('loading');
cy.url().should('include', workflowPage.url);
cy.wait('@loading');
} else {
cy.log('User already signed up');
}
});
});
});
Cypress.Commands.add('resetAll', () => {
cy.task('reset');
Cypress.session.clearAllSavedSessions();
});
Cypress.Commands.add('setupOwner', (payload) => {
cy.task('setup-owner', payload);
});
Cypress.Commands.add('enableFeature', (feature) => {
cy.task('set-feature', { feature, enabled: true });
});
Cypress.Commands.add('disableFeature', (feature) => {
cy.task('set-feature', { feature, enabled: false });
});
Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => { Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => {
if (Cypress.isBrowser('chrome')) { if (Cypress.isBrowser('chrome')) {
@ -256,7 +101,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
const originalLocation = Cypress.$(selector)[index].getBoundingClientRect(); const originalLocation = Cypress.$(selector)[index].getBoundingClientRect();
element.trigger('mousedown'); element.trigger('mousedown', { force: true });
element.trigger('mousemove', { element.trigger('mousemove', {
which: 1, which: 1,
pageX: options?.abs ? xDiff : originalLocation.right + xDiff, pageX: options?.abs ? xDiff : originalLocation.right + xDiff,

View file

@ -1,28 +1,19 @@
// *********************************************************** import { BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import './commands'; import './commands';
before(() => { before(() => {
cy.resetAll(); cy.request('POST', `${BASE_URL}/rest/e2e/reset`, {
owner: INSTANCE_OWNER,
members: INSTANCE_MEMBERS,
});
}); });
// Load custom nodes and credentials fixtures
beforeEach(() => { beforeEach(() => {
if (!cy.config('disableAutoLogin')) {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
}
cy.intercept('GET', '/rest/settings').as('loadSettings'); cy.intercept('GET', '/rest/settings').as('loadSettings');
cy.intercept('GET', '/rest/login').as('loadLogin');
// Always intercept the request to test credentials and return a success // Always intercept the request to test credentials and return a success
cy.intercept('POST', '/rest/credentials/test', { cy.intercept('POST', '/rest/credentials/test', {

View file

@ -8,25 +8,14 @@ interface SigninPayload {
password: string; password: string;
} }
interface SetupPayload {
email: string;
password: string;
firstName: string;
lastName: string;
}
interface SignupPayload extends SetupPayload {
url: string;
}
interface InviteUsersPayload {
instanceOwner: SigninPayload;
users: SetupPayload[];
}
declare global { declare global {
namespace Cypress { namespace Cypress {
interface SuiteConfigOverrides {
disableAutoLogin: boolean;
}
interface Chainable { interface Chainable {
config(key: keyof SuiteConfigOverrides): boolean;
getByTestId( getByTestId(
selector: string, selector: string,
...args: (Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined)[] ...args: (Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined)[]
@ -35,13 +24,7 @@ declare global {
createFixtureWorkflow(fixtureKey: string, workflowName: string): void; createFixtureWorkflow(fixtureKey: string, workflowName: string): void;
signin(payload: SigninPayload): void; signin(payload: SigninPayload): void;
signout(): void; signout(): void;
signup(payload: SignupPayload): void;
setup(payload: SetupPayload, skipIntercept?: boolean): void;
setupOwner(payload: SetupPayload): void;
inviteUsers(payload: InviteUsersPayload): void;
interceptREST(method: string, url: string): Chainable<Interception>; interceptREST(method: string, url: string): Chainable<Interception>;
skipSetup(): void;
resetAll(): void;
enableFeature(feature: string): void; enableFeature(feature: string): void;
disableFeature(feature: string): void; disableFeature(feature: string): void;
waitForLoad(waitForIntercepts?: boolean): void; waitForLoad(waitForIntercepts?: boolean): void;

View file

@ -11,12 +11,6 @@ N8N_PATH=/app1/
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from # DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# above example would result in: https://example.com/n8n/ # above example would result in: https://example.com/n8n/
# The user name to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_USER=user
# The password to use for autentication - IMPORTANT ALWAYS CHANGE!
N8N_BASIC_AUTH_PASSWORD=password
# Optional timezone to set which gets used by Cron-Node by default # Optional timezone to set which gets used by Cron-Node by default
# If not set New York time will be used # If not set New York time will be used
GENERIC_TIMEZONE=Europe/Berlin GENERIC_TIMEZONE=Europe/Berlin

View file

@ -41,9 +41,6 @@ services:
- traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true - traefik.http.middlewares.n8n.headers.STSIncludeSubdomains=true
- traefik.http.middlewares.n8n.headers.STSPreload=true - traefik.http.middlewares.n8n.headers.STSPreload=true
environment: environment:
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
- N8N_HOST=${DOMAIN_NAME} - N8N_HOST=${DOMAIN_NAME}
- N8N_PORT=5678 - N8N_PORT=5678
- N8N_PROTOCOL=https - N8N_PROTOCOL=https

View file

@ -1,8 +0,0 @@
MARIADB_ROOT_PASSWORD=changePassword
MARIADB_DATABASE=n8n
MARIADB_USER=changeUser
MARIADB_PASSWORD=changePassword
N8N_BASIC_AUTH_USER=changeUser
N8N_BASIC_AUTH_PASSWORD=changePassword

View file

@ -1,24 +0,0 @@
# n8n with MariaDB
Starts n8n with MariaDB as database.
## Start
To start n8n with MariaDB simply start docker-compose by executing the following
command in the current folder.
**IMPORTANT:** But before you do that change the default users and passwords in the [`.env`](.env) file!
```
docker-compose up -d
```
To stop it execute:
```
docker-compose stop
```
## Configuration
The default name of the database, user and password for MariaDB can be changed in the [`.env`](.env) file in the current directory.

View file

@ -1,43 +0,0 @@
version: '3.8'
volumes:
db_storage:
n8n_storage:
services:
db:
image: mariadb:10.7
restart: always
environment:
- MARIADB_ROOT_PASSWORD
- MARIADB_DATABASE
- MARIADB_USER
- MARIADB_PASSWORD
- MARIADB_MYSQL_LOCALHOST_USER=true
volumes:
- db_storage:/var/lib/mysql
healthcheck:
test: "/usr/bin/mysql --user=${MARIADB_USER} --password=${MARIADB_PASSWORD} --execute 'SELECT 1;'"
interval: 10s
timeout: 5s
retries: 10
n8n:
image: docker.n8n.io/n8nio/n8n
restart: always
environment:
- DB_TYPE=mariadb
- DB_MYSQLDB_HOST=db
- DB_MYSQLDB_DATABASE=${MARIADB_DATABASE}
- DB_MYSQLDB_USER=${MARIADB_USER}
- DB_MYSQLDB_PASSWORD=${MARIADB_PASSWORD}
ports:
- 5678:5678
links:
- db
volumes:
- n8n_storage:/home/node/.n8n
command: n8n start --tunnel
depends_on:
db:
condition: service_healthy

View file

@ -4,6 +4,3 @@ POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=changeUser POSTGRES_NON_ROOT_USER=changeUser
POSTGRES_NON_ROOT_PASSWORD=changePassword POSTGRES_NON_ROOT_PASSWORD=changePassword
N8N_BASIC_AUTH_USER=changeUser
N8N_BASIC_AUTH_PASSWORD=changePassword

View file

@ -33,16 +33,12 @@ services:
- DB_POSTGRESDB_DATABASE=${POSTGRES_DB} - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
- DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER} - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
- DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD} - DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
ports: ports:
- 5678:5678 - 5678:5678
links: links:
- postgres - postgres
volumes: volumes:
- n8n_storage:/home/node/.n8n - n8n_storage:/home/node/.n8n
command: /bin/sh -c "n8n start --tunnel"
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy

View file

@ -4,6 +4,3 @@ POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=changeUser POSTGRES_NON_ROOT_USER=changeUser
POSTGRES_NON_ROOT_PASSWORD=changePassword POSTGRES_NON_ROOT_PASSWORD=changePassword
N8N_BASIC_AUTH_USER=changeUser
N8N_BASIC_AUTH_PASSWORD=changePassword

View file

@ -7,6 +7,7 @@ volumes:
x-shared: &shared x-shared: &shared
restart: always restart: always
image: docker.n8n.io/n8nio/n8n
environment: environment:
- DB_TYPE=postgresdb - DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres - DB_POSTGRESDB_HOST=postgres
@ -17,9 +18,6 @@ x-shared: &shared
- EXECUTIONS_MODE=queue - EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis - QUEUE_BULL_REDIS_HOST=redis
- QUEUE_HEALTH_CHECK_ACTIVE=true - QUEUE_HEALTH_CHECK_ACTIVE=true
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER
- N8N_BASIC_AUTH_PASSWORD
links: links:
- postgres - postgres
- redis - redis
@ -63,14 +61,11 @@ services:
n8n: n8n:
<<: *shared <<: *shared
image: docker.n8n.io/n8nio/n8n
command: /bin/sh -c "n8n start --tunnel"
ports: ports:
- 5678:5678 - 5678:5678
n8n-worker: n8n-worker:
<<: *shared <<: *shared
image: docker.n8n.io/n8nio/n8n command: worker
command: /bin/sh -c "sleep 5; n8n worker"
depends_on: depends_on:
- n8n - n8n

View file

@ -1,12 +1,12 @@
ARG NODE_VERSION=16 ARG NODE_VERSION=18
FROM node:${NODE_VERSION}-alpine FROM node:${NODE_VERSION}-alpine
WORKDIR /home/node WORKDIR /home/node
COPY .npmrc /usr/local/etc/npmrc COPY .npmrc /usr/local/etc/npmrc
RUN \ RUN \
apk add --update git openssh graphicsmagick tini tzdata ca-certificates && \ apk add --update git openssh graphicsmagick tini tzdata ca-certificates libc6-compat && \
npm install -g npm@8.19.2 full-icu && \ npm install -g npm@9.5.1 full-icu && \
rm -rf /var/cache/apk/* /root/.npm /tmp/* && \ rm -rf /var/cache/apk/* /root/.npm /tmp/* && \
# Install fonts # Install fonts
apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \ apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \

View file

@ -8,7 +8,7 @@ COPY --chown=node:node scripts ./scripts
COPY --chown=node:node packages ./packages COPY --chown=node:node packages ./packages
COPY --chown=node:node patches ./patches COPY --chown=node:node patches ./patches
RUN apk add --update libc6-compat jq RUN apk add --update jq
RUN corepack enable && corepack prepare --activate RUN corepack enable && corepack prepare --activate
USER node USER node
@ -28,7 +28,8 @@ RUN rm -rf patches .npmrc *.yaml node_modules/.cache packages/**/node_modules/.c
FROM n8nio/base:${NODE_VERSION} FROM n8nio/base:${NODE_VERSION}
COPY --from=builder /home/node /usr/local/lib/node_modules/n8n COPY --from=builder /home/node /usr/local/lib/node_modules/n8n
RUN ln -s /usr/local/lib/node_modules/n8n/packages/cli/bin/n8n /usr/local/bin/n8n RUN ln -s /usr/local/lib/node_modules/n8n/packages/cli/bin/n8n /usr/local/bin/n8n
COPY docker/images/n8n-custom/docker-entrypoint.sh /
COPY docker/images/n8n/docker-entrypoint.sh /
RUN \ RUN \
mkdir .n8n && \ mkdir .n8n && \

View file

@ -1,8 +0,0 @@
#!/bin/sh
if [ "$#" -gt 0 ]; then
# Got started with arguments
node "$@"
else
# Got started without arguments
n8n
fi

View file

@ -1,24 +0,0 @@
FROM node:16
ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \
apt-get update && \
apt-get -y install graphicsmagick gosu git
# Set a custom user to not have n8n run as root
USER root
RUN npm_config_user=root npm install -g npm@8.19.2 full-icu n8n@${N8N_VERSION}
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
WORKDIR /data
COPY docker-entrypoint.sh /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
EXPOSE 5678/tcp

View file

@ -1,20 +0,0 @@
## n8n - Debian Docker Image
Dockerfile to build n8n with Debian.
For information about how to run n8n with Docker check the generic
[Docker-Readme](https://github.com/n8n-io/n8n/tree/master/docker/images/n8n/README.md)
```
docker build --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
# For example:
docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-debian .
```
```
docker run -it --rm \
--name n8n \
-p 5678:5678 \
n8nio/n8n:0.43.0-debian
```

View file

@ -1,15 +0,0 @@
#!/bin/sh
if [ -d /root/.n8n ] ; then
chmod o+rx /root
chown -R node /root/.n8n
ln -s /root/.n8n /home/node/
fi
if [ "$#" -gt 0 ]; then
# Got started with arguments
exec gosu node "$@"
else
# Got started without arguments
exec gosu node n8n
fi

View file

@ -1,24 +0,0 @@
FROM richxsl/rhel7
ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \
yum install -y gcc-c++ make
RUN \
curl -sL https://rpm.nodesource.com/setup_12.x | sudo -E bash -
RUN \
sudo yum install nodejs
# Set a custom user to not have n8n run as root
USER root
RUN npm_config_user=root npm install -g npm@8.19.2 n8n@${N8N_VERSION}
WORKDIR /data
CMD "n8n"

View file

@ -1,15 +0,0 @@
## Build Docker-Image
```
docker build --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
# For example:
docker build --build-arg N8N_VERSION=0.36.1 -t n8nio/n8n:0.36.1-rhel7 .
```
```
docker run -it --rm \
--name n8n \
-p 5678:5678 \
n8nio/n8n:0.25.0-ubuntu
```

View file

@ -1,4 +1,4 @@
ARG NODE_VERSION=16 ARG NODE_VERSION=18
FROM n8nio/base:${NODE_VERSION} FROM n8nio/base:${NODE_VERSION}
ARG N8N_VERSION ARG N8N_VERSION
@ -18,9 +18,10 @@ RUN set -eux; \
find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm && \ find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm && \
rm -rf /root/.npm rm -rf /root/.npm
# Set a custom user to not have n8n run as root COPY docker-entrypoint.sh /
USER root
WORKDIR /data RUN \
RUN apk --no-cache add su-exec mkdir .n8n && \
COPY docker-entrypoint.sh /docker-entrypoint.sh chown node:node .n8n
USER node
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"] ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]

View file

@ -8,21 +8,27 @@ n8n is an extendable workflow automation tool. With a [fair-code](http://faircod
## Contents ## Contents
- [Demo](#demo) - [n8n - Workflow automation tool](#n8n---workflow-automation-tool)
- [Available integrations](#available-integrations) - [Contents](#contents)
- [Documentation](#documentation) - [Demo](#demo)
- [Start n8n in Docker](#start-n8n-in-docker) - [Available integrations](#available-integrations)
- [Start with tunnel](#start-with-tunnel) - [Documentation](#documentation)
- [Securing n8n](#securing-n8n) - [Start n8n in Docker](#start-n8n-in-docker)
- [Persist data](#persist-data) - [Start with tunnel](#start-with-tunnel)
- [Passing Sensitive Data via File](#passing-sensitive-data-via-file) - [Persist data](#persist-data)
- [Updating a Running docker-compose Instance](#updating-a-running-docker-compose-instance) - [Start with other Database](#start-with-other-database)
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt) - [Use with PostgresDB](#use-with-postgresdb)
- [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it) - [Use with MySQL](#use-with-mysql)
- [Support](#support) - [Passing Sensitive Data via File](#passing-sensitive-data-via-file)
- [Jobs](#jobs) - [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt)
- [Upgrading](#upgrading) - [Updating a running docker-compose instance](#updating-a-running-docker-compose-instance)
- [License](#license) - [Setting Timezone](#setting-timezone)
- [Build Docker-Image](#build-docker-image)
- [What does n8n mean and how do you pronounce it?](#what-does-n8n-mean-and-how-do-you-pronounce-it)
- [Support](#support)
- [Jobs](#jobs)
- [Upgrading](#upgrading)
- [License](#license)
## Demo ## Demo
@ -71,20 +77,6 @@ docker run -it --rm \
n8n start --tunnel n8n start --tunnel
``` ```
## Securing n8n
By default n8n can be accessed by everybody. This is OK if you have it only running
locally but if you deploy it on a server which is accessible from the web you have
to make sure that n8n is protected!
Right now we have very basic protection via basic-auth in place. It can be activated
by setting the following environment variables:
```text
N8N_BASIC_AUTH_ACTIVE=true
N8N_BASIC_AUTH_USER=<USER>
N8N_BASIC_AUTH_PASSWORD=<PASSWORD>
```
## Persist data ## Persist data
The workflow data gets by default saved in an SQLite database in the user The workflow data gets by default saved in an SQLite database in the user
@ -171,7 +163,7 @@ docker run -it --rm \
To avoid passing sensitive information via environment variables "\_FILE" may be To avoid passing sensitive information via environment variables "\_FILE" may be
appended to some environment variables. It will then load the data from a file appended to some environment variables. It will then load the data from a file
with the given name. That makes it possible to load data easily from with the given name. That makes it possible to load data easily from
Docker- and Kubernetes-Secrets. Docker and Kubernetes secrets.
The following environment variables support file input: The following environment variables support file input:
@ -181,8 +173,6 @@ The following environment variables support file input:
- DB_POSTGRESDB_PORT_FILE - DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE - DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE - DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE
## Example Setup with Lets Encrypt ## Example Setup with Lets Encrypt
@ -193,26 +183,25 @@ A basic step by step example setup of n8n with docker-compose and Lets Encrypt i
1. Pull the latest version from the registry 1. Pull the latest version from the registry
`docker pull docker.n8n.io/n8nio/n8n` `docker pull docker.n8n.io/n8nio/n8n`
2. Stop the current setup 2. Stop the current setup
`sudo docker-compose stop` `sudo docker-compose stop`
3. Delete it (will only delete the docker-containers, data is stored separately) 3. Delete it (will only delete the docker-containers, data is stored separately)
`sudo docker-compose rm` `sudo docker-compose rm`
4. Then start it again 4. Then start it again
`sudo docker-compose up -d` `sudo docker-compose up -d`
## Setting Timezone ## Setting Timezone
To define the timezone n8n should use, the environment variable `GENERIC_TIMEZONE` can To define the timezone n8n should use, the environment variable `GENERIC_TIMEZONE` can
be set. This gets used by for example the Cron-Node. be set. One instance where this variable is implemented is in the Schedule node. Furthermore, the system's timezone can be set separately,
Apart from that can also the timezone of the system be set separately. Which controls what which controls the output of certain scripts and commands such as `$ date`. The system timezone can be set via
some scripts and commands return like `$ date`. The system timezone can be set via
the environment variable `TZ`. the environment variable `TZ`.
Example to use the same timezone for both: Example to use the same timezone for both:

View file

@ -1,17 +1,8 @@
#!/bin/sh #!/bin/sh
if [ -d /root/.n8n ] ; then
chmod o+rx /root
chown -R node /root/.n8n
ln -s /root/.n8n /home/node/
fi
chown -R node /home/node
if [ "$#" -gt 0 ]; then if [ "$#" -gt 0 ]; then
# Got started with arguments # Got started with arguments
exec su-exec node "$@" n8n "$@"
else else
# Got started without arguments # Got started without arguments
exec su-exec node n8n n8n
fi fi

View file

@ -1,2 +0,0 @@
#!/bin/bash
docker build --build-arg N8N_VERSION=$DOCKER_TAG -f $DOCKERFILE_PATH -t $IMAGE_NAME .

View file

@ -1,10 +1,10 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.234.0", "version": "1.0.1",
"private": true, "private": true,
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
"engines": { "engines": {
"node": ">=16.9", "node": ">=18.10",
"pnpm": ">=8.6" "pnpm": ">=8.6"
}, },
"packageManager": "pnpm@8.6.1", "packageManager": "pnpm@8.6.1",
@ -30,7 +30,6 @@
"cypress:open": "CYPRESS_BASE_URL=http://localhost:8080 cypress open", "cypress:open": "CYPRESS_BASE_URL=http://localhost:8080 cypress open",
"test:e2e:ui": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress open'", "test:e2e:ui": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress open'",
"test:e2e:dev": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first CYPRESS_BASE_URL=http://localhost:8080 start-server-and-test dev http://localhost:8080/favicon.ico 'cypress open'", "test:e2e:dev": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first CYPRESS_BASE_URL=http://localhost:8080 start-server-and-test dev http://localhost:8080/favicon.ico 'cypress open'",
"test:e2e:smoke": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless --spec \"cypress/e2e/0-smoke.cy.ts\"'",
"test:e2e:all": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'" "test:e2e:all": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'"
}, },
"dependencies": { "dependencies": {
@ -52,7 +51,6 @@
"jest-mock": "^29.5.0", "jest-mock": "^29.5.0",
"jest-mock-extended": "^3.0.4", "jest-mock-extended": "^3.0.4",
"nock": "^13.2.9", "nock": "^13.2.9",
"node-fetch": "^2.6.7",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"prettier": "^2.8.3", "prettier": "^2.8.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@ -82,6 +80,7 @@
"http-cache-semantics": "4.1.1", "http-cache-semantics": "4.1.1",
"jsonwebtoken": "9.0.0", "jsonwebtoken": "9.0.0",
"prettier": "^2.8.3", "prettier": "^2.8.3",
"tough-cookie": "^4.1.3",
"tslib": "^2.5.0", "tslib": "^2.5.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.3", "typescript": "^5.1.3",
@ -93,8 +92,8 @@
"typedi@0.10.0": "patches/typedi@0.10.0.patch", "typedi@0.10.0": "patches/typedi@0.10.0.patch",
"@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch",
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
"typeorm@0.3.12": "patches/typeorm@0.3.12.patch", "element-plus@2.3.6": "patches/element-plus@2.3.6.patch",
"element-plus@2.3.6": "patches/element-plus@2.3.6.patch" "pyodide@0.23.4": "patches/pyodide@0.23.4.patch"
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@n8n/client-oauth2", "name": "@n8n/client-oauth2",
"version": "0.3.0", "version": "0.4.0",
"scripts": { "scripts": {
"clean": "rimraf dist .turbo", "clean": "rimraf dist .turbo",
"dev": "pnpm watch", "dev": "pnpm watch",

View file

@ -57,7 +57,6 @@ export class CodeFlow {
opts?: Partial<ClientOAuth2Options>, opts?: Partial<ClientOAuth2Options>,
): Promise<ClientOAuth2Token> { ): Promise<ClientOAuth2Token> {
const options = { ...this.client.options, ...opts }; const options = { ...this.client.options, ...opts };
expects(options, 'clientId', 'accessTokenUri'); expects(options, 'clientId', 'accessTokenUri');
const url = uri instanceof URL ? uri : new URL(uri, DEFAULT_URL_BASE); const url = uri instanceof URL ? uri : new URL(uri, DEFAULT_URL_BASE);

View file

@ -21,7 +21,6 @@ export class CredentialsFlow {
*/ */
async getToken(opts?: Partial<ClientOAuth2Options>): Promise<ClientOAuth2Token> { async getToken(opts?: Partial<ClientOAuth2Options>): Promise<ClientOAuth2Token> {
const options = { ...this.client.options, ...opts }; const options = { ...this.client.options, ...opts };
expects(options, 'clientId', 'clientSecret', 'accessTokenUri'); expects(options, 'clientId', 'clientSecret', 'accessTokenUri');
const body: CredentialsFlowBody = { const body: CredentialsFlowBody = {

View file

@ -1 +0,0 @@
dist/ReloadNodesAndCredentials.*

View file

@ -2,6 +2,33 @@
This list shows all the versions which include breaking changes and how to upgrade. This list shows all the versions which include breaking changes and how to upgrade.
## 1.0.0
### What changed?
The minimum Node.js version required for n8n is now v18.
### When is action necessary?
If you're using n8n via npm or PM2 or if you're contributing to n8n.
### How to upgrade:
Update the Node.js version to v18 or above.
## 0.234.0
### What changed?
This release introduces two irreversible changes:
* The n8n database will use strings instead of numeric values to identify workflows and credentials
* Execution data is split into a separate database table
### When is action necessary?
It will not be possible to read a n8n@0.234.0 database with older versions of n8n, so we recommend that you take a full backup before migrating.
## 0.232.0 ## 0.232.0
### What changed? ### What changed?

View file

@ -21,10 +21,10 @@ if (process.argv.length === 2) {
const nodeVersion = process.versions.node; const nodeVersion = process.versions.node;
const nodeVersionMajor = require('semver').major(nodeVersion); const nodeVersionMajor = require('semver').major(nodeVersion);
if (![16, 18].includes(nodeVersionMajor)) { if (![18, 20].includes(nodeVersionMajor)) {
console.log(` console.log(`
Your Node.js version (${nodeVersion}) is currently not supported by n8n. Your Node.js version (${nodeVersion}) is currently not supported by n8n.
Please use Node.js v16 (recommended), or v18 instead! Please use Node.js v18 (recommended), or v20 instead!
`); `);
process.exit(1); process.exit(1);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.234.0", "version": "1.0.1",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -53,13 +53,15 @@
"workflow" "workflow"
], ],
"engines": { "engines": {
"node": ">=16.9" "node": ">=18.10"
}, },
"files": [ "files": [
"bin", "bin",
"templates", "templates",
"dist", "dist",
"oclif.manifest.json" "oclif.manifest.json",
"!dist/**/e2e.*",
"!dist/ReloadNodesAndCredentials.*"
], ],
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-cli": "4.0.0", "@apidevtools/swagger-cli": "4.0.0",
@ -72,7 +74,7 @@
"@types/convict": "^6.1.1", "@types/convict": "^6.1.1",
"@types/cookie-parser": "^1.4.2", "@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"@types/json-diff": "^0.5.1", "@types/json-diff": "^1.0.0",
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/localtunnel": "^1.9.0", "@types/localtunnel": "^1.9.0",
"@types/lodash": "^4.14.195", "@types/lodash": "^4.14.195",
@ -137,7 +139,7 @@
"handlebars": "4.7.7", "handlebars": "4.7.7",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"ioredis": "^5.2.4", "ioredis": "^5.2.4",
"json-diff": "^0.5.4", "json-diff": "^1.0.6",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jwks-rsa": "^3.0.1", "jwks-rsa": "^3.0.1",

View file

@ -9,12 +9,9 @@ const ROOT_DIR = path.resolve(__dirname, '..');
const SPEC_FILENAME = 'openapi.yml'; const SPEC_FILENAME = 'openapi.yml';
const SPEC_THEME_FILENAME = 'swaggerTheme.css'; const SPEC_THEME_FILENAME = 'swaggerTheme.css';
const userManagementEnabled = process.env.N8N_USER_MANAGEMENT_DISABLED !== 'true';
const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true'; const publicApiEnabled = process.env.N8N_PUBLIC_API_DISABLED !== 'true';
if (userManagementEnabled) { copyUserManagementEmailTemplates();
copyUserManagementEmailTemplates();
}
if (publicApiEnabled) { if (publicApiEnabled) {
copySwaggerTheme(); copySwaggerTheme();

View file

@ -2,6 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Container, Service } from 'typedi';
import type { import type {
IDeferredPromise, IDeferredPromise,
IExecuteResponsePromiseData, IExecuteResponsePromiseData,
@ -19,8 +20,7 @@ import type {
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
import { isWorkflowIdValid } from '@/utils'; import { isWorkflowIdValid } from '@/utils';
import Container, { Service } from 'typedi'; import { ExecutionRepository } from '@db/repositories';
import { ExecutionRepository } from './databases/repositories';
@Service() @Service()
export class ActiveExecutions { export class ActiveExecutions {

View file

@ -26,16 +26,13 @@ export class CredentialTypes implements ICredentialTypes {
* Returns all parent types of the given credential type * Returns all parent types of the given credential type
*/ */
getParentTypes(typeName: string): string[] { getParentTypes(typeName: string): string[] {
const credentialType = this.getByName(typeName); const extendsArr = this.knownCredentials[typeName]?.extends ?? [];
if (credentialType?.extends === undefined) return []; if (extendsArr.length) {
extendsArr.forEach((type) => {
const types: string[] = []; extendsArr.push(...this.getParentTypes(type));
credentialType.extends.forEach((type: string) => { });
types.push(type); }
types.push(...this.getParentTypes(type)); return extendsArr;
});
return types;
} }
private getCredential(type: string): LoadedClass<ICredentialType> { private getCredential(type: string): LoadedClass<ICredentialType> {

View file

@ -46,7 +46,6 @@ export const initErrorHandling = async () => {
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
ErrorReporterProxy.error(error); ErrorReporterProxy.error(error);
if (error.constructor?.name !== 'AxiosError') throw error;
}); });
ErrorReporterProxy.init({ ErrorReporterProxy.init({

View file

@ -13,6 +13,7 @@ import type {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { Container } from 'typedi';
import { Like } from 'typeorm'; import { Like } from 'typeorm';
import config from '@/config'; import config from '@/config';
import * as Db from '@/Db'; import * as Db from '@/Db';
@ -23,8 +24,7 @@ import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import type { UserUpdatePayload } from '@/requests'; import type { UserUpdatePayload } from '@/requests';
import Container from 'typedi'; import { ExecutionRepository } from '@db/repositories';
import { ExecutionRepository } from './databases/repositories';
/** /**
* Returns the base URL n8n is reachable from * Returns the base URL n8n is reachable from

View file

@ -61,6 +61,7 @@ import type {
WorkflowStatisticsRepository, WorkflowStatisticsRepository,
WorkflowTagMappingRepository, WorkflowTagMappingRepository,
} from '@db/repositories'; } from '@db/repositories';
import type { LICENSE_FEATURES, LICENSE_QUOTAS } from './constants';
export interface IActivationError { export interface IActivationError {
time: number; time: number;
@ -306,7 +307,6 @@ export interface IDiagnosticInfo {
databaseType: DatabaseType; databaseType: DatabaseType;
notificationsEnabled: boolean; notificationsEnabled: boolean;
disableProductionWebhooksOnMainProcess: boolean; disableProductionWebhooksOnMainProcess: boolean;
basicAuthActive: boolean;
systemInfo: { systemInfo: {
os: { os: {
type?: string; type?: string;
@ -324,7 +324,6 @@ export interface IDiagnosticInfo {
}; };
deploymentType: string; deploymentType: string;
binaryDataMode: string; binaryDataMode: string;
n8n_multi_user_allowed: boolean;
smtp_set_up: boolean; smtp_set_up: boolean;
ldap_allowed: boolean; ldap_allowed: boolean;
saml_enabled: boolean; saml_enabled: boolean;
@ -718,6 +717,11 @@ export interface IExecutionTrackProperties extends ITelemetryTrackProperties {
// license // license
// ---------------------------------- // ----------------------------------
type ValuesOf<T> = T[keyof T];
export type BooleanLicenseFeature = ValuesOf<typeof LICENSE_FEATURES>;
export type NumericLicenseFeature = ValuesOf<typeof LICENSE_QUOTAS>;
export interface ILicenseReadResponse { export interface ILicenseReadResponse {
usage: { usage: {
executions: { executions: {

View file

@ -30,8 +30,8 @@ import { eventBus } from './eventbus';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { NodeTypes } from './NodeTypes'; import { NodeTypes } from './NodeTypes';
import type { ExecutionMetadata } from './databases/entities/ExecutionMetadata'; import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { ExecutionRepository } from './databases/repositories'; import { ExecutionRepository } from '@db/repositories';
function userToPayload(user: User): { function userToPayload(user: User): {
userId: string; userId: string;
@ -75,12 +75,10 @@ export class InternalHooks implements IInternalHooksClass {
db_type: diagnosticInfo.databaseType, db_type: diagnosticInfo.databaseType,
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled, n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess, n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
n8n_basic_auth_active: diagnosticInfo.basicAuthActive,
system_info: diagnosticInfo.systemInfo, system_info: diagnosticInfo.systemInfo,
execution_variables: diagnosticInfo.executionVariables, execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType, n8n_deployment_type: diagnosticInfo.deploymentType,
n8n_binary_data_mode: diagnosticInfo.binaryDataMode, n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
smtp_set_up: diagnosticInfo.smtp_set_up, smtp_set_up: diagnosticInfo.smtp_set_up,
ldap_allowed: diagnosticInfo.ldap_allowed, ldap_allowed: diagnosticInfo.ldap_allowed,
saml_enabled: diagnosticInfo.saml_enabled, saml_enabled: diagnosticInfo.saml_enabled,

View file

@ -12,7 +12,6 @@ import { User } from '@db/entities/User';
import { AuthIdentity } from '@db/entities/AuthIdentity'; import { AuthIdentity } from '@db/entities/AuthIdentity';
import { RoleRepository } from '@db/repositories'; import { RoleRepository } from '@db/repositories';
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory'; import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
import { LdapManager } from './LdapManager.ee'; import { LdapManager } from './LdapManager.ee';
import { import {
@ -37,9 +36,8 @@ import { InternalServerError } from '../ResponseHelper';
/** /**
* Check whether the LDAP feature is disabled in the instance * Check whether the LDAP feature is disabled in the instance
*/ */
export const isLdapEnabled = (): boolean => { export const isLdapEnabled = () => {
const license = Container.get(License); return Container.get(License).isLdapEnabled();
return isUserManagementEnabled() && license.isLdapEnabled();
}; };
/** /**

View file

@ -9,8 +9,16 @@ import {
LICENSE_QUOTAS, LICENSE_QUOTAS,
N8N_VERSION, N8N_VERSION,
SETTINGS_LICENSE_CERT_KEY, SETTINGS_LICENSE_CERT_KEY,
UNLIMITED_LICENSE_QUOTA,
} from './constants'; } from './constants';
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { BooleanLicenseFeature, NumericLicenseFeature } from './Interfaces';
type FeatureReturnType = Partial<
{
planName: string;
} & { [K in NumericLicenseFeature]: number } & { [K in BooleanLicenseFeature]: boolean }
>;
@Service() @Service()
export class License { export class License {
@ -96,13 +104,8 @@ export class License {
await this.manager.renew(); await this.manager.renew();
} }
isFeatureEnabled(feature: string): boolean { isFeatureEnabled(feature: BooleanLicenseFeature) {
if (!this.manager) { return this.manager?.hasFeatureEnabled(feature) ?? false;
getLogger().warn('License manager not initialized');
return false;
}
return this.manager.hasFeatureEnabled(feature);
} }
isSharingEnabled() { isSharingEnabled() {
@ -141,15 +144,8 @@ export class License {
return this.manager?.getCurrentEntitlements() ?? []; return this.manager?.getCurrentEntitlements() ?? [];
} }
getFeatureValue( getFeatureValue<T extends keyof FeatureReturnType>(feature: T): FeatureReturnType[T] {
feature: string, return this.manager?.getFeatureValue(feature) as FeatureReturnType[T];
requireValidCert?: boolean,
): undefined | boolean | number | string {
if (!this.manager) {
return undefined;
}
return this.manager.getFeatureValue(feature, requireValidCert);
} }
getManagementJwt(): string { getManagementJwt(): string {
@ -178,20 +174,20 @@ export class License {
} }
// Helper functions for computed data // Helper functions for computed data
getTriggerLimit(): number { getUsersLimit() {
return (this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? -1) as number; return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
} }
getVariablesLimit(): number { getTriggerLimit() {
return (this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? -1) as number; return this.getFeatureValue(LICENSE_QUOTAS.TRIGGER_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
} }
getUsersLimit(): number { getVariablesLimit() {
return this.getFeatureValue(LICENSE_QUOTAS.USERS_LIMIT) as number; return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA;
} }
getPlanName(): string { getPlanName(): string {
return (this.getFeatureValue('planName') ?? 'Community') as string; return this.getFeatureValue('planName') ?? 'Community';
} }
getInfo(): string { getInfo(): string {
@ -201,4 +197,8 @@ export class License {
return this.manager.toString(); return this.manager.toString();
} }
isWithinUsersLimit() {
return this.getUsersLimit() === UNLIMITED_LICENSE_QUOTA;
}
} }

View file

@ -66,21 +66,23 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath(); this.downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
// Load nodes from `n8n-nodes-base` and any other `n8n-nodes-*` package in the main `node_modules` // Load nodes from `n8n-nodes-base`
const pathsToScan = [ const basePathsToScan = [
// In case "n8n" package is in same node_modules folder. // In case "n8n" package is in same node_modules folder.
path.join(CLI_DIR, '..'), path.join(CLI_DIR, '..'),
// In case "n8n" package is the root and the packages are // In case "n8n" package is the root and the packages are
// in the "node_modules" folder underneath it. // in the "node_modules" folder underneath it.
path.join(CLI_DIR, 'node_modules'), path.join(CLI_DIR, 'node_modules'),
// Path where all community nodes are installed
path.join(this.downloadFolder, 'node_modules'),
]; ];
for (const nodeModulesDir of pathsToScan) { for (const nodeModulesDir of basePathsToScan) {
await this.loadNodesFromNodeModules(nodeModulesDir); await this.loadNodesFromNodeModules(nodeModulesDir, 'n8n-nodes-base');
} }
// Load nodes from any other `n8n-nodes-*` packages in the download directory
// This includes the community nodes
await this.loadNodesFromNodeModules(path.join(this.downloadFolder, 'node_modules'));
await this.loadNodesFromCustomDirectories(); await this.loadNodesFromCustomDirectories();
await this.postProcessLoaders(); await this.postProcessLoaders();
this.injectCustomApiCallOptions(); this.injectCustomApiCallOptions();
@ -127,12 +129,15 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
await writeStaticJSON('credentials', this.types.credentials); await writeStaticJSON('credentials', this.types.credentials);
} }
private async loadNodesFromNodeModules(nodeModulesDir: string): Promise<void> { private async loadNodesFromNodeModules(
const globOptions = { cwd: nodeModulesDir, onlyDirectories: true }; nodeModulesDir: string,
const installedPackagePaths = [ packageName?: string,
...(await glob('n8n-nodes-*', { ...globOptions, deep: 1 })), ): Promise<void> {
...(await glob('@*/n8n-nodes-*', { ...globOptions, deep: 2 })), const installedPackagePaths = await glob(packageName ?? ['n8n-nodes-*', '@*/n8n-nodes-*'], {
]; cwd: nodeModulesDir,
onlyDirectories: true,
deep: 1,
});
for (const packagePath of installedPackagePaths) { for (const packagePath of installedPackagePaths) {
try { try {
@ -326,7 +331,7 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
for (const loader of Object.values(this.loaders)) { for (const loader of Object.values(this.loaders)) {
// list of node & credential types that will be sent to the frontend // list of node & credential types that will be sent to the frontend
const { types, directory } = loader; const { known, types, directory } = loader;
this.types.nodes = this.types.nodes.concat(types.nodes); this.types.nodes = this.types.nodes.concat(types.nodes);
this.types.credentials = this.types.credentials.concat(types.credentials); this.types.credentials = this.types.credentials.concat(types.credentials);
@ -339,26 +344,30 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
this.loaded.credentials[credentialTypeName] = loader.credentialTypes[credentialTypeName]; this.loaded.credentials[credentialTypeName] = loader.credentialTypes[credentialTypeName];
} }
// Nodes and credentials that will be lazy loaded for (const type in known.nodes) {
if (loader instanceof PackageDirectoryLoader) { const { className, sourcePath } = known.nodes[type];
const { packageName, known } = loader; this.known.nodes[type] = {
className,
sourcePath: path.join(directory, sourcePath),
};
}
for (const type in known.nodes) { for (const type in known.credentials) {
const { className, sourcePath } = known.nodes[type]; const {
this.known.nodes[type] = { className,
className, sourcePath,
sourcePath: path.join(directory, sourcePath), nodesToTestWith,
}; extends: extendsArr,
} } = known.credentials[type];
this.known.credentials[type] = {
for (const type in known.credentials) { className,
const { className, sourcePath, nodesToTestWith } = known.credentials[type]; sourcePath: path.join(directory, sourcePath),
this.known.credentials[type] = { nodesToTestWith:
className, loader instanceof PackageDirectoryLoader
sourcePath: path.join(directory, sourcePath), ? nodesToTestWith?.map((nodeName) => `${loader.packageName}.${nodeName}`)
nodesToTestWith: nodesToTestWith?.map((nodeName) => `${packageName}.${nodeName}`), : undefined,
}; extends: extendsArr,
} };
} }
} }
} }

View file

@ -138,13 +138,13 @@ export type OperationID = 'getUsers' | 'getUser';
type PaginationBase = { limit: number }; type PaginationBase = { limit: number };
type PaginationOffsetDecoded = PaginationBase & { offset: number }; export type PaginationOffsetDecoded = PaginationBase & { offset: number };
type PaginationCursorDecoded = PaginationBase & { lastId: string }; export type PaginationCursorDecoded = PaginationBase & { lastId: string };
type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number }; export type OffsetPagination = PaginationBase & { offset: number; numberOfTotalRecords: number };
type CursorPagination = PaginationBase & { lastId: string; numberOfNextRecords: number }; export type CursorPagination = PaginationBase & { lastId: string; numberOfNextRecords: number };
export interface IRequired { export interface IRequired {
required?: string[]; required?: string[];
not?: { required?: string[] }; not?: { required?: string[] };

View file

@ -37,7 +37,7 @@ export = {
return res.status(404).json({ message: 'Not Found' }); return res.status(404).json({ message: 'Not Found' });
} }
await BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(execution.id!); await BinaryDataManager.getInstance().deleteBinaryDataByExecutionIds([execution.id!]);
await deleteExecution(execution); await deleteExecution(execution);

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