diff --git a/docs/docs/core/utils/async.md b/docs/docs/core/utils/async.md index 3dcb56be2f..6fef0e37e0 100644 --- a/docs/docs/core/utils/async.md +++ b/docs/docs/core/utils/async.md @@ -16,6 +16,7 @@ Available functions: - pipeAsync - mapAsync +- reduceAsync [See API reference](../../api/Utils) (TODO) diff --git a/packages/core/utils/lib/__tests__/async.test.js b/packages/core/utils/lib/__tests__/async.test.js index 74f9f53ea2..281cfdf8a2 100644 --- a/packages/core/utils/lib/__tests__/async.test.js +++ b/packages/core/utils/lib/__tests__/async.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { pipeAsync, mapAsync } = require('../async'); +const { pipeAsync, mapAsync, reduceAsync } = require('../async'); describe('Async utils', () => { describe('pipeAsync', () => { @@ -77,4 +77,58 @@ describe('Async utils', () => { expect(maxOperations).toEqual(2); }); }); + describe('reduceAsync', () => { + test('Should return an incremented number', async () => { + const numberPromiseArray = [Promise.resolve(1), Promise.resolve(2)]; + + const reduceFunc = reduceAsync(numberPromiseArray); + const result = await reduceFunc( + (previousValue, currentValue) => previousValue + currentValue, + 10 + ); + + expect(result).toEqual(13); + }); + test('Should work without initial value', async () => { + const numberPromiseArray = [Promise.resolve(1), Promise.resolve(2)]; + + const reduceFunc = reduceAsync(numberPromiseArray); + const result = await reduceFunc( + (previousValue, currentValue) => (previousValue || 10) + currentValue + ); + + expect(result).toEqual(13); + }); + test('Should work with mix of promises and values', async () => { + const numberMixArray = [1, Promise.resolve(2)]; + + const reduceFunc = reduceAsync(numberMixArray); + const result = await reduceFunc( + (previousValue, currentValue) => previousValue + currentValue, + 10 + ); + + expect(result).toEqual(13); + }); + test('Should throw an error with proper message when the provided callback throws an error', async () => { + const numberPromiseArray = [Promise.resolve(1), Promise.resolve(2)]; + + const reduceFunc = reduceAsync(numberPromiseArray); + + await expect(async () => { + await reduceFunc(() => { + throw new Error('test'); + }); + }).rejects.toThrow('test'); + }); + test('Should throw an error with proper message when the input array contains a rejected Promise', async () => { + const numberPromiseArray = [Promise.reject(new Error('input')), Promise.resolve(2)]; + + const reduceFunc = reduceAsync(numberPromiseArray); + + await expect(async () => { + await reduceFunc(() => true, null); + }).rejects.toThrow('input'); + }); + }); }); diff --git a/packages/core/utils/lib/async.d.ts b/packages/core/utils/lib/async.d.ts index 3e735d4b06..66ecb11819 100644 --- a/packages/core/utils/lib/async.d.ts +++ b/packages/core/utils/lib/async.d.ts @@ -1,4 +1,6 @@ -export type MapAsync = lodash.CurriedFunction3< +import { CurriedFunction3 } from 'lodash'; + +export type MapAsync = CurriedFunction3< T[], (element: T, index: number) => R | Promise, { concurrency?: number }, @@ -10,3 +12,10 @@ export type ForEachAsync = ( func: (element: T, index: number) => R | Promise, options?: { concurrency?: number } ) => Promise; + +export type ReduceAsync = CurriedFunction3< + T[], + (accumulator: V | R, current: Awaited, index: number) => R | Promise, + V, + Promise +>; diff --git a/packages/core/utils/lib/async.js b/packages/core/utils/lib/async.js index 5a91e70fb5..e5dd3dbe1b 100644 --- a/packages/core/utils/lib/async.js +++ b/packages/core/utils/lib/async.js @@ -1,7 +1,7 @@ 'use strict'; const pMap = require('p-map'); -const { curry } = require('lodash/fp'); +const { curry, curryN } = require('lodash/fp'); function pipeAsync(...methods) { return async (data) => { @@ -20,6 +20,17 @@ function pipeAsync(...methods) { */ const mapAsync = curry(pMap); +/** + * @type { import('./async').ReduceAsync } + */ +const reduceAsync = curryN(2, async (mixedArray, iteratee, initialValue) => { + let acc = initialValue; + for (let i = 0; i < mixedArray.length; i += 1) { + acc = await iteratee(acc, await mixedArray[i], i); + } + return acc; +}); + /** * @type { import('./async').ForEachAsync } */ @@ -29,6 +40,7 @@ const forEachAsync = curry(async (array, func, options) => { module.exports = { mapAsync, + reduceAsync, forEachAsync, pipeAsync, }; diff --git a/packages/core/utils/lib/index.js b/packages/core/utils/lib/index.js index d6a3ccfad3..5b4f9c4b69 100644 --- a/packages/core/utils/lib/index.js +++ b/packages/core/utils/lib/index.js @@ -37,7 +37,7 @@ const providerFactory = require('./provider-factory'); const pagination = require('./pagination'); const sanitize = require('./sanitize'); const traverseEntity = require('./traverse-entity'); -const { pipeAsync, mapAsync, forEachAsync } = require('./async'); +const { pipeAsync, mapAsync, reduceAsync, forEachAsync } = require('./async'); const convertQueryParams = require('./convert-query-params'); const importDefault = require('./import-default'); const template = require('./template'); @@ -83,6 +83,7 @@ module.exports = { pagination, pipeAsync, mapAsync, + reduceAsync, forEachAsync, errors, validateYupSchema,