mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
chore: merge master
This commit is contained in:
commit
b86cc334cd
|
@ -10,3 +10,5 @@ packages/**/.turbo
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
packages/cli/dist/**/e2e.*
|
||||||
|
packages/cli/dist/ReloadNodesAndCredentials.*
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
||||||
|
|
20
.github/scripts/check-tests.mjs
vendored
20
.github/scripts/check-tests.mjs
vendored
|
@ -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);
|
||||||
|
|
8
.github/scripts/package.json
vendored
8
.github/scripts/package.json
vendored
|
@ -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
42
.github/scripts/update-changelog.mjs
vendored
Normal 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));
|
18
.github/workflows/check-issue-template.yml
vendored
Normal file
18
.github/workflows/check-issue-template.yml
vendored
Normal 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
22
.github/workflows/checklist.yml
vendored
Normal 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.
|
2
.github/workflows/ci-master.yml
vendored
2
.github/workflows/ci-master.yml
vendored
|
@ -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
|
||||||
|
|
28
.github/workflows/ci-pull-requests.yml
vendored
28
.github/workflows/ci-pull-requests.yml
vendored
|
@ -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.
|
|
||||||
|
|
3
.github/workflows/docker-base-image.yml
vendored
3
.github/workflows/docker-base-image.yml
vendored
|
@ -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:
|
||||||
|
|
46
.github/workflows/docker-image-v1-rc.yml
vendored
46
.github/workflows/docker-image-v1-rc.yml
vendored
|
@ -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
|
|
10
.github/workflows/docker-images.yml
vendored
10
.github/workflows/docker-images.yml
vendored
|
@ -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 }}
|
||||||
|
|
26
.github/workflows/release-create-pr.yml
vendored
26
.github/workflows/release-create-pr.yml
vendored
|
@ -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
|
|
||||||
|
|
4
.github/workflows/release-publish.yml
vendored
4
.github/workflows/release-publish.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -20,3 +20,4 @@ packages/**/.turbo
|
||||||
cypress/videos/*
|
cypress/videos/*
|
||||||
cypress/screenshots/*
|
cypress/screenshots/*
|
||||||
*.swp
|
*.swp
|
||||||
|
CHANGELOG-*.md
|
||||||
|
|
119
CHANGELOG.md
119
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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' }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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"';
|
||||||
|
|
|
@ -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 });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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,6 +144,10 @@ describe('Workflow Actions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update workflow settings', () => {
|
it('should update workflow settings', () => {
|
||||||
|
cy.visit(WorkflowPages.url);
|
||||||
|
WorkflowPages.getters.workflowCards().then((cards) => {
|
||||||
|
const totalWorkflows = cards.length;
|
||||||
|
|
||||||
WorkflowPage.actions.visit();
|
WorkflowPage.actions.visit();
|
||||||
// Open settings dialog
|
// Open settings dialog
|
||||||
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
WorkflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
@ -118,7 +156,8 @@ describe('Workflow Actions', () => {
|
||||||
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
|
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
|
||||||
WorkflowPage.getters.workflowMenuItemSettings().click();
|
WorkflowPage.getters.workflowMenuItemSettings().click();
|
||||||
// Change all settings
|
// Change all settings
|
||||||
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', 7);
|
// totalWorkflows + 1 (current workflow) + 1 (no workflow option)
|
||||||
|
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2);
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.workflowSettingsErrorWorkflowSelect()
|
.workflowSettingsErrorWorkflowSelect()
|
||||||
.find('li')
|
.find('li')
|
||||||
|
@ -168,6 +207,7 @@ describe('Workflow Actions', () => {
|
||||||
WorkflowPage.getters.workflowSettingsSaveButton().click();
|
WorkflowPage.getters.workflowSettingsSaveButton().click();
|
||||||
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
|
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
|
||||||
WorkflowPage.getters.successToast().should('exist');
|
WorkflowPage.getters.successToast().should('exist');
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be able to delete unsaved workflow', () => {
|
it('should not be able to delete unsaved workflow', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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'),
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }), {
|
||||||
|
|
||||||
cy.session(
|
|
||||||
[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() {
|
validate() {
|
||||||
cy.getCookie(N8N_AUTH_COOKIE).should('exist');
|
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,
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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.
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 && \
|
||||||
|
|
|
@ -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 && \
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
if [ "$#" -gt 0 ]; then
|
|
||||||
# Got started with arguments
|
|
||||||
node "$@"
|
|
||||||
else
|
|
||||||
# Got started without arguments
|
|
||||||
n8n
|
|
||||||
fi
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
```
|
|
|
@ -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
|
|
|
@ -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"
|
|
|
@ -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
|
|
||||||
```
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -210,9 +200,8 @@ A basic step by step example setup of n8n with docker-compose and Lets Encrypt i
|
||||||
## 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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
docker build --build-arg N8N_VERSION=$DOCKER_TAG -f $DOCKERFILE_PATH -t $IMAGE_NAME .
|
|
11
package.json
11
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
dist/ReloadNodesAndCredentials.*
|
|
|
@ -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?
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 types;
|
return extendsArr;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCredential(type: string): LoadedClass<ICredentialType> {
|
private getCredential(type: string): LoadedClass<ICredentialType> {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +344,6 @@ 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
|
|
||||||
if (loader instanceof PackageDirectoryLoader) {
|
|
||||||
const { packageName, known } = loader;
|
|
||||||
|
|
||||||
for (const type in known.nodes) {
|
for (const type in known.nodes) {
|
||||||
const { className, sourcePath } = known.nodes[type];
|
const { className, sourcePath } = known.nodes[type];
|
||||||
this.known.nodes[type] = {
|
this.known.nodes[type] = {
|
||||||
|
@ -352,14 +353,22 @@ export class LoadNodesAndCredentials implements INodesAndCredentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const type in known.credentials) {
|
for (const type in known.credentials) {
|
||||||
const { className, sourcePath, nodesToTestWith } = known.credentials[type];
|
const {
|
||||||
|
className,
|
||||||
|
sourcePath,
|
||||||
|
nodesToTestWith,
|
||||||
|
extends: extendsArr,
|
||||||
|
} = known.credentials[type];
|
||||||
this.known.credentials[type] = {
|
this.known.credentials[type] = {
|
||||||
className,
|
className,
|
||||||
sourcePath: path.join(directory, sourcePath),
|
sourcePath: path.join(directory, sourcePath),
|
||||||
nodesToTestWith: nodesToTestWith?.map((nodeName) => `${packageName}.${nodeName}`),
|
nodesToTestWith:
|
||||||
|
loader instanceof PackageDirectoryLoader
|
||||||
|
? nodesToTestWith?.map((nodeName) => `${loader.packageName}.${nodeName}`)
|
||||||
|
: undefined,
|
||||||
|
extends: extendsArr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[] };
|
|
@ -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
Loading…
Reference in a new issue