Skip to content

Commit

Permalink
feat(dt-functions): introduce array expression extensions (n8n-io#4044)
Browse files Browse the repository at this point in the history
* ✨ Introduce Array Extensions

* ✅ Add Array Expression tests
  • Loading branch information
valya committed Nov 8, 2022
1 parent 3e0d783 commit ebb18f1
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
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']),
);
});
});
});

0 comments on commit ebb18f1

Please sign in to comment.