diff --git a/package-lock.json b/package-lock.json index 80c91cb3d0854..ec9e98d365ea0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9841,6 +9841,15 @@ "yorkie": "^2.0.0" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "braces": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", @@ -9868,11 +9877,39 @@ } } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "optional": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -9909,6 +9946,74 @@ "worker-rpc": "^0.1.0" } }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", + "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "optional": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "optional": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "optional": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -9927,6 +10032,25 @@ } } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "optional": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -9947,11 +10071,49 @@ "to-regex": "^3.0.2" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "optional": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "optional": true + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "optional": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", @@ -10045,6 +10207,18 @@ "requires": { "tslib": "^1.8.1" } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true } } }, @@ -19320,180 +19494,6 @@ } } }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.1", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz", - "integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==", - "optional": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "optional": true - }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "optional": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "optional": true, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "optional": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "optional": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "optional": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "optional": true - }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "optional": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "optional": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - } - } - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 1b3d0bc4651d2..10119618c82ce 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -19,4 +19,6 @@ module.exports = { isolatedModules: true } }, + globalTeardown: '/test/teardown.ts', + setupFiles: ['/test/setup.ts'], } diff --git a/packages/cli/package.json b/packages/cli/package.json index 877a4b3b97081..1d867c1d08cc6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,7 +29,10 @@ "start": "run-script-os", "start:default": "cd bin && ./n8n", "start:windows": "cd bin && n8n", - "test": "jest", + "test": "npm run test:sqlite", + "test:sqlite": "export DB_TYPE=sqlite && jest", + "test:postgres": "export DB_TYPE=postgresdb && jest", + "test:mysql": "export DB_TYPE=mysqldb && jest", "watch": "tsc --watch", "typeorm": "ts-node ../../node_modules/typeorm/cli.js" }, diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 302ff0ad81d67..db788c521b866 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -4,7 +4,16 @@ /* eslint-disable no-case-declarations */ /* eslint-disable @typescript-eslint/naming-convention */ import { UserSettings } from 'n8n-core'; -import { ConnectionOptions, createConnection, getRepository, LoggerOptions } from 'typeorm'; +import { + Connection, + ConnectionOptions, + createConnection, + EntityManager, + EntityTarget, + getRepository, + LoggerOptions, + Repository, +} from 'typeorm'; import { TlsOptions } from 'tls'; import * as path from 'path'; // eslint-disable-next-line import/no-cycle @@ -18,8 +27,6 @@ import { entities } from './databases/entities'; import { postgresMigrations } from './databases/postgresdb/migrations'; import { mysqlMigrations } from './databases/mysqldb/migrations'; import { sqliteMigrations } from './databases/sqlite/migrations'; -import { TEST_CONNECTION_OPTIONS } from '../test/integration/shared/constants'; -import { isTestRun } from '../test/integration/shared/utils'; export const collections: IDatabaseCollections = { Credentials: null, @@ -34,7 +41,19 @@ export const collections: IDatabaseCollections = { Settings: null, }; -export async function init(): Promise { +let connection: Connection; + +export async function transaction(fn: (entityManager: EntityManager) => Promise): Promise { + return connection.transaction(fn); +} + +export function linkRepository(entityClass: EntityTarget): Repository { + return getRepository(entityClass, connection.name); +} + +export async function init( + testConnectionOptions?: ConnectionOptions, +): Promise { const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; const n8nFolder = UserSettings.getUserN8nFolderPath(); @@ -42,74 +61,80 @@ export async function init(): Promise { const entityPrefix = config.get('database.tablePrefix'); - switch (dbType) { - case 'postgresdb': - const sslCa = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca')) as string; - const sslCert = (await GenericHelpers.getConfigValue( - 'database.postgresdb.ssl.cert', - )) as string; - const sslKey = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.key')) as string; - const sslRejectUnauthorized = (await GenericHelpers.getConfigValue( - 'database.postgresdb.ssl.rejectUnauthorized', - )) as boolean; - - let ssl: TlsOptions | undefined; - if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) { - ssl = { - ca: sslCa || undefined, - cert: sslCert || undefined, - key: sslKey || undefined, - rejectUnauthorized: sslRejectUnauthorized, + if (testConnectionOptions) { + connectionOptions = testConnectionOptions; + } else { + switch (dbType) { + case 'postgresdb': + const sslCa = (await GenericHelpers.getConfigValue('database.postgresdb.ssl.ca')) as string; + const sslCert = (await GenericHelpers.getConfigValue( + 'database.postgresdb.ssl.cert', + )) as string; + const sslKey = (await GenericHelpers.getConfigValue( + 'database.postgresdb.ssl.key', + )) as string; + const sslRejectUnauthorized = (await GenericHelpers.getConfigValue( + 'database.postgresdb.ssl.rejectUnauthorized', + )) as boolean; + + let ssl: TlsOptions | undefined; + if (sslCa !== '' || sslCert !== '' || sslKey !== '' || !sslRejectUnauthorized) { + ssl = { + ca: sslCa || undefined, + cert: sslCert || undefined, + key: sslKey || undefined, + rejectUnauthorized: sslRejectUnauthorized, + }; + } + + connectionOptions = { + type: 'postgres', + entityPrefix, + database: (await GenericHelpers.getConfigValue('database.postgresdb.database')) as string, + host: (await GenericHelpers.getConfigValue('database.postgresdb.host')) as string, + password: (await GenericHelpers.getConfigValue('database.postgresdb.password')) as string, + port: (await GenericHelpers.getConfigValue('database.postgresdb.port')) as number, + username: (await GenericHelpers.getConfigValue('database.postgresdb.user')) as string, + schema: config.get('database.postgresdb.schema'), + migrations: postgresMigrations, + migrationsRun: true, + migrationsTableName: `${entityPrefix}migrations`, + ssl, + }; + + break; + + case 'mariadb': + case 'mysqldb': + connectionOptions = { + type: dbType === 'mysqldb' ? 'mysql' : 'mariadb', + database: (await GenericHelpers.getConfigValue('database.mysqldb.database')) as string, + entityPrefix, + host: (await GenericHelpers.getConfigValue('database.mysqldb.host')) as string, + password: (await GenericHelpers.getConfigValue('database.mysqldb.password')) as string, + port: (await GenericHelpers.getConfigValue('database.mysqldb.port')) as number, + username: (await GenericHelpers.getConfigValue('database.mysqldb.user')) as string, + migrations: mysqlMigrations, + migrationsRun: true, + migrationsTableName: `${entityPrefix}migrations`, + timezone: 'Z', // set UTC as default + }; + break; + + case 'sqlite': + connectionOptions = { + type: 'sqlite', + database: path.join(n8nFolder, 'database.sqlite'), + entityPrefix, + migrations: sqliteMigrations, + migrationsRun: false, // migrations for sqlite will be ran manually for now; see below + migrationsTableName: `${entityPrefix}migrations`, }; - } - - connectionOptions = { - type: 'postgres', - entityPrefix, - database: (await GenericHelpers.getConfigValue('database.postgresdb.database')) as string, - host: (await GenericHelpers.getConfigValue('database.postgresdb.host')) as string, - password: (await GenericHelpers.getConfigValue('database.postgresdb.password')) as string, - port: (await GenericHelpers.getConfigValue('database.postgresdb.port')) as number, - username: (await GenericHelpers.getConfigValue('database.postgresdb.user')) as string, - schema: config.get('database.postgresdb.schema'), - migrations: postgresMigrations, - migrationsRun: true, - migrationsTableName: `${entityPrefix}migrations`, - ssl, - }; - - break; - - case 'mariadb': - case 'mysqldb': - connectionOptions = { - type: dbType === 'mysqldb' ? 'mysql' : 'mariadb', - database: (await GenericHelpers.getConfigValue('database.mysqldb.database')) as string, - entityPrefix, - host: (await GenericHelpers.getConfigValue('database.mysqldb.host')) as string, - password: (await GenericHelpers.getConfigValue('database.mysqldb.password')) as string, - port: (await GenericHelpers.getConfigValue('database.mysqldb.port')) as number, - username: (await GenericHelpers.getConfigValue('database.mysqldb.user')) as string, - migrations: mysqlMigrations, - migrationsRun: true, - migrationsTableName: `${entityPrefix}migrations`, - timezone: 'Z', // set UTC as default - }; - break; - - case 'sqlite': - connectionOptions = { - type: 'sqlite', - database: path.join(n8nFolder, 'database.sqlite'), - entityPrefix, - migrations: sqliteMigrations, - migrationsRun: false, // migrations for sqlite will be ran manually for now; see below - migrationsTableName: `${entityPrefix}migrations`, - }; - break; - - default: - throw new Error(`The database "${dbType}" is currently not supported!`); + break; + + default: + throw new Error(`The database "${dbType}" is currently not supported!`); + } } let loggingOption: LoggerOptions = (await GenericHelpers.getConfigValue( @@ -128,10 +153,6 @@ export async function init(): Promise { } } - if (isTestRun) { - connectionOptions = TEST_CONNECTION_OPTIONS; - } - Object.assign(connectionOptions, { entities: Object.values(entities), synchronize: false, @@ -141,9 +162,9 @@ export async function init(): Promise { )) as string, }); - let connection = await createConnection(connectionOptions); + connection = await createConnection(connectionOptions); - if (dbType === 'sqlite') { + if (!testConnectionOptions && dbType === 'sqlite') { // This specific migration changes database metadata. // A field is now nullable. We need to reconnect so that // n8n knows it has changed. Happens only on sqlite. @@ -169,17 +190,17 @@ export async function init(): Promise { } } - collections.Credentials = getRepository(entities.CredentialsEntity); - collections.Execution = getRepository(entities.ExecutionEntity); - collections.Workflow = getRepository(entities.WorkflowEntity); - collections.Webhook = getRepository(entities.WebhookEntity); - collections.Tag = getRepository(entities.TagEntity); - - collections.Role = getRepository(entities.Role); - collections.User = getRepository(entities.User); - collections.SharedCredentials = getRepository(entities.SharedCredentials); - collections.SharedWorkflow = getRepository(entities.SharedWorkflow); - collections.Settings = getRepository(entities.Settings); + collections.Credentials = linkRepository(entities.CredentialsEntity); + collections.Execution = linkRepository(entities.ExecutionEntity); + collections.Workflow = linkRepository(entities.WorkflowEntity); + collections.Webhook = linkRepository(entities.WebhookEntity); + collections.Tag = linkRepository(entities.TagEntity); + + collections.Role = linkRepository(entities.Role); + collections.User = linkRepository(entities.User); + collections.SharedCredentials = linkRepository(entities.SharedCredentials); + collections.SharedWorkflow = linkRepository(entities.SharedWorkflow); + collections.Settings = linkRepository(entities.Settings); return collections; } diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index 126a3084fe6e0..747ca0211de80 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -15,7 +15,6 @@ import { IExecutionResponse, IWorkflowDb, } from '.'; -import { isTestRun } from '../test/integration/shared/utils'; /** * Special Error which allows to return also an error code and http status code @@ -103,7 +102,9 @@ export function sendErrorResponse(res: Response, error: ResponseError, shouldLog httpStatusCode = error.httpStatusCode; } - if (process.env.NODE_ENV !== 'production' && shouldLog && !isTestRun) { + shouldLog = !process.argv[1].split('/').includes('jest'); + + if (process.env.NODE_ENV !== 'production' && shouldLog) { console.error('ERROR RESPONSE'); console.error(error); } diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 8cabd8d96ce84..203e4b8ea9c9a 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -42,8 +42,6 @@ export async function getInstanceOwner(): Promise { return owner; } -export const isEmailSetUp = Boolean(config.get('userManagement.emails.mode')); - /** * Return the n8n instance base URL without trailing slash. */ diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index b8c23ff45994d..cbb10bf658cf5 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -1,7 +1,8 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable import/no-cycle */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Response } from 'express'; -import { getConnection, In } from 'typeorm'; +import { In } from 'typeorm'; import { genSaltSync, hashSync } from 'bcryptjs'; import validator from 'validator'; import { LoggerProxy as Logger } from 'n8n-workflow'; @@ -9,12 +10,7 @@ import { LoggerProxy as Logger } from 'n8n-workflow'; import { Db, ResponseHelper } from '../..'; import { N8nApp, PublicUser } from '../Interfaces'; import { UserRequest } from '../../requests'; -import { - getInstanceBaseUrl, - isEmailSetUp, - sanitizeUser, - validatePassword, -} from '../UserManagementHelper'; +import { getInstanceBaseUrl, sanitizeUser, validatePassword } from '../UserManagementHelper'; import { User } from '../../databases/entities/User'; import { SharedWorkflow } from '../../databases/entities/SharedWorkflow'; import { SharedCredentials } from '../../databases/entities/SharedCredentials'; @@ -117,7 +113,7 @@ export function usersNamespace(this: N8nApp): void { Logger.debug(total > 1 ? `Creating ${total} user shells...` : `Creating 1 user shell...`); try { - await getConnection().transaction(async (transactionManager) => { + await Db.transaction(async (transactionManager) => { return Promise.all( usersToSetUp.map(async (email) => { const newUser = Object.assign(new User(), { @@ -204,6 +200,16 @@ export function usersNamespace(this: N8nApp): void { throw new ResponseHelper.ResponseError('Invalid payload', undefined, 400); } + // Postgres validates UUID format + for (const userId of [inviterId, inviteeId]) { + if (!validator.isUUID(userId)) { + Logger.debug('Request to resolve signup token failed because of invalid user ID', { + userId, + }); + throw new ResponseHelper.ResponseError('Invalid userId', undefined, 400); + } + } + const users = await Db.collections.User!.find({ where: { id: In([inviterId, inviteeId]) } }); if (users.length !== 2) { @@ -357,7 +363,7 @@ export function usersNamespace(this: N8nApp): void { if (transferId) { const transferee = users.find((user) => user.id === transferId); - await getConnection().transaction(async (transactionManager) => { + await Db.transaction(async (transactionManager) => { await transactionManager.update( SharedWorkflow, { user: userToDelete }, @@ -385,7 +391,7 @@ export function usersNamespace(this: N8nApp): void { }), ]); - await getConnection().transaction(async (transactionManager) => { + await Db.transaction(async (transactionManager) => { const ownedWorkflows = await Promise.all( ownedSharedWorkflows.map(async ({ workflow }) => { if (workflow.active) { @@ -414,6 +420,8 @@ export function usersNamespace(this: N8nApp): void { ResponseHelper.send(async (req: UserRequest.Reinvite) => { const { id: idToReinvite } = req.params; + const isEmailSetUp = config.get('userManagement.emails.mode') as '' | 'smtp'; + if (!isEmailSetUp) { Logger.error('Request to reinvite a user failed because email sending was not set up'); throw new ResponseHelper.ResponseError( diff --git a/packages/cli/src/api/credentials.api.ts b/packages/cli/src/api/credentials.api.ts index bab0465514578..bce869c6d9d03 100644 --- a/packages/cli/src/api/credentials.api.ts +++ b/packages/cli/src/api/credentials.api.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ import express = require('express'); -import { getConnection, In } from 'typeorm'; +import { In } from 'typeorm'; import { UserSettings, Credentials } from 'n8n-core'; import { INodeCredentialTestResult } from 'n8n-workflow'; @@ -15,8 +15,8 @@ import { ICredentialsDb, ICredentialsResponse, whereClause, + ResponseHelper, } from '..'; -import * as ResponseHelper from '../ResponseHelper'; import { RESPONSE_ERROR_MESSAGES } from '../constants'; import { CredentialsEntity } from '../databases/entities/CredentialsEntity'; @@ -162,7 +162,7 @@ credentialsController.post( scope: 'credential', }); - const { id, ...rest } = await getConnection().transaction(async (transactionManager) => { + const { id, ...rest } = await Db.transaction(async (transactionManager) => { const savedCredential = await transactionManager.save(newCredential); savedCredential.data = newCredential.data; diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 4c74961ad449c..b345b26534b13 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -20,6 +20,7 @@ import { Role } from './Role'; import { SharedWorkflow } from './SharedWorkflow'; import { SharedCredentials } from './SharedCredentials'; import { NoXss } from '../utils/customValidators'; +import { answersFormatter } from '../utils/transformers'; export const MIN_PASSWORD_LENGTH = 8; @@ -92,6 +93,7 @@ export class User { @Column({ type: resolveDataType('json') as ColumnOptions['type'], nullable: true, + transformer: answersFormatter, }) personalizationAnswers: IPersonalizationSurveyAnswers | null; diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index f2451e34e7c83..ca4e4f2f61e51 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -10,193 +10,128 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { const tablePrefix = config.get('database.tablePrefix'); await queryRunner.query( - 'CREATE TABLE `' + - tablePrefix + - 'role` ( ' + - '`id` int NOT NULL AUTO_INCREMENT, ' + - '`name` varchar(32) NOT NULL, ' + - '`scope` varchar(255) NOT NULL, ' + - '`createdAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`updatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - 'PRIMARY KEY (`id`), ' + - 'UNIQUE KEY `UQ_' + - tablePrefix + - '5b49d0f504f7ef31045a1fb2eb8` (`scope`,`name`) ' + - ');', + `CREATE TABLE ${tablePrefix}role ( + \`id\` int NOT NULL AUTO_INCREMENT, + \`name\` varchar(32) NOT NULL, + \`scope\` varchar(255) NOT NULL, + \`createdAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updatedAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (\`id\`), + UNIQUE KEY \`UQ_${tablePrefix}5b49d0f504f7ef31045a1fb2eb8\` (\`scope\`,\`name\`) + ) ENGINE=InnoDB;`, ); await queryRunner.query( - 'CREATE TABLE `' + - tablePrefix + - 'user` ( ' + - '`id` VARCHAR(36) NOT NULL, ' + - '`email` VARCHAR(255) NULL DEFAULT NULL, ' + - '`firstName` VARCHAR(32) NULL DEFAULT NULL, ' + - '`lastName` VARCHAR(32) NULL DEFAULT NULL, ' + - '`password` VARCHAR(255) NULL DEFAULT NULL, ' + - '`resetPasswordToken` VARCHAR(255) NULL DEFAULT NULL, ' + - '`resetPasswordTokenExpiration` INT NULL DEFAULT NULL, ' + - '`personalizationAnswers` TEXT NULL DEFAULT NULL, ' + - '`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`globalRoleId` INT NOT NULL, ' + - 'PRIMARY KEY (`id`), ' + - 'UNIQUE INDEX `IDX_' + - tablePrefix + - 'e12875dfb3b1d92d7d7c5377e2` (`email` ASC), ' + - 'INDEX `FK_' + - tablePrefix + - 'f0609be844f9200ff4365b1bb3d` (`globalRoleId` ASC), ' + - 'CONSTRAINT `FK_' + - tablePrefix + - 'f0609be844f9200ff4365b1bb3d` ' + - 'FOREIGN KEY (`globalRoleId`) ' + - 'REFERENCES `n8n`.`role` (`id`) ' + - 'ON DELETE NO ACTION ' + - 'ON UPDATE NO ACTION);', + `CREATE TABLE ${tablePrefix}user ( + \`id\` VARCHAR(36) NOT NULL, + \`email\` VARCHAR(255) NULL DEFAULT NULL, + \`firstName\` VARCHAR(32) NULL DEFAULT NULL, + \`lastName\` VARCHAR(32) NULL DEFAULT NULL, + \`password\` VARCHAR(255) NULL DEFAULT NULL, + \`resetPasswordToken\` VARCHAR(255) NULL DEFAULT NULL, + \`resetPasswordTokenExpiration\` INT NULL DEFAULT NULL, + \`personalizationAnswers\` TEXT NULL DEFAULT NULL, + \`createdAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updatedAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`globalRoleId\` INT NOT NULL, + PRIMARY KEY (\`id\`), + UNIQUE INDEX \`IDX_${tablePrefix}e12875dfb3b1d92d7d7c5377e2\` (\`email\` ASC), + INDEX \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` (\`globalRoleId\` ASC) + ) ENGINE=InnoDB;`, ); await queryRunner.query( - 'CREATE TABLE `' + - tablePrefix + - 'shared_workflow` ( ' + - '`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`roleId` INT NOT NULL, ' + - '`userId` VARCHAR(36) NOT NULL, ' + - '`workflowId` INT NOT NULL, ' + - 'INDEX `FK_' + - tablePrefix + - '3540da03964527aa24ae014b780x` (`roleId` ASC), ' + - 'INDEX `FK_' + - tablePrefix + - '82b2fd9ec4e3e24209af8160282x` (`userId` ASC), ' + - 'INDEX `FK_' + - tablePrefix + - 'b83f8d2530884b66a9c848c8b88x` (`workflowId` ASC), ' + - 'PRIMARY KEY (`userId`, `workflowId`), ' + - 'CONSTRAINT `FK_' + - tablePrefix + - '3540da03964527aa24ae014b780` ' + - 'FOREIGN KEY (`roleId`) ' + - 'REFERENCES `' + - tablePrefix + - 'role` (`id`) ' + - 'ON DELETE NO ACTION ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT `FK_' + - tablePrefix + - '82b2fd9ec4e3e24209af8160282` ' + - 'FOREIGN KEY (`userId`) ' + - 'REFERENCES `' + - tablePrefix + - 'user` (`id`) ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT `FK_' + - tablePrefix + - 'b83f8d2530884b66a9c848c8b88` ' + - 'FOREIGN KEY (`workflowId`) ' + - 'REFERENCES `' + - tablePrefix + - 'workflow_entity` (`id`) ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION);', + `ALTER TABLE \`${tablePrefix}user\` ADD CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) REFERENCES \`${tablePrefix}role\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - 'CREATE TABLE `' + - tablePrefix + - 'shared_credentials` ( ' + - '`createdAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`updatedAt` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '`roleId` INT NOT NULL, ' + - '`userId` VARCHAR(36) NOT NULL, ' + - '`credentialsId` INT NOT NULL, ' + - 'INDEX `FK_' + - tablePrefix + - 'c68e056637562000b68f480815a` (`roleId` ASC), ' + - 'INDEX `FK_' + - tablePrefix + - '484f0327e778648dd04f1d70493` (`userId` ASC), ' + - 'INDEX `FK_' + - tablePrefix + - '68661def1d4bcf2451ac8dbd949` (`credentialsId` ASC), ' + - 'PRIMARY KEY (`userId`, `credentialsId`), ' + - 'CONSTRAINT `FK_' + - tablePrefix + - 'c68e056637562000b68f480815a` ' + - 'FOREIGN KEY (`roleId`) ' + - 'REFERENCES `' + - tablePrefix + - 'role` (`id`) ' + - 'ON DELETE NO ACTION ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT `FK_' + - tablePrefix + - '484f0327e778648dd04f1d70493` ' + - 'FOREIGN KEY (`userId`) ' + - 'REFERENCES `' + - tablePrefix + - 'user` (`id`) ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT `FK_' + - tablePrefix + - '68661def1d4bcf2451ac8dbd949` ' + - 'FOREIGN KEY (`credentialsId`) ' + - 'REFERENCES `' + - tablePrefix + - 'credentials_entity` (`id`) ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION);', + `CREATE TABLE ${tablePrefix}shared_workflow ( + \`createdAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updatedAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`roleId\` INT NOT NULL, + \`userId\` VARCHAR(36) NOT NULL, + \`workflowId\` INT NOT NULL, + INDEX \`FK_${tablePrefix}3540da03964527aa24ae014b780x\` (\`roleId\` ASC), + INDEX \`FK_${tablePrefix}82b2fd9ec4e3e24209af8160282x\` (\`userId\` ASC), + INDEX \`FK_${tablePrefix}b83f8d2530884b66a9c848c8b88x\` (\`workflowId\` ASC), + PRIMARY KEY (\`userId\`, \`workflowId\`) + ) ENGINE=InnoDB;`, ); await queryRunner.query( - 'CREATE TABLE `' + - tablePrefix + - 'settings` ( ' + - '`key` VARCHAR(255) NOT NULL, ' + - '`value` TEXT NOT NULL, ' + - '`loadOnStartup` TINYINT(1) NOT NULL DEFAULT 0, ' + - 'PRIMARY KEY (`key`));', + `ALTER TABLE \`${tablePrefix}shared_workflow\` ADD CONSTRAINT \`FK_${tablePrefix}3540da03964527aa24ae014b780\` FOREIGN KEY (\`roleId\`) REFERENCES \`${tablePrefix}role\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - 'ALTER TABLE `' + - tablePrefix + - 'workflow_entity` DROP INDEX `IDX_' + - tablePrefix + - '943d8f922be094eb507cb9a7f9`', + `ALTER TABLE \`${tablePrefix}shared_workflow\` ADD CONSTRAINT \`FK_${tablePrefix}82b2fd9ec4e3e24209af8160282\` FOREIGN KEY (\`userId\`) REFERENCES \`${tablePrefix}user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - 'CREATE INDEX `IDX_' + - tablePrefix + - 'xeendlvptc5jy4hbol17b5xery` ON `' + - tablePrefix + - 'execution_entity` (`workflowId`)', + `ALTER TABLE \`${tablePrefix}shared_workflow\` ADD CONSTRAINT \`FK_${tablePrefix}b83f8d2530884b66a9c848c8b88\` FOREIGN KEY (\`workflowId\`) REFERENCES \`${tablePrefix}workflow_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}shared_credentials ( + \`createdAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updatedAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`roleId\` INT NOT NULL, + \`userId\` VARCHAR(36) NOT NULL, + \`credentialsId\` INT NOT NULL, + INDEX \`FK_${tablePrefix}c68e056637562000b68f480815a\` (\`roleId\` ASC), + INDEX \`FK_${tablePrefix}484f0327e778648dd04f1d70493\` (\`userId\` ASC), + INDEX \`FK_${tablePrefix}68661def1d4bcf2451ac8dbd949\` (\`credentialsId\` ASC), + PRIMARY KEY (\`userId\`, \`credentialsId\`) + ) ENGINE=InnoDB;`, + ); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}shared_credentials\` ADD CONSTRAINT \`FK_${tablePrefix}484f0327e778648dd04f1d70493\` FOREIGN KEY (\`userId\`) REFERENCES \`${tablePrefix}user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}shared_credentials\` ADD CONSTRAINT \`FK_${tablePrefix}68661def1d4bcf2451ac8dbd949\` FOREIGN KEY (\`credentialsId\`) REFERENCES \`${tablePrefix}credentials_entity\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + + await queryRunner.query( + `ALTER TABLE \`${tablePrefix}shared_credentials\` ADD CONSTRAINT \`FK_${tablePrefix}c68e056637562000b68f480815a\` FOREIGN KEY (\`roleId\`) REFERENCES \`${tablePrefix}role\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}settings ( + \`key\` VARCHAR(255) NOT NULL, + \`value\` TEXT NOT NULL, + \`loadOnStartup\` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (\`key\`) + ) ENGINE=InnoDB;`, + ); + + await queryRunner.query( + `ALTER TABLE ${tablePrefix}workflow_entity DROP INDEX IDX_${tablePrefix}943d8f922be094eb507cb9a7f9`, + ); + + await queryRunner.query( + `CREATE INDEX IDX_${tablePrefix}xeendlvptc5jy4hbol17b5xery ON ${tablePrefix}execution_entity (\`workflowId\`)`, ); // Insert initial roles await queryRunner.query( - 'INSERT INTO `' + tablePrefix + 'role` (name, scope) VALUES ("owner", "global");', + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ("owner", "global");`, ); const instanceOwnerRole = await queryRunner.query('SELECT LAST_INSERT_ID() as insertId'); await queryRunner.query( - 'INSERT INTO `' + tablePrefix + 'role` (name, scope) VALUES ("member", "global");', + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ("member", "global");`, ); await queryRunner.query( - 'INSERT INTO `' + tablePrefix + 'role` (name, scope) VALUES ("owner", "workflow");', + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ("owner", "workflow");`, ); const workflowOwnerRole = await queryRunner.query('SELECT LAST_INSERT_ID() as insertId'); await queryRunner.query( - 'INSERT INTO `' + tablePrefix + 'role` (name, scope) VALUES ("owner", "credential");', + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ("owner", "credential");`, ); const credentialOwnerRole = await queryRunner.query('SELECT LAST_INSERT_ID() as insertId'); @@ -204,47 +139,23 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { const survey = loadSurveyFromDisk(); const ownerUserId = uuid(); + await queryRunner.query( - 'INSERT INTO `' + - tablePrefix + - 'user` ' + - '(id, globalRoleId, personalizationAnswers) values ' + - '(?, ?, ?)', + `INSERT INTO ${tablePrefix}user (id, globalRoleId, personalizationAnswers) values (?, ?, ?)`, [ownerUserId, instanceOwnerRole[0].insertId, survey], ); await queryRunner.query( - 'INSERT INTO `' + - tablePrefix + - 'shared_workflow` (createdAt, updatedAt, roleId, userId, workflowId) ' + - ' select NOW(), NOW(), "' + - workflowOwnerRole[0].insertId + - '", "' + - ownerUserId + - '", id from `' + - tablePrefix + - 'workflow_entity`', + `INSERT INTO ${tablePrefix}shared_workflow (createdAt, updatedAt, roleId, userId, workflowId) select + NOW(), NOW(), '${workflowOwnerRole[0].insertId}', '${ownerUserId}', id FROM ${tablePrefix}workflow_entity`, ); await queryRunner.query( - 'INSERT INTO `' + - tablePrefix + - 'shared_credentials` (createdAt, updatedAt, roleId, userId, credentialsId) ' + - ' select NOW(), NOW(), "' + - credentialOwnerRole[0].insertId + - '", "' + - ownerUserId + - '", id from `' + - tablePrefix + - 'credentials_entity`', + `INSERT INTO ${tablePrefix}shared_credentials (createdAt, updatedAt, roleId, userId, credentialsId) SELECT NOW(), NOW(), '${credentialOwnerRole[0].insertId}', '${ownerUserId}', id FROM ${tablePrefix} credentials_entity`, ); await queryRunner.query( - 'INSERT INTO `' + - tablePrefix + - 'settings` (`key`, value, loadOnStartup) values ' + - '("userManagement.isInstanceOwnerSetUp", "false", 1), ' + - '("userManagement.skipInstanceOwnerSetup", "false", 1)', + `INSERT INTO ${tablePrefix}settings (\`key\`, value, loadOnStartup) VALUES ("userManagement.isInstanceOwnerSetUp", "false", 1), ("userManagement.skipInstanceOwnerSetup", "false", 1)`, ); } @@ -252,18 +163,11 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { const tablePrefix = config.get('database.tablePrefix'); await queryRunner.query( - 'ALTER TABLE `' + - tablePrefix + - 'workflow_entity` ADD UNIQUE INDEX `IDX_' + - tablePrefix + - '943d8f922be094eb507cb9a7f9` (`name`)', + `ALTER TABLE ${tablePrefix}workflow_entity ADD UNIQUE INDEX \`IDX_${tablePrefix}943d8f922be094eb507cb9a7f9\` (\`name\`)`, ); + await queryRunner.query( - 'DROP INDEX `IDX_' + - tablePrefix + - 'xeendlvptc5jy4hbol17b5xery` ON `' + - tablePrefix + - 'execution_entity`', + `DROP INDEX \`IDX_${tablePrefix}xeendlvptc5jy4hbol17b5xery\` ON ${tablePrefix}execution_entity`, ); await queryRunner.query(`DROP TABLE "${tablePrefix}shared_credentials"`); diff --git a/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts b/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts index 683fdf0dff775..eeb4793b1e64e 100644 --- a/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts +++ b/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts @@ -15,89 +15,49 @@ export class CreateUserManagement1636626154934 implements MigrationInterface { } await queryRunner.query( - 'CREATE TABLE "' + - tablePrefix + - 'role" (' + - '"id" serial NOT NULL,' + - '"name" VARCHAR(32) NOT NULL,' + - '"scope" VARCHAR(255) NOT NULL,' + - '"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' + - '"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' + - 'CONSTRAINT "PK_' + - tablePrefixPure + - 'e853ce24e8200abe5721d2c6ac552b73" PRIMARY KEY ("id"),' + - 'CONSTRAINT "UQ_' + - tablePrefixPure + - '5b49d0f504f7ef31045a1fb2eb8" UNIQUE ("scope", "name")' + - ');', - ); - - await queryRunner.query( - 'CREATE TABLE "' + - tablePrefix + - 'user" (' + - '"id" UUID NOT NULL DEFAULT gen_random_uuid(),' + - '"email" VARCHAR(255),' + - '"firstName" VARCHAR(32),' + - '"lastName" VARCHAR(32),' + - '"password" VARCHAR(255),' + - '"resetPasswordToken" VARCHAR(255),' + - '"resetPasswordTokenExpiration" int,' + - '"personalizationAnswers" text,' + - '"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' + - '"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,' + - '"globalRoleId" int NOT NULL,' + - 'CONSTRAINT "PK_' + - tablePrefixPure + - 'ea8f538c94b6e352418254ed6474a81f" PRIMARY KEY ("id"),' + - 'CONSTRAINT "UQ_' + - tablePrefixPure + - 'e12875dfb3b1d92d7d7c5377e2" UNIQUE (email),' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - 'f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES "role"(id)' + - ');', - ); - - await queryRunner.query( - 'CREATE TABLE "' + - tablePrefix + - 'shared_workflow" ( ' + - '"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '"roleId" INT NOT NULL, ' + - '"userId" UUID NOT NULL, ' + - '"workflowId" INT NOT NULL, ' + - 'CONSTRAINT "PK_' + - tablePrefixPure + - 'cc5d5a71c7b2591f5154ffb0c785e85e" PRIMARY KEY ("userId", "workflowId"), ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - '3540da03964527aa24ae014b780" ' + - 'FOREIGN KEY ("roleId") ' + - 'REFERENCES "' + - tablePrefix + - 'role" ("id") ' + - 'ON DELETE NO ACTION ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - '82b2fd9ec4e3e24209af8160282" ' + - 'FOREIGN KEY ("userId") ' + - 'REFERENCES "' + - tablePrefix + - 'user" ("id") ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - 'b83f8d2530884b66a9c848c8b88" ' + - 'FOREIGN KEY ("workflowId") ' + - 'REFERENCES "' + - tablePrefix + - 'workflow_entity" ("id") ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION);', + `CREATE TABLE ${tablePrefix}role ( + "id" serial NOT NULL, + "name" VARCHAR(32) NOT NULL, + "scope" VARCHAR(255) NOT NULL, + "createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "PK_${tablePrefixPure}e853ce24e8200abe5721d2c6ac552b73" PRIMARY KEY ("id"), + CONSTRAINT "UQ_${tablePrefixPure}5b49d0f504f7ef31045a1fb2eb8" UNIQUE ("scope", "name") + );`, + ); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}user ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "email" VARCHAR(255), + "firstName" VARCHAR(32), + "lastName" VARCHAR(32), + "password" VARCHAR(255), + "resetPasswordToken" VARCHAR(255), + "resetPasswordTokenExpiration" int, + "personalizationAnswers" text, + "createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "globalRoleId" int NOT NULL, + CONSTRAINT "PK_${tablePrefixPure}ea8f538c94b6e352418254ed6474a81f" PRIMARY KEY ("id"), + CONSTRAINT "UQ_${tablePrefixPure}e12875dfb3b1d92d7d7c5377e2" UNIQUE (email), + CONSTRAINT "FK_${tablePrefixPure}f0609be844f9200ff4365b1bb3d" FOREIGN KEY ("globalRoleId") REFERENCES ${tablePrefix}role (id) + );`, + ); + + await queryRunner.query( + `CREATE TABLE ${tablePrefix}shared_workflow ( + "createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "roleId" INT NOT NULL, + "userId" UUID NOT NULL, + "workflowId" INT NOT NULL, + CONSTRAINT "PK_${tablePrefixPure}cc5d5a71c7b2591f5154ffb0c785e85e" PRIMARY KEY ("userId", "workflowId"), + CONSTRAINT "FK_${tablePrefixPure}3540da03964527aa24ae014b780" FOREIGN KEY ("roleId") REFERENCES ${tablePrefix}role ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "FK_${tablePrefixPure}82b2fd9ec4e3e24209af8160282" FOREIGN KEY ("userId") REFERENCES ${tablePrefix}user ("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_${tablePrefixPure}b83f8d2530884b66a9c848c8b88" FOREIGN KEY ("workflowId") REFERENCES + ${tablePrefixPure}workflow_entity ("id") ON DELETE CASCADE ON UPDATE NO ACTION + );`, ); await queryRunner.query( @@ -105,87 +65,57 @@ export class CreateUserManagement1636626154934 implements MigrationInterface { ); await queryRunner.query( - 'CREATE TABLE "' + - tablePrefix + - 'shared_credentials" ( ' + - '"createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '"updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + - '"roleId" INT NOT NULL, ' + - '"userId" UUID NOT NULL, ' + - '"credentialsId" INT NOT NULL, ' + - 'CONSTRAINT "PK_' + - tablePrefixPure + - '10dd1527ffb639609be7aadd98f628c6" PRIMARY KEY ("userId", "credentialsId"), ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - 'c68e056637562000b68f480815a" ' + - 'FOREIGN KEY ("roleId") ' + - 'REFERENCES "' + - tablePrefix + - 'role" ("id") ' + - 'ON DELETE NO ACTION ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - '484f0327e778648dd04f1d70493" ' + - 'FOREIGN KEY ("userId") ' + - 'REFERENCES "' + - tablePrefix + - 'user" ("id") ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION, ' + - 'CONSTRAINT "FK_' + - tablePrefixPure + - '68661def1d4bcf2451ac8dbd949" ' + - 'FOREIGN KEY ("credentialsId") ' + - 'REFERENCES "' + - tablePrefix + - 'credentials_entity" ("id") ' + - 'ON DELETE CASCADE ' + - 'ON UPDATE NO ACTION);', + `CREATE TABLE ${tablePrefix}shared_credentials ( + "createdAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "roleId" INT NOT NULL, + "userId" UUID NOT NULL, + "credentialsId" INT NOT NULL, + CONSTRAINT "PK_${tablePrefixPure}10dd1527ffb639609be7aadd98f628c6" PRIMARY KEY ("userId", "credentialsId"), + CONSTRAINT "FK_${tablePrefixPure}c68e056637562000b68f480815a" FOREIGN KEY ("roleId") REFERENCES ${tablePrefix}role ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, + CONSTRAINT "FK_${tablePrefixPure}484f0327e778648dd04f1d70493" FOREIGN KEY ("userId") REFERENCES ${tablePrefix}user ("id") ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT "FK_${tablePrefixPure}68661def1d4bcf2451ac8dbd949" FOREIGN KEY ("credentialsId") REFERENCES ${tablePrefix}credentials_entity ("id") ON DELETE CASCADE ON UPDATE NO ACTION + );`, ); await queryRunner.query( - `CREATE INDEX "IDX_${tablePrefixPure}829d16efa0e265cb076d50eca8d21733" ON "${tablePrefix}shared_credentials" ("credentialsId");`, + `CREATE INDEX "IDX_${tablePrefixPure}829d16efa0e265cb076d50eca8d21733" ON ${tablePrefix}shared_credentials ("credentialsId");`, ); await queryRunner.query( - 'CREATE TABLE "' + - tablePrefix + - 'settings" ( ' + - '"key" VARCHAR(255) NOT NULL, ' + - '"value" TEXT NOT NULL, ' + - '"loadOnStartup" boolean NOT NULL DEFAULT false, ' + - 'CONSTRAINT "PK_' + - tablePrefixPure + - 'dc0fe14e6d9943f268e7b119f69ab8bd" PRIMARY KEY ("key"));', + `CREATE TABLE ${tablePrefix}settings ( + "key" VARCHAR(255) NOT NULL, + "value" TEXT NOT NULL, + "loadOnStartup" boolean NOT NULL DEFAULT false, + CONSTRAINT "PK_${tablePrefixPure}dc0fe14e6d9943f268e7b119f69ab8bd" PRIMARY KEY ("key") + );`, ); await queryRunner.query(`DROP INDEX "IDX_${tablePrefixPure}a252c527c4c89237221fe2c0ab"`); await queryRunner.query( - `CREATE INDEX "IDX_${tablePrefixPure}xeendlvptc5jy4hbol17b5xery" ON "${tablePrefix}execution_entity" ("workflowId");`, + `CREATE INDEX "IDX_${tablePrefixPure}xeendlvptc5jy4hbol17b5xery" ON ${tablePrefix}execution_entity ("workflowId");`, ); // Insert initial roles await queryRunner.query( - 'INSERT INTO "' + tablePrefix + "role\" (name, scope) VALUES ('owner', 'global');", + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ('owner', 'global');`, ); const instanceOwnerRole = await queryRunner.query('SELECT lastval() as "insertId"'); await queryRunner.query( - 'INSERT INTO "' + tablePrefix + "role\" (name, scope) VALUES ('member', 'global');", + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ('member', 'global');`, ); await queryRunner.query( - 'INSERT INTO "' + tablePrefix + "role\" (name, scope) VALUES ('owner', 'workflow');", + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ('owner', 'workflow');`, ); const workflowOwnerRole = await queryRunner.query('SELECT lastval() as "insertId"'); await queryRunner.query( - 'INSERT INTO "' + tablePrefix + "role\" (name, scope) VALUES ('owner', 'credential');", + `INSERT INTO ${tablePrefix}role (name, scope) VALUES ('owner', 'credential');`, ); const credentialOwnerRole = await queryRunner.query('SELECT lastval() as "insertId"'); @@ -193,45 +123,23 @@ export class CreateUserManagement1636626154934 implements MigrationInterface { const survey = loadSurveyFromDisk(); const ownerUserId = uuid(); + await queryRunner.query( - 'INSERT INTO "' + - tablePrefix + - 'user" ' + - '("id", "globalRoleId", "personalizationAnswers") values ($1, $2, $3)', + `INSERT INTO ${tablePrefix}user ("id", "globalRoleId", "personalizationAnswers") values ($1, $2, $3)`, [ownerUserId, instanceOwnerRole[0].insertId, survey], ); + await queryRunner.query( - 'INSERT INTO "' + - tablePrefix + - 'shared_workflow" ("createdAt", "updatedAt", "roleId", "userId", "workflowId") ' + - " select NOW(), NOW(), '" + - workflowOwnerRole[0].insertId + - "', '" + - ownerUserId + - '\', "id" from "' + - tablePrefix + - 'workflow_entity"', + `INSERT INTO ${tablePrefix}shared_workflow ("createdAt", "updatedAt", "roleId", "userId", "workflowId") select + NOW(), NOW(), '${workflowOwnerRole[0].insertId}', '${ownerUserId}', "id" FROM ${tablePrefix}workflow_entity`, ); await queryRunner.query( - 'INSERT INTO "' + - tablePrefix + - 'shared_credentials" ("createdAt", "updatedAt", "roleId", "userId", "credentialsId") ' + - " select NOW(), NOW(), '" + - credentialOwnerRole[0].insertId + - "', '" + - ownerUserId + - '\', "id" from "' + - tablePrefix + - 'credentials_entity"', + `INSERT INTO ${tablePrefix}shared_credentials ("createdAt", "updatedAt", "roleId", "userId", "credentialsId") SELECT NOW(), NOW(), '${credentialOwnerRole[0].insertId}', '${ownerUserId}', "id" FROM ${tablePrefix} credentials_entity`, ); await queryRunner.query( - 'INSERT INTO "' + - tablePrefix + - 'settings" ("key", "value", "loadOnStartup") values ' + - "('userManagement.isInstanceOwnerSetUp', 'false', true), " + - "('userManagement.skipInstanceOwnerSetup', 'false', true)", + `INSERT INTO ${tablePrefix}settings ("key", "value", "loadOnStartup") VALUES ('userManagement.isInstanceOwnerSetUp', 'false', true), ('userManagement.skipInstanceOwnerSetup', 'false', true)`, ); } @@ -244,14 +152,15 @@ export class CreateUserManagement1636626154934 implements MigrationInterface { } await queryRunner.query( - `CREATE UNIQUE INDEX "IDX_${tablePrefixPure}a252c527c4c89237221fe2c0ab" ON ${tablePrefix}workflow_entity ("name") `, + `CREATE UNIQUE INDEX "IDX_${tablePrefixPure}a252c527c4c89237221fe2c0ab" ON ${tablePrefix}workflow_entity ("name")`, ); + await queryRunner.query(`DROP INDEX IDX_${tablePrefixPure}xeendlvptc5jy4hbol17b5xery`); - await queryRunner.query(`DROP TABLE "${tablePrefix}shared_credentials"`); - await queryRunner.query(`DROP TABLE "${tablePrefix}shared_workflow"`); - await queryRunner.query(`DROP TABLE "${tablePrefix}user"`); - await queryRunner.query(`DROP TABLE "${tablePrefix}role"`); - await queryRunner.query(`DROP TABLE "${tablePrefix}settings"`); + await queryRunner.query(`DROP TABLE ${tablePrefix}shared_credentials`); + await queryRunner.query(`DROP TABLE ${tablePrefix}shared_workflow`); + await queryRunner.query(`DROP TABLE ${tablePrefix}user`); + await queryRunner.query(`DROP TABLE ${tablePrefix}role`); + await queryRunner.query(`DROP TABLE ${tablePrefix}settings`); } } diff --git a/packages/cli/src/databases/utils/transformers.ts b/packages/cli/src/databases/utils/transformers.ts index 88403b90512d6..f843c1c2f795e 100644 --- a/packages/cli/src/databases/utils/transformers.ts +++ b/packages/cli/src/databases/utils/transformers.ts @@ -1,4 +1,20 @@ +// eslint-disable-next-line import/no-cycle +import { IPersonalizationSurveyAnswers } from '../../Interfaces'; + export const idStringifier = { from: (value: number): string | number => (value ? value.toString() : value), to: (value: string): number | string => (value ? Number(value) : value), }; + +/** + * Ensure a consistent return type for personalization answers in `User`. + * Answers currently stored as `TEXT` on Postgres. + */ +export const answersFormatter = { + to: (answers: IPersonalizationSurveyAnswers): IPersonalizationSurveyAnswers => answers, + from: (answers: IPersonalizationSurveyAnswers | string): IPersonalizationSurveyAnswers => { + return typeof answers === 'string' + ? (JSON.parse(answers) as IPersonalizationSurveyAnswers) + : answers; + }, +}; diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 8b3a3f677ef10..a29504a1b648e 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -1,6 +1,5 @@ import { hashSync, genSaltSync } from 'bcryptjs'; import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; import { v4 as uuid } from 'uuid'; @@ -10,23 +9,27 @@ import { LOGGED_OUT_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; import { Role } from '../../src/databases/entities/Role'; import { randomEmail, randomValidPassword, randomName } from './shared/random'; -import { getGlobalOwnerRole } from './shared/utils'; +import { getGlobalOwnerRole } from './shared/testDb'; +import * as testDb from './shared/testDb'; let globalOwnerRole: Role; let app: express.Application; +let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true }); - await utils.initTestDb(); - await utils.truncate(['User']); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + + await testDb.truncate(['User'], testDbName); globalOwnerRole = await getGlobalOwnerRole(); - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { - await utils.createUser({ + await testDb.createUser({ id: uuid(), email: TEST_USER.email, firstName: TEST_USER.firstName, @@ -44,11 +47,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); test('POST /login should log user in', async () => { diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index 8586032f52786..3dfb6685ad201 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -1,6 +1,5 @@ import express = require('express'); import * as request from 'supertest'; -import { getConnection } from 'typeorm'; import { REST_PATH_SEGMENT, ROUTES_REQUIRING_AUTHORIZATION, @@ -8,17 +7,23 @@ import { } from './shared/constants'; import * as utils from './shared/utils'; +import * as testDb from './shared/testDb'; let app: express.Application; +let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ applyAuth: true, endpointGroups: ['me', 'auth', 'owner', 'users'] }); - await utils.initTestDb(); - utils.initLogger(); + app = utils.initTestServer({ + applyAuth: true, + endpointGroups: ['me', 'auth', 'owner', 'users'], + }); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + utils.initTestLogger(); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); ROUTES_REQUIRING_AUTHENTICATION.concat(ROUTES_REQUIRING_AUTHORIZATION).forEach((route) => { @@ -35,7 +40,7 @@ ROUTES_REQUIRING_AUTHORIZATION.forEach(async (route) => { const [method, endpoint] = getMethodAndEndpoint(route); test(`${route} should return 403 Forbidden for member`, async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const response = await authMemberAgent[method](endpoint); if (response.statusCode === 500) { diff --git a/packages/cli/test/integration/credentials.api.test.ts b/packages/cli/test/integration/credentials.api.test.ts index 55199ad44f67f..c1a3737f86ff4 100644 --- a/packages/cli/test/integration/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials.api.test.ts @@ -1,34 +1,43 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import { UserSettings } from 'n8n-core'; - import { Db } from '../../src'; import { randomName, randomString } from './shared/random'; import * as utils from './shared/utils'; -import type { SaveCredentialFunction } from './shared/types'; +import type { CredentialPayload, SaveCredentialFunction } from './shared/types'; +import { Role } from '../../src/databases/entities/Role'; +import { User } from '../../src/databases/entities/User'; +import * as testDb from './shared/testDb'; let app: express.Application; +let testDbName = ''; let saveCredential: SaveCredentialFunction; beforeAll(async () => { - app = utils.initTestServer({ endpointGroups: ['credentials'], applyAuth: true }); - await utils.initTestDb(); + app = utils.initTestServer({ + endpointGroups: ['credentials'], + applyAuth: true, + }); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + utils.initConfigFile(); - const credentialOwnerRole = await utils.getCredentialOwnerRole(); - saveCredential = utils.affixRoleToSaveCredential(credentialOwnerRole); + const credentialOwnerRole = await testDb.getCredentialOwnerRole(); + saveCredential = affixRoleToSaveCredential(credentialOwnerRole); }); beforeEach(async () => { - await utils.createOwnerShell(); + await testDb.createOwnerShell(); }); afterEach(async () => { - await utils.truncate(['User', 'Credentials', 'SharedCredentials']); + // do not combine calls - shared table must be cleared first and separately + await testDb.truncate(['SharedCredentials'], testDbName); + await testDb.truncate(['User', 'Credentials'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); test('POST /credentials should create cred', async () => { @@ -126,7 +135,7 @@ test('DELETE /credentials/:id should delete owned cred for owner', async () => { test('DELETE /credentials/:id should delete non-owned cred for owner', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const member = await utils.createUser(); + const member = await testDb.createUser(); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const response = await authOwnerAgent.delete(`/credentials/${savedCredential.id}`); @@ -144,7 +153,7 @@ test('DELETE /credentials/:id should delete non-owned cred for owner', async () }); test('DELETE /credentials/:id should delete owned cred for member', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -164,7 +173,7 @@ test('DELETE /credentials/:id should delete owned cred for member', async () => test('DELETE /credentials/:id should not delete non-owned cred for member', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); @@ -227,7 +236,7 @@ test('PATCH /credentials/:id should update owned cred for owner', async () => { test('PATCH /credentials/:id should update non-owned cred for owner', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const member = await utils.createUser(); + const member = await testDb.createUser(); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const patchPayload = credentialPayload(); @@ -260,7 +269,7 @@ test('PATCH /credentials/:id should update non-owned cred for owner', async () = }); test('PATCH /credentials/:id should update owned cred for member', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const patchPayload = credentialPayload(); @@ -295,7 +304,7 @@ test('PATCH /credentials/:id should update owned cred for member', async () => { test('PATCH /credentials/:id should not update non-owned cred for member', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const patchPayload = credentialPayload(); @@ -356,7 +365,7 @@ test('GET /credentials should retrieve all creds for owner', async () => { await saveCredential(credentialPayload(), { user: owner }); } - const member = await utils.createUser(); + const member = await testDb.createUser(); await saveCredential(credentialPayload(), { user: member }); @@ -376,7 +385,7 @@ test('GET /credentials should retrieve all creds for owner', async () => { }); test('GET /credentials should retrieve owned creds for member', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { @@ -400,7 +409,7 @@ test('GET /credentials should retrieve owned creds for member', async () => { test('GET /credentials should not retrieve non-owned creds for member', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { @@ -439,7 +448,7 @@ test('GET /credentials/:id should retrieve owned cred for owner', async () => { }); test('GET /credentials/:id should retrieve owned cred for member', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -466,7 +475,7 @@ test('GET /credentials/:id should retrieve owned cred for member', async () => { test('GET /credentials/:id should not retrieve non-owned cred for member', async () => { const owner = await Db.collections.User!.findOneOrFail(); - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); @@ -534,3 +543,8 @@ const INVALID_PAYLOADS = [ [], undefined, ]; + +function affixRoleToSaveCredential(role: Role) { + return (credentialPayload: CredentialPayload, { user }: { user: User }) => + testDb.saveCredential(credentialPayload, { user, role }); +} diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index a927a9cd52391..0da438d5c6895 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -1,45 +1,39 @@ import { hashSync, genSaltSync } from 'bcryptjs'; import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; -import { v4 as uuid } from 'uuid'; import config = require('../../config'); import * as utils from './shared/utils'; import { SUCCESS_RESPONSE_BODY } from './shared/constants'; import { Db } from '../../src'; -import { User } from '../../src/databases/entities/User'; import { Role } from '../../src/databases/entities/Role'; -import { - randomValidPassword, - randomInvalidPassword, - randomEmail, - randomName, - randomString, -} from './shared/random'; -import { getGlobalOwnerRole } from './shared/utils'; +import { randomValidPassword, randomEmail, randomName, randomString } from './shared/random'; +import * as testDb from './shared/testDb'; let app: express.Application; +let testDbName = ''; let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true }); - await utils.initTestDb(); - globalOwnerRole = await getGlobalOwnerRole(); - utils.initLogger(); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + + globalOwnerRole = await testDb.getGlobalOwnerRole(); + utils.initTestLogger(); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); describe('Owner shell', () => { beforeEach(async () => { - await utils.createOwnerShell(); + await testDb.createOwnerShell(); }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner shell', async () => { @@ -169,21 +163,15 @@ describe('Owner shell', () => { expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - const storedOwnerShell = await Db.collections.User!.findOneOrFail(); - expect(storedOwnerShell.personalizationAnswers).toEqual(validPayload); + const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); + + expect(storedAnswers).toEqual(validPayload); } }); }); describe('Member', () => { beforeEach(async () => { - // await utils.createUser({ - // email: TEST_USER.email, - // firstName: TEST_USER.firstName, - // lastName: TEST_USER.lastName, - // password: hashSync(randomValidPassword(), genSaltSync(10)), - // }); - config.set('userManagement.isInstanceOwnerSetUp', true); await Db.collections.Settings!.update( @@ -193,11 +181,11 @@ describe('Member', () => { }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized member', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const response = await authMemberAgent.get('/me'); @@ -229,7 +217,7 @@ describe('Member', () => { }); test('PATCH /me should succeed with valid inputs', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { @@ -269,7 +257,7 @@ describe('Member', () => { }); test('PATCH /me should fail with invalid inputs', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const invalidPayload of INVALID_PATCH_ME_PAYLOADS) { @@ -285,7 +273,7 @@ describe('Member', () => { test('PATCH /me/password should succeed with valid inputs', async () => { const memberPassword = randomValidPassword(); - const member = await utils.createUser({ + const member = await testDb.createUser({ password: hashSync(memberPassword, genSaltSync(10)), }); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); @@ -305,7 +293,7 @@ describe('Member', () => { }); test('PATCH /me/password should fail with invalid inputs', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); for (const payload of INVALID_PASSWORD_PAYLOADS) { @@ -324,7 +312,7 @@ describe('Member', () => { }); test('POST /me/survey should succeed with valid inputs', async () => { - const member = await utils.createUser(); + const member = await testDb.createUser(); const authMemberAgent = utils.createAgent(app, { auth: true, user: member }); const validPayloads = [SURVEY, {}]; @@ -334,32 +322,24 @@ describe('Member', () => { expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - const storedMember = await Db.collections.User!.findOneOrFail(); - expect(storedMember.personalizationAnswers).toEqual(validPayload); + const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); + + expect(storedAnswers).toEqual(validPayload); } }); }); describe('Owner', () => { beforeEach(async () => { - // await Db.collections.User!.save({ - // id: uuid(), - // email: TEST_USER.email, - // firstName: TEST_USER.firstName, - // lastName: TEST_USER.lastName, - // password: hashSync(randomValidPassword(), genSaltSync(10)), - // globalRole: globalOwnerRole, - // }); - config.set('userManagement.isInstanceOwnerSetUp', true); }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner', async () => { - const owner = await utils.createUser({ globalRole: globalOwnerRole }); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); const response = await authOwnerAgent.get('/me'); @@ -391,7 +371,7 @@ describe('Owner', () => { }); test('PATCH /me should succeed with valid inputs', async () => { - const owner = await utils.createUser({ globalRole: globalOwnerRole }); + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); for (const validPayload of VALID_PATCH_ME_PAYLOADS) { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 65f765ae41fe5..f4313301174f9 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -1,8 +1,8 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; import * as utils from './shared/utils'; +import * as testDb from './shared/testDb'; import { Db } from '../../src'; import config = require('../../config'); import { @@ -13,24 +13,26 @@ import { } from './shared/random'; let app: express.Application; +let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true }); - await utils.initTestDb(); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { - await utils.createOwnerShell(); + await testDb.createOwnerShell(); }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); test('POST /owner should create owner and enable isInstanceOwnerSetUp', async () => { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 8eba1ad1343d9..8cb3ef0cc7289 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -1,5 +1,4 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import { v4 as uuid } from 'uuid'; import * as utils from './shared/utils'; @@ -13,20 +12,24 @@ import { randomValidPassword, } from './shared/random'; import { Role } from '../../src/databases/entities/Role'; +import * as testDb from './shared/testDb'; let app: express.Application; let globalOwnerRole: Role; +let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true }); - await utils.initTestDb(); - await utils.truncate(['User']); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; + + await testDb.truncate(['User'], testDbName); globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', scope: 'global', }); - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { @@ -37,7 +40,7 @@ beforeEach(async () => { config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.emails.mode', ''); - await utils.createUser({ + await testDb.createUser({ id: INITIAL_TEST_USER.id, email: INITIAL_TEST_USER.email, password: INITIAL_TEST_USER.password, @@ -48,11 +51,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User']); + await testDb.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); test('POST /forgot-password should send password reset email', async () => { diff --git a/packages/cli/test/integration/shared/augmentation.d.ts b/packages/cli/test/integration/shared/augmentation.d.ts index 4dfa538e9a582..43a74a2f34ef8 100644 --- a/packages/cli/test/integration/shared/augmentation.d.ts +++ b/packages/cli/test/integration/shared/augmentation.d.ts @@ -1,4 +1,5 @@ import superagent = require('superagent'); +import { ObjectLiteral } from 'typeorm'; /** * Make `SuperTest` string-indexable. @@ -8,3 +9,13 @@ declare module 'supertest' { extends superagent.SuperAgent, Record {} } + +/** + * Prevent `repository.delete({})` (non-criteria) from triggering the type error + * `Expression produces a union type that is too complex to represent.ts(2590)` + */ +declare module 'typeorm' { + interface Repository { + delete(criteria: {}): Promise; + } +} diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index f1105cd6cd102..99c624efb4159 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -1,7 +1,4 @@ -import { ConnectionOptions } from 'typeorm'; - import config = require('../../../config'); -import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; export const REST_PATH_SEGMENT = config.get('endpoints.rest') as Readonly; @@ -13,17 +10,6 @@ export const AUTHLESS_ENDPOINTS: Readonly = [ config.get('endpoints.webhookTest') as string, ]; -export const TEST_CONNECTION_OPTIONS: Readonly = { - type: 'sqlite', - database: ':memory:', - entityPrefix: '', - dropSchema: true, - migrations: sqliteMigrations, - migrationsTableName: 'migrations', - migrationsRun: false, - logging: false, -}; - export const SUCCESS_RESPONSE_BODY = { data: { success: true, @@ -59,3 +45,15 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly = [ 'POST /owner', 'POST /owner/skip-setup', ]; + +/** + * Name of the connection used for creating and dropping a Postgres DB + * for each suite test run. + */ +export const BOOTSTRAP_POSTGRES_CONNECTION_NAME: Readonly = 'n8n_bs_postgres'; + +/** + * Name of the connection (and database) used for creating and dropping a MySQL DB + * for each suite test run. + */ +export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly = 'n8n_bs_mysql'; diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts new file mode 100644 index 0000000000000..1d254fce2fad5 --- /dev/null +++ b/packages/cli/test/integration/shared/testDb.ts @@ -0,0 +1,389 @@ +import { createConnection, getConnection, ConnectionOptions } from 'typeorm'; +import { Credentials, UserSettings } from 'n8n-core'; + +import config = require('../../../config'); +import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants'; +import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src'; +import { randomEmail, randomValidPassword, randomName } from './random'; +import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; + +import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; +import { entities } from '../../../src/databases/entities'; +import { mysqlMigrations } from '../../../src/databases/mysqldb/migrations'; +import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; +import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; + +import type { Role } from '../../../src/databases/entities/Role'; +import type { User } from '../../../src/databases/entities/User'; +import type { CredentialPayload } from './types'; + +/** + * Initialize one test DB per suite run, with bootstrap connection if needed. + */ +export async function init() { + const dbType = config.get('database.type') as DatabaseType; + + if (dbType === 'sqlite') { + // no bootstrap connection required + const testDbName = `n8n_test_sqlite_${Date.now()}`; + await Db.init(getSqliteOptions({ name: testDbName })); + await getConnection(testDbName).runMigrations({ transaction: 'none' }); + + return { testDbName }; + } + + if (dbType === 'postgresdb') { + let bootstrapPostgres; + const bootstrapPostgresOptions = getBootstrapPostgresOptions(); + + try { + bootstrapPostgres = await createConnection(bootstrapPostgresOptions); + } catch (error) { + const { username, password, host, port, schema } = bootstrapPostgresOptions; + console.error( + `ERROR: Failed to connect to Postgres default DB 'postgres'.\nPlease review your Postgres connection options:\n\thost: "${host}"\n\tusername: "${username}"\n\tpassword: "${password}"\n\tport: "${port}"\n\tschema: "${schema}"\nFix by setting correct values via environment variables:\n\texport DB_POSTGRESDB_HOST=value\n\texport DB_POSTGRESDB_USER=value\n\texport DB_POSTGRESDB_PASSWORD=value\n\texport DB_POSTGRESDB_PORT=value\n\texport DB_POSTGRESDB_SCHEMA=value`, + ); + process.exit(1); + } + + const testDbName = `pg_${Date.now()}_n8n_test`; + await bootstrapPostgres.query(`CREATE DATABASE ${testDbName};`); + + await Db.init(getPostgresOptions({ name: testDbName })); + + return { testDbName }; + } + + if (dbType === 'mysqldb') { + const bootstrapMysql = await createConnection(getBootstrapMySqlOptions()); + + const testDbName = `mysql_${Date.now()}_n8n_test`; + await bootstrapMysql.query(`CREATE DATABASE ${testDbName};`); + + await Db.init(getMySqlOptions({ name: testDbName })); + + return { testDbName }; + } + + throw new Error(`Unrecognized DB type: ${dbType}`); +} + +/** + * Drop test DB, closing bootstrap connection if existing. + */ +export async function terminate(testDbName: string) { + const dbType = config.get('database.type') as DatabaseType; + + if (dbType === 'sqlite') { + await getConnection(testDbName).close(); + } + + if (dbType === 'postgresdb') { + await getConnection(testDbName).close(); + + const bootstrapPostgres = getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME); + await bootstrapPostgres.query(`DROP DATABASE ${testDbName}`); + await bootstrapPostgres.close(); + } + + if (dbType === 'mysqldb') { + await getConnection(testDbName).close(); + + const bootstrapMySql = getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME); + await bootstrapMySql.query(`DROP DATABASE ${testDbName}`); + await bootstrapMySql.close(); + } +} + +/** + * Truncate DB tables for specified entities. + * + * @param entities Array of entity names whose tables to truncate. + * @param testDbName Name of the test DB to truncate tables in. + */ +export async function truncate(entities: Array, testDbName: string) { + const dbType = config.get('database.type'); + + if (dbType === 'sqlite') { + const testDb = getConnection(testDbName); + await testDb.query('PRAGMA foreign_keys=OFF'); + await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); + return testDb.query('PRAGMA foreign_keys=ON'); + } + + const map: { [K in keyof IDatabaseCollections]: string } = { + Credentials: 'credentials_entity', + Workflow: 'workflow_entity', + Execution: 'execution_entity', + Tag: 'tag_entity', + Webhook: 'webhook_entity', + Role: 'role', + User: 'user', + SharedCredentials: 'shared_credentials', + SharedWorkflow: 'shared_workflow', + Settings: 'settings', + }; + + if (dbType === 'postgresdb') { + return Promise.all( + entities.map((entity) => + getConnection(testDbName).query( + `TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`, + ), + ), + ); + } + + // MySQL truncation requires globals, which cannot be safely manipulated by parallel tests + if (dbType === 'mysqldb') { + await Promise.all( + entities.map(async (entity) => { + await Db.collections[entity]!.delete({}); + await getConnection(testDbName).query(`ALTER TABLE ${map[entity]} AUTO_INCREMENT = 1;`); + }), + ); + } +} + +// ---------------------------------- +// credential creation +// ---------------------------------- + +/** + * Save a credential to the test DB, sharing it with a user. + */ +export async function saveCredential( + credentialPayload: CredentialPayload, + { user, role }: { user: User; role: Role }, +) { + const newCredential = new CredentialsEntity(); + + Object.assign(newCredential, credentialPayload); + + const encryptedData = await encryptCredentialData(newCredential); + + Object.assign(newCredential, encryptedData); + + const savedCredential = await Db.collections.Credentials!.save(newCredential); + + savedCredential.data = newCredential.data; + + await Db.collections.SharedCredentials!.save({ + user, + credentials: savedCredential, + role, + }); + + return savedCredential; +} + +// ---------------------------------- +// user creation +// ---------------------------------- + +/** + * Store a user in the DB, defaulting to a `member`. + */ +export async function createUser(attributes: Partial = {}): Promise { + const { email, password, firstName, lastName, globalRole, ...rest } = attributes; + const user = { + email: email ?? randomEmail(), + password: password ?? randomValidPassword(), + firstName: firstName ?? randomName(), + lastName: lastName ?? randomName(), + globalRole: globalRole ?? (await getGlobalMemberRole()), + ...rest, + }; + + return Db.collections.User!.save(user); +} + +export async function createOwnerShell() { + const globalRole = await getGlobalOwnerRole(); + return Db.collections.User!.save({ globalRole }); +} + +export async function createMemberShell() { + const globalRole = await getGlobalMemberRole(); + return Db.collections.User!.save({ globalRole }); +} + +// ---------------------------------- +// role fetchers +// ---------------------------------- + +export async function getGlobalOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'global', + }); +} + +export async function getGlobalMemberRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'member', + scope: 'global', + }); +} + +export async function getWorkflowOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'workflow', + }); +} + +export async function getCredentialOwnerRole() { + return await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'credential', + }); +} + +export function getAllRoles() { + return Promise.all([ + getGlobalOwnerRole(), + getGlobalMemberRole(), + getWorkflowOwnerRole(), + getCredentialOwnerRole(), + ]); +} + +// ---------------------------------- +// connection options +// ---------------------------------- + +/** + * Generate options for an in-memory sqlite database connection, + * one per test suite run. + */ +export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions => { + return { + name, + type: 'sqlite', + database: ':memory:', + entityPrefix: '', + dropSchema: true, + migrations: sqliteMigrations, + migrationsTableName: 'migrations', + migrationsRun: false, + }; +}; + +/** + * Generate options for a bootstrap Postgres connection, + * to create and drop test Postgres databases. + */ +export const getBootstrapPostgresOptions = () => { + const username = config.get('database.postgresdb.user'); + const password = config.get('database.postgresdb.password'); + const host = config.get('database.postgresdb.host'); + const port = config.get('database.postgresdb.port'); + const schema = config.get('database.postgresdb.schema'); + + return { + name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, + type: 'postgres', + database: 'postgres', // pre-existing default database + host, + port, + username, + password, + schema, + } as const; +}; + +export const getPostgresOptions = ({ name }: { name: string }): ConnectionOptions => { + const username = config.get('database.postgresdb.user'); + const password = config.get('database.postgresdb.password'); + const host = config.get('database.postgresdb.host'); + const port = config.get('database.postgresdb.port'); + const schema = config.get('database.postgresdb.schema'); + + return { + name, + type: 'postgres', + database: name, + host, + port, + username, + password, + entityPrefix: '', + schema, + dropSchema: true, + migrations: postgresMigrations, + migrationsRun: true, + migrationsTableName: 'migrations', + entities: Object.values(entities), + synchronize: false, + logging: false, + }; +}; + +/** + * Generate options for a bootstrap MySQL connection, + * to create and drop test MySQL databases. + */ +export const getBootstrapMySqlOptions = (): ConnectionOptions => { + const username = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + const host = config.get('database.mysqldb.host'); + const port = config.get('database.mysqldb.port'); + + return { + name: BOOTSTRAP_MYSQL_CONNECTION_NAME, + database: BOOTSTRAP_MYSQL_CONNECTION_NAME, + type: 'mysql', + host, + port, + username, + password, + }; +}; + +/** + * Generate options for a MySQL database connection, + * one per test suite run. + */ +export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions => { + const username = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + const host = config.get('database.mysqldb.host'); + const port = config.get('database.mysqldb.port'); + + return { + name, + database: name, + type: 'mysql', + host, + port, + username, + password, + migrations: mysqlMigrations, + migrationsTableName: 'migrations', + migrationsRun: true, + }; +}; + +// ---------------------------------- +// encryption +// ---------------------------------- + +async function encryptCredentialData(credential: CredentialsEntity) { + const encryptionKey = await UserSettings.getEncryptionKey(); + + if (!encryptionKey) { + throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY); + } + + const coreCredential = new Credentials( + { id: null, name: credential.name }, + credential.type, + credential.nodesAccess, + ); + + // @ts-ignore + coreCredential.setData(credential.data, encryptionKey); + + return coreCredential.getDataToSave() as ICredentialsDb; +} diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 8c6356e147099..7cbceb19861b8 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -8,48 +8,31 @@ import bodyParser = require('body-parser'); import * as util from 'util'; import { createTestAccount } from 'nodemailer'; import { LoggerProxy } from 'n8n-workflow'; -import { Credentials, UserSettings } from 'n8n-core'; -import { getConnection } from 'typeorm'; +import { UserSettings } from 'n8n-core'; import config = require('../../../config'); -import { AUTH_COOKIE_NAME } from '../../../src/constants'; import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; +import { AUTH_COOKIE_NAME } from '../../../src/constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; -import { Db, ExternalHooks, ICredentialsDb, IDatabaseCollections } from '../../../src'; +import { Db, ExternalHooks } from '../../../src'; import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/me'; import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/routes/users'; import { authenticationMethods as authEndpoints } from '../../../src/UserManagement/routes/auth'; import { ownerNamespace as ownerEndpoints } from '../../../src/UserManagement/routes/owner'; import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/UserManagement/routes/passwordReset'; - import { issueJWT } from '../../../src/UserManagement/auth/jwt'; -import { credentialsController } from '../../../src/api/credentials.api'; -import { randomEmail, randomValidPassword, randomName } from './random'; import { getLogger } from '../../../src/Logger'; -import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity'; -import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; +import { credentialsController } from '../../../src/api/credentials.api'; -import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; +import type { EndpointGroup, SmtpTestAccount } from './types'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; -import type { CredentialPayload, SmtpTestAccount, EndpointGroup } from './types'; - -export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out - -// ---------------------------------- -// test server -// ---------------------------------- - -export const initLogger = () => { - config.set('logs.output', 'file'); // declutter console output during tests - LoggerProxy.init(getLogger()); -}; /** - * Initialize a test server to make requests to. + * Initialize a test server. * - * @param applyAuth Whether to apply auth middleware to the test server. - * @param endpointGroups Groups of endpoints to apply to the test server. + * @param applyAuth Whether to apply auth middleware to test server. + * @param endpointGroups Groups of endpoints to apply to test server. */ export function initTestServer({ applyAuth, @@ -105,6 +88,10 @@ export function initTestServer({ return testServer.app; } +/** + * Classify endpoint groups into `routerEndpoints` (newest, using `express.Router`), + * and `functionEndpoints` (legacy, namespaced inside a function). + */ const classifyEndpointGroups = (endpointGroups: string[]) => { const routerEndpoints: string[] = []; const functionEndpoints: string[] = []; @@ -117,19 +104,19 @@ const classifyEndpointGroups = (endpointGroups: string[]) => { }; // ---------------------------------- -// test logger +// initializers // ---------------------------------- /** * Initialize a silent logger for test runs. */ export function initTestLogger() { - config.set('logs.output', 'file'); + config.set('logs.output', 'file'); // declutter console output LoggerProxy.init(getLogger()); } /** - * Initialize a config file if non-existent. + * Initialize a user settings config file if non-existent. */ export function initConfigFile() { const settingsPath = UserSettings.getUserSettingsPath(); @@ -140,117 +127,6 @@ export function initConfigFile() { } } -// ---------------------------------- -// test DB -// ---------------------------------- - -export async function initTestDb() { - await Db.init(); - await getConnection().runMigrations({ transaction: 'none' }); -} - -export async function truncate(entities: Array) { - await getConnection().query('PRAGMA foreign_keys=OFF'); - await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); - await getConnection().query('PRAGMA foreign_keys=ON'); -} - -export function affixRoleToSaveCredential(role: Role) { - return (credentialPayload: CredentialPayload, { user }: { user: User }) => - saveCredential(credentialPayload, { user, role }); -} - -/** - * Save a credential to the DB, sharing it with a user. - */ -async function saveCredential( - credentialPayload: CredentialPayload, - { user, role }: { user: User; role: Role }, -) { - const newCredential = new CredentialsEntity(); - - Object.assign(newCredential, credentialPayload); - - const encryptedData = await encryptCredentialData(newCredential); - - Object.assign(newCredential, encryptedData); - - const savedCredential = await Db.collections.Credentials!.save(newCredential); - - savedCredential.data = newCredential.data; - - await Db.collections.SharedCredentials!.save({ - user, - credentials: savedCredential, - role, - }); - - return savedCredential; -} - -/** - * Store a user in the DB, defaulting to a `member`. - */ -export async function createUser(attributes: Partial = {}): Promise { - const { email, password, firstName, lastName, globalRole, ...rest } = attributes; - const user = { - email: email ?? randomEmail(), - password: password ?? randomValidPassword(), - firstName: firstName ?? randomName(), - lastName: lastName ?? randomName(), - globalRole: globalRole ?? (await getGlobalMemberRole()), - ...rest, - }; - - return Db.collections.User!.save(user); -} - -export async function createOwnerShell(): Promise { - const globalRole = await getGlobalOwnerRole(); - return Db.collections.User!.save({ globalRole }); -} -export async function createMemberShell(): Promise { - const globalRole = await getGlobalMemberRole(); - return Db.collections.User!.save({ globalRole }); -} - -export async function getGlobalOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'global', - }); -} - -export async function getGlobalMemberRole() { - return await Db.collections.Role!.findOneOrFail({ - name: 'member', - scope: 'global', - }); -} - -export async function getWorkflowOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'workflow', - }); -} - -export async function getCredentialOwnerRole() { - return await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'credential', - }); -} - -export function getAllRoles() { - return Promise.all([ - getGlobalOwnerRole(), - getGlobalMemberRole(), - getWorkflowOwnerRole(), - getCredentialOwnerRole(), - ]); -} - // ---------------------------------- // request agent // ---------------------------------- @@ -333,25 +209,3 @@ export async function isInstanceOwnerSetUp() { */ export const getSmtpTestAccount = util.promisify(createTestAccount); -// ---------------------------------- -// encryption -// ---------------------------------- - -async function encryptCredentialData(credential: CredentialsEntity) { - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (!encryptionKey) { - throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY); - } - - const coreCredential = new Credentials( - { id: null, name: credential.name }, - credential.type, - credential.nodesAccess, - ); - - // @ts-ignore - coreCredential.setData(credential.data, encryptionKey); - - return coreCredential.getDataToSave() as ICredentialsDb; -} diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index b850f12148497..ed94cc3f2f1c9 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -1,9 +1,8 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; import { v4 as uuid } from 'uuid'; +import { compare } from 'bcryptjs'; -import * as utils from './shared/utils'; import { Db } from '../../src'; import config = require('../../config'); import { SUCCESS_RESPONSE_BODY } from './shared/constants'; @@ -14,13 +13,13 @@ import { randomName, randomInvalidPassword, } from './shared/random'; -import { createMemberShell, createUser } from './shared/utils'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; -import * as UMHelper from '../../src/UserManagement/UserManagementHelper'; -import { compare } from 'bcryptjs'; +import * as utils from './shared/utils'; +import * as testDb from './shared/testDb'; let app: express.Application; +let testDbName = ''; let globalOwnerRole: Role; let globalMemberRole: Role; let workflowOwnerRole: Role; @@ -28,14 +27,15 @@ let credentialOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['users'], applyAuth: true }); - await utils.initTestDb(); + const initResult = await testDb.init(); + testDbName = initResult.testDbName; const [ fetchedGlobalOwnerRole, fetchedGlobalMemberRole, fetchedWorkflowOwnerRole, fetchedCredentialOwnerRole, - ] = await utils.getAllRoles(); + ] = await testDb.getAllRoles(); globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; @@ -46,13 +46,15 @@ beforeAll(async () => { }); beforeEach(async () => { - await utils.truncate(['User', 'Workflow', 'Credentials', 'SharedCredentials', 'SharedWorkflow']); + // do not combine calls - shared tables must be cleared first and separately + await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName); + await testDb.truncate(['User', 'Workflow', 'Credentials'], testDbName); jest.isolateModules(() => { jest.mock('../../config'); }); - await createUser({ + await testDb.createUser({ id: INITIAL_TEST_USER.id, email: INITIAL_TEST_USER.email, password: INITIAL_TEST_USER.password, @@ -63,19 +65,17 @@ beforeEach(async () => { config.set('userManagement.isInstanceOwnerSetUp', true); config.set('userManagement.emails.mode', ''); - // @ts-ignore hack because config doesn't change for helper - UMHelper.isEmailSetUp = false; }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await testDb.terminate(testDbName); }); test('GET /users should return all users', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - await createUser(); + await testDb.createUser(); const response = await authOwnerAgent.get('/users'); @@ -111,7 +111,7 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const userToDelete = await createUser(); + const userToDelete = await testDb.createUser(); const newWorkflow = new WorkflowEntity(); @@ -119,6 +119,7 @@ test('DELETE /users/:id should delete the user', async () => { name: randomName(), active: false, connections: {}, + nodes: [], }); const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow); @@ -169,7 +170,7 @@ test('DELETE /users/:id should delete the user', async () => { const workflow = await Db.collections.Workflow!.findOne(savedWorkflow.id); expect(workflow).toBeUndefined(); // deleted - // TODO: also include active workflow and check whether webhook has been removed + // TODO: Include active workflow and check whether webhook has been removed const credential = await Db.collections.Credentials!.findOne(savedCredential.id); expect(credential).toBeUndefined(); // deleted @@ -191,7 +192,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: idToDelete } = await createUser(); + const { id: idToDelete } = await testDb.createUser(); const response = await authOwnerAgent.delete(`/users/${idToDelete}`).query({ transferId: idToDelete, @@ -224,6 +225,7 @@ test('DELETE /users/:id with transferId should perform transfer', async () => { name: randomName(), active: false, connections: {}, + nodes: [], }); const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow); @@ -278,7 +280,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await createMemberShell(); + const { id: inviteeId } = await testDb.createMemberShell(); const response = await authOwnerAgent .get('/resolve-signup-token') @@ -300,7 +302,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const { id: inviteeId } = await createUser(); + const { id: inviteeId } = await testDb.createUser(); const first = await authOwnerAgent .get('/resolve-signup-token') @@ -308,11 +310,12 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const second = await authOwnerAgent.get('/resolve-signup-token').query({ inviteeId }); - const third = await authOwnerAgent - .get('/resolve-signup-token') - .query({ inviterId: '123', inviteeId: '456' }); + const third = await authOwnerAgent.get('/resolve-signup-token').query({ + inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d', + inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d', + }); - // user is already setup, thus call should error + // user is already set up, so call should error const fourth = await authOwnerAgent .get('/resolve-signup-token') .query({ inviterId: INITIAL_TEST_USER.id }) @@ -452,8 +455,6 @@ test('POST /users should email invites and create user shells', async () => { smtp: { host, port, secure }, } = await utils.getSmtpTestAccount(); - // @ts-ignore hack because config doesn't change for helper - UMHelper.isEmailSetUp = true; config.set('userManagement.emails.mode', 'smtp'); config.set('userManagement.emails.smtp.host', host); config.set('userManagement.emails.smtp.port', port); @@ -490,8 +491,6 @@ test('POST /users should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - // @ts-ignore hack because config doesn't change for helper - UMHelper.isEmailSetUp = true; config.set('userManagement.emails.mode', 'smtp'); const invalidPayloads = [ @@ -515,8 +514,6 @@ test('POST /users should ignore an empty payload', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - // @ts-ignore hack because config doesn't change for helper - UMHelper.isEmailSetUp = true; config.set('userManagement.emails.mode', 'smtp'); const response = await authOwnerAgent.post('/users').send([]); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts new file mode 100644 index 0000000000000..df32738c6750b --- /dev/null +++ b/packages/cli/test/setup.ts @@ -0,0 +1,33 @@ +import { exec as callbackExec } from 'child_process'; +import { promisify } from 'util'; + +import config = require('../config'); +import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants'; +import { DatabaseType } from '../src'; + +const exec = promisify(callbackExec); + +const dbType = config.get('database.type') as DatabaseType; + +if (dbType === 'mysqldb') { + const username = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + const host = config.get('database.mysqldb.host'); + + const passwordSegment = password ? `-p${password}` : ''; + + (async () => { + try { + await exec( + `echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} ${passwordSegment}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME};`, + ); + } catch (error) { + if (error.stderr.includes('Access denied')) { + console.error( + `ERROR: Failed to log into MySQL to create bootstrap DB.\nPlease review your MySQL connection options:\n\thost: "${host}"\n\tusername: "${username}"\n\tpassword: "${password}"\nFix by setting correct values via environment variables.\n\texport DB_MYSQLDB_HOST=value\n\texport DB_MYSQLDB_USERNAME=value\n\texport DB_MYSQLDB_PASSWORD=value`, + ); + process.exit(1); + } + } + })(); +} diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts new file mode 100644 index 0000000000000..95fb810e394ca --- /dev/null +++ b/packages/cli/test/teardown.ts @@ -0,0 +1,48 @@ +import { createConnection } from 'typeorm'; +import config = require('../config'); +import { exec } from 'child_process'; +import { DatabaseType } from '../src'; +import { getBootstrapMySqlOptions, getBootstrapPostgresOptions } from './integration/shared/testDb'; +import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants'; + +export default async () => { + const dbType = config.get('database.type') as DatabaseType; + + if (dbType === 'postgresdb') { + const bootstrapPostgres = await createConnection(getBootstrapPostgresOptions()); + + const results: { db_name: string }[] = await bootstrapPostgres.query( + 'SELECT datname as db_name FROM pg_database;', + ); + + const promises = results + .filter(({ db_name: dbName }) => dbName.startsWith('pg_') && dbName.endsWith('_n8n_test')) + .map(({ db_name: dbName }) => bootstrapPostgres.query(`DROP DATABASE ${dbName};`)); + + await Promise.all(promises); + + bootstrapPostgres.close(); + } + + if (dbType === 'mysqldb') { + const user = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + const host = config.get('database.mysqldb.host'); + + const bootstrapMySql = await createConnection(getBootstrapMySqlOptions()); + + const results: { Database: string }[] = await bootstrapMySql.query('SHOW DATABASES;'); + + const promises = results + .filter(({ Database: dbName }) => dbName.startsWith('mysql_') && dbName.endsWith('_n8n_test')) + .map(({ Database: dbName }) => bootstrapMySql.query(`DROP DATABASE ${dbName};`)); + + await Promise.all(promises); + + await bootstrapMySql.close(); + + exec( + `echo "DROP DATABASE ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${user} -p${password}`, + ); + } +};