2023-01-27 03:22:44 -08:00
import type {
2023-03-09 09:13:15 -08:00
IExecuteFunctions ,
2022-11-15 05:57:07 -08:00
IDataObject ,
INodeExecutionData ,
INodeListSearchItems ,
INodePropertyOptions ,
} from 'n8n-workflow' ;
2023-01-27 03:22:44 -08:00
import { NodeOperationError } from 'n8n-workflow' ;
2022-12-21 01:46:26 -08:00
import type { GoogleSheet } from './GoogleSheet' ;
2023-01-27 03:22:44 -08:00
import type {
2022-11-15 05:57:07 -08:00
RangeDetectionOptions ,
ResourceLocator ,
SheetRangeData ,
ValueInputOption ,
} from './GoogleSheets.types' ;
2023-01-27 03:22:44 -08:00
import { ResourceLocatorUiNames , ROW_NUMBER } from './GoogleSheets.types' ;
2022-11-15 05:57:07 -08:00
export const untilSheetSelected = { sheetName : [ '' ] } ;
// Used to extract the ID from the URL
export function getSpreadsheetId ( documentIdType : ResourceLocator , value : string ) : string {
if ( ! value ) {
throw new Error (
` Can not get sheet ' ${ ResourceLocatorUiNames [ documentIdType ] } ' with a value of ' ${ value } ' ` ,
) ;
}
if ( documentIdType === 'url' ) {
const regex = /([-\w]{25,})/ ;
const parts = value . match ( regex ) ;
if ( parts == null || parts . length < 2 ) {
return '' ;
} else {
return parts [ 0 ] ;
}
}
// If it is byID or byList we can just return
return value ;
}
// Convert number to Sheets / Excel column name
export function getColumnName ( colNumber : number ) : string {
const baseChar = 'A' . charCodeAt ( 0 ) ;
let letters = '' ;
do {
colNumber -= 1 ;
letters = String . fromCharCode ( baseChar + ( colNumber % 26 ) ) + letters ;
colNumber = ( colNumber / 26 ) >> 0 ;
} while ( colNumber > 0 ) ;
return letters ;
}
// Convert Column Name to Number (A = 1, B = 2, AA = 27)
export function getColumnNumber ( colPosition : string ) : number {
let colNum = 0 ;
for ( let i = 0 ; i < colPosition . length ; i ++ ) {
colNum *= 26 ;
colNum += colPosition [ i ] . charCodeAt ( 0 ) - 'A' . charCodeAt ( 0 ) + 1 ;
}
return colNum ;
}
// Hex to RGB
export function hexToRgb ( hex : string ) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i ;
hex = hex . replace ( shorthandRegex , ( m , r , g , b ) = > {
2023-01-13 09:11:56 -08:00
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
2022-11-15 05:57:07 -08:00
return r + r + g + g + b + b ;
} ) ;
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i . exec ( hex ) ;
if ( result ) {
return {
red : parseInt ( result [ 1 ] , 16 ) ,
green : parseInt ( result [ 2 ] , 16 ) ,
blue : parseInt ( result [ 3 ] , 16 ) ,
} ;
} else {
return null ;
}
}
export function addRowNumber ( data : SheetRangeData , headerRow : number ) {
if ( data . length === 0 ) return data ;
const sheetData = data . map ( ( row , i ) = > [ i + 1 , . . . row ] ) ;
sheetData [ headerRow ] [ 0 ] = ROW_NUMBER ;
return sheetData ;
}
export function trimToFirstEmptyRow ( data : SheetRangeData , includesRowNumber = true ) {
const baseLength = includesRowNumber ? 1 : 0 ;
const emtyRowIndex = data . findIndex ( ( row ) = > row . slice ( baseLength ) . every ( ( cell ) = > cell === '' ) ) ;
if ( emtyRowIndex === - 1 ) {
return data ;
}
return data . slice ( 0 , emtyRowIndex ) ;
}
export function removeEmptyRows ( data : SheetRangeData , includesRowNumber = true ) {
const baseLength = includesRowNumber ? 1 : 0 ;
const notEmptyRows = data . filter ( ( row ) = >
row . slice ( baseLength ) . some ( ( cell ) = > cell || typeof cell === 'number' ) ,
) ;
if ( includesRowNumber ) {
notEmptyRows [ 0 ] [ 0 ] = ROW_NUMBER ;
}
return notEmptyRows ;
}
export function trimLeadingEmptyRows (
data : SheetRangeData ,
includesRowNumber = true ,
rowNumbersColumnName = ROW_NUMBER ,
) {
const baseLength = includesRowNumber ? 1 : 0 ;
const firstNotEmptyRowIndex = data . findIndex ( ( row ) = >
row . slice ( baseLength ) . some ( ( cell ) = > cell || typeof cell === 'number' ) ,
) ;
const returnData = data . slice ( firstNotEmptyRowIndex ) ;
if ( includesRowNumber ) {
returnData [ 0 ] [ 0 ] = rowNumbersColumnName ;
}
return returnData ;
}
export function removeEmptyColumns ( data : SheetRangeData ) {
2022-11-29 03:29:47 -08:00
if ( ! data || data . length === 0 ) return [ ] ;
2022-11-15 05:57:07 -08:00
const returnData : SheetRangeData = [ ] ;
const longestRow = data . reduce ( ( a , b ) = > ( a . length > b . length ? a : b ) , [ ] ) . length ;
for ( let col = 0 ; col < longestRow ; col ++ ) {
const column = data . map ( ( row ) = > row [ col ] ) ;
2023-01-05 14:25:28 -08:00
if ( column [ 0 ] !== '' ) {
returnData . push ( column ) ;
continue ;
}
2022-11-15 05:57:07 -08:00
const hasData = column . slice ( 1 ) . some ( ( cell ) = > cell || typeof cell === 'number' ) ;
if ( hasData ) {
returnData . push ( column ) ;
}
}
2022-11-29 03:29:47 -08:00
return ( returnData [ 0 ] || [ ] ) . map ( ( _ , i ) = > returnData . map ( ( row ) = > row [ i ] || '' ) ) ;
2022-11-15 05:57:07 -08:00
}
export function prepareSheetData (
data : SheetRangeData ,
options : RangeDetectionOptions ,
addRowNumbersToData = true ,
) {
let returnData : SheetRangeData = [ . . . ( data || [ ] ) ] ;
let headerRow = 0 ;
let firstDataRow = 1 ;
if ( options . rangeDefinition === 'specifyRange' ) {
headerRow = parseInt ( options . headerRow as string , 10 ) - 1 ;
firstDataRow = parseInt ( options . firstDataRow as string , 10 ) - 1 ;
}
if ( addRowNumbersToData ) {
returnData = addRowNumber ( returnData , headerRow ) ;
}
if ( options . rangeDefinition === 'detectAutomatically' ) {
returnData = removeEmptyColumns ( returnData ) ;
returnData = trimLeadingEmptyRows ( returnData , addRowNumbersToData ) ;
if ( options . readRowsUntil === 'firstEmptyRow' ) {
returnData = trimToFirstEmptyRow ( returnData , addRowNumbersToData ) ;
} else {
returnData = removeEmptyRows ( returnData , addRowNumbersToData ) ;
}
}
return { data : returnData , headerRow , firstDataRow } ;
}
export function getRangeString ( sheetName : string , options : RangeDetectionOptions ) {
if ( options . rangeDefinition === 'specifyRangeA1' ) {
2022-12-02 12:54:28 -08:00
return options . range ? ` ${ sheetName } ! ${ options . range } ` : sheetName ;
2022-11-15 05:57:07 -08:00
}
return sheetName ;
}
export async function getExistingSheetNames ( sheet : GoogleSheet ) {
const { sheets } = await sheet . spreadsheetGetSheets ( ) ;
2022-12-02 12:54:28 -08:00
return ( ( sheets as IDataObject [ ] ) || [ ] ) . map ( ( entry ) = > ( entry . properties as IDataObject ) ? . title ) ;
2022-11-15 05:57:07 -08:00
}
export function mapFields ( this : IExecuteFunctions , inputSize : number ) {
const returnData : IDataObject [ ] = [ ] ;
for ( let i = 0 ; i < inputSize ; i ++ ) {
const fields = this . getNodeParameter ( 'fieldsUi.fieldValues' , i , [ ] ) as IDataObject [ ] ;
let dataToSend : IDataObject = { } ;
for ( const field of fields ) {
dataToSend = { . . . dataToSend , [ field . fieldId as string ] : field . fieldValue } ;
}
returnData . push ( dataToSend ) ;
}
return returnData ;
}
export async function autoMapInputData (
this : IExecuteFunctions ,
sheetNameWithRange : string ,
sheet : GoogleSheet ,
items : INodeExecutionData [ ] ,
options : IDataObject ,
) {
const returnData : IDataObject [ ] = [ ] ;
const [ sheetName , _sheetRange ] = sheetNameWithRange . split ( '!' ) ;
2022-12-02 12:54:28 -08:00
const locationDefine = ( options . locationDefine as IDataObject ) ? . values as IDataObject ;
2022-11-15 05:57:07 -08:00
const handlingExtraData = ( options . handlingExtraData as string ) || 'insertInNewColumn' ;
let headerRow = 1 ;
if ( locationDefine ) {
headerRow = parseInt ( locationDefine . headerRow as string , 10 ) ;
}
let columnNames : string [ ] = [ ] ;
const response = await sheet . getData ( ` ${ sheetName } ! ${ headerRow } : ${ headerRow } ` , 'FORMATTED_VALUE' ) ;
columnNames = response ? response [ 0 ] : [ ] ;
if ( handlingExtraData === 'insertInNewColumn' ) {
if ( ! columnNames . length ) {
await sheet . updateRows (
sheetName ,
[ Object . keys ( items [ 0 ] . json ) . filter ( ( key ) = > key !== ROW_NUMBER ) ] ,
( options . cellFormat as ValueInputOption ) || 'RAW' ,
headerRow ,
) ;
columnNames = Object . keys ( items [ 0 ] . json ) ;
}
const newColumns = new Set < string > ( ) ;
items . forEach ( ( item ) = > {
Object . keys ( item . json ) . forEach ( ( key ) = > {
2022-12-02 12:54:28 -08:00
if ( key !== ROW_NUMBER && ! columnNames . includes ( key ) ) {
2022-11-15 05:57:07 -08:00
newColumns . add ( key ) ;
}
} ) ;
if ( item . json [ ROW_NUMBER ] ) {
delete item . json [ ROW_NUMBER ] ;
}
returnData . push ( item . json ) ;
} ) ;
if ( newColumns . size ) {
await sheet . updateRows (
sheetName ,
[ columnNames . concat ( [ . . . newColumns ] ) ] ,
( options . cellFormat as ValueInputOption ) || 'RAW' ,
headerRow ,
) ;
}
}
if ( handlingExtraData === 'ignoreIt' ) {
items . forEach ( ( item ) = > {
returnData . push ( item . json ) ;
} ) ;
}
if ( handlingExtraData === 'error' ) {
items . forEach ( ( item , itemIndex ) = > {
Object . keys ( item . json ) . forEach ( ( key ) = > {
2022-12-02 12:54:28 -08:00
if ( ! columnNames . includes ( key ) ) {
2022-12-29 03:20:43 -08:00
throw new NodeOperationError ( this . getNode ( ) , 'Unexpected fields in node input' , {
2022-11-15 05:57:07 -08:00
itemIndex ,
description : ` The input field ' ${ key } ' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'. ` ,
} ) ;
}
} ) ;
returnData . push ( item . json ) ;
} ) ;
}
return returnData ;
}
export function sortLoadOptions ( data : INodePropertyOptions [ ] | INodeListSearchItems [ ] ) {
const returnData = [ . . . data ] ;
returnData . sort ( ( a , b ) = > {
2022-12-02 12:54:28 -08:00
const aName = a . name . toLowerCase ( ) ;
const bName = b . name . toLowerCase ( ) ;
2022-11-15 05:57:07 -08:00
if ( aName < bName ) {
return - 1 ;
}
if ( aName > bName ) {
return 1 ;
}
return 0 ;
} ) ;
return returnData ;
}