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 dc4971bb59..0000000000 Binary files a/packages/nodes-base/nodes/Airtable/airtable.png and /dev/null differ diff --git a/packages/nodes-base/nodes/Airtable/airtable.svg b/packages/nodes-base/nodes/Airtable/airtable.svg new file mode 100644 index 0000000000..c38de2b550 --- /dev/null +++ b/packages/nodes-base/nodes/Airtable/airtable.svg @@ -0,0 +1,21 @@ + + + + + + + + + + +