From e8f53effb4f53ede3632221809ab83b8e07d363c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 08:44:06 +0100 Subject: [PATCH 01/97] :books: Fix and unify number of nodes --- README.md | 4 ++-- docker/images/n8n/README.md | 2 +- packages/cli/README.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a0b4885a49..56467671a5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) -n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything. +n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything. n8n.io - Screenshot @@ -16,7 +16,7 @@ received or lost a star. ## Available integrations -n8n has 170+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) +n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) ## Documentation diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index a23a63cef5..87e812bdf0 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -33,7 +33,7 @@ Slack notification every time a Github repository received or lost a star. ## Available integrations -n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) +n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) ## Documentation diff --git a/packages/cli/README.md b/packages/cli/README.md index b7cbd5417d..5c519a8964 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -32,7 +32,7 @@ Slack notification every time a Github repository received or lost a star. ## Available integrations -n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) +n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) ## Documentation From c87382c0867a2628b111d18291c9110d228feab2 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 10 Dec 2020 04:17:16 -0500 Subject: [PATCH 02/97] :sparkles: Add Snowflake Node (#1230) --- .../credentials/Snowflake.credentials.ts | 70 +++++ .../nodes/Snowflake/GenericFunctions.ts | 62 +++++ .../nodes/Snowflake/Snowflake.node.ts | 244 ++++++++++++++++++ .../nodes-base/nodes/Snowflake/snowflake.png | Bin 0 -> 2347 bytes packages/nodes-base/package.json | 4 + 5 files changed, 380 insertions(+) create mode 100644 packages/nodes-base/credentials/Snowflake.credentials.ts create mode 100644 packages/nodes-base/nodes/Snowflake/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Snowflake/Snowflake.node.ts create mode 100644 packages/nodes-base/nodes/Snowflake/snowflake.png diff --git a/packages/nodes-base/credentials/Snowflake.credentials.ts b/packages/nodes-base/credentials/Snowflake.credentials.ts new file mode 100644 index 0000000000..dde6b8df14 --- /dev/null +++ b/packages/nodes-base/credentials/Snowflake.credentials.ts @@ -0,0 +1,70 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class Snowflake implements ICredentialType { + name = 'snowflake'; + displayName = 'Snowflake'; + documentationUrl = 'snowflake'; + properties = [ + { + displayName: 'Account', + name: 'account', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the name of your Snowflake account.', + }, + { + displayName: 'Database', + name: 'database', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Specify the database you want to use after creating the connection.', + }, + { + displayName: 'Warehouse', + name: 'warehouse', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The default virtual warehouse to use for the session after connecting. Used for performing queries, loading data, etc.', + }, + { + displayName: 'Username', + name: 'username', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'Schema', + name: 'schema', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the schema you want to use after creating the connection', + }, + { + displayName: 'Role', + name: 'role', + type: 'string' as NodePropertyTypes, + default: '', + description: 'Enter the security role you want to use after creating the connection', + }, + { + displayName: 'Client Session Keep Alive', + name: 'clientSessionKeepAlive', + type: 'boolean' as NodePropertyTypes, + default: false, + description: `By default, client connections typically time out approximately 3-4 hours after the most recent query was executed.
+ If the parameter clientSessionKeepAlive is set to true, the client’s connection to the server will be kept alive indefinitely, even if no queries are executed.`, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts new file mode 100644 index 0000000000..193a43e1e1 --- /dev/null +++ b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts @@ -0,0 +1,62 @@ +import { + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + +import * as snowflake from 'snowflake-sdk'; + +export function connect(conn: snowflake.Connection) { + return new Promise((resolve, reject) => { + conn.connect((err, conn) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function destroy(conn: snowflake.Connection) { + return new Promise((resolve, reject) => { + conn.destroy((err, conn) => { + if (!err) { + resolve(); + } else { + reject(err); + } + }); + }); +} + +export function execute(conn: snowflake.Connection, sqlText: string, binds: snowflake.InsertBinds) { + return new Promise((resolve, reject) => { + conn.execute({ + sqlText, + binds, + complete: (err, stmt, rows) => { + if (!err) { + resolve(rows); + } else { + reject(err); + } + }, + }); + }); +} + +export function copyInputItems(items: INodeExecutionData[], properties: string[]): IDataObject[] { + // Prepare the data to insert and copy it to be returned + let newItem: IDataObject; + return items.map((item) => { + newItem = {}; + for (const property of properties) { + if (item.json[property] === undefined) { + newItem[property] = null; + } else { + newItem[property] = JSON.parse(JSON.stringify(item.json[property])); + } + } + return newItem; + }); +} diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts new file mode 100644 index 0000000000..3a7cfeb260 --- /dev/null +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -0,0 +1,244 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + connect, + copyInputItems, + destroy, + execute, +} from './GenericFunctions'; + +import * as snowflake from 'snowflake-sdk'; + +export class Snowflake implements INodeType { + description: INodeTypeDescription = { + displayName: 'Snowflake', + name: 'snowflake', + icon: 'file:snowflake.png', + group: ['input'], + version: 1, + description: 'Get, add and update data in Snowflake.', + defaults: { + name: 'Snowflake', + color: '#5ebbeb', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'snowflake', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Execute Query', + value: 'executeQuery', + description: 'Execute an SQL query.', + }, + { + name: 'Insert', + value: 'insert', + description: 'Insert rows in database.', + }, + { + name: 'Update', + value: 'update', + description: 'Update rows in database.', + }, + ], + default: 'insert', + description: 'The operation to perform.', + }, + + // ---------------------------------- + // executeQuery + // ---------------------------------- + { + displayName: 'Query', + name: 'query', + type: 'string', + typeOptions: { + rows: 5, + }, + displayOptions: { + show: { + operation: [ + 'executeQuery', + ], + }, + }, + default: '', + placeholder: 'SELECT id, name FROM product WHERE id < 40', + required: true, + description: 'The SQL query to execute.', + }, + + + // ---------------------------------- + // insert + // ---------------------------------- + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to insert data to.', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: [ + 'insert', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + + + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: 'Table', + name: 'table', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'Name of the table in which to update data in', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: 'id', + required: true, + description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'name,description', + description: 'Comma separated list of the properties which should used as columns for rows to update.', + }, + + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const credentials = this.getCredentials('snowflake') as unknown as snowflake.ConnectionOptions; + const returnData: IDataObject[] = []; + let responseData; + + const connection = snowflake.createConnection(credentials); + + await connect(connection); + + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0) as string; + + if (operation === 'executeQuery') { + // ---------------------------------- + // executeQuery + // ---------------------------------- + + for (let i = 0; i < items.length; i++) { + const query = this.getNodeParameter('query', i) as string; + responseData = await execute(connection, query, []); + returnData.push.apply(returnData, responseData as IDataObject[]); + } + } + + if (operation === 'insert') { + // ---------------------------------- + // insert + // ---------------------------------- + + const table = this.getNodeParameter('table', 0) as string; + const columnString = this.getNodeParameter('columns', 0) as string; + const columns = columnString.split(',').map(column => column.trim()); + const query = `INSERT INTO ${table}(${columns.join(',')}) VALUES (${columns.map(column => '?').join(',')})`; + const data = copyInputItems(items, columns); + const binds = data.map((element => Object.values(element))); + await execute(connection, query, binds as unknown as snowflake.InsertBinds); + returnData.push.apply(returnData, data); + } + + if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + const table = this.getNodeParameter('table', 0) as string; + const updateKey = this.getNodeParameter('updateKey', 0) as string; + const columnString = this.getNodeParameter('columns', 0) as string; + const columns = columnString.split(',').map(column => column.trim()); + + if (!columns.includes(updateKey)) { + columns.unshift(updateKey); + } + + const query = `UPDATE ${table} SET ${columns.map(column => `${column} = ?`).join(',')} WHERE ${updateKey} = ?;`; + const data = copyInputItems(items, columns); + const binds = data.map((element => Object.values(element).concat(element[updateKey]))); + for (let i = 0; i < binds.length; i++) { + await execute(connection, query, binds[i] as unknown as snowflake.InsertBinds); + } + returnData.push.apply(returnData, data); + } + + await destroy(connection); + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Snowflake/snowflake.png b/packages/nodes-base/nodes/Snowflake/snowflake.png new file mode 100644 index 0000000000000000000000000000000000000000..6d149707ddc9eb560b698e4e9502db63b0deba02 GIT binary patch literal 2347 zcmX|Bc|6ox8$V{w8FMAMC6&~@3D;IPEiPr>2HD9dvb5huN((9#CF?bnRFblH-DIiI z#Gn~724S+3aw(LxEW?=PH+Q^$yr0i=p7T7d(;+HIH+U5Y@7F-=YZKNt7}LS|2eXRkhuU~ zPk=~iD6v!~RDOr$qMwl02@V+$w!<;So|5%X9%=O zM4v@3hhf<`to)8>Q@}S2=o$i_Ghm$thg`N{3cyni_Ezy&S(Gl;>%=Wz*)IXrC~r7>X9zqsN7NSlI1{qW5sF{cU9 zzvJR)`0z)vuSQVOCwy22KKvofZO329Bs~8}ET6zf=HhbO_SMh93-^E}8CN_AA2;KC zLQ%zc5O5JCmJ_m@fyOqN(F|OUgDrmmIuE#q0b4g|l~kpG*+SGHQvD=BTrp$|P%R%Z zXAnn-nwM%N%UnQZBd}r$KJ9er!XRUwKpP{}OvAFTFuk6@1b`3?ZEZ^V-op1}gd&C6(g?F5ydu@s5G` zs#*Nm+jz&rFtrwvFTl`Lc%u-oCD7Cj{@o264nx`)%iL|L;y+olN+ubN)I66q50GK)N?(6|D z8L1^5j;0?3Ga8y>@2Xw`yysZ(6IoY88fw zCnvuRe&}g$Z|63%Y8fvIo;}UYOuG{u9d$YMe8}ljL6npJem>rAF3t|N=ElamcN^{6 zv2BZ*s>-^x3d@9i-t6?$(5H_d-g8-RYbxK+Xr+bEpXEzKWMte3ryXaq%cEnxn<5zK zeN$i%oUVE5WMBEI|C6qLUrqS8{;F&KX+bWMYF>JamGk^W#GY5=pG3DL)2{>PoD06L z1{=Byx`L?s*>Tv$tz>o{?a%_rgBhBX%qCi0Tfw<_*763;m?U4Tf2>Sb#c*rrQMT@H=lbWfeqKh!+0ff4Y?IUbtNTc8o(dW( zvYy#9j!ypRX)NCw*QLaO7i}!obHAx_Q%<=bU=XjE$W0A7S@mI;6r#O08>A8&Iuu7+apgcq<3HL|g>&?( zcM)>bdIWL|HLtpMs@C$pU@ZEpgF3gf>QzbVNr@hLIwx*E5boHYJ-b|P>*}Nn4Z1B> z{;d&{HOC(P8vEy$391Hu|6NB?>b`~})Vk0LW(qMSfBWUUzdSyxpE-Mrea?_BQ!MQns)4aFn~z2C(-4&T;SPkEo5qYMnQXePkFDNt-8KqSy7BtbBC;R^Lb? zafV;)wrKO{S9Hrw5KkW zJy?5ylS1`db?^D+L^H*}W8=qssXFacSP8RTy}LzdAa7X;FM+px0+m{+|sn| zLAa+|Rg5hQcX-b=_x89HIGk1Kx45Fr^roTKrdogb)hBmZvh2JZ-Gdv6_8=?R*@ zGVb-dFh};s$k~dYC|ZhUq`P*skYueVB`@7fGOlak=JcK@v5t8~#vJ@Gw)VFy>!iN| z=b!Y}<{P;c$H%qH&abO3?a_sLNpFLKkZ~ZB!|910)1msm_yX7wbk=72ubTV9cE#_KiD73ix_frl5pVCL zxT?`pJme|DL+cZbEV?YVFt9V3H(f1FyQ6(|M(l#Fnp>>v-%wm?(0YGO1GA*cxqD`f za$nZ)#!!{_jc>AIuJ89w{>6uA8F-Cx^kckop*?8Dze$f!m{H2s}Q>61`g>Fo6OXf1Wkl!d@ zG#`FS-!#sDN_@BDW%=D0CDBn+qGf@7h{NgUx+f2Ndm6X~%k(Jr>!y~Q@N%x3b>g86 zZ*M{r$Shn|HJuHJ%^l?w3zLL{f5^xv%8YtbC@OQCe(BQUpK}PeX+CWK7N11VemngMd*!!4AU5iOja;Ymud9bFvj@h%|q$e5A1E6t&1!JuKx#6UvftP literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 9f1ccd681e..f22717a75b 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -183,6 +183,7 @@ "dist/credentials/SlackApi.credentials.js", "dist/credentials/SlackOAuth2Api.credentials.js", "dist/credentials/Sms77Api.credentials.js", + "dist/credentials/Snowflake.credentials.js", "dist/credentials/Smtp.credentials.js", "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/StravaOAuth2Api.credentials.js", @@ -410,6 +411,7 @@ "dist/nodes/Signl4/Signl4.node.js", "dist/nodes/Slack/Slack.node.js", "dist/nodes/Sms77/Sms77.node.js", + "dist/nodes/Snowflake/Snowflake.node.js", "dist/nodes/SplitInBatches.node.js", "dist/nodes/Spontit/Spontit.node.js", "dist/nodes/Spotify/Spotify.node.js", @@ -497,6 +499,7 @@ }, "dependencies": { "@types/promise-ftp": "^1.3.4", + "@types/snowflake-sdk": "^1.5.1", "aws4": "^1.8.0", "basic-auth": "^2.0.1", "change-case": "^4.1.1", @@ -531,6 +534,7 @@ "request": "^2.88.2", "rhea": "^1.0.11", "rss-parser": "^3.7.0", + "snowflake-sdk": "^1.5.3", "ssh2-sftp-client": "^5.2.1", "tmp-promise": "^3.0.2", "uuid": "^3.4.0", From b73349a3a540173b46d1ada281d5f0197500d5d0 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 10:39:11 +0100 Subject: [PATCH 03/97] :bug: Fix bug that OAuth1 requests did not work anymore --- packages/core/src/NodeExecuteFunctions.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index fcca59d9fe..1006b29100 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -241,6 +241,15 @@ export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: strin //@ts-ignore requestOptions.data = { ...requestOptions.qs, ...requestOptions.form }; + // Fixes issue that OAuth1 library only works with "url" property and not with "uri" + // @ts-ignore + if (requestOptions.uri && !requestOptions.url) { + // @ts-ignore + requestOptions.url = requestOptions.uri; + // @ts-ignore + delete requestOptions.uri; + } + //@ts-ignore requestOptions.headers = oauth.toHeader(oauth.authorize(requestOptions, token)); From 701871944d86094f83da2a4dd45735ce8d2a54ee Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 10 Dec 2020 05:09:38 -0500 Subject: [PATCH 04/97] :sparkles: Add Twist Node (#1231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Twist Node * :zap: Small improvements * 🔨Fix descriptions * :zap: Minor improvements to Twist-Node Co-authored-by: Harshil Co-authored-by: Jan Oberhauser --- .../credentials/TwistOAuth2Api.credentials.ts | 55 +++ .../nodes/Twist/ChannelDescription.ts | 428 ++++++++++++++++++ .../nodes/Twist/GenericFunctions.ts | 50 ++ .../Twist/MessageConversationDescription.ts | 230 ++++++++++ packages/nodes-base/nodes/Twist/Twist.node.ts | 293 ++++++++++++ packages/nodes-base/nodes/Twist/twist.png | Bin 0 -> 1360 bytes packages/nodes-base/package.json | 2 + 7 files changed, 1058 insertions(+) create mode 100644 packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Twist/ChannelDescription.ts create mode 100644 packages/nodes-base/nodes/Twist/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Twist/MessageConversationDescription.ts create mode 100644 packages/nodes-base/nodes/Twist/Twist.node.ts create mode 100644 packages/nodes-base/nodes/Twist/twist.png diff --git a/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts new file mode 100644 index 0000000000..850a53dabc --- /dev/null +++ b/packages/nodes-base/credentials/TwistOAuth2Api.credentials.ts @@ -0,0 +1,55 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'attachments:write', + 'channels:remove', + 'messages:remove', + 'workspaces:read', +]; + +export class TwistOAuth2Api implements ICredentialType { + name = 'twistOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Twist OAuth2 API'; + documentationUrl = 'twist'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://twist.com/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://twist.com/oauth/access_token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(','), + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Twist/ChannelDescription.ts b/packages/nodes-base/nodes/Twist/ChannelDescription.ts new file mode 100644 index 0000000000..6412125330 --- /dev/null +++ b/packages/nodes-base/nodes/Twist/ChannelDescription.ts @@ -0,0 +1,428 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const channelOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'channel', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Initiates a public or private channel-based conversation', + }, + { + name: 'Get', + value: 'get', + description: 'Get information about a channel', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all channels', + }, + { + name: 'Update', + value: 'update', + description: 'Update a channel', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const channelFields = [ + /*-------------------------------------------------------------------------- */ + /* channel:create */ + /* ------------------------------------------------------------------------- */ + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The id of the workspace.', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The name of the channel.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'options', + options: [ + { + name: 'Berry Red', + value: 6, + }, + { + name: 'Blue', + value: 1, + }, + { + name: 'Green', + value: 4, + }, + { + name: 'Grey', + value: 0, + }, + { + name: 'Magenta', + value: 7, + }, + { + name: 'Mint Green', + value: 9, + }, + { + name: 'Red', + value: 5, + }, + { + name: 'Salmon', + value: 11, + }, + { + name: 'Sky Blue', + value: 8, + }, + { + name: 'Teal Blue', + value: 3, + }, + { + name: 'Turquoise', + value: 2, + }, + { + name: 'Yellow', + value: 10, + }, + ], + default: 0, + description: 'The color of the channel', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the channel', + }, + { + displayName: 'Public', + name: 'public', + type: 'boolean', + default: false, + description: 'If enabled, the channel will be marked as public', + }, + { + displayName: 'Temp ID', + name: 'temp_id', + type: 'number', + default: -1, + description: 'The temporary id of the channel. It needs to be a negative number.', + }, + { + displayName: 'User IDs', + name: 'user_ids', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: [], + description: 'The users that will participate in the channel.', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* channel:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the channel', + }, + /* -------------------------------------------------------------------------- */ + /* channel:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the workspace.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Archived', + name: 'archived', + type: 'boolean', + default: false, + description: 'If enabled, only archived conversations are returned', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* channel:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel ID', + name: 'channelId', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + description: 'The ID of the channel.', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Color', + name: 'color', + type: 'options', + options: [ + { + name: 'Berry Red', + value: 6, + }, + { + name: 'Blue', + value: 1, + }, + { + name: 'Green', + value: 4, + }, + { + name: 'Grey', + value: 0, + }, + { + name: 'Magenta', + value: 7, + }, + { + name: 'Mint Green', + value: 9, + }, + { + name: 'Red', + value: 5, + }, + { + name: 'Salmon', + value: 11, + }, + { + name: 'Sky Blue', + value: 8, + }, + { + name: 'Teal Blue', + value: 3, + }, + { + name: 'Turquoise', + value: 2, + }, + { + name: 'Yellow', + value: 10, + }, + ], + default: 0, + description: 'The color of the channel', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the channel', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the channel', + }, + { + displayName: 'Public', + name: 'public', + type: 'boolean', + default: false, + description: 'If enabled, the channel will be marked as public', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/GenericFunctions.ts b/packages/nodes-base/nodes/Twist/GenericFunctions.ts new file mode 100644 index 0000000000..8a633ea5f0 --- /dev/null +++ b/packages/nodes-base/nodes/Twist/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function twistApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + body, + qs, + uri: `https://api.twist.com/api/v3${endpoint}`, + json: true, + }; + + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (Object.keys(qs).length === 0) { + delete options.qs; + } + + Object.assign(options, option); + + try { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'twistOAuth2Api', options); + + } catch (error) { + if (error.response && error.response.body && error.response.body.error_string) { + + const message = error.response.body.error_string; + + // Try to return the error prettier + throw new Error( + `Twist error response [${error.statusCode}]: ${message}`, + ); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts new file mode 100644 index 0000000000..d81f1254dc --- /dev/null +++ b/packages/nodes-base/nodes/Twist/MessageConversationDescription.ts @@ -0,0 +1,230 @@ +import { + INodeProperties +} from 'n8n-workflow'; + +export const messageConversationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'messageConversation', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a message in a conversation', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const messageConversationFields = [ + + /* -------------------------------------------------------------------------- */ + /* messageConversation:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Workspace ID', + name: 'workspaceId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getWorkspaces', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the workspace.', + }, + { + displayName: 'Conversation ID', + name: 'conversationId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getConversations', + loadOptionsDependsOn: [ + 'workspaceId', + ], + }, + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'messageConversation', + ], + }, + }, + required: true, + description: 'The ID of the conversation.', + }, + { + displayName: 'Content', + name: 'content', + type: 'string', + default: '', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'messageConversation', + ], + }, + }, + description: `The content of the new message. Mentions can be used as [Name](twist-mention://user_id) for users or [Group name](twist-group-mention://group_id) for groups.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'messageConversation', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Actions', + name: 'actionsUi', + type: 'fixedCollection', + placeholder: 'Add Action', + typeOptions: { + multipleValues: true, + }, + options: [ + { + displayName: 'Action', + name: 'actionValues', + values: [ + { + displayName: 'Action', + name: 'action', + type: 'options', + description: 'The action of the button', + options: [ + { + name: 'Open URL', + value: 'open_url', + }, + { + name: 'Prefill Message', + value: 'prefill_message', + }, + { + name: 'Send Reply', + value: 'send_reply', + }, + ], + default: '', + }, + { + displayName: 'Button Text', + name: 'button_text', + type: 'string', + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + displayOptions: { + show: { + action: [ + 'send_reply', + 'prefill_message', + ], + }, + }, + description: 'The text for the action button.', + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + description: 'The type of the button, for now just action is available.', + options: [ + { + name: 'Action', + value: 'action', + }, + ], + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + action: [ + 'open_url', + ], + }, + }, + description: 'URL to redirect', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Attachments', + name: 'binaryProperties', + type: 'string', + default: 'data', + description: 'Name of the property that holds the binary data. Multiple can be defined separated by comma.', + }, + { + displayName: 'Direct Mentions', + name: 'direct_mentions', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: [], + description: `The users that are directly mentioned`, + }, + // { + // displayName: 'Direct Group Mentions ', + // name: 'direct_group_mentions', + // type: 'multiOptions', + // typeOptions: { + // loadOptionsMethod: 'getGroups', + // }, + // default: [], + // description: `The groups that are directly mentioned`, + // }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Twist/Twist.node.ts b/packages/nodes-base/nodes/Twist/Twist.node.ts new file mode 100644 index 0000000000..f46017176d --- /dev/null +++ b/packages/nodes-base/nodes/Twist/Twist.node.ts @@ -0,0 +1,293 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IBinaryKeyData, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + twistApiRequest, +} from './GenericFunctions'; + +import { + channelFields, + channelOperations, +} from './ChannelDescription'; + +import { + messageConversationFields, + messageConversationOperations, +} from './MessageConversationDescription'; + +import uuid = require('uuid'); + +export class Twist implements INodeType { + description: INodeTypeDescription = { + displayName: 'Twist', + name: 'twist', + icon: 'file:twist.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Twist API', + defaults: { + name: 'Twist', + color: '#316fea', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'twistOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Channel', + value: 'channel', + }, + { + name: 'Message Conversation', + value: 'messageConversation', + }, + ], + default: 'messageConversation', + description: 'The resource to operate on.', + }, + ...channelOperations, + ...channelFields, + ...messageConversationOperations, + ...messageConversationFields, + ], + }; + + methods = { + loadOptions: { + // Get all the available workspaces to display them to user so that he can + // select them easily + async getWorkspaces(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const workspaces = await twistApiRequest.call(this, 'GET', '/workspaces/get'); + for (const workspace of workspaces) { + returnData.push({ + name: workspace.name, + value: workspace.id, + }); + } + + return returnData; + }, + // Get all the available conversations to display them to user so that he can + // select them easily + async getConversations(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = { + workspace_id: this.getCurrentNodeParameter('workspaceId') as string, + }; + const conversations = await twistApiRequest.call(this, 'GET', '/conversations/get', {}, qs); + for (const conversation of conversations) { + returnData.push({ + name: conversation.title || conversation.id, + value: conversation.id, + }); + } + return returnData; + }, + + // Get all the available users to display them to user so that he can + // select them easily + async getUsers(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = { + id: this.getCurrentNodeParameter('workspaceId') as string, + }; + const users = await twistApiRequest.call(this, 'GET', '/workspaces/get_users', {}, qs); + for (const user of users) { + returnData.push({ + name: user.name, + value: user.id, + }); + } + return returnData; + }, + + // Get all the available groups to display them to user so that he can + // select them easily + async getGroups(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const qs: IDataObject = { + workspace_id: this.getCurrentNodeParameter('workspaceId') as string, + }; + const groups = await twistApiRequest.call(this, 'GET', '/groups/get', {}, qs); + for (const group of groups) { + returnData.push({ + name: group.name, + value: group.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'channel') { + //https://developer.twist.com/v3/#add-channel + if (operation === 'create') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const name = this.getNodeParameter('name', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + workspace_id: workspaceId, + name, + }; + Object.assign(body, additionalFields); + + responseData = await twistApiRequest.call(this, 'POST', '/channels/add', body); + } + //https://developer.twist.com/v3/#get-channel + if (operation === 'get') { + const channelId = this.getNodeParameter('channelId', i) as string; + qs.id = channelId; + + responseData = await twistApiRequest.call(this, 'GET', '/channels/getone', {}, qs); + } + //https://developer.twist.com/v3/#get-all-channels + if (operation === 'getAll') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('filters', i) as IDataObject; + qs.workspace_id = workspaceId; + Object.assign(qs, filters); + + responseData = await twistApiRequest.call(this, 'GET', '/channels/get', {}, qs); + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.splice(0, limit); + } + } + //https://developer.twist.com/v3/#update-channel + if (operation === 'update') { + const channelId = this.getNodeParameter('channelId', i) as string; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + const body: IDataObject = { + id: channelId, + }; + Object.assign(body, updateFields); + + responseData = await twistApiRequest.call(this, 'POST', '/channels/update', body); + } + } + if (resource === 'messageConversation') { + //https://developer.twist.com/v3/#add-message-to-conversation + if (operation === 'create') { + const workspaceId = this.getNodeParameter('workspaceId', i) as string; + const conversationId = this.getNodeParameter('conversationId', i) as string; + const content = this.getNodeParameter('content', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const body: IDataObject = { + conversation_id: conversationId, + workspace_id: workspaceId, + content, + }; + Object.assign(body, additionalFields); + + if (body.actionsUi) { + const actions = (body.actionsUi as IDataObject).actionValues as IDataObject[]; + + if (actions) { + body.actions = actions; + delete body.actionsUi; + } + } + + if (body.binaryProperties) { + const binaryProperties = (body.binaryProperties as string).split(',') as string[]; + + const attachments: IDataObject[] = []; + + for (const binaryProperty of binaryProperties) { + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryProperty] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryProperty}" does not exists on item!`); + } + + attachments.push(await twistApiRequest.call( + this, + 'POST', + `/attachments/upload`, + {}, + {}, + { + formData: { + file_name: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }, + attachment_id: uuid(), + }, + }, + )); + } + + body.attachments = attachments; + } + + if (body.direct_mentions) { + const direcMentions: string[] = []; + for (const directMention of body.direct_mentions as number[]) { + direcMentions.push(`[name](twist-mention://${directMention})`); + } + body.content = `${direcMentions.join(' ')} ${body.content}`; + } + + // if (body.direct_group_mentions) { + // const directGroupMentions: string[] = []; + // for (const directGroupMention of body.direct_group_mentions as number[]) { + // directGroupMentions.push(`[Group name](twist-group-mention://${directGroupMention})`); + // } + // body.content = `${directGroupMentions.join(' ')} ${body.content}`; + // } + + responseData = await twistApiRequest.call(this, 'POST', '/conversation_messages/add', body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Twist/twist.png b/packages/nodes-base/nodes/Twist/twist.png new file mode 100644 index 0000000000000000000000000000000000000000..37a82172093860be344d5fd13088b805e39cfafe GIT binary patch literal 1360 zcmV-W1+V&vP)@9HYH&dc6a_}y4@Gf)lFLd)Vc>6AF=`eKl7B_PT2N5oD?k#WXEpO{9Zt5R4 zbS`o3001B`b@nfG@-A@eFLLuOaO*B`>oRWaE^+G|I&~O0bSGlX0002s=ld~s_bO@K zE_3uQaO^5#&Mt53FLU!PZ|o^)-Z6CYDrnpoalaGiKir zHF8~(`Uqv~>+A0UMDPFq{~tVb+1=qSaO?m7|NZ{|E^zBLa_s#5|1ojvICSeWa_sv1 z{wr_mSc~uY`ThxE>dMymp1JfKXX^r9>hJRXFLd-ebn7E*>mY0E8*1x6ckJ=={V8ti zCT;8G>-^&A`@+rkq`md1y7Y^s@neI`1$1X=0Y z;rY|t`OVn)!q51~)Ay09@r0o8J$>y9X6gxK>hksdxySXd!S$B3^oXzYfTr?jneb4G z?qG@THhSz1YU>qg>lI?^^Y;C6r}CSy@phK)SCH>;lI}o!>_&O)08{Dw>JuLT002F7 zQchC<1N;32{O4L~V_x2?5L z-vHIM&zX^FwY#x*s9mS8%Z}bbt}6FCtsfMKJMVZV}h+rVY1zavyK!zx$l6C)t%GzXu&?kO$ zC=~kTix%7^ckX=aFn+KLg#c)~y|Rug!zy^{p0rI2B(C;~rOWKMWPH`bVi>ko^Z^RG zQ(O6p_2UjD++z`sG_iB~C9$y<3-dUR3#4Z~@jVZtAmhNoRN2~Sd8LG(S%q=9yCEz% zj*}!QpyBwo%O0S8g|lgv#6B^VxXsU%@NkPTOxqn2Ea3e5*Naktq9IpIc`#uV!7Wdf z@S<6S-3}>O$8q|n%N2zT<%*odFgb(2u=R-&zPlnhi*T~&=4dEPrRc);G)7{As+~K# zm9PPnVVqr#`lNZ#f1M-+ObLT7B@86kEx|!y8o>lFoG}TNVGk-8gO;=_SISv4dJC-AsXhTazfRnZ5YnqdP8sDCJ-Mkqfn#<>@hW9p-sc+6u zMH5857enpce&e^Q`B$>}_qqAbpm9%PMm Date: Thu, 10 Dec 2020 05:30:13 -0500 Subject: [PATCH 05/97] :sparkles: Add NASA Node (#1232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: NASA node * Test up to asteroidNeoLookup * Add earth imagery * :zap: Small improvement * :zap: Improvements to NASA node * :zap: Small Improvements * :zap: Some minor fixes for NASA Node Co-authored-by: Iván Ovejero Co-authored-by: Jan Oberhauser --- .../credentials/NasaApi.credentials.ts | 17 + .../nodes-base/nodes/Nasa/GenericFunctions.ts | 71 + packages/nodes-base/nodes/Nasa/Nasa.node.ts | 1235 +++++++++++++++++ packages/nodes-base/nodes/Nasa/nasa.png | Bin 0 -> 2570 bytes packages/nodes-base/package.json | 2 + 5 files changed, 1325 insertions(+) create mode 100644 packages/nodes-base/credentials/NasaApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Nasa/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Nasa/Nasa.node.ts create mode 100644 packages/nodes-base/nodes/Nasa/nasa.png diff --git a/packages/nodes-base/credentials/NasaApi.credentials.ts b/packages/nodes-base/credentials/NasaApi.credentials.ts new file mode 100644 index 0000000000..b6bfbd4560 --- /dev/null +++ b/packages/nodes-base/credentials/NasaApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class NasaApi implements ICredentialType { + name = 'nasaApi'; + displayName = 'NASA API'; + properties = [ + { + displayName: 'API Key', + name: 'api_key', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Nasa/GenericFunctions.ts b/packages/nodes-base/nodes/Nasa/GenericFunctions.ts new file mode 100644 index 0000000000..50331f8ad0 --- /dev/null +++ b/packages/nodes-base/nodes/Nasa/GenericFunctions.ts @@ -0,0 +1,71 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function nasaApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, qs: IDataObject, option: IDataObject = {}, uri?: string | undefined): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('nasaApi') as IDataObject; + + qs.api_key = credentials['api_key'] as string; + + const options: OptionsWithUri = { + method, + qs, + uri: uri || `https://api.nasa.gov${endpoint}`, + json: true, + }; + + if (Object.keys(option)) { + Object.assign(options, option); + } + + try { + return await this.helpers.request(options); + + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The NASA credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.msg) { + // Try to return the error prettier + throw new Error(`NASA error response [${error.statusCode}]: ${error.response.body.msg}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function nasaApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query.size = 20; + + let uri: string | undefined = undefined; + + do { + responseData = await nasaApiRequest.call(this, method, resource, query, {}, uri); + uri = responseData.links.next; + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.links.next !== undefined + ); + + return returnData; +} + + diff --git a/packages/nodes-base/nodes/Nasa/Nasa.node.ts b/packages/nodes-base/nodes/Nasa/Nasa.node.ts new file mode 100644 index 0000000000..c6a4f2e569 --- /dev/null +++ b/packages/nodes-base/nodes/Nasa/Nasa.node.ts @@ -0,0 +1,1235 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + nasaApiRequest, + nasaApiRequestAllItems, +} from './GenericFunctions'; + +import * as moment from 'moment'; + +export class Nasa implements INodeType { + description: INodeTypeDescription = { + displayName: 'NASA', + name: 'nasa', + icon: 'file:nasa.png', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}', + description: 'Retrieve data the from NASA API', + defaults: { + name: 'NASA', + color: '#0B3D91', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'nasaApi', + required: true, + }, + ], + properties: [ + // ---------------------------------- + // resources + // ---------------------------------- + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Astronomy Picture of the Day', + value: 'astronomyPictureOfTheDay', + }, + { + name: 'Asteroid Neo-Feed', + value: 'asteroidNeoFeed', + }, + { + name: 'Asteroid Neo-Lookup', + value: 'asteroidNeoLookup', + }, + { + name: 'Asteroid Neo-Browse', + value: 'asteroidNeoBrowse', + }, + { + name: 'DONKI Coronal Mass Ejection', + value: 'donkiCoronalMassEjection', + }, + // { + // name: 'DONKI Geomagnetic Storm', + // value: 'donkiGeomagneticStorm', + // }, + { + name: 'DONKI Interplanetary Shock', + value: 'donkiInterplanetaryShock', + }, + { + name: 'DONKI Solar Flare', + value: 'donkiSolarFlare', + }, + { + name: 'DONKI Solar Energetic Particle', + value: 'donkiSolarEnergeticParticle', + }, + { + name: 'DONKI Magnetopause Crossing', + value: 'donkiMagnetopauseCrossing', + }, + { + name: 'DONKI Radiation Belt Enhancement', + value: 'donkiRadiationBeltEnhancement', + }, + { + name: 'DONKI High Speed Stream', + value: 'donkiHighSpeedStream', + }, + { + name: 'DONKI WSA+EnlilSimulation', + value: 'donkiWsaEnlilSimulation', + }, + { + name: 'DONKI Notifications', + value: 'donkiNotifications', + }, + { + name: 'Earth Imagery', + value: 'earthImagery', + }, + { + name: 'Earth Assets', + value: 'earthAssets', + }, + ], + default: 'astronomyPictureOfTheDay', + description: 'The resource to operate on', + }, + // ---------------------------------- + // operations + // ---------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'astronomyPictureOfTheDay', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get the Astronomy Picture of the Day', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'asteroidNeoFeed', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve a list of asteroids based on their closest approach date to Earth', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'asteroidNeoLookup', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Look up an asteroid based on its NASA SPK-ID', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'asteroidNeoBrowse', + ], + }, + }, + options: [ + { + name: 'Get All', + value: 'getAll', + description: 'Browse the overall asteroid dataset', + }, + ], + default: 'getAll', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiCoronalMassEjection', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI coronal mass ejection data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiGeomagneticStorm', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI geomagnetic storm data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiInterplanetaryShock', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI interplanetary shock data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiSolarFlare', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI solar flare data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiSolarEnergeticParticle', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI solar energetic particle data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiMagnetopauseCrossing', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve data on DONKI magnetopause crossings', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiRadiationBeltEnhancement', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI radiation belt enhancement data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiHighSpeedStream', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI high speed stream data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiWsaEnlilSimulation', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI WSA+EnlilSimulation data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'donkiNotifications', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve DONKI notifications data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'earthImagery', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve Earth imagery', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'earthAssets', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve Earth assets', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'inSightMarsWeatherService', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve Insight Mars Weather Service data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'imageAndVideoLibrary', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve Image and Video Library data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'techTransfer', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve TechTransfer data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'twoLineElementSet', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Retrieve Two-Line Element Set data', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + + // ---------------------------------- + // fields + // ---------------------------------- + + /* asteroidId and additionalFields (includeCloseApproachData) for asteroidNeoLookup */ + { + displayName: 'Asteroid ID', + name: 'asteroidId', + type: 'string', + required: true, + default: '', + placeholder: '3542519', + description: 'The ID of the asteroid to be returned', + displayOptions: { + show: { + resource: [ + 'asteroidNeoLookup', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'asteroidNeoLookup', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Include Close Approach Data', + name: 'includeCloseApproachData', + type: 'boolean', + default: false, + description: 'Whether to include all the close approach data in the asteroid lookup', + }, + ], + }, + { + displayName: 'Download Image', + name: 'download', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'astronomyPictureOfTheDay', + ], + }, + }, + default: true, + description: 'By default just the url of the image is returned. When set to true the image will be downloaded', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'astronomyPictureOfTheDay', + ], + download: [ + true, + ], + }, + }, + description: 'Name of the binary property to which to write to', + }, + + /* date for astronomyPictureOfTheDay */ + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + default: {}, + placeholder: 'Add field', + displayOptions: { + show: { + resource: [ + 'astronomyPictureOfTheDay', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + }, + ], + }, + + + /* startDate and endDate for various resources */ + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + default: {}, + placeholder: 'Add field', + displayOptions: { + show: { + resource: [ + 'asteroidNeoFeed', + 'donkiCoronalMassEjection', + 'donkiGeomagneticStorm', + 'donkiSolarFlare', + 'donkiSolarEnergeticParticle', + 'donkiMagnetopauseCrossing', + 'donkiRadiationBeltEnhancement', + 'donkiHighSpeedStream', + 'donkiWsaEnlilSimulation', + 'donkiNotifications', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Start date', + name: 'startDate', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + }, + { + displayName: 'End date', + name: 'endDate', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + }, + ], + }, + + /* startDate, endDate, location and catalog for donkiInterplanetaryShock */ + // Note: If I move startDate and endDate to the Additional Fields above, + // then this resource gets _two_ Additional Fields with two fields each, + // instead of _one_ Additional Fields with four fields. So I cannot avoid + // duplication without cluttering up the UI. Ideas? + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + default: {}, + placeholder: 'Add field', + displayOptions: { + show: { + resource: [ + 'donkiInterplanetaryShock', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Start date', + name: 'startDate', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + }, + { + displayName: 'End date', + name: 'endDate', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + }, + { + displayName: 'Location', + name: 'location', + type: 'options', + default: 'ALL', + description: 'The location of the geomagnetic storm', + options: [ + { + name: 'All', + value: 'ALL', + }, + { + name: 'Earth', + value: 'earth', + }, + { + name: 'Messenger', + value: 'MESSENGER', + }, + { + name: 'Stereo A', + value: 'STEREO A', + }, + { + name: 'Stereo B', + value: 'STEREO B', + }, + ], + }, + { + displayName: 'Catalog', + name: 'catalog', + type: 'options', + default: 'ALL', + description: 'The catalog of the geomagnetic storm', + options: [ + { + name: 'All', + value: 'ALL', + }, + { + name: 'SWRC Catalog', + value: 'SWRC_CATALOG', + }, + { + name: 'Winslow Messenger ICME Catalog', + value: 'WINSLOW_MESSENGER_ICME_CATALOG', + }, + ], + }, + ], + }, + + /* latitude, longitude and additionaFields (date, degrees) for earthImagery and earthAssets*/ + { + displayName: 'Latitude', + name: 'lat', + type: 'number', + default: '', + placeholder: '47.751076', + description: 'Latitude for the location of the image', + displayOptions: { + show: { + resource: [ + 'earthImagery', + 'earthAssets', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Longitude', + name: 'lon', + type: 'number', + default: '', + placeholder: '-120.740135', + description: 'Longitude for the location of the image', + displayOptions: { + show: { + resource: [ + 'earthImagery', + 'earthAssets', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'get', + ], + resource: [ + 'earthImagery', + ], + }, + }, + description: 'Name of the binary property to which to write to', + }, + + + //aqui + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + default: {}, + placeholder: 'Add field', + displayOptions: { + show: { + resource: [ + 'earthImagery', + 'earthAssets', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Date', + name: 'date', + type: 'dateTime', + default: '', + placeholder: 'YYYY-MM-DD', + description: 'Date of the image', + }, + { + displayName: 'Degrees', + name: 'dim', + type: 'number', + default: '', + placeholder: '0.025', + description: 'Width and height of the image in degrees', + }, + ], + }, + + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 20, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + ], + }; + + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + const qs: IDataObject = {}; + let returnAll = false; + let propertyName = ''; + let download = false; + + for (let i = 0; i < items.length; i++) { + + let endpoint = ''; + let includeCloseApproachData = false; + + // additionalFields are brought up here to prevent repetition on most endpoints. + // The few endpoints like asteroidNeoBrowse that do not have additionalFields + // trigger an error in getNodeParameter dealt with in the catch block. + let additionalFields; + try { + additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + } catch (error) { + additionalFields = {} as IDataObject; + } + + if (resource === 'astronomyPictureOfTheDay') { + + if (operation === 'get') { + + endpoint = '/planetary/apod'; + + qs.date = moment(additionalFields.date as string).format('YYYY-MM-DD') || moment().format('YYYY-MM-DD'); + + } + } + if (resource === 'asteroidNeoFeed') { + + if (operation === 'get') { + + endpoint = '/neo/rest/v1/feed'; + + propertyName = 'near_earth_objects'; + + // The range defaults to the current date to reduce the number of results. + const currentDate = moment().format('YYYY-MM-DD'); + qs.start_date = moment(additionalFields.startDate as string).format('YYYY-MM-DD') || currentDate; + qs.end_date = moment(additionalFields.endDate as string).format('YYYY-MM-DD') || currentDate; + + } + } + if (resource === 'asteroidNeoLookup') { + + if (operation === 'get') { + + const asteroidId = this.getNodeParameter('asteroidId', i) as IDataObject; + + includeCloseApproachData = additionalFields.includeCloseApproachData as boolean; + + endpoint = `/neo/rest/v1/neo/${asteroidId}`; + + } else { + throw new Error(`The operation '${operation}' is unknown!`); + } + + } + if (resource === 'asteroidNeoBrowse') { + + if (operation === 'getAll') { + + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll === false) { + qs.size = this.getNodeParameter('limit', 0) as number; + } + + propertyName = 'near_earth_objects'; + + endpoint = `/neo/rest/v1/neo/browse`; + + } else { + throw new Error(`The operation '${operation}' is unknown!`); + } + + } + if (resource.startsWith('donki')) { + + if (additionalFields.startDate) { + qs.startDate = moment(additionalFields.startDate as string).format('YYYY-MM-DD'); + } else { + qs.startDate = moment().subtract(30, 'days').format('YYYY-MM-DD'); + } + + if (additionalFields.endDate) { + qs.endDate = moment(additionalFields.endDate as string).format('YYYY-MM-DD'); + } else { + qs.endDate = moment().format('YYYY-MM-DD'); + } + + if (resource === 'donkiCoronalMassEjection') { + + if (operation === 'get') { + + endpoint = '/DONKI/CME'; + + } + + } else if (resource === 'donkiGeomagneticStorm') { + + if (operation === 'get') { + + endpoint = '/DONKI/GST'; + + } + + } else if (resource === 'donkiInterplanetaryShock') { + + if (operation === 'get') { + + endpoint = '/DONKI/IPS'; + + qs.location = additionalFields.location as string || 'ALL'; // default per API + qs.catalog = additionalFields.catalog as string || 'ALL'; // default per API + + } + + } else if (resource === 'donkiSolarFlare') { + + if (operation === 'get') { + + endpoint = '/DONKI/FLR'; + + } + + } else if (resource === 'donkiSolarEnergeticParticle') { + + if (operation === 'get') { + + endpoint = '/DONKI/SEP'; + + } + + } else if (resource === 'donkiMagnetopauseCrossing') { + + if (operation === 'get') { + + endpoint = '/DONKI/MPC'; + + } + + } else if (resource === 'donkiRadiationBeltEnhancement') { + + if (operation === 'get') { + + endpoint = '/DONKI/RBE'; + + } + + } else if (resource === 'donkiHighSpeedStream') { + + if (operation === 'get') { + + endpoint = '/DONKI/HSS'; + + } + + } else if (resource === 'donkiWsaEnlilSimulation') { + + if (operation === 'get') { + + endpoint = '/DONKI/WSAEnlilSimulations'; + + } + } else if (resource === 'donkiNotifications') { + + if (operation === 'get') { + + endpoint = '/DONKI/notifications'; + + qs.type = additionalFields.type as string || 'all'; // default per API + + } + } + + } + if (resource === 'earthImagery') { + + if (operation === 'get') { + + endpoint = '/planetary/earth/imagery'; + + qs.lat = this.getNodeParameter('lat', i) as IDataObject; + qs.lon = this.getNodeParameter('lon', i) as IDataObject; + + qs.dim = additionalFields.dim as string || 0.025; // default per API + + if (additionalFields.date) { + qs.date = moment(additionalFields.date as string).format('YYYY-MM-DD'); + } else { + qs.date = moment().format('YYYY-MM-DD'); + } + } + + } + if (resource === 'earthAssets') { + + if (operation === 'get') { + + endpoint = '/planetary/earth/assets'; + + qs.lat = this.getNodeParameter('lat', i) as IDataObject; + qs.lon = this.getNodeParameter('lon', i) as IDataObject; + + qs.dim = additionalFields.dim as string || 0.025; // default per API + + if (additionalFields.date) { + qs.date = moment(additionalFields.date as string).format('YYYY-MM-DD'); + } + } + + if (operation === 'get') { + + endpoint = '/insight_weather/earth/imagery'; + + // Hardcoded because these are the only options available right now. + qs.feedtype = 'json'; + qs.ver = '1.0'; + + } + } + + if (returnAll) { + responseData = nasaApiRequestAllItems.call(this, propertyName, 'GET', endpoint, qs); + } else { + responseData = await nasaApiRequest.call(this, 'GET', endpoint, qs); + + if (propertyName !== '') { + responseData = responseData[propertyName]; + } + } + + if (resource === 'asteroidNeoLookup' && operation === 'get' && !includeCloseApproachData) { + delete responseData.close_approach_data; + } + + if (resource === 'asteroidNeoFeed') { + const date = Object.keys(responseData)[0]; + responseData = responseData[date]; + } + + if (resource === 'earthImagery') { + + const binaryProperty = this.getNodeParameter('binaryPropertyName', i) as string; + + const data = await nasaApiRequest.call(this, 'GET', endpoint, qs, { encoding: null }); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + if (items[i].binary !== undefined) { + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data); + } + + if (resource === 'astronomyPictureOfTheDay') { + download = this.getNodeParameter('download', 0) as boolean; + + if (download === true) { + + const binaryProperty = this.getNodeParameter('binaryPropertyName', i) as string; + + const data = await nasaApiRequest.call(this, 'GET', endpoint, qs, { encoding: null }, responseData.hdurl); + + const filename = (responseData.hdurl as string).split('/'); + + const newItem: INodeExecutionData = { + json: items[i].json, + binary: {}, + }; + + Object.assign(newItem.json, responseData); + + if (items[i].binary !== undefined) { + Object.assign(newItem.binary, items[i].binary); + } + + items[i] = newItem; + + items[i].binary![binaryProperty] = await this.helpers.prepareBinaryData(data, filename[filename.length - 1]); + } + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + + if (resource === 'earthImagery' && operation === 'get') { + return this.prepareOutputData(items); + } else if (resource === 'astronomyPictureOfTheDay' && operation === 'get' && download === true) { + return this.prepareOutputData(items); + } else { + return [this.helpers.returnJsonArray(returnData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Nasa/nasa.png b/packages/nodes-base/nodes/Nasa/nasa.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3a85e20dc2f784e2e7956c9621f23f1b9d15ca GIT binary patch literal 2570 zcmV+l3ib7gP)TR(u>deu03|}g7af8R6*346 z0BgGQ9VY-^r~qfQC1kGvXR-ict^hh>09cvr8y^5!q*@Cj0BX2g2nYadz3dz$0BO1a zRhRG+6aZ$m=olXF93lWVVgOyIPYMdK6co87KxbQ?^Bg1qRh7&%Sanj807ZP?8X~zC z9RO04l@S_k4iZ-k3;<@b1ZJ`aX0rYM{s3gM3TLtbVzClvvmk4<0B5ufX0reP{~c0l%5@xazVzCZgumVx615K>)^Z3Ba;ESWoQ-QupeY*f_ zx;}TgE^)WXK4t71BkdL#xyRpoo5(b8xB*nIF=VX(N~={|q0lZz@d*Rv>hjLl=%2LK zkg3p6i^g)2#Ab}aIeEH8c)AT~v?FD)3ShAUSgvJNmbpfF=^!ru{{Hj!`u_3p-{tV~ z;pN8B&n*WZlutKoykX!#+{wMHha84ZoF=Bvm9HoaB8MKU!_iGoiJFPt}1&PG{jtVbKsd;w>xI5+3Q%*vrGzQkTkWg~#Er z#2|&lQ-!}je7iDry79HSN@lq*YPUImwk)?IFI_xuhx zILF=H-QC^Y-QC^Y9~c(TEmiOaRs5+Wm88Gwzb9Sk5dZbh3jzS}(SL@2N?b)0fDfn*czXATUehDV;eYilj-BcW4lqDN|Gg zon1vxk~jqn;8>L*Ym$~G(I*L3mNi*P8YN}Tpn~v+Fh$YAXV6JER|n|NW|*e-tD$b9qtg#u_+oBeUnz3 zpTY=)^%@l~456gn3OR7KR#W6;PrPSJSFxRi7cC6>E>d0&DqxX(dGppw^QvGjIgT*4 z?{X(@Wc%TsE(u-(5}e@)rf;-7OCF+4@r)U31BG;jMuSnf3n4*jx$fb;WOY> zw|rw6-R$h;i)ZXqwZ1ejC?LZu654w_bBClP%!*-_O%2=I_PcE>E3%_{cg~nGUlAZ> zl|fqMhr{CMvLb^aiz&KGxa|uqglN&x-km#j%2>Oyf{?;xtI+Q%6XhwisAtS#0y>U5_qojPyqe7Y1! z+n0H=(4T?WxgAq<-_8PmB>YOmByJz`_&3H!*E3Gdj(jj=2e0$?jEnQd4o9*c zFV=17v$ap3-m80DJio%oB6(?olENkDn399-_O2uPPl)gDGcE2H96UAT;IXym<}I2y zZ{C|xZ4aHjJ*mGH!zHCKBy!0#J^OK7hoRH-j_HGav)mjvvG;{B8^-Iet(w#~vDfkg$8l2z=^fp=v>&jIUswk=N>4s z+8{tklshxh%n$C%x$Haj3Et(W7Ofnw=Z?HRI>_bHk6XC>Li+xM!RCjM+B&vYrd>K0CTg2iLrnD?3`++pqT5a}!32C@owmvleGg*r^}O zaVJMV-_0%Ppy#;3zP3yEojE>!Y=DG7QF3uhn=I+>K0>|}>iwS~KmdG~y@QH~>ULRQCb`NNm;GQ)3eyGbm z?%t{6mqLTxeWwo?XP^HtjB~jirmT`CfsknW>cpYs6kFR{GX~DE4jQ;;R=@bY`_7(w zym;W$sRO&2mTaG4;HU4}b!ADW>_5YhC|sjSk0_)uA{1l9T_hO9c7yE)E{tZ>YSHNx z4T5iSqRp)4@rDXWGMBGazi5sS7JV11@h#u*GKsMXtSGWb=Zl}VY^XZLEU-BWNt&dx z0_Mp^`zZ!tnRK=%sSS4Ti2nK^5xnu9b~DwHdw4pF5=goe7hMMr3LYUf!h1r*n%;+v zHv5FZBm68MDR!MQf>jT7sAo3|Fyla>Vl*-$VF*qh8<~2s2MTH1%|77EcR}r(ImM*r z%_A~ZDF^~h$81UD5W$kjGSLUmuk9fU$U36Em$f2BU{H*2(13PBO}s9u1OWrV>4Ibo zZ%8z1SFi6S++;shYGH*)wK-BF#Ds@NwwpFRWlolKAn;tSK$8q_}y9^DttsN~(Nmd&K#2UiY?572EI{Db{2wUMZ)ubpQ zuz)hFVfdQWb9BZKXR^p3Hiq}VI#ys9BR(V9Po|}G#fGDM8Lg~tu?9Q-QPxDBAtSP; zBYrK|sz#|fnea&JYUa_;;t?5D5 Date: Thu, 10 Dec 2020 11:36:23 +0100 Subject: [PATCH 06/97] :bookmark: Release n8n-core@0.55.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index cd8b36298e..7740099ec0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.54.0", + "version": "0.55.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From e0b1f88a4dc12085eca1dde69e3881bd2c4e268c Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 11:37:16 +0100 Subject: [PATCH 07/97] :arrow_up: Set n8n-core@0.55.0 on n8n-nodes-base --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 7fbcfc9e43..b6dbf5fb66 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -528,7 +528,7 @@ "mqtt": "4.2.1", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.54.0", + "n8n-core": "~0.55.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", From 42e243c3961e343f8a1c0e17d5afb793116315d3 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 11:37:46 +0100 Subject: [PATCH 08/97] :bookmark: Release n8n-nodes-base@0.92.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b6dbf5fb66..72e93575ad 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.91.0", + "version": "0.92.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 7d29c91f07a7a9cbec0a41c17ac2caf9487954a5 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 11:39:27 +0100 Subject: [PATCH 09/97] :arrow_up: Set n8n-core@0.55.0 and n8n-nodes-base@0.92.0 on n8n --- packages/cli/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a2e3a200aa..4a11a8e5db 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -103,9 +103,9 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "~2.1.0", - "n8n-core": "~0.54.0", + "n8n-core": "~0.55.0", "n8n-editor-ui": "~0.66.0", - "n8n-nodes-base": "~0.91.0", + "n8n-nodes-base": "~0.92.0", "n8n-workflow": "~0.47.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", From cfc222a87758461576156b0ea250c281096c9135 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 10 Dec 2020 11:40:12 +0100 Subject: [PATCH 10/97] :bookmark: Release n8n@0.97.0 --- 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 4a11a8e5db..9cbf8fd3c8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.96.0", + "version": "0.97.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 3e0b9f2eba201c539a6c96421cf8e4ad2e3d5478 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 11 Dec 2020 18:15:07 +0100 Subject: [PATCH 11/97] :zap: Add "isEmpty" option to IF-Node --- packages/nodes-base/nodes/If.node.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/nodes-base/nodes/If.node.ts b/packages/nodes-base/nodes/If.node.ts index 447f985bac..f5965a877d 100644 --- a/packages/nodes-base/nodes/If.node.ts +++ b/packages/nodes-base/nodes/If.node.ts @@ -112,6 +112,10 @@ export class If implements INodeType { name: 'Larger Equal', value: 'largerEqual', }, + { + name: 'Is Empty', + value: 'isEmpty', + }, ], default: 'smaller', description: 'Operation to decide where the the data should be mapped to.', @@ -120,6 +124,13 @@ export class If implements INodeType { displayName: 'Value 2', name: 'value2', type: 'number', + displayOptions: { + hide: { + operation: [ + 'isEmpty', + ], + }, + }, default: 0, description: 'The value to compare with the first one.', }, @@ -161,6 +172,10 @@ export class If implements INodeType { name: 'Regex', value: 'regex', }, + { + name: 'Is Empty', + value: 'isEmpty', + }, ], default: 'equal', description: 'Operation to decide where the the data should be mapped to.', @@ -172,6 +187,7 @@ export class If implements INodeType { displayOptions: { hide: { operation: [ + 'isEmpty', 'regex', ], }, @@ -242,6 +258,7 @@ export class If implements INodeType { largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0), smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0), smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0), + isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string), regex: (value1: NodeParameterValue, value2: NodeParameterValue) => { const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimy]*)$')); From a2222fa5d6ff5e3a6d22eddeda2316f67a4b1bba Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 11 Dec 2020 19:17:53 +0100 Subject: [PATCH 12/97] :bug: Fix issue that workflow displayed as changed on activate/deactivate --- packages/editor-ui/src/store.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index 8275908fc0..ca7c125ced 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -138,14 +138,12 @@ export const store = new Vuex.Store({ state.activeWorkflows = newActiveWorkflows; }, setWorkflowActive (state, workflowId: string) { - state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index === -1) { state.activeWorkflows.push(workflowId); } }, setWorkflowInactive (state, workflowId: string) { - state.stateIsDirty = true; const index = state.activeWorkflows.indexOf(workflowId); if (index !== -1) { state.selectedNodes.splice(index, 1); From a546f599cd5f56b32bd13f0973691001f6359b73 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 11 Dec 2020 19:22:24 +0100 Subject: [PATCH 13/97] :bug: Additional fix to dirty check --- packages/editor-ui/src/store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor-ui/src/store.ts b/packages/editor-ui/src/store.ts index ca7c125ced..92437129e8 100644 --- a/packages/editor-ui/src/store.ts +++ b/packages/editor-ui/src/store.ts @@ -138,6 +138,7 @@ export const store = new Vuex.Store({ state.activeWorkflows = newActiveWorkflows; }, setWorkflowActive (state, workflowId: string) { + state.stateIsDirty = false; const index = state.activeWorkflows.indexOf(workflowId); if (index === -1) { state.activeWorkflows.push(workflowId); From c90da82a7b168b04564f79d02c6fbc3eef53021b Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Fri, 11 Dec 2020 22:26:38 +0100 Subject: [PATCH 14/97] :shirt: Fix lint issue --- packages/nodes-base/nodes/RssFeedRead.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/RssFeedRead.node.ts b/packages/nodes-base/nodes/RssFeedRead.node.ts index 96bcb4550e..503a2f0cf7 100644 --- a/packages/nodes-base/nodes/RssFeedRead.node.ts +++ b/packages/nodes-base/nodes/RssFeedRead.node.ts @@ -46,7 +46,7 @@ export class RssFeedRead implements INodeType { const parser = new Parser(); - let feed: Parser.Output; + let feed: Parser.Output; try { feed = await parser.parseURL(url); } catch (e) { From 35f6fe8298781d3ee91198a47bf00cc9cdeae1ea Mon Sep 17 00:00:00 2001 From: AxelRueweler Date: Sat, 12 Dec 2020 08:16:06 +0100 Subject: [PATCH 15/97] :zap: Added includeEmptyCells to Spreadsheet node (#1249) * Added description to ShopifyCredentials subdomain * Added includeEmptyCells to Spreadsheet node --- .../nodes-base/nodes/SpreadsheetFile.node.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/nodes-base/nodes/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile.node.ts index c902f02793..1304471c90 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile.node.ts @@ -206,6 +206,20 @@ export class SpreadsheetFile implements INodeType { default: '', description: 'File name to set in binary data. By default will "spreadsheet." be used.', }, + { + displayName: 'Include empty cells', + name: 'includeEmptyCells', + type: 'boolean', + displayOptions: { + show: { + '/operation': [ + 'fromFile', + ], + }, + }, + default: false, + description: 'When reading from file the empty cells will be filled with an empty string in the JSON.', + }, { displayName: 'RAW Data', name: 'rawData', @@ -341,6 +355,10 @@ export class SpreadsheetFile implements INodeType { } } + if (options.includeEmptyCells) { + sheetToJsonOptions.defval = ''; + } + const sheetJson = xlsxUtils.sheet_to_json(workbook.Sheets[sheetName], sheetToJsonOptions); // Check if data could be found in file From 16438ea8f78407cf007d90a722b6220cc9f78131 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 08:16:43 +0100 Subject: [PATCH 16/97] :zap: Minor naming fix --- packages/nodes-base/nodes/SpreadsheetFile.node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile.node.ts index 1304471c90..b7bb5faaeb 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile.node.ts @@ -207,7 +207,7 @@ export class SpreadsheetFile implements INodeType { description: 'File name to set in binary data. By default will "spreadsheet." be used.', }, { - displayName: 'Include empty cells', + displayName: 'Include Empty Cells', name: 'includeEmptyCells', type: 'boolean', displayOptions: { From 8942f403d4390ba2ce5015479915ebbe6390b6ff Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Sat, 12 Dec 2020 12:59:38 +0530 Subject: [PATCH 17/97] :zap: Add credentials documentation link (#1236) --- packages/nodes-base/credentials/TheHiveApi.credentials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/credentials/TheHiveApi.credentials.ts b/packages/nodes-base/credentials/TheHiveApi.credentials.ts index 5298da770c..19251ec588 100644 --- a/packages/nodes-base/credentials/TheHiveApi.credentials.ts +++ b/packages/nodes-base/credentials/TheHiveApi.credentials.ts @@ -6,6 +6,7 @@ import { export class TheHiveApi implements ICredentialType { name = 'theHiveApi'; displayName = 'The Hive API'; + documentationUrl = 'theHive'; properties = [ { displayName: 'API Key', From 45d413de2217b8fd0852ede1393ce9cc0b031225 Mon Sep 17 00:00:00 2001 From: Harshil Agrawal Date: Sat, 12 Dec 2020 13:00:25 +0530 Subject: [PATCH 18/97] :zap: Add credentials documentation link (#1237) --- packages/nodes-base/credentials/CortexApi.credentials.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nodes-base/credentials/CortexApi.credentials.ts b/packages/nodes-base/credentials/CortexApi.credentials.ts index 5cdc70f13a..ce9a6b7dd7 100644 --- a/packages/nodes-base/credentials/CortexApi.credentials.ts +++ b/packages/nodes-base/credentials/CortexApi.credentials.ts @@ -7,6 +7,7 @@ import { export class CortexApi implements ICredentialType { name = 'cortexApi'; displayName = 'Cortex API'; + documentationUrl = 'cortex'; properties = [ { displayName: 'API Key', From 23b61475d954fda96c75acd4813fc05ba6abb07b Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 12 Dec 2020 02:58:58 -0500 Subject: [PATCH 19/97] :sparkles: Add Pushcut Node & Trigger (#1075) * :sparkles: Pushcut Node & Trigger * :zap: Small improvements Co-authored-by: Jan --- .../credentials/PushcutApi.credentials.ts | 17 ++ .../nodes/Pushcut/GenericFunctions.ts | 50 ++++ .../nodes-base/nodes/Pushcut/Pushcut.node.ts | 215 ++++++++++++++++++ .../nodes/Pushcut/PushcutTrigger.node.ts | 130 +++++++++++ packages/nodes-base/nodes/Pushcut/pushcut.png | Bin 0 -> 4009 bytes packages/nodes-base/package.json | 3 + 6 files changed, 415 insertions(+) create mode 100644 packages/nodes-base/credentials/PushcutApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Pushcut/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Pushcut/Pushcut.node.ts create mode 100644 packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Pushcut/pushcut.png diff --git a/packages/nodes-base/credentials/PushcutApi.credentials.ts b/packages/nodes-base/credentials/PushcutApi.credentials.ts new file mode 100644 index 0000000000..e52dc2a29d --- /dev/null +++ b/packages/nodes-base/credentials/PushcutApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class PushcutApi implements ICredentialType { + name = 'pushcutApi'; + displayName = 'Pushcut API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts new file mode 100644 index 0000000000..0bbbc05e30 --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IHookFunctions, +} from 'n8n-workflow'; + +export async function pushcutApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('pushcutApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'API-Key': credentials.apiKey, + }, + method, + body, + qs, + uri: uri || `https://api.pushcut.io/v1${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + const message = error.response.body.error; + + // Try to return the error prettier + throw new Error( + `Pushcut error response [${error.statusCode}]: ${message}` + ); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts new file mode 100644 index 0000000000..66aafe9d05 --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts @@ -0,0 +1,215 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + pushcutApiRequest, +} from './GenericFunctions'; + +export class Pushcut implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushcut', + name: 'pushcut', + icon: 'file:pushcut.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Pushcut API.', + defaults: { + name: 'Pushcut', + color: '#1f2957', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'pushcutApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Notification', + value: 'notification', + }, + ], + default: 'notification', + description: 'The resource to operate on.' + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Send a notification', + }, + ], + default: 'send', + description: 'The resource to operate on.' + }, + { + displayName: 'Notification Name', + name: 'notificationName', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getNotifications', + }, + displayOptions: { + show: { + resource: [ + 'notification', + ], + operation: [ + 'send', + ], + }, + }, + default: 'Notification Name', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Devices', + name: 'devices', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getDevices', + }, + default: '', + description: 'List of devices this notification is sent to. (default is all devices)', + }, + { + displayName: 'Input', + name: 'input', + type: 'string', + default: '', + description: 'Value that is passed as input to the notification action.', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + description: 'Text that is used instead of the one defined in the app.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title that is used instead of the one defined in the app.', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available devices to display them to user so that he can + // select them easily + async getDevices(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const devices = await pushcutApiRequest.call(this, 'GET', '/devices'); + for (const device of devices) { + returnData.push({ + name: device.id, + value: device.id, + }); + } + return returnData; + }, + // Get all the available notifications to display them to user so that he can + // select them easily + async getNotifications(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const notifications = await pushcutApiRequest.call(this, 'GET', '/notifications'); + for (const notification of notifications) { + returnData.push({ + name: notification.title, + value: notification.id, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'notification') { + if (operation === 'send') { + const notificationName = this.getNodeParameter('notificationName', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, additionalFields); + + responseData = await pushcutApiRequest.call( + this, + 'POST', + `/notifications/${encodeURI(notificationName)}`, + body, + ); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts new file mode 100644 index 0000000000..64093c85cf --- /dev/null +++ b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts @@ -0,0 +1,130 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeType, + INodeTypeDescription, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + pushcutApiRequest, +} from './GenericFunctions'; + +export class PushcutTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Pushcut Trigger', + name: 'pushcutTrigger', + icon: 'file:pushcut.png', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when a Github events occurs.', + defaults: { + name: 'Pushcut Trigger', + color: '#1f2957', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'pushcutApi', + required: true, + }, + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Action Name', + name: 'actionName', + type: 'string', + description: 'Choose any name you would like. It will show up as a server action in the app', + default: '', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + const webhookData = this.getWorkflowStaticData('node'); + const actionName = this.getNodeParameter('actionName'); + // Check all the webhooks which exist already if it is identical to the + // one that is supposed to get created. + const endpoint = '/subscriptions'; + const webhooks = await pushcutApiRequest.call(this, 'GET', endpoint, {}); + + for (const webhook of webhooks) { + if (webhook.url === webhookUrl && + webhook.actionName === actionName) { + webhookData.webhookId = webhook.id; + return true; + } + } + + return false; + }, + async create(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const webhookUrl = this.getNodeWebhookUrl('default'); + const actionName = this.getNodeParameter('actionName'); + + const endpoint = '/subscriptions'; + + const body = { + actionName, + url: webhookUrl + }; + + const responseData = await pushcutApiRequest.call(this, 'POST', endpoint, body); + + if (responseData.id === undefined) { + // Required data is missing so was not successful + return false; + } + + webhookData.webhookId = responseData.id as string; + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + if (webhookData.webhookId !== undefined) { + + const endpoint = `/subscriptions/${webhookData.webhookId}`; + + try { + await pushcutApiRequest.call(this, 'DELETE', endpoint); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + } + return true; + }, + }, + }; + + async webhook(this: IWebhookFunctions): Promise { + const body = this.getBodyData() as IDataObject; + + return { + workflowData: [ + this.helpers.returnJsonArray(body) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Pushcut/pushcut.png b/packages/nodes-base/nodes/Pushcut/pushcut.png new file mode 100644 index 0000000000000000000000000000000000000000..82a9d9f87e2b1e021bec1c28af0701c72862f69e GIT binary patch literal 4009 zcmY*cc|4SB*q#N=Flfj=F^24BY{ek!*s>E5vhNLJj6+gcvWz9mSjrYshe)y|5kj^& zWnYiMh{%$C>FDE}zH`3sd4KnNU(fwq_jNz_^LzhzAF`Q=9y22^BLDzkHqggeoOsn! zgD{-Dm4A#APaH7NLQe}&(U16gGP&<=W8h(I43Id%5CA=p4FEo+oIC&^FM$4U3;t4oc9s z4Fmv~*iQ`z$j;?HQM2{FYD2OyHd1!=$D^Iy{9WA9A$Y=R7C<#b`2^zKNzTX+{7t_= z<&ev$zZuFW_%w_`A^)b3d@rMHjLneR{zP}=MYIB12BpS`L?Ts*ZXU`OINe|AC$q~a zFA|BMjKKs42cv`K(f&kFj4T$5#mLBE+M1YfmtT)-a<^kS@t$`;C> z=Wh%-`~)F{!=r>NuN2?9P9Ds$*Bq}rIKVh3M%7VzbPAIrwCVzD$C}lic#m6xit0!$ z?vqsx(5OwR{Q&A_^S$rcg*W-_Ko6f*X!-wSQm;Ahe~(*>SQIx09s|6`<(+}l0I^`prQ|~p~ zQevN9fN>pNzctV&{bwY3dK{Zfj@A520e;moiu}1*fVHYqY8Sb@rG#B&;5+DerDboq z3<2>CLijWs5UcT;V|qt94b$^+)eJEu2=(MIA{O!7XEA}rZ%5u8s;)niapIxNlZX#% zoK8EI@}!=Zr#vVy{piR(wc3*QWUzk4rs`>5hWK`H{V?g~tJK=|IUa@s1yLJmDQJvDIL-L95%h3gC>{Le|KClfGWE3 z%w!crMnx;da^za4-}%h8Q+W>cBq@}+aR@)Wo+8RuZWkE{6*tLxx+UcM^k{)!JWqlT ztr(-t(|E5)iPCm1LsUhGz(PlbBP0RVxj}tt%JrPD1EnbqOb<9KrN>iEd#@oY{4;ve zSV7?oVjhoqkmjrsV&=b_=yDWYAa_X#N|)En)E^K9JW)Rv#`6(6XT3R2%b zp2f)mg(E4QPuYeG`ag*z{Rq3pS1tq8617e1Es-RICqe3A=r%1mNj)g+4+)LP;an*I zy3oa{xPDsfBHc&N;iH1l8Gr4Y0XJ~`F*bNm65xj6i%wi2{|GvMq}4ZFhwwrm#-~z< z-q$hH{%nFQ@Oelz4aHr_n4O@}a?Tx;WSBetbuLi*_R2@*dLsl$Y(tm8Y{$F%u!Al~ z9yRlZt^2!O9j!W_+5)-3P{h2Ugq7#HwvlKRFk0wZ3EeB*yR^Oj6`i{Q(u5*7<1Q)s z2xq#SFC;tJ`0T#<#zNQY77^+-4L(0!V+3RF9sA>haIKJg7*Sh+8)_X|X)Zt=PG`t+ z8+0*`U=Ge98X zfnJ)5%sNN?eQ15+ZIMIx{-?p)irt|Yp$mlc+>55{VNVdU~{{ulu_m?3Nn= zlms=+gXB1s#dQ*mhYu?2>+7~ss$ky?WK%WO8VN2+LswC(*lpo=){)0(28JNA!fzVQ zu^tkeIeQ`t8OBYzf*g#qA3X);HXr5hRNd|?$<32A;Cg>0RM42aK{?dEu(QU8L#L>mIRP0Zs_A&wIS;d*W^sPXhZjDjxWqI+a#)0RQd5-&0 zWz&gawd(zKmF+cNEny|*k+$Y##*Rma3$mpE z#Of_8?7XsO)+|wNxb{Z=-C{!%6t}l!@37bQ&%gczpZTcM%)EB9&4WtasL##L|= zKvUj2VM0r^qu`0f(dQ3-VuD8=VsFy2rv{#);M+(q!_v=6!K;%s|b#pgSRGt4kr7(v*4V0D9=CiUG zVQDp(D*1RuLBmpthk#}MglD3qPegTKK1N1#&c!O2a-(gD>|-L%>1umj%5ml6mjy2+ z+^LpzQiZSk*$LJ;rZ^_SdUIELhOg-OA-9KMj~!lAGh1kuO)rG_jF&RlzxS{h^ecVT z>T{G54kD5CdMGoFZ^iHNH;b6=Z1*58(KWDIv(3&Rmuu!qKV^CfoKK-xQV4i{mNe{j z)RZp$SW#a}DLZA0lO@MkM(;7(1kbkG$j$t}8 zTN*saGIeoB46WVzGD6PsA;rP>Qg7KEm4&Hqcnqc$6>@jaZ)}Pbwe1v{D*@E+NE7fhxeWPyOM& z$%|E+%PNbsxv84sX(ev+J4qeaqX&kVM{Pw%eYl>n=t{qz2z7W(BiaH2<&FnTd?l31 zRpfru;VNd4f5{va^Vk>P%~{E2t2gGm<)c=}D~9tJlgMwl1=TCLN{N=4BI zu+ItGo5LQg>_bdUCkUaDTb_i}BaaVLN>nQ*zkXAK>E!rgfAX0-3tOUo`JEyDJ?w8- z{&J5>m=yag{aHkckH8NN_+25sWO<{msp>ZVaSc{U`#n(2fdY3YJ}!dgSvfT2G@_;k zauoZ!14ZviPFIFw5q~MnW@>?+)kxa{ESe{-%I7DV3CDUa(z-Jw-3%+(ogRrz7!5hx zeS({L%ck5UuPCm~e$5@6?|`GuvWo$>g5hQ^%?K)w+f)9 zAgPJ;{9zI4B)z!x%s#R)*X(cjCWd2VvLk|hZCl!P6Win0mlQtF4_1Y9o literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 72e93575ad..56c1922704 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -166,6 +166,7 @@ "dist/credentials/ProfitWellApi.credentials.js", "dist/credentials/PushbulletOAuth2Api.credentials.js", "dist/credentials/PushoverApi.credentials.js", + "dist/credentials/PushcutApi.credentials.js", "dist/credentials/QuestDb.credentials.js", "dist/credentials/QuickBaseApi.credentials.js", "dist/credentials/Redis.credentials.js", @@ -394,6 +395,8 @@ "dist/nodes/Postmark/PostmarkTrigger.node.js", "dist/nodes/ProfitWell/ProfitWell.node.js", "dist/nodes/Pushbullet/Pushbullet.node.js", + "dist/nodes/Pushcut/Pushcut.node.js", + "dist/nodes/Pushcut/PushcutTrigger.node.js", "dist/nodes/Pushover/Pushover.node.js", "dist/nodes/QuestDb/QuestDb.node.js", "dist/nodes/QuickBase/QuickBase.node.js", From 46e6212750576f802e3bae9826d93c263df503d1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 08:59:25 +0100 Subject: [PATCH 20/97] :zap: Minor improvements to Pushcut Nodes --- .../nodes-base/nodes/Pushcut/Pushcut.node.ts | 2 +- .../nodes/Pushcut/PushcutTrigger.node.ts | 2 +- packages/nodes-base/nodes/Pushcut/pushcut.png | Bin 4009 -> 1871 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts index 66aafe9d05..626d788954 100644 --- a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts +++ b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts @@ -88,7 +88,7 @@ export class Pushcut implements INodeType { ], }, }, - default: 'Notification Name', + default: '', }, { displayName: 'Additional Fields', diff --git a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts index 64093c85cf..694a2b0db2 100644 --- a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts +++ b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts @@ -21,7 +21,7 @@ export class PushcutTrigger implements INodeType { icon: 'file:pushcut.png', group: ['trigger'], version: 1, - description: 'Starts the workflow when a Github events occurs.', + description: 'Starts the workflow when a Pushcut events occurs.', defaults: { name: 'Pushcut Trigger', color: '#1f2957', diff --git a/packages/nodes-base/nodes/Pushcut/pushcut.png b/packages/nodes-base/nodes/Pushcut/pushcut.png index 82a9d9f87e2b1e021bec1c28af0701c72862f69e..5a6cbdde05d8c3e6c7b51bcce4f91f9bcfd622e7 100644 GIT binary patch delta 1867 zcmV-R2ekO9AI}bu8Gi!+000dlDL?=K0{2i%R7C&)03Ik>KtO;VC|VvUS{^7`9w=EJ zC|VvUTOKG|9Vl8JC|VsSS|2G~A}d}SC|({YS{^A|94J~KDqtuoTpuZ103uQ+DOw*b zTO=u4AShbpYS92AS?6ogEGk`EHe4<#Tj*%D9x7b`Bw158Zh!4yZ0Bms>Sm+rWR>e< ziybCd6((5iT~`1dQR!&8MYXXSdbjVt!dUbnRer?p|FVFk31qS{)-; z<8ILBYQ*Siy+E$H=x4F%XRb`ZqU&Xz>|u57UuW)LWFb9W03TWnC0GC=RTcnI=W55~ zYrI0SwnMVBHh+(tl%PVukEtf$U*>>|uBSK6ofpaqVAWRW)4z zCtCm}R~#Hy?Qz)XYtZRx&Essyfh1W~xcNr!RY@E_S2nWusHdpe=Er@oAt;z@F!2o+@XW;AWX9UX$@>lI&!X z>SK^3RE+FngeppdFiL?WM{nt3Y#=;r>0)L{Gh{R`Vd`OB9yVMYELy8gS@U668VFPz z98>ah+ke_?%IRvz>S?~_X}7puu|uw|@N2E?X{qsQs7A70xRvVQL>U zXDKdVc|KYk6;m4xQve=K031mG8b|4A#Pe{w+ka`ZKc=usz^PKjpw(oaPrRH)t((DK znzLD!w^@>;Q;|}Z9UwL>!c{YA{ctUteLU_hocUVDlg+*$0Kx$DoXB;qN-dyz=;Y3;n3I5MOCA&PXS2fq00Z|) zL_t(Y$DGee3Iib!h2b)GJ5QBDGAzACF4pIdrs=q762TwA)yJdY(*+=UtE5;sW9dhX z@kK~weQcn1*kIO7R5D`v6Jjnzc{SnC?SFTiGHazltd)toKCTJ78^;)uK8E#nqC>vv z3hQ%prejX7>A-OYBM$&z+VIS@0f=j!!+DM1o1N1hEX`uFpysgxS!QvOAm(otuwd#v zUbWla^NM?=+;^JCbT2@xo)r%G#KYrr(iC+sBF075W5Pw8y zW@cc7>y;F8uaJj>S+TK4KOH$TW5$eW)22m%KnW1Id1)amXM}4OlG2Wy|6|UaIngs` z9-3ZQxbN+|$SJ`=fdQ>KjadzLzJB@!hPjTHZ(CUjA~eHTQsT!=JAwCeLWp5+5Va7cD_K(&3}%L>u%N}tcGj0bU(I0M@Ppz+SSEHRn^Qn;>Ckq zkyA0O?rJ|=Yk{s=NWp!66;LlwGe|E`bGXUmr+c>{tOjXr25R2pgecMus;&KV-n@AhbLY-=1A#?e5(2225y?|q84P5UpX>zb&7Zh&zper( zm4cFKJP-(42%u|5SPsb(6@NAF3xJyQCunTyo$VnB%^;wBAt-=TGg4~XQ2?}8Lt}I2 zEnNj6Bq_q0g)D16YynwpZ0ywi(NmmA&F~}xP|dD>pv6GFYp&zAngP48i045Ku*K@? zs}FjL2$7^&K%wUD#?8h~PU>x|u8LKNz<{I>5t@Z0<7V}40$FTilz;K^kB6tHrx*xm z%T)>ywpmC)_g-f=NN-cdhFK~waP*Xbq!2;P0+JpTy`639Mh*`4ipza`d{R=9lk4l3 z6 zrX~#p3ENM~5!Ngur+>S9RTETiDoAgltgNhbLf&aPAtG#+jN6a_)@uvbOIS0)>eoQc z%Mp&obT&TSV%N^E5vhNLJj6+gcvWz9mSjrYshe)y|5kj^& zWnYiMh{%$C>FDE}zH`3sd4KnNU(fwq_jNz_^LzhzAF`Q=9y22^BLDzkHqggeoOsn! zgD{-Dm4A#APaH7NLQe}&(U16gGP&<=W8h(I43Id%5CA=p4FEo+oIC&^FM$4U3;t4oc9s z4Fmv~*iQ`z$j;?HQM2{FYD2OyHd1!=$D^Iy{9WA9A$Y=R7C<#b`2^zKNzTX+{7t_= z<&ev$zZuFW_%w_`A^)b3d@rMHjLneR{zP}=MYIB12BpS`L?Ts*ZXU`OINe|AC$q~a zFA|BMjKKs42cv`K(f&kFj4T$5#mLBE+M1YfmtT)-a<^kS@t$`;C> z=Wh%-`~)F{!=r>NuN2?9P9Ds$*Bq}rIKVh3M%7VzbPAIrwCVzD$C}lic#m6xit0!$ z?vqsx(5OwR{Q&A_^S$rcg*W-_Ko6f*X!-wSQm;Ahe~(*>SQIx09s|6`<(+}l0I^`prQ|~p~ zQevN9fN>pNzctV&{bwY3dK{Zfj@A520e;moiu}1*fVHYqY8Sb@rG#B&;5+DerDboq z3<2>CLijWs5UcT;V|qt94b$^+)eJEu2=(MIA{O!7XEA}rZ%5u8s;)niapIxNlZX#% zoK8EI@}!=Zr#vVy{piR(wc3*QWUzk4rs`>5hWK`H{V?g~tJK=|IUa@s1yLJmDQJvDIL-L95%h3gC>{Le|KClfGWE3 z%w!crMnx;da^za4-}%h8Q+W>cBq@}+aR@)Wo+8RuZWkE{6*tLxx+UcM^k{)!JWqlT ztr(-t(|E5)iPCm1LsUhGz(PlbBP0RVxj}tt%JrPD1EnbqOb<9KrN>iEd#@oY{4;ve zSV7?oVjhoqkmjrsV&=b_=yDWYAa_X#N|)En)E^K9JW)Rv#`6(6XT3R2%b zp2f)mg(E4QPuYeG`ag*z{Rq3pS1tq8617e1Es-RICqe3A=r%1mNj)g+4+)LP;an*I zy3oa{xPDsfBHc&N;iH1l8Gr4Y0XJ~`F*bNm65xj6i%wi2{|GvMq}4ZFhwwrm#-~z< z-q$hH{%nFQ@Oelz4aHr_n4O@}a?Tx;WSBetbuLi*_R2@*dLsl$Y(tm8Y{$F%u!Al~ z9yRlZt^2!O9j!W_+5)-3P{h2Ugq7#HwvlKRFk0wZ3EeB*yR^Oj6`i{Q(u5*7<1Q)s z2xq#SFC;tJ`0T#<#zNQY77^+-4L(0!V+3RF9sA>haIKJg7*Sh+8)_X|X)Zt=PG`t+ z8+0*`U=Ge98X zfnJ)5%sNN?eQ15+ZIMIx{-?p)irt|Yp$mlc+>55{VNVdU~{{ulu_m?3Nn= zlms=+gXB1s#dQ*mhYu?2>+7~ss$ky?WK%WO8VN2+LswC(*lpo=){)0(28JNA!fzVQ zu^tkeIeQ`t8OBYzf*g#qA3X);HXr5hRNd|?$<32A;Cg>0RM42aK{?dEu(QU8L#L>mIRP0Zs_A&wIS;d*W^sPXhZjDjxWqI+a#)0RQd5-&0 zWz&gawd(zKmF+cNEny|*k+$Y##*Rma3$mpE z#Of_8?7XsO)+|wNxb{Z=-C{!%6t}l!@37bQ&%gczpZTcM%)EB9&4WtasL##L|= zKvUj2VM0r^qu`0f(dQ3-VuD8=VsFy2rv{#);M+(q!_v=6!K;%s|b#pgSRGt4kr7(v*4V0D9=CiUG zVQDp(D*1RuLBmpthk#}MglD3qPegTKK1N1#&c!O2a-(gD>|-L%>1umj%5ml6mjy2+ z+^LpzQiZSk*$LJ;rZ^_SdUIELhOg-OA-9KMj~!lAGh1kuO)rG_jF&RlzxS{h^ecVT z>T{G54kD5CdMGoFZ^iHNH;b6=Z1*58(KWDIv(3&Rmuu!qKV^CfoKK-xQV4i{mNe{j z)RZp$SW#a}DLZA0lO@MkM(;7(1kbkG$j$t}8 zTN*saGIeoB46WVzGD6PsA;rP>Qg7KEm4&Hqcnqc$6>@jaZ)}Pbwe1v{D*@E+NE7fhxeWPyOM& z$%|E+%PNbsxv84sX(ev+J4qeaqX&kVM{Pw%eYl>n=t{qz2z7W(BiaH2<&FnTd?l31 zRpfru;VNd4f5{va^Vk>P%~{E2t2gGm<)c=}D~9tJlgMwl1=TCLN{N=4BI zu+ItGo5LQg>_bdUCkUaDTb_i}BaaVLN>nQ*zkXAK>E!rgfAX0-3tOUo`JEyDJ?w8- z{&J5>m=yag{aHkckH8NN_+25sWO<{msp>ZVaSc{U`#n(2fdY3YJ}!dgSvfT2G@_;k zauoZ!14ZviPFIFw5q~MnW@>?+)kxa{ESe{-%I7DV3CDUa(z-Jw-3%+(ogRrz7!5hx zeS({L%ck5UuPCm~e$5@6?|`GuvWo$>g5hQ^%?K)w+f)9 zAgPJ;{9zI4B)z!x%s#R)*X(cjCWd2VvLk|hZCl!P6Win0mlQtF4_1Y9o From 18313e11997b61d845f7755722096baf7a3f1c58 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 12 Dec 2020 11:00:57 -0500 Subject: [PATCH 21/97] :zap: Now OAuth2 send authentication data in the body (#1241) --- packages/core/src/NodeExecuteFunctions.ts | 4 ++++ .../nodes-base/credentials/MauticOAuth2Api.credentials.ts | 2 +- packages/nodes-base/nodes/Mautic/GenericFunctions.ts | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 1006b29100..e6bcbd24cb 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -172,6 +172,10 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin client_secret: credentials.clientSecret as string, }; tokenRefreshOptions.body = body; + // Override authorization property so the credentails are not included in it + tokenRefreshOptions.headers = { + Authorization: '', + }; } const newToken = await token.refresh(tokenRefreshOptions); diff --git a/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts index cea3276934..1a9fb99395 100644 --- a/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MauticOAuth2Api.credentials.ts @@ -50,7 +50,7 @@ export class MauticOAuth2Api implements ICredentialType { displayName: 'Authentication', name: 'authentication', type: 'hidden' as NodePropertyTypes, - default: 'header', + default: 'body', }, ]; } diff --git a/packages/nodes-base/nodes/Mautic/GenericFunctions.ts b/packages/nodes-base/nodes/Mautic/GenericFunctions.ts index 9ab48b8dd8..d36add52df 100644 --- a/packages/nodes-base/nodes/Mautic/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Mautic/GenericFunctions.ts @@ -63,7 +63,7 @@ export async function mauticApiRequest(this: IHookFunctions | IExecuteFunctions options.uri = `${credentials.url}${options.uri}`; //@ts-ignore - returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options); + returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options, { includeCredentialsOnRefreshOnBody: true }); } if (returnData.errors) { From c2d80d6d70e1a7d0d09bfa82a93b4f864dd8300d Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 17:35:35 +0100 Subject: [PATCH 22/97] :shirt: Fix lint issue --- packages/nodes-base/nodes/Pushcut/GenericFunctions.ts | 2 +- packages/nodes-base/nodes/Pushcut/Pushcut.node.ts | 4 ++-- packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts index 0bbbc05e30..269df83578 100644 --- a/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Pushcut/GenericFunctions.ts @@ -42,7 +42,7 @@ export async function pushcutApiRequest(this: IExecuteFunctions | ILoadOptionsFu // Try to return the error prettier throw new Error( - `Pushcut error response [${error.statusCode}]: ${message}` + `Pushcut error response [${error.statusCode}]: ${message}`, ); } throw error; diff --git a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts index 626d788954..28fb18cb97 100644 --- a/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts +++ b/packages/nodes-base/nodes/Pushcut/Pushcut.node.ts @@ -48,7 +48,7 @@ export class Pushcut implements INodeType { }, ], default: 'notification', - description: 'The resource to operate on.' + description: 'The resource to operate on.', }, { displayName: 'Operation', @@ -69,7 +69,7 @@ export class Pushcut implements INodeType { }, ], default: 'send', - description: 'The resource to operate on.' + description: 'The resource to operate on.', }, { displayName: 'Notification Name', diff --git a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts index 694a2b0db2..024ca035d9 100644 --- a/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts +++ b/packages/nodes-base/nodes/Pushcut/PushcutTrigger.node.ts @@ -84,7 +84,7 @@ export class PushcutTrigger implements INodeType { const body = { actionName, - url: webhookUrl + url: webhookUrl, }; const responseData = await pushcutApiRequest.call(this, 'POST', endpoint, body); @@ -123,7 +123,7 @@ export class PushcutTrigger implements INodeType { return { workflowData: [ - this.helpers.returnJsonArray(body) + this.helpers.returnJsonArray(body), ], }; } From 8b567c20b9f881db10e677561cfaa99b0a2969dd Mon Sep 17 00:00:00 2001 From: lublak <44057030+lublak@users.noreply.github.com> Date: Sat, 12 Dec 2020 18:36:47 +0100 Subject: [PATCH 23/97] :zap: Allow workflow to trigger on own error trigger (#1205) * allow workflow to trigger on own error trigger * use config errorTriggerType instance of a static string * don't trigger twice --- packages/cli/src/WorkflowExecuteAdditionalData.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index cf755a6eaa..3d851db10b 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -43,9 +43,11 @@ import * as config from '../config'; import { LessThanOrEqual } from "typeorm"; +const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string; + /** - * Checks if there was an error and if errorWorkflow is defined. If so it collects + * Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects * all the data and executes it * * @param {IWorkflowBase} workflowData The workflow which got executed @@ -54,14 +56,14 @@ import { LessThanOrEqual } from "typeorm"; * @param {string} [executionId] The id the execution got saved as */ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mode: WorkflowExecuteMode, executionId?: string, retryOf?: string): void { - // Check if there was an error and if so if an errorWorkflow is set + // Check if there was an error and if so if an errorWorkflow or a trigger is set let pastExecutionUrl: string | undefined = undefined; if (executionId !== undefined) { pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}execution/${executionId}`; } - if (fullRunData.data.resultData.error !== undefined && workflowData.settings !== undefined && workflowData.settings.errorWorkflow) { + if (fullRunData.data.resultData.error !== undefined) { const workflowErrorData = { execution: { id: executionId, @@ -77,7 +79,12 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo }, }; // Run the error workflow - WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); + if(workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { + WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); + } + if(workflowData.settings !== undefined && workflowData.settings.errorWorkflow && workflowData.settings.errorWorkflow != workflowData.id) { + WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); + } } } From d306be8099f6bff2044e169febdcf6e0a3697d20 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 18:41:11 +0100 Subject: [PATCH 24/97] :bug: Fix bug in own error-workflow addition --- packages/cli/src/WorkflowExecuteAdditionalData.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 3d851db10b..80354c4af2 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -78,12 +78,15 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo name: workflowData.name, }, }; + // Run the error workflow - if(workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { - WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); - } - if(workflowData.settings !== undefined && workflowData.settings.errorWorkflow && workflowData.settings.errorWorkflow != workflowData.id) { + // To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow. + if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) { + // If a specific error workflow is set run only that one WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); + } else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { + // If the workflow contains + WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData); } } } From 7cdbe5c147c9603413199704e8b0a23072284483 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 20:59:17 +0100 Subject: [PATCH 25/97] :shirt: Fix lint issue --- packages/cli/src/WorkflowExecuteAdditionalData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 80354c4af2..592b13ca0f 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -81,7 +81,7 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo // Run the error workflow // To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow. - if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) { + if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) { // If a specific error workflow is set run only that one WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData); } else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) { From 5e56dcb037a9eda602174d5ca178d2e26cb7d029 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sat, 12 Dec 2020 22:53:13 +0100 Subject: [PATCH 26/97] :books: Extend PI docker README to include mount example --- docker/images/n8n-rpi/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/images/n8n-rpi/README.md b/docker/images/n8n-rpi/README.md index 9eca14e3f6..cc15b0c9d8 100644 --- a/docker/images/n8n-rpi/README.md +++ b/docker/images/n8n-rpi/README.md @@ -17,5 +17,6 @@ docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-rpi . docker run -it --rm \ --name n8n \ -p 5678:5678 \ + -v ~/.n8n:/home/node/.n8n \ n8nio/n8n:0.70.0-rpi ``` From 532503b69f840c2644c6f7928c4af555f6cc6b5e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 13 Dec 2020 04:47:52 -0500 Subject: [PATCH 27/97] :sparkles: Feature/slack node extended (#1239) * Added reaction method to message * Increased limit for channels on getChannels, removed unused fields in the code * Using own operation for reactions * Registering reaction fields and operations * Added Operation "Reaction" to Slack.node.ts * Fixing variable name for emoji * now removing reaction on "remove" instead of "add" * Using GET for reactions.get and passing arguments as query * Added members operation * Fixed typo in timestamp * Added user.info and user.getPresence * Fixed: wrong operation name * :zap: Improvements to #1238 * :zap: Add field resolve data when retrieving channel members * :zap: Minor improvements to Slack-Node Co-authored-by: Andreas Scherren Co-authored-by: Jan Oberhauser --- .../credentials/SlackOAuth2Api.credentials.ts | 8 +- .../nodes/Slack/ChannelDescription.ts | 199 +++++++++++++----- .../nodes-base/nodes/Slack/FileDescription.ts | 40 ++-- .../nodes/Slack/GenericFunctions.ts | 14 +- .../nodes/Slack/MessageDescription.ts | 47 +++-- .../nodes/Slack/MessageInterface.ts | 1 - .../nodes/Slack/ReactionDescription.ts | 101 +++++++++ packages/nodes-base/nodes/Slack/Slack.node.ts | 93 +++++++- .../nodes-base/nodes/Slack/StarDescription.ts | 34 +-- .../nodes-base/nodes/Slack/UserDescription.ts | 85 ++++++++ .../nodes/Slack/UserProfileDescription.ts | 13 +- 11 files changed, 509 insertions(+), 126 deletions(-) create mode 100644 packages/nodes-base/nodes/Slack/ReactionDescription.ts create mode 100644 packages/nodes-base/nodes/Slack/UserDescription.ts diff --git a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts index cdca307de5..050bdfb641 100644 --- a/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/SlackOAuth2Api.credentials.ts @@ -8,13 +8,15 @@ const userScopes = [ 'chat:write', 'files:read', 'files:write', + 'groups:read', + 'im:read', + 'mpim:read', + 'reactions:read', + 'reactions:write', 'stars:read', 'stars:write', 'users.profile:read', 'users.profile:write', - 'groups:read', - 'im:read', - 'mpim:read', ]; export class SlackOAuth2Api implements ICredentialType { diff --git a/packages/nodes-base/nodes/Slack/ChannelDescription.ts b/packages/nodes-base/nodes/Slack/ChannelDescription.ts index 52437134e7..2dbf1059e8 100644 --- a/packages/nodes-base/nodes/Slack/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Slack/ChannelDescription.ts @@ -1,4 +1,6 @@ -import { INodeProperties } from 'n8n-workflow'; +import { + INodeProperties, +} from 'n8n-workflow'; export const channelOperations = [ { @@ -63,6 +65,11 @@ export const channelOperations = [ value: 'leave', description: 'Leaves a conversation.', }, + { + name: 'Member', + value: 'member', + description: 'List members of a conversation.', + }, { name: 'Open', value: 'open', @@ -101,9 +108,9 @@ export const channelOperations = [ export const channelFields = [ -/* -------------------------------------------------------------------------- */ -/* channel:archive */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* channel:archive */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -125,9 +132,10 @@ export const channelFields = [ required: true, description: 'The name of the channel to archive.', }, -/* -------------------------------------------------------------------------- */ -/* channel:close */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:close */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -149,9 +157,10 @@ export const channelFields = [ required: true, description: 'The name of the channel to close.', }, -/* -------------------------------------------------------------------------- */ -/* channel:create */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:create */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -197,9 +206,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:invite */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:invite */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -242,9 +252,10 @@ export const channelFields = [ required: true, description: 'The ID of the user to invite into channel.', }, -/* -------------------------------------------------------------------------- */ -/* channel:get */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -288,9 +299,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:kick */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:kick */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -332,9 +344,10 @@ export const channelFields = [ }, default: '', }, -/* -------------------------------------------------------------------------- */ -/* channel:join */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:join */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -356,9 +369,10 @@ export const channelFields = [ }, required: true, }, -/* -------------------------------------------------------------------------- */ -/* channel:getAll */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -451,9 +465,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:history */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:history */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -557,9 +572,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:leave */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:leave */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -581,9 +597,89 @@ export const channelFields = [ required: true, description: 'The name of the channel to leave.', }, -/* -------------------------------------------------------------------------- */ -/* channel:open */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:member */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'member', + ], + resource: [ + 'channel', + ], + }, + }, + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'member', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 100, + placeholder: 'Limit', + displayOptions: { + show: { + operation: [ + 'member', + ], + resource: [ + 'channel', + ], + returnAll: [ + false, + ], + }, + }, + required: false, + }, + { + displayName: 'Resolve Data', + name: 'resolveData', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: [ + 'channel', + ], + operation: [ + 'member', + ], + }, + }, + description: 'By default the response only contain the ID to resource. If this
option gets activated it will resolve the data automatically.', + }, + + /* -------------------------------------------------------------------------- */ + /* channel:open */ + /* -------------------------------------------------------------------------- */ { displayName: 'Options', name: 'options', @@ -627,9 +723,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:rename */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:rename */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -669,9 +766,10 @@ export const channelFields = [ required: true, description: 'New name for conversation.', }, -/* -------------------------------------------------------------------------- */ -/* channel:replies */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:replies */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -793,9 +891,10 @@ export const channelFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* channel:setPurpose */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:setPurpose */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -835,9 +934,10 @@ export const channelFields = [ required: true, description: 'A new, specialer purpose', }, -/* -------------------------------------------------------------------------- */ -/* channel:setTopic */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:setTopic */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', @@ -877,9 +977,10 @@ export const channelFields = [ required: true, description: 'The new topic string. Does not support formatting or linkification.', }, -/* -------------------------------------------------------------------------- */ -/* channel:unarchive */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* channel:unarchive */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', diff --git a/packages/nodes-base/nodes/Slack/FileDescription.ts b/packages/nodes-base/nodes/Slack/FileDescription.ts index 1b78fc294b..e33b9e356e 100644 --- a/packages/nodes-base/nodes/Slack/FileDescription.ts +++ b/packages/nodes-base/nodes/Slack/FileDescription.ts @@ -36,9 +36,9 @@ export const fileOperations = [ export const fileFields = [ -/* -------------------------------------------------------------------------- */ -/* file:upload */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* file:upload */ + /* -------------------------------------------------------------------------- */ { displayName: 'Binary Data', name: 'binaryData', @@ -159,9 +159,10 @@ export const fileFields = [ }, ], }, -/* ----------------------------------------------------------------------- */ -/* file:getAll */ -/* ----------------------------------------------------------------------- */ + + /* ----------------------------------------------------------------------- */ + /* file:getAll */ + /* ----------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', @@ -261,29 +262,29 @@ export const fileFields = [ value: 'all', }, { - name: 'Spaces', - value: 'spaces', - }, - { - name: 'Snippets', - value: 'snippets', + name: 'Google Docs', + value: 'gdocs', }, { name: 'Images', value: 'images', }, { - name: 'Google Docs', - value: 'gdocs', + name: 'Snippets', + value: 'snippets', }, { - name: 'Zips', - value: 'zips', + name: 'Spaces', + value: 'spaces', }, { name: 'pdfs', value: 'pdfs', }, + { + name: 'Zips', + value: 'zips', + }, ], default: ['all'], description: 'Filter files by type', @@ -300,9 +301,10 @@ export const fileFields = [ }, ], }, -/* ----------------------------------------------------------------------- */ -/* file:get */ -/* ----------------------------------------------------------------------- */ + + /* ----------------------------------------------------------------------- */ + /* file:get */ + /* ----------------------------------------------------------------------- */ { displayName: 'File ID', name: 'fileId', diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index e31a505892..43cc376b5c 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -76,7 +76,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu } } -export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; let responseData; query.page = 1; @@ -88,13 +88,13 @@ export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOpt returnData.push.apply(returnData, responseData[propertyName]); } while ( (responseData.response_metadata !== undefined && - responseData.response_metadata.mext_cursor !== undefined && - responseData.response_metadata.next_cursor !== '' && - responseData.response_metadata.next_cursor !== null) || + responseData.response_metadata.mext_cursor !== undefined && + responseData.response_metadata.next_cursor !== '' && + responseData.response_metadata.next_cursor !== null) || (responseData.paging !== undefined && - responseData.paging.pages !== undefined && - responseData.paging.page !== undefined && - responseData.paging.page < responseData.paging.pages + responseData.paging.pages !== undefined && + responseData.paging.page !== undefined && + responseData.paging.page < responseData.paging.pages ) ); diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index ad5ac67f1b..9953441afe 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -31,9 +31,9 @@ export const messageOperations = [ export const messageFields = [ -/* -------------------------------------------------------------------------- */ -/* message:post */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* message:post */ + /* -------------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channel', @@ -382,6 +382,13 @@ export const messageFields = [ default: '', description: 'URL to an image to use as the icon for this message.', }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, { displayName: 'Make Reply', name: 'thread_ts', @@ -389,20 +396,6 @@ export const messageFields = [ default: '', description: 'Provide another message\'s ts value to make this message a reply.', }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, { displayName: 'Markdown', name: 'mrkdwn', @@ -418,17 +411,25 @@ export const messageFields = [ description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', }, { - displayName: 'Link Names', - name: 'link_names', + displayName: 'Unfurl Links', + name: 'unfurl_links', type: 'boolean', default: false, - description: 'Find and link channel names and usernames.', + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', }, ], }, -/* ----------------------------------------------------------------------- */ -/* message:update */ -/* ----------------------------------------------------------------------- */ + + /* ----------------------------------------------------------------------- */ + /* message:update */ + /* ----------------------------------------------------------------------- */ { displayName: 'Channel', name: 'channelId', diff --git a/packages/nodes-base/nodes/Slack/MessageInterface.ts b/packages/nodes-base/nodes/Slack/MessageInterface.ts index caff7c0765..463c2fa636 100644 --- a/packages/nodes-base/nodes/Slack/MessageInterface.ts +++ b/packages/nodes-base/nodes/Slack/MessageInterface.ts @@ -4,4 +4,3 @@ export interface IAttachment { item?: object[]; }; } - diff --git a/packages/nodes-base/nodes/Slack/ReactionDescription.ts b/packages/nodes-base/nodes/Slack/ReactionDescription.ts new file mode 100644 index 0000000000..0e41dc9282 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/ReactionDescription.ts @@ -0,0 +1,101 @@ +import { INodeProperties } from 'n8n-workflow'; + +export const reactionOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'reaction', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Adds a reaction to a message', + }, + { + name: 'Get', + value: 'get', + description: 'Get the reactions of a message', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a reaction of a message', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const reactionFields = [ + { + displayName: 'Channel', + name: 'channelId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getChannels', + }, + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'add', + 'get', + 'remove', + ], + }, + }, + description: 'Channel containing the message.', + }, + { + displayName: 'Emoji', + name: 'name', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'add', + 'remove', + ], + }, + }, + description: 'Name of emoji.', + placeholder: '+1', + }, + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'reaction', + ], + operation: [ + 'add', + 'get', + 'remove', + ], + }, + }, + description: `Timestamp of the message.`, + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 24553d7f79..096b479192 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -32,6 +32,16 @@ import { fileOperations, } from './FileDescription'; +import { + reactionFields, + reactionOperations, +} from './ReactionDescription'; + +import { + userFields, + userOperations, +} from './UserDescription'; + import { userProfileFields, userProfileOperations, @@ -46,6 +56,7 @@ import { import { IAttachment, } from './MessageInterface'; + import moment = require('moment'); interface Attachment { @@ -163,10 +174,18 @@ export class Slack implements INodeType { name: 'Message', value: 'message', }, + { + name: 'Reaction', + value: 'reaction', + }, { name: 'Star', value: 'star', }, + { + name: 'User', + value: 'user', + }, { name: 'User Profile', value: 'userProfile', @@ -184,6 +203,10 @@ export class Slack implements INodeType { ...starFields, ...fileOperations, ...fileFields, + ...reactionOperations, + ...reactionFields, + ...userOperations, + ...userFields, ...userProfileOperations, ...userProfileFields, ], @@ -217,7 +240,7 @@ export class Slack implements INodeType { // select them easily async getChannels(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; - const qs = { types: 'public_channel,private_channel' }; + const qs = { types: 'public_channel,private_channel', limit: 1000 }; const channels = await slackApiRequestAllItems.call(this, 'channels', 'GET', '/conversations.list', {}, qs); for (const channel of channels) { const channelName = channel.name; @@ -265,7 +288,7 @@ export class Slack implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { - responseData = { error: 'Resource ' + resource + ' / operation ' + operation + ' not found!'}; + responseData = { error: 'Resource ' + resource + ' / operation ' + operation + ' not found!' }; qs = {}; if (resource === 'channel') { //https://api.slack.com/methods/conversations.archive @@ -383,6 +406,29 @@ export class Slack implements INodeType { }; responseData = await slackApiRequest.call(this, 'POST', '/conversations.leave', body, qs); } + //https://api.slack.com/methods/conversations.members + if (operation === 'member') { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const resolveData = this.getNodeParameter('resolveData', 0) as boolean; + qs.channel = this.getNodeParameter('channelId', i) as string; + if (returnAll) { + responseData = await slackApiRequestAllItems.call(this, 'members', 'GET', '/conversations.members', {}, qs); + responseData = responseData.map((member: string) => ({ member })); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await slackApiRequest.call(this, 'GET', '/conversations.members', {}, qs); + responseData = responseData.members.map((member: string) => ({ member })); + } + + if (resolveData) { + const data: IDataObject[] = []; + for (const { member } of responseData) { + const { user } = await slackApiRequest.call(this, 'GET', '/users.info', {}, { user: member }); + data.push(user); + } + responseData = data; + } + } //https://api.slack.com/methods/conversations.open if (operation === 'open') { const options = this.getNodeParameter('options', i) as IDataObject; @@ -745,6 +791,36 @@ export class Slack implements INodeType { responseData = await slackApiRequest.call(this, 'POST', '/chat.update', body, qs); } } + if (resource === 'reaction') { + const channel = this.getNodeParameter('channelId', i) as string; + const timestamp = this.getNodeParameter('timestamp', i) as string; + //https://api.slack.com/methods/reactions.add + if (operation === 'add') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.add', body, qs); + } + //https://api.slack.com/methods/reactions.remove + if (operation === 'remove') { + const name = this.getNodeParameter('name', i) as string; + const body: IDataObject = { + channel, + name, + timestamp, + }; + responseData = await slackApiRequest.call(this, 'POST', '/reactions.remove', body, qs); + } + //https://api.slack.com/methods/reactions.get + if (operation === 'get') { + qs.channel = channel; + qs.timestamp = timestamp; + responseData = await slackApiRequest.call(this, 'GET', '/reactions.get', {}, qs); + } + } if (resource === 'star') { //https://api.slack.com/methods/stars.add if (operation === 'add') { @@ -879,6 +955,19 @@ export class Slack implements INodeType { responseData = responseData.file; } } + if (resource === 'user') { + //https://api.slack.com/methods/users.info + if (operation === 'info') { + qs.user = this.getNodeParameter('user', i) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.info', {}, qs); + responseData = responseData.user; + } + //https://api.slack.com/methods/users.getPresence + if (operation === 'getPresence') { + qs.user = this.getNodeParameter('user', i) as string; + responseData = await slackApiRequest.call(this, 'GET', '/users.getPresence', {}, qs); + } + } if (resource === 'userProfile') { //https://api.slack.com/methods/users.profile.set if (operation === 'update') { diff --git a/packages/nodes-base/nodes/Slack/StarDescription.ts b/packages/nodes-base/nodes/Slack/StarDescription.ts index 0f77db7596..66d95c127c 100644 --- a/packages/nodes-base/nodes/Slack/StarDescription.ts +++ b/packages/nodes-base/nodes/Slack/StarDescription.ts @@ -36,9 +36,9 @@ export const starOperations = [ export const starFields = [ -/* -------------------------------------------------------------------------- */ -/* star:add */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* star:add */ + /* -------------------------------------------------------------------------- */ { displayName: 'Options', name: 'options', @@ -67,13 +67,6 @@ export const starFields = [ default: '', description: 'Channel to add star to, or channel where the message to add star to was posted (used with timestamp).', }, - { - displayName: 'File ID', - name: 'fileId', - type: 'string', - default: '', - description: 'File to add star to.', - }, { displayName: 'File Comment', name: 'fileComment', @@ -81,6 +74,13 @@ export const starFields = [ default: '', description: 'File comment to add star to.', }, + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + default: '', + description: 'File to add star to.', + }, { displayName: 'Timestamp', name: 'timestamp', @@ -90,9 +90,10 @@ export const starFields = [ }, ], }, -/* ----------------------------------------------------------------------- */ -/* star:delete */ -/* ----------------------------------------------------------------------- */ + + /* ----------------------------------------------------------------------- */ + /* star:delete */ + /* ----------------------------------------------------------------------- */ { displayName: 'Options', name: 'options', @@ -144,9 +145,10 @@ export const starFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* star:getAll */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* star:getAll */ + /* -------------------------------------------------------------------------- */ { displayName: 'Return All', name: 'returnAll', diff --git a/packages/nodes-base/nodes/Slack/UserDescription.ts b/packages/nodes-base/nodes/Slack/UserDescription.ts new file mode 100644 index 0000000000..7505a3b8d7 --- /dev/null +++ b/packages/nodes-base/nodes/Slack/UserDescription.ts @@ -0,0 +1,85 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Info', + value: 'info', + description: `Get information about a user`, + }, + { + name: 'Get Presence', + value: 'getPresence', + description: `Get online status of a user`, + }, + ], + default: 'info', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + + /* -------------------------------------------------------------------------- */ + /* user:info */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'user', + type: 'string', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'info', + ], + resource: [ + 'user', + ], + }, + }, + required: true, + description: 'The ID of the user to get information about.', + }, + + /* -------------------------------------------------------------------------- */ + /* user:getPresence */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'user', + type: 'string', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + displayOptions: { + show: { + operation: [ + 'getPresence', + ], + resource: [ + 'user', + ], + }, + }, + required: true, + description: 'The ID of the user to get the online status of.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Slack/UserProfileDescription.ts b/packages/nodes-base/nodes/Slack/UserProfileDescription.ts index a3475ee993..9bf9d103ef 100644 --- a/packages/nodes-base/nodes/Slack/UserProfileDescription.ts +++ b/packages/nodes-base/nodes/Slack/UserProfileDescription.ts @@ -34,9 +34,9 @@ export const userProfileOperations = [ export const userProfileFields = [ -/* -------------------------------------------------------------------------- */ -/* userProfile:update */ -/* -------------------------------------------------------------------------- */ + /* -------------------------------------------------------------------------- */ + /* userProfile:update */ + /* -------------------------------------------------------------------------- */ { displayName: 'Additional Fields', name: 'additionalFields', @@ -144,9 +144,10 @@ export const userProfileFields = [ }, ], }, -/* -------------------------------------------------------------------------- */ -/* userProfile:get */ -/* -------------------------------------------------------------------------- */ + + /* -------------------------------------------------------------------------- */ + /* userProfile:get */ + /* -------------------------------------------------------------------------- */ { displayName: 'Additional Fields', name: 'additionalFields', From d5da7865b9c0d5ad854fc02da0a9a517ecffd3bd Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sun, 13 Dec 2020 09:36:48 -0500 Subject: [PATCH 28/97] :zap: Add create:spreadsheet operation (#1234) * :zap: Add create:spreadsheet operation * :zap: Small changes * :zap: Improvements to Google Sheets Co-authored-by: Jan Oberhauser --- .../nodes/Google/Sheet/GoogleSheets.node.ts | 598 +++++++++++++----- 1 file changed, 437 insertions(+), 161 deletions(-) diff --git a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts index 8096ae53b7..b5cd0ced1a 100644 --- a/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/Sheet/GoogleSheets.node.ts @@ -21,6 +21,10 @@ import { ValueRenderOption, } from './GoogleSheet'; +import { + googleApiRequest, +} from './GenericFunctions'; + export class GoogleSheets implements INodeType { description: INodeTypeDescription = { displayName: 'Google Sheets ', @@ -28,6 +32,7 @@ export class GoogleSheets implements INodeType { icon: 'file:googlesheets.png', group: ['input', 'output'], version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Read, update and write data to Google Sheets', defaults: { name: 'Google Sheets', @@ -76,10 +81,35 @@ export class GoogleSheets implements INodeType { ], default: 'serviceAccount', }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Spreadsheet', + value: 'spreadsheet', + }, + { + name: 'Sheet', + value: 'sheet', + }, + + ], + default: 'sheet', + description: 'The operation to perform.', + }, { displayName: 'Operation', name: 'operation', type: 'options', + displayOptions: { + show: { + resource: [ + 'sheet', + ], + }, + }, options: [ { name: 'Append', @@ -123,6 +153,13 @@ export class GoogleSheets implements INodeType { displayName: 'Sheet ID', name: 'sheetId', type: 'string', + displayOptions: { + show: { + resource: [ + 'sheet', + ], + }, + }, default: '', required: true, description: 'The ID of the Google Sheet.
Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/', @@ -132,6 +169,11 @@ export class GoogleSheets implements INodeType { name: 'range', type: 'string', displayOptions: { + show: { + resource: [ + 'sheet', + ], + }, hide: { operation: [ 'delete', @@ -158,6 +200,9 @@ export class GoogleSheets implements INodeType { }, displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'delete', ], @@ -254,6 +299,9 @@ export class GoogleSheets implements INodeType { type: 'boolean', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'read', ], @@ -269,6 +317,9 @@ export class GoogleSheets implements INodeType { default: 'data', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'read', ], @@ -289,6 +340,9 @@ export class GoogleSheets implements INodeType { type: 'boolean', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'update', ], @@ -304,6 +358,9 @@ export class GoogleSheets implements INodeType { default: 'data', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'update', ], @@ -327,6 +384,11 @@ export class GoogleSheets implements INodeType { }, default: 1, displayOptions: { + show: { + resource: [ + 'sheet', + ], + }, hide: { operation: [ 'append', @@ -352,6 +414,11 @@ export class GoogleSheets implements INodeType { minValue: 0, }, displayOptions: { + show: { + resource: [ + 'sheet', + ], + }, hide: { operation: [ 'clear', @@ -379,6 +446,9 @@ export class GoogleSheets implements INodeType { required: true, displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'lookup', ], @@ -394,6 +464,9 @@ export class GoogleSheets implements INodeType { placeholder: 'frank@example.com', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'lookup', ], @@ -412,6 +485,9 @@ export class GoogleSheets implements INodeType { default: 'id', displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'update', ], @@ -431,6 +507,9 @@ export class GoogleSheets implements INodeType { default: {}, displayOptions: { show: { + resource: [ + 'sheet', + ], operation: [ 'append', 'lookup', @@ -566,6 +645,144 @@ export class GoogleSheets implements INodeType { ], }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'spreadsheet', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a spreadsheet', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, + // ---------------------------------- + // spreadsheet:create + // ---------------------------------- + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'spreadsheet', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The title of the spreadsheet.', + }, + { + displayName: 'Sheets', + name: 'sheetsUi', + placeholder: 'Add Sheet', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'sheetValues', + displayName: 'Sheet', + values: [ + { + displayName: 'Sheet Properties', + name: 'propertiesUi', + placeholder: 'Add Property', + type: 'collection', + default: {}, + options: [ + { + displayName: 'Hidden', + name: 'hidden', + type: 'boolean', + default: false, + description: 'If the Sheet should be hidden in the UI', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the property to create', + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + resource: [ + 'spreadsheet', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Locale', + name: 'locale', + type: 'string', + default: '', + placeholder: 'en_US', + description: 'The locale of the spreadsheet in one of the following formats:
  • en (639-1)
  • fil (639-2 if no 639-1 format exists)
  • en_US (combination of ISO language an country)
    • ', + }, + { + displayName: 'Recalculation Interval', + name: 'autoRecalc', + type: 'options', + options: [ + { + name: 'Default', + value: '', + description: 'Default value', + }, + { + name: 'On Change', + value: 'ON_CHANGE', + description: 'Volatile functions are updated on every change.', + }, + { + name: 'Minute', + value: 'MINUTE', + description: 'Volatile functions are updated on every change and every minute.', + }, + { + name: 'Hour', + value: 'HOUR', + description: ' Volatile functions are updated on every change and hourly.', + }, + ], + default: '', + description: 'Cell recalculation interval options.', + }, + ], + }, ], }; @@ -602,188 +819,247 @@ export class GoogleSheets implements INodeType { async execute(this: IExecuteFunctions): Promise { - const spreadsheetId = this.getNodeParameter('sheetId', 0) as string; - - const sheet = new GoogleSheet(spreadsheetId, this); const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; - let range = ''; - if (operation !== 'delete') { - range = this.getNodeParameter('range', 0) as string; - } + if (resource === 'sheet') { - const options = this.getNodeParameter('options', 0, {}) as IDataObject; + const spreadsheetId = this.getNodeParameter('sheetId', 0) as string; - const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption; - const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption; + const sheet = new GoogleSheet(spreadsheetId, this); - if (operation === 'append') { - // ---------------------------------- - // append - // ---------------------------------- - const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); - - const items = this.getInputData(); - - const setData: IDataObject[] = []; - items.forEach((item) => { - setData.push(item.json); - }); - - // Convert data into array format - const data = await sheet.appendSheetData(setData, sheet.encodeRange(range), keyRow, valueInputMode); - - // TODO: Should add this data somewhere - // TODO: Should have something like add metadata which does not get passed through - - return this.prepareOutputData(items); - } else if (operation === 'clear') { - // ---------------------------------- - // clear - // ---------------------------------- - - await sheet.clearData(sheet.encodeRange(range)); - - const items = this.getInputData(); - return this.prepareOutputData(items); - } else if (operation === 'delete') { - // ---------------------------------- - // delete - // ---------------------------------- - - const requests: IDataObject[] = []; - - const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete; - - const deletePropertyToDimensions: IDataObject = { - 'columns': 'COLUMNS', - 'rows': 'ROWS', - }; - - for (const propertyName of Object.keys(deletePropertyToDimensions)) { - if (toDelete[propertyName] !== undefined) { - toDelete[propertyName]!.forEach(range => { - requests.push({ - deleteDimension: { - range: { - sheetId: range.sheetId, - dimension: deletePropertyToDimensions[propertyName] as string, - startIndex: range.startIndex, - endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10), - }, - }, - }); - }); - } + let range = ''; + if (operation !== 'delete') { + range = this.getNodeParameter('range', 0) as string; } - const data = await sheet.spreadsheetBatchUpdate(requests); + const options = this.getNodeParameter('options', 0, {}) as IDataObject; - const items = this.getInputData(); - return this.prepareOutputData(items); - } else if (operation === 'lookup') { - // ---------------------------------- - // lookup - // ---------------------------------- + const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption; + const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption; - const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode); - - if (sheetData === undefined) { - return []; - } - - const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); - const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); - - const items = this.getInputData(); - - const lookupValues: ILookupValues[] = []; - for (let i = 0; i < items.length; i++) { - lookupValues.push({ - lookupColumn: this.getNodeParameter('lookupColumn', i) as string, - lookupValue: this.getNodeParameter('lookupValue', i) as string, - }); - } - - let returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined); - - if (returnData.length === 0 && options.continue && options.returnAllMatches) { - returnData = [{}]; - } else if (returnData.length === 1 && Object.keys(returnData[0]).length === 0 && !options.continue && !options.returnAllMatches) { - returnData = []; - } - - return [this.helpers.returnJsonArray(returnData)]; - } else if (operation === 'read') { - // ---------------------------------- - // read - // ---------------------------------- - - const rawData = this.getNodeParameter('rawData', 0) as boolean; - - const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode); - - let returnData: IDataObject[]; - if (!sheetData) { - returnData = []; - } else if (rawData === true) { - const dataProperty = this.getNodeParameter('dataProperty', 0) as string; - returnData = [ - { - [dataProperty]: sheetData, - }, - ]; - } else { - const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + if (operation === 'append') { + // ---------------------------------- + // append + // ---------------------------------- const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); - returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow); - } - - if (returnData.length === 0 && options.continue) { - returnData = [{}]; - } - - return [this.helpers.returnJsonArray(returnData)]; - } else if (operation === 'update') { - // ---------------------------------- - // update - // ---------------------------------- - - const rawData = this.getNodeParameter('rawData', 0) as boolean; - - const items = this.getInputData(); - - if (rawData === true) { - const dataProperty = this.getNodeParameter('dataProperty', 0) as string; - - const updateData: ISheetUpdateData[] = []; - for (let i = 0; i < items.length; i++) { - updateData.push({ - range, - values: items[i].json[dataProperty] as string[][], - }); - } - - const data = await sheet.batchUpdate(updateData, valueInputMode); - } else { - const keyName = this.getNodeParameter('key', 0) as string; - const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); - const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + const items = this.getInputData(); const setData: IDataObject[] = []; items.forEach((item) => { setData.push(item.json); }); - const data = await sheet.updateSheetData(setData, keyName, range, keyRow, dataStartRow, valueInputMode, valueRenderMode); + // Convert data into array format + const data = await sheet.appendSheetData(setData, sheet.encodeRange(range), keyRow, valueInputMode); + + // TODO: Should add this data somewhere + // TODO: Should have something like add metadata which does not get passed through + + return this.prepareOutputData(items); + } else if (operation === 'clear') { + // ---------------------------------- + // clear + // ---------------------------------- + + await sheet.clearData(sheet.encodeRange(range)); + + const items = this.getInputData(); + return this.prepareOutputData(items); + } else if (operation === 'delete') { + // ---------------------------------- + // delete + // ---------------------------------- + + const requests: IDataObject[] = []; + + const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete; + + const deletePropertyToDimensions: IDataObject = { + 'columns': 'COLUMNS', + 'rows': 'ROWS', + }; + + for (const propertyName of Object.keys(deletePropertyToDimensions)) { + if (toDelete[propertyName] !== undefined) { + toDelete[propertyName]!.forEach(range => { + requests.push({ + deleteDimension: { + range: { + sheetId: range.sheetId, + dimension: deletePropertyToDimensions[propertyName] as string, + startIndex: range.startIndex, + endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10), + }, + }, + }); + }); + } + } + + const data = await sheet.spreadsheetBatchUpdate(requests); + + const items = this.getInputData(); + return this.prepareOutputData(items); + } else if (operation === 'lookup') { + // ---------------------------------- + // lookup + // ---------------------------------- + + const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode); + + if (sheetData === undefined) { + return []; + } + + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); + + const items = this.getInputData(); + + const lookupValues: ILookupValues[] = []; + for (let i = 0; i < items.length; i++) { + lookupValues.push({ + lookupColumn: this.getNodeParameter('lookupColumn', i) as string, + lookupValue: this.getNodeParameter('lookupValue', i) as string, + }); + } + + let returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined); + + if (returnData.length === 0 && options.continue && options.returnAllMatches) { + returnData = [{}]; + } else if (returnData.length === 1 && Object.keys(returnData[0]).length === 0 && !options.continue && !options.returnAllMatches) { + returnData = []; + } + + return [this.helpers.returnJsonArray(returnData)]; + } else if (operation === 'read') { + // ---------------------------------- + // read + // ---------------------------------- + + const rawData = this.getNodeParameter('rawData', 0) as boolean; + + const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode); + + let returnData: IDataObject[]; + if (!sheetData) { + returnData = []; + } else if (rawData === true) { + const dataProperty = this.getNodeParameter('dataProperty', 0) as string; + returnData = [ + { + [dataProperty]: sheetData, + }, + ]; + } else { + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); + + returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow); + } + + if (returnData.length === 0 && options.continue) { + returnData = [{}]; + } + + return [this.helpers.returnJsonArray(returnData)]; + } else if (operation === 'update') { + // ---------------------------------- + // update + // ---------------------------------- + + const rawData = this.getNodeParameter('rawData', 0) as boolean; + + const items = this.getInputData(); + + if (rawData === true) { + const dataProperty = this.getNodeParameter('dataProperty', 0) as string; + + const updateData: ISheetUpdateData[] = []; + for (let i = 0; i < items.length; i++) { + updateData.push({ + range, + values: items[i].json[dataProperty] as string[][], + }); + } + + const data = await sheet.batchUpdate(updateData, valueInputMode); + } else { + const keyName = this.getNodeParameter('key', 0) as string; + const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10); + const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10); + + const setData: IDataObject[] = []; + items.forEach((item) => { + setData.push(item.json); + }); + + const data = await sheet.updateSheetData(setData, keyName, range, keyRow, dataStartRow, valueInputMode, valueRenderMode); + } + // TODO: Should add this data somewhere + // TODO: Should have something like add metadata which does not get passed through + + + return this.prepareOutputData(items); } - // TODO: Should add this data somewhere - // TODO: Should have something like add metadata which does not get passed through + } - return this.prepareOutputData(items); + if (resource === 'spreadsheet') { + + const returnData: IDataObject[] = []; + + let responseData; + + if (operation === 'create') { + // ---------------------------------- + // create + // ---------------------------------- + // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create + + for (let i = 0; i < this.getInputData().length; i++) { + + const title = this.getNodeParameter('title', i) as string; + const sheetsUi = this.getNodeParameter('sheetsUi', i, {}) as IDataObject; + + const body = { + properties: { + title, + autoRecalc: undefined as undefined | string, + locale: undefined as undefined | string, + }, + sheets: [] as IDataObject[], + }; + + const options = this.getNodeParameter('options', i, {}) as IDataObject; + + if (Object.keys(sheetsUi).length) { + const data = []; + const sheets = sheetsUi.sheetValues as IDataObject[]; + for (const sheet of sheets) { + const properties = sheet.propertiesUi as IDataObject; + if (properties) { + data.push({ properties }); + } + } + body.sheets = data; + } + + body.properties!.autoRecalc = options.autoRecalc ? (options.autoRecalc as string) : undefined; + body.properties!.locale = options.locale ? (options.locale as string) : undefined; + + responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets`, body); + + returnData.push(responseData); + } + + } + + return [this.helpers.returnJsonArray(returnData)]; } return []; From e981118316a351f19f61f5602ebd85501bf08b13 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 13 Dec 2020 16:21:26 +0100 Subject: [PATCH 29/97] :bug: Fix issue that Gitlab OAuth did not use specified server #1243 --- .../nodes-base/credentials/GitlabOAuth2Api.credentials.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/credentials/GitlabOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GitlabOAuth2Api.credentials.ts index 39e1b04a06..21a17cb2d1 100644 --- a/packages/nodes-base/credentials/GitlabOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GitlabOAuth2Api.credentials.ts @@ -22,14 +22,14 @@ export class GitlabOAuth2Api implements ICredentialType { displayName: 'Authorization URL', name: 'authUrl', type: 'hidden' as NodePropertyTypes, - default: 'https://gitlab.com/oauth/authorize', + default: '={{$parameter["server"]}}/oauth/authorize', required: true, }, { displayName: 'Access Token URL', name: 'accessTokenUrl', type: 'hidden' as NodePropertyTypes, - default: 'https://gitlab.com/oauth/token', + default: '={{$parameter["server"]}}/oauth/token', required: true, }, { From 8528c2a2f117e4caa85f3392c2381a2bf529dc12 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Sun, 13 Dec 2020 17:33:43 +0100 Subject: [PATCH 30/97] :bug: Fix issue that GitHub OAuth did not use specified server --- .../nodes-base/credentials/GithubOAuth2Api.credentials.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts index 6625f81a00..ed5238353b 100644 --- a/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/GithubOAuth2Api.credentials.ts @@ -23,14 +23,14 @@ export class GithubOAuth2Api implements ICredentialType { displayName: 'Authorization URL', name: 'authUrl', type: 'hidden' as NodePropertyTypes, - default: 'https://github.com/login/oauth/authorize', + default: '={{$parameter["server"] === "https://api.github.com" ? "https://github.com" : $parameter["server"]}}/login/oauth/authorize', required: true, }, { displayName: 'Access Token URL', name: 'accessTokenUrl', type: 'hidden' as NodePropertyTypes, - default: 'https://github.com/login/oauth/access_token', + default: '={{$parameter["server"] === "https://api.github.com" ? "https://github.com" : $parameter["server"]}}/login/oauth/access_token', required: true, }, { From 09181063287c792402f9178315f619e54c8bf6d1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 14 Dec 2020 15:09:41 +0100 Subject: [PATCH 31/97] :zap: Make it possible to return string via Webhook-Node --- packages/cli/src/ResponseHelper.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ResponseHelper.ts b/packages/cli/src/ResponseHelper.ts index d09011eac0..4458316dcf 100644 --- a/packages/cli/src/ResponseHelper.ts +++ b/packages/cli/src/ResponseHelper.ts @@ -64,7 +64,11 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res } if (raw === true) { - res.json(data); + if (typeof data === 'string') { + res.send(data); + } else { + res.json(data); + } } else { res.json({ data, From f9911c65fe007f383a47cd8d07d450934d8778c7 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 14 Dec 2020 17:19:20 +0100 Subject: [PATCH 32/97] :sparkles: Make it possible that Webhook-Node can send custom response --- packages/nodes-base/nodes/Webhook.node.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/nodes-base/nodes/Webhook.node.ts b/packages/nodes-base/nodes/Webhook.node.ts index 8d819d37b5..31036a55b1 100644 --- a/packages/nodes-base/nodes/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook.node.ts @@ -250,6 +250,21 @@ export class Webhook implements INodeType { the received file. If the data gets received via "Form-Data Multipart"
      it will be the prefix and a number starting with 0 will be attached to it.`, }, + { + displayName: 'Response Data', + name: 'responseData', + type: 'string', + displayOptions: { + show: { + '/responseMode': [ + 'onReceived', + ], + }, + }, + default: '', + placeholder: 'success', + description: 'Custom response data to send.', + }, { displayName: 'Response Content-Type', name: 'responseContentType', @@ -480,7 +495,13 @@ export class Webhook implements INodeType { }; } + let webhookResponse: string | undefined; + if (options.responseData) { + webhookResponse = options.responseData as string; + } + return { + webhookResponse, workflowData: [ [ response, From abb1c54fcea9a6b4cbd7a2509a0e8464575c0caf Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Mon, 14 Dec 2020 22:06:58 +0100 Subject: [PATCH 33/97] :zap: Remove accidentally committed node --- packages/nodes-base/nodes/Example.node.ts | 68 ----------------------- 1 file changed, 68 deletions(-) delete mode 100644 packages/nodes-base/nodes/Example.node.ts diff --git a/packages/nodes-base/nodes/Example.node.ts b/packages/nodes-base/nodes/Example.node.ts deleted file mode 100644 index fcc025197e..0000000000 --- a/packages/nodes-base/nodes/Example.node.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { IExecuteFunctions } from 'n8n-core'; -import { - INodeExecutionData, - INodeType, - INodeTypeDescription, -} from 'n8n-workflow'; - - -export class Example implements INodeType { - description: INodeTypeDescription = { - displayName: 'Example', - name: 'example', - group: ['input'], - version: 1, - description: 'Example', - defaults: { - name: 'Example', - color: '#0000FF', - }, - inputs: ['main'], - outputs: ['main'], - properties: [ - { - displayName: 'Age', - name: 'age', - type: 'number', - default: 0, - }, - { - displayName: 'Brand Name', - name: 'brandName', - type: 'string', - default: '', - }, - ], - }; - - - execute(this: IExecuteFunctions): Promise { - - const items = this.getInputData(); - - if (items.length === 0) { - items.push({json: {}}); - } - - const returnData: INodeExecutionData[] = []; - let item: INodeExecutionData; - - for (let i = 0; i < items.length; i++) { - const age = this.getNodeParameter('age', i) as number | undefined; - const brandName = this.getNodeParameter('brandName', i) as string | undefined; - - item = items[i]; - - const newItem: INodeExecutionData = { - json: { - age: age || item.json.age, - brandName: brandName || item.json.brandName, - }, - }; - - returnData.push(newItem); - } - - return this.prepareOutputData(returnData); - } -} From 53a97cc27b91ad7a738bf8dfde761543bdcdfb52 Mon Sep 17 00:00:00 2001 From: Mathieu Ledru Date: Tue, 15 Dec 2020 10:50:52 +0100 Subject: [PATCH 34/97] :art: update wordings to distinct distribution model to it's license (#1220) --- README.md | 2 +- docker/images/n8n/README.md | 4 ++-- packages/cli/README.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 56467671a5..7fa03385ac 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,6 @@ Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to cont ## License -n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md). +n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license. Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license). diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index 87e812bdf0..5a8198ed9d 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -2,7 +2,7 @@ ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) -n8n is a free and open [fair-code](http://faircode.io) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools. +n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools. n8n.io - Screenshot @@ -305,6 +305,6 @@ Before you upgrade to the latest version make sure to check here if there are an ## License -n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) +n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license) diff --git a/packages/cli/README.md b/packages/cli/README.md index 5c519a8964..e7f951bbbe 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2,7 +2,7 @@ ![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) -n8n is a free and open [fair-code](http://faircode.io) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools. +n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools. n8n.io - Screenshot @@ -100,7 +100,7 @@ Before you upgrade to the latest version make sure to check here if there are an ## License -n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) +n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license) From d43aad2ca37715267d3223560e774719bfd8da2e Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 15 Dec 2020 16:56:28 +0100 Subject: [PATCH 35/97] :sparkles: Add Brandfetch node (#1253) * :sparkles: Add Brandfetch node * :zap: Small improvement * :zap: Add donwload field to logo operation * :bug: Minor fixes * :zap: Minor improvements to Brandfetch-Node Co-authored-by: Tanay Pant Co-authored-by: ricardo --- .../credentials/BrandfetchApi.credentials.ts | 18 ++ .../nodes/Brandfetch/Brandfetch.node.ts | 271 ++++++++++++++++++ .../nodes/Brandfetch/GenericFunctions.ts | 65 +++++ .../nodes/Brandfetch/brandfetch.png | Bin 0 -> 555 bytes packages/nodes-base/package.json | 2 + 5 files changed, 356 insertions(+) create mode 100644 packages/nodes-base/credentials/BrandfetchApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Brandfetch/Brandfetch.node.ts create mode 100644 packages/nodes-base/nodes/Brandfetch/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Brandfetch/brandfetch.png diff --git a/packages/nodes-base/credentials/BrandfetchApi.credentials.ts b/packages/nodes-base/credentials/BrandfetchApi.credentials.ts new file mode 100644 index 0000000000..27ebc7f78e --- /dev/null +++ b/packages/nodes-base/credentials/BrandfetchApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class BrandfetchApi implements ICredentialType { + name = 'brandfetchApi'; + displayName = 'Brandfetch API'; + documentationUrl = 'brandfetch'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Brandfetch/Brandfetch.node.ts b/packages/nodes-base/nodes/Brandfetch/Brandfetch.node.ts new file mode 100644 index 0000000000..6eafcf3c01 --- /dev/null +++ b/packages/nodes-base/nodes/Brandfetch/Brandfetch.node.ts @@ -0,0 +1,271 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + brandfetchApiRequest, +} from './GenericFunctions'; + +export class Brandfetch implements INodeType { + description: INodeTypeDescription = { + displayName: 'Brandfetch', + name: 'Brandfetch', + icon: 'file:brandfetch.png', + group: ['output'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Consume Brandfetch API', + defaults: { + name: 'Brandfetch', + color: '#1f1f1f', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'brandfetchApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + + { + name: 'Color', + value: 'color', + description: 'Return a company\'s colors', + }, + { + name: 'Company', + value: 'company', + description: 'Return a company\'s data', + }, + { + name: 'Font', + value: 'font', + description: 'Return a company\'s fonts', + }, + { + name: 'Industry', + value: 'industry', + description: 'Return a company\'s industry', + }, + { + name: 'Logo', + value: 'logo', + description: 'Return a company\'s logo & icon', + }, + ], + default: 'logo', + description: 'The operation to perform', + }, + + // ---------------------------------- + // All + // ---------------------------------- + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: '', + description: 'The domain name of the company.', + required: true, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + operation: [ + 'logo', + ], + }, + }, + description: 'Name of the binary property to which to
      write the data of the read file.', + }, + { + displayName: 'Image Type', + name: 'imageTypes', + type: 'multiOptions', + displayOptions: { + show: { + operation: [ + 'logo', + ], + download: [ + true, + ], + }, + }, + options: [ + { + name: 'Icon', + value: 'icon', + }, + { + name: 'Logo', + value: 'logo', + }, + ], + default: [ + 'logo', + 'icon', + ], + required: true, + }, + { + displayName: 'Image Format', + name: 'imageFormats', + type: 'multiOptions', + displayOptions: { + show: { + operation: [ + 'logo', + ], + download: [ + true, + ], + }, + }, + options: [ + { + name: 'PNG', + value: 'png', + }, + { + name: 'SVG', + value: 'svg', + }, + ], + default: [ + 'png', + ], + description: 'The image format in which the logo should be returned as.', + required: true, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const length = items.length as unknown as number; + + const operation = this.getNodeParameter('operation', 0) as string; + const responseData = []; + for (let i = 0; i < length; i++) { + if (operation === 'logo') { + const domain = this.getNodeParameter('domain', i) as string; + const download = this.getNodeParameter('download', i) as boolean; + + const body: IDataObject = { + domain, + }; + + const response = await brandfetchApiRequest.call(this, 'POST', `/logo`, body); + + if (download === true) { + + const imageTypes = this.getNodeParameter('imageTypes', i) as string[]; + + const imageFormats = this.getNodeParameter('imageFormats', i) as string[]; + + const newItem: INodeExecutionData = { + json: {}, + binary: {}, + }; + + if (items[i].binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + Object.assign(newItem.binary, items[i].binary); + } + + newItem.json = response.response; + + for (const imageType of imageTypes) { + for (const imageFormat of imageFormats) { + + const url = response.response[imageType][(imageFormat === 'png') ? 'image' : imageFormat] as string; + + if (url !== null) { + const data = await brandfetchApiRequest.call(this, 'GET', '', {}, {}, url, { json: false, encoding: null }); + + newItem.binary![`${imageType}_${imageFormat}`] = await this.helpers.prepareBinaryData(data, `${imageType}_${domain}.${imageFormat}`); + + items[i] = newItem; + } + items[i] = newItem; + } + } + if (Object.keys(items[i].binary!).length === 0) { + delete items[i].binary; + } + } else { + responseData.push(response.response); + } + } + if (operation === 'color') { + const domain = this.getNodeParameter('domain', i) as string; + + const body: IDataObject = { + domain, + }; + + const response = await brandfetchApiRequest.call(this, 'POST', `/color`, body); + responseData.push(response.response); + } + if (operation === 'font') { + const domain = this.getNodeParameter('domain', i) as string; + + const body: IDataObject = { + domain, + }; + + const response = await brandfetchApiRequest.call(this, 'POST', `/font`, body); + responseData.push(response.response); + } + if (operation === 'company') { + const domain = this.getNodeParameter('domain', i) as string; + + const body: IDataObject = { + domain, + }; + + const response = await brandfetchApiRequest.call(this, 'POST', `/company`, body); + responseData.push(response.response); + } + if (operation === 'industry') { + const domain = this.getNodeParameter('domain', i) as string; + + const body: IDataObject = { + domain, + }; + + const response = await brandfetchApiRequest.call(this, 'POST', `/industry`, body); + responseData.push.apply(responseData, response.response); + } + } + + if (operation === 'logo' && this.getNodeParameter('download', 0) === true) { + // For file downloads the files get attached to the existing items + return this.prepareOutputData(items); + } else { + return [this.helpers.returnJsonArray(responseData)]; + } + } +} diff --git a/packages/nodes-base/nodes/Brandfetch/GenericFunctions.ts b/packages/nodes-base/nodes/Brandfetch/GenericFunctions.ts new file mode 100644 index 0000000000..f3ae6e48ca --- /dev/null +++ b/packages/nodes-base/nodes/Brandfetch/GenericFunctions.ts @@ -0,0 +1,65 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function brandfetchApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + try { + const credentials = this.getCredentials('brandfetchApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + let options: OptionsWithUri = { + headers: { + 'x-api-key': credentials.apiKey, + }, + method, + qs, + body, + uri: uri || `https://api.brandfetch.io/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + + if (this.getNodeParameter('operation', 0) === 'logo' && options.json === false) { + delete options.headers; + } + + if (!Object.keys(body).length) { + delete options.body; + } + if (!Object.keys(qs).length) { + delete options.qs; + } + + const response = await this.helpers.request!(options); + + if (response.statusCode && response.statusCode !== 200) { + throw new Error(`Brandfetch error response [${response.statusCode}]: ${response.response}`); + } + + return response; + + } catch (error) { + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + const errorBody = error.response.body; + throw new Error(`Brandfetch error response [${error.statusCode}]: ${errorBody.message}`); + } + + // Expected error data did not get returned so throw the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Brandfetch/brandfetch.png b/packages/nodes-base/nodes/Brandfetch/brandfetch.png new file mode 100644 index 0000000000000000000000000000000000000000..144390c6205e91317c1e9f03e1753b514e06f48b GIT binary patch literal 555 zcmV+`0@VG9P)PM2bB}^DL9=@2Lk<0E|){^gbp5!MlT^%2Qz5k)9Lh3c}>%xQmH_{-v`UGU@#bf zVHl81Cf|Za1K%s!Y&M^|&iQ;+UZ>G$p!7rmuh(nzjZkeFeZ$^3jsx9p7sli9Q`hTs zI-d-DSBnM~&HXKmZQ`#X$@1t0|bZP#%eCC+ Date: Wed, 16 Dec 2020 12:38:58 +0100 Subject: [PATCH 36/97] :bookmark: Release n8n-core@0.56.0 --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 7740099ec0..6dd5793c99 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.55.0", + "version": "0.56.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 1fb4db21d65f661aacddf93bed020124033cf257 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 16 Dec 2020 12:39:52 +0100 Subject: [PATCH 37/97] :arrow_up: Set n8n-core@0.56.0 on n8n-nodes-base --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 766ce972be..d07b02e7ed 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -533,7 +533,7 @@ "mqtt": "4.2.1", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.55.0", + "n8n-core": "~0.56.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", From 9517981e9f17d4354bc1e0b6bb986e5b15f95ca3 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 16 Dec 2020 12:40:28 +0100 Subject: [PATCH 38/97] :bookmark: Release n8n-nodes-base@0.93.0 --- packages/nodes-base/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index d07b02e7ed..07ed3f877f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.92.0", + "version": "0.93.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From 51131d30cd8ecc8a30c739be579dfac3e9f4d1a1 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 16 Dec 2020 12:41:59 +0100 Subject: [PATCH 39/97] :bookmark: Release n8n-editor-ui@0.67.0 --- packages/editor-ui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 514c8d6d49..6aa3cf0d32 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.66.0", + "version": "0.67.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From ebd475fb32ff3c11eccb43f1cd145a387d68b068 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 16 Dec 2020 12:44:31 +0100 Subject: [PATCH 40/97] :arrow_up: Set n8n-core@0.56.0, n8n-editor-ui@0.67.0 and n8n-nodes-base@0.93.0 on n8n --- packages/cli/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9cbf8fd3c8..6e0591ea2c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -103,9 +103,9 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "~2.1.0", - "n8n-core": "~0.55.0", - "n8n-editor-ui": "~0.66.0", - "n8n-nodes-base": "~0.92.0", + "n8n-core": "~0.56.0", + "n8n-editor-ui": "~0.67.0", + "n8n-nodes-base": "~0.93.0", "n8n-workflow": "~0.47.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", From b5d4391ace58358b8a68e6a270813a92863f6677 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Wed, 16 Dec 2020 12:45:22 +0100 Subject: [PATCH 41/97] :bookmark: Release n8n@0.98.0 --- 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 6e0591ea2c..fdc579dbe1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.97.0", + "version": "0.98.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", From bfb344a23c238875e67e02ee1b67f42c6577d27f Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 18 Dec 2020 18:55:53 +0100 Subject: [PATCH 42/97] :sparkles: Add functionality to easily copy data and path of output data (#1260) * :sparkles: Add functionality to easily copy data and path of output data * :zap: Fix issues with copied path * :shirt: Fix lint issue * ;bug: Fix issue that some paths were wrong * :zap: Final improvements --- packages/editor-ui/package.json | 2 +- packages/editor-ui/src/components/RunData.vue | 165 +++++++++++++++--- packages/editor-ui/src/main.ts | 1 + packages/editor-ui/tsconfig.json | 1 + 4 files changed, 142 insertions(+), 27 deletions(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 6aa3cf0d32..92c1587883 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -79,7 +79,7 @@ "uuid": "^8.1.0", "vue": "^2.6.9", "vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0", - "vue-json-tree": "^0.4.1", + "vue-json-pretty": "^1.7.1", "vue-prism-editor": "^0.3.0", "vue-router": "^3.0.6", "vue-template-compiler": "^2.5.17", diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index b08f52e3dd..4f85a15ac5 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -62,6 +62,21 @@ +
      + + + + + + + + Copy Item Path + Copy Parameter Path + Copy Value + + + +
      @@ -104,10 +119,19 @@
      - @@ -171,8 +195,8 @@