diff --git a/packages/cli/databases/sqlite/database.sqlite b/packages/cli/databases/sqlite/database.sqlite new file mode 100644 index 0000000000..85193ea151 Binary files /dev/null and b/packages/cli/databases/sqlite/database.sqlite differ diff --git a/packages/cli/migrations/ormconfig.ts b/packages/cli/migrations/ormconfig.ts new file mode 100644 index 0000000000..2a0cda0d9c --- /dev/null +++ b/packages/cli/migrations/ormconfig.ts @@ -0,0 +1,108 @@ +import {MongoDb, SQLite, MySQLDb, PostgresDb} from '../src/databases/index'; + +module.exports = [ + { + "name": "sqlite", + "type": "sqlite", + "logging": true, + "entities": Object.values(SQLite), + "database": "./packages/cli/database.sqlite", + "migrations": [ + "./src/databases/sqlite/migrations/*.ts" + ], + "subscribers": [ + "./src/databases/sqlite/subscribers/*.ts" + ], + "cli": { + "entitiesDir": "./src/databases/sqlite", + "migrationsDir": "./src/databases/sqlite/migrations", + "subscribersDir": "./src/databases/sqlite/subscribers" + } + }, + { + "name": "mongodb", + "type": "mongodb", + "logging": false, + "entities": Object.values(MongoDb), + "url": "mongodb://root:example@localhost:27017/n8n", + "authSource": 'admin', + "migrations": [ + "./src/databases/mongodb/migrations/*.ts" + ], + "subscribers": [ + "src/subscriber/**/*.ts" + ], + "cli": { + "entitiesDir": "./src/databases/mongodb", + "migrationsDir": "./src/databases/mongodb/Migrations", + "subscribersDir": "./src/databases/mongodb/Subscribers" + } + }, + { + "name": "postgres", + "type": "postgres", + "logging": false, + "host": "localhost", + "username": "postgres", + "password": "docker", + "port": 5432, + "database": "postgres", + "schema": "public", + "entities": Object.values(PostgresDb), + "migrations": [ + "./src/databases/postgresdb/migrations/*.ts" + ], + "subscribers": [ + "src/subscriber/**/*.ts" + ], + "cli": { + "entitiesDir": "./src/databases/postgresdb", + "migrationsDir": "./src/databases/postgresdb/migrations", + "subscribersDir": "./src/databases/postgresdb/subscribers" + } + }, + { + "name": "mysql", + "type": "mysql", + "database": "n8n", + "username": "root", + "password": "password", + "host": "localhost", + "port": "3308", + "logging": false, + "entities": Object.values(MySQLDb), + "migrations": [ + "./src/databases/mysqldb/migrations/*.ts" + ], + "subscribers": [ + "src/subscriber/**/*.ts" + ], + "cli": { + "entitiesDir": "./src/databases/mysqldb", + "migrationsDir": "./src/databases/mysqldb/migrations", + "subscribersDir": "./src/databases/mysqldb/Subscribers" + } + }, + { + "name": "mariadb", + "type": "mariadb", + "database": "n8n", + "username": "root", + "password": "password", + "host": "localhost", + "port": "3308", + "logging": false, + "entities": Object.values(MySQLDb), + "migrations": [ + "./src/databases/mysqldb/migrations/*.ts" + ], + "subscribers": [ + "src/subscriber/**/*.ts" + ], + "cli": { + "entitiesDir": "./src/databases/mysqldb", + "migrationsDir": "./src/databases/mysqldb/migrations", + "subscribersDir": "./src/databases/mysqldb/Subscribers" + } + }, +]; \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index bb18b70179..a6ddb40a44 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -28,7 +28,8 @@ "start:windows": "cd bin && n8n", "test": "jest", "tslint": "tslint -p tsconfig.json -c tslint.json", - "watch": "tsc --watch" + "watch": "tsc --watch", + "typeorm": "ts-node ./node_modules/typeorm/cli.js" }, "bin": { "n8n": "./bin/n8n" @@ -70,8 +71,9 @@ "p-cancelable": "^2.0.0", "run-script-os": "^1.0.7", "ts-jest": "^24.0.2", - "tslint": "^5.17.0", - "typescript": "~3.7.4" + "tslint": "^6.1.2", + "typescript": "~3.7.4", + "ts-node": "^8.9.1" }, "dependencies": { "@oclif/command": "^1.5.18", @@ -102,9 +104,9 @@ "open": "^7.0.0", "pg": "^7.11.0", "request-promise-native": "^1.0.7", - "sqlite3": "^4.0.6", + "sqlite3": "^4.2.0", "sse-channel": "^3.1.1", - "typeorm": "^0.2.16" + "typeorm": "^0.2.24" }, "jest": { "transform": { diff --git a/packages/cli/packages/cli/database.sqlite b/packages/cli/packages/cli/database.sqlite new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 57cab86179..9fef6ed243 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -315,6 +315,7 @@ export class ActiveWorkflowRunner { return ((workflow: Workflow, node: INode) => { const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode); returnFunctions.emit = (data: INodeExecutionData[][]): void => { + WorkflowHelpers.saveStaticData(workflow); this.runWorkflow(workflowData, node, data, additionalData, mode); }; return returnFunctions; diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index ed24baabd2..089119e737 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -1,7 +1,7 @@ import { + DatabaseType, GenericHelpers, IDatabaseCollections, - DatabaseType, } from './'; import { @@ -16,9 +16,9 @@ import { import { MongoDb, + MySQLDb, PostgresDb, SQLite, - MySQLDb, } from './databases'; export let collections: IDatabaseCollections = { @@ -27,16 +27,27 @@ export let collections: IDatabaseCollections = { Workflow: null, }; +import { + InitialMigration1587669153312 +} from './databases/postgresdb/migrations'; + +import { + InitialMigration1588157391238 +} from './databases/mysqldb/migrations'; + +import { + InitialMigration1588102412422 +} from './databases/sqlite/migrations'; + import * as path from 'path'; -export async function init(synchronize?: boolean): Promise { +export async function init(): Promise { const dbType = await GenericHelpers.getConfigValue('database.type') as DatabaseType; const n8nFolder = UserSettings.getUserN8nFolderPath(); let entities; let connectionOptions: ConnectionOptions; - let dbNotExistError: string | undefined; switch (dbType) { case 'mongodb': entities = MongoDb; @@ -49,7 +60,6 @@ export async function init(synchronize?: boolean): Promise break; case 'postgresdb': - dbNotExistError = 'does not exist'; entities = PostgresDb; connectionOptions = { type: 'postgres', @@ -60,12 +70,13 @@ export async function init(synchronize?: boolean): Promise port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number, username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string, schema: await GenericHelpers.getConfigValue('database.postgresdb.schema') as string, + migrations: [InitialMigration1587669153312], + migrationsRun: true }; break; case 'mariadb': case 'mysqldb': - dbNotExistError = 'does not exist'; entities = MySQLDb; connectionOptions = { type: dbType === 'mysqldb' ? 'mysql' : 'mariadb', @@ -75,16 +86,19 @@ export async function init(synchronize?: boolean): Promise 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: [InitialMigration1588157391238], + migrationsRun: true }; break; case 'sqlite': - dbNotExistError = 'no such table:'; entities = SQLite; connectionOptions = { type: 'sqlite', - database: path.join(n8nFolder, 'database.sqlite'), + database: path.join(n8nFolder, 'database.sqlite'), entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string, + migrations: [InitialMigration1588102412422], + migrationsRun: true, }; break; @@ -94,38 +108,19 @@ export async function init(synchronize?: boolean): Promise Object.assign(connectionOptions, { entities: Object.values(entities), - synchronize: synchronize === true || process.env['NODE_ENV'] !== 'production', - logging: false + synchronize: false, + logging: false, }); const connection = await createConnection(connectionOptions); - // TODO: Fix that properly - // @ts-ignore + await connection.runMigrations({ + transaction: 'none', + }); + collections.Credentials = getRepository(entities.CredentialsEntity); - // @ts-ignore collections.Execution = getRepository(entities.ExecutionEntity); - // @ts-ignore collections.Workflow = getRepository(entities.WorkflowEntity); - // Make sure that database did already get initialized - try { - // Try a simple query, if it fails it is normally a sign that - // database did not get initialized - await collections.Workflow!.findOne({ id: 1 }); - } catch (error) { - // If query errors and the problem is that the database does not exist - // run the init again with "synchronize: true" - if (dbNotExistError !== undefined && error.message.includes(dbNotExistError)) { - // Disconnect before we try to connect again - if (connection.isConnected) { - await connection.close(); - } - - return init(true); - } - throw error; - } - return collections; } diff --git a/packages/cli/src/databases/mongodb/Migrations/1587563438936-InitialMigration.ts b/packages/cli/src/databases/mongodb/Migrations/1587563438936-InitialMigration.ts new file mode 100644 index 0000000000..df2785c350 --- /dev/null +++ b/packages/cli/src/databases/mongodb/Migrations/1587563438936-InitialMigration.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialMigration1587563438936 implements MigrationInterface { + + async up(queryRunner: QueryRunner): Promise { + } + + async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/packages/cli/src/databases/mongodb/Migrations/index.ts b/packages/cli/src/databases/mongodb/Migrations/index.ts new file mode 100644 index 0000000000..a60bdc7cf8 --- /dev/null +++ b/packages/cli/src/databases/mongodb/Migrations/index.ts @@ -0,0 +1 @@ +export * from './1587563438936-InitialMigration'; diff --git a/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts b/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts new file mode 100644 index 0000000000..a156dd949e --- /dev/null +++ b/packages/cli/src/databases/mysqldb/migrations/1588157391238-InitialMigration.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitialMigration1588157391238 implements MigrationInterface { + name = 'InitialMigration1588157391238'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE TABLE IF NOT EXISTS `credentials_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `data` text NOT NULL, `type` varchar(32) NOT NULL, `nodesAccess` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, INDEX `IDX_07fde106c0b471d8cc80a64fc8` (`type`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); + await queryRunner.query('CREATE TABLE IF NOT EXISTS `execution_entity` (`id` int NOT NULL AUTO_INCREMENT, `data` text NOT NULL, `finished` tinyint NOT NULL, `mode` varchar(255) NOT NULL, `retryOf` varchar(255) NULL, `retrySuccessId` varchar(255) NULL, `startedAt` datetime NOT NULL, `stoppedAt` datetime NOT NULL, `workflowData` json NOT NULL, `workflowId` varchar(255) NULL, INDEX `IDX_c4d999a5e90784e8caccf5589d` (`workflowId`), PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); + await queryRunner.query('CREATE TABLE IF NOT EXISTS`workflow_entity` (`id` int NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL, `active` tinyint NOT NULL, `nodes` json NOT NULL, `connections` json NOT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `settings` json NULL, `staticData` json NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB', undefined); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TABLE `workflow_entity`', undefined); + await queryRunner.query('DROP INDEX `IDX_c4d999a5e90784e8caccf5589d` ON `execution_entity`', undefined); + await queryRunner.query('DROP TABLE `execution_entity`', undefined); + await queryRunner.query('DROP INDEX `IDX_07fde106c0b471d8cc80a64fc8` ON `credentials_entity`', undefined); + await queryRunner.query('DROP TABLE `credentials_entity`', undefined); + } + +} diff --git a/packages/cli/src/databases/mysqldb/migrations/index.ts b/packages/cli/src/databases/mysqldb/migrations/index.ts new file mode 100644 index 0000000000..ac2dcab467 --- /dev/null +++ b/packages/cli/src/databases/mysqldb/migrations/index.ts @@ -0,0 +1 @@ +export * from './1588157391238-InitialMigration'; \ No newline at end of file diff --git a/packages/cli/src/databases/postgresdb/CredentialsEntity.ts b/packages/cli/src/databases/postgresdb/CredentialsEntity.ts index e3ef375254..cddaf7559f 100644 --- a/packages/cli/src/databases/postgresdb/CredentialsEntity.ts +++ b/packages/cli/src/databases/postgresdb/CredentialsEntity.ts @@ -41,4 +41,5 @@ export class CredentialsEntity implements ICredentialsDb { @Column('timestamp') updatedAt: Date; + } diff --git a/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts new file mode 100644 index 0000000000..fe050c9c3d --- /dev/null +++ b/packages/cli/src/databases/postgresdb/migrations/1587669153312-InitialMigration.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InitialMigration1587669153312 implements MigrationInterface { + name = 'InitialMigration1587669153312'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE IF NOT EXISTS credentials_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "data" text NOT NULL, "type" character varying(32) NOT NULL, "nodesAccess" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, CONSTRAINT PK_814c3d3c36e8a27fa8edb761b0e PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_07fde106c0b471d8cc80a64fc8 ON credentials_entity (type) `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS execution_entity ("id" SERIAL NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" character varying NOT NULL, "retryOf" character varying, "retrySuccessId" character varying, "startedAt" TIMESTAMP NOT NULL, "stoppedAt" TIMESTAMP NOT NULL, "workflowData" json NOT NULL, "workflowId" character varying, CONSTRAINT PK_e3e63bbf986767844bbe1166d4e PRIMARY KEY ("id"))`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS IDX_c4d999a5e90784e8caccf5589d ON execution_entity ("workflowId") `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS workflow_entity ("id" SERIAL NOT NULL, "name" character varying(128) NOT NULL, "active" boolean NOT NULL, "nodes" json NOT NULL, "connections" json NOT NULL, "createdAt" TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP NOT NULL, "settings" json, "staticData" json, CONSTRAINT PK_eded7d72664448da7745d551207 PRIMARY KEY ("id"))`, undefined); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE workflow_entity`, undefined); + await queryRunner.query(`DROP INDEX IDX_c4d999a5e90784e8caccf5589d`, undefined); + await queryRunner.query(`DROP TABLE execution_entity`, undefined); + await queryRunner.query(`DROP INDEX IDX_07fde106c0b471d8cc80a64fc8`, undefined); + await queryRunner.query(`DROP TABLE credentials_entity`, undefined); + } + +} diff --git a/packages/cli/src/databases/postgresdb/migrations/index.ts b/packages/cli/src/databases/postgresdb/migrations/index.ts new file mode 100644 index 0000000000..5bb6551492 --- /dev/null +++ b/packages/cli/src/databases/postgresdb/migrations/index.ts @@ -0,0 +1 @@ +export * from './1587669153312-InitialMigration'; diff --git a/packages/cli/src/databases/sqlite/index.ts b/packages/cli/src/databases/sqlite/index.ts index 164d67fd0c..2c7d6e25e9 100644 --- a/packages/cli/src/databases/sqlite/index.ts +++ b/packages/cli/src/databases/sqlite/index.ts @@ -1,3 +1,4 @@ export * from './CredentialsEntity'; export * from './ExecutionEntity'; export * from './WorkflowEntity'; + diff --git a/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts b/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts new file mode 100644 index 0000000000..038c24edaa --- /dev/null +++ b/packages/cli/src/databases/sqlite/migrations/1588102412422-InitialMigration.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InitialMigration1588102412422 implements MigrationInterface { + name = 'InitialMigration1588102412422'; + + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "credentials_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "data" text NOT NULL, "type" varchar(32) NOT NULL, "nodesAccess" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL)`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_07fde106c0b471d8cc80a64fc8" ON "credentials_entity" ("type") `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "execution_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "data" text NOT NULL, "finished" boolean NOT NULL, "mode" varchar NOT NULL, "retryOf" varchar, "retrySuccessId" varchar, "startedAt" datetime NOT NULL, "stoppedAt" datetime NOT NULL, "workflowData" text NOT NULL, "workflowId" varchar)`, undefined); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_c4d999a5e90784e8caccf5589d" ON "execution_entity" ("workflowId") `, undefined); + await queryRunner.query(`CREATE TABLE IF NOT EXISTS "workflow_entity" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(128) NOT NULL, "active" boolean NOT NULL, "nodes" text NOT NULL, "connections" text NOT NULL, "createdAt" datetime NOT NULL, "updatedAt" datetime NOT NULL, "settings" text, "staticData" text)`, undefined); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "workflow_entity"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_c4d999a5e90784e8caccf5589d"`, undefined); + await queryRunner.query(`DROP TABLE "execution_entity"`, undefined); + await queryRunner.query(`DROP INDEX "IDX_07fde106c0b471d8cc80a64fc8"`, undefined); + await queryRunner.query(`DROP TABLE "credentials_entity"`, undefined); + } + +} diff --git a/packages/cli/src/databases/sqlite/migrations/index.ts b/packages/cli/src/databases/sqlite/migrations/index.ts new file mode 100644 index 0000000000..8d9a0a0b16 --- /dev/null +++ b/packages/cli/src/databases/sqlite/migrations/index.ts @@ -0,0 +1 @@ +export * from './1588102412422-InitialMigration'; \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index b58064344b..e7df6a4e26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -36,7 +36,7 @@ "jest": "^24.9.0", "source-map-support": "^0.5.9", "ts-jest": "^24.0.2", - "tslint": "^5.17.0", + "tslint": "6.1.2", "typescript": "~3.7.4" }, "dependencies": { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index f25c05530f..0fdcb6e756 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -32,7 +32,7 @@ "@types/dateformat": "^3.0.0", "@types/express": "^4.17.6", "@types/file-saver": "^2.0.1", - "@types/jest": "^24.0.18", + "@types/jest": "^25.2.1", "@types/lodash.get": "^4.4.6", "@types/lodash.set": "^4.3.6", "@types/node": "12.12.22", @@ -71,8 +71,8 @@ "quill-autoformat": "^0.1.1", "sass-loader": "^8.0.0", "string-template-parser": "^1.2.6", - "ts-jest": "^24.0.2", - "tslint": "^5.17.0", + "ts-jest": "^25.4.0", + "tslint": "^6.1.2", "typescript": "~3.7.4", "vue": "^2.6.9", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", diff --git a/packages/node-dev/commands/new.ts b/packages/node-dev/commands/new.ts index 5f721f0f16..9168e470d4 100644 --- a/packages/node-dev/commands/new.ts +++ b/packages/node-dev/commands/new.ts @@ -140,7 +140,7 @@ export class New extends Command { // in the correct way const replaceValues = { ClassNameReplace: changeCase.pascalCase(nodeName), - DisplayNameReplace: changeCase.titleCase(nodeName), + DisplayNameReplace: changeCase.capitalCase(nodeName), N8nNameReplace: changeCase.camelCase(nodeName), NodeDescriptionReplace: additionalAnswers.description, }; diff --git a/packages/node-dev/package.json b/packages/node-dev/package.json index d36ccac518..31312ffcab 100644 --- a/packages/node-dev/package.json +++ b/packages/node-dev/package.json @@ -55,12 +55,12 @@ "@oclif/errors": "^1.2.2", "@types/express": "^4.16.1", "@types/node": "^10.10.1", - "change-case": "^3.1.0", + "change-case": "^4.1.1", "copyfiles": "^2.1.1", "inquirer": "^7.0.0", "n8n-core": "^0.21.0", "n8n-workflow": "^0.20.0", - "replace-in-file": "^4.1.0", + "replace-in-file": "^6.0.0", "request": "^2.88.2", "tmp-promise": "^2.0.2", "typescript": "~3.7.4" diff --git a/packages/node-dev/src/Create.ts b/packages/node-dev/src/Create.ts index 14eeff015b..7bec6f2159 100644 --- a/packages/node-dev/src/Create.ts +++ b/packages/node-dev/src/Create.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; -import replaceInFile, { ReplaceInFileConfig } from 'replace-in-file'; +import {replaceInFile, ReplaceInFileConfig } from 'replace-in-file'; const { promisify } = require('util'); const fsCopyFile = promisify(fs.copyFile); diff --git a/packages/nodes-base/nodes/Bannerbear/Bannerbear.node.ts b/packages/nodes-base/nodes/Bannerbear/Bannerbear.node.ts index 8c0069c4a2..ea1ac20289 100644 --- a/packages/nodes-base/nodes/Bannerbear/Bannerbear.node.ts +++ b/packages/nodes-base/nodes/Bannerbear/Bannerbear.node.ts @@ -76,7 +76,7 @@ export class Bannerbear implements INodeType { methods = { loadOptions: { - // Get all the available escalation policies to display them to user so that he can + // Get all the available templates to display them to user so that he can // select them easily async getTemplates(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; @@ -91,6 +91,23 @@ export class Bannerbear implements INodeType { } return returnData; }, + + // Get all the available modifications to display them to user so that he can + // select them easily + async getModificationNames(this: ILoadOptionsFunctions): Promise { + const templateId = this.getCurrentNodeParameter('templateId'); + const returnData: INodePropertyOptions[] = []; + const { available_modifications } = await bannerbearApiRequest.call(this, 'GET', `/templates/${templateId}`); + for (const modification of available_modifications) { + const modificationName = modification.name; + const modificationId = modification.name; + returnData.push({ + name: modificationName, + value: modificationId, + }); + } + return returnData; + }, }, }; @@ -130,6 +147,29 @@ export class Bannerbear implements INodeType { } } responseData = await bannerbearApiRequest.call(this, 'POST', '/images', body); + if (additionalFields.waitForImage && responseData.status !== 'completed') { + let maxTries = (additionalFields.waitForImageMaxTries as number) || 3; + + const promise = (uid: string) => { + let data: IDataObject = {}; + return new Promise((resolve, reject) => { + const timeout = setInterval(async () => { + data = await bannerbearApiRequest.call(this, 'GET', `/images/${uid}`); + + if (data.status === 'completed') { + clearInterval(timeout); + resolve(data); + } + if (--maxTries === 0) { + clearInterval(timeout); + reject(new Error('Image did not finish processing after multiple tries.')); + } + }, 2000); + }); + }; + + responseData = await promise(responseData.uid); + } } //https://developers.bannerbear.com/#get-a-specific-image if (operation === 'get') { diff --git a/packages/nodes-base/nodes/Bannerbear/ImageDescription.ts b/packages/nodes-base/nodes/Bannerbear/ImageDescription.ts index a387f57f99..223368d924 100644 --- a/packages/nodes-base/nodes/Bannerbear/ImageDescription.ts +++ b/packages/nodes-base/nodes/Bannerbear/ImageDescription.ts @@ -81,6 +81,33 @@ export const imageFields = [ default: '', description: 'Metadata that you need to store e.g. ID of a record in your DB', }, + { + displayName: 'Wait for Image', + name: 'waitForImage', + type: 'boolean', + default: false, + description: `Wait for the image to be proccesed before returning.
+ If after three tries the images is not ready, an error will be thrown.
+ Number of tries can be increased by setting "Wait Max Tries".`, + }, + { + displayName: 'Wait Max Tries', + name: 'waitForImageMaxTries', + type: 'number', + typeOptions: { + minValue: 1, + maxValue: 10, + }, + displayOptions: { + show: { + waitForImage: [ + true, + ], + }, + }, + default: 3, + description: `How often it should check if the image is available before it fails.`, + }, { displayName: 'Webhook URL', name: 'webhookUrl', @@ -117,7 +144,13 @@ export const imageFields = [ { displayName: 'Name', name: 'name', - type: 'string', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getModificationNames', + loadOptionsDependsOn: [ + 'templateId', + ], + }, default: '', description: 'The name of the item you want to change', }, diff --git a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts index ef3fc8be7b..0c0e8fb633 100644 --- a/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Rocketchat/GenericFunctions.ts @@ -29,14 +29,13 @@ export async function rocketchatApiRequest(this: IHookFunctions | IExecuteFuncti try { return await this.helpers.request!(options); } catch (error) { - console.error(error); + let errorMessage = error.message; - const errorMessage = error.response.body.message || error.response.body.Message; - - if (errorMessage !== undefined) { - throw errorMessage; + if (error.response.body.error) { + errorMessage = error.response.body.error; } - throw error.response.body; + + throw new Error(`Rocket.chat error response [${error.statusCode}]: ${errorMessage}`); } } diff --git a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts index 5b05f8cb70..e67dfe192c 100644 --- a/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts +++ b/packages/nodes-base/nodes/Rocketchat/Rocketchat.node.ts @@ -397,12 +397,12 @@ export class Rocketchat implements INodeType { async executeSingle(this: IExecuteSingleFunctions): Promise { const resource = this.getNodeParameter('resource') as string; - const opeation = this.getNodeParameter('operation') as string; + const operation = this.getNodeParameter('operation') as string; let response; if (resource === 'chat') { //https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage - if (opeation === 'postMessage') { + if (operation === 'postMessage') { const channel = this.getNodeParameter('channel') as string; const text = this.getNodeParameter('text') as string; const options = this.getNodeParameter('options') as IDataObject; @@ -489,11 +489,7 @@ export class Rocketchat implements INodeType { body.attachments = validateJSON(this.getNodeParameter('attachmentsJson') as string); } - try { - response = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); - } catch (err) { - throw new Error(`Rocketchat Error: ${err}`); - } + response = await rocketchatApiRequest.call(this, '/chat', 'POST', 'postMessage', body); } } diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts new file mode 100644 index 0000000000..99b3d726ca --- /dev/null +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -0,0 +1,10 @@ + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c20f8ba12b..2b5ad83142 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,4 +1,7 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, + } from 'n8n-core'; + import { IDataObject, INodeTypeDescription, @@ -6,12 +9,50 @@ import { INodeType, } from 'n8n-workflow'; +import { + validateJSON, +} from './GenericFunctions'; + interface Attachment { fields: { item?: object[]; }; } +interface Text { + type?: string; + text?: string; + emoji?: boolean; + verbatim?: boolean; +} + +interface Confirm { + title?: Text; + text?: Text; + confirm?: Text; + deny?: Text; + style?: string; +} + +interface Element { + type?: string; + text?: Text; + action_id?: string; + url?: string; + value?: string; + style?: string; + confirm?: Confirm; +} + +interface Block { + type?: string; + elements?: Element[]; + block_id?: string; + text?: Text; + fields?: Text[]; + accessory?: Element; +} + export class Slack implements INodeType { description: INodeTypeDescription = { displayName: 'Slack', @@ -262,6 +303,22 @@ export class Slack implements INodeType { }, description: 'Set the bot\'s user name.', }, + { + displayName: 'JSON parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + }, + }, + }, { displayName: 'Attachments', name: 'attachments', @@ -278,10 +335,13 @@ export class Slack implements INodeType { resource: [ 'message', ], + jsonParameters: [ + false, + ], }, }, default: {}, // TODO: Remove comment: has to make default array for the main property, check where that happens in UI - description: 'The attachment to add', + description: 'The attachments to add', placeholder: 'Add attachment item', options: [ { @@ -457,6 +517,926 @@ export class Slack implements INodeType { } ], }, + { + displayName: 'Blocks', + name: 'blocksUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Block', + }, + displayOptions: { + show: { + operation: [ + 'post' + ], + resource: [ + 'message', + ], + jsonParameters: [ + false, + ], + }, + }, + default: {}, + description: 'The blocks to add', + placeholder: 'Add Block', + options: [ + { + name: 'blocksValues', + displayName: 'Block', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Actions', + value: 'actions', + }, + { + name: 'Section', + value: 'section', + }, + ], + default: 'actions', + }, + { + displayName: 'Block ID', + name: 'blockId', + type: 'string', + displayOptions: { + show: { + type: [ + 'actions', + ], + }, + }, + default: '', + description: `A string acting as a unique identifier for a block.
+ You can use this block_id when you receive an interaction payload to
+ identify the source of the action. If not specified, a block_id will be generated.
+ Maximum length for this field is 255 characters.`, + }, + { + displayName: 'Elements', + name: 'elementsUi', + placeholder: 'Add Element', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + type: [ + 'actions', + ], + }, + }, + default: {}, + options: [ + { + name: 'elementsValues', + displayName: 'Element', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Button', + value: 'button', + }, + ], + default: 'button', + description: 'The type of element', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: '', + description: 'The text for the block.', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format.', + }, + { + displayName: 'Action ID', + name: 'actionId', + type: 'string', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: '', + description: `An identifier for this action. You can use this when you receive an interaction
+ payload to identify the source of the action. Should be unique among all other action_ids used
+ elsewhere by your app. `, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: '', + description: `A URL to load in the user's browser when the button is clicked.
+ Maximum length for this field is 3000 characters. If you're using url, you'll still
+ receive an interaction payload and will need to send an acknowledgement response.`, + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: '', + description: 'The value to send along with the interaction payload.', + }, + { + displayName: 'Style', + name: 'style', + type: 'options', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + options: [ + { + name: 'Danger', + value: 'danger', + }, + { + name: 'Default', + value: 'default', + }, + { + name: 'Primary', + value: 'primary', + }, + ], + default: 'default', + description: 'Decorates buttons with alternative visual color schemes.', + }, + { + displayName: 'Confirm', + name: 'confirmUi', + placeholder: 'Add Confirm', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'confirmValue', + displayName: 'Confirm', + values: [ + { + displayName: 'Title', + name: 'titleUi', + placeholder: 'Add Title', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'titleValue', + displayName: 'Title', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Text of the title.', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: `Defines the dialog's title.`, + }, + { + displayName: 'Text', + name: 'textUi', + placeholder: 'Add Text', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'textValue', + displayName: 'Text', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text for the block', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: `Defines the explanatory text that appears in the confirm dialog.`, + }, + { + displayName: 'Confirm', + name: 'confirmTextUi', + placeholder: 'Add Confirm', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'confirmValue', + displayName: 'Confirm', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: `Defines the explanatory text that appears in the confirm dialog.`, + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: 'Defines the text of the button that confirms the action', + }, + { + displayName: 'Deny', + name: 'denyUi', + placeholder: 'Add Deny', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'denyValue', + displayName: 'Deny', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Defines the text of the button that cancels the action', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: 'Defines the text of the button that cancels the action', + }, + { + displayName: 'Style', + name: 'style', + type: 'options', + options: [ + { + name: 'Danger', + value: 'danger', + }, + { + name: 'Default', + value: 'default', + }, + { + name: 'Primary', + value: 'primary', + }, + ], + default: 'default', + description: 'Defines the color scheme applied to the confirm button.', + }, + ], + }, + ], + description: 'Defines an optional confirmation dialog after the button is clicked.', + }, + ], + }, + ], + }, + { + displayName: 'Block ID', + name: 'blockId', + type: 'string', + displayOptions: { + show: { + type: [ + 'section', + ], + }, + }, + default: '', + description: `A string acting as a unique identifier for a block.
+ You can use this block_id when you receive an interaction payload to
+ identify the source of the action. If not specified, a block_id will be generated.
+ Maximum length for this field is 255 characters.`, + }, + { + displayName: 'Text', + name: 'textUi', + placeholder: 'Add Text', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + type: [ + 'section', + ], + }, + }, + default: {}, + options: [ + { + name: 'textValue', + displayName: 'Text', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Markdowm', + value: 'mrkwdn', + }, + { + name: 'Plain Text', + value: 'plainText', + }, + ], + default: 'mrkwdn', + description: 'The formatting to use for this text object.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text for the block. This field accepts any of the standard text formatting markup when type is mrkdwn.', + }, + { + displayName: 'Emoji', + name: 'emoji', + displayOptions: { + show: { + type: [ + 'plainText', + ], + }, + }, + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format. This field is only usable when type is plain_text.', + }, + { + displayName: 'Verbatim', + name: 'verbatim', + displayOptions: { + show: { + type: [ + 'mrkwdn', + ], + }, + }, + type: 'boolean', + default: false, + description: 'When set to false (as is default) URLs will be auto-converted into links, conversation names will be link-ified, and certain mentions will be automatically parsed. ', + }, + ], + }, + ], + description: 'Define the text of the button that cancels the action', + }, + { + displayName: 'Fields', + name: 'fieldsUi', + placeholder: 'Add Fields', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + type: [ + 'section', + ], + }, + }, + default: {}, + options: [ + { + name: 'fieldsValues', + displayName: 'Text', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Markdowm', + value: 'mrkwdn', + }, + { + name: 'Plain Text', + value: 'plainText', + }, + ], + default: 'mrkwdn', + description: 'The formatting to use for this text object.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text for the block. This field accepts any of the standard text formatting markup when type is mrkdwn.', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'plainText', + ], + }, + }, + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format. This field is only usable when type is plain_text.', + }, + { + displayName: 'Verbatim', + name: 'verbatim', + displayOptions: { + show: { + type: [ + 'mrkwdn', + ], + }, + }, + type: 'boolean', + default: false, + description: 'When set to false (as is default) URLs will be auto-converted into links, conversation names will be link-ified, and certain mentions will be automatically parsed. ', + }, + ], + }, + ], + description: `An array of text objects. Any text objects included with
+ fields will be rendered in a compact format that allows for 2 columns of
+ side-by-side text. Maximum number of items is 10.`, + }, + { + displayName: 'Accessory', + name: 'accessoryUi', + placeholder: 'Add Accessory', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + type: [ + 'section', + ], + }, + }, + default: {}, + options: [ + { + name: 'accessoriesValues', + displayName: 'Accessory', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Button', + value: 'button', + }, + ], + default: 'button', + description: 'The type of element', + }, + { + displayName: 'Text', + name: 'text', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'string', + default: '', + description: 'The text for the block.', + }, + { + displayName: 'Emoji', + name: 'emoji', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format.', + }, + { + displayName: 'Action ID', + name: 'actionId', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'string', + default: '', + description: `An identifier for this action. You can use this when you receive an interaction
+ payload to identify the source of the action. Should be unique among all other action_ids used
+ elsewhere by your app. `, + }, + { + displayName: 'URL', + name: 'url', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'string', + default: '', + description: `A URL to load in the user's browser when the button is clicked.
+ Maximum length for this field is 3000 characters. If you're using url, you'll still
+ receive an interaction payload and will need to send an acknowledgement response.`, + }, + { + displayName: 'Value', + name: 'value', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'string', + default: '', + description: 'The value to send along with the interaction payload.', + }, + { + displayName: 'Style', + name: 'style', + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + type: 'options', + options: [ + { + name: 'Danger', + value: 'danger', + }, + { + name: 'Default', + value: 'default', + }, + { + name: 'Primary', + value: 'primary', + }, + ], + default: 'default', + description: 'Decorates buttons with alternative visual color schemes.', + }, + { + displayName: 'Confirm', + name: 'confirmUi', + placeholder: 'Add Confirm', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + displayOptions: { + show: { + type: [ + 'button', + ], + }, + }, + default: {}, + options: [ + { + name: 'confirmValue', + displayName: 'Confirm', + values: [ + { + displayName: 'Title', + name: 'titleUi', + placeholder: 'Add Title', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'titleValue', + displayName: 'Title', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Text of the title.', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: 'Defines an optional confirmation dialog after the button is clicked.', + }, + { + displayName: 'Text', + name: 'textUi', + placeholder: 'Add Text', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'textValue', + displayName: 'Text', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'The text for the block', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: `Defines the explanatory text that appears in the confirm dialog.`, + }, + { + displayName: 'Confirm', + name: 'confirmTextUi', + placeholder: 'Add Confirm', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'confirmValue', + displayName: 'Confirm', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: `Defines the explanatory text that appears in the confirm dialog.`, + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: `Defines the explanatory text that appears in the confirm dialog.`, + }, + { + displayName: 'Deny', + name: 'denyUi', + placeholder: 'Add Deny', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'denyValue', + displayName: 'Deny', + values: [ + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Define the text of the button that cancels the action', + }, + { + displayName: 'Emoji', + name: 'emoji', + type: 'boolean', + default: false, + description: 'Indicates whether emojis in a text field should be escaped into the colon emoji format', + }, + ], + }, + ], + description: 'Define the text of the button that cancels the action', + }, + { + displayName: 'Style', + name: 'style', + type: 'options', + options: [ + { + name: 'Danger', + value: 'danger', + }, + { + name: 'Default', + value: 'default', + }, + { + name: 'Primary', + value: 'primary', + }, + ], + default: 'default', + description: 'Defines the color scheme applied to the confirm button.', + }, + ], + }, + ], + description: 'Defines an optional confirmation dialog after the button is clicked.', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'attachmentsJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'post', + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'The attachments to add', + }, + { + displayName: 'Blocks', + name: 'blocksJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'post', + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'The blocks to add', + }, { displayName: 'Other Options', name: 'otherOptions', @@ -562,8 +1542,6 @@ export class Slack implements INodeType { ], }; - - async execute(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: IDataObject[] = []; @@ -625,28 +1603,233 @@ export class Slack implements INodeType { body.channel = this.getNodeParameter('channel', i) as string; body.text = this.getNodeParameter('text', i) as string; body.as_user = this.getNodeParameter('as_user', i) as boolean; + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + if (body.as_user === false) { body.username = this.getNodeParameter('username', i) as string; } - const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; + if (!jsonParameters) { + const attachments = this.getNodeParameter('attachments', i, []) as unknown as Attachment[]; + const blocksUi = (this.getNodeParameter('blocksUi', i, []) as IDataObject).blocksValues as IDataObject[]; - // The node does save the fields data differently than the API - // expects so fix the data befre we send the request - for (const attachment of attachments) { - if (attachment.fields !== undefined) { - if (attachment.fields.item !== undefined) { - // Move the field-content up - // @ts-ignore - attachment.fields = attachment.fields.item; - } else { - // If it does not have any items set remove it - delete attachment.fields; + + // The node does save the fields data differently than the API + // expects so fix the data befre we send the request + for (const attachment of attachments) { + if (attachment.fields !== undefined) { + if (attachment.fields.item !== undefined) { + // Move the field-content up + // @ts-ignore + attachment.fields = attachment.fields.item; + } else { + // If it does not have any items set remove it + delete attachment.fields; + } } } - } - body['attachments'] = attachments; + body['attachments'] = attachments; + if (blocksUi) { + const blocks: Block[] = []; + for (const blockUi of blocksUi) { + const block: Block = {}; + const elements: Element[] = []; + block.block_id = blockUi.blockId as string; + block.type = blockUi.type as string; + if (block.type === 'actions') { + const elementsUi = (blockUi.elementsUi as IDataObject).elementsValues as IDataObject[]; + if (elementsUi) { + for (const elementUi of elementsUi) { + const element: Element = {}; + if (elementUi.actionId === '') { + throw new Error('Action ID must be set'); + } + if (elementUi.text === '') { + throw new Error('Text must be set'); + } + element.action_id = elementUi.actionId as string; + element.type = elementUi.type as string; + element.text = { + text: elementUi.text as string, + type: 'plain_text', + emoji: elementUi.emoji as boolean, + }; + if (elementUi.url) { + element.url = elementUi.url as string; + } + if (elementUi.value) { + element.value = elementUi.value as string; + } + if (elementUi.style !== 'default') { + element.style = elementUi.style as string; + } + const confirmUi = (elementUi.confirmUi as IDataObject).confirmValue as IDataObject; + if (confirmUi) { + const confirm: Confirm = {}; + const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; + const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; + const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; + const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; + const style = confirmUi.style as string; + if (titleUi) { + confirm.title = { + type: 'plain_text', + text: titleUi.text as string, + emoji: titleUi.emoji as boolean, + }; + } + if (textUi) { + confirm.text = { + type: 'plain_text', + text: textUi.text as string, + emoji: textUi.emoji as boolean, + }; + } + if (confirmTextUi) { + confirm.confirm = { + type: 'plain_text', + text: confirmTextUi.text as string, + emoji: confirmTextUi.emoji as boolean, + }; + } + if (denyUi) { + confirm.deny = { + type: 'plain_text', + text: denyUi.text as string, + emoji: denyUi.emoji as boolean, + }; + } + if (style !== 'default') { + confirm.style = style as string; + } + element.confirm = confirm; + } + elements.push(element); + } + block.elements = elements; + } + } else if (block.type === 'section') { + const textUi = (blockUi.textUi as IDataObject).textValue as IDataObject; + if (textUi) { + const text: Text = {}; + if (textUi.type === 'plainText') { + text.type = 'plain_text'; + text.emoji = textUi.emoji as boolean; + } else { + text.type = 'mrkdwn'; + text.verbatim = textUi.verbatim as boolean; + } + text.text = textUi.text as string; + block.text = text; + } else { + throw new Error('Property text must be defined'); + } + const fieldsUi = (blockUi.fieldsUi as IDataObject).fieldsValues as IDataObject[]; + if (fieldsUi) { + const fields: Text[] = []; + for (const fieldUi of fieldsUi) { + const field: Text = {}; + if (fieldUi.type === 'plainText') { + field.type = 'plain_text'; + field.emoji = fieldUi.emoji as boolean; + } else { + field.type = 'mrkdwn'; + field.verbatim = fieldUi.verbatim as boolean; + } + field.text = fieldUi.text as string; + fields.push(field); + } + // If not fields were added then it's not needed to send the property + if (fields.length > 0) { + block.fields = fields; + } + } + const accessoryUi = (blockUi.accessoryUi as IDataObject).accessoriesValues as IDataObject; + if (accessoryUi) { + const accessory: Element = {}; + if (accessoryUi.type === 'button') { + accessory.type = 'button'; + accessory.text = { + text: accessoryUi.text as string, + type: 'plain_text', + emoji: accessoryUi.emoji as boolean, + }; + if (accessoryUi.url) { + accessory.url = accessoryUi.url as string; + } + if (accessoryUi.value) { + accessory.value = accessoryUi.value as string; + } + if (accessoryUi.style !== 'default') { + accessory.style = accessoryUi.style as string; + } + const confirmUi = (accessoryUi.confirmUi as IDataObject).confirmValue as IDataObject; + if (confirmUi) { + const confirm: Confirm = {}; + const titleUi = (confirmUi.titleUi as IDataObject).titleValue as IDataObject; + const textUi = (confirmUi.textUi as IDataObject).textValue as IDataObject; + const confirmTextUi = (confirmUi.confirmTextUi as IDataObject).confirmValue as IDataObject; + const denyUi = (confirmUi.denyUi as IDataObject).denyValue as IDataObject; + const style = confirmUi.style as string; + if (titleUi) { + confirm.title = { + type: 'plain_text', + text: titleUi.text as string, + emoji: titleUi.emoji as boolean, + }; + } + if (textUi) { + confirm.text = { + type: 'plain_text', + text: textUi.text as string, + emoji: textUi.emoji as boolean, + }; + } + if (confirmTextUi) { + confirm.confirm = { + type: 'plain_text', + text: confirmTextUi.text as string, + emoji: confirmTextUi.emoji as boolean, + }; + } + if (denyUi) { + confirm.deny = { + type: 'plain_text', + text: denyUi.text as string, + emoji: denyUi.emoji as boolean, + }; + } + if (style !== 'default') { + confirm.style = style as string; + } + accessory.confirm = confirm; + } + } + block.accessory = accessory; + } + } + blocks.push(block); + } + body.blocks = blocks; + } + + } else { + const attachmentsJson = this.getNodeParameter('attachmentsJson', i, []) as string; + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { + throw new Error('Attachments it is not a valid json'); + } + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new Error('Blocks it is not a valid json'); + } + if (attachmentsJson !== '') { + body.attachments = attachmentsJson; + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } + } // Add all the other options to the request const otherOptions = this.getNodeParameter('otherOptions', i) as IDataObject; Object.assign(body, otherOptions); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 2bb3628c13..8efbb1bbd2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -269,7 +269,7 @@ "@types/moment-timezone": "^0.5.12", "@types/mongodb": "^3.5.4", "@types/node": "^10.10.1", - "@types/nodemailer": "^4.6.5", + "@types/nodemailer": "^6.4.0", "@types/redis": "^2.8.11", "@types/request-promise-native": "~1.0.15", "@types/uuid": "^3.4.6", @@ -291,7 +291,7 @@ "formidable": "^1.2.1", "glob-promise": "^3.4.0", "gm": "^1.23.1", - "googleapis": "~46.0.0", + "googleapis": "~50.0.0", "imap-simple": "^4.3.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", @@ -301,7 +301,7 @@ "mongodb": "^3.5.5", "mysql2": "^2.0.1", "n8n-core": "~0.33.0", - "nodemailer": "^5.1.1", + "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg-promise": "^9.0.3", "redis": "^2.8.0", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 72af56092d..968399ce39 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -31,7 +31,7 @@ "@types/node": "^10.10.1", "jest": "^24.9.0", "ts-jest": "^24.0.2", - "tslint": "^5.17.0", + "tslint": "^6.1.2", "typescript": "~3.7.4" }, "dependencies": { diff --git a/packages/workflow/src/NodeHelpers.ts b/packages/workflow/src/NodeHelpers.ts index 08e9660b99..a72ee565de 100644 --- a/packages/workflow/src/NodeHelpers.ts +++ b/packages/workflow/src/NodeHelpers.ts @@ -277,6 +277,7 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr nodeValuesRoot = nodeValuesRoot || nodeValues; let value; + const values: any[] = []; // tslint:disable-line:no-any if (parameter.displayOptions.show) { // All the defined rules have to match to display parameter for (const propertyName of Object.keys(parameter.displayOptions.show)) { @@ -288,7 +289,14 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr value = get(nodeValues, propertyName); } - if (value === undefined || !parameter.displayOptions.show[propertyName].includes(value as string)) { + values.length = 0; + if (!Array.isArray(value)) { + values.push(value); + } else { + values.push.apply(values, value); + } + + if (values.length === 0 || !parameter.displayOptions.show[propertyName].some(v => values.includes(v))) { return false; } } @@ -304,7 +312,15 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr // Get the value from current level value = get(nodeValues, propertyName); } - if (value !== undefined && parameter.displayOptions.hide[propertyName].includes(value as string)) { + + values.length = 0; + if (!Array.isArray(value)) { + values.push(value); + } else { + values.push.apply(values, value); + } + + if (values.length !== 0 && parameter.displayOptions.hide[propertyName].some(v => values.includes(v))) { return false; } }