Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e1d039a
add new config to batch properties calls
gyDBD Jul 1, 2021
1f0d45e
add unit tests to add coverage and add assert when batchKey and newKe…
gyDBD Jul 1, 2021
2231852
change BatchItemNotFoundError
gyDBD Jul 1, 2021
c7c0352
add documentations for this config and fix review issues
gyDBD Jul 2, 2021
2d15a11
add swagger validation
gyDBD Jul 21, 2021
0d71c98
add documentations for config
gyDBD Jul 21, 2021
89bfeb3
add documentations for config
gyDBD Jul 21, 2021
a0c5018
add responseKey as the key in the response objects corresponds to
gyDBD Jul 21, 2021
8a91b1c
add responseKey as the key in the response objects corresponds to
gyDBD Jul 21, 2021
10dab84
change some helper function and docstring
gyDBD Jul 21, 2021
4c8c375
use public registry not yelp one
gyDBD Jul 21, 2021
6c17d4f
update docstring
gyDBD Jul 21, 2021
192bb2c
update swapi example
gyDBD Jul 22, 2021
60dbcc2
update API_DOCS
gyDBD Jul 22, 2021
678529c
revise some documentation
gyDBD Jul 22, 2021
af21432
update BatchResourceConfig
gyDBD Jul 22, 2021
7bad34a
update README.md
gyDBD Jul 23, 2021
ebbfe28
update README.md
gyDBD Jul 23, 2021
4df94f8
remove the script and several config
gyDBD Jul 26, 2021
4603062
remove swagger-parser package
gyDBD Jul 26, 2021
a245c58
test swapi
gyDBD Jul 26, 2021
31a14c8
remove import
gyDBD Jul 27, 2021
344462d
update comments
gyDBD Jul 27, 2021
0a5a759
fix review issues
gyDBD Jul 28, 2021
5804bec
fix tests
gyDBD Jul 28, 2021
398fc74
fix review issues
gyDBD Jul 28, 2021
e30035f
some format issues
gyDBD Jul 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions API_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,65 @@ getLoaders(resources[, options])

To see an example call to `getLoaders`, [check out the SWAPI example](./examples/swapi/swapi-server.js) or [the tests](./__tests__/implementation.test.js).

## Batch Resources with `properties` parameters

Instead of accepting just a list of users (`user_ids`), a batch resource could accept both a list of users (`user_ids`) and a list of properties (`properties`) to fetch about that user:

```js
const getUserInfo = (args: { user_ids: Array<number>, properties: Array<string> }): Promise<Array<UserInfo>> =>
fetch('/userInfo', args);

const users = getUserInfo({
user_ids: [1, 2, 3],
properties: ['firstName', 'age'],
});

/**
* e.g. users =>
* [
* { id: 1, firstName: 'Alice', age: 42 },
* { id: 2, firstName: 'Bob', age: 70 },
* { id: 3, firstName: 'Carol', age: 50 },
* ]
*/
```

To batch up calls to this resource with different `properties` for different `user_ids`, we specify `propertyBatchKey` in the config to describe the "properties" argument.
We specify `responseKey` in the config as the key in the response objects corresponds to `batchKey`.

The config for our `getUserInfoV2` would look like this:

```yaml
resources:
getUserInfo:
isBatchResource: true
batchKey: user_ids
newKey: user_id
propertyBatchKey: properties
responseKey: id
```

