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 0000000000..144390c620 Binary files /dev/null and b/packages/nodes-base/nodes/Brandfetch/brandfetch.png differ diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 56c1922704..766ce972be 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -43,6 +43,7 @@ "dist/credentials/BitlyApi.credentials.js", "dist/credentials/BitlyOAuth2Api.credentials.js", "dist/credentials/BoxOAuth2Api.credentials.js", + "dist/credentials/BrandfetchApi.credentials.js", "dist/credentials/ChargebeeApi.credentials.js", "dist/credentials/CircleCiApi.credentials.js", "dist/credentials/ClearbitApi.credentials.js", @@ -259,6 +260,7 @@ "dist/nodes/Bitly/Bitly.node.js", "dist/nodes/Box/Box.node.js", "dist/nodes/Box/BoxTrigger.node.js", + "dist/nodes/Brandfetch/Brandfetch.node.js", "dist/nodes/Calendly/CalendlyTrigger.node.js", "dist/nodes/Chargebee/Chargebee.node.js", "dist/nodes/Chargebee/ChargebeeTrigger.node.js",