mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
✨ Add AWS Textract node (#2330)
* ✨ AWS Textract node * ⚡ Fix issue with types * ⚡ Small change to labels * ⚡ Improvements * ⚡ Improve error message * ⚡ Improve description Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
cba2874c2e
commit
bb05f8113d
|
@ -17,7 +17,7 @@ export class Aws implements ICredentialType {
|
|||
default: 'us-east-1',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Key Id',
|
||||
displayName: 'Access Key ID',
|
||||
name: 'accessKeyId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
|
|
|
@ -22,7 +22,7 @@ export class S3 implements ICredentialType {
|
|||
default: 'us-east-1',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Key Id',
|
||||
displayName: 'Access Key ID',
|
||||
name: 'accessKeyId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
|
|
163
packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts
Normal file
163
packages/nodes-base/nodes/Aws/Textract/AwsTextract.node.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IBinaryKeyData,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialTestFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeCredentialTestResult,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
awsApiRequestREST,
|
||||
IExpenseDocument,
|
||||
simplify,
|
||||
validateCrendetials,
|
||||
} from './GenericFunctions';
|
||||
|
||||
export class AwsTextract implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'AWS Textract',
|
||||
name: 'awsTextract',
|
||||
icon: 'file:textract.svg',
|
||||
group: ['output'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Sends data to Amazon Textract',
|
||||
defaults: {
|
||||
name: 'AWS Textract',
|
||||
color: '#5aa08d',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'aws',
|
||||
required: true,
|
||||
testedBy: 'awsTextractApiCredentialTest',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Analyze Receipt or Invoice',
|
||||
value: 'analyzeExpense',
|
||||
},
|
||||
],
|
||||
default: 'analyzeExpense',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
name: 'binaryPropertyName',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'analyzeExpense',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG',
|
||||
},
|
||||
{
|
||||
displayName: 'Simplify Response',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'analyzeExpense',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
description: 'Return a simplified version of the response instead of the raw data.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async awsTextractApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
|
||||
try {
|
||||
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject, 'sts');
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'Error',
|
||||
message: 'The security token included in the request is invalid',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'OK',
|
||||
message: 'Connection successful!',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
let responseData;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
//https://docs.aws.amazon.com/textract/latest/dg/API_AnalyzeExpense.html
|
||||
if (operation === 'analyzeExpense') {
|
||||
const binaryProperty = this.getNodeParameter('binaryPropertyName', i) as string;
|
||||
const simple = this.getNodeParameter('simple', i) as boolean;
|
||||
|
||||
if (items[i].binary === undefined) {
|
||||
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
|
||||
}
|
||||
|
||||
if ((items[i].binary as IBinaryKeyData)[binaryProperty] === undefined) {
|
||||
throw new NodeOperationError(this.getNode(), `No binary data property "${binaryProperty}" does not exists on item!`);
|
||||
}
|
||||
|
||||
const binaryPropertyData = (items[i].binary as IBinaryKeyData)[binaryProperty];
|
||||
|
||||
const body: IDataObject = {
|
||||
Document: {
|
||||
Bytes: binaryPropertyData.data,
|
||||
},
|
||||
};
|
||||
|
||||
const action = 'Textract.AnalyzeExpense';
|
||||
responseData = await awsApiRequestREST.call(this, 'textract', 'POST', '', JSON.stringify(body), { 'x-amz-target': action, 'Content-Type': 'application/x-amz-json-1.1' }) as IExpenseDocument;
|
||||
if (simple) {
|
||||
responseData = simplify(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData as unknown as IDataObject);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ error: error.message });
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
156
packages/nodes-base/nodes/Aws/Textract/GenericFunctions.ts
Normal file
156
packages/nodes-base/nodes/Aws/Textract/GenericFunctions.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
import {
|
||||
URL,
|
||||
} from 'url';
|
||||
|
||||
import {
|
||||
Request,
|
||||
sign,
|
||||
} from 'aws4';
|
||||
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
parseString,
|
||||
} from 'xml2js';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestFunctions,
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
function getEndpointForService(service: string, credentials: ICredentialDataDecryptedObject): string {
|
||||
let endpoint;
|
||||
if (service === 'lambda' && credentials.lambdaEndpoint) {
|
||||
endpoint = credentials.lambdaEndpoint;
|
||||
} else if (service === 'sns' && credentials.snsEndpoint) {
|
||||
endpoint = credentials.snsEndpoint;
|
||||
} else {
|
||||
endpoint = `https://${service}.${credentials.region}.amazonaws.com`;
|
||||
}
|
||||
return (endpoint as string).replace('{region}', credentials.region as string);
|
||||
}
|
||||
|
||||
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = await this.getCredentials('aws');
|
||||
if (credentials === undefined) {
|
||||
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
|
||||
}
|
||||
|
||||
// Concatenate path and instantiate URL object so it parses correctly query strings
|
||||
const endpoint = new URL(getEndpointForService(service, credentials) + path);
|
||||
|
||||
// Sign AWS API request with the user credentials
|
||||
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
|
||||
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
|
||||
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: signOpts.headers,
|
||||
method,
|
||||
uri: endpoint.href,
|
||||
body: signOpts.body,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this.helpers.request!(options);
|
||||
} catch (error) {
|
||||
if (error?.response?.data || error?.response?.body) {
|
||||
const errorMessage = error?.response?.data || error?.response?.body;
|
||||
if (errorMessage.includes('AccessDeniedException')) {
|
||||
const user = JSON.parse(errorMessage).Message.split(' ')[1];
|
||||
throw new NodeApiError(this.getNode(), error, {
|
||||
message: 'Unauthorized — please check your AWS policy configuration',
|
||||
description: `Make sure an identity-based policy allows user ${user} to perform textract:AnalyzeExpense` });
|
||||
}
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error); // no XML parsing needed
|
||||
}
|
||||
}
|
||||
|
||||
export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const response = await awsApiRequest.call(this, service, method, path, body, headers);
|
||||
try {
|
||||
return JSON.parse(response);
|
||||
} catch (error) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string, headers?: object): Promise<any> { // tslint:disable-line:no-any
|
||||
const response = await awsApiRequest.call(this, service, method, path, body, headers);
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
parseString(response, { explicitArray: false }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export function simplify(data: IExpenseDocument) {
|
||||
const result: { [key: string]: string } = {};
|
||||
for (const document of data.ExpenseDocuments) {
|
||||
for (const field of document.SummaryFields) {
|
||||
result[field?.Type?.Text || field?.LabelDetection?.Text] = field.ValueDetection.Text;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface IExpenseDocument {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
LabelDetection: { Text: string },
|
||||
ValueDetection: { Text: string },
|
||||
Type: { Text: string }
|
||||
}]
|
||||
}];
|
||||
}
|
||||
|
||||
export async function validateCrendetials(this: ICredentialTestFunctions, decryptedCredentials: ICredentialDataDecryptedObject, service: string): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = decryptedCredentials;
|
||||
|
||||
// Concatenate path and instantiate URL object so it parses correctly query strings
|
||||
const endpoint = new URL(getEndpointForService(service, credentials) + `?Action=GetCallerIdentity&Version=2011-06-15`);
|
||||
|
||||
// Sign AWS API request with the user credentials
|
||||
const signOpts = { host: endpoint.host, method: 'POST', path: '?Action=GetCallerIdentity&Version=2011-06-15' } as Request;
|
||||
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: signOpts.headers,
|
||||
method: 'POST',
|
||||
uri: endpoint.href,
|
||||
body: signOpts.body,
|
||||
};
|
||||
|
||||
const response = await this.helpers.request!(options);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
parseString(response, { explicitArray: false }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
18
packages/nodes-base/nodes/Aws/Textract/textract.svg
Normal file
18
packages/nodes-base/nodes/Aws/Textract/textract.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
|
||||
<title>Icon-Architecture/64/Arch_AWS-Textract_64</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#055F4E" offset="0%"></stop>
|
||||
<stop stop-color="#56C0A7" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Icon-Architecture/64/Arch_AWS-Textract_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Icon-Architecture-BG/64/Machine-Learning" fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<path d="M22.0624102,50 C24.3763895,53.603 28.4103535,56 33.0003125,56 C40.1672485,56 45.9991964,50.168 45.9991964,43 C45.9991964,35.832 40.1672485,30 33.0003125,30 C27.6033607,30 22.9664021,33.307 21.0024196,38 L23.2143999,38 C25.0393836,34.444 28.7363506,32 33.0003125,32 C39.0652583,32 43.9992143,36.935 43.9992143,43 C43.9992143,49.065 39.0652583,54 33.0003125,54 C29.5913429,54 26.5413702,52.441 24.5213882,50 L22.0624102,50 Z M37.0002768,45 L37.0002768,43 L41.9992321,43 C41.9992321,38.038 37.9622682,34 33.0003125,34 C28.0373568,34 23.9993929,38.038 23.9993929,43 L28.9993482,43 L28.9993482,45 L24.2313908,45 C25.1443826,49.002 28.7253507,52 33.0003125,52 C35.1362934,52 37.0992759,51.249 38.6442621,50 L34.0003036,50 L34.0003036,48 L40.4782457,48 C41.0812403,47.102 41.5202364,46.087 41.7682342,45 L37.0002768,45 Z M21.0024196,48 L23.2143999,48 C22.4434068,46.498 22.0004107,44.801 22.0004107,43 C22.0004107,41.959 22.1554093,40.955 22.4264069,40 L20.3634253,40 C20.1344274,40.965 19.9994286,41.966 19.9994286,43 C19.9994286,44.771 20.3584254,46.46 21.0024196,48 L21.0024196,48 Z M19.7434309,50 L17.0004554,50 L17.0004554,48 L18.8744386,48 C18.5344417,47.04 18.2894438,46.038 18.1494451,45 L15.4144695,45 L16.707458,46.293 L15.2924706,47.707 L12.2924974,44.707 C11.9025009,44.316 11.9025009,43.684 12.2924974,43.293 L15.2924706,40.293 L16.707458,41.707 L15.4144695,43 L18.0004464,43 C18.0004464,41.973 18.1044455,40.97 18.3024437,40 L17.0004554,40 L17.0004554,38 L18.8744386,38 C20.9404202,32.184 26.4833707,28 33.0003125,28 C37.427273,28 41.4002375,29.939 44.148213,33 L59.0000804,33 L59.0000804,35 L45.6661994,35 C47.1351863,37.318 47.9991786,40.058 47.9991786,43 L59.0000804,43 L59.0000804,45 L47.8501799,45 C46.8681887,52.327 40.5912447,58 33.0003125,58 C27.2563638,58 22.2624084,54.752 19.7434309,50 L19.7434309,50 Z M37.0002768,39 C37.0002768,38.448 36.5522808,38 36.0002857,38 L29.9993482,38 C29.4473442,38 28.9993482,38.448 28.9993482,39 L28.9993482,41 L31.0003304,41 L31.0003304,40 L32.0003214,40 L32.0003214,43 L31.0003304,43 L31.0003304,45 L35.0002946,45 L35.0002946,43 L34.0003036,43 L34.0003036,40 L35.0002946,40 L35.0002946,41 L37.0002768,41 L37.0002768,39 Z M49.0001696,40 L59.0000804,40 L59.0000804,38 L49.0001696,38 L49.0001696,40 Z M49.0001696,50 L59.0000804,50 L59.0000804,48 L49.0001696,48 L49.0001696,50 Z M57.0000982,27 L60.5850662,27 L57.0000982,23.414 L57.0000982,27 Z M63.7070383,27.293 C63.8940367,27.48 64.0000357,27.735 64.0000357,28 L64.0000357,63 C64.0000357,63.552 63.5520397,64 63.0000446,64 L32.0003304,64 C31.4473264,64 31.0003304,63.552 31.0003304,63 L31.0003304,59 L33.0003125,59 L33.0003125,62 L62.0000536,62 L62.0000536,29 L56.0001071,29 C55.4471121,29 55.0001161,28.552 55.0001161,28 L55.0001161,22 L33.0003125,22 L33.0003125,27 L31.0003304,27 L31.0003304,21 C31.0003304,20.448 31.4473264,20 32.0003304,20 L56.0001071,20 C56.2651048,20 56.5191025,20.105 56.7071008,20.293 L63.7070383,27.293 Z M68,24.166 L68,61 C68,61.552 67.552004,62 67.0000089,62 L65.0000268,62 L65.0000268,60 L66.0000179,60 L66.0000179,24.612 L58.6170838,18 L36.0002857,18 L36.0002857,19 L34.0003036,19 L34.0003036,17 C34.0003036,16.448 34.4472996,16 35.0003036,16 L59.0000804,16 C59.2460782,16 59.483076,16.091 59.6660744,16.255 L67.666003,23.42 C67.8780011,23.61 68,23.881 68,24.166 L68,24.166 Z" id="Amazon-Textract_Icon_64_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -330,6 +330,7 @@
|
|||
"dist/nodes/Aws/S3/AwsS3.node.js",
|
||||
"dist/nodes/Aws/SES/AwsSes.node.js",
|
||||
"dist/nodes/Aws/SQS/AwsSqs.node.js",
|
||||
"dist/nodes/Aws/Textract/AwsTextract.node.js",
|
||||
"dist/nodes/Aws/Transcribe/AwsTranscribe.node.js",
|
||||
"dist/nodes/Aws/AwsSns.node.js",
|
||||
"dist/nodes/Aws/AwsSnsTrigger.node.js",
|
||||
|
|
Loading…
Reference in a new issue