From 419b58024a4984435ae573fc0f1c1b9cebf89345 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Sat, 3 Oct 2020 08:08:50 -0400 Subject: [PATCH] :sparkles: Add Mindee-Node (#1004) * :sparkles: Mindee-Node * :zap: Improvements --- .../MindeeInvoiceApi.credentials.ts | 17 ++ .../MindeeReceiptApi.credentials.ts | 17 ++ .../nodes/Mindee/GenericFunctions.ts | 84 +++++++ .../nodes-base/nodes/Mindee/Mindee.node.ts | 220 ++++++++++++++++++ packages/nodes-base/nodes/Mindee/mindee.png | Bin 0 -> 1909 bytes packages/nodes-base/package.json | 3 + 6 files changed, 341 insertions(+) create mode 100644 packages/nodes-base/credentials/MindeeInvoiceApi.credentials.ts create mode 100644 packages/nodes-base/credentials/MindeeReceiptApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Mindee/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Mindee/Mindee.node.ts create mode 100644 packages/nodes-base/nodes/Mindee/mindee.png diff --git a/packages/nodes-base/credentials/MindeeInvoiceApi.credentials.ts b/packages/nodes-base/credentials/MindeeInvoiceApi.credentials.ts new file mode 100644 index 0000000000..527551aea4 --- /dev/null +++ b/packages/nodes-base/credentials/MindeeInvoiceApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MindeeInvoiceApi implements ICredentialType { + name = 'mindeeInvoiceApi'; + displayName = 'Mindee Invoice API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/MindeeReceiptApi.credentials.ts b/packages/nodes-base/credentials/MindeeReceiptApi.credentials.ts new file mode 100644 index 0000000000..bdd1337070 --- /dev/null +++ b/packages/nodes-base/credentials/MindeeReceiptApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MindeeReceiptApi implements ICredentialType { + name = 'mindeeReceiptApi'; + displayName = 'Mindee Receipt API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Mindee/GenericFunctions.ts b/packages/nodes-base/nodes/Mindee/GenericFunctions.ts new file mode 100644 index 0000000000..33055adddf --- /dev/null +++ b/packages/nodes-base/nodes/Mindee/GenericFunctions.ts @@ -0,0 +1,84 @@ +import { + OptionsWithUri, + } from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function mindeeApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise { // tslint:disable-line:no-any + + const resource = this.getNodeParameter('resource', 0) as string; + + let credentials; + + if (resource === 'receipt') { + credentials = this.getCredentials('mindeeReceiptApi') as IDataObject; + } else { + credentials = this.getCredentials('mindeeInvoiceApi') as IDataObject; + } + + const options: OptionsWithUri = { + headers: { + 'X-Inferuser-Token': credentials.apiKey, + }, + method, + body, + qs, + uri: `https://api.mindee.net/products${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + if (Object.keys(qs).length === 0) { + delete options.qs; + } + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + if (error.response && error.response.body && error.response.body.error) { + + let errors = error.response.body.error.errors; + + errors = errors.map((e: IDataObject) => e.message); + // Try to return the error prettier + throw new Error( + `Mindee error response [${error.statusCode}]: ${errors.join('|')}` + ); + } + throw error; + } +} + +export function cleanData(predictions: IDataObject[]) { + + const newData: IDataObject = {}; + + for (const key of Object.keys(predictions[0])) { + + const data = predictions[0][key] as IDataObject | IDataObject[]; + + if (key === 'taxes' && data.length) { + newData[key] = { + amount: (data as IDataObject[])[0].amount, + rate: (data as IDataObject[])[0].rate, + }; + } else { + //@ts-ignore + newData[key] = data.value || data.name || data.raw || data.degrees || data.amount || data.iban; + } + } + + return newData; +} diff --git a/packages/nodes-base/nodes/Mindee/Mindee.node.ts b/packages/nodes-base/nodes/Mindee/Mindee.node.ts new file mode 100644 index 0000000000..c59ecb61bb --- /dev/null +++ b/packages/nodes-base/nodes/Mindee/Mindee.node.ts @@ -0,0 +1,220 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + cleanData, + mindeeApiRequest, +} from './GenericFunctions'; + +export class Mindee implements INodeType { + description: INodeTypeDescription = { + displayName: 'Mindee', + name: 'mindee', + icon: 'file:mindee.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Mindee API.', + defaults: { + name: 'Mindee', + color: '#e94950', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'mindeeReceiptApi', + required: true, + displayOptions: { + show: { + resource: [ + 'receipt', + ], + }, + }, + }, + { + name: 'mindeeInvoiceApi', + required: true, + displayOptions: { + show: { + resource: [ + 'invoice', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Receipt', + value: 'receipt', + }, + { + name: 'Invoice', + value: 'invoice', + }, + ], + default: 'receipt', + description: 'The resource to operate on.' + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Predict', + value: 'predict', + }, + ], + default: 'predict', + description: 'The resource to operate on.' + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + required: true, + default: 'data', + displayOptions: { + show: { + operation: [ + 'predict' + ], + resource: [ + 'receipt', + 'invoice', + ], + }, + }, + description: 'Name of the binary property which containsthe data for the file to be uploaded.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: `Returns the data exactly in the way it got received from the API.`, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'receipt') { + if (operation === 'predict') { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + const rawData = this.getNodeParameter('rawData', i) as boolean; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await mindeeApiRequest.call( + this, + 'POST', + `/expense_receipts/v2/predict`, + {}, + {}, + { + formData: { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + } + }, + }, + }, + ); + + if (rawData === false) { + responseData = cleanData(responseData.predictions); + } + } + } + + if (resource === 'invoice') { + if (operation === 'predict') { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + + const rawData = this.getNodeParameter('rawData', i) as boolean; + + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + + const item = items[i].binary as IBinaryKeyData; + + const binaryData = item[binaryPropertyName] as IBinaryData; + + if (binaryData === undefined) { + throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`); + } + + responseData = await mindeeApiRequest.call( + this, + 'POST', + `/invoices/v1/predict`, + {}, + {}, + { + formData: { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + } + }, + }, + }, + ); + + if (rawData === false) { + responseData = cleanData(responseData.predictions); + } + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Mindee/mindee.png b/packages/nodes-base/nodes/Mindee/mindee.png new file mode 100644 index 0000000000000000000000000000000000000000..26d8060cda5dbbb84e51686960a3fa4037e9c8c5 GIT binary patch literal 1909 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD3OsG;hE;^%b*2hb1<+l zvN13NS&R%!Ktc%0W(2Y|5aR8b3@l(Z3=DQant_3N0V6`?0w%buH|Sk5DQD z1Jk3-kcg59UmvUF{9L`nl>DSry^7odkOBsq3M(KpH?<^Dp&~aYuh^=>Rtc=a3djZt z>nkaMm6T-LDnNc-DA*LGq*(>IxIwi8dA3R!B_#z``ugSN z<$C4Ddih1^`i7R4mih)p`bI{&Koz>hm3bwJ6}oxF$`C_f=D4I5Cl_TFlw{`TDS*sP zOv*1Uu~kw6Sp)|Vca~(PA#BPkhI$L=L4A;nzM-ChJ~nNs6`44+fn*@s!2W_*X9F_K zDj*}jBp(Vr~?^K(i;#)sx*mcSi|CXA#DYXzR?C&@hBw@_$? zBy(J!LSGeE?t9i-S&cD$et{lw5nB3+9>M`JIsOu+d-L;`=scag)ZyOwhW~|&y@TFg z^;Zy7_vl;Et+`;{!snTLH2gL+q~Fo9O6J`Wa3|%H!>mt_J}>t+e5X2n`PvqlxrKAf zd>5?ak(k}G(~#kE{iU-{-AqKz32*6o7`ziw#^Dgho>73H2w5+O5@aJcrr>9nx z`|V>Fy8iU+C4tZPuRJhZ{BM`r%21b|*Pdx`F6hY&vgkUY9{HVr#_gPI)^UrUf70>U z#eFyY!?`s*G6GAdmc6>gQTfF6QIY7b^AioHzU)qH+v}vcY@xu5_o6eLe=|;7p3W|>9(xHigMGH3{QdqU_;Bf|Tk=2?r zlG_`ZtM{5O_$Ii_SEhq4@P^5oJGCr5HR<>EZ{X%X#Brqdl%PW5g#+_C^&ObJHmxe# z(Bqn=`rq^7#1k`S#fGZ~gg@rf4wlTA5Y5ufwo%olpz&s6=ca-^$BwUGmDer8uyE5} zo7&W#{M6SjvDeQX+gq8pv-(`6_1~Pm+DyCubv={zEoeISa`)V6S3WC6K1g|Ull}S4 z?1Zkf9se2D*z4Rqdr0WaO1lN$7qBmttbcB&J)!yJKW3%(s=H^gO?y$$w7>QdgPPx@ z7iZTj@{fOLQ+d2YBOy5a_%jPbo;NRVN(ipxnX~ZuffxMes+O6Y{<+veWNXRoSAVQ? z3g`AFm+{T0IlcAv+G&XehN2zQsUS4WZAQqTYlZt zPg6Z!@>TZfftO5P7G}(qOIqBe&VE);xE1}DGgb2E&C3q|3-w)@I3~>rc=hbu0h4Qw Z`L*wJ_56LJd<0bcc)I$ztaD0e0szVw