diff --git a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.json b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.json new file mode 100644 index 0000000000..fdf3169be8 --- /dev/null +++ b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.apiTemplateIo", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Marketing & Content" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/apiTemplateIo" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.apiTemplateIo/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts new file mode 100644 index 0000000000..ee6e63effa --- /dev/null +++ b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts @@ -0,0 +1,560 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + apiTemplateIoApiRequest, + downloadImage, + loadResource, + validateJSON, +} from './GenericFunctions'; + +export class ApiTemplateIo implements INodeType { + description: INodeTypeDescription = { + displayName: 'APITemplate.io', + name: 'ApiTemplateIo', + icon: 'file:apiTemplateIo.svg', + group: ['transform'], + version: 1, + description: 'Consume the APITemplate.io API', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'APITemplate.io', + color: '#c0c0c0', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'apiTemplateIoApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Account', + value: 'account', + }, + { + name: 'Image', + value: 'image', + }, + { + name: 'PDF', + value: 'pdf', + }, + ], + default: 'image', + description: 'Resource to consume', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'create', + required: true, + description: 'Operation to perform', + options: [ + { + name: 'Create', + value: 'create', + }, + ], + displayOptions: { + show: { + resource: [ + 'image', + 'pdf', + ], + }, + }, + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + default: 'get', + required: true, + description: 'Operation to perform', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + }, + { + displayName: 'Template ID', + name: 'imageTemplateId', + type: 'options', + required: true, + default: '', + description: 'ID of the image template to use.', + typeOptions: { + loadOptionsMethod: 'getImageTemplates', + }, + displayOptions: { + show: { + resource: [ + 'image', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Template ID', + name: 'pdfTemplateId', + type: 'options', + required: true, + default: '', + description: 'ID of the PDF template to use.', + typeOptions: { + loadOptionsMethod: 'getPdfTemplates', + }, + displayOptions: { + show: { + resource: [ + 'pdf', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'JSON Parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: [ + 'pdf', + 'image', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Download', + name: 'download', + type: 'boolean', + default: false, + displayOptions: { + show: { + resource: [ + 'pdf', + 'image', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Name of the binary property to which to
write the data of the read file.', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + required: true, + default: 'data', + description: 'Name of the binary property to which to write to.', + displayOptions: { + show: { + resource: [ + 'pdf', + 'image', + ], + operation: [ + 'create', + ], + download: [ + true, + ], + }, + }, + }, + { + displayName: 'Overrides (JSON)', + name: 'overridesJson', + type: 'json', + default: '', + displayOptions: { + show: { + resource: [ + 'image', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + placeholder: `[ {"name": "text_1", "text": "hello world", "textBackgroundColor": "rgba(246, 243, 243, 0)" } ]`, + }, + { + displayName: 'Properties (JSON)', + name: 'propertiesJson', + type: 'json', + default: '', + displayOptions: { + show: { + resource: [ + 'pdf', + ], + operation: [ + 'create', + ], + jsonParameters: [ + true, + ], + }, + }, + placeholder: `{ "name": "text_1" }`, + }, + { + displayName: 'Overrides', + name: 'overridesUi', + placeholder: 'Add Override', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'image', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + default: {}, + options: [ + { + name: 'overrideValues', + displayName: 'Override', + values: [ + { + displayName: 'Properties', + name: 'propertiesUi', + placeholder: 'Add Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'propertyValues', + displayName: 'Property', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the property', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to the property.', + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Properties', + name: 'propertiesUi', + placeholder: 'Add Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + displayOptions: { + show: { + resource: [ + 'pdf', + ], + operation: [ + 'create', + ], + jsonParameters: [ + false, + ], + }, + }, + options: [ + { + name: 'propertyValues', + displayName: 'Property', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: 'Name of the property', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value to the property.', + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'create', + ], + resource: [ + 'pdf', + 'image', + ], + 'download': [ + true, + ], + }, + }, + default: {}, + options: [ + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'The name of the downloaded image/pdf. It has to include the extension. For example: report.pdf', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + async getImageTemplates(this: ILoadOptionsFunctions): Promise { + return await loadResource.call(this, 'image'); + }, + + async getPdfTemplates(this: ILoadOptionsFunctions): Promise { + return await loadResource.call(this, 'pdf'); + }, + }, + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = items.length; + + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + if (resource === 'account') { + + // ********************************************************************* + // account + // ********************************************************************* + + if (operation === 'get') { + + // ---------------------------------- + // account: get + // ---------------------------------- + + for (let i = 0; i < length; i++) { + + responseData = await apiTemplateIoApiRequest.call(this, 'GET', '/account-information'); + + returnData.push(responseData); + } + } + + } else if (resource === 'image') { + + // ********************************************************************* + // image + // ********************************************************************* + + if (operation === 'create') { + // ---------------------------------- + // image: create + // ---------------------------------- + + const download = this.getNodeParameter('download', 0) as boolean; + + // https://docs.apitemplate.io/reference/api-reference.html#create-an-image-jpeg-and-png + for (let i = 0; i < length; i++) { + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const qs = { + template_id: this.getNodeParameter('imageTemplateId', i), + }; + + const body = { overrides: [] } as IDataObject; + + if (jsonParameters === false) { + const overrides = (this.getNodeParameter('overridesUi', i) as IDataObject || {}).overrideValues as IDataObject[] || []; + if (overrides.length !== 0) { + const data: IDataObject[] = []; + for (const override of overrides) { + const properties = (override.propertiesUi as IDataObject || {}).propertyValues as IDataObject[] || []; + data.push(properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {})); + } + body.overrides = data; + } + } else { + const overrideJson = this.getNodeParameter('overridesJson', i) as string; + if (overrideJson !== '') { + const data = validateJSON(overrideJson); + if (data === undefined) { + throw new Error('A valid JSON must be provided.'); + } + body.overrides = data; + } + } + + responseData = await apiTemplateIoApiRequest.call(this, 'POST', '/create', qs, body); + + if (download === true) { + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + const data = await downloadImage.call(this, responseData.download_url); + const fileName = responseData.download_url.split('/').pop(); + const binaryData = await this.helpers.prepareBinaryData(data, options.fileName || fileName); + responseData = { + json: responseData, + binary: { + [binaryProperty]: binaryData, + }, + }; + } + returnData.push(responseData); + } + + if (download === true) { + return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); + } + } + + } else if (resource === 'pdf') { + + // ********************************************************************* + // pdf + // ********************************************************************* + + if (operation === 'create') { + + // ---------------------------------- + // pdf: create + // ---------------------------------- + + // https://docs.apitemplate.io/reference/api-reference.html#create-a-pdf + const download = this.getNodeParameter('download', 0) as boolean; + + for (let i = 0; i < length; i++) { + const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const qs = { + template_id: this.getNodeParameter('pdfTemplateId', i), + }; + + let data; + + if (jsonParameters === false) { + const properties = (this.getNodeParameter('propertiesUi', i) as IDataObject || {}).propertyValues as IDataObject[] || []; + if (properties.length === 0) { + throw new Error('The parameter properties cannot be empty'); + } + data = properties.reduce((obj, value) => Object.assign(obj, { [`${value.key}`]: value.value }), {}); + } else { + const propertiesJson = this.getNodeParameter('propertiesJson', i) as string; + data = validateJSON(propertiesJson); + if (data === undefined) { + throw new Error('A valid JSON must be provided.'); + } + } + + responseData = await apiTemplateIoApiRequest.call(this, 'POST', '/create', qs, data); + + if (download === true) { + const binaryProperty = this.getNodeParameter('binaryProperty', i) as string; + const data = await downloadImage.call(this, responseData.download_url); + const fileName = responseData.download_url.split('/').pop(); + const binaryData = await this.helpers.prepareBinaryData(data, options.fileName || fileName); + responseData = { + json: responseData, + binary: { + [binaryProperty]: binaryData, + }, + }; + } + returnData.push(responseData); + } + if (download === true) { + return this.prepareOutputData(returnData as unknown as INodeExecutionData[]); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/ApiTemplateIo/GenericFunctions.ts b/packages/nodes-base/nodes/ApiTemplateIo/GenericFunctions.ts new file mode 100644 index 0000000000..083683434d --- /dev/null +++ b/packages/nodes-base/nodes/ApiTemplateIo/GenericFunctions.ts @@ -0,0 +1,91 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +export async function apiTemplateIoApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + qs = {}, + body = {}, +) { + const { apiKey } = this.getCredentials('apiTemplateIoApi') as { apiKey: string }; + + const options: OptionsWithUri = { + headers: { + 'user-agent': 'n8n', + Accept: 'application/json', + 'X-API-KEY': `${apiKey}`, + }, + uri: `https://api.apitemplate.io/v1${endpoint}`, + method, + qs, + body, + followRedirect: true, + followAllRedirects: true, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + const response = await this.helpers.request!(options); + if (response.status === 'error') { + throw new Error(response.message); + } + return response; + } catch (error) { + if (error?.response?.body?.message) { + throw new Error(`APITemplate.io error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function loadResource( + this: ILoadOptionsFunctions, + resource: 'image' | 'pdf', +) { + const target = resource === 'image' ? ['JPEG', 'PNG'] : ['PDF']; + const templates = await apiTemplateIoApiRequest.call(this, 'GET', '/list-templates'); + const filtered = templates.filter(({ format }: { format: 'PDF' | 'JPEG' | 'PNG' }) => target.includes(format)); + + return filtered.map(({ format, name, id }: { format: string, name: string, id: string }) => ({ + name: `${name} (${format})`, + value: id, + })); +} + +export function validateJSON(json: string | object | undefined): any { // tslint:disable-line:no-any + let result; + if (typeof json === 'object') { + return json; + } + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} + + +export function downloadImage(this: IExecuteFunctions, url: string) { + return this.helpers.request({ + uri: url, + method: 'GET', + json: false, + encoding: null, + }); +} diff --git a/packages/nodes-base/nodes/ApiTemplateIo/apiTemplateIo.svg b/packages/nodes-base/nodes/ApiTemplateIo/apiTemplateIo.svg new file mode 100755 index 0000000000..5ccaf179a9 --- /dev/null +++ b/packages/nodes-base/nodes/ApiTemplateIo/apiTemplateIo.svg @@ -0,0 +1,102 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 01f2943c56..1fbde4bdda 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -35,6 +35,7 @@ "dist/credentials/Amqp.credentials.js", "dist/credentials/AsanaApi.credentials.js", "dist/credentials/AsanaOAuth2Api.credentials.js", + "dist/credentials/ApiTemplateIoApi.credentials.js", "dist/credentials/AutomizyApi.credentials.js", "dist/credentials/Aws.credentials.js", "dist/credentials/AffinityApi.credentials.js", @@ -268,6 +269,7 @@ "dist/nodes/Amqp/AmqpTrigger.node.js", "dist/nodes/Asana/Asana.node.js", "dist/nodes/Asana/AsanaTrigger.node.js", + "dist/nodes/ApiTemplateIo/ApiTemplateIo.node.js", "dist/nodes/Affinity/Affinity.node.js", "dist/nodes/Affinity/AffinityTrigger.node.js", "dist/nodes/Automizy/Automizy.node.js",