From d43aad2ca37715267d3223560e774719bfd8da2e Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 15 Dec 2020 16:56:28 +0100 Subject: [PATCH] :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+