2023-01-10 05:06:12 -08:00
import { ExpressionError , ExpressionExtensionError } from '../ExpressionError' ;
import type { ExtensionMap } from './Extensions' ;
2023-02-15 01:50:16 -08:00
import { compact as oCompact } from './ObjectExtensions' ;
import deepEqual from 'deep-equal' ;
2023-01-10 05:06:12 -08:00
function first ( value : unknown [ ] ) : unknown {
return value [ 0 ] ;
}
2023-02-02 03:35:38 -08:00
function isEmpty ( value : unknown [ ] ) : boolean {
2023-01-10 05:06:12 -08:00
return value . length === 0 ;
}
2023-02-02 03:35:38 -08:00
function isNotEmpty ( value : unknown [ ] ) : boolean {
2023-01-10 05:06:12 -08:00
return value . length > 0 ;
}
function last ( value : unknown [ ] ) : unknown {
return value [ value . length - 1 ] ;
}
function pluck ( value : unknown [ ] , extraArgs : unknown [ ] ) : unknown [ ] {
if ( ! Array . isArray ( extraArgs ) ) {
throw new ExpressionError ( 'arguments must be passed to pluck' ) ;
}
2023-02-15 01:50:16 -08:00
if ( ! extraArgs || extraArgs . length === 0 ) {
return value ;
}
const plucked = value . reduce < unknown [ ] > ( ( pluckedFromObject , current ) = > {
if ( current && typeof current === 'object' ) {
const p : unknown [ ] = [ ] ;
Object . keys ( current ) . forEach ( ( k ) = > {
extraArgs . forEach ( ( field : string ) = > {
if ( current && field === k ) {
p . push ( ( current as { [ key : string ] : unknown } ) [ k ] ) ;
}
} ) ;
} ) ;
if ( p . length > 0 ) {
pluckedFromObject . push ( p . length === 1 ? p [ 0 ] : p ) ;
2023-01-10 05:06:12 -08:00
}
2023-02-15 01:50:16 -08:00
}
return pluckedFromObject ;
} , new Array < unknown > ( ) ) ;
return plucked ;
2023-01-10 05:06:12 -08:00
}
2023-02-02 03:35:38 -08:00
function randomItem ( value : unknown [ ] ) : unknown {
2023-01-10 05:06:12 -08:00
const len = value === undefined ? 0 : value.length ;
return len ? value [ Math . floor ( Math . random ( ) * len ) ] : undefined ;
}
function unique ( value : unknown [ ] , extraArgs : string [ ] ) : unknown [ ] {
if ( extraArgs . length ) {
return value . reduce < unknown [ ] > ( ( l , v ) = > {
if ( typeof v === 'object' && v !== null && extraArgs . every ( ( i ) = > i in v ) ) {
const alreadySeen = l . find ( ( i ) = >
2023-02-15 01:50:16 -08:00
extraArgs . every ( ( j ) = >
deepEqual (
( i as Record < string , unknown > ) [ j ] ,
( v as Record < string , unknown > , { strict : true } ) [ j ] ,
{ strict : true } ,
) ,
) ,
2023-01-10 05:06:12 -08:00
) ;
if ( ! alreadySeen ) {
l . push ( v ) ;
}
}
return l ;
} , [ ] ) ;
}
return value . reduce < unknown [ ] > ( ( l , v ) = > {
2023-02-15 01:50:16 -08:00
if ( l . findIndex ( ( i ) = > deepEqual ( i , v , { strict : true } ) ) === - 1 ) {
2023-01-10 05:06:12 -08:00
l . push ( v ) ;
}
return l ;
} , [ ] ) ;
}
2023-02-02 03:35:38 -08:00
const ensureNumberArray = ( arr : unknown [ ] , { fnName } : { fnName : string } ) = > {
if ( arr . some ( ( i ) = > typeof i !== 'number' ) ) {
throw new ExpressionExtensionError ( ` ${ fnName } (): all array elements must be numbers ` ) ;
}
} ;
2023-01-10 05:06:12 -08:00
function sum ( value : unknown [ ] ) : number {
2023-02-02 03:35:38 -08:00
ensureNumberArray ( value , { fnName : 'sum' } ) ;
2023-01-10 05:06:12 -08:00
return value . reduce ( ( p : number , c : unknown ) = > {
if ( typeof c === 'string' ) {
return p + parseFloat ( c ) ;
}
if ( typeof c !== 'number' ) {
return NaN ;
}
return p + c ;
} , 0 ) ;
}
function min ( value : unknown [ ] ) : number {
2023-02-02 03:35:38 -08:00
ensureNumberArray ( value , { fnName : 'min' } ) ;
2023-01-10 05:06:12 -08:00
return Math . min (
. . . value . map ( ( v ) = > {
if ( typeof v === 'string' ) {
return parseFloat ( v ) ;
}
if ( typeof v !== 'number' ) {
return NaN ;
}
return v ;
} ) ,
) ;
}
function max ( value : unknown [ ] ) : number {
2023-02-02 03:35:38 -08:00
ensureNumberArray ( value , { fnName : 'max' } ) ;
2023-01-10 05:06:12 -08:00
return Math . max (
. . . value . map ( ( v ) = > {
if ( typeof v === 'string' ) {
return parseFloat ( v ) ;
}
if ( typeof v !== 'number' ) {
return NaN ;
}
return v ;
} ) ,
) ;
}
export function average ( value : unknown [ ] ) {
2023-02-02 03:35:38 -08:00
ensureNumberArray ( value , { fnName : 'average' } ) ;
2023-01-10 05:06:12 -08:00
// This would usually be NaN but I don't think users
// will expect that
if ( value . length === 0 ) {
return 0 ;
}
return sum ( value ) / value . length ;
}
function compact ( value : unknown [ ] ) : unknown [ ] {
return value
2023-02-02 03:35:38 -08:00
. filter ( ( v ) = > {
if ( v && typeof v === 'object' && Object . keys ( v ) . length === 0 ) return false ;
return v !== null && v !== undefined && v !== 'nil' && v !== '' ;
} )
2023-01-10 05:06:12 -08:00
. map ( ( v ) = > {
if ( typeof v === 'object' && v !== null ) {
return oCompact ( v ) ;
}
return v ;
} ) ;
}
function smartJoin ( value : unknown [ ] , extraArgs : string [ ] ) : object {
const [ keyField , valueField ] = extraArgs ;
if ( ! keyField || ! valueField || typeof keyField !== 'string' || typeof valueField !== 'string' ) {
throw new ExpressionExtensionError (
2023-02-02 03:35:38 -08:00
'smartJoin(): expected two string args, e.g. .smartJoin("name", "value")' ,
2023-01-10 05:06:12 -08:00
) ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
return value . reduce < any > ( ( o , v ) = > {
if ( typeof v === 'object' && v !== null && keyField in v && valueField in v ) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
o [ ( v as any ) [ keyField ] ] = ( v as any ) [ valueField ] ;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return o ;
} , { } ) ;
}
function chunk ( value : unknown [ ] , extraArgs : number [ ] ) {
const [ chunkSize ] = extraArgs ;
2023-02-02 03:35:38 -08:00
if ( typeof chunkSize !== 'number' || chunkSize === 0 ) {
throw new ExpressionExtensionError ( 'chunk(): expected non-zero numeric arg, e.g. .chunk(5)' ) ;
2023-01-10 05:06:12 -08:00
}
const chunks : unknown [ ] [ ] = [ ] ;
for ( let i = 0 ; i < value . length ; i += chunkSize ) {
// I have no clue why eslint thinks 2 numbers could be anything but that but here we are
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
chunks . push ( value . slice ( i , i + chunkSize ) ) ;
}
return chunks ;
}
function renameKeys ( value : unknown [ ] , extraArgs : string [ ] ) : unknown [ ] {
if ( extraArgs . length === 0 || extraArgs . length % 2 !== 0 ) {
throw new ExpressionExtensionError (
2023-02-02 03:35:38 -08:00
'renameKeys(): expected an even amount of args: from1, to1 [, from2, to2, ...]. e.g. .renameKeys("name", "title")' ,
2023-01-10 05:06:12 -08:00
) ;
}
return value . map ( ( v ) = > {
if ( typeof v !== 'object' || v === null ) {
return v ;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
const newObj = { . . . ( v as any ) } ;
const chunkedArgs = chunk ( extraArgs , [ 2 ] ) as string [ ] [ ] ;
chunkedArgs . forEach ( ( [ from , to ] ) = > {
if ( from in newObj ) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
newObj [ to ] = newObj [ from ] ;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
delete newObj [ from ] ;
}
} ) ;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return newObj ;
} ) ;
}
2023-02-15 01:50:16 -08:00
function mergeObjects ( value : Record < string , unknown > , extraArgs : unknown [ ] ) : unknown {
const [ other ] = extraArgs ;
if ( ! other ) {
return value ;
}
if ( typeof other !== 'object' ) {
throw new ExpressionExtensionError ( 'merge(): expected object arg' ) ;
}
const newObject = { . . . value } ;
for ( const [ key , val ] of Object . entries ( other ) ) {
if ( ! ( key in newObject ) ) {
newObject [ key ] = val ;
}
}
return newObject ;
}
function merge ( value : unknown [ ] , extraArgs : unknown [ ] [ ] ) : unknown {
2023-01-10 05:06:12 -08:00
const [ others ] = extraArgs ;
2023-02-15 01:50:16 -08:00
if ( others === undefined ) {
// If there are no arguments passed, merge all objects within the array
const merged = value . reduce ( ( combined , current ) = > {
if ( current !== null && typeof current === 'object' && ! Array . isArray ( current ) ) {
combined = mergeObjects ( combined as Record < string , unknown > , [ current ] ) ;
}
return combined ;
} , { } ) ;
return merged ;
}
2023-01-10 05:06:12 -08:00
if ( ! Array . isArray ( others ) ) {
throw new ExpressionExtensionError (
2023-02-02 03:35:38 -08:00
'merge(): expected array arg, e.g. .merge([{ id: 1, otherValue: 3 }])' ,
2023-01-10 05:06:12 -08:00
) ;
}
const listLength = value . length > others . length ? value.length : others.length ;
2023-02-15 01:50:16 -08:00
let merged = { } ;
2023-01-10 05:06:12 -08:00
for ( let i = 0 ; i < listLength ; i ++ ) {
if ( value [ i ] !== undefined ) {
if ( typeof value [ i ] === 'object' && typeof others [ i ] === 'object' ) {
2023-02-15 01:50:16 -08:00
merged = Object . assign (
merged ,
mergeObjects ( value [ i ] as Record < string , unknown > , [ others [ i ] ] ) ,
) ;
2023-01-10 05:06:12 -08:00
}
}
}
2023-02-15 01:50:16 -08:00
return merged ;
2023-01-10 05:06:12 -08:00
}
function union ( value : unknown [ ] , extraArgs : unknown [ ] [ ] ) : unknown [ ] {
const [ others ] = extraArgs ;
if ( ! Array . isArray ( others ) ) {
2023-02-02 03:35:38 -08:00
throw new ExpressionExtensionError ( 'union(): expected array arg, e.g. .union([1, 2, 3, 4])' ) ;
2023-01-10 05:06:12 -08:00
}
const newArr : unknown [ ] = Array . from ( value ) ;
for ( const v of others ) {
2023-02-15 01:50:16 -08:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if ( newArr . findIndex ( ( w ) = > deepEqual ( w , v , { strict : true } ) ) === - 1 ) {
2023-01-10 05:06:12 -08:00
newArr . push ( v ) ;
}
}
return unique ( newArr , [ ] ) ;
}
function difference ( value : unknown [ ] , extraArgs : unknown [ ] [ ] ) : unknown [ ] {
const [ others ] = extraArgs ;
if ( ! Array . isArray ( others ) ) {
throw new ExpressionExtensionError (
2023-02-02 03:35:38 -08:00
'difference(): expected array arg, e.g. .difference([1, 2, 3, 4])' ,
2023-01-10 05:06:12 -08:00
) ;
}
const newArr : unknown [ ] = [ ] ;
for ( const v of value ) {
2023-02-15 01:50:16 -08:00
if ( others . findIndex ( ( w ) = > deepEqual ( w , v , { strict : true } ) ) === - 1 ) {
2023-01-10 05:06:12 -08:00
newArr . push ( v ) ;
}
}
return unique ( newArr , [ ] ) ;
}
function intersection ( value : unknown [ ] , extraArgs : unknown [ ] [ ] ) : unknown [ ] {
const [ others ] = extraArgs ;
if ( ! Array . isArray ( others ) ) {
throw new ExpressionExtensionError (
2023-02-02 03:35:38 -08:00
'intersection(): expected array arg, e.g. .intersection([1, 2, 3, 4])' ,
2023-01-10 05:06:12 -08:00
) ;
}
const newArr : unknown [ ] = [ ] ;
for ( const v of value ) {
2023-02-15 01:50:16 -08:00
if ( others . findIndex ( ( w ) = > deepEqual ( w , v , { strict : true } ) ) !== - 1 ) {
2023-01-10 05:06:12 -08:00
newArr . push ( v ) ;
}
}
for ( const v of others ) {
2023-02-15 01:50:16 -08:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if ( value . findIndex ( ( w ) = > deepEqual ( w , v , { strict : true } ) ) !== - 1 ) {
2023-01-10 05:06:12 -08:00
newArr . push ( v ) ;
}
}
return unique ( newArr , [ ] ) ;
}
2023-02-02 03:35:38 -08:00
average . doc = {
name : 'average' ,
2023-02-27 20:34:03 -08:00
description : 'Returns the mean average of all values in the array.' ,
2023-02-02 03:35:38 -08:00
returnType : 'number' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-average' ,
2023-02-02 03:35:38 -08:00
} ;
compact . doc = {
name : 'compact' ,
2023-02-27 20:34:03 -08:00
description : 'Removes all empty values from the array.' ,
returnType : 'Array' ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-compact' ,
2023-02-02 03:35:38 -08:00
} ;
isEmpty . doc = {
name : 'isEmpty' ,
2023-02-27 20:34:03 -08:00
description : 'Checks if the array doesn’ t have any elements.' ,
2023-02-02 03:35:38 -08:00
returnType : 'boolean' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-isEmpty' ,
2023-02-02 03:35:38 -08:00
} ;
isNotEmpty . doc = {
name : 'isNotEmpty' ,
2023-02-27 20:34:03 -08:00
description : 'Checks if the array has elements.' ,
2023-02-02 03:35:38 -08:00
returnType : 'boolean' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-isNotEmpty' ,
2023-02-02 03:35:38 -08:00
} ;
first . doc = {
name : 'first' ,
2023-02-27 20:34:03 -08:00
description : 'Returns the first element of the array.' ,
returnType : 'Element' ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-first' ,
2023-02-02 03:35:38 -08:00
} ;
last . doc = {
name : 'last' ,
2023-02-27 20:34:03 -08:00
description : 'Returns the last element of the array.' ,
returnType : 'Element' ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-last' ,
2023-02-02 03:35:38 -08:00
} ;
max . doc = {
name : 'max' ,
2023-02-27 20:34:03 -08:00
description : 'Gets the maximum value from a number-only array.' ,
2023-02-02 03:35:38 -08:00
returnType : 'number' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-max' ,
2023-02-02 03:35:38 -08:00
} ;
min . doc = {
name : 'min' ,
2023-02-27 20:34:03 -08:00
description : 'Gets the minimum value from a number-only array.' ,
2023-02-02 03:35:38 -08:00
returnType : 'number' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-min' ,
2023-02-02 03:35:38 -08:00
} ;
randomItem . doc = {
name : 'randomItem' ,
2023-02-27 20:34:03 -08:00
description : 'Returns a random element from an array.' ,
returnType : 'Element' ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-randomItem' ,
2023-02-02 03:35:38 -08:00
} ;
sum . doc = {
name : 'sum' ,
2023-02-27 20:34:03 -08:00
description : 'Returns the total sum all the values in an array of parsable numbers.' ,
2023-02-02 03:35:38 -08:00
returnType : 'number' ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-sum' ,
2023-02-02 03:35:38 -08:00
} ;
chunk . doc = {
name : 'chunk' ,
2023-02-27 20:34:03 -08:00
description : 'Splits arrays into chunks with a length of `size`.' ,
returnType : 'Array' ,
args : [ { name : 'size' , type : 'number' } ] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-chunk' ,
2023-02-02 03:35:38 -08:00
} ;
difference . doc = {
name : 'difference' ,
2023-02-27 20:34:03 -08:00
description :
'Compares two arrays. Returns all elements in the base array that aren’ t present in `arr`.' ,
returnType : 'Array' ,
args : [ { name : 'arr' , type : 'Array' } ] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-difference' ,
2023-02-02 03:35:38 -08:00
} ;
intersection . doc = {
name : 'intersection' ,
2023-02-27 20:34:03 -08:00
description :
'Compares two arrays. Returns all elements in the base array that are present in `arr`.' ,
returnType : 'Array' ,
args : [ { name : 'arr' , type : 'Array' } ] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-intersection' ,
2023-02-02 03:35:38 -08:00
} ;
merge . doc = {
name : 'merge' ,
2023-02-27 20:34:03 -08:00
description :
'Merges two Object-arrays into one array by merging the key-value pairs of each element.' ,
2023-02-02 03:35:38 -08:00
returnType : 'array' ,
2023-02-27 20:34:03 -08:00
args : [ { name : 'arr' , type : 'Array' } ] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-merge' ,
2023-02-02 03:35:38 -08:00
} ;
pluck . doc = {
name : 'pluck' ,
2023-02-27 20:34:03 -08:00
description : 'Returns an array of Objects where the key is equal the given `fieldName`s.' ,
returnType : 'Array' ,
args : [
{ name : 'fieldName1' , type : 'string' } ,
{ name : 'fieldName1?' , type : 'string' } ,
{ name : '...' } ,
{ name : 'fieldNameN?' , type : 'string' } ,
] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-pluck' ,
2023-02-02 03:35:38 -08:00
} ;
renameKeys . doc = {
name : 'renameKeys' ,
2023-02-27 20:34:03 -08:00
description : 'Renames all matching keys in the array.' ,
returnType : 'Array' ,
args : [
{ name : 'from1' , type : 'string' } ,
{ name : 'to1' , type : 'string' } ,
{ name : 'from2?' , type : 'string' } ,
{ name : 'to2?' , type : 'string' } ,
{ name : '...' } ,
{ name : 'fromN?' , type : 'string' } ,
{ name : 'toN?' , type : 'string' } ,
] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-renameKeys' ,
2023-02-02 03:35:38 -08:00
} ;
smartJoin . doc = {
name : 'smartJoin' ,
2023-02-27 20:34:03 -08:00
description :
'Operates on an array of objects where each object contains key-value pairs. Creates a new object containing key-value pairs, where the key is the value of the first pair, and the value is the value of the second pair. Removes non-matching and empty values and trims any whitespace before joining.' ,
returnType : 'Array' ,
args : [
{ name : 'keyField' , type : 'string' } ,
{ name : 'nameField' , type : 'string' } ,
] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-smartJoin' ,
2023-02-02 03:35:38 -08:00
} ;
union . doc = {
name : 'union' ,
2023-02-27 20:34:03 -08:00
description : 'Concatenates two arrays and then removes duplicates.' ,
returnType : 'Array' ,
args : [ { name : 'arr' , type : 'Array' } ] ,
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-union' ,
2023-02-02 03:35:38 -08:00
} ;
unique . doc = {
name : 'unique' ,
2023-02-27 20:34:03 -08:00
description : 'Remove duplicates from an array. ' ,
returnType : 'Element' ,
2023-02-02 03:35:38 -08:00
aliases : [ 'removeDuplicates' ] ,
2023-02-27 20:34:03 -08:00
docURL :
'https://docs.n8n.io/code-examples/expressions/data-transformation-functions/arrays/#array-unique' ,
2023-02-02 03:35:38 -08:00
} ;
2023-01-10 05:06:12 -08:00
export const arrayExtensions : ExtensionMap = {
typeName : 'Array' ,
functions : {
2023-02-02 03:35:38 -08:00
removeDuplicates : unique ,
2023-02-27 20:34:03 -08:00
unique ,
2023-01-10 05:06:12 -08:00
first ,
last ,
pluck ,
2023-02-02 03:35:38 -08:00
randomItem ,
2023-01-10 05:06:12 -08:00
sum ,
min ,
max ,
average ,
2023-02-02 03:35:38 -08:00
isNotEmpty ,
isEmpty ,
2023-01-10 05:06:12 -08:00
compact ,
smartJoin ,
chunk ,
renameKeys ,
merge ,
union ,
difference ,
intersection ,
} ,
} ;