From 8111fea470ece1eca969458bc9f85c3050c7e0e4 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 24 Nov 2020 04:53:39 -0500 Subject: [PATCH] :zap: List operation now can download attachaments automatically (#1199) * :zap: List operation now can download attachaments automatically https://community.n8n.io/t/how-to-send-attachments-using-mailgun-node/2979/4 * :zap: Minor improvements to Airtable node Co-authored-by: Jan Oberhauser --- .../nodes/Airtable/Airtable.node.ts | 47 +++++++++++++- .../nodes/Airtable/GenericFunctions.ts | 60 ++++++++++++++++-- .../nodes-base/nodes/Airtable/airtable.png | Bin 1435 -> 0 bytes .../nodes-base/nodes/Airtable/airtable.svg | 21 ++++++ 4 files changed, 121 insertions(+), 7 deletions(-) delete mode 100644 packages/nodes-base/nodes/Airtable/airtable.png create mode 100644 packages/nodes-base/nodes/Airtable/airtable.svg diff --git a/packages/nodes-base/nodes/Airtable/Airtable.node.ts b/packages/nodes-base/nodes/Airtable/Airtable.node.ts index 98d499f72d..20e62b4e5c 100644 --- a/packages/nodes-base/nodes/Airtable/Airtable.node.ts +++ b/packages/nodes-base/nodes/Airtable/Airtable.node.ts @@ -1,6 +1,7 @@ import { IExecuteFunctions, } from 'n8n-core'; + import { IDataObject, INodeExecutionData, @@ -11,19 +12,20 @@ import { import { apiRequest, apiRequestAllItems, + downloadRecordAttachments, } from './GenericFunctions'; export class Airtable implements INodeType { description: INodeTypeDescription = { displayName: 'Airtable', name: 'airtable', - icon: 'file:airtable.png', + icon: 'file:airtable.svg', group: ['input'], version: 1, description: 'Read, update, write and delete data from Airtable', defaults: { name: 'Airtable', - color: '#445599', + color: '#000000', }, inputs: ['main'], outputs: ['main'], @@ -188,7 +190,38 @@ export class Airtable implements INodeType { default: 100, description: 'Number of results to return.', }, - + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'list', + ], + }, + }, + default: false, + description: `When set to true the attachment fields define in 'Download Fields' will be downloaded.`, + }, + { + displayName: 'Download Fields', + name: 'downloadFieldNames', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'list', + ], + downloadAttachments: [ + true, + ], + }, + }, + default: '', + description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive.`, + }, { displayName: 'Additional Options', name: 'additionalOptions', @@ -489,6 +522,8 @@ export class Airtable implements INodeType { returnAll = this.getNodeParameter('returnAll', 0) as boolean; + const downloadAttachments = this.getNodeParameter('downloadAttachments', 0) as boolean; + const additionalOptions = this.getNodeParameter('additionalOptions', 0, {}) as IDataObject; for (const key of Object.keys(additionalOptions)) { @@ -508,6 +543,12 @@ export class Airtable implements INodeType { returnData.push.apply(returnData, responseData.records); + if (downloadAttachments === true) { + const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', 0) as string).split(','); + const data = await downloadRecordAttachments.call(this, responseData.records, downloadFieldNames); + return [data]; + } + } else if (operation === 'read') { // ---------------------------------- // read diff --git a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts index dd9c0aab91..6a0e688f49 100644 --- a/packages/nodes-base/nodes/Airtable/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Airtable/GenericFunctions.ts @@ -4,9 +4,28 @@ import { ILoadOptionsFunctions, } from 'n8n-core'; -import { OptionsWithUri } from 'request'; -import { IDataObject } from 'n8n-workflow'; +import { + OptionsWithUri, +} from 'request'; +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, +} from 'n8n-workflow'; + + +interface IAttachment { + url: string; + filename: string; + type: string; +} + +export interface IRecord { + fields: { + [key: string]: string | IAttachment[], + }; +} /** * Make an API request to Airtable @@ -17,7 +36,7 @@ import { IDataObject } from 'n8n-workflow'; * @param {object} body * @returns {Promise} */ -export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject): Promise { // tslint:disable-line:no-any +export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: object, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('airtableApi'); if (credentials === undefined) { @@ -37,10 +56,18 @@ export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoa method, body, qs: query, - uri: `https://api.airtable.com/v0/${endpoint}`, + uri: uri || `https://api.airtable.com/v0/${endpoint}`, json: true, }; + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + try { return await this.helpers.request!(options); } catch (error) { @@ -101,3 +128,28 @@ export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunction records: returnData, }; } + +export async function downloadRecordAttachments(this: IExecuteFunctions, records: IRecord[], fieldNames: string[]): Promise { + const elements: INodeExecutionData[] = []; + for (const record of records) { + const element: INodeExecutionData = { json: {}, binary: {} }; + element.json = record as unknown as IDataObject; + for (const fieldName of fieldNames) { + if (record.fields[fieldName] !== undefined) { + for (const [index, attachment] of (record.fields[fieldName] as IAttachment[]).entries()) { + const file = await apiRequest.call(this, 'GET', '', {}, {}, attachment.url, { json: false, encoding: null }); + element.binary![`${fieldName}_${index}`] = { + data: Buffer.from(file).toString('base64'), + fileName: attachment.filename, + mimeType: attachment.type, + }; + } + } + } + if (Object.keys(element.binary as IBinaryKeyData).length === 0) { + delete element.binary; + } + elements.push(element); + } + return elements; +} diff --git a/packages/nodes-base/nodes/Airtable/airtable.png b/packages/nodes-base/nodes/Airtable/airtable.png deleted file mode 100644 index dc4971bb5911c4f8f953a661979bfbae2b215380..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1435 zcmXYw2}~1a6oCKU(6!}mxr)^av``9CsB%oiiCC^NMB)`u22*s3h)%^x5HKF7h-@BV z0xHNR2&G(7PJvPoA}Ap90`Wi>)^ZD_$6HDE@?PGNm%QY??_CHF3#5{*$p|5;Xw!xW zz4u)*1Kd(&1$P}n*x2xp$N+tc$<#VjFR;o~3O5lnBKt~}w!q+16>~Ag@Z&84G$Uxm zKnt>wf0nU{D=&{stlG4)c5%Wj5ZX|My$~KFXhI-IE2L_8gqGK6rFMMi#zH-Y(W0Aj z5!%$ML5yAMX(gH#LLH3_9XN844*F0o)q;FoYnTVnhJ$JYYMx3rjUm?QBse1Ek!D{Q zQf10nnZdl20ynS~2m@ECNTQoH#faH!^mFYi)yh>NdbO+5DO>~Cl0}eVYL*G02?Grn zkYb<{iJ6|v2G9pg$s(&<#d@H+&*VWJ0x_aXVbsBkA%<5RPrEj?l*zke&4&i;oX{O) z*m4FxkKx>8$Axx8uTg)o=h4IfT{wnR2i<5~=n6$RXu^MW$gCc-@Jd`5%lLAligmR!YYGD`yFYr*0E2Wr#oN(RR1n!VXCD3GO zddG|oHH440bSNZi1SW77ODcX}LbTM}puiQw85a{mnhvsvP(U78ZUSeBluqp+oqP=u ztL%)i{Qx}(TMStw>eosNE?=wjH-!rF=N2nCMj6rX5TV5$Fbl;Pp$0@kTvo6-Oh&HY2=ZP@$XQ zQb{=F%O)E$tWKp;&CSkCE8dU392jV8dt6uh@Imf{bE%2@c0>e;*86y`ap!Vvt*jUn zgN23pndzwsrE+X+?BmA|!^5v%4)pgw>+9|5>F$upT3egzrFFH{VsUwC@%^HE_wMHX zk$dxcPWIJH7tWteO+9@o`NZ)f35O2skB^IuiQctq=eDS*i0?y!MS=eQervz=_4M#? zcjGy6*ejMZtr>I+b2B62A}f%he-=;C2LH%Saz$TUf{nj%>S*E)fv~6N&EXt(!T*wU zYP`Nt&>og6Eb<$yDDBLaepM+CXi~Pt7X?=O7&}MxmV9cDDmfM$mqXrO>zH{lvBwaEjcs2)f+;iZ7Yqlk7k8A4+gR?53MfB=G#1voqgq$P8rPa3cmlF zn`wNjhMnEv=WG|87(8m)b(6UzI`!>xfuTq^L~|0lKkT{~$@gxwx;a?x(cc-FTUC`U ziXJAMXmk~5^0~JjjjvkQtfu(-BzDF7Sn?T{{*C=L%6#)Qzk#$PV9$PUoRL;>Bx_{X zH+_%%a&+^7;t`CGvm-DGP z)|5F@{f_gq5%=V!12d$2WuBVskwdVz^em&w!(7IV3jTR$OnT9=*H()!AgW&-x$+^> z^Y5Y6pG%qRs5*=0cN&B&EQ|Tg@%Zp7=hmEV8`>%RYyV`=!rbx6t8#B*irSn-=82M0 zfA^(lm~=lm_(0qq>U7fJW(-dx-NG2baO9jMqqWZvM{asUs@m?%2@?eD%j$MXKwGxN hCX^gHyR{;DD + + + + + + + + + +