**IMPORTANT NOTE**
To use this feature, there are several restrictions. (Please open an issue if you're interested in helping us support other use cases):

**Contract**

1. The resource accepts a list of `ids` and a list of `properties`; to specify the entity IDs and the properties for each entity to fetch:

```js
({
// this is the batchKey
ids: Array<string>,
// this is the propertyBatchKey
properties: Array<string>,
}): Array<T>
```

2. In the response, `properties` are spread at the same level as the `responseKey`. (Check out `getFilmsV2` in [swapi example](./examples/swapi/swapi.js).)
3. All `properties` must be optional in the response object. The flow types currently don't handle the nullability of these properties correctly, so to enforce this, we recommend a build step to ensure that the underlying types are always set as maybe types.
4. The resource must have a one-to-one correspondence between the input "properties" and the output "properties".
- e.g. if we request property "name", the response must have "name" in it, and no extra data associated with it.

## Config File

The config file should be a [YAML](https://yaml.org/) file in the following format:
Expand All @@ -94,6 +153,8 @@ resources:
commaSeparatedBatchKey: ?string (can only use if isBatchResource=true)
isResponseDictionary: ?boolean (can only use if isBatchResource=true)
isBatchKeyASet: ?boolean (can only use if isBatchResource=true)
propertyBatchKey: ?string (can only use if isBatchResource=true)
responseKey: ?string (non-optional when propertyBatchKey is used)

typings:
language: flow
Expand Down Expand Up @@ -125,6 +186,8 @@ Describes the shape and behaviour of the resources object you will pass to `getL
| `commaSeparatedBatchKey` | (Optional) Set to true if the interface of the resource takes the batch key as a comma separated list (rather than an array of IDs, as is more common). Default: false |
| `isResponseDictionary` | (Optional) Set to true if the batch resource returns the results as a dictionary with key mapped to values (instead of a list of items). If this option is supplied `reorderResultsByKey` should not be. Default: false |
| `isBatchKeyASet` | (Optional) Set to true if the interface of the resource takes the batch key as a set (rather than an array). For example, when using a generated clientlib based on swagger where `uniqueItems: true` is set for the batchKey parameter. Default: false. |
| `propertyBatchKey` | (Optional) The argument to the resource that represents the optional properties we want to fetch. (e.g. usually 'properties' or 'features'). |
| `responseKey` | (Non-optional when propertyBatchKey is used) The key in the response objects corresponds to `batchKey`. This should be the only field that are marked as required in your swagger endpoint response, except nestedPath. |

### `typings`

Expand Down
242 changes: 242 additions & 0 deletions __tests__/implementation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1217,3 +1217,245 @@ test('bail if errorHandler does not return an error', async () => {
]);
});
});

test('batch endpoint (multiple requests) with propertyBatchKey', async () => {
const config = {
resources: {
foo: {
isBatchResource: true,
docsLink: 'example.com/docs/bar',
batchKey: 'foo_ids',
newKey: 'foo_id',
propertyBatchKey: 'properties',
responseKey: 'id',
},
},
};

const resources = {
foo: ({ foo_ids, properties, include_extra_info }) => {
if (_.isEqual(foo_ids, [2, 1])) {
expect(include_extra_info).toBe(false);
return Promise.resolve([
{ id: 1, rating: 3, name: 'Burger King' },
{ id: 2, rating: 4, name: 'In N Out' },
]);
}

if (_.isEqual(foo_ids, [3])) {
expect(include_extra_info).toBe(true);
return Promise.resolve([
{
id: 3,
rating: 5,
name: 'Shake Shack',
},
]);
}
},
};

await createDataLoaders(config, async (getLoaders) => {
const loaders = getLoaders(resources);

const results = await loaders.foo.loadMany([
{ foo_id: 2, properties: ['name', 'rating'], include_extra_info: false },
{ foo_id: 1, properties: ['rating'], include_extra_info: false },
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
]);

expect(results).toEqual([
{ id: 2, name: 'In N Out', rating: 4 },
{ id: 1, rating: 3 },
{ id: 3, rating: 5 },
]);
});
});

test('batch endpoint with propertyBatchKey throws error for response with non existant items', async () => {
const config = {
resources: {
foo: {
isBatchResource: true,
docsLink: 'example.com/docs/bar',
batchKey: 'foo_ids',
newKey: 'foo_id',
propertyBatchKey: 'properties',
responseKey: 'foo_id',
},
},
};

const resources = {
foo: ({ foo_ids, properties, include_extra_info }) => {
if (_.isEqual(foo_ids, [1, 2, 3])) {
expect(include_extra_info).toBe(true);
return Promise.resolve([
{
foo_id: 1,
name: 'Shake Shack',
rating: 4,
},
// deliberately omit 2
{
foo_id: 3,
name: 'Burger King',
rating: 3,
},
]);
} else if (_.isEqual(foo_ids, [4])) {
expect(include_extra_info).toBe(false);
return Promise.resolve([
{
foo_id: 4,
name: 'In N Out',
rating: 3.5,
},
]);
}
},
};

await createDataLoaders(config, async (getLoaders) => {
const loaders = getLoaders(resources);

const results = await loaders.foo.loadMany([
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: true },
{ foo_id: 2, properties: ['rating'], include_extra_info: true },
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
{ foo_id: 4, properties: ['rating'], include_extra_info: false },
]);

expect(results).toEqual([
{ foo_id: 1, name: 'Shake Shack', rating: 4 },
expect.toBeError(
[
'Could not find foo_id = 2 in the response dict. Or your endpoint does not follow the contract we support.',
'Please read https://github.com/Yelp/dataloader-codegen/blob/master/API_DOCS.md.',
].join(' '),
'BatchItemNotFoundError',
),
{ foo_id: 3, rating: 3 },
{ foo_id: 4, rating: 3.5 },
]);
});
});

test('batch endpoint (multiple requests) with propertyBatchKey error handling', async () => {
const config = {
resources: {
foo: {
isBatchResource: true,
docsLink: 'example.com/docs/bar',
batchKey: 'foo_ids',
newKey: 'foo_id',
propertyBatchKey: 'properties',
responseKey: 'id',
},
},
};

const resources = {
foo: ({ foo_ids, properties, include_extra_info }) => {
if (_.isEqual(foo_ids, [2, 4, 5])) {
expect(include_extra_info).toBe(true);
return Promise.resolve([
{
id: 2,
name: 'Burger King',
rating: 3,
},
{
id: 4,
name: 'In N Out',
rating: 3.5,
},
{
id: 5,
name: 'Shake Shack',
rating: 4,
},
]);
}
if (_.isEqual(foo_ids, [1, 3])) {
expect(include_extra_info).toBe(false);
throw new Error('yikes');
}
},
};

await createDataLoaders(config, async (getLoaders) => {
const loaders = getLoaders(resources);

const results = await loaders.foo.loadMany([
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: false },
{ foo_id: 2, properties: ['name', 'rating'], include_extra_info: true },
{ foo_id: 3, properties: ['name'], include_extra_info: false },
{ foo_id: 4, properties: ['rating'], include_extra_info: true },
{ foo_id: 5, properties: ['name'], include_extra_info: true },
]);

expect(results).toEqual([
expect.toBeError(/yikes/),
{ id: 2, name: 'Burger King', rating: 3 },
expect.toBeError(/yikes/),
{ id: 4, rating: 3.5 },
{ id: 5, name: 'Shake Shack' },
]);
});
});

test('batch endpoint with propertyBatchKey with reorderResultsByKey handles response with non existant items', async () => {
const config = {
resources: {
foo: {
isBatchResource: true,
docsLink: 'example.com/docs/bar',
batchKey: 'foo_ids',
newKey: 'foo_id',
reorderResultsByKey: 'foo_id',
propertyBatchKey: 'properties',
responseKey: 'foo_id',
},
},
};

const resources = {
foo: ({ foo_ids, properties, include_extra_info }) => {
if (_.isEqual(foo_ids, [1, 2, 3])) {
expect(include_extra_info).toBe(true);
return Promise.resolve([
{ foo_id: 3, rating: 4, name: 'Shake Shack' },
{ foo_id: 1, rating: 3, name: 'Burger King' },
// deliberately omit 2
]);
} else if (_.isEqual(foo_ids, [4])) {
expect(include_extra_info).toBe(false);
return Promise.resolve([{ foo_id: 4, rating: 5, name: 'In N Out' }]);
}
},
};
await createDataLoaders(config, async (getLoaders) => {
const loaders = getLoaders(resources);

const results = await loaders.foo.loadMany([
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: true },
{ foo_id: 2, properties: ['name'], include_extra_info: true },
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
{ foo_id: 4, properties: ['rating'], include_extra_info: false },
]);

expect(results).toEqual([
{ foo_id: 1, rating: 3, name: 'Burger King' },
expect.toBeError(
[
'Could not find foo_id = 2 in the response dict. Or your endpoint does not follow the contract we support.',
'Please read https://github.com/Yelp/dataloader-codegen/blob/master/API_DOCS.md.',
].join(' '),
'BatchItemNotFoundError',
),
{ foo_id: 3, rating: 4 },
{ foo_id: 4, rating: 5 },
]);
});
});
Loading