Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dt-functions): introduce array expression extensions #4044

Merged
merged 13 commits into from
Sep 9, 2022
110 changes: 110 additions & 0 deletions packages/workflow/src/Extensions/ArrayExtensions.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
methodMapping = new Map<string, ExtensionMethodHandler<any>>();

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<typeof v>).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<typeof v>).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));
}
}
11 changes: 11 additions & 0 deletions packages/workflow/src/Extensions/ExpressionExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions packages/workflow/test/ExpressionExtensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -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']),
);
});
});
});