chore: merge master

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

View file

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

View file

@ -23,10 +23,10 @@ A clear and concise description of what you expected to happen.
**Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux 22.04]
- n8n Version [e.g. 0.200.1]
- Node.js Version [e.g. 16.17.0]
- n8n Version [e.g. 1.0.1]
- Node.js Version [e.g. 18.16.0]
- 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**
Add any other context about the problem here.

View file

@ -17,6 +17,18 @@ const filterAsync = async (asyncPredicate, arr) => {
return filterResults.filter(({shouldKeep}) => shouldKeep).map(({item}) => item);
}
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
const hasFunctionOrClass = async filePath => {
const fileContent = await readFileAsync(filePath, 'utf-8');
@ -24,7 +36,13 @@ const hasFunctionOrClass = async filePath => {
let hasFunctionOrClass = false;
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;
}
node.forEachChild(visit);

View file

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

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

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

1
.gitignore vendored
View file

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

View file

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

View file

@ -54,8 +54,8 @@ The most important directories:
## Development setup
If you want to change or extend n8n you have to make sure that all needed
dependencies are installed and the packages get linked correctly. Here a short guide on how that can be done:
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's a short guide on how that can be done:
### Requirements
@ -69,7 +69,7 @@ dependencies are installed and the packages get linked correctly. Here a short g
##### 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.
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!
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:
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
```
3. Go into repository folder
3. Go into repository folder:
```
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
@ -172,13 +172,13 @@ automatically build your code, restart the backend and refresh the frontend
pnpm dev
```
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 start
```
1. Create tests
1. Run all [tests](#test-suite)
1. Run all [tests](#test-suite):
```
pnpm test
```
@ -198,7 +198,7 @@ tests of all packages.
## Releasing
To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then
To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then:
1. Bump versions of packages that have changed or have dependencies that have changed
2. Update the Changelog
@ -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
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
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.
A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in.
Once a pull request is opened, an automated bot will promptly leave a comment requesting the agreement to be signed. The pull request can only be merged once the signature is obtained.

View file

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

View file

@ -1,9 +1,32 @@
export const BACKEND_BASE_URL = 'http://localhost:5678';
import { randFirstName, randLastName } from '@ngneat/falso';
export const BASE_URL = 'http://localhost:5678';
export const BACKEND_BASE_URL = 'http://localhost:5678';
export const N8N_AUTH_COOKIE = 'n8n-auth';
export const DEFAULT_USER_EMAIL = 'nathan@n8n.io';
export const DEFAULT_USER_PASSWORD = 'CypressTest123';
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_DISPLAY_NAME = 'When clicking "Execute Workflow"';

View file

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

View file

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

View file

@ -1,22 +1,8 @@
import { randFirstName, randLastName } from '@ngneat/falso';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { SettingsLogStreamingPage } from '../pages';
const email = DEFAULT_USER_EMAIL;
const password = DEFAULT_USER_PASSWORD;
const firstName = randFirstName();
const lastName = randLastName();
const settingsLogStreamingPage = new SettingsLogStreamingPage();
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', () => {
cy.visit('/settings/log-streaming');
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', () => {
cy.enableFeature('feat:logStreaming');
cy.enableFeature('logStreaming');
cy.visit('/settings/log-streaming');
settingsLogStreamingPage.getters.getActionBoxLicensed().should('be.visible');
settingsLogStreamingPage.getters.getAddFirstDestinationButton().should('be.visible');

View file

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

View file

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

View file

@ -11,10 +11,6 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
WorkflowPage.actions.visit();
});
@ -103,7 +99,7 @@ describe('Canvas Actions', () => {
WorkflowPage.getters
.canvasNodes()
.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', () => {

View file

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

View file

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

View file

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

View file

@ -9,10 +9,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('Data mapping', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
workflowPage.actions.visit();
@ -192,7 +188,11 @@ describe('Data mapping', () => {
ndv.getters
.inlineExpressionEditorInput()
.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.mapDataFromHeader(1, 'value');

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import {
CredentialsModal,
CredentialsPage,
@ -28,47 +28,12 @@ const workflowPage = new WorkflowPage();
const workflowSharingModal = new WorkflowSharingModal();
const ndv = new NDV();
const instanceOwner = {
email: `${DEFAULT_USER_EMAIL}one`,
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 });
});
describe('Sharing', { disableAutoLogin: true }, () => {
before(() => cy.enableFeature('sharing', true));
let workflowW2Url = '';
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);
credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -87,7 +52,7 @@ describe('Sharing', () => {
ndv.actions.close();
workflowPage.actions.openShareModal();
workflowSharingModal.actions.addUser(users[1].email);
workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[1].email);
workflowSharingModal.actions.save();
workflowPage.actions.saveWorkflowOnButtonClick();
@ -100,23 +65,23 @@ describe('Sharing', () => {
});
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);
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.connectionParameter('API Key').type('1234567890');
credentialsModal.getters.connectionParameter('Access Token').type('1234567890');
credentialsModal.actions.setName('Credential C2');
credentialsModal.actions.changeTab('Sharing');
credentialsModal.actions.addUser(instanceOwner.email);
credentialsModal.actions.addUser(users[0].email);
credentialsModal.actions.addUser(INSTANCE_OWNER.email);
credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email);
credentialsModal.actions.save();
credentialsModal.actions.close();
});
it('should open W1, add node using C2 as U3', () => {
cy.signin(users[1]);
cy.signin(INSTANCE_MEMBERS[1]);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
@ -136,7 +101,7 @@ describe('Sharing', () => {
});
it('should not have access to W2, as U3', () => {
cy.signin(users[1]);
cy.signin(INSTANCE_MEMBERS[1]);
cy.visit(workflowW2Url);
cy.waitForLoad();
@ -145,7 +110,7 @@ describe('Sharing', () => {
});
it('should have access to W1, W2, as U1', () => {
cy.signin(instanceOwner);
cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
@ -165,7 +130,7 @@ describe('Sharing', () => {
});
it('should automatically test C2 when opened by U2 sharee', () => {
cy.signin(users[0]);
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C2').click();

View file

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

View file

@ -1,6 +1,5 @@
import { MainSidebar } from './../pages/sidebar/main-sidebar';
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import { SettingsSidebar, SettingsUsersPage, WorkflowPage, WorkflowsPage } from '../pages';
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { SettingsUsersPage, WorkflowPage } from '../pages';
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
*/
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 = {
newFirstName: 'Something',
newLastName: 'Else',
@ -49,47 +26,38 @@ const usersSettingsPage = new SettingsUsersPage();
const workflowPage = new WorkflowPage();
const personalSettingsPage = new PersonalSettingsPage();
describe('User Management', () => {
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 B and User C to instance`, () => {
cy.inviteUsers({ instanceOwner, users });
});
describe('User Management', { disableAutoLogin: true }, () => {
before(() => cy.enableFeature('sharing'));
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', () => {
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', () => {
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
usersSettingsPage.getters.userListItems().should('have.length', 3);
// List item for current user should have the `Owner` badge
usersSettingsPage.getters
.userItem(instanceOwner.email)
.userItem(INSTANCE_OWNER.email)
.find('.n8n-badge:contains("Owner")')
.should('exist');
// Other users list items should contain action pop-up list
usersSettingsPage.getters.userActionsToggle(users[0].email).should('exist');
usersSettingsPage.getters.userActionsToggle(users[1].email).should('exist');
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist');
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist');
});
it('should delete user and their data', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true);
usersSettingsPage.actions.opedDeleteDialog(users[0].email);
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email);
usersSettingsPage.getters.deleteDataRadioButton().realClick();
usersSettingsPage.getters.deleteDataInput().type('delete all data');
usersSettingsPage.getters.deleteUserButton().realClick();
@ -97,8 +65,8 @@ describe('User Management', () => {
});
it('should delete user and transfer their data', () => {
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true);
usersSettingsPage.actions.opedDeleteDialog(users[1].email);
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email);
usersSettingsPage.getters.transferDataRadioButton().realClick();
usersSettingsPage.getters.userSelectDropDown().realClick();
usersSettingsPage.getters.userSelectOptions().first().realClick();
@ -107,7 +75,7 @@ describe('User Management', () => {
});
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(
updatedPersonalData.newFirstName,
updatedPersonalData.newLastName,
@ -119,14 +87,14 @@ describe('User Management', () => {
});
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) {
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`, () => {
personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password);
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
workflowPage.getters
.errorToast()
@ -135,21 +103,21 @@ describe('User Management', () => {
});
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(
instanceOwner.password,
INSTANCE_OWNER.password,
updatedPersonalData.newPassword,
);
workflowPage.getters.successToast().should('contain', 'Password updated');
personalSettingsPage.actions.loginWithNewData(
instanceOwner.email,
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
);
});
it(`shouldn't allow users to set invalid email`, () => {
personalSettingsPage.actions.loginAndVisit(
instanceOwner.email,
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
);
// try without @ part
@ -160,7 +128,7 @@ describe('User Management', () => {
it(`should change user email`, () => {
personalSettingsPage.actions.loginAndVisit(
instanceOwner.email,
INSTANCE_OWNER.email,
updatedPersonalData.newPassword,
);
personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail);

View file

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

View file

@ -6,24 +6,14 @@ import {
NEW_QUERY_AUTH_ACCOUNT_NAME,
} from './../constants';
import {
DEFAULT_USER_EMAIL,
DEFAULT_USER_PASSWORD,
GMAIL_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME,
NEW_TRELLO_ACCOUNT_NAME,
SCHEDULE_TRIGGER_NODE_NAME,
TRELLO_NODE_NAME,
} from '../constants';
import { randFirstName, randLastName } from '@ngneat/falso';
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 credentialsModal = new CredentialsModal();
const workflowPage = new WorkflowPage();
@ -32,10 +22,6 @@ const nodeDetailsView = new NDV();
const NEW_CREDENTIAL_NAME = 'Something else';
describe('Credentials', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
cy.visit(credentialsPage.url);
});

View file

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

View file

@ -13,9 +13,6 @@ const workflowPage = new WorkflowPage();
// so the /nodes and /credentials endpoints are intercepted and non-cached.
// 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', () => {
before(() => {
cy.skipSetup();
})
beforeEach(() => {
cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store';
@ -36,6 +33,7 @@ describe('Community Nodes', () => {
credentials.push(CustomCredential);
})
})
workflowPage.actions.visit();
});

View file

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

View file

@ -5,10 +5,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('NDV', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
@ -277,7 +273,11 @@ describe('NDV', () => {
.should('equal', 'hovering-item');
ndv.actions.close();
workflowPage.actions.openNode('Set5');
ndv.actions.switchInputBranch('True Branch');
ndv.actions.changeOutputRunSelector('1 of 2 (2 items)')
ndv.getters.outputTableRow(1)
.should('have.text', '8888')
.realHover();
@ -288,16 +288,21 @@ describe('NDV', () => {
.realHover();
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)
.should('have.text', '8888')
.realHover();
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
// ndv.getters.outputHoveringItem().should('not.exist');
});

View file

@ -15,10 +15,6 @@ function checkStickiesStyle( top: number, left: number, height: number, width: n
}
describe('Canvas Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
workflowPage.actions.visit();
@ -94,66 +90,66 @@ describe('Canvas Actions', () => {
moveSticky({ left: 600, top: 200 });
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]);
checkStickiesStyle(140, 466, 160, 194);
checkStickiesStyle(100, 466, 160, 194);
});
it('expands/shrinks sticky from the top edge', () => {
workflowPage.actions.addSticky();
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]);
checkStickiesStyle(440, 620, 80, 240);
checkStickiesStyle(380, 620, 80, 240);
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', () => {
workflowPage.actions.addSticky();
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]);
checkStickiesStyle(360, 620, 254, 240);
checkStickiesStyle(300, 620, 254, 240);
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', () => {
workflowPage.actions.addSticky();
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]);
checkStickiesStyle(160, 420, 254, 346);
checkStickiesStyle(100, 420, 254, 346);
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', () => {
addDefaultSticky();
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]);
checkStickiesStyle(364, 400, 136, 302);
checkStickiesStyle(304, 400, 136, 302);
});
it('expands/shrinks sticky from the top left edge, and reach min height/width', () => {
addDefaultSticky();
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]);
checkStickiesStyle(264, 346, 236, 294);
checkStickiesStyle(204, 346, 236, 294);
});
it('sets sticky behind node', () => {
@ -161,7 +157,7 @@ describe('Canvas Actions', () => {
addDefaultSticky();
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)
.should(($el) => {
@ -239,7 +235,7 @@ function addDefaultSticky() {
}
function stickyShouldBePositionedCorrectly(position: Position) {
const yOffset = -60;
const yOffset = -100;
const xOffset = -180;
workflowPage.getters.stickies()
.should(($el) => {

View file

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

View file

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

View file

@ -7,10 +7,6 @@ const WorkflowPage = new WorkflowPageClass();
const NDVModal = new NDV();
describe('Node Creator', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
WorkflowPage.actions.visit();
});
@ -271,7 +267,7 @@ describe('Node Creator', () => {
NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
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);
})
});

View file

@ -5,10 +5,6 @@ const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('NDV', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
workflowPage.actions.visit();
workflowPage.actions.renameWorkflow(uuid());
@ -68,15 +64,15 @@ describe('NDV', () => {
it('should show validation errors only after blur or re-opening of NDV', () => {
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');
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('application').find('input').eq(1).focus().blur();
cy.get('.has-issues').should('have.length', 2);
ndv.getters.parameterInput('base').find('input').eq(1).focus().blur();
cy.get('.has-issues').should('have.length', 0);
ndv.getters.backToCanvas().click();
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);
});

View file

@ -5,8 +5,8 @@ const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV();
describe('Code node', () => {
before(() => {
cy.skipSetup();
beforeEach(() => {
WorkflowPage.actions.visit();
});
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', () => {
WorkflowPage.actions.visit();
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');

View file

@ -5,6 +5,7 @@ import {
SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
const NEW_WORKFLOW_NAME = 'Something else';
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 WorkflowPage = new WorkflowPageClass();
const WorkflowPages = new WorkflowsPageClass();
describe('Workflow Actions', () => {
before(() => {
cy.skipSetup();
});
beforeEach(() => {
WorkflowPage.actions.visit();
});
@ -66,6 +64,42 @@ describe('Workflow Actions', () => {
.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', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -110,64 +144,70 @@ describe('Workflow Actions', () => {
});
it('should update workflow settings', () => {
WorkflowPage.actions.visit();
// Open settings dialog
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', 7);
WorkflowPage.getters
.workflowSettingsErrorWorkflowSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist');
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist');
cy.visit(WorkflowPages.url);
WorkflowPages.getters.workflowCards().then((cards) => {
const totalWorkflows = cards.length;
WorkflowPage.actions.visit();
// Open settings dialog
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.getters.workflowMenu().should('be.visible');
WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters.workflowMenuItemSettings().should('be.visible');
WorkflowPage.getters.workflowMenuItemSettings().click();
// Change all settings
// totalWorkflows + 1 (current workflow) + 1 (no workflow option)
WorkflowPage.getters.workflowSettingsErrorWorkflowSelect().find('li').should('have.length', totalWorkflows + 2);
WorkflowPage.getters
.workflowSettingsErrorWorkflowSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').should('exist');
WorkflowPage.getters.workflowSettingsTimezoneSelect().find('li').eq(1).click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveFiledExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveSuccessExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.should('have.length', 3);
WorkflowPage.getters
.workflowSettingsSaveExecutionProgressSelect()
.find('li')
.last()
.click({ force: true });
WorkflowPage.getters.workflowSettingsTimeoutWorkflowSwitch().click();
WorkflowPage.getters.workflowSettingsTimeoutForm().find('input').first().type('1');
// Save settings
WorkflowPage.getters.workflowSettingsSaveButton().click();
WorkflowPage.getters.workflowSettingsModal().should('not.exist');
WorkflowPage.getters.successToast().should('exist');
})
});
it('should not be able to delete unsaved workflow', () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -178,7 +178,7 @@ export class WorkflowPage extends BasePage {
},
saveWorkflowUsingKeyboardShortcut: () => {
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) => {
this.getters.canvasNodeByName(name).first().click();
@ -242,14 +242,15 @@ export class WorkflowPage extends BasePage {
executeWorkflow: () => {
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
.getConnectionActionsBetweenNodes(sourceNodeName, targetNodeName)
.find('.add')
.first()
.click({ force: true });
this.actions.addNodeToCanvas(newNodeName, false);
this.actions.addNodeToCanvas(newNodeName, false, false, action);
},
deleteNodeBetweenNodes: (
sourceNodeName: string,
@ -281,23 +282,5 @@ export class WorkflowPage extends BasePage {
.type(content)
.type('{esc}');
},
turnOnManualExecutionSaving: () => {
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
cy.get('.el-loading-mask').should('not.be.visible');
this.getters
.workflowSettingsSaveManualExecutionsSelect()
.find('li:contains("Yes")')
.click({ force: true });
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
this.getters.successToast().should('exist');
this.getters.workflowMenu().click();
this.getters.workflowMenuItemSettings().click();
this.getters.workflowSettingsSaveManualExecutionsSelect().should('contain', 'Yes');
this.getters.workflowSettingsSaveButton().click();
},
};
}

View file

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

View file

@ -1,32 +1,6 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import 'cypress-real-events';
import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages';
import { N8N_AUTH_COOKIE } from '../constants';
import { MessageBox } from '../pages/modals/message-box';
import { WorkflowPage } from '../pages';
import { BASE_URL, N8N_AUTH_COOKIE } from '../constants';
Cypress.Commands.add('getByTestId', (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
// and the requests would already have been made
if (waitForIntercepts) {
cy.wait(['@loadSettings', '@loadLogin']);
cy.wait(['@loadSettings']);
}
cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist');
cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist');
});
Cypress.Commands.add('signin', ({ email, password }) => {
const signinPage = new SigninPage();
const workflowsPage = new WorkflowsPage();
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);
Cypress.session.clearAllSavedSessions();
cy.session([email, password], () => cy.request('POST', '/rest/login', { email, password }), {
validate() {
cy.getCookie(N8N_AUTH_COOKIE).should('exist');
},
{
validate() {
cy.getCookie(N8N_AUTH_COOKIE).should('exist');
},
},
);
});
});
Cypress.Commands.add('signout', () => {
cy.visit('/signout');
cy.waitForLoad();
cy.url().should('include', '/signin');
cy.request('POST', '/rest/logout');
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) => {
cy.intercept(method, `http://localhost:5678/rest${url}`);
});
Cypress.Commands.add('inviteUsers', ({ instanceOwner, users }) => {
const settingsUsersPage = new SettingsUsersPage();
const setFeature = (feature: string, enabled: boolean) =>
cy.request('PATCH', `${BASE_URL}/rest/e2e/feature`, { feature: `feat:${feature}`, enabled });
cy.signin(instanceOwner);
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('enableFeature', (feature: string) => setFeature(feature, true));
Cypress.Commands.add('disableFeature', (feature): string => setFeature(feature, false));
Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => {
if (Cypress.isBrowser('chrome')) {
@ -256,7 +101,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => {
const originalLocation = Cypress.$(selector)[index].getBoundingClientRect();
element.trigger('mousedown');
element.trigger('mousedown', { force: true });
element.trigger('mousemove', {
which: 1,
pageX: options?.abs ? xDiff : originalLocation.right + xDiff,

View file

@ -1,28 +1,19 @@
// ***********************************************************
// 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 { BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import './commands';
before(() => {
cy.resetAll();
cy.request('POST', `${BASE_URL}/rest/e2e/reset`, {
owner: INSTANCE_OWNER,
members: INSTANCE_MEMBERS,
});
});
// Load custom nodes and credentials fixtures
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/login').as('loadLogin');
// Always intercept the request to test credentials and return a success
cy.intercept('POST', '/rest/credentials/test', {

View file

@ -8,25 +8,14 @@ interface SigninPayload {
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 {
namespace Cypress {
interface SuiteConfigOverrides {
disableAutoLogin: boolean;
}
interface Chainable {
config(key: keyof SuiteConfigOverrides): boolean;
getByTestId(
selector: string,
...args: (Partial<Loggable & Timeoutable & Withinable & Shadow> | undefined)[]
@ -35,13 +24,7 @@ declare global {
createFixtureWorkflow(fixtureKey: string, workflowName: string): void;
signin(payload: SigninPayload): 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>;
skipSetup(): void;
resetAll(): void;
enableFeature(feature: string): void;
disableFeature(feature: string): void;
waitForLoad(waitForIntercepts?: boolean): void;

View file

@ -11,12 +11,6 @@ N8N_PATH=/app1/
# DOMAIN_NAME and SUBDOMAIN combined decide where n8n will be reachable from
# 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
# If not set New York time will be used
GENERIC_TIMEZONE=Europe/Berlin

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ COPY --chown=node:node scripts ./scripts
COPY --chown=node:node packages ./packages
COPY --chown=node:node patches ./patches
RUN apk add --update libc6-compat jq
RUN apk add --update jq
RUN corepack enable && corepack prepare --activate
USER node
@ -28,7 +28,8 @@ RUN rm -rf patches .npmrc *.yaml node_modules/.cache packages/**/node_modules/.c
FROM n8nio/base:${NODE_VERSION}
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
COPY docker/images/n8n-custom/docker-entrypoint.sh /
COPY docker/images/n8n/docker-entrypoint.sh /
RUN \
mkdir .n8n && \

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
ARG NODE_VERSION=16
ARG NODE_VERSION=18
FROM n8nio/base:${NODE_VERSION}
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 && \
rm -rf /root/.npm
# Set a custom user to not have n8n run as root
USER root
WORKDIR /data
RUN apk --no-cache add su-exec
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY docker-entrypoint.sh /
RUN \
mkdir .n8n && \
chown node:node .n8n
USER node
ENTRYPOINT ["tini", "--", "/docker-entrypoint.sh"]

View file

@ -8,21 +8,27 @@ n8n is an extendable workflow automation tool. With a [fair-code](http://faircod
## Contents
- [Demo](#demo)
- [Available integrations](#available-integrations)
- [Documentation](#documentation)
- [Start n8n in Docker](#start-n8n-in-docker)
- [Start with tunnel](#start-with-tunnel)
- [Securing n8n](#securing-n8n)
- [Persist data](#persist-data)
- [Passing Sensitive Data via File](#passing-sensitive-data-via-file)
- [Updating a Running docker-compose Instance](#updating-a-running-docker-compose-instance)
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt)
- [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)
- [n8n - Workflow automation tool](#n8n---workflow-automation-tool)
- [Contents](#contents)
- [Demo](#demo)
- [Available integrations](#available-integrations)
- [Documentation](#documentation)
- [Start n8n in Docker](#start-n8n-in-docker)
- [Start with tunnel](#start-with-tunnel)
- [Persist data](#persist-data)
- [Start with other Database](#start-with-other-database)
- [Use with PostgresDB](#use-with-postgresdb)
- [Use with MySQL](#use-with-mysql)
- [Passing Sensitive Data via File](#passing-sensitive-data-via-file)
- [Example Setup with Lets Encrypt](#example-setup-with-lets-encrypt)
- [Updating a running docker-compose instance](#updating-a-running-docker-compose-instance)
- [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
@ -71,20 +77,6 @@ docker run -it --rm \
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
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
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
Docker- and Kubernetes-Secrets.
Docker and Kubernetes secrets.
The following environment variables support file input:
@ -181,8 +173,6 @@ The following environment variables support file input:
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE
## Example Setup with Lets Encrypt
@ -193,26 +183,25 @@ A basic step by step example setup of n8n with docker-compose and Lets Encrypt i
1. Pull the latest version from the registry
`docker pull docker.n8n.io/n8nio/n8n`
`docker pull docker.n8n.io/n8nio/n8n`
2. Stop the current setup
`sudo docker-compose stop`
`sudo docker-compose stop`
3. Delete it (will only delete the docker-containers, data is stored separately)
`sudo docker-compose rm`
`sudo docker-compose rm`
4. Then start it again
`sudo docker-compose up -d`
`sudo docker-compose up -d`
## Setting Timezone
To define the timezone n8n should use, the environment variable `GENERIC_TIMEZONE` can
be set. This gets used by for example the Cron-Node.
Apart from that can also the timezone of the system be set separately. Which controls what
some scripts and commands return like `$ date`. The system timezone can be set via
be set. One instance where this variable is implemented is in the Schedule node. Furthermore, the system's timezone can be set separately,
which controls the output of certain scripts and commands such as `$ date`. The system timezone can be set via
the environment variable `TZ`.
Example to use the same timezone for both:

View file

@ -1,17 +1,8 @@
#!/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
# Got started with arguments
exec su-exec node "$@"
n8n "$@"
else
# Got started without arguments
exec su-exec node n8n
n8n
fi

View file

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

View file

@ -1,10 +1,10 @@
{
"name": "n8n",
"version": "0.234.0",
"version": "1.0.1",
"private": true,
"homepage": "https://n8n.io",
"engines": {
"node": ">=16.9",
"node": ">=18.10",
"pnpm": ">=8.6"
},
"packageManager": "pnpm@8.6.1",
@ -30,7 +30,6 @@
"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: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'"
},
"dependencies": {
@ -52,7 +51,6 @@
"jest-mock": "^29.5.0",
"jest-mock-extended": "^3.0.4",
"nock": "^13.2.9",
"node-fetch": "^2.6.7",
"p-limit": "^3.1.0",
"prettier": "^2.8.3",
"rimraf": "^3.0.2",
@ -82,6 +80,7 @@
"http-cache-semantics": "4.1.1",
"jsonwebtoken": "9.0.0",
"prettier": "^2.8.3",
"tough-cookie": "^4.1.3",
"tslib": "^2.5.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3",
@ -93,8 +92,8 @@
"typedi@0.10.0": "patches/typedi@0.10.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",
"typeorm@0.3.12": "patches/typeorm@0.3.12.patch",
"element-plus@2.3.6": "patches/element-plus@2.3.6.patch"
"element-plus@2.3.6": "patches/element-plus@2.3.6.patch",
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch"
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,33 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 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
### What changed?

View file

@ -21,10 +21,10 @@ if (process.argv.length === 2) {
const nodeVersion = process.versions.node;
const nodeVersionMajor = require('semver').major(nodeVersion);
if (![16, 18].includes(nodeVersionMajor)) {
if (![18, 20].includes(nodeVersionMajor)) {
console.log(`
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ import type {
WorkflowExecuteMode,
} from 'n8n-workflow';
import { validate } from 'class-validator';
import { Container } from 'typedi';
import { Like } from 'typeorm';
import config from '@/config';
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 { User } from '@db/entities/User';
import type { UserUpdatePayload } from '@/requests';
import Container from 'typedi';
import { ExecutionRepository } from './databases/repositories';
import { ExecutionRepository } from '@db/repositories';
/**
* Returns the base URL n8n is reachable from

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -138,13 +138,13 @@ export type OperationID = 'getUsers' | 'getUser';
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 {
required?: string[];
not?: { required?: string[] };

View file

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

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