diff --git a/packages/workflow/src/Extensions/ArrayExtensions.ts b/packages/workflow/src/Extensions/ArrayExtensions.ts new file mode 100644 index 0000000000000..3c21dc77f40b7 --- /dev/null +++ b/packages/workflow/src/Extensions/ArrayExtensions.ts @@ -0,0 +1,110 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ +// eslint-disable-next-line import/no-cycle +import { ExpressionError } from '../ExpressionError'; +import { BaseExtension, ExtensionMethodHandler } from './Extensions'; + +export class ArrayExtensions extends BaseExtension { + methodMapping = new Map>(); + + constructor() { + super(); + this.initializeMethodMap(); + } + + bind(mainArg: any[], extraArgs?: number[] | string[] | boolean[] | undefined) { + return Array.from(this.methodMapping).reduce((p, c) => { + const [key, method] = c; + Object.assign(p, { + [key]: () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return method.call(this, mainArg, extraArgs); + }, + }); + return p; + }, {} as object); + } + + private initializeMethodMap(): void { + this.methodMapping = new Map< + string, + ( + value: any[], + extraArgs?: number[] | string[] | boolean[] | undefined, + ) => any[] | boolean | string | Date | number + >([ + ['duplicates', this.unique], + ['isPresent', this.isPresent], + ['filter', this.filter], + ['first', this.first], + ['last', this.last], + ['length', this.length], + ['pluck', this.pluck], + ['unique', this.unique], + ['random', this.random], + ['remove', this.unique], + ]); + } + + filter(value: any[], extraArgs?: any[]): any[] { + if (!Array.isArray(extraArgs)) { + throw new ExpressionError('arguments must be passed to filter'); + } + const terms = extraArgs as string[] | number[]; + return value.filter((v: string | number) => (terms as Array).includes(v)); + } + + first(value: any[]): any { + return value[0]; + } + + isBlank(value: any[]): boolean { + return Array.isArray(value) && value.length === 0; + } + + isPresent(value: any[], extraArgs?: any[]): boolean { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + if (!Array.isArray(extraArgs)) { + throw new ExpressionError('arguments must be passed to isPresent'); + } + const comparators = extraArgs as string[] | number[]; + return value.some((v: string | number) => { + return (comparators as Array).includes(v); + }); + } + + last(value: any[]): any { + return value[value.length - 1]; + } + + length(value: any[]): number { + return Array.isArray(value) ? value.length : 0; + } + + pluck(value: any[], extraArgs: any[]): any[] { + if (!Array.isArray(extraArgs)) { + throw new ExpressionError('arguments must be passed to pluck'); + } + const fieldsToPluck = extraArgs; + return value.map((element: object) => { + const entries = Object.entries(element); + return entries.reduce((p, c) => { + const [key, val] = c as [string, Date | string | number]; + if (fieldsToPluck.includes(key)) { + Object.assign(p, { [key]: val }); + } + return p; + }, {}); + }); + } + + random(value: any[]): any { + const length = value == null ? 0 : value.length; + return length ? value[Math.floor(Math.random() * length)] : undefined; + } + + unique(value: any[]): any[] { + return Array.from(new Set(value)); + } +} diff --git a/packages/workflow/src/Extensions/ExpressionExtension.ts b/packages/workflow/src/Extensions/ExpressionExtension.ts index 6323bf7cd1b54..7622b7063ebea 100644 --- a/packages/workflow/src/Extensions/ExpressionExtension.ts +++ b/packages/workflow/src/Extensions/ExpressionExtension.ts @@ -12,16 +12,19 @@ import { ExpressionExtensionError } from '../ExpressionError'; import { DateExtensions } from './DateExtensions'; import { StringExtensions } from './StringExtensions'; +import { ArrayExtensions } from './ArrayExtensions'; const EXPRESSION_EXTENDER = 'extend'; const stringExtensions = new StringExtensions(); const dateExtensions = new DateExtensions(); +const arrayExtensions = new ArrayExtensions(); const EXPRESSION_EXTENSION_METHODS = Array.from( new Set([ ...stringExtensions.listMethods(), ...dateExtensions.listMethods(), + ...arrayExtensions.listMethods(), 'toDecimal', 'isBlank', 'toLocaleString', @@ -137,6 +140,10 @@ export function extend(mainArg: unknown, ...extraArgs: unknown[]): ExtMethods { return stringExtensions.isBlank(mainArg); } + if (Array.isArray(mainArg)) { + return arrayExtensions.isBlank(mainArg); + } + return true; }, toLocaleString(): string { @@ -147,6 +154,10 @@ export function extend(mainArg: unknown, ...extraArgs: unknown[]): ExtMethods { new Date(mainArg as string), extraArgs as number[] | string[] | boolean[] | undefined, ), + ...arrayExtensions.bind( + Array.isArray(mainArg) ? mainArg : ([mainArg] as unknown[]), + extraArgs as string[] | undefined, + ), }; return extensions; diff --git a/packages/workflow/test/ExpressionExtensions.test.ts b/packages/workflow/test/ExpressionExtensions.test.ts index 2c606fb692e9f..196b4fbee6c6d 100644 --- a/packages/workflow/test/ExpressionExtensions.test.ts +++ b/packages/workflow/test/ExpressionExtensions.test.ts @@ -8,6 +8,7 @@ import { DateTime } from 'luxon'; import { extend } from '../src/Extensions'; import { DateExtensions } from '../src/Extensions/DateExtensions'; import { StringExtensions } from '../src/Extensions/StringExtensions'; +import { ArrayExtensions } from '../src/Extensions/ArrayExtensions'; describe('Expression Extensions', () => { describe('extend()', () => { @@ -130,5 +131,51 @@ describe('Expression Extensions', () => { new Date('2022-09-01T19:42:28.164Z'), ); }); + + const arrayExtensions = (data: any[], ...args: any[]) => { + return extend(data, ...args) as unknown as ArrayExtensions; + }; + + it('should be able to utilize array expression extension methods', () => { + expect(evaluate('={{ [1,2,3].random() }}')).not.toBeUndefined(); + + expect(evaluate('={{ [1,2,3, "imhere"].isPresent("imhere") }}')).toEqual(true); + + expect( + evaluate(`={{ [ + { value: 1, string: '1' }, + { value: 2, string: '2' }, + { value: 3, string: '3' }, + { value: 4, string: '4' }, + { value: 5, string: '5' }, + { value: 6, string: '6' } + ].pluck("value") }}`), + ).toEqual( + expect.arrayContaining([ + { value: 1 }, + { value: 2 }, + { value: 3 }, + { value: 4 }, + { value: 5 }, + { value: 6 }, + ]), + ); + + expect(evaluate('={{ ["repeat","repeat","a","b","c"].unique() }}')).toEqual( + expect.arrayContaining(['repeat', 'repeat', 'a', 'b', 'c']), + ); + + expect(evaluate('={{ [].isBlank() }}')).toEqual(arrayExtensions([]).isBlank([])); + + expect(evaluate('={{ [].length() }}')).toEqual(arrayExtensions([]).length([])); + + expect(evaluate('={{ ["repeat","repeat","a","b","c"].last() }}')).toEqual('c'); + + expect(evaluate('={{ ["repeat","repeat","a","b","c"].first() }}')).toEqual('repeat'); + + expect(evaluate('={{ ["repeat","repeat","a","b","c"].filter("repeat") }}')).toEqual( + expect.arrayContaining(['repeat', 'repeat']), + ); + }); }); });