first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.awsTextract",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Utility"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/aws/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awstextract/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "7 no-code workflow automations for Amazon Web Services",
|
||||
"url": "https://n8n.io/blog/aws-workflow-automation/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
NodeConnectionTypes,
|
||||
type ICredentialDataDecryptedObject,
|
||||
type ICredentialsDecrypted,
|
||||
type ICredentialTestFunctions,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
type INodeCredentialTestResult,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { IExpenseDocument } from './GenericFunctions';
|
||||
import { awsApiRequestREST, simplify, validateCredentials } from './GenericFunctions';
|
||||
import { awsNodeAuthOptions, awsNodeCredentials } from '../utils';
|
||||
|
||||
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',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: awsNodeCredentials,
|
||||
properties: [
|
||||
awsNodeAuthOptions,
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Analyze Receipt or Invoice',
|
||||
value: 'analyzeExpense',
|
||||
},
|
||||
],
|
||||
default: 'analyzeExpense',
|
||||
},
|
||||
{
|
||||
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',
|
||||
name: 'simple',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['analyzeExpense'],
|
||||
},
|
||||
},
|
||||
default: true,
|
||||
description:
|
||||
'Whether to return a simplified version of the response instead of the raw data',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
credentialTest: {
|
||||
async awsTextractApiCredentialTest(
|
||||
this: ICredentialTestFunctions,
|
||||
credential: ICredentialsDecrypted,
|
||||
): Promise<INodeCredentialTestResult> {
|
||||
try {
|
||||
await validateCredentials.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);
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
try {
|
||||
//https://docs.aws.amazon.com/textract/latest/dg/API_AnalyzeExpense.html
|
||||
if (operation === 'analyzeExpense') {
|
||||
const simple = this.getNodeParameter('simple', i) as boolean;
|
||||
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
|
||||
const binaryBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
|
||||
// Convert the binary buffer to a base64 string
|
||||
const binaryData = Buffer.from(binaryBuffer).toString(BINARY_ENCODING);
|
||||
|
||||
const body: IDataObject = {
|
||||
Document: {
|
||||
Bytes: binaryData,
|
||||
},
|
||||
};
|
||||
|
||||
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)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import type { Request } from 'aws4';
|
||||
import { sign } from 'aws4';
|
||||
import type {
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestFunctions,
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IWebhookFunctions,
|
||||
IHttpRequestOptions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
import { URL } from 'url';
|
||||
import { parseString } from 'xml2js';
|
||||
import { getAwsCredentials } from '../GenericFunctions';
|
||||
|
||||
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: IHttpRequestMethods,
|
||||
path: string,
|
||||
body?: string,
|
||||
headers?: object,
|
||||
): Promise<any> {
|
||||
const { credentials, credentialsType } = await getAwsCredentials(this);
|
||||
|
||||
const requestOptions = {
|
||||
qs: {
|
||||
service,
|
||||
path,
|
||||
},
|
||||
method,
|
||||
body,
|
||||
url: '',
|
||||
headers,
|
||||
region: credentials?.region as string,
|
||||
} as IHttpRequestOptions;
|
||||
|
||||
try {
|
||||
return await this.helpers.requestWithAuthentication.call(this, credentialsType, requestOptions);
|
||||
} 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 as string).Message.split(' ')[1];
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, {
|
||||
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 as JsonObject); // no XML parsing needed
|
||||
}
|
||||
}
|
||||
|
||||
export async function awsApiRequestREST(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
||||
service: string,
|
||||
method: IHttpRequestMethods,
|
||||
path: string,
|
||||
body?: string,
|
||||
headers?: object,
|
||||
): Promise<any> {
|
||||
const response = await awsApiRequest.call(this, service, method, path, body, headers);
|
||||
try {
|
||||
return JSON.parse(response as string);
|
||||
} catch (error) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
export async function awsApiRequestSOAP(
|
||||
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions,
|
||||
service: string,
|
||||
method: IHttpRequestMethods,
|
||||
path: string,
|
||||
body?: string,
|
||||
headers?: object,
|
||||
): Promise<any> {
|
||||
const response = await awsApiRequest.call(this, service, method, path, body, headers);
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
parseString(response as string, { 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 validateCredentials(
|
||||
this: ICredentialTestFunctions,
|
||||
decryptedCredentials: ICredentialDataDecryptedObject,
|
||||
service: string,
|
||||
): Promise<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;
|
||||
const securityHeaders = {
|
||||
accessKeyId: `${credentials.accessKeyId}`.trim(),
|
||||
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
|
||||
sessionToken: credentials.temporaryCredentials
|
||||
? `${credentials.sessionToken}`.trim()
|
||||
: undefined,
|
||||
};
|
||||
|
||||
sign(signOpts, securityHeaders);
|
||||
|
||||
const options: IRequestOptions = {
|
||||
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 as string, { explicitArray: false }, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
import nock from 'nock';
|
||||
|
||||
import { AwsTextract } from '../AwsTextract.node';
|
||||
import * as GenericFunctions from '../GenericFunctions';
|
||||
|
||||
const mockTextractResponse = {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
Type: {
|
||||
Text: 'VENDOR_NAME',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: 'Test Company',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockSimplifiedResponse = {
|
||||
VENDOR_NAME: 'Test Company',
|
||||
};
|
||||
|
||||
describe('AWS Textract Node', () => {
|
||||
const executeFunctionsMock = mockDeep<IExecuteFunctions>();
|
||||
const awsApiRequestSpy = jest.spyOn(GenericFunctions, 'awsApiRequestREST');
|
||||
const simplifySpy = jest.spyOn(GenericFunctions, 'simplify');
|
||||
const node = new AwsTextract();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
executeFunctionsMock.getCredentials.mockResolvedValue({
|
||||
accessKeyId: 'test-key',
|
||||
secretAccessKey: 'test-secret',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
executeFunctionsMock.getNode.mockReturnValue({
|
||||
typeVersion: 1,
|
||||
} as INode);
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }]);
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(false);
|
||||
executeFunctionsMock.helpers.returnJsonArray.mockImplementation((data) =>
|
||||
Array.isArray(data) ? data.map((item: any) => ({ json: item })) : ([{ json: data }] as any),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
describe('analyzeExpense operation', () => {
|
||||
beforeEach(() => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'analyzeExpense';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
case 'simple':
|
||||
return true;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should process binary image data and return simplified response', async () => {
|
||||
const testImageBuffer = Buffer.from('test-image-data');
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testImageBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'data');
|
||||
expect(awsApiRequestSpy).toHaveBeenCalledWith(
|
||||
'textract',
|
||||
'POST',
|
||||
'',
|
||||
JSON.stringify({
|
||||
Document: {
|
||||
Bytes: testImageBuffer.toString('base64'),
|
||||
},
|
||||
}),
|
||||
{
|
||||
'x-amz-target': 'Textract.AnalyzeExpense',
|
||||
'Content-Type': 'application/x-amz-json-1.1',
|
||||
},
|
||||
);
|
||||
expect(simplifySpy).toHaveBeenCalledWith(mockTextractResponse);
|
||||
expect(result).toEqual([[{ json: mockSimplifiedResponse }]]);
|
||||
});
|
||||
|
||||
it('should return raw response when simple is false', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'analyzeExpense';
|
||||
case 'binaryPropertyName':
|
||||
return 'data';
|
||||
case 'simple':
|
||||
return false;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const testImageBuffer = Buffer.from('test-image-data');
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testImageBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(simplifySpy).not.toHaveBeenCalled();
|
||||
expect(result).toEqual([[{ json: mockTextractResponse }]]);
|
||||
});
|
||||
|
||||
it('should handle different binary property names', async () => {
|
||||
executeFunctionsMock.getNodeParameter.mockImplementation((paramName) => {
|
||||
switch (paramName) {
|
||||
case 'operation':
|
||||
return 'analyzeExpense';
|
||||
case 'binaryPropertyName':
|
||||
return 'document';
|
||||
case 'simple':
|
||||
return true;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const testImageBuffer = Buffer.from('test-document-data');
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testImageBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenCalledWith(0, 'document');
|
||||
expect(result).toEqual([[{ json: mockSimplifiedResponse }]]);
|
||||
});
|
||||
|
||||
it('should handle JPEG images', async () => {
|
||||
const testJpegBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0]); // JPEG header bytes
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testJpegBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(awsApiRequestSpy).toHaveBeenCalledWith(
|
||||
'textract',
|
||||
'POST',
|
||||
'',
|
||||
JSON.stringify({
|
||||
Document: {
|
||||
Bytes: testJpegBuffer.toString('base64'),
|
||||
},
|
||||
}),
|
||||
{
|
||||
'x-amz-target': 'Textract.AnalyzeExpense',
|
||||
'Content-Type': 'application/x-amz-json-1.1',
|
||||
},
|
||||
);
|
||||
expect(result).toEqual([[{ json: mockSimplifiedResponse }]]);
|
||||
});
|
||||
|
||||
it('should handle PNG images', async () => {
|
||||
const testPngBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG header bytes
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testPngBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toEqual([[{ json: mockSimplifiedResponse }]]);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
executeFunctionsMock.getInputData.mockReturnValue([{ json: {} }, { json: {} }]);
|
||||
|
||||
const testImageBuffer = Buffer.from('test-image-data');
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(testImageBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(executeFunctionsMock.helpers.getBinaryDataBuffer).toHaveBeenCalledTimes(2);
|
||||
expect(awsApiRequestSpy).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual([
|
||||
[{ json: mockSimplifiedResponse }, { json: mockSimplifiedResponse }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle errors and continue on fail', async () => {
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(true);
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockRejectedValue(
|
||||
new Error('Binary data not found'),
|
||||
);
|
||||
|
||||
const result = await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(result).toEqual([[{ json: { error: 'Binary data not found' } }]]);
|
||||
});
|
||||
|
||||
it('should throw error when continueOnFail is false', async () => {
|
||||
executeFunctionsMock.continueOnFail.mockReturnValue(false);
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockRejectedValue(
|
||||
new Error('Binary data not found'),
|
||||
);
|
||||
|
||||
await expect(node.execute.call(executeFunctionsMock)).rejects.toThrow(
|
||||
'Binary data not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty binary data', async () => {
|
||||
const emptyBuffer = Buffer.from('');
|
||||
|
||||
executeFunctionsMock.helpers.getBinaryDataBuffer.mockResolvedValue(emptyBuffer);
|
||||
awsApiRequestSpy.mockResolvedValue(mockTextractResponse);
|
||||
simplifySpy.mockReturnValue(mockSimplifiedResponse);
|
||||
|
||||
await node.execute.call(executeFunctionsMock);
|
||||
|
||||
expect(awsApiRequestSpy).toHaveBeenCalledWith(
|
||||
'textract',
|
||||
'POST',
|
||||
'',
|
||||
JSON.stringify({
|
||||
Document: {
|
||||
Bytes: '',
|
||||
},
|
||||
}),
|
||||
{
|
||||
'x-amz-target': 'Textract.AnalyzeExpense',
|
||||
'Content-Type': 'application/x-amz-json-1.1',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { simplify, type IExpenseDocument } from '../GenericFunctions';
|
||||
|
||||
describe('AWS Textract Generic Functions', () => {
|
||||
describe('simplify function', () => {
|
||||
it('should simplify expense document response correctly', () => {
|
||||
const input = {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
Type: {
|
||||
Text: 'VENDOR_NAME',
|
||||
},
|
||||
LabelDetection: {
|
||||
Text: 'Vendor',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: 'Acme Corporation',
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: {
|
||||
Text: 'INVOICE_RECEIPT_DATE',
|
||||
},
|
||||
LabelDetection: {
|
||||
Text: 'Date',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: '2023-12-01',
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: {
|
||||
Text: 'TOTAL',
|
||||
},
|
||||
LabelDetection: {
|
||||
Text: 'Total',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: '$125.50',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as IExpenseDocument;
|
||||
|
||||
const result = simplify(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
VENDOR_NAME: 'Acme Corporation',
|
||||
INVOICE_RECEIPT_DATE: '2023-12-01',
|
||||
TOTAL: '$125.50',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle fields without Type but with LabelDetection', () => {
|
||||
const input = {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
Type: undefined as any,
|
||||
LabelDetection: {
|
||||
Text: 'Custom Field',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: 'Custom Value',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as IExpenseDocument;
|
||||
|
||||
const result = simplify(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
'Custom Field': 'Custom Value',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty expense documents', () => {
|
||||
const input = {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = simplify(input);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle multiple expense documents', () => {
|
||||
const input = {
|
||||
ExpenseDocuments: [
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
Type: {
|
||||
Text: 'VENDOR_NAME',
|
||||
},
|
||||
LabelDetection: {
|
||||
Text: 'Vendor',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: 'First Company',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
SummaryFields: [
|
||||
{
|
||||
Type: {
|
||||
Text: 'TOTAL',
|
||||
},
|
||||
LabelDetection: {
|
||||
Text: 'Total',
|
||||
},
|
||||
ValueDetection: {
|
||||
Text: '$50.00',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as any;
|
||||
|
||||
const result = simplify(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
VENDOR_NAME: 'First Company',
|
||||
TOTAL: '$50.00',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80"><defs><linearGradient id="a" x1="0%" x2="100%" y1="100%" y2="0%"><stop offset="0%" stop-color="#055F4E"/><stop offset="100%" stop-color="#56C0A7"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" d="M0 0h80v80H0z"/><path fill="#FFF" d="M22.062 50c2.314 3.603 6.348 6 10.938 6 7.167 0 13-5.832 13-13s-5.833-13-13-13c-5.397 0-10.034 3.307-11.998 8h2.212c1.825-3.556 5.522-6 9.786-6 6.065 0 11 4.935 11 11s-4.935 11-11 11a10.99 10.99 0 0 1-8.479-4zM37 45v-2h5c0-4.962-4.038-9-9-9-4.963 0-9 4.038-9 9h5v2h-4.77c.913 4.002 4.494 7 8.77 7a8.95 8.95 0 0 0 5.643-2H34v-2h6.478a9 9 0 0 0 1.29-3zm-15.998 3h2.212A10.9 10.9 0 0 1 22 43c0-1.041.155-2.045.426-3h-2.063A13 13 0 0 0 20 43c0 1.771.36 3.46 1.003 5m-1.259 2H17v-2h1.874c-.34-.96-.585-1.962-.725-3h-2.735l1.293 1.293-1.415 1.414-3-3a1 1 0 0 1 0-1.414l3-3 1.415 1.414L15.414 43H18c0-1.027.104-2.03.302-3H17v-2h1.874C20.94 32.184 26.484 28 33 28c4.427 0 8.4 1.939 11.148 5H59v2H45.666A14.9 14.9 0 0 1 48 43h11v2H47.85c-.982 7.327-7.259 13-14.85 13-5.744 0-10.738-3.248-13.257-8M37 39a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v2h2v-1h1v3h-1v2h4v-2h-1v-3h1v1h2zm12 1h10v-2H49zm0 10h10v-2H49zm8-23h3.585L57 23.414zm6.707.293c.187.187.293.442.293.707v35a1 1 0 0 1-1 1H32a1 1 0 0 1-1-1v-4h2v3h29V29h-6a1 1 0 0 1-1-1v-6H33v5h-2v-6a1 1 0 0 1 1-1h24a1 1 0 0 1 .707.293zM68 24.166V61a1 1 0 0 1-1 1h-2v-2h1V24.612L58.617 18H36v1h-2v-2a1 1 0 0 1 1-1h24c.246 0 .483.091.666.255l8 7.165c.212.19.334.461.334.746"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
Reference in New Issue
Block a user