diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index a55d33de0beb..67a2a1b1b34b 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -6,17 +6,12 @@ import {Context} from './context'; import {ResolutionSession} from './resolution-session'; import {Constructor, instantiateClass} from './resolver'; -import {isPromise} from './is-promise'; +import {isPromise, BoundValue, ValueOrPromise} from './value-promise'; import {Provider} from './provider'; import * as debugModule from 'debug'; const debug = debugModule('loopback:context:binding'); -// tslint:disable-next-line:no-any -export type BoundValue = any; - -export type ValueOrPromise = T | Promise; - /** * Scope for binding values */ diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 8a7d630231ad..9d2a61471235 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -3,8 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Binding, BoundValue, ValueOrPromise} from './binding'; -import {isPromise} from './is-promise'; +import {Binding} from './binding'; +import { + isPromise, + BoundValue, + ValueOrPromise, + getDeepProperty, +} from './value-promise'; import {ResolutionSession} from './resolution-session'; import * as debugModule from 'debug'; @@ -276,19 +281,3 @@ export class Context { return json; } } - -/** - * Get nested properties by path - * @param value Value of an object - * @param path Path to the property - */ -function getDeepProperty(value: BoundValue, path: string) { - const props = path.split('.'); - for (const p of props) { - value = value[p]; - if (value === undefined || value === null) { - return value; - } - } - return value; -} diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index ef450bcfd69e..b755d3829d54 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -3,20 +3,23 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -export { - Binding, - BindingScope, - BindingType, - BoundValue, - ValueOrPromise, -} from './binding'; +export {Binding, BindingScope, BindingType} from './binding'; export {Context} from './context'; export {Constructor} from './resolver'; export {ResolutionSession} from './resolution-session'; export {inject, Setter, Getter} from './inject'; export {Provider} from './provider'; -export {isPromise} from './is-promise'; +export { + isPromise, + BoundValue, + ValueOrPromise, + MapObject, + resolveList, + resolveMap, + tryWithFinally, + getDeepProperty, +} from './value-promise'; // internals for testing export {instantiateClass, invokeMethod} from './resolver'; diff --git a/packages/context/src/inject.ts b/packages/context/src/inject.ts index c401e8b074f5..006f24ca2495 100644 --- a/packages/context/src/inject.ts +++ b/packages/context/src/inject.ts @@ -10,10 +10,14 @@ import { PropertyDecoratorFactory, MetadataMap, } from '@loopback/metadata'; -import {BoundValue, ValueOrPromise} from './binding'; +import { + BoundValue, + ValueOrPromise, + isPromise, + resolveList, +} from './value-promise'; import {Context} from './context'; import {ResolutionSession} from './resolution-session'; -import {isPromise} from './is-promise'; const PARAMETERS_KEY = 'inject:parameters'; const PROPERTIES_KEY = 'inject:properties'; @@ -249,28 +253,12 @@ function resolveByTag( ) { const tag: string | RegExp = injection.metadata!.tag; const bindings = ctx.findByTag(tag); - const values: BoundValue[] = new Array(bindings.length); - - // A closure to set a value by index - const valSetter = (i: number) => (val: BoundValue) => (values[i] = val); - let asyncResolvers: PromiseLike[] = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < bindings.length; i++) { + return resolveList(bindings, b => { // We need to clone the session so that resolution of multiple bindings // can be tracked in parallel - const val = bindings[i].getValue(ctx, ResolutionSession.fork(session)); - if (isPromise(val)) { - asyncResolvers.push(val.then(valSetter(i))); - } else { - values[i] = val; - } - } - if (asyncResolvers.length) { - return Promise.all(asyncResolvers).then(vals => values); - } else { - return values; - } + return b.getValue(ctx, ResolutionSession.fork(session)); + }); } /** diff --git a/packages/context/src/is-promise.ts b/packages/context/src/is-promise.ts deleted file mode 100644 index e5ef3b23a27c..000000000000 --- a/packages/context/src/is-promise.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright IBM Corp. 2017,2018. All Rights Reserved. -// Node module: @loopback/context -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -/** - * Check whether a value is a Promise-like instance. - * Recognizes both native promises and third-party promise libraries. - * - * @param value The value to check. - */ -export function isPromise( - value: T | PromiseLike, -): value is PromiseLike { - if (!value) return false; - if (typeof value !== 'object' && typeof value !== 'function') return false; - return typeof (value as PromiseLike).then === 'function'; -} diff --git a/packages/context/src/provider.ts b/packages/context/src/provider.ts index bd6711e1f414..0571cbb9388b 100644 --- a/packages/context/src/provider.ts +++ b/packages/context/src/provider.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ValueOrPromise} from './binding'; +import {ValueOrPromise} from './value-promise'; /** * Providers allow developers to compute injected values dynamically, diff --git a/packages/context/src/resolution-session.ts b/packages/context/src/resolution-session.ts index a5d6ed9f40cb..aa13a1e0d86b 100644 --- a/packages/context/src/resolution-session.ts +++ b/packages/context/src/resolution-session.ts @@ -3,9 +3,14 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Binding, ValueOrPromise, BoundValue} from './binding'; +import {Binding} from './binding'; import {Injection} from './inject'; -import {isPromise} from './is-promise'; +import { + isPromise, + ValueOrPromise, + BoundValue, + tryWithFinally, +} from './value-promise'; import * as debugModule from 'debug'; import {DecoratorFactory} from '@loopback/metadata'; @@ -19,43 +24,6 @@ export type ResolutionAction = ( session?: ResolutionSession, ) => ValueOrPromise; -/** - * Try to run an action that returns a promise or a value - * @param action A function that returns a promise or a value - * @param finalAction A function to be called once the action - * is fulfilled or rejected (synchronously or asynchronously) - */ -function tryWithFinally( - action: () => ValueOrPromise, - finalAction: () => void, -) { - let result: ValueOrPromise; - try { - result = action(); - } catch (err) { - finalAction(); - throw err; - } - if (isPromise(result)) { - // Once (promise.finally)[https://github.com/tc39/proposal-promise-finally - // is supported, the following can be simplifed as - // `result = result.finally(finalAction);` - result = result.then( - val => { - finalAction(); - return val; - }, - err => { - finalAction(); - throw err; - }, - ); - } else { - finalAction(); - } - return result; -} - /** * Wrapper for bindings tracked by resolution sessions */ diff --git a/packages/context/src/resolver.ts b/packages/context/src/resolver.ts index cdee89428e0a..900bf93ec624 100644 --- a/packages/context/src/resolver.ts +++ b/packages/context/src/resolver.ts @@ -5,8 +5,15 @@ import {DecoratorFactory} from '@loopback/metadata'; import {Context} from './context'; -import {BoundValue, ValueOrPromise} from './binding'; -import {isPromise} from './is-promise'; +import { + BoundValue, + ValueOrPromise, + MapObject, + isPromise, + resolveList, + resolveMap, +} from './value-promise'; + import { describeInjectedArguments, describeInjectedProperties, @@ -45,7 +52,7 @@ export function instantiateClass( session?: ResolutionSession, // tslint:disable-next-line:no-any nonInjectedArgs?: any[], -): T | Promise { +): ValueOrPromise { /* istanbul ignore if */ if (debug.enabled) { debug('Instantiating %s', getTargetName(ctor)); @@ -55,7 +62,7 @@ export function instantiateClass( } const argsOrPromise = resolveInjectedArguments(ctor, '', ctx, session); const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session); - let inst: T | Promise; + let inst: ValueOrPromise; if (isPromise(argsOrPromise)) { // Instantiate the class asynchronously inst = argsOrPromise.then(args => { @@ -159,7 +166,7 @@ export function resolveInjectedArguments( session?: ResolutionSession, // tslint:disable-next-line:no-any nonInjectedArgs?: any[], -): BoundValue[] | Promise { +): ValueOrPromise { /* istanbul ignore if */ if (debug.enabled) { debug('Resolving injected arguments for %s', getTargetName(target, method)); @@ -176,20 +183,19 @@ export function resolveInjectedArguments( // Example value: // [ , 'key1', , 'key2'] const injectedArgs = describeInjectedArguments(target, method); - nonInjectedArgs = nonInjectedArgs || []; + const extraArgs = nonInjectedArgs || []; const argLength = DecoratorFactory.getNumberOfParameters(target, method); - const args: BoundValue[] = new Array(argLength); - let asyncResolvers: Promise[] | undefined = undefined; let nonInjectedIndex = 0; - for (let ix = 0; ix < argLength; ix++) { + return resolveList(new Array(argLength), (val, ix) => { + // The `val` argument is not used as the resolver only uses `injectedArgs` + // and `extraArgs` to return the new value const injection = ix < injectedArgs.length ? injectedArgs[ix] : undefined; if (injection == null || (!injection.bindingKey && !injection.resolve)) { - if (nonInjectedIndex < nonInjectedArgs.length) { + if (nonInjectedIndex < extraArgs.length) { // Set the argument from the non-injected list - args[ix] = nonInjectedArgs[nonInjectedIndex++]; - continue; + return extraArgs[nonInjectedIndex++]; } else { const name = getTargetName(target, method, ix); throw new Error( @@ -200,27 +206,13 @@ export function resolveInjectedArguments( } } - // Clone the session so that multiple arguments can be resolved in parallel - const valueOrPromise = resolve( + return resolve( ctx, injection, + // Clone the session so that multiple arguments can be resolved in parallel ResolutionSession.fork(session), ); - if (isPromise(valueOrPromise)) { - if (!asyncResolvers) asyncResolvers = []; - asyncResolvers.push( - valueOrPromise.then((v: BoundValue) => (args[ix] = v)), - ); - } else { - args[ix] = valueOrPromise as BoundValue; - } - } - - if (asyncResolvers) { - return Promise.all(asyncResolvers).then(() => args); - } else { - return args; - } + }); } /** @@ -277,8 +269,6 @@ export function invokeMethod( } } -export type KV = {[p: string]: BoundValue}; - /** * Given a class with properties decorated with `@inject`, * return the map of properties resolved using the values @@ -295,21 +285,14 @@ export function resolveInjectedProperties( constructor: Function, ctx: Context, session?: ResolutionSession, -): KV | Promise { +): ValueOrPromise> { /* istanbul ignore if */ if (debug.enabled) { debug('Resolving injected properties for %s', getTargetName(constructor)); } const injectedProperties = describeInjectedProperties(constructor.prototype); - const properties: KV = {}; - let asyncResolvers: Promise[] | undefined = undefined; - - const propertyResolver = (p: string) => (v: BoundValue) => - (properties[p] = v); - - for (const p in injectedProperties) { - const injection = injectedProperties[p]; + return resolveMap(injectedProperties, (injection, p) => { if (!injection.bindingKey && !injection.resolve) { const name = getTargetName(constructor, p); throw new Error( @@ -318,23 +301,11 @@ export function resolveInjectedProperties( ); } - // Clone the session so that multiple properties can be resolved in parallel - const valueOrPromise = resolve( + return resolve( ctx, injection, + // Clone the session so that multiple properties can be resolved in parallel ResolutionSession.fork(session), ); - if (isPromise(valueOrPromise)) { - if (!asyncResolvers) asyncResolvers = []; - asyncResolvers.push(valueOrPromise.then(propertyResolver(p))); - } else { - properties[p] = valueOrPromise as BoundValue; - } - } - - if (asyncResolvers) { - return Promise.all(asyncResolvers).then(() => properties); - } else { - return properties; - } + }); } diff --git a/packages/context/src/value-promise.ts b/packages/context/src/value-promise.ts new file mode 100644 index 000000000000..d3394dfe5ce5 --- /dev/null +++ b/packages/context/src/value-promise.ts @@ -0,0 +1,196 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/context +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +/** + * This module contains types for values and/or promises as well as a set of + * utility methods to handle values and/or promises. + */ + +// tslint:disable-next-line:no-any +export type BoundValue = any; + +/** + * Representing a value or promise. This type is used to represent results of + * synchronous/asynchronous resolution of values. + */ +export type ValueOrPromise = T | Promise; + +export type MapObject = {[name: string]: T}; + +/** + * Check whether a value is a Promise-like instance. + * Recognizes both native promises and third-party promise libraries. + * + * @param value The value to check. + */ +export function isPromise( + value: T | PromiseLike, +): value is PromiseLike { + if (!value) return false; + if (typeof value !== 'object' && typeof value !== 'function') return false; + return typeof (value as PromiseLike).then === 'function'; +} + +/** + * Get nested properties by path + * @param value Value of an object + * @param path Path to the property + */ +export function getDeepProperty(value: BoundValue, path: string) { + const props = path.split('.').filter(Boolean); + for (const p of props) { + if (value == null) { + return value; + } + value = value[p]; + } + return value; +} + +/** + * Resolve entries of an object into a new object with the same keys. If one or + * more entries of the source object are resolved to a promise by the `resolver` + * function, this method returns a promise which will be resolved to the new + * object with fully resolved entries. + * + * @example + * + * - Example 1: resolve all entries synchronously + * ```ts + * const result = resolveMap({a: 'x', b: 'y'}, v => v.toUpperCase()); + * ``` + * The `result` will be `{a: 'X', b: 'Y'}`. + * + * - Example 2: resolve one or more entries asynchronously + * ```ts + * const result = resolveMap({a: 'x', b: 'y'}, v => + * Promise.resolve(v.toUpperCase()), + * ); + * ``` + * The `result` will be a promise of `{a: 'X', b: 'Y'}`. + * + * @param map The original object containing the source entries + * @param resolver A function resolves an entry to a value or promise. It will + * be invoked with the property value, the property name, and the source object. + */ +export function resolveMap( + map: MapObject, + resolver: (val: T, key: string, map: MapObject) => ValueOrPromise, +): ValueOrPromise> { + const result: MapObject = {}; + let asyncResolvers: PromiseLike[] | undefined = undefined; + + const setter = (key: string) => (val: V) => { + result[key] = val; + }; + + for (const key in map) { + const valueOrPromise = resolver(map[key], key, map); + if (isPromise(valueOrPromise)) { + if (!asyncResolvers) asyncResolvers = []; + asyncResolvers.push(valueOrPromise.then(setter(key))); + } else { + result[key] = valueOrPromise; + } + } + + if (asyncResolvers) { + return Promise.all(asyncResolvers).then(() => result); + } else { + return result; + } +} + +/** + * Resolve entries of an array into a new array with the same indexes. If one or + * more entries of the source array are resolved to a promise by the `resolver` + * function, this method returns a promise which will be resolved to the new + * array with fully resolved entries. + * + * @example + * + * - Example 1: resolve all entries synchronously + * ```ts + * const result = resolveList(['a', 'b'], v => v.toUpperCase()); + * ``` + * The `result` will be `['A', 'B']`. + * + * - Example 2: resolve one or more entries asynchronously + * ```ts + * const result = resolveList(['a', 'b'], v => + * Promise.resolve(v.toUpperCase()), + * ); + * ``` + * The `result` will be a promise of `['A', 'B']`. + * + * @param list The original array containing the source entries + * @param resolver A function resolves an entry to a value or promise. It will + * be invoked with the property value, the property index, and the source array. + */ +export function resolveList( + list: T[], + resolver: (val: T, index: number, list: T[]) => ValueOrPromise, +): ValueOrPromise { + const result: V[] = new Array(list.length); + let asyncResolvers: PromiseLike[] | undefined = undefined; + + const setter = (index: number) => (val: V) => { + result[index] = val; + }; + + // tslint:disable-next-line:prefer-for-of + for (let ix = 0; ix < list.length; ix++) { + const valueOrPromise = resolver(list[ix], ix, list); + if (isPromise(valueOrPromise)) { + if (!asyncResolvers) asyncResolvers = []; + asyncResolvers.push(valueOrPromise.then(setter(ix))); + } else { + result[ix] = valueOrPromise; + } + } + + if (asyncResolvers) { + return Promise.all(asyncResolvers).then(() => result); + } else { + return result; + } +} + +/** + * Try to run an action that returns a promise or a value + * @param action A function that returns a promise or a value + * @param finalAction A function to be called once the action + * is fulfilled or rejected (synchronously or asynchronously) + */ +export function tryWithFinally( + action: () => ValueOrPromise, + finalAction: () => void, +): ValueOrPromise { + let result: ValueOrPromise; + try { + result = action(); + } catch (err) { + finalAction(); + throw err; + } + if (isPromise(result)) { + // Once (promise.finally)[https://github.com/tc39/proposal-promise-finally + // is supported, the following can be simplifed as + // `result = result.finally(finalAction);` + result = result.then( + val => { + finalAction(); + return val; + }, + err => { + finalAction(); + throw err; + }, + ); + } else { + finalAction(); + } + return result; +} diff --git a/packages/context/test/acceptance/class-level-bindings.ts b/packages/context/test/acceptance/class-level-bindings.ts index 999a2320ba83..4b4b9ae0733b 100644 --- a/packages/context/test/acceptance/class-level-bindings.ts +++ b/packages/context/test/acceptance/class-level-bindings.ts @@ -4,10 +4,15 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Context, inject, Setter, Getter} from '../..'; -import {Provider} from '../../src/provider'; -import {Injection} from '../../src/inject'; -import {ResolutionSession} from '../../src/resolution-session'; +import { + Context, + inject, + Setter, + Getter, + Provider, + Injection, + ResolutionSession, +} from '../..'; const INFO_CONTROLLER = 'controllers.info'; diff --git a/packages/context/test/unit/provider.ts b/packages/context/test/unit/provider.ts index 36e0547aeae4..58625ffc7587 100644 --- a/packages/context/test/unit/provider.ts +++ b/packages/context/test/unit/provider.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {expect} from '@loopback/testlab'; -import {Provider} from '../../src'; +import {Provider} from '../../'; describe('Provider', () => { let provider: Provider; diff --git a/packages/context/test/unit/value-promise.test.ts b/packages/context/test/unit/value-promise.test.ts new file mode 100644 index 000000000000..e93ebdc4f4f2 --- /dev/null +++ b/packages/context/test/unit/value-promise.test.ts @@ -0,0 +1,188 @@ +// Copyright IBM Corp. 2017,2018. All Rights Reserved. +// Node module: @loopback/context +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import { + BoundValue, + ValueOrPromise, + resolveList, + resolveMap, + tryWithFinally, + getDeepProperty, +} from '../..'; + +describe('tryWithFinally', () => { + it('performs final action for a fulfilled promise', async () => { + let finalActionInvoked = false; + const action = () => Promise.resolve(1); + const finalAction = () => (finalActionInvoked = true); + await tryWithFinally(action, finalAction); + expect(finalActionInvoked).to.be.true(); + }); + + it('performs final action for a resolved value', () => { + let finalActionInvoked = false; + const action = () => 1; + const finalAction = () => (finalActionInvoked = true); + tryWithFinally(action, finalAction); + expect(finalActionInvoked).to.be.true(); + }); + + it('performs final action for a rejected promise', async () => { + let finalActionInvoked = false; + const action = () => Promise.reject(new Error('error')); + const finalAction = () => (finalActionInvoked = true); + await expect(tryWithFinally(action, finalAction)).be.rejectedWith('error'); + expect(finalActionInvoked).to.be.true(); + }); + + it('performs final action for an action that throws an error', () => { + let finalActionInvoked = false; + const action = () => { + throw new Error('error'); + }; + const finalAction = () => (finalActionInvoked = true); + expect(() => tryWithFinally(action, finalAction)).to.throw('error'); + expect(finalActionInvoked).to.be.true(); + }); +}); + +describe('getDeepProperty', () => { + it('gets the root value if path is empty', () => { + const obj = {x: {y: 1}}; + expect(getDeepProperty(obj, '')).to.eql(obj); + }); + + it('gets the root value with a name', () => { + const obj = {x: {y: 1}}; + expect(getDeepProperty(obj, 'x')).to.eql({y: 1}); + }); + + it('gets the root value with a path', () => { + const obj = {x: {y: 1}}; + expect(getDeepProperty(obj, 'x.y')).to.eql(1); + }); + + it('returns undefined for non-existent path', () => { + const obj = {x: {y: 1}}; + expect(getDeepProperty(obj, 'x.z')).to.be.undefined(); + }); + + it('allows undefined value', () => { + expect(getDeepProperty(undefined, 'x.z')).to.be.undefined(); + }); + + it('allows null value', () => { + expect(getDeepProperty(null, 'x.z')).to.be.null(); + }); + + it('allows boolean value', () => { + expect(getDeepProperty(true, 'x.z')).to.be.undefined(); + }); + + it('allows number value', () => { + expect(getDeepProperty(1, 'x.z')).to.be.undefined(); + expect(getDeepProperty(NaN, 'x.z')).to.be.undefined(); + }); + + it('allows to get length string value', () => { + expect(getDeepProperty('xyz', 'length')).to.eql(3); + }); + + it('allows to get length and items of an array by index', () => { + const arr = ['x', 'y']; + expect(getDeepProperty(arr, 'length')).to.eql(2); + expect(getDeepProperty(arr, '0')).to.eql('x'); + expect(getDeepProperty(arr, '1')).to.eql('y'); + }); + + it('allows to get items of a nested array by index', () => { + const obj = {a: ['x', 'y']}; + expect(getDeepProperty(obj, 'a.0')).to.eql('x'); + expect(getDeepProperty(obj, 'a.1')).to.eql('y'); + }); +}); + +describe('resolveList', () => { + it('resolves an array of values', () => { + const source = ['a', 'b']; + const result = resolveList(source, v => v.toUpperCase()); + expect(result).to.eql(['A', 'B']); + }); + + it('resolves an array of promises', async () => { + const source = ['a', 'b']; + const result = await resolveList(source, v => + Promise.resolve(v.toUpperCase()), + ); + expect(result).to.eql(['A', 'B']); + }); + + it('resolves an array of promises or values', async () => { + const source = ['a', 'b']; + const result = await resolveList( + source, + v => (v === 'a' ? 'A' : Promise.resolve(v.toUpperCase())), + ); + expect(result).to.eql(['A', 'B']); + }); + + it('resolves an array of promises or values with index', async () => { + const source = ['a', 'b']; + const result = await resolveList( + source, + (v, i) => (v === 'a' ? 'A' + i : Promise.resolve(v.toUpperCase() + i)), + ); + expect(result).to.eql(['A0', 'B1']); + }); + + it('resolves an object of values with index and array', () => { + const result = resolveList(['a', 'b'], (v, i, list) => { + return v.toUpperCase() + i + list.length; + }); + expect(result).to.eql(['A02', 'B12']); + }); +}); + +describe('resolveMap', () => { + it('resolves an object of values', () => { + const source = {a: 'x', b: 'y'}; + const result = resolveMap(source, v => v.toUpperCase()); + expect(result).to.eql({a: 'X', b: 'Y'}); + }); + + it('resolves an object of promises', async () => { + const source = {a: 'x', b: 'y'}; + const result = await resolveMap(source, v => + Promise.resolve(v.toUpperCase()), + ); + expect(result).to.eql({a: 'X', b: 'Y'}); + }); + + it('resolves an object of promises or values', async () => { + const source = {a: 'x', b: 'y'}; + const result = await resolveMap( + source, + v => (v === 'x' ? 'X' : Promise.resolve(v.toUpperCase())), + ); + expect(result).to.eql({a: 'X', b: 'Y'}); + }); + + it('resolves an object of promises or values with key', async () => { + const source = {a: 'x', b: 'y'}; + const result = await resolveMap( + source, + (v, p) => (v === 'x' ? 'X' + p : Promise.resolve(v.toUpperCase() + p)), + ); + expect(result).to.eql({a: 'Xa', b: 'Yb'}); + }); + + it('resolves an object of values with key and object', () => { + const result = resolveMap({a: 'x', b: 'y'}, (v, p, map) => { + return v.toUpperCase() + p + Object.keys(map).length; + }); + expect(result).to.eql({a: 'Xa2', b: 'Yb2'}); + }); +});