mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
feat(Form Node): Support multiple files when returning binary from form ending (#33780)
This commit is contained in:
@@ -201,15 +201,15 @@
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const binary = "{{{responseBinary}}}"
|
||||
const binaries = "{{{responseBinary}}}"
|
||||
? JSON.parse(decodeURIComponent("{{{responseBinary}}}"))
|
||||
: '';
|
||||
: [];
|
||||
|
||||
const byteArray = binary.data.type === 'Buffer'
|
||||
? new Uint8Array(binary.data.data)
|
||||
: Uint8Array.from(binary.data, c => c.charCodeAt(0));
|
||||
for (const binary of binaries) {
|
||||
const byteArray = binary.data.type === 'Buffer'
|
||||
? new Uint8Array(binary.data.data)
|
||||
: Uint8Array.from(binary.data, c => c.charCodeAt(0));
|
||||
|
||||
if (binary) {
|
||||
const blob = new Blob(
|
||||
[byteArray],
|
||||
{ type: binary.type }
|
||||
|
||||
@@ -230,7 +230,7 @@ const completionProperties = updateDisplayOptions(
|
||||
description: 'The text to display on the page. Use HTML to show a customized web page.',
|
||||
},
|
||||
{
|
||||
displayName: 'Input Data Field Name',
|
||||
displayName: 'Input Data Field Name(s)',
|
||||
name: 'inputDataFieldName',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
@@ -241,8 +241,8 @@ const completionProperties = updateDisplayOptions(
|
||||
default: 'data',
|
||||
placeholder: 'e.g. data',
|
||||
description:
|
||||
'Find the name of input field containing the binary data to return in the Input panel on the left, in the Binary tab',
|
||||
hint: 'The name of the input field containing the binary file data to be returned',
|
||||
'Find the name of input field containing the binary data to return in the Input panel on the left, in the Binary tab. You can provide multiple comma-separated field names.',
|
||||
hint: 'The name of the input field containing the binary file data to be returned. You can provide multiple comma-separated field names.',
|
||||
},
|
||||
...waitTimeProperties,
|
||||
{
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('Form Node', () => {
|
||||
message: 'Test Message',
|
||||
redirectUrl: undefined,
|
||||
title: 'Test Title',
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
responseText: '',
|
||||
dangerousCustomCss: undefined,
|
||||
},
|
||||
@@ -255,7 +255,7 @@ describe('Form Node', () => {
|
||||
redirectUrl: undefined,
|
||||
title: 'Test Title',
|
||||
responseText: '<div>hey</div><script>alert("hi")</script>',
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
dangerousCustomCss: undefined,
|
||||
},
|
||||
},
|
||||
@@ -268,7 +268,7 @@ describe('Form Node', () => {
|
||||
formTitle: 'test',
|
||||
message: 'Test Message',
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
title: 'Test Title',
|
||||
responseText: 'my text over here',
|
||||
dangerousCustomCss: undefined,
|
||||
@@ -732,7 +732,7 @@ describe('Form Node', () => {
|
||||
redirectUrl: 'https://n8n.io',
|
||||
responseText: '',
|
||||
title: 'Test Title',
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('formCompletionUtils', () => {
|
||||
formTitle: 'Form Title',
|
||||
message: 'Form has been submitted successfully',
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
responseText: '',
|
||||
title: 'Form Completion',
|
||||
});
|
||||
@@ -220,7 +220,7 @@ describe('formCompletionUtils', () => {
|
||||
formTitle: 'Form Title',
|
||||
message: 'Safe message<b>bold</b>',
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
responseText: 'Response text',
|
||||
title: 'Form Completion',
|
||||
dangerousCustomCss: undefined,
|
||||
@@ -252,7 +252,7 @@ describe('formCompletionUtils', () => {
|
||||
formTitle: 'Form Title',
|
||||
message: `Some message${replacement}Other text`,
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(JSON.stringify('')),
|
||||
responseBinary: encodeURIComponent(JSON.stringify([])),
|
||||
responseText: 'Response text',
|
||||
title: 'Form Completion',
|
||||
dangerousCustomCss: undefined,
|
||||
@@ -330,11 +330,13 @@ describe('formCompletionUtils', () => {
|
||||
message: 'Form has been submitted successfully',
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
data: buffer,
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
}),
|
||||
JSON.stringify([
|
||||
{
|
||||
data: buffer,
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
},
|
||||
]),
|
||||
),
|
||||
responseText: '',
|
||||
title: 'Form Completion',
|
||||
@@ -384,11 +386,13 @@ describe('formCompletionUtils', () => {
|
||||
message: 'Form has been submitted successfully',
|
||||
redirectUrl: undefined,
|
||||
responseBinary: encodeURIComponent(
|
||||
JSON.stringify({
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
}),
|
||||
JSON.stringify([
|
||||
{
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
},
|
||||
]),
|
||||
),
|
||||
responseText: '',
|
||||
title: 'Form Completion',
|
||||
@@ -524,11 +528,103 @@ describe('formCompletionUtils', () => {
|
||||
|
||||
const result = await binaryResponse(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual({
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
expect(result).toEqual([
|
||||
{
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return multiple binary files from comma-separated field names', async () => {
|
||||
const expectedBinaryResponse = {
|
||||
inputData: {
|
||||
data: 'Zmlyc3Q=',
|
||||
fileName: 'first.txt',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
otherData: {
|
||||
data: 'c2Vjb25k',
|
||||
fileName: 'second.txt',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
};
|
||||
|
||||
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
inputDataFieldName: 'inputData, otherData',
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodesWithSingleNodeFile);
|
||||
mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
|
||||
if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
|
||||
return expectedBinaryResponse;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const result = await binaryResponse(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
},
|
||||
{
|
||||
data: atob(expectedBinaryResponse.otherData.data),
|
||||
fileName: expectedBinaryResponse.otherData.fileName,
|
||||
type: expectedBinaryResponse.otherData.mimeType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should trim comma-separated field names', async () => {
|
||||
const expectedBinaryResponse = {
|
||||
inputData: {
|
||||
data: 'Zmlyc3Q=',
|
||||
fileName: 'first.txt',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
otherData: {
|
||||
data: 'c2Vjb25k',
|
||||
fileName: 'second.txt',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
};
|
||||
|
||||
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
|
||||
const params: Record<string, string> = {
|
||||
inputDataFieldName: ' inputData , otherData ',
|
||||
};
|
||||
return params[parameterName];
|
||||
});
|
||||
mockWebhookFunctions.getParentNodes.mockReturnValueOnce(parentNodesWithSingleNodeFile);
|
||||
mockWebhookFunctions.evaluateExpression.mockImplementation((arg) => {
|
||||
if (arg === `{{ $('${nodeNameWithFileToDownload}').first().binary }}`) {
|
||||
return expectedBinaryResponse;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const result = await binaryResponse(mockWebhookFunctions);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
data: atob(expectedBinaryResponse.inputData.data),
|
||||
fileName: expectedBinaryResponse.inputData.fileName,
|
||||
type: expectedBinaryResponse.inputData.mimeType,
|
||||
},
|
||||
{
|
||||
data: atob(expectedBinaryResponse.otherData.data),
|
||||
fileName: expectedBinaryResponse.otherData.fileName,
|
||||
type: expectedBinaryResponse.otherData.mimeType,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,34 +19,55 @@ import {
|
||||
validateSafeRedirectUrl,
|
||||
} from './utils';
|
||||
|
||||
const getBinaryDataFromNode = (context: IWebhookFunctions, nodeName: string): IDataObject => {
|
||||
return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as IDataObject;
|
||||
type BinaryResponse = { data: string | Buffer; fileName: string; type: string };
|
||||
|
||||
const getBinaryDataFromNode = (
|
||||
context: IWebhookFunctions,
|
||||
nodeName: string,
|
||||
): IDataObject | undefined => {
|
||||
return context.evaluateExpression(`{{ $('${nodeName}').first().binary }}`) as
|
||||
| IDataObject
|
||||
| undefined;
|
||||
};
|
||||
|
||||
export const binaryResponse = async (
|
||||
context: IWebhookFunctions,
|
||||
): Promise<{ data: string | Buffer; fileName: string; type: string }> => {
|
||||
const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
|
||||
const parentNodes = context.getParentNodes(context.getNode().name);
|
||||
const binaryNode = parentNodes
|
||||
.reverse()
|
||||
.find((node) => getBinaryDataFromNode(context, node?.name)?.hasOwnProperty(inputDataFieldName));
|
||||
if (!binaryNode) {
|
||||
throw new OperationalError(`No binary data with field ${inputDataFieldName} found.`);
|
||||
}
|
||||
const binaryData = getBinaryDataFromNode(context, binaryNode?.name)[
|
||||
inputDataFieldName
|
||||
] as IBinaryData;
|
||||
const getInputDataFieldNames = (inputDataFieldName: string) => {
|
||||
const fieldNames = inputDataFieldName
|
||||
.split(',')
|
||||
.map((fieldName) => fieldName.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
// If a binaryData has an id, the following field is set:
|
||||
// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
|
||||
data: binaryData.id
|
||||
? await context.helpers.binaryToBuffer(await context.helpers.getBinaryStream(binaryData.id))
|
||||
: atob(binaryData.data),
|
||||
fileName: binaryData.fileName ?? 'file',
|
||||
type: binaryData.mimeType,
|
||||
};
|
||||
return fieldNames.length ? fieldNames : [inputDataFieldName];
|
||||
};
|
||||
|
||||
export const binaryResponse = async (context: IWebhookFunctions): Promise<BinaryResponse[]> => {
|
||||
const inputDataFieldName = context.getNodeParameter('inputDataFieldName', '') as string;
|
||||
const inputDataFieldNames = getInputDataFieldNames(inputDataFieldName);
|
||||
const responses: BinaryResponse[] = [];
|
||||
const parentNodesBinaries = context
|
||||
.getParentNodes(context.getNode().name)
|
||||
.reverse()
|
||||
.map((node) => getBinaryDataFromNode(context, node.name) ?? {});
|
||||
|
||||
for (const fieldName of inputDataFieldNames) {
|
||||
const nodeBinary = parentNodesBinaries.find((bin) => Object.hasOwn(bin, fieldName));
|
||||
if (!nodeBinary) {
|
||||
throw new OperationalError(`No binary data with field ${fieldName} found.`);
|
||||
}
|
||||
|
||||
const binaryData = nodeBinary[fieldName] as IBinaryData;
|
||||
|
||||
responses.push({
|
||||
// If a binaryData has an id, the following field is set:
|
||||
// N8N_DEFAULT_BINARY_DATA_MODE=filesystem
|
||||
data: binaryData.id
|
||||
? await context.helpers.binaryToBuffer(await context.helpers.getBinaryStream(binaryData.id))
|
||||
: atob(binaryData.data),
|
||||
fileName: binaryData.fileName ?? 'file',
|
||||
type: binaryData.mimeType,
|
||||
});
|
||||
}
|
||||
|
||||
return responses;
|
||||
};
|
||||
|
||||
export const renderFormCompletion = async (
|
||||
@@ -70,7 +91,7 @@ export const renderFormCompletion = async (
|
||||
| 'redirect'
|
||||
| 'showText'
|
||||
| 'returnBinary';
|
||||
const binary = respondWith === 'returnBinary' ? await binaryResponse(context) : '';
|
||||
const binary = respondWith === 'returnBinary' ? await binaryResponse(context) : [];
|
||||
|
||||
let title = options.formTitle;
|
||||
if (!title) {
|
||||
|
||||
Reference in New Issue
Block a user