From 14c7efedff8c6e0aa9b1dbc04dfd6f2993fd3d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 24 Feb 2022 18:28:28 +0100 Subject: [PATCH 01/59] :card_file_box: Fix Postgres migrations --- .../1636626154934-CreateUserManagement.ts | 250 ++++++------------ 1 file changed, 80 insertions(+), 170 deletions(-) diff --git a/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts b/packages/cli/src/databases/postgresdb/migrations/1636626154934-CreateUserManagement.ts index d8cc484325f5d..1930d32ab8950 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,44 +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 ?? null], ); + 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.hasOwner', 'false', true)", + `INSERT INTO ${tablePrefix}settings ("key", "value", "loadOnStartup") VALUES ('userManagement.hasOwner', 'false', true)`, ); } @@ -243,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`); } } From 46ed29b9a0291682f204484f49196ebec5578786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 24 Feb 2022 18:28:42 +0100 Subject: [PATCH 02/59] :zap: Add DB-specific scripts --- packages/cli/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cli/package.json b/packages/cli/package.json index b9fe83980c328..9c22c429c4fb7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -30,6 +30,8 @@ "start:default": "cd bin && ./n8n", "start:windows": "cd bin && n8n", "test": "jest", + "test:sqlite": "export DB_TYPE=sqlite && jest", + "test:postgres": "export DB_TYPE=postgresdb && jest", "watch": "tsc --watch", "typeorm": "ts-node ../../node_modules/typeorm/cli.js" }, From 26e7dd5e97b2fc35b567b57616a31faa4a5c6f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 24 Feb 2022 18:29:28 +0100 Subject: [PATCH 03/59] :sparkles: Set up test connections --- packages/cli/src/Db.ts | 163 +++++++++--------- .../cli/test/integration/shared/constants.ts | 33 +++- packages/cli/test/integration/shared/utils.ts | 61 ++++++- 3 files changed, 172 insertions(+), 85 deletions(-) diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 302ff0ad81d67..c31d4f426f483 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -4,7 +4,13 @@ /* 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, + getRepository, + LoggerOptions, +} from 'typeorm'; import { TlsOptions } from 'tls'; import * as path from 'path'; // eslint-disable-next-line import/no-cycle @@ -18,8 +24,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,82 +38,91 @@ export const collections: IDatabaseCollections = { Settings: null, }; -export async function init(): Promise { +export async function init( + testConnectionOptions?: ConnectionOptions, +): Promise { const dbType = (await GenericHelpers.getConfigValue('database.type')) as DatabaseType; const n8nFolder = UserSettings.getUserN8nFolderPath(); let connectionOptions: ConnectionOptions; + let connection: Connection; 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, }; - } - - 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; + + 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!`); + } } let loggingOption: LoggerOptions = (await GenericHelpers.getConfigValue( @@ -128,10 +141,6 @@ export async function init(): Promise { } } - if (isTestRun) { - connectionOptions = TEST_CONNECTION_OPTIONS; - } - Object.assign(connectionOptions, { entities: Object.values(entities), synchronize: false, @@ -141,9 +150,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. diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 413f01f8f72ee..e9d07ac20c4a1 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -1,6 +1,8 @@ import { ConnectionOptions } from 'typeorm'; import config = require('../../../config'); +import { entities } from '../../../src/databases/entities'; +import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; export const REST_PATH_SEGMENT = config.get('endpoints.rest') as Readonly; @@ -13,7 +15,7 @@ export const AUTHLESS_ENDPOINTS: Readonly = [ config.get('endpoints.webhookTest') as string, ]; -export const TEST_CONNECTION_OPTIONS: Readonly = { +export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { type: 'sqlite', database: ':memory:', entityPrefix: '', @@ -24,6 +26,35 @@ export const TEST_CONNECTION_OPTIONS: Readonly = { logging: false, }; + +export const POSTGRES_TEST_CONNECTION_OPTIONS: Readonly = { + type: 'postgres', + entityPrefix: '', + database: 'n8ntest', // TODO: Create automatically + host: 'localhost', + password: 'password', + port: 5432, + username: 'postgres', + schema: 'public', + + migrations: postgresMigrations, + migrationsRun: true, + migrationsTableName: 'migrations', + + entities: Object.values(entities), + synchronize: false, + logging: false, + + // dropSchema: true, +}; + +export const TEST_CONNECTION_OPTIONS: Record> = { + sqlite: SQLITE_TEST_CONNECTION_OPTIONS, + postgresdb: POSTGRES_TEST_CONNECTION_OPTIONS, + // @ts-ignore + mysql: {}, +}; + export const SUCCESS_RESPONSE_BODY = { data: { success: true, diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 14fe18669e5bd..99f0251b6ce7a 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -11,9 +11,10 @@ import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; import { Credentials, UserSettings } from 'n8n-core'; import { getConnection } from 'typeorm'; +import { snakeCase } from 'change-case'; import config = require('../../../config'); -import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; +import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT, TEST_CONNECTION_OPTIONS } from './constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; import { Db, ExternalHooks, ICredentialsDb, IDatabaseCollections } from '../../../src'; import { meNamespace as meEndpoints } from '../../../src/UserManagement/routes/me'; @@ -101,7 +102,7 @@ export function initTestServer({ export function initTestLogger() { config.set('logs.output', 'file'); LoggerProxy.init(getLogger()); -}; +} /** * Initialize a config file if non-existent. @@ -120,14 +121,53 @@ export function initConfigFile() { // ---------------------------------- export async function initTestDb() { - await Db.init(); - await getConnection().runMigrations({ transaction: 'none' }); + const dbType = config.get('database.type'); + + await Db.init(TEST_CONNECTION_OPTIONS[dbType]); + + if (dbType === 'sqlite') { + 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'); + const dbType = config.get('database.type'); + + if (dbType === 'sqlite') { + await getConnection().query('PRAGMA foreign_keys=OFF'); + await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); + return getConnection().query('PRAGMA foreign_keys=ON'); + } + + if (dbType === 'postgresdb') { + const tablePrefix = config.get('database.tablePrefix'); + const map = { + 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', + }; + + return Promise.all( + entities.map((entity) => + getConnection().query( + `TRUNCATE TABLE ${tablePrefix}"${map[entity]}" RESTART IDENTITY CASCADE;`, + ), + ), + ); + } + + if (dbType === 'mysqldb') { + // await getConnection().query('SET FOREIGN_KEY_CHECKS = 0;'); + // await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); + // return getConnection().query('SET FOREIGN_KEY_CHECKS = 1;'); + } } export function affixRoleToSaveCredential(role: Role) { @@ -350,3 +390,10 @@ async function encryptCredentialData(credential: CredentialsEntity) { return coreCredential.getDataToSave() as ICredentialsDb; } + +/** + * Remove quote-escaping backslashes added by Postgres. + */ +export const toObject = (json: string) => { + return typeof json === 'string' ? JSON.parse(json) : json; +}; From f7c1ec79f18a82d7534163470002bce20ee61167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 24 Feb 2022 18:30:08 +0100 Subject: [PATCH 04/59] :zap: Add Postgres UUID check --- packages/cli/src/UserManagement/routes/users.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index 88334a5fcbcae..e9a4222600011 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ /* eslint-disable import/no-cycle */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Response } from 'express'; @@ -200,6 +201,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) { From f81559901843e690e3d1de3bf0743d07b4d3348b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 24 Feb 2022 18:30:48 +0100 Subject: [PATCH 05/59] :test_tube: Make test adjustments for Postgres --- .../cli/test/integration/me.endpoints.test.ts | 19 +++++++++++++------ .../test/integration/users.endpoints.test.ts | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 94f496f64c9c9..dd34d230110d8 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -17,8 +17,9 @@ import { randomName, randomString, } from './shared/random'; -import { getGlobalOwnerRole } from './shared/utils'; +import { getGlobalOwnerRole, toObject } from './shared/utils'; +const dbType = config.get('database.type'); let globalOwnerRole: Role; describe('/me endpoints', () => { @@ -178,10 +179,13 @@ describe('/me endpoints', () => { for (const validPayload of validPayloads) { const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); expect(response.statusCode).toBe(200); - expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); + expect(toObject(response.body)).toEqual(SUCCESS_RESPONSE_BODY); - const storedOwnerShell = await Db.collections.User!.findOneOrFail(); - expect(storedOwnerShell.personalizationAnswers).toEqual(validPayload); + const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); + // @ts-ignore + const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; + + expect(storedAnswers).toEqual(validPayload); } }); }); @@ -365,8 +369,11 @@ describe('/me endpoints', () => { 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: answers } = await Db.collections.User!.findOneOrFail(); + // @ts-ignore + const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; + + expect(storedAnswers).toEqual(validPayload); } }); }); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index cd67c0883a1a7..6a0028f31872a 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -117,6 +117,7 @@ test('DELETE /users/:id should delete the user', async () => { name: randomName(), active: false, connections: {}, + nodes: [], }); const savedWorkflow = await Db.collections.Workflow!.save(newWorkflow); @@ -222,6 +223,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); @@ -308,7 +310,10 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const third = await authOwnerAgent .get('/resolve-signup-token') - .query({ inviterId: '123', inviteeId: '456' }); + .query({ + inviterId: '5531199e-b7ae-425b-a326-a95ef8cca59d', + inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d', + }); // user is already setup, thus call should error const fourth = await authOwnerAgent From e8b9f67ce03387c3f55e5ddd78098b1cb104e31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 25 Feb 2022 17:30:09 +0100 Subject: [PATCH 06/59] :zap: Refactor connection logic --- .../integration/shared/connectionOptions.ts | 74 +++++++++++++++++++ .../cli/test/integration/shared/constants.ts | 45 ----------- packages/cli/test/integration/shared/utils.ts | 43 ++++++++++- 3 files changed, 113 insertions(+), 49 deletions(-) create mode 100644 packages/cli/test/integration/shared/connectionOptions.ts diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts new file mode 100644 index 0000000000000..42becebe74f04 --- /dev/null +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -0,0 +1,74 @@ +import { ConnectionOptions } from 'typeorm'; + +import { entities } from '../../../src/databases/entities'; +import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; +import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; + +// ---------------------------------- +// sqlite +// ---------------------------------- + +export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { + type: 'sqlite', + database: ':memory:', + + entityPrefix: '', + dropSchema: true, + + migrations: sqliteMigrations, + migrationsTableName: 'migrations', + migrationsRun: false, + + logging: false, +}; + +// ---------------------------------- +// postgres +// ---------------------------------- + +/** + * Bootstrap connection used for creating and dropping test Postgres DBs. + */ +export const POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS: Readonly = { + name: 'bootstrap', + type: 'postgres', + database: 'postgres', + host: 'localhost', + port: 5432, + username: 'postgres', // TODO: Make configurable + password: 'password', // TODO: Make configurable + schema: 'public', +}; + +export const getPostgresConnectionOptions = ({ + databaseName, +}: { + databaseName: string; +}): ConnectionOptions => { + return { + type: 'postgres', + database: databaseName, + host: 'localhost', + port: 5432, + password: 'password', // TODO: Make configurable + username: 'postgres', // TODO: Make configurable + + entityPrefix: '', + schema: 'public', + dropSchema: true, + + migrations: postgresMigrations, + migrationsRun: true, + migrationsTableName: 'migrations', + + entities: Object.values(entities), + synchronize: false, + logging: false, + }; +}; + +// ---------------------------------- +// mysql +// ---------------------------------- + +// TODO: Pending mysql connection options diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 967f558db72f0..87cb58e05f71c 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -1,9 +1,4 @@ -import { ConnectionOptions } from 'typeorm'; - import config = require('../../../config'); -import { entities } from '../../../src/databases/entities'; -import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; -import { sqliteMigrations } from '../../../src/databases/sqlite/migrations'; export const REST_PATH_SEGMENT = config.get('endpoints.rest') as Readonly; @@ -15,46 +10,6 @@ export const AUTHLESS_ENDPOINTS: Readonly = [ config.get('endpoints.webhookTest') as string, ]; -export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { - type: 'sqlite', - database: ':memory:', - entityPrefix: '', - dropSchema: true, - migrations: sqliteMigrations, - migrationsTableName: 'migrations', - migrationsRun: false, - logging: false, -}; - - -export const POSTGRES_TEST_CONNECTION_OPTIONS: Readonly = { - type: 'postgres', - entityPrefix: '', - database: 'n8ntest', // TODO: Create automatically - host: 'localhost', - password: 'password', - port: 5432, - username: 'postgres', - schema: 'public', - - migrations: postgresMigrations, - migrationsRun: true, - migrationsTableName: 'migrations', - - entities: Object.values(entities), - synchronize: false, - logging: false, - - // dropSchema: true, -}; - -export const TEST_CONNECTION_OPTIONS: Record> = { - sqlite: SQLITE_TEST_CONNECTION_OPTIONS, - postgresdb: POSTGRES_TEST_CONNECTION_OPTIONS, - // @ts-ignore - mysql: {}, -}; - export const SUCCESS_RESPONSE_BODY = { data: { success: true, diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 51866ab62f004..1883f60e266a8 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -10,10 +10,10 @@ import { createTestAccount } from 'nodemailer'; import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; import { Credentials, UserSettings } from 'n8n-core'; -import { getConnection } from 'typeorm'; +import { Connection, createConnection, getConnection } from 'typeorm'; import config = require('../../../config'); -import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT, TEST_CONNECTION_OPTIONS } from './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'; @@ -31,6 +31,11 @@ import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; +import { + getPostgresConnectionOptions, + POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS, + SQLITE_TEST_CONNECTION_OPTIONS, +} from './connectionOptions'; export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out @@ -123,11 +128,41 @@ export function initConfigFile() { export async function initTestDb() { const dbType = config.get('database.type'); - await Db.init(TEST_CONNECTION_OPTIONS[dbType]); + if (dbType === 'postgresdb') { + const bootstrap = await createConnection(POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS); + await removeTestPostgresDatabases(bootstrap); + const newDatabaseName = await createTestPostgresDatabase(bootstrap); + bootstrap.close(); + + const options = getPostgresConnectionOptions({ databaseName: newDatabaseName }); + return Db.init(options); + } if (dbType === 'sqlite') { - await getConnection().runMigrations({ transaction: 'none' }); + await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); + return getConnection().runMigrations({ transaction: 'none' }); } + + throw new Error('MySQL test connection pending implementation'); // TODO +} + +export async function removeTestPostgresDatabases(bootstrap: Connection) { + const results: { db_name: string }[] = await bootstrap.query( + 'SELECT datname as db_name FROM pg_database;', + ); + + const promises = results + .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) + .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); + + Promise.all(promises); +} + +export async function createTestPostgresDatabase(bootstrap: Connection) { + const newDatabaseName = `n8n_test_pg_${Date.now()}`; + await bootstrap.query(`CREATE DATABASE ${newDatabaseName};`); + + return newDatabaseName; } export async function truncate(entities: Array) { From 47f6e65cf88ef1e29b4e839f7fdcd03211c6951f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 25 Feb 2022 17:50:09 +0100 Subject: [PATCH 07/59] :sparkles: Set up double init for Postgres --- packages/cli/src/Db.ts | 25 ++++--- .../test/integration/auth.endpoints.test.ts | 16 +++-- .../test/integration/auth.middleware.test.ts | 11 +-- .../integration/credentials.endpoints.test.ts | 14 ++-- .../cli/test/integration/me.endpoints.test.ts | 22 +++--- .../test/integration/owner.endpoints.test.ts | 13 ++-- .../passwordReset.endpoints.test.ts | 16 +++-- .../integration/shared/connectionOptions.ts | 32 ++++----- packages/cli/test/integration/shared/utils.ts | 69 ++++++++++++------- .../test/integration/users.endpoints.test.ts | 26 ++++--- 10 files changed, 148 insertions(+), 96 deletions(-) diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index c31d4f426f483..b45094e00abf7 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -178,17 +178,20 @@ export async function init( } } - 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 = getRepository(entities.CredentialsEntity, testConnectionOptions?.name); + collections.Execution = getRepository(entities.ExecutionEntity, testConnectionOptions?.name); + collections.Workflow = getRepository(entities.WorkflowEntity, testConnectionOptions?.name); + collections.Webhook = getRepository(entities.WebhookEntity, testConnectionOptions?.name); + collections.Tag = getRepository(entities.TagEntity, testConnectionOptions?.name); + + collections.Role = getRepository(entities.Role, testConnectionOptions?.name); + collections.User = getRepository(entities.User, testConnectionOptions?.name); + collections.SharedCredentials = getRepository( + entities.SharedCredentials, + testConnectionOptions?.name, + ); + collections.SharedWorkflow = getRepository(entities.SharedWorkflow, testConnectionOptions?.name); + collections.Settings = getRepository(entities.Settings, testConnectionOptions?.name); return collections; } diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index eba9a3ef51bc3..b4085ed5498cc 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'; @@ -15,11 +14,16 @@ import { getGlobalOwnerRole } from './shared/utils'; let globalOwnerRole: Role; let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); - await utils.initTestDb(); - await utils.truncate(['User']); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; + + await utils.truncate(['User'], testDbName); globalOwnerRole = await getGlobalOwnerRole(); utils.initLogger(); @@ -44,11 +48,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); 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 0875495cfbd18..154d112fe119a 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, @@ -10,15 +9,19 @@ import { import * as utils from './shared/utils'; let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ applyAuth: true, namespaces: ['me', 'auth', 'owner', 'users'] }); - await utils.initTestDb(); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; utils.initLogger(); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); ROUTES_REQUIRING_AUTHENTICATION.forEach((route) => { diff --git a/packages/cli/test/integration/credentials.endpoints.test.ts b/packages/cli/test/integration/credentials.endpoints.test.ts index 157e960123ce1..5b4b1555d0824 100644 --- a/packages/cli/test/integration/credentials.endpoints.test.ts +++ b/packages/cli/test/integration/credentials.endpoints.test.ts @@ -1,5 +1,4 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import { UserSettings } from 'n8n-core'; import { Db } from '../../src'; @@ -8,6 +7,8 @@ import * as utils from './shared/utils'; import type { SaveCredentialFunction } from './shared/types'; let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; let saveCredential: SaveCredentialFunction; beforeAll(async () => { @@ -16,7 +17,10 @@ beforeAll(async () => { applyAuth: true, externalHooks: true, }); - await utils.initTestDb(); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; + utils.initConfigFile(); const credentialOwnerRole = await utils.getCredentialOwnerRole(); @@ -28,11 +32,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User', 'Credentials', 'SharedCredentials']); + await utils.truncate(['User', 'Credentials', 'SharedCredentials'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); test('POST /credentials should create cred', async () => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index e7f1baddd69e2..2c8f1d09fb7e4 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.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'; @@ -21,17 +20,22 @@ import { getGlobalOwnerRole, toObject } from './shared/utils'; const dbType = config.get('database.type'); let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); - await utils.initTestDb(); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; + globalOwnerRole = await getGlobalOwnerRole(); utils.initLogger(); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); describe('Owner shell', () => { @@ -40,7 +44,7 @@ describe('Owner shell', () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner shell', async () => { @@ -179,7 +183,7 @@ describe('Owner shell', () => { expect(toObject(response.body)).toEqual(SUCCESS_RESPONSE_BODY); const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); - // @ts-ignore + // @ts-ignore TODO: JSON.parse() at model level const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; expect(storedAnswers).toEqual(validPayload); @@ -216,7 +220,7 @@ describe('Member', () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); test('GET /me should return sanitized member', async () => { @@ -355,7 +359,7 @@ describe('Member', () => { expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); - // @ts-ignore + // @ts-ignore TODO: JSON.parse() at model level const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; expect(storedAnswers).toEqual(validPayload); @@ -378,7 +382,7 @@ describe('Owner', () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner', async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 46cfb0a6c22fe..67edcf0d8457e 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -1,5 +1,4 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; import * as utils from './shared/utils'; @@ -13,10 +12,14 @@ import { } from './shared/random'; let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); - await utils.initTestDb(); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; utils.initLogger(); }); @@ -26,11 +29,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); test('POST /owner should create owner and enable hasOwner setting', async () => { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 7adbfd94a9d75..89a8a0f8d25df 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'; @@ -16,11 +15,16 @@ import { Role } from '../../src/databases/entities/Role'; let app: express.Application; let globalOwnerRole: Role; +let testDbName = ''; +let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); - await utils.initTestDb(); - await utils.truncate(['User']); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; + + await utils.truncate(['User'], testDbName); globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', @@ -48,11 +52,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User']); + await utils.truncate(['User'], testDbName); }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); test('POST /forgot-password should send password reset email', async () => { diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 42becebe74f04..7e381796c366d 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -26,28 +26,28 @@ export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { // postgres // ---------------------------------- -/** - * Bootstrap connection used for creating and dropping test Postgres DBs. - */ -export const POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS: Readonly = { - name: 'bootstrap', - type: 'postgres', - database: 'postgres', - host: 'localhost', - port: 5432, - username: 'postgres', // TODO: Make configurable - password: 'password', // TODO: Make configurable - schema: 'public', -}; +export const getPostgresBootstrapConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { + return { + name, + type: 'postgres', + database: 'postgres', + host: 'localhost', + port: 5432, + username: 'postgres', // TODO: Make configurable + password: 'password', // TODO: Make configurable + schema: 'public', + }; +} export const getPostgresConnectionOptions = ({ - databaseName, + name, }: { - databaseName: string; + name: string; }): ConnectionOptions => { return { + name, type: 'postgres', - database: databaseName, + database: name, host: 'localhost', port: 5432, password: 'password', // TODO: Make configurable diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 1883f60e266a8..a682676f6df85 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -10,7 +10,7 @@ import { createTestAccount } from 'nodemailer'; import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; import { Credentials, UserSettings } from 'n8n-core'; -import { Connection, createConnection, getConnection } from 'typeorm'; +import { createConnection, getConnection } from 'typeorm'; import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; @@ -32,8 +32,8 @@ import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; import { + getPostgresBootstrapConnectionOptions, getPostgresConnectionOptions, - POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS, SQLITE_TEST_CONNECTION_OPTIONS, } from './connectionOptions'; @@ -126,27 +126,44 @@ export function initConfigFile() { // ---------------------------------- export async function initTestDb() { - const dbType = config.get('database.type'); + const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysql'; - if (dbType === 'postgresdb') { - const bootstrap = await createConnection(POSTGRES_BOOTSTRAP_CONNECTION_OPTIONS); - await removeTestPostgresDatabases(bootstrap); - const newDatabaseName = await createTestPostgresDatabase(bootstrap); - bootstrap.close(); + if (dbType === 'sqlite') { + await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); + await getConnection().runMigrations({ transaction: 'none' }); - const options = getPostgresConnectionOptions({ databaseName: newDatabaseName }); - return Db.init(options); + return { testDbName: 'temp', bootstrapName: 'temp' }; // TODO } - if (dbType === 'sqlite') { - await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); - return getConnection().runMigrations({ transaction: 'none' }); + if (dbType === 'postgresdb') { + const bootstrapName = `n8n_bs_${Date.now()}`; + const bsConnectionOptions = getPostgresBootstrapConnectionOptions({ name: bootstrapName }); + await createConnection(bsConnectionOptions); + const testDbName = await createPostgresTestDb(bootstrapName); + + const pgConnectionOptions = getPostgresConnectionOptions({ name: testDbName }); + await Db.init(pgConnectionOptions); + + return { testDbName, bootstrapName }; } throw new Error('MySQL test connection pending implementation'); // TODO } -export async function removeTestPostgresDatabases(bootstrap: Connection) { +export async function terminateTestDb(testDbName: string, bootstrapName: string) { + const dbType = config.get('database.type'); + + if (dbType === 'postgresdb') { + await getConnection(testDbName).close(); + + await removePostgresTestDb(bootstrapName); + await getConnection(bootstrapName).close(); + } +} + +export async function removePostgresTestDb(bootstrapName: string) { + const bootstrap = getConnection(bootstrapName); + const results: { db_name: string }[] = await bootstrap.query( 'SELECT datname as db_name FROM pg_database;', ); @@ -155,17 +172,23 @@ export async function removeTestPostgresDatabases(bootstrap: Connection) { .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); - Promise.all(promises); + await Promise.all(promises); } -export async function createTestPostgresDatabase(bootstrap: Connection) { - const newDatabaseName = `n8n_test_pg_${Date.now()}`; - await bootstrap.query(`CREATE DATABASE ${newDatabaseName};`); +export async function createPostgresTestDb(bootstrapName: string) { + const testDbName = `n8n_test_pg_${Date.now()}`; + await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); - return newDatabaseName; + return testDbName; } -export async function truncate(entities: Array) { +/** + * Truncate tables for an array of 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') { @@ -175,7 +198,6 @@ export async function truncate(entities: Array) { } if (dbType === 'postgresdb') { - const tablePrefix = config.get('database.tablePrefix'); const map = { Credentials: 'credentials_entity', Workflow: 'workflow_entity', @@ -191,14 +213,15 @@ export async function truncate(entities: Array) { return Promise.all( entities.map((entity) => - getConnection().query( - `TRUNCATE TABLE ${tablePrefix}"${map[entity]}" RESTART IDENTITY CASCADE;`, + getConnection(testDbName).query( + `TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`, ), ), ); } if (dbType === 'mysqldb') { + // TODO // await getConnection().query('SET FOREIGN_KEY_CHECKS = 0;'); // await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); // return getConnection().query('SET FOREIGN_KEY_CHECKS = 1;'); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 6a0028f31872a..0455b26522bbe 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -1,5 +1,4 @@ import express = require('express'); -import { getConnection } from 'typeorm'; import validator from 'validator'; import { v4 as uuid } from 'uuid'; @@ -21,6 +20,8 @@ import * as UMHelper from '../../src/UserManagement/UserManagementHelper'; import { compare } from 'bcryptjs'; let app: express.Application; +let testDbName = ''; +let bootstrapName = ''; let globalOwnerRole: Role; let globalMemberRole: Role; let workflowOwnerRole: Role; @@ -28,7 +29,9 @@ let credentialOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); - await utils.initTestDb(); + const initResult = await utils.initTestDb(); + testDbName = initResult.testDbName; + bootstrapName = initResult.bootstrapName; const [ fetchedGlobalOwnerRole, @@ -46,7 +49,10 @@ beforeAll(async () => { }); beforeEach(async () => { - await utils.truncate(['User', 'Workflow', 'Credentials', 'SharedCredentials', 'SharedWorkflow']); + await utils.truncate( + ['User', 'Workflow', 'Credentials', 'SharedCredentials', 'SharedWorkflow'], + testDbName, + ); jest.isolateModules(() => { jest.mock('../../config'); @@ -67,8 +73,8 @@ beforeEach(async () => { UMHelper.isEmailSetUp = false; }); -afterAll(() => { - return getConnection().close(); +afterAll(async () => { + await utils.terminateTestDb(testDbName, bootstrapName); }); test('GET /users should return all users', async () => { @@ -308,12 +314,10 @@ 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: '5531199e-b7ae-425b-a326-a95ef8cca59d', - inviteeId: 'cb133beb-7729-4c34-8cd1-a06be8834d9d', - }); + 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 const fourth = await authOwnerAgent From 8a32a2ac5d8231dc927ab106aad64e96f8b02b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 11:48:55 +0100 Subject: [PATCH 08/59] :pencil2: Add TODOs --- packages/cli/src/Db.ts | 1 + packages/cli/test/integration/auth.endpoints.test.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index b45094e00abf7..0765ebc9ee908 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -178,6 +178,7 @@ export async function init( } } + // TODO: Remove testConnectionOptions if possible collections.Credentials = getRepository(entities.CredentialsEntity, testConnectionOptions?.name); collections.Execution = getRepository(entities.ExecutionEntity, testConnectionOptions?.name); collections.Workflow = getRepository(entities.WorkflowEntity, testConnectionOptions?.name); diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index b4085ed5498cc..55e6c2976db2c 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -51,6 +51,8 @@ afterEach(async () => { await utils.truncate(['User'], testDbName); }); +// TODO: find a post-entire-run hook to clean up DBs + afterAll(async () => { await utils.terminateTestDb(testDbName, bootstrapName); }); From dff5dd4dc0d78e3d894e183525d36124e0c10aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 11:50:56 +0100 Subject: [PATCH 09/59] :zap: Refactor DB dropping logic --- .../integration/shared/connectionOptions.ts | 32 ++++++------ packages/cli/test/integration/shared/utils.ts | 50 ++++++++----------- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 7e381796c366d..0e61f05a4ee68 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -26,35 +26,37 @@ export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { // postgres // ---------------------------------- -export const getPostgresBootstrapConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { +export function getOptions({ connectionName }: { connectionName: string }) { + return connectionName.startsWith('n8n_bs_') + ? getPostgresBootstrapConnectionOptions({ name: connectionName }) + : getPostgresConnectionOptions({ name: connectionName }); +} + +const getPostgresBootstrapConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { return { name, type: 'postgres', database: 'postgres', host: 'localhost', - port: 5432, - username: 'postgres', // TODO: Make configurable - password: 'password', // TODO: Make configurable - schema: 'public', + port: 5432, // TODO: Make configurable? + username: 'postgres', // TODO: Make configurable? + password: 'password', // TODO: Make configurable? + schema: 'public', // TODO: Make configurable? }; -} +}; -export const getPostgresConnectionOptions = ({ - name, -}: { - name: string; -}): ConnectionOptions => { +const getPostgresConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { return { name, type: 'postgres', database: name, host: 'localhost', - port: 5432, - password: 'password', // TODO: Make configurable - username: 'postgres', // TODO: Make configurable + port: 5432, // TODO: Make configurable? + password: 'password', // TODO: Make configurable? + username: 'postgres', // TODO: Make configurable? entityPrefix: '', - schema: 'public', + schema: 'public', // TODO: Make configurable? dropSchema: true, migrations: postgresMigrations, diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index a682676f6df85..bdca2257b9f7c 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -32,8 +32,7 @@ import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; import { - getPostgresBootstrapConnectionOptions, - getPostgresConnectionOptions, + getOptions, SQLITE_TEST_CONNECTION_OPTIONS, } from './connectionOptions'; @@ -137,12 +136,25 @@ export async function initTestDb() { if (dbType === 'postgresdb') { const bootstrapName = `n8n_bs_${Date.now()}`; - const bsConnectionOptions = getPostgresBootstrapConnectionOptions({ name: bootstrapName }); - await createConnection(bsConnectionOptions); - const testDbName = await createPostgresTestDb(bootstrapName); + const bootstrapConnection = await createConnection( + getOptions({ connectionName: bootstrapName }), + ); + + // TODO: Remove later -- Temp for cleanup + // const results: { db_name: string }[] = await bootstrapConnection.query( + // 'SELECT datname as db_name FROM pg_database;', + // ); + + // const promises = results + // .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) + // .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); + + // await Promise.all(promises); - const pgConnectionOptions = getPostgresConnectionOptions({ name: testDbName }); - await Db.init(pgConnectionOptions); + const testDbName = `n8n_test_pg_${Date.now()}`; + await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); + + await Db.init(getOptions({ connectionName: testDbName })); return { testDbName, bootstrapName }; } @@ -155,33 +167,11 @@ export async function terminateTestDb(testDbName: string, bootstrapName: string) if (dbType === 'postgresdb') { await getConnection(testDbName).close(); - - await removePostgresTestDb(bootstrapName); + await getConnection(bootstrapName).query(`DROP DATABASE ${testDbName}`); await getConnection(bootstrapName).close(); } } -export async function removePostgresTestDb(bootstrapName: string) { - const bootstrap = getConnection(bootstrapName); - - const results: { db_name: string }[] = await bootstrap.query( - 'SELECT datname as db_name FROM pg_database;', - ); - - const promises = results - .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) - .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); - - await Promise.all(promises); -} - -export async function createPostgresTestDb(bootstrapName: string) { - const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); - - return testDbName; -} - /** * Truncate tables for an array of entities. * From ea3baec59ecb4872cdfabacd53d60349daddca23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 12:07:32 +0100 Subject: [PATCH 10/59] :sparkles: Implement global teardown --- packages/cli/jest.config.js | 1 + .../integration/shared/connectionOptions.ts | 8 +++--- packages/cli/test/integration/shared/utils.ts | 22 +++------------- packages/cli/test/teardown.ts | 25 +++++++++++++++++++ 4 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 packages/cli/test/teardown.ts diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 1b3d0bc4651d2..55ae79f53380d 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -19,4 +19,5 @@ module.exports = { isolatedModules: true } }, + globalTeardown: '/test/teardown.ts' } diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 0e61f05a4ee68..7a5109e561ce0 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -26,10 +26,10 @@ export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { // postgres // ---------------------------------- -export function getOptions({ connectionName }: { connectionName: string }) { - return connectionName.startsWith('n8n_bs_') - ? getPostgresBootstrapConnectionOptions({ name: connectionName }) - : getPostgresConnectionOptions({ name: connectionName }); +export function getOptions({ name }: { name: string }) { + return name.startsWith('n8n_bs_') + ? getPostgresBootstrapConnectionOptions({ name }) + : getPostgresConnectionOptions({ name }); } const getPostgresBootstrapConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index bdca2257b9f7c..b6118a2311073 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -31,10 +31,7 @@ import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; -import { - getOptions, - SQLITE_TEST_CONNECTION_OPTIONS, -} from './connectionOptions'; +import { getOptions, SQLITE_TEST_CONNECTION_OPTIONS } from './connectionOptions'; export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out @@ -136,25 +133,12 @@ export async function initTestDb() { if (dbType === 'postgresdb') { const bootstrapName = `n8n_bs_${Date.now()}`; - const bootstrapConnection = await createConnection( - getOptions({ connectionName: bootstrapName }), - ); - - // TODO: Remove later -- Temp for cleanup - // const results: { db_name: string }[] = await bootstrapConnection.query( - // 'SELECT datname as db_name FROM pg_database;', - // ); - - // const promises = results - // .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) - // .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); - - // await Promise.all(promises); + await createConnection(getOptions({ name: bootstrapName })); const testDbName = `n8n_test_pg_${Date.now()}`; await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); - await Db.init(getOptions({ connectionName: testDbName })); + await Db.init(getOptions({ name: testDbName })); return { testDbName, bootstrapName }; } diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts new file mode 100644 index 0000000000000..e448da8b72e3c --- /dev/null +++ b/packages/cli/test/teardown.ts @@ -0,0 +1,25 @@ +import { createConnection, getConnection } from 'typeorm'; +import config = require('../config'); +import { getOptions } from './integration/shared/connectionOptions'; + +module.exports = async function () { + const dbType = config.get('database.type'); + + // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` + if (dbType === 'postgresdb') { + const bootstrapName = `n8n_bs_${Date.now()}`; + const bootstrap = await createConnection(getOptions({ name: bootstrapName })); + + const results: { db_name: string }[] = await bootstrap.query( + 'SELECT datname as db_name FROM pg_database;', + ); + + const promises = results + .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) + .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); + + await Promise.all(promises); + + await getConnection(bootstrapName).close(); + } +}; From d36efac2250dee5e16eefa0bf913b3398d1fcec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 14:16:14 +0100 Subject: [PATCH 11/59] :sparkles: Create TypeORM wrappers --- packages/cli/src/Db.ts | 39 ++++++++++++------- .../cli/src/UserManagement/routes/users.ts | 8 ++-- .../cli/src/api/namespaces/credentials.ts | 4 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 0765ebc9ee908..e5b3c0f06f411 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -8,8 +8,11 @@ import { Connection, ConnectionOptions, createConnection, + EntityManager, + EntityTarget, getRepository, LoggerOptions, + Repository, } from 'typeorm'; import { TlsOptions } from 'tls'; import * as path from 'path'; @@ -38,6 +41,16 @@ export const collections: IDatabaseCollections = { Settings: null, }; +let connection: Connection; + +export async function transaction(fn: (entityManager: EntityManager) => Promise): Promise { + return connection.transaction(fn); +} + +export function findRepository(entityClass: EntityTarget): Repository { + return getRepository(entityClass, connection.name); +} + export async function init( testConnectionOptions?: ConnectionOptions, ): Promise { @@ -45,7 +58,6 @@ export async function init( const n8nFolder = UserSettings.getUserN8nFolderPath(); let connectionOptions: ConnectionOptions; - let connection: Connection; const entityPrefix = config.get('database.tablePrefix'); @@ -179,20 +191,17 @@ export async function init( } // TODO: Remove testConnectionOptions if possible - collections.Credentials = getRepository(entities.CredentialsEntity, testConnectionOptions?.name); - collections.Execution = getRepository(entities.ExecutionEntity, testConnectionOptions?.name); - collections.Workflow = getRepository(entities.WorkflowEntity, testConnectionOptions?.name); - collections.Webhook = getRepository(entities.WebhookEntity, testConnectionOptions?.name); - collections.Tag = getRepository(entities.TagEntity, testConnectionOptions?.name); - - collections.Role = getRepository(entities.Role, testConnectionOptions?.name); - collections.User = getRepository(entities.User, testConnectionOptions?.name); - collections.SharedCredentials = getRepository( - entities.SharedCredentials, - testConnectionOptions?.name, - ); - collections.SharedWorkflow = getRepository(entities.SharedWorkflow, testConnectionOptions?.name); - collections.Settings = getRepository(entities.Settings, testConnectionOptions?.name); + collections.Credentials = findRepository(entities.CredentialsEntity); + collections.Execution = findRepository(entities.ExecutionEntity); + collections.Workflow = findRepository(entities.WorkflowEntity); + collections.Webhook = findRepository(entities.WebhookEntity); + collections.Tag = findRepository(entities.TagEntity); + + collections.Role = findRepository(entities.Role); + collections.User = findRepository(entities.User); + collections.SharedCredentials = findRepository(entities.SharedCredentials); + collections.SharedWorkflow = findRepository(entities.SharedWorkflow); + collections.Settings = findRepository(entities.Settings); return collections; } diff --git a/packages/cli/src/UserManagement/routes/users.ts b/packages/cli/src/UserManagement/routes/users.ts index f2beebf2a5399..068626b39813a 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -2,7 +2,7 @@ /* 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'; @@ -118,7 +118,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(), { @@ -366,7 +366,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 }, @@ -394,7 +394,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) { diff --git a/packages/cli/src/api/namespaces/credentials.ts b/packages/cli/src/api/namespaces/credentials.ts index e941fba7b52f0..e03fe3f7d02b0 100644 --- a/packages/cli/src/api/namespaces/credentials.ts +++ b/packages/cli/src/api/namespaces/credentials.ts @@ -3,7 +3,7 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable import/no-cycle */ -import { getConnection, In } from 'typeorm'; +import { In } from 'typeorm'; import { UserSettings, Credentials } from 'n8n-core'; import { INodeCredentialTestResult } from 'n8n-workflow'; @@ -112,7 +112,7 @@ export function credentialsEndpoints(this: N8nApp): void { 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; From c12f42acfe85a250c0a5642e24f67cb0bf77a0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 15:55:26 +0100 Subject: [PATCH 12/59] :sparkles: Initial MySQL setup --- packages/cli/jest.config.js | 3 +- packages/cli/package.json | 1 + .../integration/shared/connectionOptions.ts | 50 +++++++++++++++++++ packages/cli/test/integration/shared/utils.ts | 32 ++++++++++-- packages/cli/test/setup.ts | 16 ++++++ packages/cli/test/teardown.ts | 14 +++++- 6 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 packages/cli/test/setup.ts diff --git a/packages/cli/jest.config.js b/packages/cli/jest.config.js index 55ae79f53380d..10119618c82ce 100644 --- a/packages/cli/jest.config.js +++ b/packages/cli/jest.config.js @@ -19,5 +19,6 @@ module.exports = { isolatedModules: true } }, - globalTeardown: '/test/teardown.ts' + globalTeardown: '/test/teardown.ts', + setupFiles: ['/test/setup.ts'], } diff --git a/packages/cli/package.json b/packages/cli/package.json index 9c22c429c4fb7..e31b283261e34 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,6 +32,7 @@ "test": "jest", "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/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 7a5109e561ce0..57a0fd32ca5c4 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -1,6 +1,8 @@ import { ConnectionOptions } from 'typeorm'; +import config = require('../../../config'); 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'; @@ -74,3 +76,51 @@ const getPostgresConnectionOptions = ({ name }: { name: string }): ConnectionOpt // ---------------------------------- // TODO: Pending mysql connection options + +export const MYSQL_TEST_CONNECTION_OPTIONS: ConnectionOptions = { + type: 'mysql', + database: 'n8n', + username: 'root', + password: 'password', + host: 'localhost', + port: 3306, +}; + +export const getBootstrapMySqlConnectionOptions = (): ConnectionOptions => { + const username = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + // const password = 'password'; + const host = config.get('database.mysqldb.host'); + const port = config.get('database.mysqldb.port'); + + return { + name: 'n8n_bs_mysql', + database: 'n8n_bs_mysql', + type: 'mysql', + host, + port, + username, + password, + }; +}; + +export const getMySqlConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { + const username = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + // const password = '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, + }; +}; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index b6118a2311073..1189df4bf2a48 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -31,7 +31,13 @@ import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants'; import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; -import { getOptions, SQLITE_TEST_CONNECTION_OPTIONS } from './connectionOptions'; +import { + getBootstrapMySqlConnectionOptions, + getMySqlConnectionOptions, + getOptions, + MYSQL_TEST_CONNECTION_OPTIONS, + SQLITE_TEST_CONNECTION_OPTIONS, +} from './connectionOptions'; export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out @@ -122,7 +128,7 @@ export function initConfigFile() { // ---------------------------------- export async function initTestDb() { - const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysql'; + const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; if (dbType === 'sqlite') { await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); @@ -143,13 +149,29 @@ export async function initTestDb() { return { testDbName, bootstrapName }; } - throw new Error('MySQL test connection pending implementation'); // TODO + if (dbType === 'mysqldb') { + const bootstrapName = `n8n_bs_mysql`; + await createConnection(getBootstrapMySqlConnectionOptions()); + + const testDbName = `n8n_test_pg_${Date.now()}`; + await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); + + await Db.init(getMySqlConnectionOptions({ name: testDbName })); + + // TODO + // await Db.init(MYSQL_TEST_CONNECTION_OPTIONS); + // await createConnection(getOptions({ name: 'n8n_bs_test' })); + + return { testDbName, bootstrapName }; + } + + throw new Error(`Unrecognized DB type: ${dbType}`); } export async function terminateTestDb(testDbName: string, bootstrapName: string) { - const dbType = config.get('database.type'); + const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; - if (dbType === 'postgresdb') { + if (dbType === 'postgresdb' || dbType === 'mysqldb') { await getConnection(testDbName).close(); await getConnection(bootstrapName).query(`DROP DATABASE ${testDbName}`); await getConnection(bootstrapName).close(); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts new file mode 100644 index 0000000000000..b41af09f207b9 --- /dev/null +++ b/packages/cli/test/setup.ts @@ -0,0 +1,16 @@ +import config = require('../config'); +const { exec } = require('child_process'); + +const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; + +// create bootstrap mysql database +if (dbType === 'mysqldb') { + const user = config.get('database.mysqldb.user'); + const password = config.get('database.mysqldb.password'); + // const password = 'password'; + const host = config.get('database.mysqldb.host'); + + exec( + `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`, + ); +} diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index e448da8b72e3c..4f5a39a542790 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -1,9 +1,10 @@ import { createConnection, getConnection } from 'typeorm'; import config = require('../config'); import { getOptions } from './integration/shared/connectionOptions'; +const { exec } = require('child_process'); module.exports = async function () { - const dbType = config.get('database.type'); + const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` if (dbType === 'postgresdb') { @@ -22,4 +23,15 @@ module.exports = async function () { await getConnection(bootstrapName).close(); } + + if (dbType === 'mysqldb') { + const user = config.get('database.mysqldb.user'); + // const password = config.get('database.mysqldb.password') ?? 'password'; // TODO + const password = 'password'; + const host = config.get('database.mysqldb.host'); + + exec( + `echo "DROP DATABASE n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`, + ); + } }; From 9e473229fe1f41f63e1cff1255f1191f64418308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 16:24:42 +0100 Subject: [PATCH 13/59] :zap: Clean up Postgres connection options --- .../test/integration/auth.endpoints.test.ts | 6 +- .../integration/shared/connectionOptions.ts | 56 ++++++++++--------- packages/cli/test/integration/shared/utils.ts | 51 ++++++++++------- packages/cli/test/setup.ts | 22 ++++++-- packages/cli/test/teardown.ts | 16 ++---- 5 files changed, 85 insertions(+), 66 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 55e6c2976db2c..17d9d294b2fe1 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -15,13 +15,11 @@ let globalOwnerRole: Role; let app: express.Application; let testDbName = ''; -let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; await utils.truncate(['User'], testDbName); @@ -51,10 +49,8 @@ afterEach(async () => { await utils.truncate(['User'], testDbName); }); -// TODO: find a post-entire-run hook to clean up DBs - afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); test('POST /login should log user in', async () => { diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 57a0fd32ca5c4..a370abec377d1 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -28,37 +28,43 @@ export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { // postgres // ---------------------------------- -export function getOptions({ name }: { name: string }) { - return name.startsWith('n8n_bs_') - ? getPostgresBootstrapConnectionOptions({ name }) - : getPostgresConnectionOptions({ name }); -} +export const getBootstrapPostgresOptions = (): 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'); -const getPostgresBootstrapConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { return { - name, + name: 'n8n_bs_postgres', type: 'postgres', - database: 'postgres', - host: 'localhost', - port: 5432, // TODO: Make configurable? - username: 'postgres', // TODO: Make configurable? - password: 'password', // TODO: Make configurable? - schema: 'public', // TODO: Make configurable? + database: 'postgres', // pre-existing + host, + port, + username, // change via env to 'postgres' + password, // change via env to 'password' + schema, }; }; -const getPostgresConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { +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: 'localhost', - port: 5432, // TODO: Make configurable? - password: 'password', // TODO: Make configurable? - username: 'postgres', // TODO: Make configurable? + host, + port, + username, // change via env to 'postgres' + password, // change via env to 'password' entityPrefix: '', - schema: 'public', // TODO: Make configurable? + schema, dropSchema: true, migrations: postgresMigrations, @@ -75,8 +81,6 @@ const getPostgresConnectionOptions = ({ name }: { name: string }): ConnectionOpt // mysql // ---------------------------------- -// TODO: Pending mysql connection options - export const MYSQL_TEST_CONNECTION_OPTIONS: ConnectionOptions = { type: 'mysql', database: 'n8n', @@ -86,10 +90,9 @@ export const MYSQL_TEST_CONNECTION_OPTIONS: ConnectionOptions = { port: 3306, }; -export const getBootstrapMySqlConnectionOptions = (): ConnectionOptions => { +export const getBootstrapMySqlOptions = (): ConnectionOptions => { const username = config.get('database.mysqldb.user'); - const password = config.get('database.mysqldb.password'); - // const password = 'password'; + const password = config.get('database.mysqldb.password'); // change via env to 'password' const host = config.get('database.mysqldb.host'); const port = config.get('database.mysqldb.port'); @@ -104,10 +107,9 @@ export const getBootstrapMySqlConnectionOptions = (): ConnectionOptions => { }; }; -export const getMySqlConnectionOptions = ({ name }: { name: string }): ConnectionOptions => { +export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions => { const username = config.get('database.mysqldb.user'); - const password = config.get('database.mysqldb.password'); - // const password = 'password'; + const password = config.get('database.mysqldb.password'); // change via env to 'password' const host = config.get('database.mysqldb.host'); const port = config.get('database.mysqldb.port'); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 1189df4bf2a48..144085391ef6c 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -32,10 +32,9 @@ import type { Role } from '../../../src/databases/entities/Role'; import type { User } from '../../../src/databases/entities/User'; import type { CredentialPayload, EndpointNamespace, NamespacesMap, SmtpTestAccount } from './types'; import { - getBootstrapMySqlConnectionOptions, - getMySqlConnectionOptions, - getOptions, - MYSQL_TEST_CONNECTION_OPTIONS, + getBootstrapMySqlOptions, + getMySqlOptions, + getPostgresOptions, SQLITE_TEST_CONNECTION_OPTIONS, } from './connectionOptions'; @@ -127,6 +126,7 @@ export function initConfigFile() { // test DB // ---------------------------------- +// TODO: Create and drop separate DBs per test run export async function initTestDb() { const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; @@ -134,33 +134,43 @@ export async function initTestDb() { await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); await getConnection().runMigrations({ transaction: 'none' }); - return { testDbName: 'temp', bootstrapName: 'temp' }; // TODO + return { testDbName: 'temp', bootstrapName: 'temp' }; } if (dbType === 'postgresdb') { - const bootstrapName = `n8n_bs_${Date.now()}`; - await createConnection(getOptions({ name: bootstrapName })); + 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'); + + await createConnection({ + name: 'n8n_bs_postgres', + type: 'postgres', + database: 'postgres', // pre-existing + host, + port, + username, // change via env to 'postgres' + password, // change via env to 'password' + schema, + }); const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); + await getConnection('n8n_bs_postgres').query(`CREATE DATABASE ${testDbName};`); - await Db.init(getOptions({ name: testDbName })); + await Db.init(getPostgresOptions({ name: testDbName })); - return { testDbName, bootstrapName }; + return { testDbName }; } if (dbType === 'mysqldb') { - const bootstrapName = `n8n_bs_mysql`; - await createConnection(getBootstrapMySqlConnectionOptions()); + const bootstrapName = 'n8n_bs_mysql'; + await createConnection(getBootstrapMySqlOptions()); const testDbName = `n8n_test_pg_${Date.now()}`; await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); - await Db.init(getMySqlConnectionOptions({ name: testDbName })); - - // TODO - // await Db.init(MYSQL_TEST_CONNECTION_OPTIONS); - // await createConnection(getOptions({ name: 'n8n_bs_test' })); + await Db.init(getMySqlOptions({ name: testDbName })); return { testDbName, bootstrapName }; } @@ -168,13 +178,13 @@ export async function initTestDb() { throw new Error(`Unrecognized DB type: ${dbType}`); } -export async function terminateTestDb(testDbName: string, bootstrapName: string) { +export async function terminateTestDb(testDbName: string) { const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; if (dbType === 'postgresdb' || dbType === 'mysqldb') { await getConnection(testDbName).close(); - await getConnection(bootstrapName).query(`DROP DATABASE ${testDbName}`); - await getConnection(bootstrapName).close(); + await getConnection('n8n_bs_postgres').query(`DROP DATABASE ${testDbName}`); + await getConnection('n8n_bs_postgres').close(); } } @@ -298,6 +308,7 @@ 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 }); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index b41af09f207b9..3d52884abe17a 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -1,16 +1,30 @@ +import { ConnectionOptions, createConnection, getConnection } from 'typeorm'; import config = require('../config'); const { exec } = require('child_process'); const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; -// create bootstrap mysql database if (dbType === 'mysqldb') { - const user = config.get('database.mysqldb.user'); + const username = config.get('database.mysqldb.user'); const password = config.get('database.mysqldb.password'); - // const password = 'password'; const host = config.get('database.mysqldb.host'); + const port = config.get('database.mysqldb.port'); exec( - `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`, + `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${username} -p${password}`, ); + + const bsMySqlConnectionOptions: ConnectionOptions = { + name: 'n8n_bs_mysql', + database: 'n8n_bs_mysql', + type: 'mysql', + host, + port, + username, + password, + }; + + (async () => { + await createConnection(bsMySqlConnectionOptions); + })(); } diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index 4f5a39a542790..15ef13c47680c 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -1,6 +1,6 @@ -import { createConnection, getConnection } from 'typeorm'; +import { createConnection } from 'typeorm'; import config = require('../config'); -import { getOptions } from './integration/shared/connectionOptions'; +import { getBootstrapPostgresOptions } from './integration/shared/connectionOptions'; const { exec } = require('child_process'); module.exports = async function () { @@ -8,8 +8,7 @@ module.exports = async function () { // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` if (dbType === 'postgresdb') { - const bootstrapName = `n8n_bs_${Date.now()}`; - const bootstrap = await createConnection(getOptions({ name: bootstrapName })); + const bootstrap = await createConnection(getBootstrapPostgresOptions()); const results: { db_name: string }[] = await bootstrap.query( 'SELECT datname as db_name FROM pg_database;', @@ -21,17 +20,14 @@ module.exports = async function () { await Promise.all(promises); - await getConnection(bootstrapName).close(); + bootstrap.close(); } if (dbType === 'mysqldb') { const user = config.get('database.mysqldb.user'); - // const password = config.get('database.mysqldb.password') ?? 'password'; // TODO - const password = 'password'; + const password = config.get('database.mysqldb.password'); // change via env to 'password' const host = config.get('database.mysqldb.host'); - exec( - `echo "DROP DATABASE n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`, - ); + exec(`echo "DROP DATABASE n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`); } }; From daf03202b81c844ac9a1c727a01370dcf5c31c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 16:38:37 +0100 Subject: [PATCH 14/59] :zap: Simplify by sharing bootstrap connection name --- .../cli/test/integration/auth.middleware.test.ts | 4 +--- .../test/integration/credentials.endpoints.test.ts | 4 +--- packages/cli/test/integration/me.endpoints.test.ts | 4 +--- .../cli/test/integration/owner.endpoints.test.ts | 4 +--- .../integration/passwordReset.endpoints.test.ts | 4 +--- .../test/integration/shared/connectionOptions.ts | 8 -------- packages/cli/test/integration/shared/utils.ts | 14 +++++++++----- .../cli/test/integration/users.endpoints.test.ts | 4 +--- packages/cli/test/setup.ts | 2 +- 9 files changed, 16 insertions(+), 32 deletions(-) diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index 1be675cc2c735..ba8f6daf69f61 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -10,18 +10,16 @@ import * as utils from './shared/utils'; let app: express.Application; let testDbName = ''; -let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ applyAuth: true, namespaces: ['me', 'auth', 'owner', 'users'] }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; utils.initLogger(); }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); ROUTES_REQUIRING_AUTHENTICATION.concat(ROUTES_REQUIRING_AUTHORIZATION).forEach((route) => { diff --git a/packages/cli/test/integration/credentials.endpoints.test.ts b/packages/cli/test/integration/credentials.endpoints.test.ts index 5b4b1555d0824..40c67e092117a 100644 --- a/packages/cli/test/integration/credentials.endpoints.test.ts +++ b/packages/cli/test/integration/credentials.endpoints.test.ts @@ -8,7 +8,6 @@ import type { SaveCredentialFunction } from './shared/types'; let app: express.Application; let testDbName = ''; -let bootstrapName = ''; let saveCredential: SaveCredentialFunction; beforeAll(async () => { @@ -19,7 +18,6 @@ beforeAll(async () => { }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; utils.initConfigFile(); @@ -36,7 +34,7 @@ afterEach(async () => { }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); test('POST /credentials should create cred', async () => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 2c8f1d09fb7e4..5515df4679167 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -21,21 +21,19 @@ import { getGlobalOwnerRole, toObject } from './shared/utils'; const dbType = config.get('database.type'); let app: express.Application; let testDbName = ''; -let bootstrapName = ''; let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; globalOwnerRole = await getGlobalOwnerRole(); utils.initLogger(); }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); describe('Owner shell', () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 67edcf0d8457e..7b9af733449b6 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -13,13 +13,11 @@ import { let app: express.Application; let testDbName = ''; -let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; utils.initLogger(); }); @@ -33,7 +31,7 @@ afterEach(async () => { }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); test('POST /owner should create owner and enable hasOwner setting', async () => { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 89a8a0f8d25df..28c56f2875ad4 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -16,13 +16,11 @@ import { Role } from '../../src/databases/entities/Role'; let app: express.Application; let globalOwnerRole: Role; let testDbName = ''; -let bootstrapName = ''; beforeAll(async () => { app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; await utils.truncate(['User'], testDbName); @@ -56,7 +54,7 @@ afterEach(async () => { }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); test('POST /forgot-password should send password reset email', async () => { diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index a370abec377d1..a841742e44b21 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -81,14 +81,6 @@ export const getPostgresOptions = ({ name }: { name: string }): ConnectionOption // mysql // ---------------------------------- -export const MYSQL_TEST_CONNECTION_OPTIONS: ConnectionOptions = { - type: 'mysql', - database: 'n8n', - username: 'root', - password: 'password', - host: 'localhost', - port: 3306, -}; export const getBootstrapMySqlOptions = (): ConnectionOptions => { const username = config.get('database.mysqldb.user'); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 144085391ef6c..6a5674f654ba6 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -164,15 +164,19 @@ export async function initTestDb() { } if (dbType === 'mysqldb') { - const bootstrapName = 'n8n_bs_mysql'; await createConnection(getBootstrapMySqlOptions()); - const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection(bootstrapName).query(`CREATE DATABASE ${testDbName};`); + const testDbName = `n8n_test_mysql_${Date.now()}`; + await getConnection('n8n_bs_mysql').query(`CREATE DATABASE ${testDbName};`); + + try { + await Db.init(getMySqlOptions({ name: testDbName })); + } catch (e) { + console.log(e); + } - await Db.init(getMySqlOptions({ name: testDbName })); - return { testDbName, bootstrapName }; + return { testDbName }; } throw new Error(`Unrecognized DB type: ${dbType}`); diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index a660c39a7fd49..05e94de03fb1b 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -21,7 +21,6 @@ import { compare } from 'bcryptjs'; let app: express.Application; let testDbName = ''; -let bootstrapName = ''; let globalOwnerRole: Role; let globalMemberRole: Role; let workflowOwnerRole: Role; @@ -31,7 +30,6 @@ beforeAll(async () => { app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; - bootstrapName = initResult.bootstrapName; const [ fetchedGlobalOwnerRole, @@ -74,7 +72,7 @@ beforeEach(async () => { }); afterAll(async () => { - await utils.terminateTestDb(testDbName, bootstrapName); + await utils.terminateTestDb(testDbName); }); test('GET /users should return all users', async () => { diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 3d52884abe17a..51713f70a52a8 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -11,7 +11,7 @@ if (dbType === 'mysqldb') { const port = config.get('database.mysqldb.port'); exec( - `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${username} -p${password}`, + `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${username} -p${password}; USE n8n_bs_mysql`, ); const bsMySqlConnectionOptions: ConnectionOptions = { From 64476121a8d98a5149ee6715dd86d910fccdb28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 17:40:17 +0100 Subject: [PATCH 15/59] :card_file_box: Fix MySQL migrations --- .../1636626154933-CreateUserManagement.ts | 426 ++++++++++-------- packages/cli/test/integration/shared/utils.ts | 8 +- 2 files changed, 241 insertions(+), 193 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index 5df3cd87eb50f..9b23cc0cab494 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -11,193 +11,265 @@ 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\`) + );`, ); 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), + CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) + REFERENCES role (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + );`, + ); // n8n.role??? + + // 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);', + // ); 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);', + `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 + );`, ); + // 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);', + // ); + 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_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 + );`, ); + // 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);', + // ); + 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`));', + `CREATE TABLE ${tablePrefix}settings ( + \`key\` VARCHAR(255) NOT NULL, + \`value\` TEXT NOT NULL, + \`loadOnStartup\` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (\`key\`) + );`, ); + // 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`));', + // ); + await queryRunner.query( - 'ALTER TABLE `' + - tablePrefix + - 'workflow_entity` DROP INDEX `IDX_' + - tablePrefix + - '943d8f922be094eb507cb9a7f9`', + `ALTER TABLE ${tablePrefix}workflow_entity DROP INDEX IDX_${tablePrefix}943d8f922be094eb507cb9a7f9`, ); + // 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`)', + `CREATE INDEX IDX_${tablePrefix}xeendlvptc5jy4hbol17b5xery ON ${tablePrefix}execution_entity (\`workflowId\`)`, ); + // 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'); @@ -205,46 +277,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 ?? null], ); 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.hasOwner", "false", 1)', + `INSERT INTO ${tablePrefix}settings (\`key\`, value, loadOnStartup) VALUES ("userManagement.hasOwner","false", 1)`, ); } @@ -252,18 +301,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/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 6a5674f654ba6..f34ae0e527fb4 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -185,11 +185,17 @@ export async function initTestDb() { export async function terminateTestDb(testDbName: string) { const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; - if (dbType === 'postgresdb' || dbType === 'mysqldb') { + if (dbType === 'postgresdb') { await getConnection(testDbName).close(); await getConnection('n8n_bs_postgres').query(`DROP DATABASE ${testDbName}`); await getConnection('n8n_bs_postgres').close(); } + + if (dbType === 'mysqldb') { + await getConnection(testDbName).close(); + await getConnection('n8n_bs_mysql').query(`DROP DATABASE ${testDbName}`); + await getConnection('n8n_bs_mysql').close(); + } } /** From fb53cb4f199600b59d3a6d95e2eec986c490842a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 28 Feb 2022 17:47:26 +0100 Subject: [PATCH 16/59] :fire: Remove comments --- .../1636626154933-CreateUserManagement.ts | 156 +----------------- 1 file changed, 1 insertion(+), 155 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index 9b23cc0cab494..028e7c4a5b9e1 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -1,4 +1,3 @@ -import { table } from 'console'; import { MigrationInterface, QueryRunner } from 'typeorm'; import { v4 as uuid } from 'uuid'; import config = require('../../../../config'); @@ -41,38 +40,7 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) REFERENCES role (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION );`, - ); // n8n.role??? - - // 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);', - // ); + ); await queryRunner.query( `CREATE TABLE ${tablePrefix}shared_workflow ( @@ -93,54 +61,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { );`, ); - // 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);', - // ); - await queryRunner.query( `CREATE TABLE ${tablePrefix}shared_credentials ( \`createdAt\` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -160,54 +80,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { );`, ); - // 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);', - // ); - await queryRunner.query( `CREATE TABLE ${tablePrefix}settings ( \`key\` VARCHAR(255) NOT NULL, @@ -217,40 +89,14 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { );`, ); - // 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`));', - // ); - await queryRunner.query( `ALTER TABLE ${tablePrefix}workflow_entity DROP INDEX IDX_${tablePrefix}943d8f922be094eb507cb9a7f9`, ); - // 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\`)`, ); - // 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");`, From 1acbadd4305a0fb1b24ea3046fbdae1d2ae9f4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 09:07:37 +0100 Subject: [PATCH 17/59] :zap: Use ES6 imports --- packages/cli/test/setup.ts | 4 ++-- packages/cli/test/teardown.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 51713f70a52a8..395ce3f464968 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -1,6 +1,6 @@ -import { ConnectionOptions, createConnection, getConnection } from 'typeorm'; +import { ConnectionOptions, createConnection } from 'typeorm'; import config = require('../config'); -const { exec } = require('child_process'); +import { exec } from 'child_process'; const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index 15ef13c47680c..eed973cf6aab0 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -1,7 +1,7 @@ import { createConnection } from 'typeorm'; import config = require('../config'); import { getBootstrapPostgresOptions } from './integration/shared/connectionOptions'; -const { exec } = require('child_process'); +import { exec } from 'child_process'; module.exports = async function () { const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; @@ -25,7 +25,7 @@ module.exports = async function () { if (dbType === 'mysqldb') { const user = config.get('database.mysqldb.user'); - const password = config.get('database.mysqldb.password'); // change via env to 'password' + const password = config.get('database.mysqldb.password'); const host = config.get('database.mysqldb.host'); exec(`echo "DROP DATABASE n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`); From 6ace92c054c48c3e87f0eef826ff2e63a9cade60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 09:07:50 +0100 Subject: [PATCH 18/59] :fire: Remove outdated comments --- .../cli/test/integration/shared/connectionOptions.ts | 12 ++++++------ packages/cli/test/integration/shared/utils.ts | 11 +++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index a841742e44b21..e5c8e200491fe 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -41,8 +41,8 @@ export const getBootstrapPostgresOptions = (): ConnectionOptions => { database: 'postgres', // pre-existing host, port, - username, // change via env to 'postgres' - password, // change via env to 'password' + username, + password, schema, }; }; @@ -60,8 +60,8 @@ export const getPostgresOptions = ({ name }: { name: string }): ConnectionOption database: name, host, port, - username, // change via env to 'postgres' - password, // change via env to 'password' + username, + password, entityPrefix: '', schema, @@ -84,7 +84,7 @@ export const getPostgresOptions = ({ name }: { name: string }): ConnectionOption export const getBootstrapMySqlOptions = (): ConnectionOptions => { const username = config.get('database.mysqldb.user'); - const password = config.get('database.mysqldb.password'); // change via env to 'password' + const password = config.get('database.mysqldb.password'); const host = config.get('database.mysqldb.host'); const port = config.get('database.mysqldb.port'); @@ -101,7 +101,7 @@ export const getBootstrapMySqlOptions = (): ConnectionOptions => { export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions => { const username = config.get('database.mysqldb.user'); - const password = config.get('database.mysqldb.password'); // change via env to 'password' + const password = config.get('database.mysqldb.password'); const host = config.get('database.mysqldb.host'); const port = config.get('database.mysqldb.port'); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index f34ae0e527fb4..fbe41cc197cf4 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -150,8 +150,8 @@ export async function initTestDb() { database: 'postgres', // pre-existing host, port, - username, // change via env to 'postgres' - password, // change via env to 'password' + username, + password, schema, }); @@ -169,12 +169,7 @@ export async function initTestDb() { const testDbName = `n8n_test_mysql_${Date.now()}`; await getConnection('n8n_bs_mysql').query(`CREATE DATABASE ${testDbName};`); - try { - await Db.init(getMySqlOptions({ name: testDbName })); - } catch (e) { - console.log(e); - } - + await Db.init(getMySqlOptions({ name: testDbName })); return { testDbName }; } From 727146a377cf904d3d0ccd27576111ce0ba15548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 09:18:23 +0100 Subject: [PATCH 19/59] :zap: Centralize bootstrap connection name handles --- .../integration/shared/connectionOptions.ts | 7 ++++--- .../cli/test/integration/shared/constants.ts | 12 ++++++++++++ packages/cli/test/integration/shared/utils.ts | 19 +++++++++++-------- packages/cli/test/setup.ts | 7 ++++--- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index e5c8e200491fe..7da0d50a65cce 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -5,6 +5,7 @@ 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 { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants'; // ---------------------------------- // sqlite @@ -36,7 +37,7 @@ export const getBootstrapPostgresOptions = (): ConnectionOptions => { const schema = config.get('database.postgresdb.schema'); return { - name: 'n8n_bs_postgres', + name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, type: 'postgres', database: 'postgres', // pre-existing host, @@ -89,8 +90,8 @@ export const getBootstrapMySqlOptions = (): ConnectionOptions => { const port = config.get('database.mysqldb.port'); return { - name: 'n8n_bs_mysql', - database: 'n8n_bs_mysql', + name: BOOTSTRAP_MYSQL_CONNECTION_NAME, + database: BOOTSTRAP_MYSQL_CONNECTION_NAME, type: 'mysql', host, port, diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 87cb58e05f71c..2e4e94df3fd3c 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -44,3 +44,15 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly = [ 'DELETE /users/123', 'POST /users/123/reinvite', ]; + +/** + * 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/utils.ts b/packages/cli/test/integration/shared/utils.ts index fbe41cc197cf4..5509a8bb70f29 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -13,7 +13,7 @@ import { Credentials, UserSettings } from 'n8n-core'; import { createConnection, getConnection } from 'typeorm'; import config = require('../../../config'); -import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants'; +import { AUTHLESS_ENDPOINTS, BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME, 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'; @@ -145,7 +145,7 @@ export async function initTestDb() { const schema = config.get('database.postgresdb.schema'); await createConnection({ - name: 'n8n_bs_postgres', + name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, type: 'postgres', database: 'postgres', // pre-existing host, @@ -156,7 +156,7 @@ export async function initTestDb() { }); const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection('n8n_bs_postgres').query(`CREATE DATABASE ${testDbName};`); + await getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); await Db.init(getPostgresOptions({ name: testDbName })); @@ -167,7 +167,7 @@ export async function initTestDb() { await createConnection(getBootstrapMySqlOptions()); const testDbName = `n8n_test_mysql_${Date.now()}`; - await getConnection('n8n_bs_mysql').query(`CREATE DATABASE ${testDbName};`); + await getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); await Db.init(getMySqlOptions({ name: testDbName })); @@ -182,14 +182,17 @@ export async function terminateTestDb(testDbName: string) { if (dbType === 'postgresdb') { await getConnection(testDbName).close(); - await getConnection('n8n_bs_postgres').query(`DROP DATABASE ${testDbName}`); - await getConnection('n8n_bs_postgres').close(); + + const bootstrapPostgres = getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME); + await bootstrapPostgres.query(`DROP DATABASE ${testDbName}`); + await bootstrapPostgres.close(); } if (dbType === 'mysqldb') { await getConnection(testDbName).close(); - await getConnection('n8n_bs_mysql').query(`DROP DATABASE ${testDbName}`); - await getConnection('n8n_bs_mysql').close(); + const bootstrapMySql = getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME); + await bootstrapMySql.query(`DROP DATABASE ${testDbName}`); + await bootstrapMySql.close(); } } diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 395ce3f464968..31557696e1ca0 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -1,6 +1,7 @@ import { ConnectionOptions, createConnection } from 'typeorm'; import config = require('../config'); import { exec } from 'child_process'; +import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants'; const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; @@ -11,12 +12,12 @@ if (dbType === 'mysqldb') { const port = config.get('database.mysqldb.port'); exec( - `echo "CREATE DATABASE IF NOT EXISTS n8n_bs_mysql" | mysql -h ${host} -u ${username} -p${password}; USE n8n_bs_mysql`, + `echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} -p${password}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME}`, ); const bsMySqlConnectionOptions: ConnectionOptions = { - name: 'n8n_bs_mysql', - database: 'n8n_bs_mysql', + name: BOOTSTRAP_MYSQL_CONNECTION_NAME, + database: BOOTSTRAP_MYSQL_CONNECTION_NAME, type: 'mysql', host, port, From e67fbc0f0a7cac0926bfbe174f9b51308fd0587c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 09:20:25 +0100 Subject: [PATCH 20/59] :zap: Centralize database types --- packages/cli/test/integration/shared/utils.ts | 6 +++--- packages/cli/test/setup.ts | 3 ++- packages/cli/test/teardown.ts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 5509a8bb70f29..7e9602a74ebde 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -16,7 +16,7 @@ import config = require('../../../config'); import { AUTHLESS_ENDPOINTS, BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME, 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 { DatabaseType, Db, ExternalHooks, ICredentialsDb, IDatabaseCollections } 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'; @@ -128,7 +128,7 @@ export function initConfigFile() { // TODO: Create and drop separate DBs per test run export async function initTestDb() { - const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; + const dbType = config.get('database.type') as DatabaseType; if (dbType === 'sqlite') { await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); @@ -178,7 +178,7 @@ export async function initTestDb() { } export async function terminateTestDb(testDbName: string) { - const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; + const dbType = config.get('database.type') as DatabaseType; if (dbType === 'postgresdb') { await getConnection(testDbName).close(); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 31557696e1ca0..e7e1cc6640a69 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -2,8 +2,9 @@ import { ConnectionOptions, createConnection } from 'typeorm'; import config = require('../config'); import { exec } from 'child_process'; import { BOOTSTRAP_MYSQL_CONNECTION_NAME } from './integration/shared/constants'; +import { DatabaseType } from '../src'; -const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; +const dbType = config.get('database.type') as DatabaseType; if (dbType === 'mysqldb') { const username = config.get('database.mysqldb.user'); diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index eed973cf6aab0..efe494e0caaa1 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -2,9 +2,10 @@ import { createConnection } from 'typeorm'; import config = require('../config'); import { getBootstrapPostgresOptions } from './integration/shared/connectionOptions'; import { exec } from 'child_process'; +import { DatabaseType } from '../src'; module.exports = async function () { - const dbType = config.get('database.type') as 'sqlite' | 'postgresdb' | 'mysqldb'; + const dbType = config.get('database.type') as DatabaseType; // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` if (dbType === 'postgresdb') { From 529ec603ec9d885e87937c7839e0cdbfc5047596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 10:18:02 +0100 Subject: [PATCH 21/59] :pencil2: Update comment --- packages/cli/test/integration/shared/connectionOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 7da0d50a65cce..95935afa333de 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -39,7 +39,7 @@ export const getBootstrapPostgresOptions = (): ConnectionOptions => { return { name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, type: 'postgres', - database: 'postgres', // pre-existing + database: 'postgres', // pre-existing default database host, port, username, From 9ffebcb7ef29e58f4d805cae6d8f5adb2fb32b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 10:57:13 +0100 Subject: [PATCH 22/59] :truck: Rename `findRepository` --- packages/cli/src/Db.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index e5b3c0f06f411..db788c521b866 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -47,7 +47,7 @@ export async function transaction(fn: (entityManager: EntityManager) => Promi return connection.transaction(fn); } -export function findRepository(entityClass: EntityTarget): Repository { +export function linkRepository(entityClass: EntityTarget): Repository { return getRepository(entityClass, connection.name); } @@ -190,18 +190,17 @@ export async function init( } } - // TODO: Remove testConnectionOptions if possible - collections.Credentials = findRepository(entities.CredentialsEntity); - collections.Execution = findRepository(entities.ExecutionEntity); - collections.Workflow = findRepository(entities.WorkflowEntity); - collections.Webhook = findRepository(entities.WebhookEntity); - collections.Tag = findRepository(entities.TagEntity); - - collections.Role = findRepository(entities.Role); - collections.User = findRepository(entities.User); - collections.SharedCredentials = findRepository(entities.SharedCredentials); - collections.SharedWorkflow = findRepository(entities.SharedWorkflow); - collections.Settings = findRepository(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; } From 510284b094b98ade50ebe80f77d052c1963e7e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 10:57:24 +0100 Subject: [PATCH 23/59] :construction: Attempt to truncate MySQL --- packages/cli/test/integration/shared/utils.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 7e9602a74ebde..903877f9327d4 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -13,10 +13,21 @@ import { Credentials, UserSettings } from 'n8n-core'; import { createConnection, getConnection } from 'typeorm'; import config = require('../../../config'); -import { AUTHLESS_ENDPOINTS, BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME, REST_PATH_SEGMENT } from './constants'; +import { + AUTHLESS_ENDPOINTS, + BOOTSTRAP_MYSQL_CONNECTION_NAME, + BOOTSTRAP_POSTGRES_CONNECTION_NAME, + REST_PATH_SEGMENT, +} from './constants'; import { AUTH_COOKIE_NAME } from '../../../src/constants'; import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes'; -import { DatabaseType, Db, ExternalHooks, ICredentialsDb, IDatabaseCollections } from '../../../src'; +import { + DatabaseType, + Db, + ExternalHooks, + ICredentialsDb, + IDatabaseCollections, +} 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'; @@ -126,7 +137,6 @@ export function initConfigFile() { // test DB // ---------------------------------- -// TODO: Create and drop separate DBs per test run export async function initTestDb() { const dbType = config.get('database.type') as DatabaseType; @@ -235,10 +245,10 @@ export async function truncate(entities: Array, test } if (dbType === 'mysqldb') { - // TODO - // await getConnection().query('SET FOREIGN_KEY_CHECKS = 0;'); - // await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); - // return getConnection().query('SET FOREIGN_KEY_CHECKS = 1;'); + const testDb = getConnection(testDbName); + await testDb.query('SET FOREIGN_KEY_CHECKS = 0;'); + await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); + return testDb.query('SET FOREIGN_KEY_CHECKS = 1;'); } } From dec005bc4d56381d27afd46030c17e7cd11c7feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 15:43:55 +0100 Subject: [PATCH 24/59] :sparkles: Implement creds router --- packages/cli/src/Server.ts | 11 +- packages/cli/src/api/credentials.api.ts | 382 ++++++++++++++++++ .../cli/src/api/namespaces/credentials.ts | 373 ----------------- .../test/integration/auth.endpoints.test.ts | 2 +- .../test/integration/auth.middleware.test.ts | 2 +- ...points.test.ts => credentials.api.test.ts} | 3 +- .../cli/test/integration/me.endpoints.test.ts | 2 +- .../test/integration/owner.endpoints.test.ts | 2 +- .../passwordReset.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 46 ++- .../test/integration/users.endpoints.test.ts | 2 +- 11 files changed, 432 insertions(+), 395 deletions(-) create mode 100644 packages/cli/src/api/credentials.api.ts delete mode 100644 packages/cli/src/api/namespaces/credentials.ts rename packages/cli/test/integration/{credentials.endpoints.test.ts => credentials.api.test.ts} (99%) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index d4c313d6d1759..5b0ac2c7e595c 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -170,10 +170,13 @@ import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpe import { ExecutionEntity } from './databases/entities/ExecutionEntity'; import { SharedWorkflow } from './databases/entities/SharedWorkflow'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants'; -import { credentialsEndpoints } from './api/namespaces/credentials'; +// import { credentialsEndpoints } from './api/namespaces/credentials'; +import { credentialsController } from './api/credentials.api'; require('body-parser-xml')(bodyParser); +export const externalHooks: IExternalHooksClass = ExternalHooks(); + class App { app: express.Application; @@ -263,7 +266,7 @@ class App { this.sslKey = config.get('ssl_key'); this.sslCert = config.get('ssl_cert'); - this.externalHooks = ExternalHooks(); + this.externalHooks = externalHooks; this.presetCredentialsLoaded = false; this.endpointPresetCredentials = config.get('credentials.overwrite.endpoint') as string; @@ -659,6 +662,8 @@ class App { // ---------------------------------------- await userManagementRouter.addRoutes.apply(this, [ignoredEndpoints, this.restEndpoint]); + this.app.use(`/${this.restEndpoint}/credentials`, credentialsController); + // ---------------------------------------- // Healthcheck // ---------------------------------------- @@ -1563,7 +1568,7 @@ class App { // Credentials // ---------------------------------------- - credentialsEndpoints.apply(this); + // credentialsEndpoints.apply(this); // ---------------------------------------- // Credential-Types diff --git a/packages/cli/src/api/credentials.api.ts b/packages/cli/src/api/credentials.api.ts new file mode 100644 index 0000000000000..f6dfcac979830 --- /dev/null +++ b/packages/cli/src/api/credentials.api.ts @@ -0,0 +1,382 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable import/no-cycle */ +import express = require('express'); +import { getConnection, In } from 'typeorm'; +import { UserSettings, Credentials } from 'n8n-core'; +import { INodeCredentialTestResult } from 'n8n-workflow'; + +import { + CredentialsHelper, + Db, + GenericHelpers, + ICredentialsDb, + ICredentialsResponse, + whereClause, +} from '..'; +import * as ResponseHelper from '../ResponseHelper'; + +import { RESPONSE_ERROR_MESSAGES } from '../constants'; +import { CredentialsEntity } from '../databases/entities/CredentialsEntity'; +import { SharedCredentials } from '../databases/entities/SharedCredentials'; +import { validateEntity } from '../GenericHelpers'; +import type { CredentialRequest } from '../requests'; +import config = require('../../config'); +import { externalHooks } from '../Server'; + +export const credentialsController = express.Router(); + +/** + * GET /credentials + */ +credentialsController.get( + '/', + ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise => { + let credentials: ICredentialsDb[] = []; + + const filter = req.query.filter ? (JSON.parse(req.query.filter) as Record) : {}; + + if (req.user.globalRole.name === 'owner') { + credentials = await Db.collections.Credentials!.find({ + select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'], + where: filter, + }); + } else { + const shared = await Db.collections.SharedCredentials!.find({ + where: whereClause({ + user: req.user, + entityType: 'credentials', + }), + }); + + if (!shared.length) return []; + + credentials = await Db.collections.Credentials!.find({ + select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'], + where: { + id: In(shared.map(({ credentialId }) => credentialId)), + ...filter, + }, + }); + } + + return credentials.map((credential) => { + // eslint-disable-next-line no-param-reassign + credential.id = credential.id.toString(); + return credential as ICredentialsResponse; + }); + }), +); + +/** + * GET /credentials/new + * + * Generate a unique credential name. + */ +credentialsController.get( + '/new', + ResponseHelper.send(async (req: CredentialRequest.NewName): Promise<{ name: string }> => { + const { name: newName } = req.query; + + return GenericHelpers.generateUniqueName( + newName ?? config.get('credentials.defaultName'), + 'credentials', + ); + }), +); + +/** + * POST /credentials/test + * + * Test if a credential is valid. + */ +credentialsController.post( + '/test', + ResponseHelper.send(async (req: CredentialRequest.Test): Promise => { + const { credentials, nodeToTestWith } = req.body; + + const encryptionKey = await UserSettings.getEncryptionKey(); + + if (!encryptionKey) { + return { + status: 'Error', + message: RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, + }; + } + + const helper = new CredentialsHelper(encryptionKey); + + return helper.testCredentials(req.user, credentials.type, credentials, nodeToTestWith); + }), +); + +/** + * POST /credentials + */ +credentialsController.post( + '/', + ResponseHelper.send(async (req: CredentialRequest.Create) => { + delete req.body.id; // delete if sent + + const newCredential = new CredentialsEntity(); + + Object.assign(newCredential, req.body); + + await validateEntity(newCredential); + + // Add the added date for node access permissions + for (const nodeAccess of newCredential.nodesAccess) { + nodeAccess.date = new Date(); + } + + const encryptionKey = await UserSettings.getEncryptionKey(); + + if (!encryptionKey) { + throw new ResponseHelper.ResponseError( + RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, + undefined, + 500, + ); + } + + // Encrypt the data + const coreCredential = new Credentials( + { id: null, name: newCredential.name }, + newCredential.type, + newCredential.nodesAccess, + ); + + // @ts-ignore + coreCredential.setData(newCredential.data, encryptionKey); + + const encryptedData = coreCredential.getDataToSave() as ICredentialsDb; + + Object.assign(newCredential, encryptedData); + + await externalHooks.run('credentials.create', [encryptedData]); + + const role = await Db.collections.Role!.findOneOrFail({ + name: 'owner', + scope: 'credential', + }); + + const { id, ...rest } = await Db.transaction(async (transactionManager) => { + const savedCredential = await transactionManager.save(newCredential); + + savedCredential.data = newCredential.data; + + const newSharedCredential = new SharedCredentials(); + + Object.assign(newSharedCredential, { + role, + user: req.user, + credentials: savedCredential, + }); + + await transactionManager.save(newSharedCredential); + + return savedCredential; + }); + + return { id: id.toString(), ...rest }; + }), +); + +/** + * DELETE /credentials/:id + */ +credentialsController.delete( + '/:id', + ResponseHelper.send(async (req: CredentialRequest.Delete) => { + const { id: credentialId } = req.params; + + const shared = await Db.collections.SharedCredentials!.findOne({ + relations: ['credentials'], + where: whereClause({ + user: req.user, + entityType: 'credentials', + entityId: credentialId, + }), + }); + + if (!shared) { + throw new ResponseHelper.ResponseError( + `Credential with ID "${credentialId}" could not be found to be deleted.`, + undefined, + 404, + ); + } + + await externalHooks.run('credentials.delete', [credentialId]); + + await Db.collections.Credentials!.remove(shared.credentials); + + return true; + }), +); + +/** + * PATCH /credentials/:id + */ +credentialsController.patch( + '/:id', + ResponseHelper.send(async (req: CredentialRequest.Update): Promise => { + const { id: credentialId } = req.params; + + const updateData = new CredentialsEntity(); + Object.assign(updateData, req.body); + + await validateEntity(updateData); + + const shared = await Db.collections.SharedCredentials!.findOne({ + relations: ['credentials'], + where: whereClause({ + user: req.user, + entityType: 'credentials', + entityId: credentialId, + }), + }); + + if (!shared) { + throw new ResponseHelper.ResponseError( + `Credential with ID "${credentialId}" could not be found to be updated.`, + undefined, + 404, + ); + } + + const { credentials: credential } = shared; + + // Add the date for newly added node access permissions + for (const nodeAccess of updateData.nodesAccess) { + if (!nodeAccess.date) { + nodeAccess.date = new Date(); + } + } + + const encryptionKey = await UserSettings.getEncryptionKey(); + + if (!encryptionKey) { + throw new ResponseHelper.ResponseError( + RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, + undefined, + 500, + ); + } + + const coreCredential = new Credentials( + { id: credential.id.toString(), name: credential.name }, + credential.type, + credential.nodesAccess, + credential.data, + ); + + const decryptedData = coreCredential.getData(encryptionKey); + + // Do not overwrite the oauth data else data like the access or refresh token would get lost + // everytime anybody changes anything on the credentials even if it is just the name. + if (decryptedData.oauthTokenData) { + // @ts-ignore + updateData.data.oauthTokenData = decryptedData.oauthTokenData; + } + + // Encrypt the data + const credentials = new Credentials( + { id: credentialId, name: updateData.name }, + updateData.type, + updateData.nodesAccess, + ); + + // @ts-ignore + credentials.setData(updateData.data, encryptionKey); + + const newCredentialData = credentials.getDataToSave() as ICredentialsDb; + + // Add special database related data + newCredentialData.updatedAt = new Date(); + + await externalHooks.run('credentials.update', [newCredentialData]); + + // Update the credentials in DB + await Db.collections.Credentials!.update(credentialId, newCredentialData); + + // We sadly get nothing back from "update". Neither if it updated a record + // nor the new value. So query now the updated entry. + const responseData = await Db.collections.Credentials!.findOne(credentialId); + + if (responseData === undefined) { + throw new ResponseHelper.ResponseError( + `Credential ID "${credentialId}" could not be found to be updated.`, + undefined, + 404, + ); + } + + // Remove the encrypted data as it is not needed in the frontend + const { id, data, ...rest } = responseData; + + return { + id: id.toString(), + ...rest, + }; + }), +); + +/** + * GET /credentials/:id + */ +credentialsController.get( + '/:id', + ResponseHelper.send(async (req: CredentialRequest.Get) => { + const { id: credentialId } = req.params; + + const shared = await Db.collections.SharedCredentials!.findOne({ + relations: ['credentials'], + where: whereClause({ + user: req.user, + entityType: 'credentials', + entityId: credentialId, + }), + }); + + if (!shared) return {}; + + const { credentials: credential } = shared; + + if (req.query.includeData !== 'true') { + const { data, id, ...rest } = credential; + + return { + id: id.toString(), + ...rest, + }; + } + + const { data, id, ...rest } = credential; + + const encryptionKey = await UserSettings.getEncryptionKey(); + + if (!encryptionKey) { + throw new ResponseHelper.ResponseError( + RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, + undefined, + 500, + ); + } + + const coreCredential = new Credentials( + { id: credential.id.toString(), name: credential.name }, + credential.type, + credential.nodesAccess, + credential.data, + ); + + return { + id: id.toString(), + data: coreCredential.getData(encryptionKey), + ...rest, + }; + }), +); diff --git a/packages/cli/src/api/namespaces/credentials.ts b/packages/cli/src/api/namespaces/credentials.ts deleted file mode 100644 index e03fe3f7d02b0..0000000000000 --- a/packages/cli/src/api/namespaces/credentials.ts +++ /dev/null @@ -1,373 +0,0 @@ -/* eslint-disable @typescript-eslint/no-shadow */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable no-restricted-syntax */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/* eslint-disable import/no-cycle */ -import { In } from 'typeorm'; -import { UserSettings, Credentials } from 'n8n-core'; -import { INodeCredentialTestResult } from 'n8n-workflow'; - -import { - CredentialsHelper, - Db, - GenericHelpers, - ICredentialsDb, - ICredentialsResponse, - ResponseHelper, - whereClause, -} from '../..'; -import { RESPONSE_ERROR_MESSAGES } from '../../constants'; -import { CredentialsEntity } from '../../databases/entities/CredentialsEntity'; -import { SharedCredentials } from '../../databases/entities/SharedCredentials'; -import { validateEntity } from '../../GenericHelpers'; -import type { CredentialRequest } from '../../requests'; -import type { N8nApp } from '../../UserManagement/Interfaces'; - -export function credentialsEndpoints(this: N8nApp): void { - /** - * Generate a unique credential name. - */ - this.app.get( - `/${this.restEndpoint}/credentials/new`, - ResponseHelper.send(async (req: CredentialRequest.NewName): Promise<{ name: string }> => { - const { name: newName } = req.query; - - return GenericHelpers.generateUniqueName( - newName ?? this.defaultCredentialsName, - 'credentials', - ); - }), - ); - - /** - * Test a credential. - */ - this.app.post( - `/${this.restEndpoint}/credentials-test`, - ResponseHelper.send(async (req: CredentialRequest.Test): Promise => { - const { credentials, nodeToTestWith } = req.body; - - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (!encryptionKey) { - return { - status: 'Error', - message: RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, - }; - } - - const helper = new CredentialsHelper(encryptionKey); - - return helper.testCredentials(req.user, credentials.type, credentials, nodeToTestWith); - }), - ); - - /** - * Create a credential. - */ - this.app.post( - `/${this.restEndpoint}/credentials`, - ResponseHelper.send(async (req: CredentialRequest.Create) => { - delete req.body.id; // delete if sent - - const newCredential = new CredentialsEntity(); - - Object.assign(newCredential, req.body); - - await validateEntity(newCredential); - - // Add the added date for node access permissions - for (const nodeAccess of newCredential.nodesAccess) { - nodeAccess.date = new Date(); - } - - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (!encryptionKey) { - throw new ResponseHelper.ResponseError( - RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, - undefined, - 500, - ); - } - - // Encrypt the data - const coreCredential = new Credentials( - { id: null, name: newCredential.name }, - newCredential.type, - newCredential.nodesAccess, - ); - - // @ts-ignore - coreCredential.setData(newCredential.data, encryptionKey); - - const encryptedData = coreCredential.getDataToSave() as ICredentialsDb; - - Object.assign(newCredential, encryptedData); - - await this.externalHooks.run('credentials.create', [encryptedData]); - - const role = await Db.collections.Role!.findOneOrFail({ - name: 'owner', - scope: 'credential', - }); - - const { id, ...rest } = await Db.transaction(async (transactionManager) => { - const savedCredential = await transactionManager.save(newCredential); - - savedCredential.data = newCredential.data; - - const newSharedCredential = new SharedCredentials(); - - Object.assign(newSharedCredential, { - role, - user: req.user, - credentials: savedCredential, - }); - - await transactionManager.save(newSharedCredential); - - return savedCredential; - }); - - return { id: id.toString(), ...rest }; - }), - ); - - /** - * Delete a credential. - */ - this.app.delete( - `/${this.restEndpoint}/credentials/:id`, - ResponseHelper.send(async (req: CredentialRequest.Delete) => { - const { id: credentialId } = req.params; - - const shared = await Db.collections.SharedCredentials!.findOne({ - relations: ['credentials'], - where: whereClause({ - user: req.user, - entityType: 'credentials', - entityId: credentialId, - }), - }); - - if (!shared) { - throw new ResponseHelper.ResponseError( - `Credential with ID "${credentialId}" could not be found to be deleted.`, - undefined, - 404, - ); - } - - await this.externalHooks.run('credentials.delete', [credentialId]); - - await Db.collections.Credentials!.remove(shared.credentials); - - return true; - }), - ); - - /** - * Update a credential. - */ - this.app.patch( - `/${this.restEndpoint}/credentials/:id`, - ResponseHelper.send(async (req: CredentialRequest.Update): Promise => { - const { id: credentialId } = req.params; - - const updateData = new CredentialsEntity(); - Object.assign(updateData, req.body); - - await validateEntity(updateData); - - const shared = await Db.collections.SharedCredentials!.findOne({ - relations: ['credentials'], - where: whereClause({ - user: req.user, - entityType: 'credentials', - entityId: credentialId, - }), - }); - - if (!shared) { - throw new ResponseHelper.ResponseError( - `Credential with ID "${credentialId}" could not be found to be updated.`, - undefined, - 404, - ); - } - - const { credentials: credential } = shared; - - // Add the date for newly added node access permissions - for (const nodeAccess of updateData.nodesAccess) { - if (!nodeAccess.date) { - nodeAccess.date = new Date(); - } - } - - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (!encryptionKey) { - throw new ResponseHelper.ResponseError( - RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, - undefined, - 500, - ); - } - - const coreCredential = new Credentials( - { id: credential.id.toString(), name: credential.name }, - credential.type, - credential.nodesAccess, - credential.data, - ); - - const decryptedData = coreCredential.getData(encryptionKey); - - // Do not overwrite the oauth data else data like the access or refresh token would get lost - // everytime anybody changes anything on the credentials even if it is just the name. - if (decryptedData.oauthTokenData) { - // @ts-ignore - updateData.data.oauthTokenData = decryptedData.oauthTokenData; - } - - // Encrypt the data - const credentials = new Credentials( - { id: credentialId, name: updateData.name }, - updateData.type, - updateData.nodesAccess, - ); - - // @ts-ignore - credentials.setData(updateData.data, encryptionKey); - - const newCredentialData = credentials.getDataToSave() as ICredentialsDb; - - // Add special database related data - newCredentialData.updatedAt = new Date(); - - await this.externalHooks.run('credentials.update', [newCredentialData]); - - // Update the credentials in DB - await Db.collections.Credentials!.update(credentialId, newCredentialData); - - // We sadly get nothing back from "update". Neither if it updated a record - // nor the new value. So query now the updated entry. - const responseData = await Db.collections.Credentials!.findOne(credentialId); - - if (responseData === undefined) { - throw new ResponseHelper.ResponseError( - `Credential ID "${credentialId}" could not be found to be updated.`, - undefined, - 404, - ); - } - - // Remove the encrypted data as it is not needed in the frontend - const { id, data, ...rest } = responseData; - - return { - id: id.toString(), - ...rest, - }; - }), - ); - - /** - * Retrieve a credential. - */ - this.app.get( - `/${this.restEndpoint}/credentials/:id`, - ResponseHelper.send(async (req: CredentialRequest.Get) => { - const { id: credentialId } = req.params; - - const shared = await Db.collections.SharedCredentials!.findOne({ - relations: ['credentials'], - where: whereClause({ - user: req.user, - entityType: 'credentials', - entityId: credentialId, - }), - }); - - if (!shared) return {}; - - const { credentials: credential } = shared; - - if (req.query.includeData !== 'true') { - const { data, id, ...rest } = credential; - - return { - id: id.toString(), - ...rest, - }; - } - - const { data, id, ...rest } = credential; - - const encryptionKey = await UserSettings.getEncryptionKey(); - - if (!encryptionKey) { - throw new ResponseHelper.ResponseError( - RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY, - undefined, - 500, - ); - } - - const coreCredential = new Credentials( - { id: credential.id.toString(), name: credential.name }, - credential.type, - credential.nodesAccess, - credential.data, - ); - - return { - id: id.toString(), - data: coreCredential.getData(encryptionKey), - ...rest, - }; - }), - ); - - /** - * Retrieve all credentials. - */ - this.app.get( - `/${this.restEndpoint}/credentials`, - ResponseHelper.send(async (req: CredentialRequest.GetAll): Promise => { - let credentials: ICredentialsDb[] = []; - - const filter = req.query.filter - ? (JSON.parse(req.query.filter) as Record) - : {}; - - if (req.user.globalRole.name === 'owner') { - credentials = await Db.collections.Credentials!.find({ - select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'], - where: filter, - }); - } else { - const shared = await Db.collections.SharedCredentials!.find({ - where: whereClause({ - user: req.user, - entityType: 'credentials', - }), - }); - - if (!shared.length) return []; - - credentials = await Db.collections.Credentials!.find({ - select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'], - where: { - id: In(shared.map(({ credentialId }) => credentialId)), - ...filter, - }, - }); - } - - return credentials.map(({ id, ...rest }) => ({ id: id.toString(), ...rest })); - }), - ); -} diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 17d9d294b2fe1..cad7a68a6b7d0 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -17,7 +17,7 @@ let app: express.Application; let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['auth'], applyAuth: true }); + app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index ba8f6daf69f61..924ffc12e9c7f 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -12,7 +12,7 @@ let app: express.Application; let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ applyAuth: true, namespaces: ['me', 'auth', 'owner', 'users'] }); + app = utils.initTestServer({ applyAuth: true, endpointGroups: ['me', 'auth', 'owner', 'users'] }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; utils.initLogger(); diff --git a/packages/cli/test/integration/credentials.endpoints.test.ts b/packages/cli/test/integration/credentials.api.test.ts similarity index 99% rename from packages/cli/test/integration/credentials.endpoints.test.ts rename to packages/cli/test/integration/credentials.api.test.ts index 40c67e092117a..6b96ddeeaa4be 100644 --- a/packages/cli/test/integration/credentials.endpoints.test.ts +++ b/packages/cli/test/integration/credentials.api.test.ts @@ -12,9 +12,8 @@ let saveCredential: SaveCredentialFunction; beforeAll(async () => { app = utils.initTestServer({ - namespaces: ['credentials'], + endpointGroups: ['credentials'], applyAuth: true, - externalHooks: true, }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 5515df4679167..556d7008e6f82 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -24,7 +24,7 @@ let testDbName = ''; let globalOwnerRole: Role; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['me'], applyAuth: true }); + app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index 7b9af733449b6..f2af6efb8f4c3 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -15,7 +15,7 @@ let app: express.Application; let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['owner'], applyAuth: true }); + app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index 28c56f2875ad4..655f4abbf13b0 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -18,7 +18,7 @@ let globalOwnerRole: Role; let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['passwordReset'], applyAuth: true }); + app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 903877f9327d4..42f9fc60d2fb0 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -33,7 +33,7 @@ import { usersNamespace as usersEndpoints } from '../../../src/UserManagement/ro 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 { credentialsEndpoints } from '../../../src/api/namespaces/credentials'; +// import { credentialsEndpoints } from '../../../src/api/namespaces/credentials.X'; import { issueJWT } from '../../../src/UserManagement/auth/jwt'; import { randomEmail, randomValidPassword, randomName } from './random'; import { getLogger } from '../../../src/Logger'; @@ -48,6 +48,8 @@ import { getPostgresOptions, SQLITE_TEST_CONNECTION_OPTIONS, } from './connectionOptions'; +import { N8nApp } from '../../../src/UserManagement/Interfaces'; +import { credentialsController } from '../../../src/api/credentials.api'; export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out @@ -68,17 +70,15 @@ export const initLogger = () => { */ export function initTestServer({ applyAuth, - namespaces, - externalHooks, + endpointGroups, }: { applyAuth: boolean; - externalHooks?: true; - namespaces?: EndpointNamespace[]; + endpointGroups?: EndpointNamespace[]; }) { const testServer = { app: express(), restEndpoint: REST_PATH_SEGMENT, - ...(externalHooks ? { externalHooks: ExternalHooks() } : {}), + ...(endpointGroups?.includes('credentials') ? { externalHooks: ExternalHooks() } : {}), }; testServer.app.use(bodyParser.json()); @@ -91,24 +91,48 @@ export function initTestServer({ authMiddleware.apply(testServer, [AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT]); } - if (namespaces) { - const map: NamespacesMap = { + if (!endpointGroups) return testServer.app; + + const [routerEndpoints, functionEndpoints] = classifyEndpointGroups(endpointGroups); + + if (routerEndpoints.length) { + const map: Record = { + credentials: credentialsController, + }; + + for (const group of routerEndpoints) { + testServer.app.use(`/${testServer.restEndpoint}/${group}`, map[group]); + } + } + + if (functionEndpoints.length) { + const map: Record void> = { me: meEndpoints, users: usersEndpoints, auth: authEndpoints, owner: ownerEndpoints, passwordReset: passwordResetEndpoints, - credentials: credentialsEndpoints, }; - for (const namespace of namespaces) { - map[namespace].apply(testServer); + for (const group of functionEndpoints) { + map[group].apply(testServer); } } return testServer.app; } +const classifyEndpointGroups = (endpointGroups: string[]) => { + const routerEndpoints: string[] = []; + const functionEndpoints: string[] = []; + + endpointGroups.forEach((group) => + (group === 'credentials' ? routerEndpoints : functionEndpoints).push(group), + ); + + return [routerEndpoints, functionEndpoints]; +}; + // ---------------------------------- // test logger // ---------------------------------- diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 05e94de03fb1b..542c415d6aaf7 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -27,7 +27,7 @@ let workflowOwnerRole: Role; let credentialOwnerRole: Role; beforeAll(async () => { - app = utils.initTestServer({ namespaces: ['users'], applyAuth: true }); + app = utils.initTestServer({ endpointGroups: ['users'], applyAuth: true }); const initResult = await utils.initTestDb(); testDbName = initResult.testDbName; From ce3f50fac095afebc5c54a863f04eadd6d841002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 16:34:03 +0100 Subject: [PATCH 25/59] :bug: Fix duplicated MySQL bootstrap --- packages/cli/test/integration/shared/utils.ts | 4 ++-- packages/cli/test/setup.ts | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 2fe1b9352e0dd..74f4ddaa7cadf 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -198,10 +198,10 @@ export async function initTestDb() { } if (dbType === 'mysqldb') { - await createConnection(getBootstrapMySqlOptions()); + const bootstrapMysql = await createConnection(getBootstrapMySqlOptions()); const testDbName = `n8n_test_mysql_${Date.now()}`; - await getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); + await bootstrapMysql.query(`CREATE DATABASE ${testDbName};`); await Db.init(getMySqlOptions({ name: testDbName })); diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index e7e1cc6640a69..00281229c5f42 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -10,23 +10,8 @@ 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 port = config.get('database.mysqldb.port'); exec( `echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} -p${password}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME}`, ); - - const bsMySqlConnectionOptions: ConnectionOptions = { - name: BOOTSTRAP_MYSQL_CONNECTION_NAME, - database: BOOTSTRAP_MYSQL_CONNECTION_NAME, - type: 'mysql', - host, - port, - username, - password, - }; - - (async () => { - await createConnection(bsMySqlConnectionOptions); - })(); } From fbeaef85618780b6ca05425083bf9a96a0fec2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 16:48:58 +0100 Subject: [PATCH 26/59] :bug: Fix misresolved merge conflict --- packages/cli/src/Server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index c1622f325db5f..8820afc00bb6d 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1275,7 +1275,7 @@ class App { throw new ResponseHelper.ResponseError('Workflow tags are disabled'); } if ( - config.get('userManagement.hasOwner') === true && + config.get('userManagement.isInstanceOwnerSetUp') === true && req.user.globalRole.name !== 'owner' ) { throw new ResponseHelper.ResponseError( From 3ede58f5128a44392e03d2f9098613809d8e8e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 18:50:14 +0100 Subject: [PATCH 27/59] :card_file_box: Fix tags migration --- .../mysqldb/migrations/1617268711084-CreateTagEntity.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts b/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts index cd2ec52c5b422..56290daaa6529 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts @@ -9,6 +9,9 @@ export class CreateTagEntity1617268711084 implements MigrationInterface { // create tags table + relationship with workflow entity + await queryRunner.query('SET @@GLOBAL.FOREIGN_KEY_CHECKS=1;'); + await queryRunner.query('SET @@SESSION.FOREIGN_KEY_CHECKS=1;'); + await queryRunner.query('CREATE TABLE `' + tablePrefix + 'tag_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, UNIQUE INDEX `IDX_' + tablePrefix + '8f949d7a3a984759044054e89b` (`name`), PRIMARY KEY (`id`)) ENGINE=InnoDB'); await queryRunner.query('CREATE TABLE `' + tablePrefix + 'workflows_tags` (`workflowId` int NOT NULL, `tagId` int NOT NULL, INDEX `IDX_' + tablePrefix + '54b2f0343d6a2078fa13744386` (`workflowId`), INDEX `IDX_' + tablePrefix + '77505b341625b0b4768082e217` (`tagId`), PRIMARY KEY (`workflowId`, `tagId`)) ENGINE=InnoDB'); await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` ADD CONSTRAINT `FK_' + tablePrefix + '54b2f0343d6a2078fa137443869` FOREIGN KEY (`workflowId`) REFERENCES `' + tablePrefix + 'workflow_entity`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION'); From e4c71856d01e0943f22a66237f5b33028a6aa05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 18:50:33 +0100 Subject: [PATCH 28/59] :card_file_box: Fix MySQL UM migration --- .../1636626154933-CreateUserManagement.ts | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index 3ea61833795c8..d97a7513c94cb 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -9,6 +9,9 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tablePrefix = config.get('database.tablePrefix'); + await queryRunner.query('SET @@GLOBAL.FOREIGN_KEY_CHECKS=1;'); + await queryRunner.query('SET @@SESSION.FOREIGN_KEY_CHECKS=1;'); + await queryRunner.query( `CREATE TABLE ${tablePrefix}role ( \`id\` int NOT NULL AUTO_INCREMENT, @@ -18,7 +21,7 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { \`updatedAt\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), UNIQUE KEY \`UQ_${tablePrefix}5b49d0f504f7ef31045a1fb2eb8\` (\`scope\`,\`name\`) - );`, + ) ENGINE=InnoDB;`, ); await queryRunner.query( @@ -36,10 +39,15 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { \`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 role (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION - );`, + INDEX \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` (\`globalRoleId\` ASC) + ) ENGINE=InnoDB;`, + ); + + // CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) + // REFERENCES role (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + + await queryRunner.query( + `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( @@ -52,32 +60,56 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { 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 - );`, + PRIMARY KEY (\`userId\`, \`workflowId\`) + ) ENGINE=InnoDB;`, + ); + + // 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 + + await queryRunner.query( + `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}shared_workflow\` ADD CONSTRAINT \`FK_${tablePrefix}82b2fd9ec4e3e24209af8160282\` FOREIGN KEY (\`userId\`) REFERENCES \`${tablePrefix}user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + + await queryRunner.query( + `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\`), - 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 - );`, + \`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;`, + ); + + // 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 + + // CONSTRAINT \`FK_${tablePrefix}c68e056637562000b68f480815a\` FOREIGN KEY (\`roleId\`) REFERENCES \`${tablePrefix}role\` (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION + + 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( @@ -86,7 +118,7 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { \`value\` TEXT NOT NULL, \`loadOnStartup\` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (\`key\`) - );`, + ) ENGINE=InnoDB;`, ); await queryRunner.query( From 896798268de6493305d44f1eafb61b7f911cfd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 19:27:29 +0100 Subject: [PATCH 29/59] :bug: Fix MySQL parallelization issues --- packages/cli/test/integration/credentials.api.test.ts | 4 +++- packages/cli/test/integration/shared/utils.ts | 9 +++++---- packages/cli/test/integration/users.endpoints.test.ts | 7 +++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/cli/test/integration/credentials.api.test.ts b/packages/cli/test/integration/credentials.api.test.ts index 7dda011dcb07a..5aa1d2c2a82e5 100644 --- a/packages/cli/test/integration/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials.api.test.ts @@ -28,7 +28,9 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User', 'Credentials', 'SharedCredentials'], testDbName); + // do not combine calls - shared table must be cleared first and separately + await utils.truncate(['SharedCredentials'], testDbName); + await utils.truncate(['User', 'Credentials'], testDbName); }); afterAll(async () => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 74f4ddaa7cadf..5dd55f176b6f7 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -269,10 +269,11 @@ export async function truncate(entities: Array, test } if (dbType === 'mysqldb') { - const testDb = getConnection(testDbName); - await testDb.query('SET FOREIGN_KEY_CHECKS = 0;'); - await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); - return testDb.query('SET FOREIGN_KEY_CHECKS = 1;'); + await Promise.all( + entities.map(async (entity) => { + await Db.collections[entity]!.delete({}); + }), + ); } } diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index fdb00780b14c3..6196e7f925aae 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -47,10 +47,9 @@ beforeAll(async () => { }); beforeEach(async () => { - await utils.truncate( - ['User', 'Workflow', 'Credentials', 'SharedCredentials', 'SharedWorkflow'], - testDbName, - ); + // do not combine calls - shared tables must be cleared first and separately + await utils.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName); + await utils.truncate(['User', 'Workflow', 'Credentials'], testDbName); jest.isolateModules(() => { jest.mock('../../config'); From c5eaeb6c1251a3f30c78f924d46635070d486178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 19:27:47 +0100 Subject: [PATCH 30/59] :blue_book: Augment TypeORM to prevent error --- .../cli/test/integration/shared/augmentation.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) 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; + } +} From 0e32cc52610b432d1846ff93f733648e6c057422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 1 Mar 2022 19:29:06 +0100 Subject: [PATCH 31/59] :fire: Remove comments --- .../1636626154933-CreateUserManagement.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index d97a7513c94cb..5bbb96f1af338 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -43,9 +43,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { ) ENGINE=InnoDB;`, ); - // CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) - // REFERENCES role (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION - await queryRunner.query( `ALTER TABLE \`${tablePrefix}user\` ADD CONSTRAINT \`FK_${tablePrefix}f0609be844f9200ff4365b1bb3d\` FOREIGN KEY (\`globalRoleId\`) REFERENCES \`${tablePrefix}role\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, ); @@ -64,10 +61,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { ) ENGINE=InnoDB;`, ); - // 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 - await queryRunner.query( `ALTER TABLE \`${tablePrefix}shared_workflow\` ADD CONSTRAINT \`FK_${tablePrefix}3540da03964527aa24ae014b780\` FOREIGN KEY (\`roleId\`) REFERENCES \`${tablePrefix}role\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION`, ); @@ -94,12 +87,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { ) ENGINE=InnoDB;`, ); - // 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 - - // CONSTRAINT \`FK_${tablePrefix}c68e056637562000b68f480815a\` FOREIGN KEY (\`roleId\`) REFERENCES \`${tablePrefix}role\` (\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION - 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`, ); From 1f487099959a7b906ed0810bea3c317fe10c76ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 09:32:38 +0100 Subject: [PATCH 32/59] :sparkles: Support one sqlite DB per suite run --- .../integration/shared/connectionOptions.ts | 45 ++++++++++++------- packages/cli/test/integration/shared/utils.ts | 27 ++++++----- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts index 95935afa333de..34a81cd10eb65 100644 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ b/packages/cli/test/integration/shared/connectionOptions.ts @@ -1,6 +1,6 @@ import { ConnectionOptions } from 'typeorm'; -import config = require('../../../config'); +import config = require('../../../config'); import { entities } from '../../../src/databases/entities'; import { mysqlMigrations } from '../../../src/databases/mysqldb/migrations'; import { postgresMigrations } from '../../../src/databases/postgresdb/migrations'; @@ -11,24 +11,31 @@ import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } f // sqlite // ---------------------------------- -export const SQLITE_TEST_CONNECTION_OPTIONS: Readonly = { - type: 'sqlite', - database: ':memory:', - - entityPrefix: '', - dropSchema: true, - - migrations: sqliteMigrations, - migrationsTableName: 'migrations', - migrationsRun: false, - - logging: false, +/** + * Generate options to for an in-memory sqlite database connection, + * one per test suite run. No bootstrap connection required. + */ +export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions => { + return { + name, + type: 'sqlite', + database: ':memory:', + entityPrefix: '', + dropSchema: true, + migrations: sqliteMigrations, + migrationsTableName: 'migrations', + migrationsRun: false, + }; }; // ---------------------------------- // postgres // ---------------------------------- +/** + * Generate options for a bootstrap Postgres connection, + * to create and drop test Postgres databases. + */ export const getBootstrapPostgresOptions = (): ConnectionOptions => { const username = config.get('database.postgresdb.user'); const password = config.get('database.postgresdb.password'); @@ -63,15 +70,12 @@ export const getPostgresOptions = ({ name }: { name: string }): ConnectionOption port, username, password, - entityPrefix: '', schema, dropSchema: true, - migrations: postgresMigrations, migrationsRun: true, migrationsTableName: 'migrations', - entities: Object.values(entities), synchronize: false, logging: false, @@ -82,7 +86,10 @@ export const getPostgresOptions = ({ name }: { name: string }): ConnectionOption // mysql // ---------------------------------- - +/** + * 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'); @@ -100,6 +107,10 @@ export const getBootstrapMySqlOptions = (): ConnectionOptions => { }; }; +/** + * Generate options to 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'); diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 5dd55f176b6f7..a898a5e8261b1 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -46,7 +46,7 @@ import { getBootstrapMySqlOptions, getMySqlOptions, getPostgresOptions, - SQLITE_TEST_CONNECTION_OPTIONS, + getSqliteOptions, } from './connectionOptions'; import { credentialsController } from '../../../src/api/credentials.api'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; @@ -58,7 +58,7 @@ export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: P // ---------------------------------- export const initLogger = () => { - config.set('logs.output', 'file'); // declutter console output during tests + config.set('logs.output', 'file'); // declutter console output LoggerProxy.init(getLogger()); }; @@ -134,7 +134,7 @@ const classifyEndpointGroups = (endpointGroups: string[]) => { }; // ---------------------------------- -// test logger +// initializers // ---------------------------------- /** @@ -146,7 +146,7 @@ export function initTestLogger() { } /** - * Initialize a config file if non-existent. + * Initialize a user settings config file if non-existent. */ export function initConfigFile() { const settingsPath = UserSettings.getUserSettingsPath(); @@ -165,10 +165,11 @@ export async function initTestDb() { const dbType = config.get('database.type') as DatabaseType; if (dbType === 'sqlite') { - await Db.init(SQLITE_TEST_CONNECTION_OPTIONS); - await getConnection().runMigrations({ transaction: 'none' }); + const testDbName = `n8n_test_sqlite_${Date.now()}`; + await Db.init(getSqliteOptions({ name: testDbName })); + await getConnection(testDbName).runMigrations({ transaction: 'none' }); - return { testDbName: 'temp', bootstrapName: 'temp' }; + return { testDbName }; } if (dbType === 'postgresdb') { @@ -214,6 +215,10 @@ export async function initTestDb() { export async function terminateTestDb(testDbName: string) { const dbType = config.get('database.type') as DatabaseType; + if (dbType === 'sqlite') { + await getConnection(testDbName).close(); + } + if (dbType === 'postgresdb') { await getConnection(testDbName).close(); @@ -224,6 +229,7 @@ export async function terminateTestDb(testDbName: string) { if (dbType === 'mysqldb') { await getConnection(testDbName).close(); + const bootstrapMySql = getConnection(BOOTSTRAP_MYSQL_CONNECTION_NAME); await bootstrapMySql.query(`DROP DATABASE ${testDbName}`); await bootstrapMySql.close(); @@ -240,13 +246,14 @@ export async function truncate(entities: Array, test const dbType = config.get('database.type'); if (dbType === 'sqlite') { - await getConnection().query('PRAGMA foreign_keys=OFF'); + const testDb = getConnection(testDbName); + await testDb.query('PRAGMA foreign_keys=OFF'); await Promise.all(entities.map((entity) => Db.collections[entity]!.clear())); - return getConnection().query('PRAGMA foreign_keys=ON'); + return testDb.query('PRAGMA foreign_keys=ON'); } if (dbType === 'postgresdb') { - const map = { + const map: { [K in keyof IDatabaseCollections]: string } = { Credentials: 'credentials_entity', Workflow: 'workflow_entity', Execution: 'execution_entity', From 6871d81571156e4715847e6d003602c0210d5242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 09:59:38 +0100 Subject: [PATCH 33/59] :truck: Move `testDb` to own module --- .../test/integration/auth.endpoints.test.ts | 13 +- .../test/integration/auth.middleware.test.ts | 7 +- .../test/integration/credentials.api.test.ts | 46 +- .../cli/test/integration/me.endpoints.test.ts | 17 +- .../test/integration/owner.endpoints.test.ts | 9 +- .../passwordReset.endpoints.test.ts | 11 +- .../integration/shared/connectionOptions.ts | 132 ------ .../cli/test/integration/shared/testDb.ts | 401 ++++++++++++++++++ packages/cli/test/integration/shared/utils.ts | 322 +------------- .../test/integration/users.endpoints.test.ts | 32 +- packages/cli/test/teardown.ts | 4 +- 11 files changed, 492 insertions(+), 502 deletions(-) delete mode 100644 packages/cli/test/integration/shared/connectionOptions.ts create mode 100644 packages/cli/test/integration/shared/testDb.ts diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 0b0585336b142..5b4a37ce3d7ae 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -9,7 +9,8 @@ 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; @@ -18,17 +19,17 @@ let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['auth'], applyAuth: true }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); globalOwnerRole = await getGlobalOwnerRole(); utils.initLogger(); }); beforeEach(async () => { - await utils.createUser({ + await testDb.createUser({ id: uuid(), email: TEST_USER.email, firstName: TEST_USER.firstName, @@ -46,11 +47,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + 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 924ffc12e9c7f..4421fed389d0e 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -7,19 +7,20 @@ 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'] }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; utils.initLogger(); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + await testDb.terminate(testDbName); }); ROUTES_REQUIRING_AUTHENTICATION.concat(ROUTES_REQUIRING_AUTHORIZATION).forEach((route) => { @@ -36,7 +37,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 = await 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 5aa1d2c2a82e5..a4b35ef1c6009 100644 --- a/packages/cli/test/integration/credentials.api.test.ts +++ b/packages/cli/test/integration/credentials.api.test.ts @@ -3,7 +3,10 @@ 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 = ''; @@ -14,27 +17,27 @@ beforeAll(async () => { endpointGroups: ['credentials'], applyAuth: true, }); - const initResult = await utils.initTestDb(); + 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 () => { // do not combine calls - shared table must be cleared first and separately - await utils.truncate(['SharedCredentials'], testDbName); - await utils.truncate(['User', 'Credentials'], testDbName); + await testDb.truncate(['SharedCredentials'], testDbName); + await testDb.truncate(['User', 'Credentials'], testDbName); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + await testDb.terminate(testDbName); }); test('POST /credentials should create cred', async () => { @@ -132,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 = await 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}`); @@ -150,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -170,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); @@ -233,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 = await 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(); @@ -266,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); const patchPayload = credentialPayload(); @@ -301,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); const patchPayload = credentialPayload(); @@ -362,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 }); @@ -382,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 = await utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { @@ -406,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 = await utils.createAgent(app, { auth: true, user: member }); for (let i = 0; i < 3; i++) { @@ -445,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: member }); @@ -472,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 = await utils.createAgent(app, { auth: true, user: member }); const savedCredential = await saveCredential(credentialPayload(), { user: owner }); @@ -541,3 +544,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 3ac8384e160dc..529e70f3df385 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -16,7 +16,8 @@ import { randomName, randomString, } from './shared/random'; -import { getGlobalOwnerRole, toObject } from './shared/utils'; +import { toObject } from './shared/utils'; +import * as testDb from './shared/testDb'; const dbType = config.get('database.type'); let app: express.Application; @@ -25,24 +26,24 @@ let globalOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['me'], applyAuth: true }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; - globalOwnerRole = await getGlobalOwnerRole(); + globalOwnerRole = await testDb.getGlobalOwnerRole(); utils.initLogger(); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + await testDb.terminate(testDbName); }); describe('Owner shell', () => { beforeEach(async () => { - await utils.createOwnerShell(); + await testDb.createOwnerShell(); }); afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner shell', async () => { @@ -218,7 +219,7 @@ describe('Member', () => { }); afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized member', async () => { @@ -381,7 +382,7 @@ describe('Owner', () => { afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); test('GET /me should return sanitized owner', async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index dd158997b7790..ada5a3fd6d03a 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -2,6 +2,7 @@ import express = require('express'); 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 { @@ -16,22 +17,22 @@ let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['owner'], applyAuth: true }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; utils.initLogger(); }); beforeEach(async () => { - await utils.createOwnerShell(); + await testDb.createOwnerShell(); }); afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + 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 e49355634b492..a2439ba1b76d8 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -12,6 +12,7 @@ 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; @@ -19,10 +20,10 @@ let testDbName = ''; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); globalOwnerRole = await Db.collections.Role!.findOneOrFail({ name: 'owner', @@ -39,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, @@ -50,11 +51,11 @@ beforeEach(async () => { }); afterEach(async () => { - await utils.truncate(['User'], testDbName); + await testDb.truncate(['User'], testDbName); }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + await testDb.terminate(testDbName); }); test('POST /forgot-password should send password reset email', async () => { diff --git a/packages/cli/test/integration/shared/connectionOptions.ts b/packages/cli/test/integration/shared/connectionOptions.ts deleted file mode 100644 index 34a81cd10eb65..0000000000000 --- a/packages/cli/test/integration/shared/connectionOptions.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { ConnectionOptions } from 'typeorm'; - -import config = require('../../../config'); -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 { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants'; - -// ---------------------------------- -// sqlite -// ---------------------------------- - -/** - * Generate options to for an in-memory sqlite database connection, - * one per test suite run. No bootstrap connection required. - */ -export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions => { - return { - name, - type: 'sqlite', - database: ':memory:', - entityPrefix: '', - dropSchema: true, - migrations: sqliteMigrations, - migrationsTableName: 'migrations', - migrationsRun: false, - }; -}; - -// ---------------------------------- -// postgres -// ---------------------------------- - -/** - * Generate options for a bootstrap Postgres connection, - * to create and drop test Postgres databases. - */ -export const getBootstrapPostgresOptions = (): 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: BOOTSTRAP_POSTGRES_CONNECTION_NAME, - type: 'postgres', - database: 'postgres', // pre-existing default database - host, - port, - username, - password, - schema, - }; -}; - -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, - }; -}; - -// ---------------------------------- -// mysql -// ---------------------------------- - -/** - * 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 to 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, - }; -}; diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts new file mode 100644 index 0000000000000..9118c8836d857 --- /dev/null +++ b/packages/cli/test/integration/shared/testDb.ts @@ -0,0 +1,401 @@ +import { v4 as uuid } from 'uuid'; +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') { + const testDbName = `n8n_test_sqlite_${Date.now()}`; + await Db.init(getSqliteOptions({ name: testDbName })); + await getConnection(testDbName).runMigrations({ transaction: 'none' }); + + return { testDbName }; + } + + if (dbType === 'postgresdb') { + 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'); + + await createConnection({ + name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, + type: 'postgres', + database: 'postgres', // pre-existing + host, + port, + username, + password, + schema, + }); + + const testDbName = `n8n_test_pg_${Date.now()}`; + await getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); + + await Db.init(getPostgresOptions({ name: testDbName })); + + return { testDbName }; + } + + if (dbType === 'mysqldb') { + const bootstrapMysql = await createConnection(getBootstrapMySqlOptions()); + + const testDbName = `n8n_test_mysql_${Date.now()}`; + 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 tables for an array of 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'); + } + + if (dbType === 'postgresdb') { + 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', + }; + + return Promise.all( + entities.map((entity) => + getConnection(testDbName).query( + `TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`, + ), + ), + ); + } + + // Truncation in MySQL requires globals, which cannot be safely manipulated + // by multiple parallel tests, hence the use of `repository.delete()`. + if (dbType === 'mysqldb') { + await Promise.all( + entities.map(async (entity) => { + await Db.collections[entity]!.delete({}); + }), + ); + } +} + +/** + * 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; +} + +/** + * Store a user in the test DB, defaulting to a `member`. + */ +export async function createUser( + { + id, + email, + password, + firstName, + lastName, + role, + }: { + id: string; + email: string; + password: string; + firstName: string; + lastName: string; + role?: Role; + } = { + id: uuid(), + email: randomEmail(), + password: randomValidPassword(), + firstName: randomName(), + lastName: randomName(), + }, +) { + const globalRole = role ?? (await getGlobalMemberRole()); + return Db.collections.User!.save({ + id, + email, + password, + firstName, + lastName, + globalRole, + }); +} + +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 }); +} + +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 to for an in-memory sqlite database connection, + * one per test suite run. No bootstrap connection required. + */ +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 = (): 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: BOOTSTRAP_POSTGRES_CONNECTION_NAME, + type: 'postgres', + database: 'postgres', // pre-existing default database + host, + port, + username, + password, + schema, + }; +}; + +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 to 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 a898a5e8261b1..94d2bca9f1128 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -7,66 +7,34 @@ import { URL } from 'url'; import bodyParser = require('body-parser'); import * as util from 'util'; import { createTestAccount } from 'nodemailer'; -import { v4 as uuid } from 'uuid'; import { LoggerProxy } from 'n8n-workflow'; -import { Credentials, UserSettings } from 'n8n-core'; -import { createConnection, getConnection } from 'typeorm'; +import { UserSettings } from 'n8n-core'; import config = require('../../../config'); -import { - AUTHLESS_ENDPOINTS, - BOOTSTRAP_MYSQL_CONNECTION_NAME, - BOOTSTRAP_POSTGRES_CONNECTION_NAME, - REST_PATH_SEGMENT, -} from './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 { - DatabaseType, - 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 { 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 { CredentialPayload, EndpointGroup, SmtpTestAccount } from './types'; -import { - getBootstrapMySqlOptions, - getMySqlOptions, - getPostgresOptions, - getSqliteOptions, -} from './connectionOptions'; -import { credentialsController } from '../../../src/api/credentials.api'; +import type { EndpointGroup, SmtpTestAccount } from './types'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; 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 - 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, @@ -137,6 +105,13 @@ const classifyEndpointGroups = (endpointGroups: string[]) => { // initializers // ---------------------------------- +// TODO !!!!!!!!!!!!!!!!!!!!!!!!! + +export const initLogger = () => { + config.set('logs.output', 'file'); // declutter console output + LoggerProxy.init(getLogger()); +}; + /** * Initialize a silent logger for test runs. */ @@ -157,250 +132,6 @@ export function initConfigFile() { } } -// ---------------------------------- -// test DB -// ---------------------------------- - -export async function initTestDb() { - const dbType = config.get('database.type') as DatabaseType; - - if (dbType === 'sqlite') { - const testDbName = `n8n_test_sqlite_${Date.now()}`; - await Db.init(getSqliteOptions({ name: testDbName })); - await getConnection(testDbName).runMigrations({ transaction: 'none' }); - - return { testDbName }; - } - - if (dbType === 'postgresdb') { - 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'); - - await createConnection({ - name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, - type: 'postgres', - database: 'postgres', // pre-existing - host, - port, - username, - password, - schema, - }); - - const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); - - await Db.init(getPostgresOptions({ name: testDbName })); - - return { testDbName }; - } - - if (dbType === 'mysqldb') { - const bootstrapMysql = await createConnection(getBootstrapMySqlOptions()); - - const testDbName = `n8n_test_mysql_${Date.now()}`; - await bootstrapMysql.query(`CREATE DATABASE ${testDbName};`); - - await Db.init(getMySqlOptions({ name: testDbName })); - - return { testDbName }; - } - - throw new Error(`Unrecognized DB type: ${dbType}`); -} - -export async function terminateTestDb(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 tables for an array of 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'); - } - - if (dbType === 'postgresdb') { - 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', - }; - - return Promise.all( - entities.map((entity) => - getConnection(testDbName).query( - `TRUNCATE TABLE "${map[entity]}" RESTART IDENTITY CASCADE;`, - ), - ), - ); - } - - if (dbType === 'mysqldb') { - await Promise.all( - entities.map(async (entity) => { - await Db.collections[entity]!.delete({}); - }), - ); - } -} - -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( - { - id, - email, - password, - firstName, - lastName, - role, - }: { - id: string; - email: string; - password: string; - firstName: string; - lastName: string; - role?: Role; - } = { - id: uuid(), - email: randomEmail(), - password: randomValidPassword(), - firstName: randomName(), - lastName: randomName(), - }, -) { - const globalRole = role ?? (await getGlobalMemberRole()); - return Db.collections.User!.save({ - id, - email, - password, - firstName, - lastName, - globalRole, - }); -} - -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 }); -} - -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 // ---------------------------------- @@ -483,29 +214,6 @@ 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; -} - /** * Remove quote-escaping backslashes added by Postgres. */ diff --git a/packages/cli/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index 6196e7f925aae..f028270da9f6e 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -1,8 +1,8 @@ import express = require('express'); 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'; @@ -13,11 +13,11 @@ 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 = ''; @@ -28,7 +28,7 @@ let credentialOwnerRole: Role; beforeAll(async () => { app = utils.initTestServer({ endpointGroups: ['users'], applyAuth: true }); - const initResult = await utils.initTestDb(); + const initResult = await testDb.init(); testDbName = initResult.testDbName; const [ @@ -36,7 +36,7 @@ beforeAll(async () => { fetchedGlobalMemberRole, fetchedWorkflowOwnerRole, fetchedCredentialOwnerRole, - ] = await utils.getAllRoles(); + ] = await testDb.getAllRoles(); globalOwnerRole = fetchedGlobalOwnerRole; globalMemberRole = fetchedGlobalMemberRole; @@ -48,14 +48,14 @@ beforeAll(async () => { beforeEach(async () => { // do not combine calls - shared tables must be cleared first and separately - await utils.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName); - await utils.truncate(['User', 'Workflow', 'Credentials'], testDbName); + 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, @@ -71,14 +71,14 @@ beforeEach(async () => { }); afterAll(async () => { - await utils.terminateTestDb(testDbName); + await testDb.terminate(testDbName); }); test('GET /users should return all users', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - await createUser(); + await testDb.createUser(); const response = await authOwnerAgent.get('/users'); @@ -112,7 +112,7 @@ test('DELETE /users/:id should delete the user', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await utils.createAgent(app, { auth: true, user: owner }); - const userToDelete = await createUser(); + const userToDelete = await testDb.createUser(); const newWorkflow = new WorkflowEntity(); @@ -171,7 +171,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 @@ -193,7 +193,7 @@ test('DELETE /users/:id should fail if user to delete is transferee', async () = const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await 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, @@ -281,7 +281,7 @@ test('GET /resolve-signup-token should validate invite token', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await 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') @@ -303,7 +303,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await 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') @@ -316,7 +316,7 @@ test('GET /resolve-signup-token should fail with invalid inputs', async () => { 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 }) diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index efe494e0caaa1..4ed053669133a 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -1,10 +1,10 @@ import { createConnection } from 'typeorm'; import config = require('../config'); -import { getBootstrapPostgresOptions } from './integration/shared/connectionOptions'; import { exec } from 'child_process'; import { DatabaseType } from '../src'; +import { getBootstrapPostgresOptions } from './integration/shared/testDb'; -module.exports = async function () { +export default async () => { const dbType = config.get('database.type') as DatabaseType; // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` From 7f8c3bfc2dfe287a3524776c80ab9fb2290fdb46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:02:59 +0100 Subject: [PATCH 34/59] :fire: Deduplicate bootstrap Postgres logic --- .../cli/test/integration/shared/testDb.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 9118c8836d857..a1edb86ef643b 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -33,25 +33,10 @@ export async function init() { } if (dbType === 'postgresdb') { - 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'); - - await createConnection({ - name: BOOTSTRAP_POSTGRES_CONNECTION_NAME, - type: 'postgres', - database: 'postgres', // pre-existing - host, - port, - username, - password, - schema, - }); + const bootstrapPostgres = await createConnection(getBootstrapPostgresOptions()); const testDbName = `n8n_test_pg_${Date.now()}`; - await getConnection(BOOTSTRAP_POSTGRES_CONNECTION_NAME).query(`CREATE DATABASE ${testDbName};`); + await bootstrapPostgres.query(`CREATE DATABASE ${testDbName};`); await Db.init(getPostgresOptions({ name: testDbName })); From 80a3dcd34091e3a061c188ce4b0a5965ba444e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:07:59 +0100 Subject: [PATCH 35/59] :fire: Remove unneeded comment --- packages/cli/test/teardown.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index 4ed053669133a..1d0acaed34a79 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -7,7 +7,6 @@ import { getBootstrapPostgresOptions } from './integration/shared/testDb'; export default async () => { const dbType = config.get('database.type') as DatabaseType; - // clean up any remaining test Postgres DBs prefixed with `n8n_test_pg_` if (dbType === 'postgresdb') { const bootstrap = await createConnection(getBootstrapPostgresOptions()); From 64408dfa52b856beb4ab7af8fda58dc9ef0e6d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:11:39 +0100 Subject: [PATCH 36/59] :zap: Make logger init calls consistent --- .../cli/test/integration/auth.endpoints.test.ts | 2 +- .../cli/test/integration/auth.middleware.test.ts | 2 +- packages/cli/test/integration/me.endpoints.test.ts | 2 +- .../cli/test/integration/owner.endpoints.test.ts | 2 +- .../integration/passwordReset.endpoints.test.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 13 +++++-------- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/cli/test/integration/auth.endpoints.test.ts b/packages/cli/test/integration/auth.endpoints.test.ts index 5b4a37ce3d7ae..a20e5c65664db 100644 --- a/packages/cli/test/integration/auth.endpoints.test.ts +++ b/packages/cli/test/integration/auth.endpoints.test.ts @@ -25,7 +25,7 @@ beforeAll(async () => { await testDb.truncate(['User'], testDbName); globalOwnerRole = await getGlobalOwnerRole(); - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index 4421fed389d0e..f28888e48fff0 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -16,7 +16,7 @@ beforeAll(async () => { app = utils.initTestServer({ applyAuth: true, endpointGroups: ['me', 'auth', 'owner', 'users'] }); const initResult = await testDb.init(); testDbName = initResult.testDbName; - utils.initLogger(); + utils.initTestLogger(); }); afterAll(async () => { diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 529e70f3df385..6d7fe16501b6b 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -30,7 +30,7 @@ beforeAll(async () => { testDbName = initResult.testDbName; globalOwnerRole = await testDb.getGlobalOwnerRole(); - utils.initLogger(); + utils.initTestLogger(); }); afterAll(async () => { diff --git a/packages/cli/test/integration/owner.endpoints.test.ts b/packages/cli/test/integration/owner.endpoints.test.ts index ada5a3fd6d03a..45683761d76f2 100644 --- a/packages/cli/test/integration/owner.endpoints.test.ts +++ b/packages/cli/test/integration/owner.endpoints.test.ts @@ -20,7 +20,7 @@ beforeAll(async () => { const initResult = await testDb.init(); testDbName = initResult.testDbName; - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/passwordReset.endpoints.test.ts b/packages/cli/test/integration/passwordReset.endpoints.test.ts index a2439ba1b76d8..d34e6c66ebad2 100644 --- a/packages/cli/test/integration/passwordReset.endpoints.test.ts +++ b/packages/cli/test/integration/passwordReset.endpoints.test.ts @@ -29,7 +29,7 @@ beforeAll(async () => { name: 'owner', scope: 'global', }); - utils.initLogger(); + utils.initTestLogger(); }); beforeEach(async () => { diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index 94d2bca9f1128..d2f62e2bf4c3c 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -90,6 +90,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[] = []; @@ -105,18 +109,11 @@ const classifyEndpointGroups = (endpointGroups: string[]) => { // initializers // ---------------------------------- -// TODO !!!!!!!!!!!!!!!!!!!!!!!!! - -export const initLogger = () => { - config.set('logs.output', 'file'); // declutter console output - LoggerProxy.init(getLogger()); -}; - /** * 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()); } From 67a53760215c0f0d83197d015cc81343f66dae7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:12:14 +0100 Subject: [PATCH 37/59] :pencil2: Improve comment --- packages/cli/test/integration/shared/testDb.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index a1edb86ef643b..32ba15749977a 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -85,7 +85,7 @@ export async function terminate(testDbName: string) { } /** - * Truncate tables for an array of entities. + * 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. @@ -123,8 +123,7 @@ export async function truncate(entities: Array, test ); } - // Truncation in MySQL requires globals, which cannot be safely manipulated - // by multiple parallel tests, hence the use of `repository.delete()`. + // MySQL truncation requires globals, which cannot be safely manipulated by parallel tests if (dbType === 'mysqldb') { await Promise.all( entities.map(async (entity) => { From ae3bdea7a6b3c066047b10dfe453ca256c04174f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:15:02 +0100 Subject: [PATCH 38/59] :pencil2: Add dividers --- packages/cli/test/integration/shared/testDb.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 32ba15749977a..d38a1ee29fb06 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -133,6 +133,10 @@ export async function truncate(entities: Array, test } } +// ---------------------------------- +// credential creation +// ---------------------------------- + /** * Save a credential to the test DB, sharing it with a user. */ @@ -161,6 +165,10 @@ export async function saveCredential( return savedCredential; } +// ---------------------------------- +// user creation +// ---------------------------------- + /** * Store a user in the test DB, defaulting to a `member`. */ @@ -208,6 +216,10 @@ export async function createMemberShell() { return Db.collections.User!.save({ globalRole }); } +// ---------------------------------- +// role fetchers +// ---------------------------------- + export async function getGlobalOwnerRole() { return await Db.collections.Role!.findOneOrFail({ name: 'owner', From ca69bdfda2bb88bb22d79158c1488f8f86dceb68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:17:29 +0100 Subject: [PATCH 39/59] :art: Improve formatting --- packages/cli/test/integration/auth.middleware.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/integration/auth.middleware.test.ts b/packages/cli/test/integration/auth.middleware.test.ts index f28888e48fff0..e521c8bd333d4 100644 --- a/packages/cli/test/integration/auth.middleware.test.ts +++ b/packages/cli/test/integration/auth.middleware.test.ts @@ -13,7 +13,10 @@ let app: express.Application; let testDbName = ''; beforeAll(async () => { - app = utils.initTestServer({ applyAuth: true, endpointGroups: ['me', 'auth', 'owner', 'users'] }); + app = utils.initTestServer({ + applyAuth: true, + endpointGroups: ['me', 'auth', 'owner', 'users'], + }); const initResult = await testDb.init(); testDbName = initResult.testDbName; utils.initTestLogger(); From 3a7fb21f1f1d144db37fe5aaf5bab4b277d204fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:21:35 +0100 Subject: [PATCH 40/59] :fire: Remove duplicate MySQL global setting --- .../mysqldb/migrations/1617268711084-CreateTagEntity.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts b/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts index 56290daaa6529..cd2ec52c5b422 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1617268711084-CreateTagEntity.ts @@ -9,9 +9,6 @@ export class CreateTagEntity1617268711084 implements MigrationInterface { // create tags table + relationship with workflow entity - await queryRunner.query('SET @@GLOBAL.FOREIGN_KEY_CHECKS=1;'); - await queryRunner.query('SET @@SESSION.FOREIGN_KEY_CHECKS=1;'); - await queryRunner.query('CREATE TABLE `' + tablePrefix + 'tag_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, UNIQUE INDEX `IDX_' + tablePrefix + '8f949d7a3a984759044054e89b` (`name`), PRIMARY KEY (`id`)) ENGINE=InnoDB'); await queryRunner.query('CREATE TABLE `' + tablePrefix + 'workflows_tags` (`workflowId` int NOT NULL, `tagId` int NOT NULL, INDEX `IDX_' + tablePrefix + '54b2f0343d6a2078fa13744386` (`workflowId`), INDEX `IDX_' + tablePrefix + '77505b341625b0b4768082e217` (`tagId`), PRIMARY KEY (`workflowId`, `tagId`)) ENGINE=InnoDB'); await queryRunner.query('ALTER TABLE `' + tablePrefix + 'workflows_tags` ADD CONSTRAINT `FK_' + tablePrefix + '54b2f0343d6a2078fa137443869` FOREIGN KEY (`workflowId`) REFERENCES `' + tablePrefix + 'workflow_entity`(`id`) ON DELETE CASCADE ON UPDATE NO ACTION'); From b89a604feaca9fe448cedeb5715c60fbeee62cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:26:12 +0100 Subject: [PATCH 41/59] :truck: Move comment --- packages/cli/test/integration/shared/testDb.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index d38a1ee29fb06..ae5c5eab8b150 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -25,6 +25,7 @@ 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' }); @@ -263,7 +264,7 @@ export function getAllRoles() { /** * Generate options to for an in-memory sqlite database connection, - * one per test suite run. No bootstrap connection required. + * one per test suite run. */ export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions => { return { From 086d3e8e9304def8d52d9038ef4b191df03974d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:33:34 +0100 Subject: [PATCH 42/59] :zap: Update default test script --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 2aec425e83948..a5069fc566477 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,7 +29,7 @@ "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", From ff1799dd2801848fdf4bb5f2415fad3803d4a4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 10:45:12 +0100 Subject: [PATCH 43/59] :fire: Remove unneeded helper --- packages/cli/test/integration/me.endpoints.test.ts | 3 +-- packages/cli/test/integration/shared/utils.ts | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 6d7fe16501b6b..d7d27afbfce59 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -16,7 +16,6 @@ import { randomName, randomString, } from './shared/random'; -import { toObject } from './shared/utils'; import * as testDb from './shared/testDb'; const dbType = config.get('database.type'); @@ -179,7 +178,7 @@ describe('Owner shell', () => { for (const validPayload of validPayloads) { const response = await authOwnerShellAgent.post('/me/survey').send(validPayload); expect(response.statusCode).toBe(200); - expect(toObject(response.body)).toEqual(SUCCESS_RESPONSE_BODY); + expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); // @ts-ignore TODO: JSON.parse() at model level diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index d2f62e2bf4c3c..b5d2533f3d2c4 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -211,9 +211,3 @@ export async function isInstanceOwnerSetUp() { */ export const getSmtpTestAccount = util.promisify(createTestAccount); -/** - * Remove quote-escaping backslashes added by Postgres. - */ -export const toObject = (json: string) => { - return typeof json === 'string' ? JSON.parse(json) : json; -}; From df084ffde16beb2e67c40ee1af36984384a7df47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 11:02:20 +0100 Subject: [PATCH 44/59] :zap: Unmarshal answers from Postgres --- packages/cli/src/databases/entities/User.ts | 9 +++++++++ packages/cli/test/integration/me.endpoints.test.ts | 9 ++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index 1e8258c953122..0a0326433de5f 100644 --- a/packages/cli/src/databases/entities/User.ts +++ b/packages/cli/src/databases/entities/User.ts @@ -91,6 +91,15 @@ export class User { @Column({ type: resolveDataType('json') as ColumnOptions['type'], nullable: true, + transformer: { + to: (answers: object) => answers, + from: (answers: object | string) => { + // Postgres stores and returns string + return typeof answers === 'string' + ? (JSON.parse(answers) as IPersonalizationSurveyAnswers) + : answers; + }, + }, }) personalizationAnswers: IPersonalizationSurveyAnswers | null; diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index d7d27afbfce59..6560437f5dac3 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -180,9 +180,7 @@ describe('Owner shell', () => { expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); - // @ts-ignore TODO: JSON.parse() at model level - const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; + const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); expect(storedAnswers).toEqual(validPayload); } @@ -356,9 +354,7 @@ describe('Member', () => { expect(response.statusCode).toBe(200); expect(response.body).toEqual(SUCCESS_RESPONSE_BODY); - const { personalizationAnswers: answers } = await Db.collections.User!.findOneOrFail(); - // @ts-ignore TODO: JSON.parse() at model level - const storedAnswers = dbType === 'postgresdb' ? JSON.parse(answers) : answers; + const { personalizationAnswers: storedAnswers } = await Db.collections.User!.findOneOrFail(); expect(storedAnswers).toEqual(validPayload); } @@ -379,7 +375,6 @@ describe('Owner', () => { config.set('userManagement.isInstanceOwnerSetUp', true); }); - afterEach(async () => { await testDb.truncate(['User'], testDbName); }); From 9e49aa84b76addc86ae97b3526f4bc5e715d5d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 11:09:54 +0100 Subject: [PATCH 45/59] :bug: Phase out `isTestRun` --- packages/cli/src/ResponseHelper.ts | 5 +- packages/cli/src/api/ResponseHelper2.ts | 227 ------------------ packages/cli/src/api/credentials.api.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 2 - 4 files changed, 4 insertions(+), 232 deletions(-) delete mode 100644 packages/cli/src/api/ResponseHelper2.ts 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/api/ResponseHelper2.ts b/packages/cli/src/api/ResponseHelper2.ts deleted file mode 100644 index 7d66e9a946a66..0000000000000 --- a/packages/cli/src/api/ResponseHelper2.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* eslint-disable import/no-cycle */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable no-console */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable no-param-reassign */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Request, Response } from 'express'; -import { parse, stringify } from 'flatted'; - -// eslint-disable-next-line import/no-cycle -import { - IExecutionDb, - IExecutionFlatted, - IExecutionFlattedDb, - IExecutionResponse, - IWorkflowDb, -} from '..'; -import { isTestRun } from '../../test/integration/shared/utils'; - -/** - * Special Error which allows to return also an error code and http status code - * - * @export - * @class ResponseError - * @extends {Error} - */ -export class ResponseError extends Error { - // The HTTP status code of response - httpStatusCode?: number; - - // The error code in the response - errorCode?: number; - - // The error hint the response - hint?: string; - - /** - * Creates an instance of ResponseError. - * @param {string} message The error message - * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error - * @param {number} [httpStatusCode] The HTTP status code the response should have - * @param {string} [hint] The error hint to provide a context (webhook related) - * @memberof ResponseError - */ - constructor(message: string, errorCode?: number, httpStatusCode?: number, hint?: string) { - super(message); - this.name = 'ResponseError'; - - if (errorCode) { - this.errorCode = errorCode; - } - if (httpStatusCode) { - this.httpStatusCode = httpStatusCode; - } - if (hint) { - this.hint = hint; - } - } -} - -export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) { - resp.statusCode = 401; - resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`); - resp.json({ code: resp.statusCode, message }); -} - -export function jwtAuthAuthorizationError(resp: Response, message?: string) { - resp.statusCode = 403; - resp.json({ code: resp.statusCode, message }); -} - -export function sendSuccessResponse( - res: Response, - data: any, - raw?: boolean, - responseCode?: number, - responseHeader?: object, -) { - if (responseCode !== undefined) { - res.status(responseCode); - } - - if (responseHeader) { - res.header(responseHeader); - } - - if (raw === true) { - if (typeof data === 'string') { - res.send(data); - } else { - res.json(data); - } - } else { - res.json({ - data, - }); - } -} - -export function sendErrorResponse(res: Response, error: ResponseError, shouldLog = true) { - let httpStatusCode = 500; - if (error.httpStatusCode) { - httpStatusCode = error.httpStatusCode; - } - - if (process.env.NODE_ENV !== 'production' && shouldLog && !isTestRun) { - console.error('ERROR RESPONSE'); - console.error(error); - } - - const response = { - code: 0, - message: 'Unknown error', - hint: '', - }; - - if (error.name === 'NodeApiError') { - Object.assign(response, error); - } - - if (error.errorCode) { - response.code = error.errorCode; - } - if (error.message) { - response.message = error.message; - } - if (error.hint) { - response.hint = error.hint; - } - if (error.stack && process.env.NODE_ENV !== 'production') { - // @ts-ignore - response.stack = error.stack; - } - - res.status(httpStatusCode).json(response); -} - -const isUniqueConstraintError = (error: Error) => - ['unique', 'duplicate'].some((s) => error.message.toLowerCase().includes(s)); - -/** - * A helper function which does not just allow to return Promises it also makes sure that - * all the responses have the same format - * - * - * @export - * @param {(req: Request, res: Response) => Promise} processFunction The actual function to process the request - * @returns - */ - -export function send(processFunction: (req: Request, res: Response) => Promise) { - return async (req: Request, res: Response) => { - try { - const data = await processFunction(req, res); - - sendSuccessResponse(res, data); - } catch (error) { - if (error instanceof Error && isUniqueConstraintError(error)) { - error.message = 'There is already an entry with this name'; - } - - sendErrorResponse(res, error); - } - }; -} - -/** - * Flattens the Execution data. - * As it contains a lot of references which normally would be saved as duplicate data - * with regular JSON.stringify it gets flattened which keeps the references in place. - * - * @export - * @param {IExecutionDb} fullExecutionData The data to flatten - * @returns {IExecutionFlatted} - */ -export function flattenExecutionData(fullExecutionData: IExecutionDb): IExecutionFlatted { - // Flatten the data - const returnData: IExecutionFlatted = { - data: stringify(fullExecutionData.data), - mode: fullExecutionData.mode, - // @ts-ignore - waitTill: fullExecutionData.waitTill, - startedAt: fullExecutionData.startedAt, - stoppedAt: fullExecutionData.stoppedAt, - finished: fullExecutionData.finished ? fullExecutionData.finished : false, - workflowId: fullExecutionData.workflowId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - workflowData: fullExecutionData.workflowData!, - }; - - if (fullExecutionData.id !== undefined) { - returnData.id = fullExecutionData.id.toString(); - } - - if (fullExecutionData.retryOf !== undefined) { - returnData.retryOf = fullExecutionData.retryOf.toString(); - } - - if (fullExecutionData.retrySuccessId !== undefined) { - returnData.retrySuccessId = fullExecutionData.retrySuccessId.toString(); - } - - return returnData; -} - -/** - * Unflattens the Execution data. - * - * @export - * @param {IExecutionFlattedDb} fullExecutionData The data to unflatten - * @returns {IExecutionResponse} - */ -export function unflattenExecutionData(fullExecutionData: IExecutionFlattedDb): IExecutionResponse { - const returnData: IExecutionResponse = { - id: fullExecutionData.id.toString(), - workflowData: fullExecutionData.workflowData as IWorkflowDb, - data: parse(fullExecutionData.data), - mode: fullExecutionData.mode, - waitTill: fullExecutionData.waitTill ? fullExecutionData.waitTill : undefined, - startedAt: fullExecutionData.startedAt, - stoppedAt: fullExecutionData.stoppedAt, - finished: fullExecutionData.finished ? fullExecutionData.finished : false, - workflowId: fullExecutionData.workflowId, - }; - - return returnData; -} diff --git a/packages/cli/src/api/credentials.api.ts b/packages/cli/src/api/credentials.api.ts index ad85fe68334b7..26bcd72628f9f 100644 --- a/packages/cli/src/api/credentials.api.ts +++ b/packages/cli/src/api/credentials.api.ts @@ -15,8 +15,8 @@ import { ICredentialsDb, ICredentialsResponse, whereClause, + ResponseHelper, } from '..'; -import * as ResponseHelper from './ResponseHelper2'; // TODO: Workaround import { RESPONSE_ERROR_MESSAGES } from '../constants'; import { CredentialsEntity } from '../databases/entities/CredentialsEntity'; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index b5d2533f3d2c4..3518382d3fd2a 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -28,8 +28,6 @@ import type { User } from '../../../src/databases/entities/User'; import type { EndpointGroup, SmtpTestAccount } from './types'; import type { N8nApp } from '../../../src/UserManagement/Interfaces'; -export const isTestRun = process.argv[1].split('/').includes('jest'); // TODO: Phase out - /** * Initialize a test server. * From e89fdb06c4e82861cd867407c0dd31914eec6a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Wed, 2 Mar 2022 11:27:38 +0100 Subject: [PATCH 46/59] :zap: Refactor `isEmailSetup` --- packages/cli/src/UserManagement/UserManagementHelper.ts | 2 -- packages/cli/src/UserManagement/routes/users.ts | 9 +++------ packages/cli/test/integration/users.endpoints.test.ts | 9 --------- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index a2880b6bbc795..2ac9451ebef46 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 8031bacfb1c75..7ca147c3898ed 100644 --- a/packages/cli/src/UserManagement/routes/users.ts +++ b/packages/cli/src/UserManagement/routes/users.ts @@ -10,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'; @@ -423,6 +418,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/test/integration/users.endpoints.test.ts b/packages/cli/test/integration/users.endpoints.test.ts index f028270da9f6e..71025c2514e57 100644 --- a/packages/cli/test/integration/users.endpoints.test.ts +++ b/packages/cli/test/integration/users.endpoints.test.ts @@ -15,7 +15,6 @@ import { } from './shared/random'; import { CredentialsEntity } from '../../src/databases/entities/CredentialsEntity'; import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity'; -import * as UMHelper from '../../src/UserManagement/UserManagementHelper'; import * as utils from './shared/utils'; import * as testDb from './shared/testDb'; @@ -66,8 +65,6 @@ 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(async () => { @@ -454,8 +451,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); @@ -492,8 +487,6 @@ test('POST /users should fail with invalid inputs', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await 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 = [ @@ -517,8 +510,6 @@ test('POST /users should ignore an empty payload', async () => { const owner = await Db.collections.User!.findOneOrFail(); const authOwnerAgent = await 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([]); From 729cc1b2417fc21e63bf5398dcbaef3e67322597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 3 Mar 2022 13:39:48 +0100 Subject: [PATCH 47/59] :fire: Remove unneeded imports --- package-lock.json | 236 +++++++++++++++++++++++----------------------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e63ac2786cd7..47a4432fe0263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13759,6 +13759,15 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "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" + } + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -13794,6 +13803,21 @@ } } }, + "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", @@ -13856,6 +13880,58 @@ "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" + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -13890,6 +13966,12 @@ "slash": "^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 + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -13918,6 +14000,16 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "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" + } + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -13953,6 +14045,17 @@ } } }, + "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", @@ -13963,6 +14066,15 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" }, + "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", @@ -14056,6 +14168,12 @@ "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 } } }, @@ -23606,124 +23724,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 - }, - "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 - }, - "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" - } - }, - "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 - } - } - }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", From 1b2a49480389954c3024824333a52126afc9c128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 3 Mar 2022 15:07:06 +0100 Subject: [PATCH 48/59] :zap: Handle bootstrap connection errors --- .../cli/test/integration/shared/testDb.ts | 17 +++++++++--- packages/cli/test/setup.ts | 26 +++++++++++++++---- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index ae5c5eab8b150..c306a607e5137 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -34,7 +34,18 @@ export async function init() { } if (dbType === 'postgresdb') { - const bootstrapPostgres = await createConnection(getBootstrapPostgresOptions()); + 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 = `n8n_test_pg_${Date.now()}`; await bootstrapPostgres.query(`CREATE DATABASE ${testDbName};`); @@ -283,7 +294,7 @@ export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions * Generate options for a bootstrap Postgres connection, * to create and drop test Postgres databases. */ -export const getBootstrapPostgresOptions = (): ConnectionOptions => { +export const getBootstrapPostgresOptions = () => { const username = config.get('database.postgresdb.user'); const password = config.get('database.postgresdb.password'); const host = config.get('database.postgresdb.host'); @@ -299,7 +310,7 @@ export const getBootstrapPostgresOptions = (): ConnectionOptions => { username, password, schema, - }; + } as const; }; export const getPostgresOptions = ({ name }: { name: string }): ConnectionOptions => { diff --git a/packages/cli/test/setup.ts b/packages/cli/test/setup.ts index 00281229c5f42..df32738c6750b 100644 --- a/packages/cli/test/setup.ts +++ b/packages/cli/test/setup.ts @@ -1,9 +1,12 @@ -import { ConnectionOptions, createConnection } from 'typeorm'; +import { exec as callbackExec } from 'child_process'; +import { promisify } from 'util'; + import config = require('../config'); -import { exec } from 'child_process'; 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') { @@ -11,7 +14,20 @@ if (dbType === 'mysqldb') { const password = config.get('database.mysqldb.password'); const host = config.get('database.mysqldb.host'); - exec( - `echo "CREATE DATABASE IF NOT EXISTS ${BOOTSTRAP_MYSQL_CONNECTION_NAME}" | mysql -h ${host} -u ${username} -p${password}; USE ${BOOTSTRAP_MYSQL_CONNECTION_NAME}`, - ); + 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); + } + } + })(); } From 760d52cd743654899d784e838ff1c8f35131ac33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 3 Mar 2022 16:52:57 +0100 Subject: [PATCH 49/59] :fire: Remove unneeded imports --- packages/cli/src/api/credentials.api.ts | 2 +- packages/cli/test/integration/me.endpoints.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/api/credentials.api.ts b/packages/cli/src/api/credentials.api.ts index 306f9d930fa89..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'; diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 5e4e78efb340b..594f2d5e92360 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -18,7 +18,6 @@ import { } from './shared/random'; import * as testDb from './shared/testDb'; -const dbType = config.get('database.type'); let app: express.Application; let testDbName = ''; let globalOwnerRole: Role; From cf830ed0946a60db75ba9d2b476fb1536cc47bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 3 Mar 2022 19:25:18 +0100 Subject: [PATCH 50/59] :fire: Remove outdated comments --- .../cli/test/integration/me.endpoints.test.ts | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/cli/test/integration/me.endpoints.test.ts b/packages/cli/test/integration/me.endpoints.test.ts index 703704d727490..0da438d5c6895 100644 --- a/packages/cli/test/integration/me.endpoints.test.ts +++ b/packages/cli/test/integration/me.endpoints.test.ts @@ -1,21 +1,13 @@ import { hashSync, genSaltSync } from 'bcryptjs'; import express = require('express'); 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 { randomValidPassword, randomEmail, randomName, randomString } from './shared/random'; import * as testDb from './shared/testDb'; let app: express.Application; @@ -180,13 +172,6 @@ describe('Owner shell', () => { describe('Member', () => { beforeEach(async () => { - // await testDb.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( @@ -346,15 +331,6 @@ describe('Member', () => { 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); }); From 1a6a563bcca2883204498b9b3b16d36ec1cce5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 11:54:05 +0100 Subject: [PATCH 51/59] :pencil2: Fix typos --- package.json | 281 ++++++++++++++++-- package.json.lerna_backup | 28 ++ .../cli/test/integration/shared/testDb.ts | 4 +- 3 files changed, 285 insertions(+), 28 deletions(-) create mode 100644 package.json.lerna_backup diff --git a/package.json b/package.json index 10abc22a4c8de..c141e08df0047 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,257 @@ { - "name": "n8n", - "private": true, - "homepage": "https://n8n.io", - "scripts": { - "bootstrap": "lerna bootstrap --hoist --no-ci", - "build": "lerna exec npm run build", - "dev": "lerna exec npm run dev --parallel", - "clean:dist": "lerna exec -- rimraf ./dist", - "format": "lerna exec npm run format", - "lint": "lerna exec npm run lint", - "lintfix": "lerna exec npm run lintfix", - "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", - "start": "run-script-os", - "start:default": "cd packages/cli/bin && ./n8n", - "start:windows": "cd packages/cli/bin && n8n", - "test": "lerna run test", - "watch": "lerna run --parallel watch", - "webhook": "./packages/cli/bin/n8n webhook", - "worker": "./packages/cli/bin/n8n worker" - }, - "devDependencies": { - "lerna": "^3.13.1", - "rimraf": "^3.0.2", - "run-script-os": "^1.0.7" - }, - "postcss": {} + "name": "n8n", + "private": true, + "homepage": "https://n8n.io", + "devDependencies": { + "lerna": "^3.13.1", + "rimraf": "^3.0.2", + "run-script-os": "^1.0.7" + }, + "postcss": {}, + "dependencies": { + "@babel/core": "^7.14.6", + "@fontsource/open-sans": "^4.5.0", + "@fortawesome/fontawesome-svg-core": "^1.2.35", + "@fortawesome/free-solid-svg-icons": "^5.15.3", + "@fortawesome/vue-fontawesome": "^2.0.2", + "@kafkajs/confluent-schema-registry": "1.0.6", + "@oclif/command": "^1.5.18", + "@oclif/dev-cli": "^1.22.2", + "@oclif/errors": "^1.2.2", + "@rudderstack/rudder-sdk-node": "1.0.6", + "@storybook/addon-actions": "^6.3.6", + "@storybook/addon-essentials": "^6.3.6", + "@storybook/addon-links": "^6.3.6", + "@storybook/vue": "^6.3.6", + "@types/aws4": "^1.5.1", + "@types/basic-auth": "^1.1.2", + "@types/bcryptjs": "^2.4.2", + "@types/bull": "^3.3.10", + "@types/cheerio": "^0.22.15", + "@types/compression": "1.0.1", + "@types/connect-history-api-fallback": "^1.3.1", + "@types/convict": "^4.2.1", + "@types/cookie-parser": "^1.4.2", + "@types/copyfiles": "^2.1.1", + "@types/cron": "~1.7.1", + "@types/crypto-js": "^4.0.1", + "@types/dateformat": "^3.0.0", + "@types/dotenv": "^8.2.0", + "@types/eventsource": "^1.1.2", + "@types/express": "^4.17.6", + "@types/file-saver": "^2.0.1", + "@types/formidable": "^1.0.31", + "@types/gm": "^1.18.2", + "@types/imap-simple": "^4.2.0", + "@types/inquirer": "^6.5.0", + "@types/jest": "^27.4.0", + "@types/json-diff": "^0.5.1", + "@types/jsonwebtoken": "^8.5.2", + "@types/localtunnel": "^1.9.0", + "@types/lodash.camelcase": "^4.3.6", + "@types/lodash.get": "^4.4.6", + "@types/lodash.merge": "^4.6.6", + "@types/lodash.set": "^4.3.6", + "@types/lossless-json": "^1.0.0", + "@types/mailparser": "^2.7.3", + "@types/markdown-it": "^12.2.3", + "@types/mime-types": "^2.1.0", + "@types/moment-timezone": "^0.5.12", + "@types/mongodb": "^3.5.4", + "@types/mqtt": "^2.5.0", + "@types/mssql": "^6.0.2", + "@types/node": "14.17.27", + "@types/nodemailer": "^6.4.0", + "@types/open": "^6.1.0", + "@types/parseurl": "^1.3.1", + "@types/passport-jwt": "^3.0.6", + "@types/promise-ftp": "^1.3.4", + "@types/quill": "^2.0.1", + "@types/redis": "^2.8.11", + "@types/request-promise-native": "~1.0.15", + "@types/snowflake-sdk": "^1.5.1", + "@types/ssh2-sftp-client": "^5.1.0", + "@types/superagent": "4.1.13", + "@types/supertest": "^2.0.11", + "@types/tmp": "^0.2.0", + "@types/uuid": "^8.3.2", + "@types/validator": "^13.7.0", + "@types/vorpal": "^1.11.0", + "@types/xml2js": "^0.4.3", + "@typescript-eslint/eslint-plugin": "^4.29.0", + "@typescript-eslint/parser": "^4.29.0", + "@vue/cli-plugin-babel": "~4.5.0", + "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-plugin-typescript": "~4.5.6", + "@vue/cli-plugin-unit-jest": "~4.5.0", + "@vue/cli-service": "~4.5.0", + "@vue/eslint-config-prettier": "^6.0.0", + "@vue/eslint-config-standard": "^5.0.1", + "@vue/eslint-config-typescript": "^7.0.0", + "@vue/test-utils": "^1.0.3", + "amqplib": "^0.8.0", + "aws4": "^1.8.0", + "axios": "^0.21.1", + "babel-core": "7.0.0-bridge.0", + "babel-eslint": "^10.0.1", + "babel-loader": "^8.2.2", + "basic-auth": "^2.0.1", + "bcryptjs": "^2.4.3", + "body-parser": "^1.18.3", + "body-parser-xml": "^2.0.3", + "bull": "^3.19.0", + "callsites": "^3.1.0", + "change-case": "^4.1.1", + "cheerio": "1.0.0-rc.6", + "chokidar": "3.5.2", + "class-validator": "^0.13.1", + "client-oauth2": "^4.2.5", + "compression": "^1.7.4", + "concurrently": "^5.1.0", + "connect-history-api-fallback": "^1.6.0", + "convict": "^6.0.1", + "cookie-parser": "^1.4.6", + "copyfiles": "^2.1.1", + "core-js": "^3.6.5", + "cron": "~1.7.2", + "cross-env": "^7.0.2", + "crypto-js": "~4.1.1", + "csrf": "^3.1.0", + "dateformat": "^3.0.3", + "dotenv": "^8.0.0", + "element-ui": "~2.15.7", + "eslint": "^7.32.0", + "eslint-config-airbnb-typescript": "^12.3.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^3.4.0", + "eslint-plugin-vue": "^7.16.0", + "eventsource": "^1.0.7", + "express": "^4.16.4", + "fast-glob": "^3.2.5", + "fflate": "^0.7.0", + "file-saver": "^2.0.2", + "file-type": "^14.6.2", + "flatted": "^3.2.4", + "form-data": "^4.0.0", + "formidable": "^1.2.1", + "get-system-fonts": "^2.0.2", + "gm": "^1.23.1", + "google-timezones-json": "^1.0.2", + "gulp": "^4.0.0", + "gulp-autoprefixer": "^4.0.0", + "gulp-clean-css": "^4.3.0", + "gulp-dart-sass": "^1.0.2", + "iconv-lite": "^0.6.2", + "ics": "^2.27.0", + "imap-simple": "^4.3.0", + "inquirer": "^7.0.1", + "isbot": "^3.3.4", + "iso-639-1": "^2.1.3", + "jest": "^27.4.7", + "jquery": "^3.4.1", + "jshint": "^2.9.7", + "json-diff": "^0.5.4", + "jsonwebtoken": "^8.5.1", + "jsplumb": "2.15.4", + "jwks-rsa": "~1.12.1", + "kafkajs": "^1.14.0", + "localtunnel": "^2.0.0", + "lodash.camelcase": "^4.3.0", + "lodash.debounce": "^4.0.8", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "lodash.merge": "^4.6.2", + "lodash.set": "^4.3.2", + "lodash.unset": "^4.5.2", + "lossless-json": "^1.0.4", + "mailparser": "^3.2.0", + "markdown-it": "^12.3.2", + "markdown-it-emoji": "^2.0.0", + "markdown-it-link-attributes": "^4.0.0", + "markdown-it-task-lists": "^2.1.1", + "mime-types": "^2.1.27", + "moment": "2.29.1", + "moment-timezone": "^0.5.28", + "monaco-editor": "^0.29.1", + "monaco-editor-webpack-plugin": "^5.0.0", + "mongodb": "^3.6.9", + "mqtt": "4.2.6", + "mssql": "^6.2.0", + "mysql2": "~2.3.0", + "node-notifier": ">=8.0.1", + "node-ssh": "^12.0.0", + "nodelinter": "^0.1.9", + "nodemailer": "^6.5.0", + "nodemon": "^2.0.2", + "normalize-wheel": "^1.0.1", + "oauth-1.0a": "^2.2.6", + "open": "^7.0.0", + "p-cancelable": "^2.0.0", + "passport": "^0.5.0", + "passport-cookie": "^1.0.9", + "passport-jwt": "^4.0.0", + "pdf-parse": "^1.1.1", + "pg": "^8.3.0", + "pg-promise": "^10.5.8", + "prettier": "^2.3.2", + "prismjs": "^1.17.1", + "prom-client": "^13.1.0", + "promise-ftp": "^1.3.5", + "qs": "^6.10.1", + "quill": "^2.0.0-dev.3", + "quill-autoformat": "^0.1.1", + "redis": "^3.1.1", + "replace-in-file": "^6.0.0", + "request": "^2.88.2", + "request-promise-native": "^1.0.7", + "rhea": "^1.0.11", + "riot-tmpl": "^3.0.8", + "rss-parser": "^3.7.0", + "sass": "^1.26.5", + "sass-loader": "^8.0.2", + "simple-git": "^2.36.2", + "snowflake-sdk": "^1.5.3", + "source-map-support": "^0.5.9", + "sqlite3": "^5.0.2", + "sse-channel": "^3.1.1", + "ssh2-sftp-client": "^7.0.0", + "storybook-addon-designs": "^6.0.1", + "storybook-addon-themes": "^6.1.0", + "string-template-parser": "^1.2.6", + "supertest": "^6.2.2", + "timeago.js": "^4.0.2", + "tmp-promise": "^3.0.2", + "trim": ">=0.0.3", + "ts-jest": "^27.1.3", + "ts-node": "^8.9.1", + "tslib": "1.14.1", + "tslint": "^6.1.2", + "typeorm": "0.2.30", + "typescript": "~4.3.5", + "uuid": "^8.3.2", + "v-click-outside": "^3.1.2", + "validator": "13.7.0", + "vm2": "~3.9.5", + "vue": "^2.6.11", + "vue-agile": "^2.0.0", + "vue-class-component": "^7.2.3", + "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", + "vue-fragment": "^1.5.2", + "vue-i18n": "^8.26.7", + "vue-json-pretty": "1.7.1", + "vue-loader": "^15.9.7", + "vue-prism-editor": "^0.3.0", + "vue-property-decorator": "^9.1.2", + "vue-router": "^3.0.6", + "vue-template-compiler": "^2.6.11", + "vue-typed-mixins": "^0.2.0", + "vue2-boring-avatars": "0.3.4", + "vue2-touch-events": "^3.2.1", + "vuex": "^3.1.1", + "winston": "^3.3.3", + "xlsx": "^0.17.0", + "xml2js": "^0.4.23", + "xss": "^1.0.10" + } } diff --git a/package.json.lerna_backup b/package.json.lerna_backup new file mode 100644 index 0000000000000..10abc22a4c8de --- /dev/null +++ b/package.json.lerna_backup @@ -0,0 +1,28 @@ +{ + "name": "n8n", + "private": true, + "homepage": "https://n8n.io", + "scripts": { + "bootstrap": "lerna bootstrap --hoist --no-ci", + "build": "lerna exec npm run build", + "dev": "lerna exec npm run dev --parallel", + "clean:dist": "lerna exec -- rimraf ./dist", + "format": "lerna exec npm run format", + "lint": "lerna exec npm run lint", + "lintfix": "lerna exec npm run lintfix", + "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", + "start": "run-script-os", + "start:default": "cd packages/cli/bin && ./n8n", + "start:windows": "cd packages/cli/bin && n8n", + "test": "lerna run test", + "watch": "lerna run --parallel watch", + "webhook": "./packages/cli/bin/n8n webhook", + "worker": "./packages/cli/bin/n8n worker" + }, + "devDependencies": { + "lerna": "^3.13.1", + "rimraf": "^3.0.2", + "run-script-os": "^1.0.7" + }, + "postcss": {} +} diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 8d82bdb5e838e..3e1d14e925880 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -254,7 +254,7 @@ export function getAllRoles() { // ---------------------------------- /** - * Generate options to for an in-memory sqlite database connection, + * Generate options for an in-memory sqlite database connection, * one per test suite run. */ export const getSqliteOptions = ({ name }: { name: string }): ConnectionOptions => { @@ -342,7 +342,7 @@ export const getBootstrapMySqlOptions = (): ConnectionOptions => { }; /** - * Generate options to for a MySQL database connection, + * Generate options for a MySQL database connection, * one per test suite run. */ export const getMySqlOptions = ({ name }: { name: string }): ConnectionOptions => { From 7e352c83a468191b4accc01a1a25f2080d62f75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:03:17 +0100 Subject: [PATCH 52/59] :truck: Relocate `answersFormatter` --- packages/cli/src/databases/entities/User.ts | 11 ++--------- packages/cli/src/databases/utils/transformers.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/databases/entities/User.ts b/packages/cli/src/databases/entities/User.ts index f6f2d18d8b998..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,15 +93,7 @@ export class User { @Column({ type: resolveDataType('json') as ColumnOptions['type'], nullable: true, - transformer: { - to: (answers: object) => answers, - from: (answers: object | string) => { - // Postgres stores and returns string - return typeof answers === 'string' - ? (JSON.parse(answers) as IPersonalizationSurveyAnswers) - : answers; - }, - }, + transformer: answersFormatter, }) personalizationAnswers: IPersonalizationSurveyAnswers | null; 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; + }, +}; From 93f31b7a38c59b2b8c75dec2cc364ea12e186347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:20:21 +0100 Subject: [PATCH 53/59] :rewind: Undo package.json miscommit --- package.json | 281 ++++---------------------------------- package.json.lerna_backup | 28 ---- 2 files changed, 26 insertions(+), 283 deletions(-) delete mode 100644 package.json.lerna_backup diff --git a/package.json b/package.json index c141e08df0047..10abc22a4c8de 100644 --- a/package.json +++ b/package.json @@ -1,257 +1,28 @@ { - "name": "n8n", - "private": true, - "homepage": "https://n8n.io", - "devDependencies": { - "lerna": "^3.13.1", - "rimraf": "^3.0.2", - "run-script-os": "^1.0.7" - }, - "postcss": {}, - "dependencies": { - "@babel/core": "^7.14.6", - "@fontsource/open-sans": "^4.5.0", - "@fortawesome/fontawesome-svg-core": "^1.2.35", - "@fortawesome/free-solid-svg-icons": "^5.15.3", - "@fortawesome/vue-fontawesome": "^2.0.2", - "@kafkajs/confluent-schema-registry": "1.0.6", - "@oclif/command": "^1.5.18", - "@oclif/dev-cli": "^1.22.2", - "@oclif/errors": "^1.2.2", - "@rudderstack/rudder-sdk-node": "1.0.6", - "@storybook/addon-actions": "^6.3.6", - "@storybook/addon-essentials": "^6.3.6", - "@storybook/addon-links": "^6.3.6", - "@storybook/vue": "^6.3.6", - "@types/aws4": "^1.5.1", - "@types/basic-auth": "^1.1.2", - "@types/bcryptjs": "^2.4.2", - "@types/bull": "^3.3.10", - "@types/cheerio": "^0.22.15", - "@types/compression": "1.0.1", - "@types/connect-history-api-fallback": "^1.3.1", - "@types/convict": "^4.2.1", - "@types/cookie-parser": "^1.4.2", - "@types/copyfiles": "^2.1.1", - "@types/cron": "~1.7.1", - "@types/crypto-js": "^4.0.1", - "@types/dateformat": "^3.0.0", - "@types/dotenv": "^8.2.0", - "@types/eventsource": "^1.1.2", - "@types/express": "^4.17.6", - "@types/file-saver": "^2.0.1", - "@types/formidable": "^1.0.31", - "@types/gm": "^1.18.2", - "@types/imap-simple": "^4.2.0", - "@types/inquirer": "^6.5.0", - "@types/jest": "^27.4.0", - "@types/json-diff": "^0.5.1", - "@types/jsonwebtoken": "^8.5.2", - "@types/localtunnel": "^1.9.0", - "@types/lodash.camelcase": "^4.3.6", - "@types/lodash.get": "^4.4.6", - "@types/lodash.merge": "^4.6.6", - "@types/lodash.set": "^4.3.6", - "@types/lossless-json": "^1.0.0", - "@types/mailparser": "^2.7.3", - "@types/markdown-it": "^12.2.3", - "@types/mime-types": "^2.1.0", - "@types/moment-timezone": "^0.5.12", - "@types/mongodb": "^3.5.4", - "@types/mqtt": "^2.5.0", - "@types/mssql": "^6.0.2", - "@types/node": "14.17.27", - "@types/nodemailer": "^6.4.0", - "@types/open": "^6.1.0", - "@types/parseurl": "^1.3.1", - "@types/passport-jwt": "^3.0.6", - "@types/promise-ftp": "^1.3.4", - "@types/quill": "^2.0.1", - "@types/redis": "^2.8.11", - "@types/request-promise-native": "~1.0.15", - "@types/snowflake-sdk": "^1.5.1", - "@types/ssh2-sftp-client": "^5.1.0", - "@types/superagent": "4.1.13", - "@types/supertest": "^2.0.11", - "@types/tmp": "^0.2.0", - "@types/uuid": "^8.3.2", - "@types/validator": "^13.7.0", - "@types/vorpal": "^1.11.0", - "@types/xml2js": "^0.4.3", - "@typescript-eslint/eslint-plugin": "^4.29.0", - "@typescript-eslint/parser": "^4.29.0", - "@vue/cli-plugin-babel": "~4.5.0", - "@vue/cli-plugin-eslint": "~4.5.0", - "@vue/cli-plugin-typescript": "~4.5.6", - "@vue/cli-plugin-unit-jest": "~4.5.0", - "@vue/cli-service": "~4.5.0", - "@vue/eslint-config-prettier": "^6.0.0", - "@vue/eslint-config-standard": "^5.0.1", - "@vue/eslint-config-typescript": "^7.0.0", - "@vue/test-utils": "^1.0.3", - "amqplib": "^0.8.0", - "aws4": "^1.8.0", - "axios": "^0.21.1", - "babel-core": "7.0.0-bridge.0", - "babel-eslint": "^10.0.1", - "babel-loader": "^8.2.2", - "basic-auth": "^2.0.1", - "bcryptjs": "^2.4.3", - "body-parser": "^1.18.3", - "body-parser-xml": "^2.0.3", - "bull": "^3.19.0", - "callsites": "^3.1.0", - "change-case": "^4.1.1", - "cheerio": "1.0.0-rc.6", - "chokidar": "3.5.2", - "class-validator": "^0.13.1", - "client-oauth2": "^4.2.5", - "compression": "^1.7.4", - "concurrently": "^5.1.0", - "connect-history-api-fallback": "^1.6.0", - "convict": "^6.0.1", - "cookie-parser": "^1.4.6", - "copyfiles": "^2.1.1", - "core-js": "^3.6.5", - "cron": "~1.7.2", - "cross-env": "^7.0.2", - "crypto-js": "~4.1.1", - "csrf": "^3.1.0", - "dateformat": "^3.0.3", - "dotenv": "^8.0.0", - "element-ui": "~2.15.7", - "eslint": "^7.32.0", - "eslint-config-airbnb-typescript": "^12.3.1", - "eslint-config-prettier": "^8.3.0", - "eslint-plugin-import": "^2.23.4", - "eslint-plugin-prettier": "^3.4.0", - "eslint-plugin-vue": "^7.16.0", - "eventsource": "^1.0.7", - "express": "^4.16.4", - "fast-glob": "^3.2.5", - "fflate": "^0.7.0", - "file-saver": "^2.0.2", - "file-type": "^14.6.2", - "flatted": "^3.2.4", - "form-data": "^4.0.0", - "formidable": "^1.2.1", - "get-system-fonts": "^2.0.2", - "gm": "^1.23.1", - "google-timezones-json": "^1.0.2", - "gulp": "^4.0.0", - "gulp-autoprefixer": "^4.0.0", - "gulp-clean-css": "^4.3.0", - "gulp-dart-sass": "^1.0.2", - "iconv-lite": "^0.6.2", - "ics": "^2.27.0", - "imap-simple": "^4.3.0", - "inquirer": "^7.0.1", - "isbot": "^3.3.4", - "iso-639-1": "^2.1.3", - "jest": "^27.4.7", - "jquery": "^3.4.1", - "jshint": "^2.9.7", - "json-diff": "^0.5.4", - "jsonwebtoken": "^8.5.1", - "jsplumb": "2.15.4", - "jwks-rsa": "~1.12.1", - "kafkajs": "^1.14.0", - "localtunnel": "^2.0.0", - "lodash.camelcase": "^4.3.0", - "lodash.debounce": "^4.0.8", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "lodash.merge": "^4.6.2", - "lodash.set": "^4.3.2", - "lodash.unset": "^4.5.2", - "lossless-json": "^1.0.4", - "mailparser": "^3.2.0", - "markdown-it": "^12.3.2", - "markdown-it-emoji": "^2.0.0", - "markdown-it-link-attributes": "^4.0.0", - "markdown-it-task-lists": "^2.1.1", - "mime-types": "^2.1.27", - "moment": "2.29.1", - "moment-timezone": "^0.5.28", - "monaco-editor": "^0.29.1", - "monaco-editor-webpack-plugin": "^5.0.0", - "mongodb": "^3.6.9", - "mqtt": "4.2.6", - "mssql": "^6.2.0", - "mysql2": "~2.3.0", - "node-notifier": ">=8.0.1", - "node-ssh": "^12.0.0", - "nodelinter": "^0.1.9", - "nodemailer": "^6.5.0", - "nodemon": "^2.0.2", - "normalize-wheel": "^1.0.1", - "oauth-1.0a": "^2.2.6", - "open": "^7.0.0", - "p-cancelable": "^2.0.0", - "passport": "^0.5.0", - "passport-cookie": "^1.0.9", - "passport-jwt": "^4.0.0", - "pdf-parse": "^1.1.1", - "pg": "^8.3.0", - "pg-promise": "^10.5.8", - "prettier": "^2.3.2", - "prismjs": "^1.17.1", - "prom-client": "^13.1.0", - "promise-ftp": "^1.3.5", - "qs": "^6.10.1", - "quill": "^2.0.0-dev.3", - "quill-autoformat": "^0.1.1", - "redis": "^3.1.1", - "replace-in-file": "^6.0.0", - "request": "^2.88.2", - "request-promise-native": "^1.0.7", - "rhea": "^1.0.11", - "riot-tmpl": "^3.0.8", - "rss-parser": "^3.7.0", - "sass": "^1.26.5", - "sass-loader": "^8.0.2", - "simple-git": "^2.36.2", - "snowflake-sdk": "^1.5.3", - "source-map-support": "^0.5.9", - "sqlite3": "^5.0.2", - "sse-channel": "^3.1.1", - "ssh2-sftp-client": "^7.0.0", - "storybook-addon-designs": "^6.0.1", - "storybook-addon-themes": "^6.1.0", - "string-template-parser": "^1.2.6", - "supertest": "^6.2.2", - "timeago.js": "^4.0.2", - "tmp-promise": "^3.0.2", - "trim": ">=0.0.3", - "ts-jest": "^27.1.3", - "ts-node": "^8.9.1", - "tslib": "1.14.1", - "tslint": "^6.1.2", - "typeorm": "0.2.30", - "typescript": "~4.3.5", - "uuid": "^8.3.2", - "v-click-outside": "^3.1.2", - "validator": "13.7.0", - "vm2": "~3.9.5", - "vue": "^2.6.11", - "vue-agile": "^2.0.0", - "vue-class-component": "^7.2.3", - "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", - "vue-fragment": "^1.5.2", - "vue-i18n": "^8.26.7", - "vue-json-pretty": "1.7.1", - "vue-loader": "^15.9.7", - "vue-prism-editor": "^0.3.0", - "vue-property-decorator": "^9.1.2", - "vue-router": "^3.0.6", - "vue-template-compiler": "^2.6.11", - "vue-typed-mixins": "^0.2.0", - "vue2-boring-avatars": "0.3.4", - "vue2-touch-events": "^3.2.1", - "vuex": "^3.1.1", - "winston": "^3.3.3", - "xlsx": "^0.17.0", - "xml2js": "^0.4.23", - "xss": "^1.0.10" - } + "name": "n8n", + "private": true, + "homepage": "https://n8n.io", + "scripts": { + "bootstrap": "lerna bootstrap --hoist --no-ci", + "build": "lerna exec npm run build", + "dev": "lerna exec npm run dev --parallel", + "clean:dist": "lerna exec -- rimraf ./dist", + "format": "lerna exec npm run format", + "lint": "lerna exec npm run lint", + "lintfix": "lerna exec npm run lintfix", + "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", + "start": "run-script-os", + "start:default": "cd packages/cli/bin && ./n8n", + "start:windows": "cd packages/cli/bin && n8n", + "test": "lerna run test", + "watch": "lerna run --parallel watch", + "webhook": "./packages/cli/bin/n8n webhook", + "worker": "./packages/cli/bin/n8n worker" + }, + "devDependencies": { + "lerna": "^3.13.1", + "rimraf": "^3.0.2", + "run-script-os": "^1.0.7" + }, + "postcss": {} } diff --git a/package.json.lerna_backup b/package.json.lerna_backup deleted file mode 100644 index 10abc22a4c8de..0000000000000 --- a/package.json.lerna_backup +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "n8n", - "private": true, - "homepage": "https://n8n.io", - "scripts": { - "bootstrap": "lerna bootstrap --hoist --no-ci", - "build": "lerna exec npm run build", - "dev": "lerna exec npm run dev --parallel", - "clean:dist": "lerna exec -- rimraf ./dist", - "format": "lerna exec npm run format", - "lint": "lerna exec npm run lint", - "lintfix": "lerna exec npm run lintfix", - "optimize-svg": "find ./packages -name '*.svg' ! -name 'pipedrive.svg' -print0 | xargs -0 -P16 -L20 npx svgo", - "start": "run-script-os", - "start:default": "cd packages/cli/bin && ./n8n", - "start:windows": "cd packages/cli/bin && n8n", - "test": "lerna run test", - "watch": "lerna run --parallel watch", - "webhook": "./packages/cli/bin/n8n webhook", - "worker": "./packages/cli/bin/n8n worker" - }, - "devDependencies": { - "lerna": "^3.13.1", - "rimraf": "^3.0.2", - "run-script-os": "^1.0.7" - }, - "postcss": {} -} From d11553145d439a3a0f07d81b29c11961d810bc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:20:37 +0100 Subject: [PATCH 54/59] :fire: Remove unneeded import --- packages/cli/test/integration/shared/testDb.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 3e1d14e925880..99c9053c93767 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -1,4 +1,3 @@ -import { v4 as uuid } from 'uuid'; import { createConnection, getConnection, ConnectionOptions } from 'typeorm'; import { Credentials, UserSettings } from 'n8n-core'; From 59e65e7365d813a9b5897e5e17aaebc25d3de93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:32:59 +0100 Subject: [PATCH 55/59] :zap: Refactor test DB prefixing --- packages/cli/test/integration/shared/testDb.ts | 4 ++-- packages/cli/test/teardown.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 99c9053c93767..6eac5c92b8b2c 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -46,7 +46,7 @@ export async function init() { process.exit(1); } - const testDbName = `n8n_test_pg_${Date.now()}`; + const testDbName = `pg_${Date.now()}_n8n_test`; await bootstrapPostgres.query(`CREATE DATABASE ${testDbName};`); await Db.init(getPostgresOptions({ name: testDbName })); @@ -57,7 +57,7 @@ export async function init() { if (dbType === 'mysqldb') { const bootstrapMysql = await createConnection(getBootstrapMySqlOptions()); - const testDbName = `n8n_test_mysql_${Date.now()}`; + const testDbName = `mysql_${Date.now()}_n8n_test`; await bootstrapMysql.query(`CREATE DATABASE ${testDbName};`); await Db.init(getMySqlOptions({ name: testDbName })); diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index 1d0acaed34a79..70cbc994cde3f 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -15,7 +15,7 @@ export default async () => { ); const promises = results - .filter(({ db_name }) => db_name.startsWith('n8n_test_pg_')) + .filter(({ db_name }) => db_name.startsWith('pg_') && db_name.endsWith('_n8n_test')) .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); await Promise.all(promises); From 320124990f00f34465071cea452f94da9c1d250a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:40:20 +0100 Subject: [PATCH 56/59] :zap: Add no-leftover check to MySQL --- packages/cli/test/teardown.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/teardown.ts b/packages/cli/test/teardown.ts index 70cbc994cde3f..95fb810e394ca 100644 --- a/packages/cli/test/teardown.ts +++ b/packages/cli/test/teardown.ts @@ -2,25 +2,26 @@ import { createConnection } from 'typeorm'; import config = require('../config'); import { exec } from 'child_process'; import { DatabaseType } from '../src'; -import { getBootstrapPostgresOptions } from './integration/shared/testDb'; +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 bootstrap = await createConnection(getBootstrapPostgresOptions()); + const bootstrapPostgres = await createConnection(getBootstrapPostgresOptions()); - const results: { db_name: string }[] = await bootstrap.query( + const results: { db_name: string }[] = await bootstrapPostgres.query( 'SELECT datname as db_name FROM pg_database;', ); const promises = results - .filter(({ db_name }) => db_name.startsWith('pg_') && db_name.endsWith('_n8n_test')) - .map(({ db_name }) => bootstrap.query(`DROP DATABASE ${db_name};`)); + .filter(({ db_name: dbName }) => dbName.startsWith('pg_') && dbName.endsWith('_n8n_test')) + .map(({ db_name: dbName }) => bootstrapPostgres.query(`DROP DATABASE ${dbName};`)); await Promise.all(promises); - bootstrap.close(); + bootstrapPostgres.close(); } if (dbType === 'mysqldb') { @@ -28,6 +29,20 @@ export default async () => { const password = config.get('database.mysqldb.password'); const host = config.get('database.mysqldb.host'); - exec(`echo "DROP DATABASE n8n_bs_mysql" | mysql -h ${host} -u ${user} -p${password}`); + 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}`, + ); } }; From 10073640c1895419eaac2dcb2f1b38a0e750d05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:40:27 +0100 Subject: [PATCH 57/59] :package: Update package.json --- package-lock.json | 348 +++++++++++++++++++++++----------------------- 1 file changed, 174 insertions(+), 174 deletions(-) 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", From d1faf142c297aaea17623915773d2855f2e88213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:45:09 +0100 Subject: [PATCH 58/59] :zap: Autoincrement on simulated MySQL truncation --- .../cli/test/integration/shared/testDb.ts | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 6eac5c92b8b2c..1d254fce2fad5 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -111,20 +111,20 @@ export async function truncate(entities: Array, test return testDb.query('PRAGMA foreign_keys=ON'); } - if (dbType === 'postgresdb') { - 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', - }; + 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( @@ -139,6 +139,7 @@ export async function truncate(entities: Array, test await Promise.all( entities.map(async (entity) => { await Db.collections[entity]!.delete({}); + await getConnection(testDbName).query(`ALTER TABLE ${map[entity]} AUTO_INCREMENT = 1;`); }), ); } From 32085e89d1cd90925938b225ddba75be5d7b4e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 4 Mar 2022 12:47:23 +0100 Subject: [PATCH 59/59] :fire: Remove debugging queries --- .../mysqldb/migrations/1636626154933-CreateUserManagement.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts index ef33e02962d30..ca4e4f2f61e51 100644 --- a/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts +++ b/packages/cli/src/databases/mysqldb/migrations/1636626154933-CreateUserManagement.ts @@ -9,9 +9,6 @@ export class CreateUserManagement1636626154933 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const tablePrefix = config.get('database.tablePrefix'); - await queryRunner.query('SET @@GLOBAL.FOREIGN_KEY_CHECKS=1;'); - await queryRunner.query('SET @@SESSION.FOREIGN_KEY_CHECKS=1;'); - await queryRunner.query( `CREATE TABLE ${tablePrefix}role ( \`id\` int NOT NULL AUTO_INCREMENT,