Skip to content

Commit

Permalink
feat: add option for returning object as a result of batchFn
Browse files Browse the repository at this point in the history
  • Loading branch information
jmvtrinidad committed Jul 1, 2021
1 parent 4e8f261 commit 28dc060
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ Create a new `DataLoader` given a batch loading function and options.
| *cache* | Boolean | `true` | Set to `false` to disable memoization caching, creating a new Promise and new key in the `batchLoadFn` for every load of the same key. This is equivalent to setting `cacheMap` to `null`.
| *cacheKeyFn* | Function | `key => key` | Produces cache key for a given load key. Useful when objects are keys and two objects should be considered equivalent.
| *cacheMap* | Object | `new Map()` | Instance of [Map][] (or an object with a similar API) to be used as cache. May be set to `null` to disable caching.
| *objectResult* | Boolean | `false` | Set to `true` to return an object in `batchFn` with `key` as `cacheKey` and the value of the `key` is the actual value.

##### `load(key)`

Expand Down
23 changes: 23 additions & 0 deletions src/__tests__/abuse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,29 @@ describe('Provides descriptive error messages for API abuse', () => {
);
});

it(
'Batch function must promise an Object if objectResult is true',
async () => {
// Note: this resolves to empty array
const badLoader = new DataLoader<number, number>(async () => [],
{ objectResult: true }
);

let caughtError;
try {
await badLoader.load(1);
} catch (error) {
caughtError = error;
}
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError: any).message).toBe(
'DataLoader must be constructed with a function which accepts ' +
'Array<key> and returns Promise<Record<key, value>>,' +
'but the function did not return a Promise of an Object: ' +
'.'
);
});

it('Cache should have get, set, delete, and clear methods', async () => {
class IncompleteMap {
get() {}
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/dataloader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,25 @@ describe('Accepts options', () => {
expect(loadCalls).toEqual([ [ 'A', 'B' ], [ 'A', 'B' ] ]);
});

it('Return objects as a result of batchFn', async () => {
const identityLoader = new DataLoader<Obj, Obj, string>(() => {
return Promise.resolve({
A: 'a',
C: 'c'
});
}, { objectResult: true });

const [ valueA, valueB, valueC ] = await Promise.all([
identityLoader.load('A'),
identityLoader.load('B'),
identityLoader.load('C'),
]);

expect(valueA).toBe('a');
expect(valueB).toBeUndefined();
expect(valueC).toBe('c');
});

describe('Accepts object key in custom cacheKey function', () => {
function cacheKey(key: {[string]: any}): string {
return Object.keys(key).sort().map(k => k + ':' + key[k]).join();
Expand Down
7 changes: 7 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ declare namespace DataLoader {
* to be used as cache. May be set to `null` to disable caching.
*/
cacheMap?: CacheMap<C, Promise<V>> | null;

/**
* Set to `true` to return an
* object in `batchFn` with `key` as `cacheKey` and
* the value of the `key` is the actual value.
*/
objectResult?: boolean,
}
}

Expand Down
25 changes: 22 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type BatchLoadFn<K, V> =
// custom cache instance.
export type Options<K, V, C = K> = {
batch?: boolean;
objectResult?: boolean;
maxBatchSize?: number;
batchScheduleFn?: (callback: () => void) => void;
cache?: boolean;
Expand Down Expand Up @@ -53,6 +54,7 @@ class DataLoader<K, V, C = K> {
);
}
this._batchLoadFn = batchLoadFn;
this._objectResult = options ? Boolean(options.objectResult) : false;
this._maxBatchSize = getValidMaxBatchSize(options);
this._batchScheduleFn = getValidBatchScheduleFn(options);
this._cacheKeyFn = getValidCacheKeyFn(options);
Expand All @@ -62,6 +64,7 @@ class DataLoader<K, V, C = K> {

// Private
_batchLoadFn: BatchLoadFn<K, V>;
_objectResult: boolean;
_maxBatchSize: number;
_batchScheduleFn: (() => void) => void;
_cacheKeyFn: K => C;
Expand Down Expand Up @@ -315,14 +318,14 @@ function dispatchBatch<K, V>(
batchPromise.then(values => {

// Assert the expected resolution from batchLoadFn.
if (!isArrayLike(values)) {
if (!loader._objectResult && !isArrayLike(values)) {
throw new TypeError(
'DataLoader must be constructed with a function which accepts ' +
'Array<key> and returns Promise<Array<value>>, but the function did ' +
`not return a Promise of an Array: ${String(values)}.`
);
}
if (values.length !== batch.keys.length) {
if (!loader._objectResult && values.length !== batch.keys.length) {
throw new TypeError(
'DataLoader must be constructed with a function which accepts ' +
'Array<key> and returns Promise<Array<value>>, but the function did ' +
Expand All @@ -332,13 +335,21 @@ function dispatchBatch<K, V>(
`\n\nValues:\n${String(values)}`
);
}
if (loader._objectResult && !isObjectLike(values)) {
throw new TypeError(
'DataLoader must be constructed with a function which accepts ' +
'Array<key> and returns Promise<Record<key, value>>,' +
'but the function did not return a Promise of an Object: ' +
`${String(values)}.`
);
}

// Resolve all cache hits in the same micro-task as freshly loaded values.
resolveCacheHits(batch);

// Step through values, resolving or rejecting each Promise in the batch.
for (var i = 0; i < batch.callbacks.length; i++) {
var value = values[i];
var value = loader._objectResult ? values[batch.keys[i]] : values[i];
if (value instanceof Error) {
batch.callbacks[i].reject(value);
} else {
Expand Down Expand Up @@ -456,4 +467,12 @@ function isArrayLike(x: mixed): boolean {
);
}

function isObjectLike(x: mixed): boolean {
return (
typeof x === 'object' &&
x !== null &&
!isArrayLike(x)
);
}

module.exports = DataLoader;

0 comments on commit 28dc060

Please sign in to comment.