From b0848d160da964af63db1055855421f1d1eb2336 Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Tue, 13 Feb 2024 13:40:39 +0200 Subject: [PATCH 1/2] :zap: Move String to File operation, deprecated base64 option in Move Base64 to File, format option to Convert to JSON --- .../Files/ConvertToFile/ConvertToFile.node.ts | 14 +- .../actions/toBinary.operation.ts | 15 +- .../ConvertToFile/actions/toJson.operation.ts | 9 + .../ConvertToFile/actions/toText.operation.ts | 126 +++++++ .../test/ConvertToFile.node.test.ts | 5 + .../ConvertToFile/test/toText.workflow.json | 357 ++++++++++++++++++ packages/nodes-base/utils/binary.ts | 7 +- 7 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/actions/toText.operation.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/test/ConvertToFile.node.test.ts create mode 100644 packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts index d17b431e15e34..659eeb965b29f 100644 --- a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts +++ b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts @@ -7,6 +7,7 @@ import type { import * as spreadsheet from './actions/spreadsheet.operation'; import * as toBinary from './actions/toBinary.operation'; +import * as toText from './actions/toText.operation'; import * as toJson from './actions/toJson.operation'; import * as iCall from './actions/iCall.operation'; @@ -17,7 +18,7 @@ export class ConvertToFile implements INodeType { name: 'convertToFile', icon: 'file:convertToFile.svg', group: ['input'], - version: 1, + version: [1, 1.1], description: 'Convert JSON data to binary data', defaults: { name: 'Convert to File', @@ -85,11 +86,18 @@ export class ConvertToFile implements INodeType { action: 'Move base64 string to file', description: 'Convert a base64-encoded string into its original file format', }, + { + name: 'Move String to File', + value: 'toText', + action: 'Move string to file', + description: 'Transform input data string into a file', + }, ], default: 'csv', }, ...spreadsheet.description, ...toBinary.description, + ...toText.description, ...toJson.description, ...iCall.description, ], @@ -112,6 +120,10 @@ export class ConvertToFile implements INodeType { returnData = await toBinary.execute.call(this, items); } + if (operation === 'toText') { + returnData = await toText.execute.call(this, items); + } + if (operation === 'iCal') { returnData = await iCall.execute.call(this, items); } diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts index 770b55b72ba9f..1a10bc079e12f 100644 --- a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toBinary.operation.ts @@ -54,6 +54,11 @@ export const properties: INodeProperties[] = [ type: 'boolean', default: true, description: 'Whether the data is already base64 encoded', + displayOptions: { + show: { + '@version': [1], + }, + }, }, { displayName: 'Encoding', @@ -65,6 +70,7 @@ export const properties: INodeProperties[] = [ displayOptions: { hide: { dataIsBase64: [true], + '@version': [{ _cnd: { gt: 1 } }], }, }, }, @@ -100,17 +106,24 @@ export const description = updateDisplayOptions(displayOptions, properties); export async function execute(this: IExecuteFunctions, items: INodeExecutionData[]) { const returnData: INodeExecutionData[] = []; + const nodeVersion = this.getNode().typeVersion; + 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; + let dataIsBase64 = true; + if (nodeVersion === 1) { + dataIsBase64 = options.dataIsBase64 !== false; + } + const jsonToBinaryOptions: JsonToBinaryOptions = { sourceKey: sourceProperty, fileName: options.fileName as string, mimeType: options.mimeType as string, - dataIsBase64: options.dataIsBase64 !== false, + dataIsBase64, encoding: options.encoding as string, addBOM: options.addBOM as boolean, itemIndex: i, diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts index eaccdfb5ff6f3..51b0a23105faf 100644 --- a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toJson.operation.ts @@ -52,6 +52,13 @@ export const properties: INodeProperties[] = [ }, }, }, + { + displayName: 'Format', + name: 'format', + type: 'boolean', + default: false, + description: 'Whether to format the JSON data for easier reading', + }, { displayName: 'Encoding', name: 'encoding', @@ -98,6 +105,7 @@ export async function execute(this: IExecuteFunctions, items: INodeExecutionData mimeType: 'application/json', encoding: options.encoding as string, addBOM: options.addBOM as boolean, + format: options.format as boolean, }, ); @@ -131,6 +139,7 @@ export async function execute(this: IExecuteFunctions, items: INodeExecutionData fileName: options.fileName as string, encoding: options.encoding as string, addBOM: options.addBOM as boolean, + format: options.format as boolean, mimeType: 'application/json', itemIndex: i, }); diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/actions/toText.operation.ts b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toText.operation.ts new file mode 100644 index 0000000000000..438165118b759 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/actions/toText.operation.ts @@ -0,0 +1,126 @@ +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: 'Text Input Field', + name: 'sourceProperty', + type: 'string', + default: '', + required: true, + placeholder: 'e.g data', + requiresDataPath: 'single', + description: + "The name of the input field that contains a 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: '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', + description: 'Name of the output file', + }, + ], + }, +]; + +const displayOptions = { + show: { + operation: ['toText'], + }, +}; + +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) || 'file.txt', + mimeType: 'text/plain', + 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/test/ConvertToFile.node.test.ts b/packages/nodes-base/nodes/Files/ConvertToFile/test/ConvertToFile.node.test.ts new file mode 100644 index 0000000000000..ca8993a8f8538 --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/test/ConvertToFile.node.test.ts @@ -0,0 +1,5 @@ +import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers'; + +const workflows = getWorkflowFilenames(__dirname); + +describe('Test ConvertToFile Node', () => testWorkflows(workflows)); diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json b/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json new file mode 100644 index 0000000000000..3a855ccf9aaef --- /dev/null +++ b/packages/nodes-base/nodes/Files/ConvertToFile/test/toText.workflow.json @@ -0,0 +1,357 @@ +{ + "name": "My workflow 2", + "nodes": [ + { + "parameters": {}, + "id": "59f5ae0f-52f7-4bc8-b325-29d2b0d810f8", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 460, + 500 + ] + }, + { + "parameters": { + "operation": "toText", + "sourceProperty": "notes", + "options": {} + }, + "id": "add99ca3-7bd3-4561-a654-fac4b8ded285", + "name": "Convert to File", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1, + "position": [ + 940, + 400 + ] + }, + { + "parameters": { + "operation": "toJson", + "options": { + "format": true + } + }, + "id": "89498c96-f1a0-49ec-890d-79f12c5554e6", + "name": "Convert to File1", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [ + 940, + 580 + ] + }, + { + "parameters": { + "operation": "toBinary", + "sourceProperty": "base64", + "options": {} + }, + "id": "ae06c883-f2af-4d25-bc64-4f0ecad53c85", + "name": "Convert to File2", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [ + 940, + 200 + ] + }, + { + "parameters": { + "jsCode": "return {\n \"id\": \"23423532\",\n \"name\": \"Jay Gatsby\",\n \"email\": \"gatsby@west-egg.com\",\n \"notes\": \"Keeps asking about a green light??\",\n \"country\": \"US\",\n \"created\": \"1925-04-10\",\n \"base64\": \"VGhpcyBpcyBzb21lIHRleHQgZW5jb2RlZCBhcyBiYXNlNjQ=\"\n }" + }, + "id": "b14b18b0-6570-4376-85cf-f3cc74835e58", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 680, + 500 + ] + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "id": "76373e3f-e103-465a-8b15-dd643915c532", + "name": "Extract From File", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1160, + 200 + ] + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "id": "d8ba3980-873d-47d7-ad88-f5ff6c66774c", + "name": "Extract From File1", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1160, + 400 + ] + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "id": "34838f1e-aee5-4b17-a9ec-bd9e09789045", + "name": "Extract From File2", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1160, + 580 + ] + }, + { + "parameters": { + "operation": "toJson", + "options": {} + }, + "id": "a6617075-83f4-4157-9d07-7e5df0cbd9b6", + "name": "Convert to File3", + "type": "n8n-nodes-base.convertToFile", + "typeVersion": 1.1, + "position": [ + 960, + 780 + ] + }, + { + "parameters": { + "operation": "text", + "options": {} + }, + "id": "b2deb5a4-0f7a-4a1f-858f-17235db0b94e", + "name": "Extract From File3", + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1, + "position": [ + 1160, + 780 + ] + }, + { + "parameters": {}, + "id": "11022c53-136b-44a0-af32-faac16e2fa89", + "name": "No Operation, do nothing", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1380, + 200 + ] + }, + { + "parameters": {}, + "id": "f5ec42e5-8088-4a93-91ea-3a1cb4997eee", + "name": "No Operation, do nothing1", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1380, + 400 + ] + }, + { + "parameters": {}, + "id": "d7106de2-455f-428f-bdfa-fe701136bdfa", + "name": "No Operation, do nothing2", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1380, + 580 + ] + }, + { + "parameters": {}, + "id": "14dbc74c-fc0b-4339-88ea-76b3e9534de0", + "name": "No Operation, do nothing3", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 1380, + 780 + ] + } + ], + "pinData": { + "No Operation, do nothing": [ + { + "json": { + "data": "This is some text encoded as base64" + } + } + ], + "No Operation, do nothing1": [ + { + "json": { + "data": "Keeps asking about a green light??" + } + } + ], + "No Operation, do nothing2": [ + { + "json": { + "data": "[\n {\n \"id\": \"23423532\",\n \"name\": \"Jay Gatsby\",\n \"email\": \"gatsby@west-egg.com\",\n \"notes\": \"Keeps asking about a green light??\",\n \"country\": \"US\",\n \"created\": \"1925-04-10\",\n \"base64\": \"VGhpcyBpcyBzb21lIHRleHQgZW5jb2RlZCBhcyBiYXNlNjQ=\"\n }\n]" + } + } + ], + "No Operation, do nothing3": [ + { + "json": { + "data": "[{\"id\":\"23423532\",\"name\":\"Jay Gatsby\",\"email\":\"gatsby@west-egg.com\",\"notes\":\"Keeps asking about a green light??\",\"country\":\"US\",\"created\":\"1925-04-10\",\"base64\":\"VGhpcyBpcyBzb21lIHRleHQgZW5jb2RlZCBhcyBiYXNlNjQ=\"}]" + } + } + ] + }, + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Convert to File2", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File1", + "type": "main", + "index": 0 + }, + { + "node": "Convert to File3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File2": { + "main": [ + [ + { + "node": "Extract From File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File": { + "main": [ + [ + { + "node": "Extract From File1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File1": { + "main": [ + [ + { + "node": "Extract From File2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Convert to File3": { + "main": [ + [ + { + "node": "Extract From File3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From File": { + "main": [ + [ + { + "node": "No Operation, do nothing", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From File3": { + "main": [ + [ + { + "node": "No Operation, do nothing3", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From File2": { + "main": [ + [ + { + "node": "No Operation, do nothing2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract From File1": { + "main": [ + [ + { + "node": "No Operation, do nothing1", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "6555e00a-2c20-46f2-9a2c-fb368d557035", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "RSOCg1c3be66ZqVH", + "tags": [] +} diff --git a/packages/nodes-base/utils/binary.ts b/packages/nodes-base/utils/binary.ts index 825a0ca08ec33..551e2ea7c167d 100644 --- a/packages/nodes-base/utils/binary.ts +++ b/packages/nodes-base/utils/binary.ts @@ -26,6 +26,7 @@ export type JsonToBinaryOptions = { mimeType?: string; dataIsBase64?: boolean; itemIndex?: number; + format?: boolean; }; type PdfDocument = Awaited>['promise']>; @@ -102,7 +103,11 @@ export async function createBinaryFromJson( if (typeof value === 'object') { options.mimeType = 'application/json'; - valueAsString = JSON.stringify(value); + if (options.format) { + valueAsString = JSON.stringify(value, null, 2); + } else { + valueAsString = JSON.stringify(value); + } } buffer = iconv.encode(valueAsString, options.encoding || 'utf8', { From 76950e732e54245579ec79cd20cf19125c344b69 Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Tue, 13 Feb 2024 15:27:41 +0200 Subject: [PATCH 2/2] :zap: operation display name update --- .../nodes/Files/ConvertToFile/ConvertToFile.node.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts index 659eeb965b29f..8a2fd7f7d3a9d 100644 --- a/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts +++ b/packages/nodes-base/nodes/Files/ConvertToFile/ConvertToFile.node.ts @@ -68,6 +68,12 @@ export class ConvertToFile implements INodeType { action: 'Convert to RTF', description: 'Transform input data into a table in an RTF file', }, + { + name: 'Convert to Text File', + value: 'toText', + action: 'Convert to text file', + description: 'Transform input data string into a file', + }, { name: 'Convert to XLS', value: 'xls', @@ -86,12 +92,6 @@ export class ConvertToFile implements INodeType { action: 'Move base64 string to file', description: 'Convert a base64-encoded string into its original file format', }, - { - name: 'Move String to File', - value: 'toText', - action: 'Move string to file', - description: 'Transform input data string into a file', - }, ], default: 'csv', },