From 5e16dd4ab4457acf21d3d7a3566d07944ff7f857 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:08:16 +0200 Subject: [PATCH] feat(core): Improvements/overhaul for nodes working with binary data (#7651) Github issue / Community forum post (link here to close automatically): --------- Co-authored-by: Giulio Andreini Co-authored-by: Marcus --- cypress/e2e/16-webhook-node.cy.ts | 8 +- cypress/e2e/4-node-creator.cy.ts | 7 +- packages/core/src/NodeExecuteFunctions.ts | 23 +- .../components/Node/NodeCreator/viewsData.ts | 22 +- packages/editor-ui/src/constants.ts | 4 + .../nodes/ApiTemplateIo/ApiTemplateIo.node.ts | 4 +- .../Aws/Rekognition/AwsRekognition.node.ts | 6 +- .../nodes/Aws/S3/V1/FileDescription.ts | 10 +- .../nodes/Aws/S3/V2/FileDescription.ts | 10 +- .../nodes-base/nodes/Box/FileDescription.ts | 10 +- .../Webex/descriptions/MessageDescription.ts | 2 +- .../nodes/Compression/Compression.node.json | 15 +- .../nodes/Compression/Compression.node.ts | 189 ++++- .../nodes/Cortex/AnalyzerDescriptions.ts | 4 +- .../nodes/Cortex/ResponderDescription.ts | 6 +- .../nodes-base/nodes/Crypto/Crypto.node.ts | 2 +- .../nodes-base/nodes/Dropbox/Dropbox.node.ts | 11 +- .../nodes/EditImage/EditImage.node.json | 3 +- .../nodes/Facebook/FacebookGraphApi.node.ts | 7 +- .../ConvertToFile/ConvertToFile.node.json | 35 + .../Files/ConvertToFile/ConvertToFile.node.ts | 121 +++ .../ConvertToFile/actions/iCall.operation.ts | 20 + .../actions/spreadsheet.operation.ts | 126 ++++ .../actions/toBinary.operation.ts | 147 ++++ .../ConvertToFile/actions/toJson.operation.ts | 165 ++++ .../Files/ConvertToFile/convertToFile.svg | 12 + .../ExtractFromFile/ExtractFromFile.node.json | 39 + .../ExtractFromFile/ExtractFromFile.node.ts | 134 ++++ .../actions/moveTo.operation.ts | 192 +++++ .../ExtractFromFile/actions/pdf.operation.ts | 141 ++++ .../actions/spreadsheet.operation.ts | 59 ++ .../Files/ExtractFromFile/extractFromFile.svg | 5 + .../ReadWriteFile/ReadWriteFile.node.json | 17 + .../Files/ReadWriteFile/ReadWriteFile.node.ts | 73 ++ .../ReadWriteFile/actions/read.operation.ts | 144 ++++ .../ReadWriteFile/actions/write.operation.ts | 123 +++ .../Files/ReadWriteFile/helpers/utils.ts | 32 + .../Files/ReadWriteFile/readWriteFile.svg | 13 + .../ReadWriteFile/test/ReadWriteFile.test.ts | 101 +++ .../test/ReadWriteFile.workflow.json | 72 ++ .../nodes/Files/ReadWriteFile/test/image.jpg | Bin 0 -> 1045 bytes .../nodes/Files/test/ConvertExtract.test.ts | 5 + .../Files/test/convert_extract.workflow.json | 707 ++++++++++++++++++ packages/nodes-base/nodes/Ftp/Ftp.node.json | 2 +- packages/nodes-base/nodes/Ftp/Ftp.node.ts | 19 +- .../nodes-base/nodes/Github/Github.node.ts | 11 +- .../nodes-base/nodes/Gitlab/Gitlab.node.ts | 11 +- .../Chat/descriptions/MediaDescription.ts | 4 +- .../Google/CloudStorage/ObjectDescription.ts | 8 +- .../Google/Drive/v1/GoogleDriveV1.node.ts | 11 +- .../v2/actions/file/download.operation.ts | 4 +- .../nodes/Google/Slides/GoogleSlides.node.ts | 4 +- .../Google/YouTube/ChannelDescription.ts | 3 +- .../nodes/Google/YouTube/VideoDescription.ts | 3 +- .../HomeAssistant/CameraProxyDescription.ts | 4 +- packages/nodes-base/nodes/Html/Html.node.ts | 5 +- .../nodes/HtmlExtract/HtmlExtract.node.ts | 5 +- .../HttpRequest/V1/HttpRequestV1.node.ts | 11 +- .../HttpRequest/V2/HttpRequestV2.node.ts | 11 +- .../HttpRequest/V3/HttpRequestV3.node.ts | 4 +- .../nodes/HumanticAI/ProfileDescription.ts | 8 +- .../nodes/ICalendar/ICalendar.node.ts | 333 +-------- .../nodes/ICalendar/createEvent.operation.ts | 376 ++++++++++ .../nodes/Jira/IssueAttachmentDescription.ts | 12 +- .../nodes-base/nodes/Keap/FileDescription.ts | 6 +- .../nodes/KoBoToolbox/FileDescription.ts | 2 +- .../nodes/Line/NotificationDescription.ts | 6 +- .../nodes/LinkedIn/PostDescription.ts | 8 +- .../nodes/Matrix/MediaDescription.ts | 3 +- .../Excel/v2/actions/versionDescription.ts | 2 +- .../Microsoft/OneDrive/FileDescription.ts | 10 +- .../v1/MessageAttachmentDescription.ts | 4 +- .../Outlook/v1/MessageDescription.ts | 4 +- .../nodes-base/nodes/Mindee/Mindee.node.ts | 5 +- .../MoveBinaryData/MoveBinaryData.node.ts | 29 +- packages/nodes-base/nodes/Nasa/Nasa.node.ts | 8 +- .../nodes/NextCloud/NextCloud.node.ts | 11 +- .../nodes/NocoDB/OperationDescription.ts | 2 +- .../nodes/OpenAi/ImageDescription.ts | 2 +- .../nodes/Pipedrive/Pipedrive.node.ts | 10 +- .../nodes/Pushbullet/Pushbullet.node.ts | 5 +- .../nodes/Pushover/Pushover.node.ts | 5 +- .../nodes/QuickBase/FileDescription.ts | 4 +- .../Estimate/EstimateDescription.ts | 4 +- .../Invoice/InvoiceDescription.ts | 4 +- .../Payment/PaymentDescription.ts | 4 +- .../ReadBinaryFiles/ReadBinaryFiles.node.ts | 1 + .../nodes-base/nodes/ReadPdf/ReadPDF.node.ts | 72 +- .../nodes/Salesforce/AttachmentDescription.ts | 9 +- .../nodes/Salesforce/DocumentDescription.ts | 4 +- .../descriptions/ReportDescription.ts | 4 +- packages/nodes-base/nodes/Set/Set.node.json | 2 +- .../nodes-base/nodes/Set/v2/SetV2.node.ts | 2 +- .../nodes/Slack/V1/FileDescription.ts | 6 +- .../nodes/Slack/V2/FileDescription.ts | 4 +- .../SpreadsheetFile/SpreadsheetFile.node.ts | 1 + .../nodes/SpreadsheetFile/description.ts | 475 +++++------- .../v1/SpreadsheetFileV1.node.ts | 14 +- .../v2/SpreadsheetFileV2.node.ts | 297 +------- .../SpreadsheetFile/v2/fromFile.operation.ts | 230 ++++++ .../SpreadsheetFile/v2/toFile.operation.ts | 44 ++ packages/nodes-base/nodes/Ssh/Ssh.node.ts | 7 +- .../nodes/Telegram/Telegram.node.ts | 5 +- .../TheHive/descriptions/AlertDescription.ts | 6 +- .../TheHive/descriptions/LogDescription.ts | 4 +- .../descriptions/ObservableDescription.ts | 4 +- .../actions/alert/create.operation.ts | 3 +- .../nodes-base/nodes/Vonage/Vonage.node.ts | 2 +- .../nodes-base/nodes/Webhook/description.ts | 7 +- .../Wise/descriptions/AccountDescription.ts | 4 +- .../Wise/descriptions/TransferDescription.ts | 4 +- .../WriteBinaryFile/WriteBinaryFile.node.ts | 1 + packages/nodes-base/nodes/Xml/Xml.node.json | 2 +- packages/nodes-base/nodes/Xml/Xml.node.ts | 12 + .../nodes/Zulip/MessageDescription.ts | 4 +- packages/nodes-base/package.json | 4 + packages/nodes-base/utils/binary.ts | 194 +++++ packages/nodes-base/utils/descriptions.ts | 413 +++++++++- pnpm-lock.yaml | 24 + 119 files changed, 4477 insertions(+), 1201 deletions(-) create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.json create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/actions/iCall.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/actions/spreadsheet.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/convertToFile.svg create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.json create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/actions/pdf.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/actions/spreadsheet.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ExtractFromFile/extractFromFile.svg create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.json create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/actions/write.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/readWriteFile.svg create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.test.ts create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json create mode 100644 packages/nodes-base/nodes/Files/ReadWriteFile/test/image.jpg create mode 100644 packages/nodes-base/nodes/Files/test/ConvertExtract.test.ts create mode 100644 packages/nodes-base/nodes/Files/test/convert_extract.workflow.json create mode 100644 packages/nodes-base/nodes/ICalendar/createEvent.operation.ts create mode 100644 packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts create mode 100644 packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts create mode 100644 packages/nodes-base/utils/binary.ts diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index c32a1407dc..da43a7cf4b 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -187,12 +187,14 @@ describe('Webhook Trigger node', async () => { ndv.getters.backToCanvas().click(); - workflowPage.actions.addNodeToCanvas('Convert to/from binary data'); + workflowPage.actions.addNodeToCanvas('Convert to File'); workflowPage.actions.zoomToFit(); - workflowPage.actions.openNode('Convert to/from binary data'); + workflowPage.actions.openNode('Convert to File'); + cy.getByTestId('parameter-input-operation').click(); + getVisibleSelect().find('.option-headline').contains('Convert to JSON').click(); cy.getByTestId('parameter-input-mode').click(); - getVisibleSelect().find('.option-headline').contains('JSON to Binary').click(); + getVisibleSelect().find('.option-headline').contains('Each Item to Separate File').click(); ndv.getters.backToCanvas().click(); workflowPage.actions.executeWorkflow(); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 49ff848cfe..42f08ba51b 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -110,7 +110,7 @@ describe('Node Creator', () => { it('should not show actions for single action nodes', () => { const singleActionNodes = [ 'DHL', - 'iCalendar', + 'Edit Fields', 'LingvaNex', 'Mailcheck', 'MSG91', @@ -484,8 +484,9 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet'); - nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Spreadsheet File'); - nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Google Sheets'); + nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Convert to File'); + nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Extract From File'); + nodeCreatorFeature.getters.nodeItemName().eq(2).should('have.text', 'Google Sheets'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sheets'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Google Sheets'); diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index df6f786582..2c014c6487 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -987,16 +987,27 @@ export function assertBinaryData( ): IBinaryData { const binaryKeyData = inputData.main[inputIndex]![itemIndex]!.binary; if (binaryKeyData === undefined) { - throw new NodeOperationError(node, 'No binary data exists on item!', { - itemIndex, - }); + throw new NodeOperationError( + node, + `This operation expects the node's input data to contain a binary file '${propertyName}', but none was found [item ${itemIndex}]`, + { + itemIndex, + description: 'Make sure that the previous node outputs a binary file', + }, + ); } const binaryPropertyData = binaryKeyData[propertyName]; if (binaryPropertyData === undefined) { - throw new NodeOperationError(node, `Item has no binary property called "${propertyName}"`, { - itemIndex, - }); + throw new NodeOperationError( + node, + `The item has no binary field '${propertyName}' [item ${itemIndex}]`, + { + itemIndex, + description: + 'Check that the parameter where you specified the input binary field name is correct, and that it matches a field in the binary input', + }, + ); } return binaryPropertyData; diff --git a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts index 91a8088807..109f21ad62 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts @@ -28,6 +28,8 @@ import { AI_CATEGORY_EMBEDDING, AI_OTHERS_NODE_CREATOR_VIEW, AI_UNCATEGORIZED_CATEGORY, + CONVERT_TO_FILE_NODE_TYPE, + EXTRACT_FROM_FILE_NODE_TYPE, SET_NODE_TYPE, CODE_NODE_TYPE, DATETIME_NODE_TYPE, @@ -48,6 +50,8 @@ import { HELPERS_SUBCATEGORY, RSS_READ_NODE_TYPE, EMAIL_SEND_NODE_TYPE, + EDIT_IMAGE_NODE_TYPE, + COMPRESSION_NODE_TYPE, } from '@/constants'; import { useI18n } from '@/composables/useI18n'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; @@ -394,7 +398,16 @@ export function RegularView(nodes: SimplifiedNodeType[]) { { key: 'convert', title: i18n.baseText('nodeCreator.sectionNames.transform.convert'), - items: [HTML_NODE_TYPE, MARKDOWN_NODE_TYPE, XML_NODE_TYPE, CRYPTO_NODE_TYPE], + items: [ + HTML_NODE_TYPE, + MARKDOWN_NODE_TYPE, + XML_NODE_TYPE, + CRYPTO_NODE_TYPE, + EXTRACT_FROM_FILE_NODE_TYPE, + CONVERT_TO_FILE_NODE_TYPE, + COMPRESSION_NODE_TYPE, + EDIT_IMAGE_NODE_TYPE, + ], }, ], }, @@ -422,6 +435,13 @@ export function RegularView(nodes: SimplifiedNodeType[]) { properties: { title: FILES_SUBCATEGORY, icon: 'file-alt', + sections: [ + { + key: 'popular', + title: i18n.baseText('nodeCreator.sectionNames.popular'), + items: [CONVERT_TO_FILE_NODE_TYPE, EXTRACT_FROM_FILE_NODE_TYPE], + }, + ], }, }, { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index d16c8ac34b..8e11557032 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -161,6 +161,8 @@ export const XERO_NODE_TYPE = 'n8n-nodes-base.xero'; export const ZENDESK_NODE_TYPE = 'n8n-nodes-base.zendesk'; export const ZENDESK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.zendeskTrigger'; export const DISCORD_NODE_TYPE = 'n8n-nodes-base.discord'; +export const EXTRACT_FROM_FILE_NODE_TYPE = 'n8n-nodes-base.extractFromFile'; +export const CONVERT_TO_FILE_NODE_TYPE = 'n8n-nodes-base.convertToFile'; export const DATETIME_NODE_TYPE = 'n8n-nodes-base.dateTime'; export const REMOVE_DUPLICATES_NODE_TYPE = 'n8n-nodes-base.removeDuplicates'; export const SPLIT_OUT_NODE_TYPE = 'n8n-nodes-base.splitOut'; @@ -172,6 +174,8 @@ export const MARKDOWN_NODE_TYPE = 'n8n-nodes-base.markdown'; export const XML_NODE_TYPE = 'n8n-nodes-base.xml'; export const CRYPTO_NODE_TYPE = 'n8n-nodes-base.crypto'; export const RSS_READ_NODE_TYPE = 'n8n-nodes-base.rssFeedRead'; +export const COMPRESSION_NODE_TYPE = 'n8n-nodes-base.compression'; +export const EDIT_IMAGE_NODE_TYPE = 'n8n-nodes-base.editImage'; export const CREDENTIAL_ONLY_NODE_PREFIX = 'n8n-creds-base'; export const CREDENTIAL_ONLY_HTTP_NODE_VERSION = 4.1; diff --git a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts index 0d3560fe1d..765a3fc76c 100644 --- a/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts +++ b/packages/nodes-base/nodes/ApiTemplateIo/ApiTemplateIo.node.ts @@ -181,12 +181,12 @@ export class ApiTemplateIo implements INodeType { description: 'Name of the binary property to which to write the data of the read file', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['pdf', 'image'], diff --git a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts index 99345befbf..405b26637d 100644 --- a/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts +++ b/packages/nodes-base/nodes/Aws/Rekognition/AwsRekognition.node.ts @@ -90,7 +90,7 @@ export class AwsRekognition implements INodeType { default: 'detectFaces', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -104,7 +104,7 @@ export class AwsRekognition implements INodeType { description: 'Whether the image to analyze should be taken from binary field', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', displayOptions: { show: { operation: ['analyze'], @@ -115,7 +115,7 @@ export class AwsRekognition implements INodeType { name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the input binary field containing the file to be written', required: true, }, { diff --git a/packages/nodes-base/nodes/Aws/S3/V1/FileDescription.ts b/packages/nodes-base/nodes/Aws/S3/V1/FileDescription.ts index be5146777e..6328aea0a0 100644 --- a/packages/nodes-base/nodes/Aws/S3/V1/FileDescription.ts +++ b/packages/nodes-base/nodes/Aws/S3/V1/FileDescription.ts @@ -373,7 +373,7 @@ export const fileFields: INodeProperties[] = [ description: 'If not set the binary data filename will be used', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: true, @@ -401,7 +401,7 @@ export const fileFields: INodeProperties[] = [ description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -414,7 +414,7 @@ export const fileFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Additional Fields', @@ -698,7 +698,7 @@ export const fileFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -709,7 +709,7 @@ export const fileFields: INodeProperties[] = [ resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, /* -------------------------------------------------------------------------- */ /* file:delete */ diff --git a/packages/nodes-base/nodes/Aws/S3/V2/FileDescription.ts b/packages/nodes-base/nodes/Aws/S3/V2/FileDescription.ts index be5146777e..6328aea0a0 100644 --- a/packages/nodes-base/nodes/Aws/S3/V2/FileDescription.ts +++ b/packages/nodes-base/nodes/Aws/S3/V2/FileDescription.ts @@ -373,7 +373,7 @@ export const fileFields: INodeProperties[] = [ description: 'If not set the binary data filename will be used', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: true, @@ -401,7 +401,7 @@ export const fileFields: INodeProperties[] = [ description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -414,7 +414,7 @@ export const fileFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Additional Fields', @@ -698,7 +698,7 @@ export const fileFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -709,7 +709,7 @@ export const fileFields: INodeProperties[] = [ resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, /* -------------------------------------------------------------------------- */ /* file:delete */ diff --git a/packages/nodes-base/nodes/Box/FileDescription.ts b/packages/nodes-base/nodes/Box/FileDescription.ts index 896100ffe9..48c4915740 100644 --- a/packages/nodes-base/nodes/Box/FileDescription.ts +++ b/packages/nodes-base/nodes/Box/FileDescription.ts @@ -161,7 +161,7 @@ export const fileFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -172,7 +172,7 @@ export const fileFields: INodeProperties[] = [ resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, /* -------------------------------------------------------------------------- */ @@ -671,7 +671,7 @@ export const fileFields: INodeProperties[] = [ description: 'The name the file should be saved as', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -700,7 +700,7 @@ export const fileFields: INodeProperties[] = [ description: 'The text content of the file', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -712,7 +712,7 @@ export const fileFields: INodeProperties[] = [ resource: ['file'], }, }, - description: 'Name of the binary property which contains the data for the file', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Parent ID', diff --git a/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts index c9f7fc3e92..6cf90d50a5 100644 --- a/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts +++ b/packages/nodes-base/nodes/Cisco/Webex/descriptions/MessageDescription.ts @@ -327,7 +327,7 @@ export const messageFields: INodeProperties[] = [ value: 'url', }, { - name: 'Binary Data', + name: 'Binary File', value: 'binaryData', }, ], diff --git a/packages/nodes-base/nodes/Compression/Compression.node.json b/packages/nodes-base/nodes/Compression/Compression.node.json index 5661d44760..c85e4aaf6b 100644 --- a/packages/nodes-base/nodes/Compression/Compression.node.json +++ b/packages/nodes-base/nodes/Compression/Compression.node.json @@ -11,8 +11,19 @@ } ] }, - "alias": ["Zip", "Gzip", "uncompress"], + "alias": [ + "Zip", + "Gzip", + "uncompress", + "compress", + "decompress", + "archive", + "unarchive", + "Binary", + "Files", + "File" + ], "subcategories": { - "Core Nodes": ["Files"] + "Core Nodes": ["Files", "Data Transformation"] } } diff --git a/packages/nodes-base/nodes/Compression/Compression.node.ts b/packages/nodes-base/nodes/Compression/Compression.node.ts index 022113a469..a26a99657b 100644 --- a/packages/nodes-base/nodes/Compression/Compression.node.ts +++ b/packages/nodes-base/nodes/Compression/Compression.node.ts @@ -50,7 +50,7 @@ export class Compression implements INodeType { icon: 'fa:file-archive', group: ['transform'], subtitle: '={{$parameter["operation"]}}', - version: 1, + version: [1, 1.1], description: 'Compress and decompress files', defaults: { name: 'Compression', @@ -68,28 +68,49 @@ export class Compression implements INodeType { { name: 'Compress', value: 'compress', + action: 'Compress file(s)', + description: 'Compress files into a zip or gzip archive', }, { name: 'Decompress', value: 'decompress', + action: 'Decompress file(s)', + description: 'Decompress zip or gzip archives', }, ], default: 'decompress', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field(s)', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, displayOptions: { show: { - operation: ['compress', 'decompress'], + operation: ['compress'], }, }, - placeholder: '', + placeholder: 'e.g. data,data2,data3', + hint: 'The name of the input binary field(s) containing the file(s) to be compressed', description: - 'Name of the binary property which contains the data for the file(s) to be compress/decompress. Multiple can be used separated by a comma (,).', + 'To process more than one file, use a comma-separated list of the binary fields names', + }, + { + displayName: 'Input Binary Field(s)', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + operation: ['decompress'], + }, + }, + placeholder: 'e.g. data', + hint: 'The name of the input binary field(s) containing the file(s) to decompress', + description: + 'To process more than one file, use a comma-separated list of the binary fields names', }, { displayName: 'Output Format', @@ -109,16 +130,42 @@ export class Compression implements INodeType { displayOptions: { show: { operation: ['compress'], + '@version': [1], }, }, - description: 'Format of the output file', + description: 'Format of the output', + }, + { + displayName: 'Output Format', + name: 'outputFormat', + type: 'options', + default: 'zip', + options: [ + { + name: 'Gzip', + value: 'gzip', + }, + { + name: 'Zip', + value: 'zip', + }, + ], + displayOptions: { + show: { + operation: ['compress'], + }, + hide: { + '@version': [1], + }, + }, + description: 'Format of the output', }, { displayName: 'File Name', name: 'fileName', type: 'string', default: '', - placeholder: 'data.zip', + placeholder: 'e.g. data.zip', required: true, displayOptions: { show: { @@ -126,10 +173,10 @@ export class Compression implements INodeType { outputFormat: ['zip'], }, }, - description: 'Name of the file to be compressed', + description: 'Name of the output file', }, { - displayName: 'Binary Property Output', + displayName: 'Put Output File in Field', name: 'binaryPropertyOutput', type: 'string', default: 'data', @@ -139,12 +186,43 @@ export class Compression implements INodeType { operation: ['compress'], }, }, - placeholder: '', - description: - 'Name of the binary property to which to write the data of the compressed files', + hint: 'The name of the output binary field to put the file in', }, { - displayName: 'Output Prefix', + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + placeholder: 'e.g. data.txt', + displayOptions: { + show: { + operation: ['compress'], + outputFormat: ['gzip'], + }, + hide: { + '@version': [1], + }, + }, + description: 'Name of the output file', + }, + { + displayName: 'Put Output File in Field', + name: 'binaryPropertyOutput', + type: 'string', + default: 'data', + displayOptions: { + show: { + outputFormat: ['gzip'], + operation: ['compress'], + }, + hide: { + '@version': [1], + }, + }, + hint: 'The name of the output binary field to put the file in', + }, + { + displayName: 'Output File Prefix', name: 'outputPrefix', type: 'string', default: 'data', @@ -153,9 +231,10 @@ export class Compression implements INodeType { show: { operation: ['compress'], outputFormat: ['gzip'], + '@version': [1], }, }, - description: 'Prefix use for all gzip compressed files', + description: 'Prefix to add to the gzip file', }, { displayName: 'Output Prefix', @@ -168,7 +247,7 @@ export class Compression implements INodeType { operation: ['decompress'], }, }, - description: 'Prefix use for all decompressed files', + description: 'Prefix to add to the decompressed files', }, ], }; @@ -178,6 +257,7 @@ export class Compression implements INodeType { const length = items.length; const returnData: INodeExecutionData[] = []; const operation = this.getNodeParameter('operation', 0); + const nodeVersion = this.getNode().typeVersion; for (let i = 0; i < length; i++) { try { @@ -212,20 +292,37 @@ export class Compression implements INodeType { binaryObject[`${outputPrefix}${zipIndex++}`] = data; } - } else if (binaryData.fileExtension?.toLowerCase() === 'gz') { + } else if (['gz', 'gzip'].includes(binaryData.fileExtension?.toLowerCase() as string)) { const file = await gunzip(binaryDataBuffer); const fileName = binaryData.fileName?.split('.')[0]; + let fileExtension; + let mimeType; + + if (binaryData.fileName?.endsWith('.gz')) { + const extractedFileExtension = binaryData.fileName.replace('.gz', '').split('.'); + if (extractedFileExtension.length > 1) { + fileExtension = extractedFileExtension[extractedFileExtension.length - 1]; + mimeType = mime.lookup(fileExtension) as string; + } + } const propertyName = `${outputPrefix}${index}`; binaryObject[propertyName] = await this.helpers.prepareBinaryData( Buffer.from(file.buffer), fileName, + mimeType, ); - const fileExtension = mime.extension(binaryObject[propertyName].mimeType) as string; + + if (!fileExtension) { + mimeType = binaryObject[propertyName].mimeType; + fileExtension = mime.extension(mimeType) as string; + } + binaryObject[propertyName].fileName = `${fileName}.${fileExtension}`; binaryObject[propertyName].fileExtension = fileExtension; + binaryObject[propertyName].mimeType = mimeType as string; } } @@ -239,14 +336,21 @@ export class Compression implements INodeType { } if (operation === 'compress') { - const binaryPropertyNames = this.getNodeParameter('binaryPropertyName', 0) + let binaryPropertyNameIndex = 0; + if (nodeVersion > 1) { + binaryPropertyNameIndex = i; + } + + const binaryPropertyNames = this.getNodeParameter( + 'binaryPropertyName', + binaryPropertyNameIndex, + ) .split(',') .map((key) => key.trim()); const outputFormat = this.getNodeParameter('outputFormat', 0) as string; const zipData: fflate.Zippable = {}; - const binaryObject: IBinaryKeyData = {}; for (const [index, binaryPropertyName] of binaryPropertyNames.entries()) { @@ -261,26 +365,53 @@ export class Compression implements INodeType { }, ]; } else if (outputFormat === 'gzip') { - const outputPrefix = this.getNodeParameter('outputPrefix', 0) as string; + let outputPrefix; + let fileName; + let binaryProperty; + let filePath; + + if (nodeVersion > 1) { + outputPrefix = this.getNodeParameter('binaryPropertyOutput', i, 'data'); + binaryProperty = `${outputPrefix}${index ? index : ''}`; + + fileName = this.getNodeParameter('fileName', i, '') as string; + if (!fileName) { + fileName = binaryData.fileName?.split('.')[0]; + } else { + fileName = fileName.replace('.gz', '').replace('.gzip', ''); + } + + const fileExtension = binaryData.fileExtension + ? `.${binaryData.fileExtension.toLowerCase()}` + : ''; + filePath = `${fileName}${fileExtension}.gz`; + } else { + outputPrefix = this.getNodeParameter('outputPrefix', 0) as string; + binaryProperty = `${outputPrefix}${index}`; + fileName = binaryData.fileName?.split('.')[0]; + filePath = `${fileName}.gzip`; + } const data = await gzip(binaryDataBuffer); - const fileName = binaryData.fileName?.split('.')[0]; - - binaryObject[`${outputPrefix}${index}`] = await this.helpers.prepareBinaryData( + binaryObject[binaryProperty] = await this.helpers.prepareBinaryData( Buffer.from(data), - `${fileName}.gzip`, + filePath, ); } } if (outputFormat === 'zip') { - const fileName = this.getNodeParameter('fileName', 0) as string; - - const binaryPropertyOutput = this.getNodeParameter('binaryPropertyOutput', 0); - + let zipOptionsIndex = 0; + if (nodeVersion > 1) { + zipOptionsIndex = i; + } + const fileName = this.getNodeParameter('fileName', zipOptionsIndex) as string; + const binaryPropertyOutput = this.getNodeParameter( + 'binaryPropertyOutput', + zipOptionsIndex, + ); const buffer = await zip(zipData); - const data = await this.helpers.prepareBinaryData(Buffer.from(buffer), fileName); returnData.push({ diff --git a/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts b/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts index f5c78b1579..449f34a593 100644 --- a/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts +++ b/packages/nodes-base/nodes/Cortex/AnalyzerDescriptions.ts @@ -89,7 +89,7 @@ export const analyzerFields: INodeProperties[] = [ description: 'Enter the observable value', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -101,7 +101,7 @@ export const analyzerFields: INodeProperties[] = [ operation: ['execute'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, { displayName: 'TLP', diff --git a/packages/nodes-base/nodes/Cortex/ResponderDescription.ts b/packages/nodes-base/nodes/Cortex/ResponderDescription.ts index 31fb47e356..64966847f3 100644 --- a/packages/nodes-base/nodes/Cortex/ResponderDescription.ts +++ b/packages/nodes-base/nodes/Cortex/ResponderDescription.ts @@ -233,7 +233,7 @@ export const responderFields: INodeProperties[] = [ name: 'artifactValues', values: [ { - displayName: 'Binary Property', + displayName: 'Binary Field', name: 'binaryProperty', type: 'string', displayOptions: { @@ -490,7 +490,7 @@ export const responderFields: INodeProperties[] = [ name: 'values', values: [ { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -499,7 +499,7 @@ export const responderFields: INodeProperties[] = [ dataType: ['file'], }, }, - description: 'Name of the binary property which contains the attachement data', + hint: 'The name of the input binary field containing the attachement data', }, { displayName: 'Data', diff --git a/packages/nodes-base/nodes/Crypto/Crypto.node.ts b/packages/nodes-base/nodes/Crypto/Crypto.node.ts index 91b74169e6..e95ff7ef65 100644 --- a/packages/nodes-base/nodes/Crypto/Crypto.node.ts +++ b/packages/nodes-base/nodes/Crypto/Crypto.node.ts @@ -121,7 +121,7 @@ export class Crypto implements INodeType { required: true, }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, diff --git a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts index af836169f3..2b2e006a60 100644 --- a/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts +++ b/packages/nodes-base/nodes/Dropbox/Dropbox.node.ts @@ -312,7 +312,7 @@ export class Dropbox implements INodeType { description: 'The file path of the file to download. Has to contain the full path.', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -323,7 +323,7 @@ export class Dropbox implements INodeType { resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, // ---------------------------------- @@ -346,7 +346,7 @@ export class Dropbox implements INodeType { 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -374,7 +374,7 @@ export class Dropbox implements INodeType { description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -387,8 +387,7 @@ export class Dropbox implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/EditImage/EditImage.node.json b/packages/nodes-base/nodes/EditImage/EditImage.node.json index f7987f9765..56219a87a2 100644 --- a/packages/nodes-base/nodes/EditImage/EditImage.node.json +++ b/packages/nodes-base/nodes/EditImage/EditImage.node.json @@ -11,7 +11,8 @@ } ] }, + "aliases": ["File", "Binary"], "subcategories": { - "Core Nodes": ["Files"] + "Core Nodes": ["Files", "Data Transformation"] } } diff --git a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts index 09e232c365..b457a2c621 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookGraphApi.node.ts @@ -184,7 +184,7 @@ export class FacebookGraphApi implements INodeType { description: 'Whether to connect even if SSL certificate validation is not possible', }, { - displayName: 'Send Binary Data', + displayName: 'Send Binary File', name: 'sendBinaryData', type: 'boolean', displayOptions: { @@ -197,7 +197,7 @@ export class FacebookGraphApi implements INodeType { description: 'Whether binary data should be sent as body', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: '', @@ -210,8 +210,9 @@ export class FacebookGraphApi implements INodeType { httpRequestMethod: ['POST', 'PUT'], }, }, + hint: 'The name of the input binary field containing the file to be uploaded', description: - 'Name of the binary property which contains the data for the file to be uploaded. For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', + 'For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', }, { displayName: 'Options', diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.json b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.json new file mode 100644 index 0000000000..bb99ab9459 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.json @@ -0,0 +1,35 @@ +{ + "node": "n8n-nodes-base.convertToFile", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Core Nodes"], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.converttofile/" + } + ] + }, + "alias": [ + "CSV", + "Spreadsheet", + "Excel", + "xls", + "xlsx", + "ods", + "tabular", + "encode", + "encoding", + "Move Binary Data", + "Binary", + "File", + "JSON", + "HTML", + "ICS", + "RTF", + "64" + ], + "subcategories": { + "Core Nodes": ["Files", "Data Transformation"] + } +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts new file mode 100644 index 0000000000..d17b431e15 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts @@ -0,0 +1,121 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import * as spreadsheet from './actions/spreadsheet.operation'; +import * as toBinary from './actions/toBinary.operation'; +import * as toJson from './actions/toJson.operation'; +import * as iCall from './actions/iCall.operation'; + +export class ConvertToFile implements INodeType { + // eslint-disable-next-line n8n-nodes-base/node-class-description-missing-subtitle + description: INodeTypeDescription = { + displayName: 'Convert to File', + name: 'convertToFile', + icon: 'file:convertToFile.svg', + group: ['input'], + version: 1, + description: 'Convert JSON data to binary data', + defaults: { + name: 'Convert to File', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Convert to CSV', + value: 'csv', + action: 'Convert to CSV', + description: 'Transform input data into a CSV file', + }, + { + name: 'Convert to HTML', + value: 'html', + action: 'Convert to HTML', + description: 'Transform input data into a table in an HTML file', + }, + { + name: 'Convert to iCal', + value: 'iCal', + action: 'Convert to iCal', + description: 'Converts each input item to an ICS event file', + }, + { + name: 'Convert to JSON', + value: 'toJson', + action: 'Convert to JSON', + description: 'Transform input data into a single or multiple JSON files', + }, + { + name: 'Convert to ODS', + value: 'ods', + action: 'Convert to ODS', + description: 'Transform input data into an ODS file', + }, + { + name: 'Convert to RTF', + value: 'rtf', + action: 'Convert to RTF', + description: 'Transform input data into a table in an RTF file', + }, + { + name: 'Convert to XLS', + value: 'xls', + action: 'Convert to XLS', + description: 'Transform input data into an Excel file', + }, + { + name: 'Convert to XLSX', + value: 'xlsx', + action: 'Convert to XLSX', + description: 'Transform input data into an Excel file', + }, + { + name: 'Move Base64 String to File', + value: 'toBinary', + action: 'Move base64 string to file', + description: 'Convert a base64-encoded string into its original file format', + }, + ], + default: 'csv', + }, + ...spreadsheet.description, + ...toBinary.description, + ...toJson.description, + ...iCall.description, + ], + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0); + let returnData: INodeExecutionData[] = []; + + if (spreadsheet.operations.includes(operation)) { + returnData = await spreadsheet.execute.call(this, items, operation); + } + + if (operation === 'toJson') { + returnData = await toJson.execute.call(this, items); + } + + if (operation === 'toBinary') { + returnData = await toBinary.execute.call(this, items); + } + + if (operation === 'iCal') { + returnData = await iCall.execute.call(this, items); + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/iCall.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/iCall.operation.ts new file mode 100644 index 0000000000..7fcca69b4c --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/iCall.operation.ts @@ -0,0 +1,20 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import * as createEvent from '../../../ICalendar/createEvent.operation'; + +import { updateDisplayOptions } from '@utils/utilities'; + +export const description: INodeProperties[] = updateDisplayOptions( + { + show: { + operation: ['iCal'], + }, + }, + createEvent.description, +); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData = await createEvent.execute.call(this, items); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/spreadsheet.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/spreadsheet.operation.ts new file mode 100644 index 0000000000..eb0beca2ce --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/spreadsheet.operation.ts @@ -0,0 +1,126 @@ +import { + NodeOperationError, + type IExecuteFunctions, + type INodeExecutionData, + type INodeProperties, +} from 'n8n-workflow'; + +import { generatePairedItemData, updateDisplayOptions } from '@utils/utilities'; +import type { JsonToSpreadsheetBinaryOptions, JsonToSpreadsheetBinaryFormat } from '@utils/binary'; + +import { convertJsonToSpreadsheetBinary } from '@utils/binary'; + +export const operations = ['csv', 'html', 'rtf', 'ods', 'xls', 'xlsx']; + +export const properties: INodeProperties[] = [ + { + displayName: 'Put Output File in Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + hint: 'The name of the output binary field to put the file in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Compression', + name: 'compression', + type: 'boolean', + displayOptions: { + show: { + '/operation': ['xlsx', 'ods'], + }, + }, + default: false, + description: 'Whether to reduce the output file size', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: 'Name of the output file', + }, + { + displayName: 'Header Row', + name: 'headerRow', + type: 'boolean', + default: true, + description: 'Whether the first row of the file contains the header names', + }, + { + displayName: 'Sheet Name', + name: 'sheetName', + type: 'string', + displayOptions: { + show: { + '/operation': ['ods', 'xls', 'xlsx'], + }, + }, + default: 'Sheet', + description: 'Name of the sheet to create in the spreadsheet', + placeholder: 'e.g. mySheet', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: operations, + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], + operation: string, +) { + let returnData: INodeExecutionData[] = []; + + const pairedItem = generatePairedItemData(items.length); + try { + const options = this.getNodeParameter('options', 0, {}) as JsonToSpreadsheetBinaryOptions; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0, 'data'); + + const binaryData = await convertJsonToSpreadsheetBinary.call( + this, + items, + operation as JsonToSpreadsheetBinaryFormat, + options, + 'File', + ); + + const newItem: INodeExecutionData = { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem, + }; + + returnData = [newItem]; + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem, + }); + } else { + throw new NodeOperationError(this.getNode(), error); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts new file mode 100644 index 0000000000..770b55b72b --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts @@ -0,0 +1,147 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import { NodeOperationError } from 'n8n-workflow'; + +import type { JsonToBinaryOptions } from '@utils/binary'; +import { createBinaryFromJson } from '@utils/binary'; +import { encodeDecodeOptions } from '@utils/descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Base64 Input Field', + name: 'sourceProperty', + type: 'string', + default: '', + required: true, + placeholder: 'e.g data', + requiresDataPath: 'single', + description: + "The name of the input field that contains the base64 string to convert to a file. Use dot-notation for deep fields (e.g. 'level1.level2.currentKey').", + }, + { + displayName: 'Put Output File in Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + hint: 'The name of the output binary field to put the file in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Add Byte Order Mark (BOM)', + description: + 'Whether to add special marker at the start of your text file. This marker helps some programs understand how to read the file correctly.', + name: 'addBOM', + displayOptions: { + show: { + encoding: ['utf8', 'cesu8', 'ucs2'], + }, + }, + type: 'boolean', + default: false, + }, + { + displayName: 'Data Is Base64', + name: 'dataIsBase64', + type: 'boolean', + default: true, + description: 'Whether the data is already base64 encoded', + }, + { + displayName: 'Encoding', + name: 'encoding', + type: 'options', + options: encodeDecodeOptions, + default: 'utf8', + description: 'Choose the character set to use to encode the data', + displayOptions: { + hide: { + dataIsBase64: [true], + }, + }, + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + placeholder: 'e.g. myFile', + description: 'Name of the output file', + }, + { + displayName: 'MIME Type', + name: 'mimeType', + type: 'string', + default: '', + placeholder: 'e.g text/plain', + description: + 'The MIME type of the output file. Common MIME types.', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['toBinary'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const options = this.getNodeParameter('options', i, {}); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i, 'data'); + const sourceProperty = this.getNodeParameter('sourceProperty', i) as string; + + const jsonToBinaryOptions: JsonToBinaryOptions = { + sourceKey: sourceProperty, + fileName: options.fileName as string, + mimeType: options.mimeType as string, + dataIsBase64: options.dataIsBase64 !== false, + encoding: options.encoding as string, + addBOM: options.addBOM as boolean, + itemIndex: i, + }; + + const binaryData = await createBinaryFromJson.call(this, items[i].json, jsonToBinaryOptions); + + const newItem: INodeExecutionData = { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem: { item: i }, + }; + + returnData.push(newItem); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts new file mode 100644 index 0000000000..eaccdfb5ff --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts @@ -0,0 +1,165 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { generatePairedItemData, updateDisplayOptions } from '@utils/utilities'; +import { createBinaryFromJson } from '@utils/binary'; +import { encodeDecodeOptions } from '@utils/descriptions'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'All Items to One File', + value: 'once', + }, + { + name: 'Each Item to Separate File', + value: 'each', + }, + ], + default: 'once', + }, + { + displayName: 'Put Output File in Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + hint: 'The name of the output binary field to put the file in', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Add Byte Order Mark (BOM)', + name: 'addBOM', + type: 'boolean', + default: false, + description: + 'Whether to add special marker at the start of your text file. This marker helps some programs understand how to read the file correctly.', + displayOptions: { + show: { + encoding: ['utf8', 'cesu8', 'ucs2'], + }, + }, + }, + { + displayName: 'Encoding', + name: 'encoding', + type: 'options', + options: encodeDecodeOptions, + default: 'utf8', + description: 'Choose the character set to use to encode the data', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + placeholder: 'e.g. myFile.json', + description: 'Name of the output file', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['toJson'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + let returnData: INodeExecutionData[] = []; + + const mode = this.getNodeParameter('mode', 0, 'once') as string; + if (mode === 'once') { + const pairedItem = generatePairedItemData(items.length); + try { + const options = this.getNodeParameter('options', 0, {}); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0, 'data'); + + const binaryData = await createBinaryFromJson.call( + this, + items.map((item) => item.json), + { + fileName: options.fileName as string, + mimeType: 'application/json', + encoding: options.encoding as string, + addBOM: options.addBOM as boolean, + }, + ); + + const newItem: INodeExecutionData = { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem, + }; + + returnData = [newItem]; + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem, + }); + } + throw new NodeOperationError(this.getNode(), error); + } + } else { + for (let i = 0; i < items.length; i++) { + try { + const options = this.getNodeParameter('options', i, {}); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i, 'data'); + + const binaryData = await createBinaryFromJson.call(this, items[i].json, { + fileName: options.fileName as string, + encoding: options.encoding as string, + addBOM: options.addBOM as boolean, + mimeType: 'application/json', + itemIndex: i, + }); + + const newItem: INodeExecutionData = { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem: { item: i }, + }; + + returnData.push(newItem); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); + } + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/convertToFile.svg b/packages/nodes-base/nodes/Files/ConvertToFile/convertToFile.svg new file mode 100644 index 0000000000..60ca4a415a --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/convertToFile.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.json b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.json new file mode 100644 index 0000000000..989173a6a5 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.json @@ -0,0 +1,39 @@ +{ + "node": "n8n-nodes-base.extractFromFile", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Core Nodes"], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.extractfromfile/" + } + ] + }, + "alias": [ + "CSV", + "Spreadsheet", + "Excel", + "xls", + "xlsx", + "ods", + "tabular", + "decode", + "decoding", + "Move Binary Data", + "Binary", + "File", + "PDF", + "JSON", + "HTML", + "ICS", + "txt", + "Text", + "RTF", + "XML", + "64" + ], + "subcategories": { + "Core Nodes": ["Files", "Data Transformation"] + } +} diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts new file mode 100644 index 0000000000..3908fc842e --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/ExtractFromFile.node.ts @@ -0,0 +1,134 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import * as spreadsheet from './actions/spreadsheet.operation'; +import * as moveTo from './actions/moveTo.operation'; +import * as pdf from './actions/pdf.operation'; + +export class ExtractFromFile implements INodeType { + // eslint-disable-next-line n8n-nodes-base/node-class-description-missing-subtitle + description: INodeTypeDescription = { + displayName: 'Extract From File', + name: 'extractFromFile', + icon: 'file:extractFromFile.svg', + group: ['input'], + version: 1, + description: 'Convert binary data to JSON', + defaults: { + name: 'Extract From File', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + // eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items + options: [ + { + name: 'Extract From CSV', + value: 'csv', + action: 'Extract from CSV', + description: 'Transform a CSV file into output items', + }, + { + name: 'Extract From HTML', + value: 'html', + action: 'Extract from HTML', + description: 'Transform a table in an HTML file into output items', + }, + { + name: 'Extract From JSON', + value: 'fromJson', + action: 'Extract from JSON', + description: 'Transform a JSON file into output items', + }, + { + name: 'Extract From ICS', + value: 'fromIcs', + action: 'Extract from ICS', + description: 'Transform a ICS file into output items', + }, + { + name: 'Extract From ODS', + value: 'ods', + action: 'Extract from ODS', + description: 'Transform an ODS file into output items', + }, + { + name: 'Extract From PDF', + value: 'pdf', + action: 'Extract from PDF', + description: 'Extracts the content and metadata from a PDF file', + }, + { + name: 'Extract From RTF', + value: 'rtf', + action: 'Extract from RTF', + description: 'Transform a table in an RTF file into output items', + }, + { + name: 'Extract From Text File', + value: 'text', + action: 'Extract from text file', + description: 'Extracts the content of a text file', + }, + { + name: 'Extract From XML', + value: 'xml', + action: 'Extract from XLS', + description: 'Extracts the content of an XML file', + }, + { + name: 'Extract From XLS', + value: 'xls', + action: 'Extract from XLS', + description: 'Transform an Excel file into output items', + }, + { + name: 'Extract From XLSX', + value: 'xlsx', + action: 'Extract from XLSX', + description: 'Transform an Excel file into output items', + }, + { + name: 'Move File to Base64 String', + value: 'binaryToPropery', + action: 'Move file to base64 string', + description: 'Convert a file into a base64-encoded string', + }, + ], + default: 'csv', + }, + ...spreadsheet.description, + ...moveTo.description, + ...pdf.description, + ], + }; + + async execute(this: IExecuteFunctions) { + const items = this.getInputData(); + const operation = this.getNodeParameter('operation', 0); + let returnData: INodeExecutionData[] = []; + + if (spreadsheet.operations.includes(operation)) { + returnData = await spreadsheet.execute.call(this, items, 'operation'); + } + + if (['binaryToPropery', 'fromJson', 'text', 'fromIcs', 'xml'].includes(operation)) { + returnData = await moveTo.execute.call(this, items, operation); + } + + if (operation === 'pdf') { + returnData = await pdf.execute.call(this, items); + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts new file mode 100644 index 0000000000..523c2090cc --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/moveTo.operation.ts @@ -0,0 +1,192 @@ +import type { + IDataObject, + IExecuteFunctions, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; + +import { BINARY_ENCODING, NodeOperationError, deepCopy, jsonParse } from 'n8n-workflow'; + +import { encodeDecodeOptions } from '@utils/descriptions'; +import { updateDisplayOptions } from '@utils/utilities'; + +import get from 'lodash/get'; +import set from 'lodash/set'; +import unset from 'lodash/unset'; + +import iconv from 'iconv-lite'; + +import { icsCalendarToObject } from 'ts-ics'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Input Binary Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + hint: 'The name of the input field containing the file data to be processed', + }, + { + displayName: 'Destination Output Field', + name: 'destinationKey', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + description: 'The name of the output field that will contain the extracted data', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'File Encoding', + name: 'encoding', + type: 'options', + options: encodeDecodeOptions, + default: 'utf8', + description: 'Specify the encoding of the file, defaults to UTF-8', + }, + { + displayName: 'Strip BOM', + name: 'stripBOM', + displayOptions: { + show: { + encoding: ['utf8', 'cesu8', 'ucs2'], + }, + }, + type: 'boolean', + default: true, + description: + 'Whether to strip the BOM (Byte Order Mark) from the file, this could help in an environment where the presence of the BOM is causing issues or inconsistencies', + }, + { + displayName: 'Keep Source', + name: 'keepSource', + type: 'options', + default: 'json', + options: [ + { + name: 'JSON', + value: 'json', + description: 'Include JSON data of the input item', + }, + { + name: 'Binary', + value: 'binary', + description: 'Include binary data of the input item', + }, + { + name: 'Both', + value: 'both', + description: 'Include both JSON and binary data of the input item', + }, + ], + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['binaryToPropery', 'fromJson', 'text', 'fromIcs', 'xml'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], + operation: string, +) { + const returnData: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + const item = items[itemIndex]; + const options = this.getNodeParameter('options', itemIndex); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex); + + const newItem: INodeExecutionData = { + json: {}, + pairedItem: { item: itemIndex }, + }; + + const value = get(item.binary, binaryPropertyName); + + if (!value) continue; + + const encoding = (options.encoding as string) || 'utf8'; + const buffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); + + if (options.keepSource && options.keepSource !== 'binary') { + newItem.json = deepCopy(item.json); + } + + let convertedValue: string | IDataObject; + if (operation !== 'binaryToPropery') { + convertedValue = iconv.decode(buffer, encoding, { + stripBOM: options.stripBOM as boolean, + }); + } else { + convertedValue = Buffer.from(buffer).toString(BINARY_ENCODING); + } + + if (operation === 'fromJson') { + if (convertedValue === '') { + convertedValue = {}; + } else { + convertedValue = jsonParse(convertedValue); + } + } + + if (operation === 'fromIcs') { + convertedValue = icsCalendarToObject(convertedValue as string); + } + + const destinationKey = this.getNodeParameter('destinationKey', itemIndex, '') as string; + set(newItem.json, destinationKey, convertedValue); + + if (options.keepSource === 'binary' || options.keepSource === 'both') { + newItem.binary = item.binary; + } else { + // this binary data would not be included, but there also might be other binary data + // which should be included, copy it over and unset current binary data + newItem.binary = deepCopy(item.binary); + unset(newItem.binary, binaryPropertyName); + } + + returnData.push(newItem); + } catch (error) { + let errorDescription; + if (error.message.includes('Unexpected token')) { + error.message = "The file selected in 'Input Binary Field' is not in JSON format"; + errorDescription = + "Try to change the operation or select a JSON file in 'Input Binary Field'"; + } + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { + itemIndex, + description: errorDescription, + }); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/pdf.operation.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/pdf.operation.ts new file mode 100644 index 0000000000..5947385145 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/pdf.operation.ts @@ -0,0 +1,141 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import { NodeOperationError, deepCopy } from 'n8n-workflow'; + +import unset from 'lodash/unset'; + +import { extractDataFromPDF } from '@utils/binary'; +import { updateDisplayOptions } from '@utils/utilities'; + +export const properties: INodeProperties[] = [ + { + displayName: 'Input Binary Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: 'e.g data', + hint: 'The name of the input binary field containing the file to be extracted', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Join Pages', + name: 'joinPages', + type: 'boolean', + default: true, + description: + 'Whether to join the text from all pages or return an array of text from each page', + }, + { + displayName: 'Keep Source', + name: 'keepSource', + type: 'options', + default: 'json', + options: [ + { + name: 'JSON', + value: 'json', + description: 'Include JSON data of the input item', + }, + { + name: 'Binary', + value: 'binary', + description: 'Include binary data of the input item', + }, + { + name: 'Both', + value: 'both', + description: 'Include both JSON and binary data of the input item', + }, + ], + }, + { + displayName: 'Max Pages', + name: 'maxPages', + type: 'number', + default: 0, + description: 'Maximum number of pages to include', + }, + { + displayName: 'Password', + name: 'password', + type: 'string', + typeOptions: { password: true }, + default: '', + description: 'Prowide password, if the PDF is encrypted', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['pdf'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + const item = items[itemIndex]; + const options = this.getNodeParameter('options', itemIndex); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex); + + const json = await extractDataFromPDF.call( + this, + binaryPropertyName, + options.password as string, + options.maxPages as number, + options.joinPages as boolean, + itemIndex, + ); + + const newItem: INodeExecutionData = { + json: {}, + pairedItem: { item: itemIndex }, + }; + + if (options.keepSource && options.keepSource !== 'binary') { + newItem.json = { ...deepCopy(item.json), ...json }; + } else { + newItem.json = json; + } + + if (options.keepSource === 'binary' || options.keepSource === 'both') { + newItem.binary = item.binary; + } else { + // this binary data would not be included, but there also might be other binary data + // which should be included, copy it over and unset current binary data + newItem.binary = deepCopy(item.binary); + unset(newItem.binary, binaryPropertyName); + } + + returnData.push(newItem); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { itemIndex }); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/actions/spreadsheet.operation.ts b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/spreadsheet.operation.ts new file mode 100644 index 0000000000..6153037438 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/actions/spreadsheet.operation.ts @@ -0,0 +1,59 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import * as fromFile from '../../../SpreadsheetFile/v2/fromFile.operation'; + +export const operations = ['csv', 'html', 'rtf', 'ods', 'xls', 'xlsx']; + +export const description: INodeProperties[] = fromFile.description + .filter((property) => property.name !== 'fileFormat') + .map((property) => { + const newProperty = { ...property }; + newProperty.displayOptions = { + show: { + operation: operations, + }, + }; + + if (newProperty.name === 'options') { + newProperty.options = (newProperty.options as INodeProperties[]).map((option) => { + let newOption = option; + if (['delimiter', 'fromLine', 'maxRowCount', 'enableBOM'].includes(option.name)) { + newOption = { ...option, displayOptions: { show: { '/operation': ['csv'] } } }; + } + if (option.name === 'sheetName') { + newOption = { + ...option, + displayOptions: { show: { '/operation': ['ods', 'xls', 'xlsx'] } }, + description: 'Name of the sheet to read from in the spreadsheet', + }; + } + if (option.name === 'range') { + newOption = { + ...option, + displayOptions: { show: { '/operation': ['ods', 'xls', 'xlsx'] } }, + }; + } + if (['includeEmptyCells', 'headerRow'].includes(option.name)) { + newOption = { + ...option, + displayOptions: { show: { '/operation': ['ods', 'xls', 'xlsx', 'csv', 'html'] } }, + }; + } + return newOption; + }); + } + return newProperty; + }); + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], + fileFormatProperty: string, +) { + const returnData: INodeExecutionData[] = await fromFile.execute.call( + this, + items, + fileFormatProperty, + ); + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ExtractFromFile/extractFromFile.svg b/packages/nodes-base/nodes/Files/ExtractFromFile/extractFromFile.svg new file mode 100644 index 0000000000..2c235b7a2d --- /dev/null +++ b/packages/nodes-base/nodes/Files/ExtractFromFile/extractFromFile.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.json b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.json new file mode 100644 index 0000000000..57bdf62dd7 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.json @@ -0,0 +1,17 @@ +{ + "node": "n8n-nodes-base.readWriteFile", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Core Nodes"], + "resources": { + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.filesreadwrite/" + } + ] + }, + "alias": ["Binary", "File", "Text", "Open", "Import", "Save", "Export", "Disk", "Transfer"], + "subcategories": { + "Core Nodes": ["Files"] + } +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts new file mode 100644 index 0000000000..9c4b95d170 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/ReadWriteFile.node.ts @@ -0,0 +1,73 @@ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import * as read from './actions/read.operation'; +import * as write from './actions/write.operation'; + +export class ReadWriteFile implements INodeType { + description: INodeTypeDescription = { + displayName: 'Read/Write Files from Disk', + name: 'readWriteFile', + icon: 'file:readWriteFile.svg', + group: ['input'], + version: 1, + description: 'Read or write files from the computer that runs n8n', + defaults: { + name: 'Read/Write Files from Disk', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: + 'Use this node to read and write files on the same computer running n8n. To handle files between different computers please use other nodes (e.g. FTP, HTTP Request, AWS).', + name: 'info', + type: 'notice', + default: '', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Read File(s) From Disk', + value: 'read', + description: 'Retrieve one or more files from the computer that runs n8n', + action: 'Read File(s) From Disk', + }, + { + name: 'Write File to Disk', + value: 'write', + description: 'Create a binary file on the computer that runs n8n', + action: 'Write File to Disk', + }, + ], + default: 'read', + }, + ...read.description, + ...write.description, + ], + }; + + async execute(this: IExecuteFunctions) { + const operation = this.getNodeParameter('operation', 0, 'read'); + const items = this.getInputData(); + let returnData: INodeExecutionData[] = []; + + if (operation === 'read') { + returnData = await read.execute.call(this, items); + } + + if (operation === 'write') { + returnData = await write.execute.call(this, items); + } + + return [returnData]; + } +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts new file mode 100644 index 0000000000..48e532c8a7 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/read.operation.ts @@ -0,0 +1,144 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import glob from 'fast-glob'; +import { updateDisplayOptions } from '@utils/utilities'; +import { errorMapper } from '../helpers/utils'; + +export const properties: INodeProperties[] = [ + { + displayName: 'File(s) Selector', + name: 'fileSelector', + type: 'string', + default: '', + required: true, + placeholder: 'e.g. /home/user/Pictures/**/*.png', + hint: 'Supports patterns, learn more here', + description: "Specify a file's path or path pattern to read multiple files", + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'File Extension', + name: 'fileExtension', + type: 'string', + default: '', + placeholder: 'e.g. zip', + description: 'Extension of the file in the output binary', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + placeholder: 'e.g. data.zip', + description: 'Name of the file in the output binary', + }, + { + displayName: 'Mime Type', + name: 'mimeType', + type: 'string', + default: '', + placeholder: 'e.g. application/zip', + description: 'Mime type of the file in the output binary', + }, + { + displayName: 'Put Output File in Field', + name: 'dataPropertyName', + type: 'string', + default: 'data', + placeholder: 'e.g. data', + description: "By default 'data' is used", + hint: 'The name of the output binary field to put the file in', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['read'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + let fileSelector; + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + fileSelector = this.getNodeParameter('fileSelector', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}); + + let dataPropertyName = 'data'; + + if (options.dataPropertyName) { + dataPropertyName = options.dataPropertyName as string; + } + + const files = await glob(fileSelector); + + const newItems: INodeExecutionData[] = []; + for (const filePath of files) { + const stream = await this.helpers.createReadStream(filePath); + const binaryData = await this.helpers.prepareBinaryData(stream, filePath); + + if (options.fileName !== undefined) { + binaryData.fileName = options.fileName as string; + } + + if (options.fileExtension !== undefined) { + binaryData.fileExtension = options.fileExtension as string; + } + + if (options.mimeType !== undefined) { + binaryData.mimeType = options.mimeType as string; + } + + newItems.push({ + binary: { + [dataPropertyName]: binaryData, + }, + json: { + mimeType: binaryData.mimeType, + fileType: binaryData.fileType, + fileName: binaryData.fileName, + directory: binaryData.directory, + fileExtension: binaryData.fileExtension, + fileSize: binaryData.fileSize, + }, + pairedItem: { + item: itemIndex, + }, + }); + } + + returnData.push(...newItems); + } catch (error) { + const nodeOperatioinError = errorMapper.call(this, error, itemIndex, { + filePath: fileSelector, + operation: 'read', + }); + if (this.continueOnFail()) { + returnData.push({ + json: { + error: nodeOperatioinError.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw nodeOperatioinError; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/actions/write.operation.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/write.operation.ts new file mode 100644 index 0000000000..310f753320 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/actions/write.operation.ts @@ -0,0 +1,123 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { BINARY_ENCODING } from 'n8n-workflow'; + +import type { Readable } from 'stream'; + +import { updateDisplayOptions } from '@utils/utilities'; +import { errorMapper } from '../helpers/utils'; + +export const properties: INodeProperties[] = [ + { + displayName: 'File Path and Name', + name: 'fileName', + type: 'string', + default: '', + required: true, + placeholder: 'e.g. /data/example.jpg', + description: + 'Path and name of the file that should be written. Also include the file extension.', + }, + { + displayName: 'Input Binary Field', + name: 'dataPropertyName', + type: 'string', + default: 'data', + placeholder: 'e.g. data', + required: true, + hint: 'The name of the input binary field containing the file to be written', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Append', + name: 'append', + type: 'boolean', + default: false, + description: + "Whether to append to an existing file. While it's commonly used with text files, it's not limited to them, however, it wouldn't be applicable for file types that have a specific structure like most binary formats.", + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['write'], + }, +}; + +export const description = updateDisplayOptions(displayOptions, properties); + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + let fileName; + + let item: INodeExecutionData; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + try { + const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex); + fileName = this.getNodeParameter('fileName', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}); + const flag: string = options.append ? 'a' : 'w'; + + item = items[itemIndex]; + + const newItem: INodeExecutionData = { + json: {}, + pairedItem: { + item: itemIndex, + }, + }; + Object.assign(newItem.json, item.json); + + const binaryData = this.helpers.assertBinaryData(itemIndex, dataPropertyName); + + let fileContent: Buffer | Readable; + if (binaryData.id) { + fileContent = await this.helpers.getBinaryStream(binaryData.id); + } else { + fileContent = Buffer.from(binaryData.data, BINARY_ENCODING); + } + + // Write the file to disk + await this.helpers.writeContentToFile(fileName, fileContent, flag); + + if (item.binary !== undefined) { + // Create a shallow copy of the binary data so that the old + // data references which do not get changed still stay behind + // but the incoming data does not get changed. + newItem.binary = {}; + Object.assign(newItem.binary, item.binary); + } + + // Add the file name to data + newItem.json.fileName = fileName; + + returnData.push(newItem); + } catch (error) { + const nodeOperatioinError = errorMapper.call(this, error, itemIndex, { + filePath: fileName, + operation: 'write', + }); + if (this.continueOnFail()) { + returnData.push({ + json: { + error: nodeOperatioinError.message, + }, + pairedItem: { + item: itemIndex, + }, + }); + continue; + } + throw nodeOperatioinError; + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts new file mode 100644 index 0000000000..8ecbb66706 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/helpers/utils.ts @@ -0,0 +1,32 @@ +import type { IDataObject, IExecuteFunctions } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +export function errorMapper( + this: IExecuteFunctions, + error: Error, + itemIndex: number, + context?: IDataObject, +) { + let message; + let description; + + if (error.message.includes('Cannot create a string longer than')) { + message = 'The file is too large'; + description = + 'The binary file you are attempting to read exceeds 512MB, which is limit when using default binary data mode, try using the filesystem binary mode. More information here.'; + } else if (error.message.includes('EACCES') && context?.operation === 'read') { + const path = + ((error as unknown as IDataObject).path as string) || (context?.filePath as string); + message = `You don't have the permissions to access ${path}`; + description = + "Verify that the path specified in 'File(s) Selector' is correct, or change the file(s) permissions if needed"; + } else if (error.message.includes('EACCES') && context?.operation === 'write') { + const path = + ((error as unknown as IDataObject).path as string) || (context?.filePath as string); + message = `You don't have the permissions to write the file ${path}`; + description = + "Specify another destination folder in 'File Path and Name', or change the permissions of the parent folder"; + } + + return new NodeOperationError(this.getNode(), error, { itemIndex, message, description }); +} diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/readWriteFile.svg b/packages/nodes-base/nodes/Files/ReadWriteFile/readWriteFile.svg new file mode 100644 index 0000000000..288224b956 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/readWriteFile.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.test.ts b/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.test.ts new file mode 100644 index 0000000000..38c8d0057e --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.test.ts @@ -0,0 +1,101 @@ +/* eslint-disable @typescript-eslint/no-loop-func */ +import * as Helpers from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; + +describe('Test ReadWriteFile Node', () => { + beforeEach(async () => { + await Helpers.initBinaryDataService(); + }); + + const temporaryDir = Helpers.createTemporaryDir(); + const directory = __dirname.replace(/\\/gi, '/'); + + const workflow = Helpers.readJsonFileSync( + 'nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json', + ); + + const readFileNode = workflow.nodes.find((n: any) => n.name === 'Read from Disk'); + readFileNode.parameters.fileSelector = `${directory}/image.jpg`; + + const writeFileNode = workflow.nodes.find((n: any) => n.name === 'Write to Disk'); + writeFileNode.parameters.fileName = `${temporaryDir}/image-written.jpg`; + + const tests: WorkflowTestData[] = [ + { + description: 'nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json', + input: { + workflowData: workflow, + }, + output: { + nodeData: { + 'Read from Disk': [ + [ + { + json: { + directory, + fileExtension: 'jpg', + fileName: 'image.jpg', + fileSize: '1.04 kB', + fileType: 'image', + mimeType: 'image/jpeg', + }, + binary: { + data: { + mimeType: 'image/jpeg', + fileType: 'image', + fileExtension: 'jpg', + data: '/9j/4AAQSkZJRgABAQEASABIAAD/4QBmRXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAAExAAIAAAAQAAAATgAAAAAAARlJAAAD6AABGUkAAAPocGFpbnQubmV0IDUuMC4xAP/bAEMAIBYYHBgUIBwaHCQiICYwUDQwLCwwYkZKOlB0Znp4cmZwboCQuJyAiK6KbnCg2qKuvsTO0M58muLy4MjwuMrOxv/bAEMBIiQkMCowXjQ0XsaEcITGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxv/AABEIAB8AOwMBEgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOgqgrXF2zNHJ5aKcD3oNPZ23di/VKG82bkuTh1OMgdaAdOSLtZ6G5ut0iSeWoOAKAdO27NCqUN8oQrcHDqccDrQDpyRNPdRwEKcsx7CobIebPLORwThc0inGMF724jagNpxG4OOM1dIDAgjIPBpkqUOxnR2pmh85pW3nJB9KkNi4yqTssZ6rSNXNX0ehHFfusYDLuI7+tXY4I40ChQcdzQRKcL7Fb7PcQO32cqUY5we1XqZPtH11KsFoFDGYK7sckkZxVqgTnJlEQXMBZYGUoTkZ7VeoH7RvcqwWaIh80K7k5JIq1QJzkyhbMtvdSxMdqnlc1amgjmx5i5I70inNSVpFdrmaWRltkBVerHvUW57B2AUNGxyOaC+VW9xXLVrcGbcjrtkXqKZZxvveeTAL9APSgiooq1ty3RTMj//2Q==', + directory, + fileName: 'image.jpg', + fileSize: '1.04 kB', + }, + }, + }, + ], + ], + 'Write to Disk': [ + [ + { + json: { + directory, + fileExtension: 'jpg', + fileName: writeFileNode.parameters.fileName, + fileSize: '1.04 kB', + fileType: 'image', + mimeType: 'image/jpeg', + }, + binary: { + data: { + mimeType: 'image/jpeg', + fileType: 'image', + fileExtension: 'jpg', + data: '/9j/4AAQSkZJRgABAQEASABIAAD/4QBmRXhpZgAATU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAAExAAIAAAAQAAAATgAAAAAAARlJAAAD6AABGUkAAAPocGFpbnQubmV0IDUuMC4xAP/bAEMAIBYYHBgUIBwaHCQiICYwUDQwLCwwYkZKOlB0Znp4cmZwboCQuJyAiK6KbnCg2qKuvsTO0M58muLy4MjwuMrOxv/bAEMBIiQkMCowXjQ0XsaEcITGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxv/AABEIAB8AOwMBEgACEQEDEQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOgqgrXF2zNHJ5aKcD3oNPZ23di/VKG82bkuTh1OMgdaAdOSLtZ6G5ut0iSeWoOAKAdO27NCqUN8oQrcHDqccDrQDpyRNPdRwEKcsx7CobIebPLORwThc0inGMF724jagNpxG4OOM1dIDAgjIPBpkqUOxnR2pmh85pW3nJB9KkNi4yqTssZ6rSNXNX0ehHFfusYDLuI7+tXY4I40ChQcdzQRKcL7Fb7PcQO32cqUY5we1XqZPtH11KsFoFDGYK7sckkZxVqgTnJlEQXMBZYGUoTkZ7VeoH7RvcqwWaIh80K7k5JIq1QJzkyhbMtvdSxMdqnlc1amgjmx5i5I70inNSVpFdrmaWRltkBVerHvUW57B2AUNGxyOaC+VW9xXLVrcGbcjrtkXqKZZxvveeTAL9APSgiooq1ty3RTMj//2Q==', + directory, + fileName: 'image.jpg', + fileSize: '1.04 kB', + }, + }, + }, + ], + ], + }, + }, + }, + ]; + + const nodeTypes = Helpers.setup(tests); + + for (const testData of tests) { + test(testData.description, async () => { + const { result } = await executeWorkflow(testData, nodeTypes); + + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => { + expect(resultData).toEqual(testData.output.nodeData[nodeName]); + }); + + expect(result.finished).toEqual(true); + }); + } +}); diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json b/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json new file mode 100644 index 0000000000..46fb3bc2a0 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ReadWriteFile/test/ReadWriteFile.workflow.json @@ -0,0 +1,72 @@ +{ + "meta": { + "instanceId": "104a4d08d8897b8bdeb38aaca515021075e0bd8544c983c2bb8c86e6a8e6081c" + }, + "nodes": [ + { + "parameters": {}, + "id": "01b8609f-a345-41de-80bf-6d84276b5e7a", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 700, + 320 + ] + }, + { + "parameters": { + "fileSelector": "C:/Test/image.jpg", + "options": {} + }, + "id": "a1ea0fd0-cc95-4de2-bc58-bc980cb1d97e", + "name": "Read from Disk", + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1, + "position": [ + 920, + 320 + ] + }, + { + "parameters": { + "operation": "write", + "fileName": "C:/Test/image-written.jpg", + "options": {} + }, + "id": "94abac52-bd10-4b57-85b0-691c70989137", + "name": "Write to Disk", + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1, + "position": [ + 1140, + 320 + ] + } + ], + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Read from Disk", + "type": "main", + "index": 0 + } + ] + ] + }, + "Read from Disk": { + "main": [ + [ + { + "node": "Write to Disk", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Files/ReadWriteFile/test/image.jpg b/packages/nodes-base/nodes/Files/ReadWriteFile/test/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24b60c421ecc14914b9e63577915a5837fb69504 GIT binary patch literal 1045 zcmex=u87UbRB?UEu022cp9fKq{FROr(w5p1tw1T{b2|MOAbgb*jD_C%A(Yk#{ z&RsZHGwac(2PZ!4ICbtA$Ph*)6%_+5gE$kDxMM8^EyqX({|_(-axlmPKLX@4FtP$e1r#t)z{teR!pg?Z!O6u9RIpWmfr*isnTds&m6e4BsJa#?&%h$c zDx_%W$R-?^$gWfnAuRebI z{N?Mn?>~P20{M%Pff?d0xX;l1B?$Bv6AKG73p>bPj7;S~%q+;ls%Xe2)1dVaEgM(k0HmnZ36tCF6*_L9oHsOAO@ zcE8)3omM*6EabW)V>PG1>H^=Ki6-9z4>--)EO%((Cb^tX=iFHy7JDq0I9PqV<5t71 zLh0r{<8Tii4rPT8nUj|C9V;nYmQnL;>h?JkYPFn`9&1hBbgXKva=2-&TuWj6u4Bx4 zkF0-Pz44&WgiA!G+(c0G&~MRw=L?y)-#j%Xd5+xGs+o2dzg}6*x**_K!n!v_o{~qS z7WfsV3bLMIoyHc_@+5t0+=9A`drxhMT%`EfY4_ww9;-t*&-pCOIh|js<5RZsX>r)H zCd-Y_^gP~sEH_on6utE c%yjAZl}`@nU*PxRSg~kr?&*?XBm4h10lOThTL1t6 literal 0 HcmV?d00001 diff --git a/packages/nodes-base/nodes/Files/test/ConvertExtract.test.ts b/packages/nodes-base/nodes/Files/test/ConvertExtract.test.ts new file mode 100644 index 0000000000..9ff4604263 --- /dev/null +++ b/packages/nodes-base/nodes/Files/test/ConvertExtract.test.ts @@ -0,0 +1,5 @@ +import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers'; + +const workflows = getWorkflowFilenames(__dirname); + +describe('Test Convert to File Node', () => testWorkflows(workflows)); diff --git a/packages/nodes-base/nodes/Files/test/convert_extract.workflow.json b/packages/nodes-base/nodes/Files/test/convert_extract.workflow.json new file mode 100644 index 0000000000..ea5c3a274c --- /dev/null +++ b/packages/nodes-base/nodes/Files/test/convert_extract.workflow.json @@ -0,0 +1,707 @@ +{ + "name": "convert to tests", + "nodes": [ + { + "parameters": {}, + "id": "35cce987-aa4f-4738-bfcd-b85098948341", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 680, + 1100 + ] + }, + { + "parameters": { + "fields": { + "values": [ + { + "name": "row_number", + "type": "numberValue", + "numberValue": "2" + }, + { + "name": "country", + "stringValue": "uk" + }, + { + "name": "browser", + "stringValue": "firefox" + }, + { + "name": "session_duration", + "type": "numberValue", + "numberValue": "1" + }, + { + "name": "visits", + "type": "numberValue", + "numberValue": "1" + } + ] + }, + "include": "none", + "options": {} + }, + "id": "13305747-c966-4f46-90b3-ffff6835b714", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [ + 980, + 880 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "b08b269d-0735-4dc4-b5a7-7870f5e115f9", + "name": "Convert to File", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 420 + ] + }, + { + "parameters": { + "operation": "html", + "options": {} + }, + "id": "c68c209f-771f-4d25-b832-3d55ee97e6ff", + "name": "Convert to File1", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 600 + ] + }, + { + "parameters": { + "operation": "toJson", + "options": {} + }, + "id": "2b6f27ed-a4dc-4a29-905f-f0a921ea6ee7", + "name": "Convert to File2", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 780 + ] + }, + { + "parameters": { + "operation": "toJson", + "mode": "each", + "options": {} + }, + "id": "cd482d12-7547-4eb6-880b-bd2c31ef06d5", + "name": "Convert to File3", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 960 + ] + }, + { + "parameters": { + "operation": "xlsx", + "options": {} + }, + "id": "1fd693e3-4286-49f4-ba45-70584c1d67f7", + "name": "Convert to File5", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 1140 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "636866e9-ca0d-44a3-b27a-90cf33e26485", + "name": "Extract From File", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 420 + ] + }, + { + "parameters": { + "operation": "html", + "options": {} + }, + "id": "5c66b2ea-94b2-4a1b-a34d-acb07fe33e13", + "name": "Extract From File1", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 600 + ] + }, + { + "parameters": { + "operation": "fromJson", + "options": {} + }, + "id": "a03752f0-e3bb-4dd3-9aae-dc4d1471c281", + "name": "Extract From File2", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 780 + ] + }, + { + "parameters": { + "operation": "fromJson", + "options": {} + }, + "id": "eb10c006-60d7-4842-b9e5-a364f42dd1ab", + "name": "Extract From File3", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 960 + ] + }, + { + "parameters": { + "operation": "xlsx", + "options": {} + }, + "id": "64c98172-2a77-4e83-b19b-232899df8113", + "name": "Extract From File4", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 1140 + ] + }, + { + "parameters": { + "operation": "xls", + "options": {} + }, + "id": "edbfd57e-d36b-470d-9e8d-9c7f67c131b4", + "name": "Convert to File6", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 1320 + ] + }, + { + "parameters": { + "operation": "xls", + "options": {} + }, + "id": "86713806-8dc6-45ec-a710-e80b839ec193", + "name": "Extract From File5", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 1320 + ] + }, + { + "parameters": { + "fields": { + "values": [ + { + "name": "base64", + "stringValue": "VGhpcyBpcyB0ZXh0IGNvbnZlcnRlZCB0byBiYXNlIDY0" + } + ] + }, + "include": "none", + "options": {} + }, + "id": "c205d380-2459-4d16-bb56-f862c53c25de", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [ + 980, + 1060 + ] + }, + { + "parameters": { + "operation": "toBinary", + "sourceProperty": "base64", + "options": {} + }, + "id": "3f5fe04c-63cf-47d0-a7ca-d427b95a0c52", + "name": "Convert to File7", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 1880 + ] + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "id": "dbb4eae0-f0ee-4453-aed7-7d8322974c94", + "name": "Extract From File6", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 1880 + ] + }, + { + "parameters": { + "operation": "ods", + "options": {} + }, + "id": "e3bf810d-7795-4663-82d1-768f76adc0d9", + "name": "Convert to File8", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 1520 + ] + }, + { + "parameters": { + "operation": "ods", + "options": {} + }, + "id": "7713d273-2d51-4e3e-8ac9-9a4e6013c690", + "name": "Extract From File7", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 1520 + ] + }, + { + "parameters": { + "operation": "rtf", + "options": {} + }, + "id": "631b29cb-dcde-42cc-a514-45622923dab6", + "name": "Convert to File9", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 1700 + ] + }, + { + "parameters": { + "operation": "rtf", + "options": {} + }, + "id": "168b6586-c89d-4b0e-880e-4ddb8ea7cb2f", + "name": "Extract From File8", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 1700 + ] + }, + { + "parameters": { + "operation": "iCal", + "start": "2024-01-03T00:00:00", + "end": "2024-01-04T00:00:00", + "allDay": true, + "additionalFields": { + "description": "event description" + } + }, + "id": "2ba4acd9-2677-4b25-9379-48433ac5e9cc", + "name": "Convert to File10", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 1380, + 2100 + ] + }, + { + "parameters": { + "operation": "fromIcs", + "options": {} + }, + "id": "c0179d40-7de0-4e42-a6bf-97c5bb764665", + "name": "Extract From File9", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1600, + 2100 + ] + }, + { + "parameters": { + "fields": { + "values": [ + { + "name": "description", + "stringValue": "={{ $json.data.events[0].description }}" + } + ] + }, + "include": "none", + "options": {} + }, + "id": "8ee98090-f3b0-41d5-8122-d27e5559738f", + "name": "Edit Fields2", + "type": "n8n-nodes-base.set", + "typeVersion": 3.2, + "position": [ + 1820, + 2100 + ] + } + ], + "pinData": { + "Extract From File": [ + { + "json": { + "row_number": "2", + "country": "uk", + "browser": "firefox", + "session_duration": "1", + "visits": "1" + } + } + ], + "Extract From File1": [ + { + "json": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + ], + "Extract From File2": [ + { + "json": { + "data": [ + { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + ] + } + } + ], + "Extract From File3": [ + { + "json": { + "data": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + } + ], + "Extract From File4": [ + { + "json": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + ], + "Extract From File5": [ + { + "json": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + ], + "Extract From File7": [ + { + "json": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + ], + "Extract From File6": [ + { + "json": { + "data": "This is text converted to base 64" + } + } + ], + "Extract From File8": [ + { + "json": { + "row_number": 2, + "country": "uk", + "browser": "firefox", + "session_duration": 1, + "visits": 1 + } + } + ], + "Edit Fields2": [ + { + "json": { + "description": "event description" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File10", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Convert to File", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File1", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File2", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File3", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File5", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File6", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File8", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File9", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File": { + "main": [ + [ + { + "node": "Extract From File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File1": { + "main": [ + [ + { + "node": "Extract From File1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File2": { + "main": [ + [ + { + "node": "Extract From File2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File3": { + "main": [ + [ + { + "node": "Extract From File3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File5": { + "main": [ + [ + { + "node": "Extract From File4", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File6": { + "main": [ + [ + { + "node": "Extract From File5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File7": { + "main": [ + [ + { + "node": "Extract From File6", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Convert to File7", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File8": { + "main": [ + [ + { + "node": "Extract From File7", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File9": { + "main": [ + [ + { + "node": "Extract From File8", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File10": { + "main": [ + [ + { + "node": "Extract From File9", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From File9": { + "main": [ + [ + { + "node": "Edit Fields2", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "3287cce3-f02f-45df-9d3b-2f116852c1fb", + "meta": { + "instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363" + }, + "id": "ZzoOtOee7hxaNcmp", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Ftp/Ftp.node.json b/packages/nodes-base/nodes/Ftp/Ftp.node.json index 33da5703c2..076aa4f81e 100644 --- a/packages/nodes-base/nodes/Ftp/Ftp.node.json +++ b/packages/nodes-base/nodes/Ftp/Ftp.node.json @@ -15,7 +15,7 @@ } ] }, - "alias": ["SFTP"], + "alias": ["SFTP", "FTP", "Binary", "File", "Transfer"], "subcategories": { "Core Nodes": ["Files"] } diff --git a/packages/nodes-base/nodes/Ftp/Ftp.node.ts b/packages/nodes-base/nodes/Ftp/Ftp.node.ts index 1ba99cf990..e7d03b1d9f 100644 --- a/packages/nodes-base/nodes/Ftp/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp/Ftp.node.ts @@ -120,7 +120,7 @@ export class Ftp implements INodeType { group: ['input'], version: 1, subtitle: '={{$parameter["protocol"] + ": " + $parameter["operation"]}}', - description: 'Transfers files via FTP or SFTP', + description: 'Transfer files via FTP or SFTP', defaults: { name: 'FTP', color: '#303050', @@ -223,6 +223,7 @@ export class Ftp implements INodeType { type: 'string', default: '', description: 'The file path of the file to delete. Has to contain the full path.', + placeholder: 'e.g. /public/documents/file-to-delete.txt', required: true, }, @@ -273,12 +274,12 @@ export class Ftp implements INodeType { name: 'path', type: 'string', default: '', - placeholder: '/documents/invoice.txt', description: 'The file path of the file to download. Has to contain the full path.', + placeholder: 'e.g. /public/documents/file-to-download.txt', required: true, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', displayOptions: { show: { operation: ['download'], @@ -287,7 +288,7 @@ export class Ftp implements INodeType { name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the output binary field to put the file in', required: true, }, @@ -304,6 +305,7 @@ export class Ftp implements INodeType { name: 'oldPath', type: 'string', default: '', + placeholder: 'e.g. /public/documents/old-file.txt', required: true, }, { @@ -316,6 +318,7 @@ export class Ftp implements INodeType { name: 'newPath', type: 'string', default: '', + placeholder: 'e.g. /public/documents/new-file.txt', required: true, }, { @@ -355,10 +358,11 @@ export class Ftp implements INodeType { type: 'string', default: '', description: 'The file path of the file to upload. Has to contain the full path.', + placeholder: 'e.g. /public/documents/file-to-upload.txt', required: true, }, { - displayName: 'Binary Data', + displayName: 'Binary File', displayOptions: { show: { operation: ['upload'], @@ -371,7 +375,7 @@ export class Ftp implements INodeType { description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', displayOptions: { show: { operation: ['upload'], @@ -381,7 +385,7 @@ export class Ftp implements INodeType { name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the input binary field containing the file to be written', required: true, }, { @@ -411,6 +415,7 @@ export class Ftp implements INodeType { name: 'path', type: 'string', default: '/', + placeholder: 'e.g. /public/folder', description: 'Path of directory to list contents of', required: true, }, diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts index 90a0ad8fa0..6b3f2e4a7f 100644 --- a/packages/nodes-base/nodes/Github/Github.node.ts +++ b/packages/nodes-base/nodes/Github/Github.node.ts @@ -550,7 +550,7 @@ export class Github implements INodeType { // file:create/edit // ---------------------------------- { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -580,7 +580,7 @@ export class Github implements INodeType { description: 'The text content of the file', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -593,7 +593,7 @@ export class Github implements INodeType { }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file', + hint: 'The name of the input binary field containing the file to be written', }, { displayName: 'Commit Message', @@ -699,7 +699,7 @@ export class Github implements INodeType { 'Whether to set the data of the file as binary property instead of returning the raw API response', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -712,8 +712,7 @@ export class Github implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property in which to save the binary data of the received file', + hint: 'The name of the output binary field to put the file in', }, { diff --git a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts index f0fb8bfcd6..ecfece1211 100644 --- a/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts +++ b/packages/nodes-base/nodes/Gitlab/Gitlab.node.ts @@ -1138,7 +1138,7 @@ export class Gitlab implements INodeType { 'Whether to set the data of the file as binary property instead of returning the raw API response', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -1151,8 +1151,7 @@ export class Gitlab implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property in which to save the binary data of the received file', + hint: 'The name of the output binary field to put the file in', }, { displayName: 'Additional Parameters', @@ -1184,7 +1183,7 @@ export class Gitlab implements INodeType { // file:create/edit // ---------------------------------- { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -1214,7 +1213,7 @@ export class Gitlab implements INodeType { description: 'The text content of the file', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -1227,7 +1226,7 @@ export class Gitlab implements INodeType { }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file', + hint: 'The name of the input binary field containing the file to be written', }, { displayName: 'Commit Message', diff --git a/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts b/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts index 37131819ae..18fc055056 100644 --- a/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts +++ b/packages/nodes-base/nodes/Google/Chat/descriptions/MediaDescription.ts @@ -42,7 +42,7 @@ export const mediaFields: INodeProperties[] = [ description: 'Name of the media that is being downloaded', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -53,6 +53,6 @@ export const mediaFields: INodeProperties[] = [ operation: ['download'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, ]; diff --git a/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts b/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts index 755c2c236c..a4378515b9 100644 --- a/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts +++ b/packages/nodes-base/nodes/Google/CloudStorage/ObjectDescription.ts @@ -496,7 +496,7 @@ export const objectFields: INodeProperties[] = [ }, }, { - displayName: 'Use Binary Property', + displayName: 'Use Input Binary Field', name: 'createFromBinary', type: 'boolean', displayOptions: { @@ -510,9 +510,10 @@ export const objectFields: INodeProperties[] = [ description: 'Whether the data for creating a file should come from a binary field', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'createBinaryPropertyName', type: 'string', + hint: 'The name of the input binary field containing the file to be written', displayOptions: { show: { resource: ['object'], @@ -537,9 +538,10 @@ export const objectFields: INodeProperties[] = [ description: 'Content of the file to be uploaded', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['object'], diff --git a/packages/nodes-base/nodes/Google/Drive/v1/GoogleDriveV1.node.ts b/packages/nodes-base/nodes/Google/Drive/v1/GoogleDriveV1.node.ts index c06dc93fa6..9099b12c58 100644 --- a/packages/nodes-base/nodes/Google/Drive/v1/GoogleDriveV1.node.ts +++ b/packages/nodes-base/nodes/Google/Drive/v1/GoogleDriveV1.node.ts @@ -355,7 +355,7 @@ const versionDescription: INodeTypeDescription = { // file:download // ---------------------------------- { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -366,7 +366,7 @@ const versionDescription: INodeTypeDescription = { resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, { displayName: 'Options', @@ -833,7 +833,7 @@ const versionDescription: INodeTypeDescription = { }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -861,7 +861,7 @@ const versionDescription: INodeTypeDescription = { description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -874,8 +874,7 @@ const versionDescription: INodeTypeDescription = { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/Google/Drive/v2/actions/file/download.operation.ts b/packages/nodes-base/nodes/Google/Drive/v2/actions/file/download.operation.ts index 2bad381bbf..0ce6805cb8 100644 --- a/packages/nodes-base/nodes/Google/Drive/v2/actions/file/download.operation.ts +++ b/packages/nodes-base/nodes/Google/Drive/v2/actions/file/download.operation.ts @@ -23,13 +23,13 @@ const properties: INodeProperties[] = [ default: {}, options: [ { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', placeholder: 'e.g. data', default: 'data', description: 'Use this field name in the following nodes, to use the binary file data', - hint: 'The name of the output field to put the binary file data in', + hint: 'The name of the output binary field to put the file in', }, { displayName: 'Google File Conversion', diff --git a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts index 183f0734e8..38ee4bb8ab 100644 --- a/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts +++ b/packages/nodes-base/nodes/Google/Slides/GoogleSlides.node.ts @@ -341,12 +341,12 @@ export class GoogleSlides implements INodeType { description: 'Name of the binary property to which to write the data of the read page', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['page'], diff --git a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts index 7475c23920..7503b0af01 100644 --- a/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/ChannelDescription.ts @@ -545,10 +545,11 @@ export const channelFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', required: true, + hint: 'The name of the input binary field containing the file to be uploaded', displayOptions: { show: { operation: ['uploadBanner'], diff --git a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts index bdb6e8160f..339d20e3f0 100644 --- a/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts +++ b/packages/nodes-base/nodes/Google/YouTube/VideoDescription.ts @@ -107,10 +107,11 @@ export const videoFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', required: true, + hint: 'The name of the input binary field containing the file to be uploaded', displayOptions: { show: { operation: ['upload'], diff --git a/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts index 31d76dda84..540a4af70d 100644 --- a/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts +++ b/packages/nodes-base/nodes/HomeAssistant/CameraProxyDescription.ts @@ -46,7 +46,7 @@ export const cameraProxyFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -57,6 +57,6 @@ export const cameraProxyFields: INodeProperties[] = [ resource: ['cameraProxy'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, ]; diff --git a/packages/nodes-base/nodes/Html/Html.node.ts b/packages/nodes-base/nodes/Html/Html.node.ts index d6b8aee568..10d3e75e9c 100644 --- a/packages/nodes-base/nodes/Html/Html.node.ts +++ b/packages/nodes-base/nodes/Html/Html.node.ts @@ -112,7 +112,7 @@ export class Html implements INodeType { }, }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'dataPropertyName', type: 'string', requiresDataPath: 'single', @@ -124,8 +124,7 @@ export class Html implements INodeType { }, default: 'data', required: true, - description: - 'Name of the binary property in which the HTML to extract the data from can be found', + hint: 'The name of the input binary field containing the file to be extracted', }, { displayName: 'JSON Property', diff --git a/packages/nodes-base/nodes/HtmlExtract/HtmlExtract.node.ts b/packages/nodes-base/nodes/HtmlExtract/HtmlExtract.node.ts index 9161a0d9e9..d62cd05f06 100644 --- a/packages/nodes-base/nodes/HtmlExtract/HtmlExtract.node.ts +++ b/packages/nodes-base/nodes/HtmlExtract/HtmlExtract.node.ts @@ -78,7 +78,7 @@ export class HtmlExtract implements INodeType { description: 'If HTML should be read from binary or JSON data', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'dataPropertyName', type: 'string', displayOptions: { @@ -88,8 +88,7 @@ export class HtmlExtract implements INodeType { }, default: 'data', required: true, - description: - 'Name of the binary property in which the HTML to extract the data from can be found', + hint: 'The name of the input binary field containing the file to be extracted', }, { displayName: 'JSON Property', diff --git a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts index 57b7d5278a..46b3fe61c9 100644 --- a/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V1/HttpRequestV1.node.ts @@ -232,7 +232,7 @@ export class HttpRequestV1 implements INodeType { description: 'Name of the property to which to write the response data', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'dataPropertyName', type: 'string', default: 'data', @@ -242,7 +242,7 @@ export class HttpRequestV1 implements INodeType { responseFormat: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, { @@ -397,7 +397,7 @@ export class HttpRequestV1 implements INodeType { // Body Parameter { - displayName: 'Send Binary Data', + displayName: 'Send Binary File', name: 'sendBinaryData', type: 'boolean', displayOptions: { @@ -414,7 +414,7 @@ export class HttpRequestV1 implements INodeType { description: 'Whether binary data should be send as body', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -428,8 +428,9 @@ export class HttpRequestV1 implements INodeType { requestMethod: ['PATCH', 'POST', 'PUT'], }, }, + hint: 'The name of the input binary field containing the file to be uploaded', description: - 'Name of the binary property which contains the data for the file to be uploaded. For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', + 'For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', }, { displayName: 'Body Parameters', diff --git a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts index 4ccdae8df4..82941b01db 100644 --- a/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V2/HttpRequestV2.node.ts @@ -247,7 +247,7 @@ export class HttpRequestV2 implements INodeType { description: 'Name of the property to which to write the response data', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'dataPropertyName', type: 'string', default: 'data', @@ -257,7 +257,7 @@ export class HttpRequestV2 implements INodeType { responseFormat: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, { @@ -412,7 +412,7 @@ export class HttpRequestV2 implements INodeType { // Body Parameter { - displayName: 'Send Binary Data', + displayName: 'Send Binary File', name: 'sendBinaryData', type: 'boolean', displayOptions: { @@ -429,7 +429,7 @@ export class HttpRequestV2 implements INodeType { description: 'Whether binary data should be send as body', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -443,8 +443,9 @@ export class HttpRequestV2 implements INodeType { requestMethod: ['PATCH', 'POST', 'PUT'], }, }, + hint: 'The name of the input binary field containing the file to be uploaded', description: - 'Name of the binary property which contains the data for the file to be uploaded. For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', + 'For Form-Data Multipart, they can be provided in the format: "sendKey1:binaryProperty1,sendKey2:binaryProperty2', }, { displayName: 'Body Parameters', diff --git a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts index d6247f9c81..fdb304083b 100644 --- a/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts +++ b/packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts @@ -372,7 +372,7 @@ export class HttpRequestV3 implements INodeType { }, { // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - name: 'n8n Binary Data', + name: 'n8n Binary File', value: 'binaryData', }, { @@ -502,7 +502,7 @@ export class HttpRequestV3 implements INodeType { options: [ { // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased - name: 'n8n Binary Data', + name: 'n8n Binary File', value: 'formBinaryData', }, { diff --git a/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts b/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts index f12dc4b4d4..e27350232e 100644 --- a/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts +++ b/packages/nodes-base/nodes/HumanticAI/ProfileDescription.ts @@ -68,7 +68,7 @@ export const profileFields: INodeProperties[] = [ description: 'Whether to send a resume for a resume based analysis', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -79,7 +79,7 @@ export const profileFields: INodeProperties[] = [ sendResume: [true], }, }, - description: 'The resume in PDF or DOCX format', + hint: 'The name of the input binary field containing the resume in PDF or DOCX format', }, /* -------------------------------------------------------------------------- */ @@ -180,7 +180,7 @@ export const profileFields: INodeProperties[] = [ description: 'Additional text written by the user', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -191,6 +191,6 @@ export const profileFields: INodeProperties[] = [ sendResume: [true], }, }, - description: 'The resume in PDF or DOCX format', + hint: 'The name of the input binary field containing the resume in PDF or DOCX format', }, ]; diff --git a/packages/nodes-base/nodes/ICalendar/ICalendar.node.ts b/packages/nodes-base/nodes/ICalendar/ICalendar.node.ts index 7e7550fd7b..f24a3644d7 100644 --- a/packages/nodes-base/nodes/ICalendar/ICalendar.node.ts +++ b/packages/nodes-base/nodes/ICalendar/ICalendar.node.ts @@ -1,21 +1,16 @@ /* eslint-disable n8n-nodes-base/node-filename-against-convention */ -import { promisify } from 'util'; import type { IExecuteFunctions, - IDataObject, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import moment from 'moment-timezone'; - -import * as ics from 'ics'; - -const createEvent = promisify(ics.createEvent); +import * as createEvent from './createEvent.operation'; export class ICalendar implements INodeType { description: INodeTypeDescription = { + hidden: true, displayName: 'iCalendar', name: 'iCal', icon: 'fa:calendar', @@ -44,330 +39,20 @@ export class ICalendar implements INodeType { ], default: 'createEventFile', }, - { - displayName: 'Event Title', - name: 'title', - type: 'string', - default: '', - }, - { - displayName: 'Start', - name: 'start', - type: 'dateTime', - default: '', - required: true, - description: - 'Date and time at which the event begins. (For all-day events, the time will be ignored.).', - }, - { - displayName: 'End', - name: 'end', - type: 'dateTime', - default: '', - required: true, - description: - 'Date and time at which the event ends. (For all-day events, the time will be ignored.).', - }, - { - displayName: 'All Day', - name: 'allDay', - type: 'boolean', - default: false, - description: 'Whether the event lasts all day or not', - }, - { - displayName: 'Binary Property', - name: 'binaryPropertyName', - type: 'string', - default: 'data', - required: true, - description: 'The field that your iCalendar file will be available under in the output', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - operation: ['createEventFile'], - }, - }, - options: [ - { - displayName: 'Attendees', - name: 'attendeesUi', - type: 'fixedCollection', - typeOptions: { - multipleValues: true, - }, - placeholder: 'Add Attendee', - default: {}, - options: [ - { - displayName: 'Attendees', - name: 'attendeeValues', - values: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - required: true, - default: '', - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - placeholder: 'name@email.com', - required: true, - default: '', - }, - { - displayName: 'RSVP', - name: 'rsvp', - type: 'boolean', - default: false, - description: 'Whether the attendee has to confirm attendance or not', - }, - ], - }, - ], - }, - { - displayName: 'Busy Status', - name: 'busyStatus', - type: 'options', - options: [ - { - name: 'Busy', - value: 'BUSY', - }, - { - name: 'Tentative', - value: 'TENTATIVE', - }, - ], - default: '', - description: 'Used to specify busy status for Microsoft applications, like Outlook', - }, - { - displayName: 'Calendar Name', - name: 'calName', - type: 'string', - default: '', - description: - 'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook (spec).', - }, - { - displayName: 'Description', - name: 'description', - type: 'string', - default: '', - }, - { - displayName: 'File Name', - name: 'fileName', - type: 'string', - default: '', - description: 'The name of the file to be generated. Default value is event.ics.', - }, - { - displayName: 'Geolocation', - name: 'geolocationUi', - type: 'fixedCollection', - typeOptions: { - multipleValues: false, - }, - placeholder: 'Add Geolocation', - default: {}, - options: [ - { - displayName: 'Geolocation', - name: 'geolocationValues', - values: [ - { - displayName: 'Latitude', - name: 'lat', - type: 'string', - default: '', - }, - { - displayName: 'Longitude', - name: 'lon', - type: 'string', - default: '', - }, - ], - }, - ], - }, - { - displayName: 'Location', - name: 'location', - type: 'string', - default: '', - description: 'The intended venue', - }, - { - displayName: 'Recurrence Rule', - name: 'recurrenceRule', - type: 'string', - default: '', - description: - 'A rule to define the repeat pattern of the event (RRULE). (Rule generator).', - }, - { - displayName: 'Organizer', - name: 'organizerUi', - type: 'fixedCollection', - typeOptions: { - multipleValues: false, - }, - placeholder: 'Add Organizer', - default: {}, - options: [ - { - displayName: 'Organizer', - name: 'organizerValues', - values: [ - { - displayName: 'Name', - name: 'name', - type: 'string', - default: '', - required: true, - }, - { - displayName: 'Email', - name: 'email', - type: 'string', - placeholder: 'name@email.com', - default: '', - required: true, - }, - ], - }, - ], - }, - { - displayName: 'Sequence', - name: 'sequence', - type: 'number', - default: 0, - description: - 'When sending an update for an event (with the same uid), defines the revision sequence number', - }, - { - displayName: 'Status', - name: 'status', - type: 'options', - options: [ - { - name: 'Confirmed', - value: 'CONFIRMED', - }, - { - name: 'Cancelled', - value: 'CANCELLED', - }, - { - name: 'Tentative', - value: 'TENTATIVE', - }, - ], - default: 'CONFIRMED', - }, - { - displayName: 'UID', - name: 'uid', - type: 'string', - default: '', - description: - 'Universally unique ID for the event (will be auto-generated if not specified here). Should be globally unique.', - }, - { - displayName: 'URL', - name: 'url', - type: 'string', - default: '', - description: 'URL associated with event', - }, - ], - }, + ...createEvent.description, ], }; - async execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions) { const items = this.getInputData(); - const length = items.length; - const returnData: INodeExecutionData[] = []; const operation = this.getNodeParameter('operation', 0); + + let returnData: INodeExecutionData[] = []; + if (operation === 'createEventFile') { - for (let i = 0; i < length; i++) { - const title = this.getNodeParameter('title', i) as string; - const allDay = this.getNodeParameter('allDay', i) as boolean; - const start = this.getNodeParameter('start', i) as string; - let end = this.getNodeParameter('end', i) as string; - end = allDay ? moment(end).utc().add(1, 'day').format() : end; - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); - const additionalFields = this.getNodeParameter('additionalFields', i); - let fileName = 'event.ics'; - - const eventStart = moment(start) - .toArray() - .splice(0, allDay ? 3 : 6) as ics.DateArray; - eventStart[1]++; - const eventEnd = moment(end) - .toArray() - .splice(0, allDay ? 3 : 6) as ics.DateArray; - eventEnd[1]++; - - if (additionalFields.fileName) { - fileName = additionalFields.fileName as string; - } - - const data: ics.EventAttributes = { - title, - start: eventStart, - end: eventEnd, - startInputType: 'utc', - endInputType: 'utc', - }; - - if (additionalFields.geolocationUi) { - data.geo = (additionalFields.geolocationUi as IDataObject) - .geolocationValues as ics.GeoCoordinates; - delete additionalFields.geolocationUi; - } - - if (additionalFields.organizerUi) { - data.organizer = (additionalFields.organizerUi as IDataObject) - .organizerValues as ics.Person; - delete additionalFields.organizerUi; - } - - if (additionalFields.attendeesUi) { - data.attendees = (additionalFields.attendeesUi as IDataObject) - .attendeeValues as ics.Attendee[]; - delete additionalFields.attendeesUi; - } - - Object.assign(data, additionalFields); - const buffer = Buffer.from((await createEvent(data)) as string); - const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar'); - returnData.push({ - json: {}, - binary: { - [binaryPropertyName]: binaryData, - }, - pairedItem: { - item: i, - }, - }); - } + returnData = await createEvent.execute.call(this, items); } + return [returnData]; } } diff --git a/packages/nodes-base/nodes/ICalendar/createEvent.operation.ts b/packages/nodes-base/nodes/ICalendar/createEvent.operation.ts new file mode 100644 index 0000000000..22f263a8c1 --- /dev/null +++ b/packages/nodes-base/nodes/ICalendar/createEvent.operation.ts @@ -0,0 +1,376 @@ +import { + type IExecuteFunctions, + type IDataObject, + type INodeExecutionData, + type INodeProperties, + NodeOperationError, +} from 'n8n-workflow'; + +import moment from 'moment-timezone'; +import * as ics from 'ics'; +import { promisify } from 'util'; + +const createEvent = promisify(ics.createEvent); + +export const description: INodeProperties[] = [ + { + displayName: 'Event Title', + name: 'title', + type: 'string', + default: '', + placeholder: 'e.g. New Event', + }, + { + displayName: 'Start', + name: 'start', + type: 'dateTime', + default: '', + required: true, + description: + 'Date and time at which the event begins. (For all-day events, the time will be ignored.).', + validateType: 'dateTime', + }, + { + displayName: 'End', + name: 'end', + type: 'dateTime', + default: '', + required: true, + description: + 'Date and time at which the event ends. (For all-day events, the time will be ignored.).', + hint: 'If not set, will be equal to the start date', + }, + { + displayName: 'All Day', + name: 'allDay', + type: 'boolean', + default: false, + description: 'Whether the event lasts all day or not', + }, + { + displayName: 'Put Output File in Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + hint: 'The name of the output binary field to put the file in', + description: 'The field that your iCalendar file will be available under in the output', + }, + { + displayName: 'Options', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Attendees', + name: 'attendeesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attendee', + default: {}, + options: [ + { + displayName: 'Attendees', + name: 'attendeeValues', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + required: true, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'e.g. name@email.com', + required: true, + default: '', + }, + { + displayName: 'RSVP', + name: 'rsvp', + type: 'boolean', + default: false, + description: 'Whether the attendee has to confirm attendance or not', + }, + ], + }, + ], + }, + { + displayName: 'Busy Status', + name: 'busyStatus', + type: 'options', + options: [ + { + name: 'Busy', + value: 'BUSY', + }, + { + name: 'Tentative', + value: 'TENTATIVE', + }, + ], + default: '', + description: 'Used to specify busy status for Microsoft applications, like Outlook', + }, + { + displayName: 'Calendar Name', + name: 'calName', + type: 'string', + default: '', + description: + 'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook. More info.', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + placeholder: 'e.g. event.ics', + description: 'The name of the file to be generated. Default name is event.ics.', + }, + { + displayName: 'Geolocation', + name: 'geolocationUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Geolocation', + default: {}, + options: [ + { + displayName: 'Geolocation', + name: 'geolocationValues', + values: [ + { + displayName: 'Latitude', + name: 'lat', + type: 'string', + default: '', + }, + { + displayName: 'Longitude', + name: 'lon', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'The intended venue', + }, + { + displayName: 'Recurrence Rule', + name: 'recurrenceRule', + type: 'string', + default: '', + description: + 'A rule to define the repeat pattern of the event (RRULE). (Rule generator).', + }, + { + displayName: 'Organizer', + name: 'organizerUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + placeholder: 'Add Organizer', + default: {}, + options: [ + { + displayName: 'Organizer', + name: 'organizerValues', + values: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'e.g. name@email.com', + default: '', + required: true, + }, + ], + }, + ], + }, + { + displayName: 'Sequence', + name: 'sequence', + type: 'number', + default: 0, + description: + 'When sending an update for an event (with the same uid), defines the revision sequence number', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Confirmed', + value: 'CONFIRMED', + }, + { + name: 'Cancelled', + value: 'CANCELLED', + }, + { + name: 'Tentative', + value: 'TENTATIVE', + }, + ], + default: 'CONFIRMED', + }, + { + displayName: 'UID', + name: 'uid', + type: 'string', + default: '', + description: + 'Universally unique ID for the event (will be auto-generated if not specified here). Should be globally unique.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + default: '', + description: 'URL associated with event', + }, + { + displayName: 'Use Workflow Timezone', + name: 'useWorkflowTimezone', + type: 'boolean', + default: false, + description: "Whether to use the workflow timezone set in node's settings rather than UTC", + }, + ], + }, +]; + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + const workflowTimezone = this.getTimezone(); + + for (let i = 0; i < items.length; i++) { + try { + const title = this.getNodeParameter('title', i) as string; + const allDay = this.getNodeParameter('allDay', i) as boolean; + + let start = this.getNodeParameter('start', i) as string; + let end = this.getNodeParameter('end', i) as string; + + if (!end) { + end = start; + } + + end = allDay ? moment(end).utc().add(1, 'day').format() : end; + + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); + const options = this.getNodeParameter('additionalFields', i); + + if (options.useWorkflowTimezone) { + start = moment(start).tz(workflowTimezone).format(); + end = moment(end).tz(workflowTimezone).format(); + delete options.useWorkflowTimezone; + } + + let fileName = 'event.ics'; + + const eventStart = moment(start) + .toArray() + .splice(0, allDay ? 3 : 6) as ics.DateArray; + eventStart[1]++; + + const eventEnd = moment(end) + .toArray() + .splice(0, allDay ? 3 : 6) as ics.DateArray; + eventEnd[1]++; + + if (options.fileName) { + fileName = options.fileName as string; + } + + const data: ics.EventAttributes = { + title, + start: eventStart, + end: eventEnd, + startInputType: 'utc', + endInputType: 'utc', + }; + + if (options.geolocationUi) { + data.geo = (options.geolocationUi as IDataObject).geolocationValues as ics.GeoCoordinates; + delete options.geolocationUi; + } + + if (options.organizerUi) { + data.organizer = (options.organizerUi as IDataObject).organizerValues as ics.Person; + delete options.organizerUi; + } + + if (options.attendeesUi) { + data.attendees = (options.attendeesUi as IDataObject).attendeeValues as ics.Attendee[]; + delete options.attendeesUi; + } + + Object.assign(data, options); + const buffer = Buffer.from((await createEvent(data)) as string); + const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar'); + returnData.push({ + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem: { + item: i, + }, + }); + } catch (error) { + const errorDescription = error.description; + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { + itemIndex: i, + description: errorDescription, + }); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts index c0b1719033..d46a86235b 100644 --- a/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts +++ b/packages/nodes-base/nodes/Jira/IssueAttachmentDescription.ts @@ -59,7 +59,7 @@ export const issueAttachmentFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', displayOptions: { show: { resource: ['issueAttachment'], @@ -69,7 +69,7 @@ export const issueAttachmentFields: INodeProperties[] = [ name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the input binary field containing the file to be written', required: true, }, @@ -104,7 +104,7 @@ export const issueAttachmentFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', default: 'data', @@ -115,7 +115,7 @@ export const issueAttachmentFields: INodeProperties[] = [ download: [true], }, }, - description: 'Object property name which holds binary data', + hint: 'The name of the output binary field to put the file in', required: true, }, /* -------------------------------------------------------------------------- */ @@ -179,7 +179,7 @@ export const issueAttachmentFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', default: 'data', @@ -190,7 +190,7 @@ export const issueAttachmentFields: INodeProperties[] = [ download: [true], }, }, - description: 'Object property name which holds binary data', + hint: 'The name of the output binary field to put the file in', required: true, }, /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Keap/FileDescription.ts b/packages/nodes-base/nodes/Keap/FileDescription.ts index 7a9cdb1239..6471c1e5dc 100644 --- a/packages/nodes-base/nodes/Keap/FileDescription.ts +++ b/packages/nodes-base/nodes/Keap/FileDescription.ts @@ -40,7 +40,7 @@ export const fileFields: INodeProperties[] = [ /* file:upload */ /* -------------------------------------------------------------------------- */ { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -53,7 +53,7 @@ export const fileFields: INodeProperties[] = [ description: 'Whether the data to upload should be taken from binary field', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -65,7 +65,7 @@ export const fileFields: INodeProperties[] = [ binaryData: [true], }, }, - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'File Association', diff --git a/packages/nodes-base/nodes/KoBoToolbox/FileDescription.ts b/packages/nodes-base/nodes/KoBoToolbox/FileDescription.ts index 8e15042e72..868d1538d7 100644 --- a/packages/nodes-base/nodes/KoBoToolbox/FileDescription.ts +++ b/packages/nodes-base/nodes/KoBoToolbox/FileDescription.ts @@ -121,7 +121,7 @@ export const fileFields: INodeProperties[] = [ }, options: [ { - name: 'Binary Data', + name: 'Binary File', value: 'binary', }, { diff --git a/packages/nodes-base/nodes/Line/NotificationDescription.ts b/packages/nodes-base/nodes/Line/NotificationDescription.ts index 289a01ceb2..b4ce909de0 100644 --- a/packages/nodes-base/nodes/Line/NotificationDescription.ts +++ b/packages/nodes-base/nodes/Line/NotificationDescription.ts @@ -68,7 +68,7 @@ export const notificationFields: INodeProperties[] = [ displayName: 'Image', values: [ { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -98,7 +98,7 @@ export const notificationFields: INodeProperties[] = [ description: 'HTTP/HTTPS URL. Maximum size of 240×240px JPEG.', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', displayOptions: { @@ -107,7 +107,7 @@ export const notificationFields: INodeProperties[] = [ }, }, default: 'data', - description: 'Name of the property that holds the binary data', + hint: 'The name of the input binary field containing the file to be written', }, ], }, diff --git a/packages/nodes-base/nodes/LinkedIn/PostDescription.ts b/packages/nodes-base/nodes/LinkedIn/PostDescription.ts index 9a4ebbaf04..8f2512f09f 100644 --- a/packages/nodes-base/nodes/LinkedIn/PostDescription.ts +++ b/packages/nodes-base/nodes/LinkedIn/PostDescription.ts @@ -121,7 +121,7 @@ export const postFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', displayOptions: { show: { operation: ['create'], @@ -132,7 +132,7 @@ export const postFields: INodeProperties[] = [ name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the input binary field containing the file to be written', required: true, }, { @@ -173,11 +173,11 @@ export const postFields: INodeProperties[] = [ }, }, { - displayName: 'Thumbnail Binary Property', + displayName: 'Input Binary Field', name: 'thumbnailBinaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data for the article thumbnail', + hint: 'The name of the input binary field containing the file for the article thumbnail', displayOptions: { show: { '/shareMediaCategory': ['ARTICLE'], diff --git a/packages/nodes-base/nodes/Matrix/MediaDescription.ts b/packages/nodes-base/nodes/Matrix/MediaDescription.ts index 38e78557a2..23f6ddeb0b 100644 --- a/packages/nodes-base/nodes/Matrix/MediaDescription.ts +++ b/packages/nodes-base/nodes/Matrix/MediaDescription.ts @@ -46,11 +46,12 @@ export const mediaFields: INodeProperties[] = [ required: true, }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, + hint: 'The name of the input binary field containing the file to be uploaded', displayOptions: { show: { resource: ['media'], diff --git a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts index 128c25231e..76deccbad3 100644 --- a/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Excel/v2/actions/versionDescription.ts @@ -27,7 +27,7 @@ export const versionDescription: INodeTypeDescription = { properties: [ { displayName: - 'This node connects to the Microsoft 365 cloud platform. Use the \'Spreadsheet File\' node to directly manipulate spreadsheet files (.xls, .csv, etc). More info.', + 'This node connects to the Microsoft 365 cloud platform. Use the \'Extract From File\' and \'Convert to File\' nodes to directly manipulate spreadsheet files (.xls, .csv, etc). More info.', name: 'notice', type: 'notice', default: '', diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts index 93b1dbcefd..80e6a71bc5 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/FileDescription.ts @@ -209,7 +209,7 @@ export const fileFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -220,7 +220,7 @@ export const fileFields: INodeProperties[] = [ resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, /* -------------------------------------------------------------------------- */ /* file:get */ @@ -380,7 +380,7 @@ export const fileFields: INodeProperties[] = [ description: 'ID of the parent folder that will contain the file', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -410,7 +410,7 @@ export const fileFields: INodeProperties[] = [ description: 'The text content of the file', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -423,6 +423,6 @@ export const fileFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file', + hint: 'The name of the input binary field containing the file to be written', }, ]; diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts index 7669c2d890..b374a7c1f7 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageAttachmentDescription.ts @@ -106,9 +106,9 @@ export const messageAttachmentFields: INodeProperties[] = [ // File operations { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', type: 'string', required: true, default: 'data', diff --git a/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts index fa5b5d53e5..a490489c72 100644 --- a/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts +++ b/packages/nodes-base/nodes/Microsoft/Outlook/v1/MessageDescription.ts @@ -559,9 +559,9 @@ export const messageFields: INodeProperties[] = [ // File operations { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', type: 'string', required: true, default: 'data', diff --git a/packages/nodes-base/nodes/Mindee/Mindee.node.ts b/packages/nodes-base/nodes/Mindee/Mindee.node.ts index d42b1f8f8c..e6fefcbd1b 100644 --- a/packages/nodes-base/nodes/Mindee/Mindee.node.ts +++ b/packages/nodes-base/nodes/Mindee/Mindee.node.ts @@ -156,7 +156,7 @@ export class Mindee implements INodeType { default: 'predict', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -167,8 +167,7 @@ export class Mindee implements INodeType { resource: ['receipt', 'invoice'], }, }, - description: - 'Name of the binary property which containsthe data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'RAW Data', diff --git a/packages/nodes-base/nodes/MoveBinaryData/MoveBinaryData.node.ts b/packages/nodes-base/nodes/MoveBinaryData/MoveBinaryData.node.ts index 1081848431..e4c536250a 100644 --- a/packages/nodes-base/nodes/MoveBinaryData/MoveBinaryData.node.ts +++ b/packages/nodes-base/nodes/MoveBinaryData/MoveBinaryData.node.ts @@ -42,6 +42,7 @@ encodeDecodeOptions.sort((a, b) => { export class MoveBinaryData implements INodeType { description: INodeTypeDescription = { + hidden: true, displayName: 'Convert to/from binary data', name: 'moveBinaryData', icon: 'fa:exchange-alt', @@ -179,6 +180,20 @@ export class MoveBinaryData implements INodeType { placeholder: 'Add Option', default: {}, options: [ + { + displayName: 'Add Byte Order Mark (BOM)', + name: 'addBOM', + description: + 'Whether to add special marker at the start of your text file. This marker helps some programs understand how to read the file correctly.', + displayOptions: { + show: { + '/mode': ['jsonToBinary'], + encoding: bomAware, + }, + }, + type: 'boolean', + default: false, + }, { displayName: 'Data Is Base64', name: 'dataIsBase64', @@ -206,7 +221,7 @@ export class MoveBinaryData implements INodeType { }, }, default: 'utf8', - description: 'Set the encoding of the data stream', + description: 'Choose the character set to use to encode the data', }, { displayName: 'Strip BOM', @@ -220,18 +235,6 @@ export class MoveBinaryData implements INodeType { type: 'boolean', default: true, }, - { - displayName: 'Add BOM', - name: 'addBOM', - displayOptions: { - show: { - '/mode': ['jsonToBinary'], - encoding: bomAware, - }, - }, - type: 'boolean', - default: false, - }, { displayName: 'File Name', name: 'fileName', diff --git a/packages/nodes-base/nodes/Nasa/Nasa.node.ts b/packages/nodes-base/nodes/Nasa/Nasa.node.ts index e9fa2a8d4c..14e601d99a 100644 --- a/packages/nodes-base/nodes/Nasa/Nasa.node.ts +++ b/packages/nodes-base/nodes/Nasa/Nasa.node.ts @@ -570,7 +570,7 @@ export class Nasa implements INodeType { 'By default just the URL of the image is returned. When set to true the image will be downloaded.', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -582,7 +582,7 @@ export class Nasa implements INodeType { download: [true], }, }, - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', }, /* date for astronomyPictureOfTheDay */ @@ -766,7 +766,7 @@ export class Nasa implements INodeType { }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -777,7 +777,7 @@ export class Nasa implements INodeType { resource: ['earthImagery'], }, }, - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', }, //aqui diff --git a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts index bd2190e82f..510dffbc0e 100644 --- a/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts +++ b/packages/nodes-base/nodes/NextCloud/NextCloud.node.ts @@ -350,7 +350,7 @@ export class NextCloud implements INodeType { 'The file path of the file to download. Has to contain the full path. The path should start with "/".', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -361,7 +361,7 @@ export class NextCloud implements INodeType { resource: ['file'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, // ---------------------------------- @@ -384,7 +384,7 @@ export class NextCloud implements INodeType { 'The absolute file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', }, { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryDataUpload', type: 'boolean', default: false, @@ -412,7 +412,7 @@ export class NextCloud implements INodeType { description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -425,8 +425,7 @@ export class NextCloud implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts index 1c73d3fa6a..0d1b7ec743 100644 --- a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts +++ b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts @@ -495,7 +495,7 @@ export const operationFields: INodeProperties[] = [ default: '', }, { - displayName: 'Is Binary Data', + displayName: 'Is Binary File', name: 'binaryData', type: 'boolean', default: false, diff --git a/packages/nodes-base/nodes/OpenAi/ImageDescription.ts b/packages/nodes-base/nodes/OpenAi/ImageDescription.ts index 15dc95147a..19f000d63e 100644 --- a/packages/nodes-base/nodes/OpenAi/ImageDescription.ts +++ b/packages/nodes-base/nodes/OpenAi/ImageDescription.ts @@ -137,7 +137,7 @@ const createOperations: INodeProperties[] = [ }, options: [ { - name: 'Binary Data', + name: 'Binary File', value: 'binaryData', }, { diff --git a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts index e3fe26861e..a3ed0da589 100644 --- a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts @@ -1890,7 +1890,7 @@ export class Pipedrive implements INodeType { // file:create // ---------------------------------- { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -1902,8 +1902,7 @@ export class Pipedrive implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be created', + hint: 'The name of the input binary field containing the file to be written', }, { displayName: 'Additional Fields', @@ -1996,7 +1995,7 @@ export class Pipedrive implements INodeType { description: 'ID of the file to download', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -2007,8 +2006,7 @@ export class Pipedrive implements INodeType { resource: ['file'], }, }, - description: - 'Name of the binary property to which to write the data of the downloaded file', + hint: 'The name of the output binary field to put the file in', }, // ---------------------------------- diff --git a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts index 99b975dcc5..ff4d4770c2 100644 --- a/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts +++ b/packages/nodes-base/nodes/Pushbullet/Pushbullet.node.ts @@ -156,7 +156,7 @@ export class Pushbullet implements INodeType { description: 'URL of the push', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -169,8 +169,7 @@ export class Pushbullet implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be created', + hint: 'The name of the input binary field containing the file to be written', }, { displayName: 'Target', diff --git a/packages/nodes-base/nodes/Pushover/Pushover.node.ts b/packages/nodes-base/nodes/Pushover/Pushover.node.ts index ed3774a5dc..fab8ecdf80 100644 --- a/packages/nodes-base/nodes/Pushover/Pushover.node.ts +++ b/packages/nodes-base/nodes/Pushover/Pushover.node.ts @@ -196,13 +196,12 @@ export class Pushover implements INodeType { displayName: 'Attachment Property', values: [ { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: '', placeholder: 'data', - description: - 'Name of the binary properties which contain data which should be added to email as attachment', + hint: 'The name of the input binary field containing the file which should be added to email as attachment', }, ], }, diff --git a/packages/nodes-base/nodes/QuickBase/FileDescription.ts b/packages/nodes-base/nodes/QuickBase/FileDescription.ts index 343f23cdfe..89e7716c3a 100644 --- a/packages/nodes-base/nodes/QuickBase/FileDescription.ts +++ b/packages/nodes-base/nodes/QuickBase/FileDescription.ts @@ -90,7 +90,7 @@ export const fileFields: INodeProperties[] = [ description: 'The file attachment version number', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', displayOptions: { show: { resource: ['file'], @@ -100,7 +100,7 @@ export const fileFields: INodeProperties[] = [ name: 'binaryPropertyName', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + hint: 'The name of the input binary field containing the file to be written', required: true, }, ]; diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts index b286dfeb04..6c19bd7743 100644 --- a/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Estimate/EstimateDescription.ts @@ -210,12 +210,12 @@ export const estimateFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['estimate'], diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts index 0e9c7c0e27..d15cd417f9 100644 --- a/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Invoice/InvoiceDescription.ts @@ -215,12 +215,12 @@ export const invoiceFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['invoice'], diff --git a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts index ff338841bc..faeec56d4a 100644 --- a/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts +++ b/packages/nodes-base/nodes/QuickBooks/descriptions/Payment/PaymentDescription.ts @@ -154,12 +154,12 @@ export const paymentFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['payment'], diff --git a/packages/nodes-base/nodes/ReadBinaryFiles/ReadBinaryFiles.node.ts b/packages/nodes-base/nodes/ReadBinaryFiles/ReadBinaryFiles.node.ts index 1368de2066..a1f0939c9e 100644 --- a/packages/nodes-base/nodes/ReadBinaryFiles/ReadBinaryFiles.node.ts +++ b/packages/nodes-base/nodes/ReadBinaryFiles/ReadBinaryFiles.node.ts @@ -10,6 +10,7 @@ import { generatePairedItemData } from '../../utils/utilities'; export class ReadBinaryFiles implements INodeType { description: INodeTypeDescription = { + hidden: true, displayName: 'Read Binary Files', name: 'readBinaryFiles', icon: 'fa:file-import', diff --git a/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts b/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts index b638862970..ff882dfca5 100644 --- a/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts +++ b/packages/nodes-base/nodes/ReadPdf/ReadPDF.node.ts @@ -1,35 +1,15 @@ import { - BINARY_ENCODING, + NodeOperationError, type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription, } from 'n8n-workflow'; - -import { getDocument as readPDF, version as pdfJsVersion } from 'pdfjs-dist'; - -type Document = Awaited>['promise']>; -type Page = Awaited>>; -type TextContent = Awaited>; - -const parseText = (textContent: TextContent) => { - let lastY = undefined; - const text = []; - for (const item of textContent.items) { - if ('str' in item) { - if (lastY == item.transform[5] || !lastY) { - text.push(item.str); - } else { - text.push(`\n${item.str}`); - } - lastY = item.transform[5]; - } - } - return text.join(''); -}; +import { extractDataFromPDF } from '@utils/binary'; export class ReadPDF implements INodeType { description: INodeTypeDescription = { + hidden: true, displayName: 'Read PDF', // eslint-disable-next-line n8n-nodes-base/node-class-description-name-miscased name: 'readPDF', @@ -45,7 +25,7 @@ export class ReadPDF implements INodeType { outputs: ['main'], properties: [ { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -84,46 +64,26 @@ export class ReadPDF implements INodeType { for (let itemIndex = 0; itemIndex < length; itemIndex++) { try { const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex); - const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName); - - const params: { password?: string; url?: URL; data?: ArrayBuffer } = {}; + let password; if (this.getNodeParameter('encrypted', itemIndex) === true) { - params.password = this.getNodeParameter('password', itemIndex) as string; + password = this.getNodeParameter('password', itemIndex) as string; } - if (binaryData.id) { - const binaryPath = this.helpers.getBinaryPath(binaryData.id); - params.url = new URL(`file://${binaryPath}`); - } else { - params.data = Buffer.from(binaryData.data, BINARY_ENCODING).buffer; - } - - const document = await readPDF(params).promise; - const { info, metadata } = await document - .getMetadata() - .catch(() => ({ info: null, metadata: null })); - - const pages = []; - for (let i = 1; i <= document.numPages; i++) { - const page = await document.getPage(i); - const text = await page.getTextContent().then(parseText); - pages.push(text); - } + const json = await extractDataFromPDF.call( + this, + binaryPropertyName, + password, + undefined, + undefined, + itemIndex, + ); returnData.push({ binary: items[itemIndex].binary, - json: { - numpages: document.numPages, - numrender: document.numPages, - info, - metadata: metadata?.getAll(), - text: pages.join('\n\n'), - version: pdfJsVersion, - }, + json, }); } catch (error) { - console.log(error); if (this.continueOnFail()) { returnData.push({ json: { @@ -135,7 +95,7 @@ export class ReadPDF implements INodeType { }); continue; } - throw error; + throw new NodeOperationError(this.getNode(), error, { itemIndex }); } } return [returnData]; diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts index d26a97d68b..0a6c17968c 100644 --- a/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/AttachmentDescription.ts @@ -86,7 +86,7 @@ export const attachmentFields: INodeProperties[] = [ 'Required. Name of the attached file. Maximum size is 255 characters. Label is File Name.', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -98,7 +98,7 @@ export const attachmentFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Additional Fields', @@ -173,13 +173,12 @@ export const attachmentFields: INodeProperties[] = [ }, options: [ { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Description', diff --git a/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts b/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts index b2232fc3bd..20b4062a47 100644 --- a/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts +++ b/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts @@ -42,7 +42,7 @@ export const documentFields: INodeProperties[] = [ description: 'Name of the file', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -54,7 +54,7 @@ export const documentFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Additional Fields', diff --git a/packages/nodes-base/nodes/SecurityScorecard/descriptions/ReportDescription.ts b/packages/nodes-base/nodes/SecurityScorecard/descriptions/ReportDescription.ts index 805e6a7d7c..f856770225 100644 --- a/packages/nodes-base/nodes/SecurityScorecard/descriptions/ReportDescription.ts +++ b/packages/nodes-base/nodes/SecurityScorecard/descriptions/ReportDescription.ts @@ -299,7 +299,7 @@ export const reportFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', required: true, @@ -310,6 +310,6 @@ export const reportFields: INodeProperties[] = [ operation: ['download'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, ]; diff --git a/packages/nodes-base/nodes/Set/Set.node.json b/packages/nodes-base/nodes/Set/Set.node.json index ec62590cae..d6f5a78a97 100644 --- a/packages/nodes-base/nodes/Set/Set.node.json +++ b/packages/nodes-base/nodes/Set/Set.node.json @@ -121,7 +121,7 @@ } ] }, - "alias": ["Set", "JSON", "Filter", "Transform", "Map"], + "alias": ["Set", "JS", "JSON", "Filter", "Transform", "Map"], "subcategories": { "Core Nodes": ["Data Transformation"] } diff --git a/packages/nodes-base/nodes/Set/v2/SetV2.node.ts b/packages/nodes-base/nodes/Set/v2/SetV2.node.ts index 6bf13b9547..1076c0d2d1 100644 --- a/packages/nodes-base/nodes/Set/v2/SetV2.node.ts +++ b/packages/nodes-base/nodes/Set/v2/SetV2.node.ts @@ -157,7 +157,7 @@ const versionDescription: INodeTypeDescription = { default: {}, options: [ { - displayName: 'Include Binary Data', + displayName: 'Include Binary File', name: 'includeBinary', type: 'boolean', default: true, diff --git a/packages/nodes-base/nodes/Slack/V1/FileDescription.ts b/packages/nodes-base/nodes/Slack/V1/FileDescription.ts index e22ad88882..57ffb0ac1a 100644 --- a/packages/nodes-base/nodes/Slack/V1/FileDescription.ts +++ b/packages/nodes-base/nodes/Slack/V1/FileDescription.ts @@ -40,7 +40,7 @@ export const fileFields: INodeProperties[] = [ /* file:upload */ /* -------------------------------------------------------------------------- */ { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -68,7 +68,7 @@ export const fileFields: INodeProperties[] = [ description: 'The text content of the file to upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -81,7 +81,7 @@ export const fileFields: INodeProperties[] = [ }, }, placeholder: '', - description: 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Options', diff --git a/packages/nodes-base/nodes/Slack/V2/FileDescription.ts b/packages/nodes-base/nodes/Slack/V2/FileDescription.ts index fdd1aaf9be..aecf8e2bfb 100644 --- a/packages/nodes-base/nodes/Slack/V2/FileDescription.ts +++ b/packages/nodes-base/nodes/Slack/V2/FileDescription.ts @@ -39,7 +39,7 @@ export const fileFields: INodeProperties[] = [ /* file:upload */ /* -------------------------------------------------------------------------- */ { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -66,7 +66,7 @@ export const fileFields: INodeProperties[] = [ placeholder: '', }, { - displayName: 'Binary Property', + displayName: 'File Property', name: 'binaryPropertyName', type: 'string', default: 'data', diff --git a/packages/nodes-base/nodes/SpreadsheetFile/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile/SpreadsheetFile.node.ts index e59692b423..d1333c7a25 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/SpreadsheetFile.node.ts @@ -7,6 +7,7 @@ import { SpreadsheetFileV2 } from './v2/SpreadsheetFileV2.node'; export class SpreadsheetFile extends VersionedNodeType { constructor() { const baseDescription: INodeTypeBaseDescription = { + hidden: true, displayName: 'Spreadsheet File', name: 'spreadsheetFile', icon: 'fa:table', diff --git a/packages/nodes-base/nodes/SpreadsheetFile/description.ts b/packages/nodes-base/nodes/SpreadsheetFile/description.ts index 903245fc61..78c8952b90 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/description.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/description.ts @@ -1,97 +1,41 @@ import type { INodeProperties } from 'n8n-workflow'; -export const operationProperties: INodeProperties[] = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Read From File', - value: 'fromFile', - description: 'Reads data from a spreadsheet file', - action: 'Read data from a spreadsheet file', - }, - { - name: 'Write to File', - value: 'toFile', - description: 'Writes the workflow data to a spreadsheet file', - action: 'Write data to a spreadsheet file', - }, - ], - default: 'fromFile', - }, -]; - -export const fromFileProperties: INodeProperties[] = [ - { - displayName: 'Binary Property', - name: 'binaryPropertyName', - type: 'string', - default: 'data', - required: true, - displayOptions: { - show: { - operation: ['fromFile'], - }, +export const operationProperty: INodeProperties = { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Read From File', + value: 'fromFile', + description: 'Reads data from a spreadsheet file', + action: 'Read data from a spreadsheet file', }, - placeholder: '', - description: - 'Name of the binary property from which to read the binary data of the spreadsheet file', - }, -]; - -export const fromFileV2Properties: INodeProperties[] = [ - { - displayName: 'File Format', - name: 'fileFormat', - type: 'options', - options: [ - { - name: 'Autodetect', - value: 'autodetect', - }, - { - name: 'CSV', - value: 'csv', - description: 'Comma-separated values', - }, - { - name: 'HTML', - value: 'html', - description: 'HTML Table', - }, - { - name: 'ODS', - value: 'ods', - description: 'OpenDocument Spreadsheet', - }, - { - name: 'RTF', - value: 'rtf', - description: 'Rich Text Format', - }, - { - name: 'XLS', - value: 'xls', - description: 'Excel', - }, - { - name: 'XLSX', - value: 'xlsx', - description: 'Excel', - }, - ], - default: 'autodetect', - displayOptions: { - show: { - operation: ['fromFile'], - }, + { + name: 'Write to File', + value: 'toFile', + description: 'Writes the workflow data to a spreadsheet file', + action: 'Write data to a spreadsheet file', + }, + ], + default: 'fromFile', +}; + +export const binaryProperty: INodeProperties = { + displayName: 'Input Binary Field', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + placeholder: '', + hint: 'The name of the input field containing the file data to be processed', + displayOptions: { + show: { + operation: ['fromFile'], }, - description: 'The format of the binary data to read from', }, -]; +}; export const toFileProperties: INodeProperties[] = [ { @@ -139,7 +83,7 @@ export const toFileProperties: INodeProperties[] = [ description: 'The format of the file to save the data as', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -150,189 +94,176 @@ export const toFileProperties: INodeProperties[] = [ }, }, placeholder: '', - description: - 'Name of the binary property in which to save the binary data of the spreadsheet file', + hint: 'The name of the output binary field to put the file in', }, ]; -export const optionsProperties: INodeProperties[] = [ - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Option', - default: {}, - options: [ - { - displayName: 'Compression', - name: 'compression', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['toFile'], - '/fileFormat': ['xlsx', 'ods'], - }, - }, - default: false, - description: 'Whether compression will be applied or not', - }, - { - displayName: 'File Name', - name: 'fileName', - type: 'string', - displayOptions: { - show: { - '/operation': ['toFile'], - }, - }, - default: '', - description: - 'File name to set in binary data. By default will "spreadsheet.<fileFormat>" be used.', - }, - { - displayName: 'Header Row', - name: 'headerRow', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['fromFile', 'toFile'], - }, - }, - default: true, - description: 'Whether the first row of the file contains the header names', - }, - { - displayName: 'Delimiter', - name: 'delimiter', - type: 'string', - displayOptions: { - show: { - '/operation': ['fromFile'], - '/fileFormat': ['csv'], - }, - }, - default: ',', - description: 'Set the field delimiter', - }, - { - displayName: 'Starting Line', - name: 'fromLine', - type: 'number', - displayOptions: { - show: { - '/operation': ['fromFile'], - '/fileFormat': ['csv'], - }, - }, - default: 0, - description: 'Start handling records from the requested line number', - }, - { - displayName: 'Max Number of Rows to Load', - name: 'maxRowCount', - type: 'number', - displayOptions: { - show: { - '/operation': ['fromFile'], - '/fileFormat': ['csv'], - }, - }, - default: -1, - description: 'Stop handling records after the requested number of rows are read', - }, - { - displayName: 'Exclude Byte Order Mark (BOM)', - name: 'enableBOM', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['fromFile'], - '/fileFormat': ['csv'], - }, - }, - default: false, - description: - 'Whether to detect and exclude the byte-order-mark from the CSV Input if present', - }, - { - displayName: 'Include Empty Cells', - name: 'includeEmptyCells', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['fromFile'], - }, - }, - default: false, - // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether - description: - 'When reading from file the empty cells will be filled with an empty string in the JSON', - }, - { - displayName: 'RAW Data', - name: 'rawData', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['fromFile'], - }, - }, - default: false, - description: 'Whether the data should be returned RAW instead of parsed', - }, - { - displayName: 'Read As String', - name: 'readAsString', - type: 'boolean', - displayOptions: { - show: { - '/operation': ['fromFile'], - }, - }, - default: false, - // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether - description: - 'In some cases and file formats, it is necessary to read specifically as string else some special character get interpreted wrong', - }, - { - displayName: 'Range', - name: 'range', - type: 'string', - displayOptions: { - show: { - '/operation': ['fromFile'], - }, - }, - default: '', - description: - 'The range to read from the table. If set to a number it will be the starting row. If set to string it will be used as A1-style bounded range.', - }, - { - displayName: 'Sheet Name', - name: 'sheetName', - type: 'string', - displayOptions: { - show: { - '/operation': ['fromFile'], - }, - }, - default: 'Sheet', - description: - 'Name of the sheet to read from in the spreadsheet (if supported). If not set, the first one gets chosen.', - }, - { - displayName: 'Sheet Name', - name: 'sheetName', - type: 'string', - displayOptions: { - show: { - '/operation': ['toFile'], - '/fileFormat': ['ods', 'xls', 'xlsx'], - }, - }, - default: 'Sheet', - description: 'Name of the sheet to create in the spreadsheet', - }, - ], +export const toFileOptions: INodeProperties = { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['toFile'], + }, }, -]; + options: [ + { + displayName: 'Compression', + name: 'compression', + type: 'boolean', + displayOptions: { + show: { + '/fileFormat': ['xlsx', 'ods'], + }, + }, + default: false, + description: 'Whether compression will be applied or not', + }, + { + displayName: 'File Name', + name: 'fileName', + type: 'string', + default: '', + description: + 'File name to set in binary data. By default will "spreadsheet.<fileFormat>" be used.', + }, + { + displayName: 'Header Row', + name: 'headerRow', + type: 'boolean', + default: true, + description: 'Whether the first row of the file contains the header names', + }, + { + displayName: 'Sheet Name', + name: 'sheetName', + type: 'string', + displayOptions: { + show: { + '/fileFormat': ['ods', 'xls', 'xlsx'], + }, + }, + default: 'Sheet', + description: 'Name of the sheet to create in the spreadsheet', + }, + ], +}; + +export const fromFileOptions: INodeProperties = { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + operation: ['fromFile'], + }, + }, + options: [ + { + displayName: 'Delimiter', + name: 'delimiter', + type: 'string', + displayOptions: { + show: { + '/fileFormat': ['csv'], + }, + }, + default: ',', + placeholder: 'e.g. ,', + description: 'Set the field delimiter, usually a comma', + }, + { + displayName: 'Exclude Byte Order Mark (BOM)', + name: 'enableBOM', + type: 'boolean', + displayOptions: { + show: { + '/fileFormat': ['csv'], + }, + }, + default: false, + description: + 'Whether to detect and exclude the byte-order-mark from the CSV Input if present', + }, + { + displayName: 'Header Row', + name: 'headerRow', + type: 'boolean', + default: true, + description: 'Whether the first row of the file contains the header names', + }, + { + displayName: 'Include Empty Cells', + name: 'includeEmptyCells', + type: 'boolean', + default: false, + description: + 'Whether to include empty cells when reading from file. They will be filled with an empty string.', + }, + { + displayName: 'Max Number of Rows to Load', + name: 'maxRowCount', + type: 'number', + displayOptions: { + show: { + '/fileFormat': ['csv'], + }, + }, + default: -1, + placeholder: 'e.g. 10', + description: + 'Stop handling records after the requested number of rows are read. Use -1 if you want to load all rows.', + }, + { + displayName: 'Range', + name: 'range', + type: 'string', + default: '', + description: + 'The range to read from the table. If set to a number it will be the starting row. If set to string it will be used as A1-style notation range.', + }, + { + displayName: 'RAW Data', + name: 'rawData', + type: 'boolean', + default: false, + description: 'Whether to return RAW data, instead of parsing it', + }, + { + displayName: 'Read As String', + name: 'readAsString', + type: 'boolean', + default: false, + // eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether + description: + 'In some cases and file formats, it is necessary to read as string to ensure special characters are interpreted correctly', + }, + { + displayName: 'Sheet Name', + name: 'sheetName', + type: 'string', + default: 'Sheet', + placeholder: 'e.g. mySheet', + description: + 'Name of the sheet to read from in the spreadsheet (if supported). If not set, the first one will be chosen.', + }, + { + displayName: 'Starting Line', + name: 'fromLine', + type: 'number', + displayOptions: { + show: { + '/fileFormat': ['csv'], + }, + }, + default: 0, + placeholder: 'e.g. 0', + description: 'Start handling records from the requested line number. Starts at 0.', + }, + ], +}; diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts b/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts index fa375859f7..3a57d58c23 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/v1/SpreadsheetFileV1.node.ts @@ -23,10 +23,11 @@ import { } from 'xlsx'; import { - operationProperties, - fromFileProperties, + operationProperty, + binaryProperty, toFileProperties, - optionsProperties, + fromFileOptions, + toFileOptions, } from '../description'; import { flattenObject, generatePairedItemData } from '@utils/utilities'; import { oldVersionNotice } from '@utils/descriptions'; @@ -46,10 +47,11 @@ export class SpreadsheetFileV1 implements INodeType { outputs: ['main'], properties: [ oldVersionNotice, - ...operationProperties, - ...fromFileProperties, + operationProperty, + binaryProperty, ...toFileProperties, - ...optionsProperties, + fromFileOptions, + toFileOptions, ], }; } diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v2/SpreadsheetFileV2.node.ts b/packages/nodes-base/nodes/SpreadsheetFile/v2/SpreadsheetFileV2.node.ts index 4c08bd2754..c1a75b69a9 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile/v2/SpreadsheetFileV2.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile/v2/SpreadsheetFileV2.node.ts @@ -1,37 +1,14 @@ import type { - IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeBaseDescription, INodeTypeDescription, } from 'n8n-workflow'; -import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; -import type { - JSON2SheetOpts, - Sheet2JSONOpts, - WorkBook, - WritingOptions, - ParsingOptions, -} from 'xlsx'; - -import { - read as xlsxRead, - readFile as xlsxReadFile, - utils as xlsxUtils, - write as xlsxWrite, -} from 'xlsx'; -import { parse as createCSVParser } from 'csv-parse'; - -import { - operationProperties, - fromFileProperties, - toFileProperties, - optionsProperties, - fromFileV2Properties, -} from '../description'; -import { flattenObject, generatePairedItemData } from '@utils/utilities'; +import { operationProperty } from '../description'; +import * as fromFile from './fromFile.operation'; +import * as toFile from './toFile.operation'; export class SpreadsheetFileV2 implements INodeType { description: INodeTypeDescription; @@ -46,271 +23,23 @@ export class SpreadsheetFileV2 implements INodeType { }, inputs: ['main'], outputs: ['main'], - properties: [ - ...operationProperties, - ...fromFileProperties, - ...fromFileV2Properties, - ...toFileProperties, - ...optionsProperties, - ], + properties: [operationProperty, ...fromFile.description, ...toFile.description], }; } - async execute(this: IExecuteFunctions): Promise { + async execute(this: IExecuteFunctions) { const items = this.getInputData(); - const operation = this.getNodeParameter('operation', 0); - - const newItems: INodeExecutionData[] = []; + let returnData: INodeExecutionData[] = []; if (operation === 'fromFile') { - // Read data from spreadsheet file to workflow - for (let i = 0; i < items.length; i++) { - try { - const options = this.getNodeParameter('options', i, {}); - let fileFormat = this.getNodeParameter('fileFormat', i, {}); - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); - const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); - - let rows: unknown[] = []; - - if ( - fileFormat === 'autodetect' && - (binaryData.mimeType === 'text/csv' || - (binaryData.mimeType === 'text/plain' && binaryData.fileExtension === 'csv')) - ) { - fileFormat = 'csv'; - } - - if (fileFormat === 'csv') { - const maxRowCount = options.maxRowCount as number; - const parser = createCSVParser({ - delimiter: options.delimiter as string, - fromLine: options.fromLine as number, - bom: options.enableBOM as boolean, - to: maxRowCount > -1 ? maxRowCount : undefined, - columns: options.headerRow !== false, - onRecord: (record) => { - if (!options.includeEmptyCells) { - record = Object.fromEntries( - Object.entries(record).filter(([_key, value]) => value !== ''), - ); - } - rows.push(record); - }, - }); - if (binaryData.id) { - const stream = await this.helpers.getBinaryStream(binaryData.id); - await new Promise(async (resolve, reject) => { - parser.on('error', reject); - parser.on('readable', () => { - stream.unpipe(parser); - stream.destroy(); - resolve(); - }); - stream.pipe(parser); - }); - } else { - parser.write(binaryData.data, BINARY_ENCODING); - parser.end(); - } - } else { - let workbook: WorkBook; - const xlsxOptions: ParsingOptions = { raw: options.rawData as boolean }; - if (options.readAsString) xlsxOptions.type = 'string'; - - if (binaryData.id) { - const binaryPath = this.helpers.getBinaryPath(binaryData.id); - workbook = xlsxReadFile(binaryPath, xlsxOptions); - } else { - const binaryDataBuffer = Buffer.from(binaryData.data, BINARY_ENCODING); - workbook = xlsxRead( - options.readAsString ? binaryDataBuffer.toString() : binaryDataBuffer, - xlsxOptions, - ); - } - - if (workbook.SheetNames.length === 0) { - throw new NodeOperationError( - this.getNode(), - 'Spreadsheet does not have any sheets!', - { - itemIndex: i, - }, - ); - } - - let sheetName = workbook.SheetNames[0]; - if (options.sheetName) { - if (!workbook.SheetNames.includes(options.sheetName as string)) { - throw new NodeOperationError( - this.getNode(), - `Spreadsheet does not contain sheet called "${options.sheetName}"!`, - { itemIndex: i }, - ); - } - sheetName = options.sheetName as string; - } - - // Convert it to json - const sheetToJsonOptions: Sheet2JSONOpts = {}; - if (options.range) { - if (isNaN(options.range as number)) { - sheetToJsonOptions.range = options.range; - } else { - sheetToJsonOptions.range = parseInt(options.range as string, 10); - } - } - - if (options.includeEmptyCells) { - sheetToJsonOptions.defval = ''; - } - - if (options.headerRow === false) { - sheetToJsonOptions.header = 1; // Consider the first row as a data row - } - - rows = xlsxUtils.sheet_to_json(workbook.Sheets[sheetName], sheetToJsonOptions); - - // Check if data could be found in file - if (rows.length === 0) { - continue; - } - } - - // Add all the found data columns to the workflow data - if (options.headerRow === false) { - // Data was returned as an array - https://github.com/SheetJS/sheetjs#json - for (const rowData of rows) { - newItems.push({ - json: { - row: rowData, - }, - pairedItem: { - item: i, - }, - } as INodeExecutionData); - } - } else { - for (const rowData of rows) { - newItems.push({ - json: rowData, - pairedItem: { - item: i, - }, - } as INodeExecutionData); - } - } - } catch (error) { - if (this.continueOnFail()) { - newItems.push({ - json: { - error: error.message, - }, - pairedItem: { - item: i, - }, - }); - continue; - } - throw new NodeOperationError(this.getNode(), error, { itemIndex: i }); - } - } - - return [newItems]; - } else if (operation === 'toFile') { - const pairedItem = generatePairedItemData(items.length); - try { - // Write the workflow data to spreadsheet file - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0); - const fileFormat = this.getNodeParameter('fileFormat', 0) as string; - const options = this.getNodeParameter('options', 0, {}); - const sheetToJsonOptions: JSON2SheetOpts = {}; - if (options.headerRow === false) { - sheetToJsonOptions.skipHeader = true; - } - // Get the json data of the items and flatten it - let item: INodeExecutionData; - const itemData: IDataObject[] = []; - for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { - item = items[itemIndex]; - itemData.push(flattenObject(item.json)); - } - - const ws = xlsxUtils.json_to_sheet(itemData, sheetToJsonOptions); - - const wopts: WritingOptions = { - bookSST: false, - type: 'buffer', - }; - - if (fileFormat === 'csv') { - wopts.bookType = 'csv'; - } else if (fileFormat === 'html') { - wopts.bookType = 'html'; - } else if (fileFormat === 'rtf') { - wopts.bookType = 'rtf'; - } else if (fileFormat === 'ods') { - wopts.bookType = 'ods'; - if (options.compression) { - wopts.compression = true; - } - } else if (fileFormat === 'xls') { - wopts.bookType = 'xls'; - } else if (fileFormat === 'xlsx') { - wopts.bookType = 'xlsx'; - if (options.compression) { - wopts.compression = true; - } - } - - // Convert the data in the correct format - const sheetName = (options.sheetName as string) || 'Sheet'; - const wb: WorkBook = { - SheetNames: [sheetName], - Sheets: { - [sheetName]: ws, - }, - }; - const wbout: Buffer = xlsxWrite(wb, wopts); - - // Create a new item with only the binary spreadsheet data - const newItem: INodeExecutionData = { - json: {}, - binary: {}, - pairedItem, - }; - - let fileName = `spreadsheet.${fileFormat}`; - if (options.fileName !== undefined) { - fileName = options.fileName as string; - } - - newItem.binary![binaryPropertyName] = await this.helpers.prepareBinaryData(wbout, fileName); - - newItems.push(newItem); - } catch (error) { - if (this.continueOnFail()) { - newItems.push({ - json: { - error: error.message, - }, - pairedItem, - }); - } else { - throw error; - } - } - } else { - if (this.continueOnFail()) { - return [[{ json: { error: `The operation "${operation}" is not supported!` } }]]; - } else { - throw new NodeOperationError( - this.getNode(), - `The operation "${operation}" is not supported!`, - ); - } + returnData = await fromFile.execute.call(this, items); } - return [newItems]; + + if (operation === 'toFile') { + returnData = await toFile.execute.call(this, items); + } + + return [returnData]; } } diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts b/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts new file mode 100644 index 0000000000..ffafaa952d --- /dev/null +++ b/packages/nodes-base/nodes/SpreadsheetFile/v2/fromFile.operation.ts @@ -0,0 +1,230 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; + +import type { Sheet2JSONOpts, WorkBook, ParsingOptions } from 'xlsx'; +import { read as xlsxRead, readFile as xlsxReadFile, utils as xlsxUtils } from 'xlsx'; + +import { parse as createCSVParser } from 'csv-parse'; +import { binaryProperty, fromFileOptions } from '../description'; + +export const description: INodeProperties[] = [ + binaryProperty, + { + displayName: 'File Format', + name: 'fileFormat', + type: 'options', + options: [ + { + name: 'Autodetect', + value: 'autodetect', + }, + { + name: 'CSV', + value: 'csv', + description: 'Comma-separated values', + }, + { + name: 'HTML', + value: 'html', + description: 'HTML Table', + }, + { + name: 'ODS', + value: 'ods', + description: 'OpenDocument Spreadsheet', + }, + { + name: 'RTF', + value: 'rtf', + description: 'Rich Text Format', + }, + { + name: 'XLS', + value: 'xls', + description: 'Excel', + }, + { + name: 'XLSX', + value: 'xlsx', + description: 'Excel', + }, + ], + default: 'autodetect', + description: 'The format of the binary data to read from', + displayOptions: { + show: { + operation: ['fromFile'], + }, + }, + }, + fromFileOptions, +]; + +export async function execute( + this: IExecuteFunctions, + items: INodeExecutionData[], + fileFormatProperty = 'fileFormat', +) { + const returnData: INodeExecutionData[] = []; + let fileExtension; + let fileFormat; + + for (let i = 0; i < items.length; i++) { + try { + const options = this.getNodeParameter('options', i, {}); + fileFormat = this.getNodeParameter(fileFormatProperty, i, ''); + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i); + const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); + fileExtension = binaryData.fileExtension; + + let rows: unknown[] = []; + + if ( + fileFormat === 'autodetect' && + (binaryData.mimeType === 'text/csv' || + (binaryData.mimeType === 'text/plain' && binaryData.fileExtension === 'csv')) + ) { + fileFormat = 'csv'; + } + + if (fileFormat === 'csv') { + const maxRowCount = options.maxRowCount as number; + const parser = createCSVParser({ + delimiter: options.delimiter as string, + fromLine: options.fromLine as number, + bom: options.enableBOM as boolean, + to: maxRowCount > -1 ? maxRowCount : undefined, + columns: options.headerRow !== false, + onRecord: (record) => { + if (!options.includeEmptyCells) { + record = Object.fromEntries( + Object.entries(record).filter(([_key, value]) => value !== ''), + ); + } + rows.push(record); + }, + }); + if (binaryData.id) { + const stream = await this.helpers.getBinaryStream(binaryData.id); + await new Promise(async (resolve, reject) => { + parser.on('error', reject); + parser.on('readable', () => { + stream.unpipe(parser); + stream.destroy(); + resolve(); + }); + stream.pipe(parser); + }); + } else { + parser.write(binaryData.data, BINARY_ENCODING); + parser.end(); + } + } else { + let workbook: WorkBook; + const xlsxOptions: ParsingOptions = { raw: options.rawData as boolean }; + if (options.readAsString) xlsxOptions.type = 'string'; + + if (binaryData.id) { + const binaryPath = this.helpers.getBinaryPath(binaryData.id); + workbook = xlsxReadFile(binaryPath, xlsxOptions); + } else { + const binaryDataBuffer = Buffer.from(binaryData.data, BINARY_ENCODING); + workbook = xlsxRead( + options.readAsString ? binaryDataBuffer.toString() : binaryDataBuffer, + xlsxOptions, + ); + } + + if (workbook.SheetNames.length === 0) { + throw new NodeOperationError(this.getNode(), 'Spreadsheet does not have any sheets!', { + itemIndex: i, + }); + } + + let sheetName = workbook.SheetNames[0]; + if (options.sheetName) { + if (!workbook.SheetNames.includes(options.sheetName as string)) { + throw new NodeOperationError( + this.getNode(), + `Spreadsheet does not contain sheet called "${options.sheetName}"!`, + { itemIndex: i }, + ); + } + sheetName = options.sheetName as string; + } + + // Convert it to json + const sheetToJsonOptions: Sheet2JSONOpts = {}; + if (options.range) { + if (isNaN(options.range as number)) { + sheetToJsonOptions.range = options.range; + } else { + sheetToJsonOptions.range = parseInt(options.range as string, 10); + } + } + + if (options.includeEmptyCells) { + sheetToJsonOptions.defval = ''; + } + + if (options.headerRow === false) { + sheetToJsonOptions.header = 1; // Consider the first row as a data row + } + + rows = xlsxUtils.sheet_to_json(workbook.Sheets[sheetName], sheetToJsonOptions); + + // Check if data could be found in file + if (rows.length === 0) { + continue; + } + } + + // Add all the found data columns to the workflow data + if (options.headerRow === false) { + // Data was returned as an array - https://github.com/SheetJS/sheetjs#json + for (const rowData of rows) { + returnData.push({ + json: { + row: rowData, + }, + pairedItem: { + item: i, + }, + } as INodeExecutionData); + } + } else { + for (const rowData of rows) { + returnData.push({ + json: rowData, + pairedItem: { + item: i, + }, + } as INodeExecutionData); + } + } + } catch (error) { + let errorDescription = error.description; + if (fileExtension && fileExtension !== fileFormat) { + error.message = `The file selected in 'Input Binary Field' is not in ${fileFormat} format`; + errorDescription = `Try to change the operation or select a ${fileFormat} file in 'Input Binary Field'`; + } + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error, { + itemIndex: i, + description: errorDescription, + }); + } + } + + return returnData; +} diff --git a/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts b/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts new file mode 100644 index 0000000000..b59d1568f2 --- /dev/null +++ b/packages/nodes-base/nodes/SpreadsheetFile/v2/toFile.operation.ts @@ -0,0 +1,44 @@ +import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; + +import { generatePairedItemData } from '@utils/utilities'; +import type { JsonToSpreadsheetBinaryFormat, JsonToSpreadsheetBinaryOptions } from '@utils/binary'; +import { convertJsonToSpreadsheetBinary } from '@utils/binary'; +import { toFileOptions, toFileProperties } from '../description'; + +export const description: INodeProperties[] = [...toFileProperties, toFileOptions]; + +export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { + const returnData: INodeExecutionData[] = []; + + const pairedItem = generatePairedItemData(items.length); + + try { + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0); + const fileFormat = this.getNodeParameter('fileFormat', 0) as JsonToSpreadsheetBinaryFormat; + const options = this.getNodeParameter('options', 0, {}) as JsonToSpreadsheetBinaryOptions; + + const binaryData = await convertJsonToSpreadsheetBinary.call(this, items, fileFormat, options); + + const newItem: INodeExecutionData = { + json: {}, + binary: { + [binaryPropertyName]: binaryData, + }, + pairedItem, + }; + + returnData.push(newItem); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem, + }); + } else { + throw error; + } + } + return returnData; +} diff --git a/packages/nodes-base/nodes/Ssh/Ssh.node.ts b/packages/nodes-base/nodes/Ssh/Ssh.node.ts index ebb30e3bcf..7625289797 100644 --- a/packages/nodes-base/nodes/Ssh/Ssh.node.ts +++ b/packages/nodes-base/nodes/Ssh/Ssh.node.ts @@ -191,7 +191,7 @@ export class Ssh implements INodeType { default: 'upload', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -203,8 +203,7 @@ export class Ssh implements INodeType { }, }, placeholder: '', - description: - 'Name of the binary property which contains the data for the file to be uploaded', + hint: 'The name of the input binary field containing the file to be uploaded', }, { displayName: 'Target Directory', @@ -239,7 +238,7 @@ export class Ssh implements INodeType { required: true, }, { - displayName: 'Binary Property', + displayName: 'File Property', displayOptions: { show: { resource: ['file'], diff --git a/packages/nodes-base/nodes/Telegram/Telegram.node.ts b/packages/nodes-base/nodes/Telegram/Telegram.node.ts index 3b1f1b3082..cf721c1adc 100644 --- a/packages/nodes-base/nodes/Telegram/Telegram.node.ts +++ b/packages/nodes-base/nodes/Telegram/Telegram.node.ts @@ -688,7 +688,7 @@ export class Telegram implements INodeType { // ---------------------------------- { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', default: false, @@ -709,11 +709,12 @@ export class Telegram implements INodeType { description: 'Whether the data to upload should be taken from binary field', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryPropertyName', type: 'string', default: 'data', required: true, + hint: 'The name of the input binary field containing the file to be written', displayOptions: { show: { operation: [ diff --git a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts index 835d47bd56..2522094645 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/AlertDescription.ts @@ -339,9 +339,10 @@ export const alertFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', + hint: 'The name of the input binary field containing the file to be written', displayOptions: { show: { dataType: ['file'], @@ -553,9 +554,10 @@ export const alertFields: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', + hint: 'The name of the input binary field containing the file to be written', displayOptions: { show: { dataType: ['file'], diff --git a/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts index 8f8f8ed16b..834201ed65 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/LogDescription.ts @@ -206,11 +206,11 @@ export const logFields: INodeProperties[] = [ name: 'attachmentValues', values: [ { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', default: 'data', - description: 'Object property name which holds binary data', + description: 'The name of the input binary field which holds binary data', }, ], }, diff --git a/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts index d4d9a82b22..747d610222 100644 --- a/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts +++ b/packages/nodes-base/nodes/TheHive/descriptions/ObservableDescription.ts @@ -120,12 +120,12 @@ export const observableFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Binary Property that represent the attachment file', + description: 'The name of the input binary field that represent the attachment file', displayOptions: { show: { resource: ['observable'], diff --git a/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts b/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts index f601184c13..e7a6fc6f9e 100644 --- a/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts +++ b/packages/nodes-base/nodes/TheHiveProject/actions/alert/create.operation.ts @@ -59,9 +59,10 @@ const properties: INodeProperties[] = [ default: '', }, { - displayName: 'Binary Property', + displayName: 'Input Binary Field', name: 'binaryProperty', type: 'string', + hint: 'The name of the input binary field containing the file to be written', displayOptions: { show: { dataType: ['file'], diff --git a/packages/nodes-base/nodes/Vonage/Vonage.node.ts b/packages/nodes-base/nodes/Vonage/Vonage.node.ts index 162b982c3a..b6944f4f1a 100644 --- a/packages/nodes-base/nodes/Vonage/Vonage.node.ts +++ b/packages/nodes-base/nodes/Vonage/Vonage.node.ts @@ -133,7 +133,7 @@ export class Vonage implements INodeType { // description: 'The format of the message body', // }, // { - // displayName: 'Binary Property', + // displayName: 'Input Binary Field', // name: 'binaryPropertyName', // displayOptions: { // show: { diff --git a/packages/nodes-base/nodes/Webhook/description.ts b/packages/nodes-base/nodes/Webhook/description.ts index 4e3822c70c..2f4cc3fdeb 100644 --- a/packages/nodes-base/nodes/Webhook/description.ts +++ b/packages/nodes-base/nodes/Webhook/description.ts @@ -196,7 +196,7 @@ export const optionsProperty: INodeProperties = { default: {}, options: [ { - displayName: 'Binary Data', + displayName: 'Binary File', name: 'binaryData', type: 'boolean', displayOptions: { @@ -209,7 +209,7 @@ export const optionsProperty: INodeProperties = { description: 'Whether the webhook will receive binary data', }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryPropertyName', type: 'string', default: 'data', @@ -219,8 +219,9 @@ export const optionsProperty: INodeProperties = { '@version': [1], }, }, + hint: 'The name of the output binary field to put the file in', description: - 'Name of the binary property to write the data of the received file to. If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it.', + 'If the data gets received via "Form-Data Multipart" it will be the prefix and a number starting with 0 will be attached to it', }, { displayName: 'Binary Property', diff --git a/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts index 00101da4b3..3f149307b8 100644 --- a/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts +++ b/packages/nodes-base/nodes/Wise/descriptions/AccountDescription.ts @@ -139,12 +139,12 @@ export const accountFields: INodeProperties[] = [ ], }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['account'], diff --git a/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts index 5d9f0a2d95..5f743cc997 100644 --- a/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts +++ b/packages/nodes-base/nodes/Wise/descriptions/TransferDescription.ts @@ -207,12 +207,12 @@ export const transferFields: INodeProperties[] = [ }, }, { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'binaryProperty', type: 'string', required: true, default: 'data', - description: 'Name of the binary property to which to write to', + hint: 'The name of the output binary field to put the file in', displayOptions: { show: { resource: ['transfer'], diff --git a/packages/nodes-base/nodes/WriteBinaryFile/WriteBinaryFile.node.ts b/packages/nodes-base/nodes/WriteBinaryFile/WriteBinaryFile.node.ts index fb5ea7b1e3..0938a31488 100644 --- a/packages/nodes-base/nodes/WriteBinaryFile/WriteBinaryFile.node.ts +++ b/packages/nodes-base/nodes/WriteBinaryFile/WriteBinaryFile.node.ts @@ -10,6 +10,7 @@ import type { export class WriteBinaryFile implements INodeType { description: INodeTypeDescription = { + hidden: true, displayName: 'Write Binary File', name: 'writeBinaryFile', icon: 'fa:file-export', diff --git a/packages/nodes-base/nodes/Xml/Xml.node.json b/packages/nodes-base/nodes/Xml/Xml.node.json index 0291e03833..09fc208546 100644 --- a/packages/nodes-base/nodes/Xml/Xml.node.json +++ b/packages/nodes-base/nodes/Xml/Xml.node.json @@ -19,6 +19,6 @@ }, "alias": ["Parse"], "subcategories": { - "Core Nodes": ["Files", "Data Transformation"] + "Core Nodes": ["Data Transformation"] } } diff --git a/packages/nodes-base/nodes/Xml/Xml.node.ts b/packages/nodes-base/nodes/Xml/Xml.node.ts index eca66e0b06..d80d97f9f5 100644 --- a/packages/nodes-base/nodes/Xml/Xml.node.ts +++ b/packages/nodes-base/nodes/Xml/Xml.node.ts @@ -42,6 +42,18 @@ export class Xml implements INodeType { default: 'xmlToJson', description: 'From and to what format the data should be converted', }, + { + displayName: + "If your XML is inside a binary file, use the 'Extract From File' node to convert it to text first", + name: 'xmlNotice', + type: 'notice', + default: '', + displayOptions: { + show: { + mode: ['xmlToJson'], + }, + }, + }, // ---------------------------------- // option:jsonToxml diff --git a/packages/nodes-base/nodes/Zulip/MessageDescription.ts b/packages/nodes-base/nodes/Zulip/MessageDescription.ts index 786cf7c745..c8f0efc339 100644 --- a/packages/nodes-base/nodes/Zulip/MessageDescription.ts +++ b/packages/nodes-base/nodes/Zulip/MessageDescription.ts @@ -249,7 +249,7 @@ export const messageFields: INodeProperties[] = [ /* message:updateFile */ /* -------------------------------------------------------------------------- */ { - displayName: 'Binary Property', + displayName: 'Put Output File in Field', name: 'dataBinaryProperty', type: 'string', required: true, @@ -260,6 +260,6 @@ export const messageFields: INodeProperties[] = [ operation: ['updateFile'], }, }, - description: 'Name of the binary property to which to write the data of the read file', + hint: 'The name of the output binary field to put the file in', }, ]; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 0781170e50..ac6c02c44a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -492,6 +492,9 @@ "dist/nodes/FacebookLeadAds/FacebookLeadAdsTrigger.node.js", "dist/nodes/Figma/FigmaTrigger.node.js", "dist/nodes/FileMaker/FileMaker.node.js", + "dist/nodes/Files/ReadWriteFile/ReadWriteFile.node.js", + "dist/nodes/Files/ConvertToFile/ConvertToFile.node.js", + "dist/nodes/Files/ExtractFromFile/ExtractFromFile.node.js", "dist/nodes/Filter/Filter.node.js", "dist/nodes/Flow/Flow.node.js", "dist/nodes/Flow/FlowTrigger.node.js", @@ -885,6 +888,7 @@ "snowflake-sdk": "1.9.2", "ssh2-sftp-client": "7.2.3", "tmp-promise": "3.0.3", + "ts-ics": "^1.2.2", "typedi": "0.10.0", "uuid": "8.3.2", "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz", diff --git a/packages/nodes-base/utils/binary.ts b/packages/nodes-base/utils/binary.ts new file mode 100644 index 0000000000..825a0ca08e --- /dev/null +++ b/packages/nodes-base/utils/binary.ts @@ -0,0 +1,194 @@ +import type { IBinaryData, IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { NodeOperationError, BINARY_ENCODING } from 'n8n-workflow'; +import type { WorkBook, WritingOptions } from 'xlsx'; +import { utils as xlsxUtils, write as xlsxWrite } from 'xlsx'; +import { flattenObject } from '@utils/utilities'; + +import get from 'lodash/get'; +import iconv from 'iconv-lite'; + +import { getDocument as readPDF, version as pdfJsVersion } from 'pdfjs-dist'; + +export type JsonToSpreadsheetBinaryFormat = 'csv' | 'html' | 'rtf' | 'ods' | 'xls' | 'xlsx'; + +export type JsonToSpreadsheetBinaryOptions = { + headerRow?: boolean; + compression?: boolean; + fileName?: string; + sheetName?: string; +}; + +export type JsonToBinaryOptions = { + fileName?: string; + sourceKey?: string; + encoding?: string; + addBOM?: boolean; + mimeType?: string; + dataIsBase64?: boolean; + itemIndex?: number; +}; + +type PdfDocument = Awaited>['promise']>; +type PdfPage = Awaited>>; +type PdfTextContent = Awaited>; + +export async function convertJsonToSpreadsheetBinary( + this: IExecuteFunctions, + items: INodeExecutionData[], + fileFormat: JsonToSpreadsheetBinaryFormat, + options: JsonToSpreadsheetBinaryOptions, + defaultFileName = 'spreadsheet', +): Promise { + const itemData: IDataObject[] = []; + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + itemData.push(flattenObject(items[itemIndex].json)); + } + + let sheetToJsonOptions; + if (options.headerRow === false) { + sheetToJsonOptions = { skipHeader: true }; + } + + const sheet = xlsxUtils.json_to_sheet(itemData, sheetToJsonOptions); + + const writingOptions: WritingOptions = { + bookType: fileFormat, + bookSST: false, + type: 'buffer', + }; + + if (['xlsx', 'ods'].includes(fileFormat) && options.compression) { + writingOptions.compression = true; + } + + // Convert the data in the correct format + const sheetName = (options.sheetName as string) || 'Sheet'; + const workbook: WorkBook = { + SheetNames: [sheetName], + Sheets: { + [sheetName]: sheet, + }, + }; + + const buffer: Buffer = xlsxWrite(workbook, writingOptions); + const fileName = + options.fileName !== undefined ? options.fileName : `${defaultFileName}.${fileFormat}`; + const binaryData = await this.helpers.prepareBinaryData(buffer, fileName); + + return binaryData; +} + +export async function createBinaryFromJson( + this: IExecuteFunctions, + data: IDataObject | IDataObject[], + options: JsonToBinaryOptions, +): Promise { + let value; + if (options.sourceKey) { + value = get(data, options.sourceKey) as IDataObject; + } else { + value = data; + } + + if (value === undefined) { + throw new NodeOperationError(this.getNode(), `The value in "${options.sourceKey}" is not set`, { + itemIndex: options.itemIndex || 0, + }); + } + + let buffer: Buffer; + if (!options.dataIsBase64) { + let valueAsString = value as unknown as string; + + if (typeof value === 'object') { + options.mimeType = 'application/json'; + valueAsString = JSON.stringify(value); + } + + buffer = iconv.encode(valueAsString, options.encoding || 'utf8', { + addBOM: options.addBOM, + }); + } else { + buffer = Buffer.from(value as unknown as string, BINARY_ENCODING); + } + + const binaryData = await this.helpers.prepareBinaryData( + buffer, + options.fileName, + options.mimeType, + ); + + if (!binaryData.fileName) { + const fileExtension = binaryData.fileExtension ? `.${binaryData.fileExtension}` : ''; + binaryData.fileName = `file${fileExtension}`; + } + + return binaryData; +} + +const parseText = (textContent: PdfTextContent) => { + let lastY = undefined; + const text = []; + for (const item of textContent.items) { + if ('str' in item) { + if (lastY == item.transform[5] || !lastY) { + text.push(item.str); + } else { + text.push(`\n${item.str}`); + } + lastY = item.transform[5]; + } + } + return text.join(''); +}; + +export async function extractDataFromPDF( + this: IExecuteFunctions, + binaryPropertyName: string, + password?: string, + maxPages?: number, + joinPages = true, + itemIndex = 0, +) { + const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName); + + const params: { password?: string; url?: URL; data?: ArrayBuffer } = { password }; + + if (binaryData.id) { + const binaryPath = this.helpers.getBinaryPath(binaryData.id); + params.url = new URL(`file://${binaryPath}`); + } else { + params.data = Buffer.from(binaryData.data, BINARY_ENCODING).buffer; + } + + const document = await readPDF(params).promise; + const { info, metadata } = await document + .getMetadata() + .catch(() => ({ info: null, metadata: null })); + + const pages = []; + if (maxPages !== 0) { + let pagesToRead = document.numPages; + if (maxPages && maxPages < document.numPages) { + pagesToRead = maxPages; + } + for (let i = 1; i <= pagesToRead; i++) { + const page = await document.getPage(i); + const text = await page.getTextContent().then(parseText); + pages.push(text); + } + } + + const text = joinPages ? pages.join('\n\n') : pages; + + const returnData = { + numpages: document.numPages, + numrender: document.numPages, + info, + metadata: metadata?.getAll(), + text, + version: pdfJsVersion, + }; + + return returnData; +} diff --git a/packages/nodes-base/utils/descriptions.ts b/packages/nodes-base/utils/descriptions.ts index 614858a28d..1465d3e314 100644 --- a/packages/nodes-base/utils/descriptions.ts +++ b/packages/nodes-base/utils/descriptions.ts @@ -1,4 +1,4 @@ -import type { INodeProperties } from 'n8n-workflow'; +import type { INodeProperties, INodePropertyOptions } from 'n8n-workflow'; export const oldVersionNotice: INodeProperties = { displayName: @@ -32,3 +32,414 @@ export const returnAllOrLimit: INodeProperties[] = [ description: 'Max number of results to return', }, ]; + +export const encodeDecodeOptions: INodePropertyOptions[] = [ + { + name: 'armscii8', + value: 'armscii8', + }, + { + name: 'ascii', + value: 'ascii', + }, + { + name: 'base64', + value: 'base64', + }, + { + name: 'big5hkscs', + value: 'big5hkscs', + }, + { + name: 'binary', + value: 'binary', + }, + { + name: 'cesu8', + value: 'cesu8', + }, + { + name: 'cp1046', + value: 'cp1046', + }, + { + name: 'cp1124', + value: 'cp1124', + }, + { + name: 'cp1125', + value: 'cp1125', + }, + { + name: 'cp1129', + value: 'cp1129', + }, + { + name: 'cp1133', + value: 'cp1133', + }, + { + name: 'cp1161', + value: 'cp1161', + }, + { + name: 'cp1162', + value: 'cp1162', + }, + { + name: 'cp1163', + value: 'cp1163', + }, + { + name: 'cp437', + value: 'cp437', + }, + { + name: 'cp720', + value: 'cp720', + }, + { + name: 'cp737', + value: 'cp737', + }, + { + name: 'cp775', + value: 'cp775', + }, + { + name: 'cp808', + value: 'cp808', + }, + { + name: 'cp850', + value: 'cp850', + }, + { + name: 'cp852', + value: 'cp852', + }, + { + name: 'cp855', + value: 'cp855', + }, + { + name: 'cp856', + value: 'cp856', + }, + { + name: 'cp857', + value: 'cp857', + }, + { + name: 'cp858', + value: 'cp858', + }, + { + name: 'cp860', + value: 'cp860', + }, + { + name: 'cp861', + value: 'cp861', + }, + { + name: 'cp862', + value: 'cp862', + }, + { + name: 'cp863', + value: 'cp863', + }, + { + name: 'cp864', + value: 'cp864', + }, + { + name: 'cp865', + value: 'cp865', + }, + { + name: 'cp866', + value: 'cp866', + }, + { + name: 'cp869', + value: 'cp869', + }, + { + name: 'cp922', + value: 'cp922', + }, + { + name: 'cp936', + value: 'cp936', + }, + { + name: 'cp949', + value: 'cp949', + }, + { + name: 'cp950', + value: 'cp950', + }, + { + name: 'eucjp', + value: 'eucjp', + }, + { + name: 'gb18030', + value: 'gb18030', + }, + { + name: 'gbk', + value: 'gbk', + }, + { + name: 'georgianacademy', + value: 'georgianacademy', + }, + { + name: 'georgianps', + value: 'georgianps', + }, + { + name: 'hex', + value: 'hex', + }, + { + name: 'hproman8', + value: 'hproman8', + }, + { + name: 'iso646cn', + value: 'iso646cn', + }, + { + name: 'iso646jp', + value: 'iso646jp', + }, + { + name: 'iso88591', + value: 'iso88591', + }, + { + name: 'iso885910', + value: 'iso885910', + }, + { + name: 'iso885911', + value: 'iso885911', + }, + { + name: 'iso885913', + value: 'iso885913', + }, + { + name: 'iso885914', + value: 'iso885914', + }, + { + name: 'iso885915', + value: 'iso885915', + }, + { + name: 'iso885916', + value: 'iso885916', + }, + { + name: 'iso88592', + value: 'iso88592', + }, + { + name: 'iso88593', + value: 'iso88593', + }, + { + name: 'iso88594', + value: 'iso88594', + }, + { + name: 'iso88595', + value: 'iso88595', + }, + { + name: 'iso88596', + value: 'iso88596', + }, + { + name: 'iso88597', + value: 'iso88597', + }, + { + name: 'iso88598', + value: 'iso88598', + }, + { + name: 'iso88599', + value: 'iso88599', + }, + { + name: 'koi8r', + value: 'koi8r', + }, + { + name: 'koi8ru', + value: 'koi8ru', + }, + { + name: 'koi8t', + value: 'koi8t', + }, + { + name: 'koi8u', + value: 'koi8u', + }, + { + name: 'maccenteuro', + value: 'maccenteuro', + }, + { + name: 'maccroatian', + value: 'maccroatian', + }, + { + name: 'maccyrillic', + value: 'maccyrillic', + }, + { + name: 'macgreek', + value: 'macgreek', + }, + { + name: 'maciceland', + value: 'maciceland', + }, + { + name: 'macintosh', + value: 'macintosh', + }, + { + name: 'macroman', + value: 'macroman', + }, + { + name: 'macromania', + value: 'macromania', + }, + { + name: 'macthai', + value: 'macthai', + }, + { + name: 'macturkish', + value: 'macturkish', + }, + { + name: 'macukraine', + value: 'macukraine', + }, + { + name: 'mik', + value: 'mik', + }, + { + name: 'pt154', + value: 'pt154', + }, + { + name: 'rk1048', + value: 'rk1048', + }, + { + name: 'shiftjis', + value: 'shiftjis', + }, + { + name: 'tcvn', + value: 'tcvn', + }, + { + name: 'tis620', + value: 'tis620', + }, + { + name: 'ucs2', + value: 'ucs2', + }, + { + name: 'utf16', + value: 'utf16', + }, + { + name: 'utf16be', + value: 'utf16be', + }, + { + name: 'utf32', + value: 'utf32', + }, + { + name: 'utf32be', + value: 'utf32be', + }, + { + name: 'utf32le', + value: 'utf32le', + }, + { + name: 'utf7', + value: 'utf7', + }, + { + name: 'utf7imap', + value: 'utf7imap', + }, + { + name: 'utf8', + value: 'utf8', + }, + { + name: 'viscii', + value: 'viscii', + }, + { + name: 'windows1250', + value: 'windows1250', + }, + { + name: 'windows1251', + value: 'windows1251', + }, + { + name: 'windows1252', + value: 'windows1252', + }, + { + name: 'windows1253', + value: 'windows1253', + }, + { + name: 'windows1254', + value: 'windows1254', + }, + { + name: 'windows1255', + value: 'windows1255', + }, + { + name: 'windows1256', + value: 'windows1256', + }, + { + name: 'windows1257', + value: 'windows1257', + }, + { + name: 'windows1258', + value: 'windows1258', + }, + { + name: 'windows874', + value: 'windows874', + }, +]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34f857df0e..effa40dcbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1370,6 +1370,9 @@ importers: tmp-promise: specifier: 3.0.3 version: 3.0.3 + ts-ics: + specifier: ^1.2.2 + version: 1.2.2(date-fns@2.30.0)(lodash@4.17.21)(zod@3.22.4) typedi: specifier: 0.10.0 version: 0.10.0(patch_hash=sk6omkefrosihg7lmqbzh7vfxe) @@ -13720,6 +13723,14 @@ packages: whatwg-url: 14.0.0 dev: true + /date-fns-tz@2.0.0(date-fns@2.30.0): + resolution: {integrity: sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==} + peerDependencies: + date-fns: '>=2.0.0' + dependencies: + date-fns: 2.30.0 + dev: false + /date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -24632,6 +24643,19 @@ packages: typescript: 5.3.2 dev: true + /ts-ics@1.2.2(date-fns@2.30.0)(lodash@4.17.21)(zod@3.22.4): + resolution: {integrity: sha512-L7T5JQi99qQ2Uv7AoCHUZ8Mx1bJYo7qBZtBckuHueR90I3WVdW5NC/tOqTVgu18c3zj08du+xlgWlTIcE+Foxw==} + peerDependencies: + date-fns: ^2 + lodash: ^4 + zod: ^3 + dependencies: + date-fns: 2.30.0 + date-fns-tz: 2.0.0(date-fns@2.30.0) + lodash: 4.17.21 + zod: 3.22.4 + dev: false + /ts-jest@29.1.1(@babel/core@7.22.9)(jest@29.6.2)(typescript@5.3.2): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}