2022-01-03 13:42:42 -08:00
import { IExecuteFunctions } from 'n8n-core' ;
2019-06-23 03:35:23 -07:00
import {
2020-10-01 05:01:39 -07:00
IDataObject ,
2019-06-23 03:35:23 -07:00
INodeExecutionData ,
INodeType ,
INodeTypeDescription ,
2021-04-16 09:33:36 -07:00
NodeOperationError ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
import {
read as xlsxRead ,
2020-10-12 02:41:34 -07:00
Sheet2JSONOpts ,
2019-06-23 03:35:23 -07:00
utils as xlsxUtils ,
2020-10-01 05:01:39 -07:00
WorkBook ,
2019-06-23 03:35:23 -07:00
write as xlsxWrite ,
WritingOptions ,
} from 'xlsx' ;
/ * *
* Flattens an object with deep data
*
* @param { IDataObject } data The object to flatten
* @returns
* /
2021-05-07 20:50:25 -07:00
function flattenObject ( data : IDataObject ) {
2019-06-23 03:35:23 -07:00
const returnData : IDataObject = { } ;
for ( const key1 of Object . keys ( data ) ) {
2020-04-12 08:58:49 -07:00
if ( data [ key1 ] !== null && ( typeof data [ key1 ] ) === 'object' ) {
2021-07-21 05:30:38 -07:00
if ( data [ key1 ] instanceof Date ) {
returnData [ key1 ] = data [ key1 ] ? . toString ( ) ;
continue ;
}
2019-06-23 03:35:23 -07:00
const flatObject = flattenObject ( data [ key1 ] as IDataObject ) ;
for ( const key2 in flatObject ) {
if ( flatObject [ key2 ] === undefined ) {
continue ;
}
returnData [ ` ${ key1 } . ${ key2 } ` ] = flatObject [ key2 ] ;
}
} else {
returnData [ key1 ] = data [ key1 ] ;
}
}
return returnData ;
}
export class SpreadsheetFile implements INodeType {
description : INodeTypeDescription = {
displayName : 'Spreadsheet File' ,
name : 'spreadsheetFile' ,
icon : 'fa:table' ,
group : [ 'transform' ] ,
version : 1 ,
2021-07-03 05:40:16 -07:00
description : 'Reads and writes data from a spreadsheet file' ,
2019-06-23 03:35:23 -07:00
defaults : {
name : 'Spreadsheet File' ,
color : '#2244FF' ,
} ,
inputs : [ 'main' ] ,
outputs : [ 'main' ] ,
properties : [
{
displayName : 'Operation' ,
name : 'operation' ,
type : 'options' ,
options : [
{
name : 'Read from file' ,
value : 'fromFile' ,
description : 'Reads data from a spreadsheet file' ,
} ,
{
name : 'Write to file' ,
value : 'toFile' ,
description : 'Writes the workflow data to a spreadsheet file' ,
} ,
] ,
default : 'fromFile' ,
description : 'The operation to perform.' ,
} ,
// ----------------------------------
// fromFile
// ----------------------------------
{
displayName : 'Binary Property' ,
name : 'binaryPropertyName' ,
type : 'string' ,
default : 'data' ,
required : true ,
displayOptions : {
show : {
operation : [
'fromFile' ,
] ,
} ,
} ,
placeholder : '' ,
2021-10-27 13:00:13 -07:00
description : 'Name of the binary property from which to read the binary data of the spreadsheet file.' ,
2019-06-23 03:35:23 -07:00
} ,
// ----------------------------------
// toFile
// ----------------------------------
{
displayName : 'File Format' ,
name : 'fileFormat' ,
type : 'options' ,
options : [
{
2019-12-27 08:33:42 -08:00
name : 'CSV' ,
2019-06-23 03:35:23 -07:00
value : 'csv' ,
description : 'Comma-separated values' ,
} ,
{
2019-12-27 08:33:42 -08:00
name : 'HTML' ,
value : 'html' ,
description : 'HTML Table' ,
} ,
{
name : 'ODS' ,
2019-06-23 03:35:23 -07:00
value : 'ods' ,
description : 'OpenDocument Spreadsheet' ,
} ,
{
2019-12-27 08:33:42 -08:00
name : 'RTF' ,
2019-06-23 03:35:23 -07:00
value : 'rtf' ,
description : 'Rich Text Format' ,
} ,
{
2019-12-27 08:33:42 -08:00
name : 'XLS' ,
2019-06-23 03:35:23 -07:00
value : 'xls' ,
description : 'Excel' ,
} ,
2020-04-14 06:38:15 -07:00
{
name : 'XLSX' ,
value : 'xlsx' ,
description : 'Excel' ,
} ,
2019-06-23 03:35:23 -07:00
] ,
default : 'xls' ,
displayOptions : {
show : {
operation : [
2020-10-22 06:46:03 -07:00
'toFile' ,
2019-06-23 03:35:23 -07:00
] ,
} ,
} ,
description : 'The format of the file to save the data as.' ,
} ,
{
displayName : 'Binary Property' ,
name : 'binaryPropertyName' ,
type : 'string' ,
default : 'data' ,
required : true ,
displayOptions : {
show : {
operation : [
'toFile' ,
] ,
} ,
} ,
placeholder : '' ,
2021-10-27 13:00:13 -07:00
description : 'Name of the binary property in which to save the binary data of the spreadsheet file.' ,
2019-06-23 03:35:23 -07:00
} ,
2019-11-02 13:33:25 -07:00
{
displayName : 'Options' ,
name : 'options' ,
type : 'collection' ,
placeholder : 'Add Option' ,
default : { } ,
options : [
2020-04-29 14:12:16 -07:00
{
displayName : 'Compression' ,
name : 'compression' ,
type : 'boolean' ,
displayOptions : {
show : {
'/operation' : [
'toFile' ,
] ,
'/fileFormat' : [
'xlsx' ,
'ods' ,
] ,
} ,
} ,
default : false ,
description : 'Weather compression will be applied or not' ,
} ,
2019-11-02 13:33:25 -07:00
{
displayName : 'File Name' ,
name : 'fileName' ,
type : 'string' ,
2019-12-27 06:50:22 -08:00
displayOptions : {
show : {
'/operation' : [
'toFile' ,
] ,
} ,
} ,
2019-11-02 13:33:25 -07:00
default : '' ,
description : 'File name to set in binary data. By default will "spreadsheet.<fileFormat>" be used.' ,
} ,
2021-05-07 20:51:00 -07:00
{
displayName : 'Header Row' ,
name : 'headerRow' ,
type : 'boolean' ,
displayOptions : {
show : {
'/operation' : [
'fromFile' ,
] ,
} ,
} ,
default : true ,
description : 'The first row of the file contains the header names.' ,
} ,
2020-12-11 23:16:06 -08:00
{
2020-12-11 23:16:43 -08:00
displayName : 'Include Empty Cells' ,
2020-12-11 23:16:06 -08:00
name : 'includeEmptyCells' ,
type : 'boolean' ,
displayOptions : {
show : {
'/operation' : [
'fromFile' ,
] ,
} ,
} ,
default : false ,
description : 'When reading from file the empty cells will be filled with an empty string in the JSON.' ,
} ,
2019-12-27 06:50:22 -08:00
{
displayName : 'RAW Data' ,
name : 'rawData' ,
type : 'boolean' ,
displayOptions : {
show : {
'/operation' : [
2020-10-22 06:46:03 -07:00
'fromFile' ,
2019-12-27 06:50:22 -08:00
] ,
} ,
} ,
default : false ,
description : 'If the data should be returned RAW instead of parsed.' ,
} ,
2020-03-26 16:25:45 -07:00
{
displayName : 'Read As String' ,
name : 'readAsString' ,
type : 'boolean' ,
displayOptions : {
show : {
'/operation' : [
2020-10-22 06:46:03 -07:00
'fromFile' ,
2020-03-26 16:25:45 -07:00
] ,
} ,
} ,
default : false ,
2021-10-27 13:00:13 -07:00
description : 'In some cases and file formats, it is necessary to read specifically as string else some special character get interpreted wrong.' ,
2020-03-26 16:25:45 -07:00
} ,
2020-10-12 02:41:34 -07:00
{
displayName : 'Range' ,
name : 'range' ,
type : 'string' ,
displayOptions : {
show : {
'/operation' : [
2020-10-22 06:46:03 -07:00
'fromFile' ,
2020-10-12 02:41:34 -07:00
] ,
} ,
} ,
default : '' ,
2021-11-25 09:10:06 -08:00
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.' ,
2020-10-12 02:41:34 -07:00
} ,
2019-12-27 08:54:51 -08:00
{
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.' ,
} ,
2019-12-27 08:33:16 -08:00
{
displayName : 'Sheet Name' ,
name : 'sheetName' ,
type : 'string' ,
displayOptions : {
show : {
'/operation' : [
'toFile' ,
] ,
'/fileFormat' : [
'ods' ,
'xls' ,
2020-04-14 06:38:15 -07:00
'xlsx' ,
2019-12-27 08:33:16 -08:00
] ,
} ,
} ,
default : 'Sheet' ,
2019-12-27 08:54:51 -08:00
description : 'Name of the sheet to create in the spreadsheet.' ,
2019-12-27 08:33:16 -08:00
} ,
2019-11-02 13:33:25 -07:00
] ,
} ,
2020-10-22 06:46:03 -07:00
] ,
2019-06-23 03:35:23 -07:00
} ;
async execute ( this : IExecuteFunctions ) : Promise < INodeExecutionData [ ] [ ] > {
const items = this . getInputData ( ) ;
const operation = this . getNodeParameter ( 'operation' , 0 ) as string ;
const newItems : INodeExecutionData [ ] = [ ] ;
if ( operation === 'fromFile' ) {
// Read data from spreadsheet file to workflow
let item : INodeExecutionData ;
for ( let i = 0 ; i < items . length ; i ++ ) {
2021-07-19 23:58:54 -07:00
try {
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
item = items [ i ] ;
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
const binaryPropertyName = this . getNodeParameter ( 'binaryPropertyName' , i ) as string ;
const options = this . getNodeParameter ( 'options' , i , { } ) as IDataObject ;
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
if ( item . binary === undefined || item . binary [ binaryPropertyName ] === undefined ) {
// Property did not get found on item
continue ;
}
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
// Read the binary spreadsheet data
2022-01-03 13:42:42 -08:00
const binaryData = await this . helpers . getBinaryDataBuffer ( i , binaryPropertyName ) ;
2021-07-19 23:58:54 -07:00
let workbook ;
if ( options . readAsString === true ) {
workbook = xlsxRead ( binaryData . toString ( ) , { type : 'string' , raw : options.rawData as boolean } ) ;
} else {
workbook = xlsxRead ( binaryData , { raw : options.rawData as boolean } ) ;
}
2019-12-27 08:54:51 -08:00
2021-07-19 23:58:54 -07:00
if ( workbook . SheetNames . length === 0 ) {
throw new NodeOperationError ( this . getNode ( ) , 'Spreadsheet does not have any sheets!' ) ;
2019-12-27 08:54:51 -08:00
}
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
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 } "! ` ) ;
}
sheetName = options . sheetName as string ;
2020-10-12 02:41:34 -07:00
}
2021-07-19 23:58:54 -07:00
// 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 ) ;
}
}
2020-12-11 23:16:06 -08:00
2021-07-19 23:58:54 -07:00
if ( options . includeEmptyCells ) {
sheetToJsonOptions . defval = '' ;
}
if ( options . headerRow === false ) {
sheetToJsonOptions . header = 1 ; // Consider the first row as a data row
}
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
const sheetJson = xlsxUtils . sheet_to_json ( workbook . Sheets [ sheetName ] , sheetToJsonOptions ) ;
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
// Check if data could be found in file
if ( sheetJson . length === 0 ) {
continue ;
2021-05-07 20:50:25 -07:00
}
2021-07-19 23:58:54 -07:00
// 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 sheetJson ) {
newItems . push ( { json : { row : rowData } } as INodeExecutionData ) ;
}
} else {
for ( const rowData of sheetJson ) {
newItems . push ( { json : rowData } as INodeExecutionData ) ;
}
2021-05-07 20:50:25 -07:00
}
2021-07-19 23:58:54 -07:00
} catch ( error ) {
if ( this . continueOnFail ( ) ) {
newItems . push ( { json : { error : error.message } } ) ;
continue ;
}
throw error ;
2019-06-23 03:35:23 -07:00
}
}
return this . prepareOutputData ( newItems ) ;
} else if ( operation === 'toFile' ) {
2021-07-19 23:58:54 -07:00
try {
// Write the workflow data to spreadsheet file
const binaryPropertyName = this . getNodeParameter ( 'binaryPropertyName' , 0 ) as string ;
const fileFormat = this . getNodeParameter ( 'fileFormat' , 0 ) as string ;
const options = this . getNodeParameter ( 'options' , 0 , { } ) as IDataObject ;
// 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 ) ) ;
2020-04-29 14:12:16 -07:00
}
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
const ws = xlsxUtils . json_to_sheet ( itemData ) ;
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 ;
}
}
2019-11-02 13:33:25 -07:00
2021-07-19 23:58:54 -07:00
// Convert the data in the correct format
const sheetName = options . sheetName as string || 'Sheet' ;
const wb : WorkBook = {
SheetNames : [ sheetName ] ,
Sheets : {
[ sheetName ] : ws ,
} ,
} ;
const wbout = xlsxWrite ( wb , wopts ) ;
// Create a new item with only the binary spreadsheet data
const newItem : INodeExecutionData = {
json : { } ,
binary : { } ,
} ;
let fileName = ` spreadsheet. ${ fileFormat } ` ;
if ( options . fileName !== undefined ) {
fileName = options . fileName as string ;
}
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
newItem . binary ! [ binaryPropertyName ] = await this . helpers . prepareBinaryData ( wbout , fileName ) ;
2019-06-23 03:35:23 -07:00
2021-07-19 23:58:54 -07:00
newItems . push ( newItem ) ;
} catch ( error ) {
if ( this . continueOnFail ( ) ) {
newItems . push ( { json : { error : error.message } } ) ;
} else {
throw error ;
}
}
2019-06-23 03:35:23 -07:00
} else {
2021-07-19 23:58:54 -07:00
if ( this . continueOnFail ( ) ) {
return this . prepareOutputData ( [ { json : { error : ` The operation " ${ operation } " is not supported! ` } } ] ) ;
} else {
throw new NodeOperationError ( this . getNode ( ) , ` The operation " ${ operation } " is not supported! ` ) ;
}
2019-06-23 03:35:23 -07:00
}
2021-07-19 23:58:54 -07:00
return this . prepareOutputData ( newItems ) ;
2019-06-23 03:35:23 -07:00
}
}