Skip to content

Commit c6a05d7

Browse files
authored
add new config to batch properties calls (#299)
add new config to batch properties calls
1 parent 79fdd2d commit c6a05d7

File tree

10 files changed

+848
-73
lines changed

10 files changed

+848
-73
lines changed

API_DOCS.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,65 @@ getLoaders(resources[, options])
7777

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

80+
## Batch Resources with `properties` parameters
81+
82+
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:
83+
84+
```js
85+
const getUserInfo = (args: { user_ids: Array<number>, properties: Array<string> }): Promise<Array<UserInfo>> =>
86+
fetch('/userInfo', args);
87+
88+
const users = getUserInfo({
89+
user_ids: [1, 2, 3],
90+
properties: ['firstName', 'age'],
91+
});
92+
93+
/**
94+
* e.g. users =>
95+
* [
96+
* { id: 1, firstName: 'Alice', age: 42 },
97+
* { id: 2, firstName: 'Bob', age: 70 },
98+
* { id: 3, firstName: 'Carol', age: 50 },
99+
* ]
100+
*/
101+
```
102+
103+
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.
104+
We specify `responseKey` in the config as the key in the response objects corresponds to `batchKey`.
105+
106+
The config for our `getUserInfoV2` would look like this:
107+
108+
```yaml
109+
resources:
110+
getUserInfo:
111+
isBatchResource: true
112+
batchKey: user_ids
113+
newKey: user_id
114+
propertyBatchKey: properties
115+
responseKey: id
116+
```
117+
118+
**IMPORTANT NOTE**
119+
To use this feature, there are several restrictions. (Please open an issue if you're interested in helping us support other use cases):
120+
121+
**Contract**
122+
123+
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:
124+
125+
```js
126+
({
127+
// this is the batchKey
128+
ids: Array<string>,
129+
// this is the propertyBatchKey
130+
properties: Array<string>,
131+
}): Array<T>
132+
```
133+
134+
2. In the response, `properties` are spread at the same level as the `responseKey`. (Check out `getFilmsV2` in [swapi example](./examples/swapi/swapi.js).)
135+
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.
136+
4. The resource must have a one-to-one correspondence between the input "properties" and the output "properties".
137+
- e.g. if we request property "name", the response must have "name" in it, and no extra data associated with it.
138+
80139
## Config File
81140

82141
The config file should be a [YAML](https://yaml.org/) file in the following format:
@@ -94,6 +153,8 @@ resources:
94153
commaSeparatedBatchKey: ?string (can only use if isBatchResource=true)
95154
isResponseDictionary: ?boolean (can only use if isBatchResource=true)
96155
isBatchKeyASet: ?boolean (can only use if isBatchResource=true)
156+
propertyBatchKey: ?string (can only use if isBatchResource=true)
157+
responseKey: ?string (non-optional when propertyBatchKey is used)
97158
98159
typings:
99160
language: flow
@@ -125,6 +186,8 @@ Describes the shape and behaviour of the resources object you will pass to `getL
125186
| `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 |
126187
| `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 |
127188
| `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. |
189+
| `propertyBatchKey` | (Optional) The argument to the resource that represents the optional properties we want to fetch. (e.g. usually 'properties' or 'features'). |
190+
| `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. |
128191
129192
### `typings`
130193

__tests__/implementation.test.js

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,3 +1217,245 @@ test('bail if errorHandler does not return an error', async () => {
12171217
]);
12181218
});
12191219
});
1220+
1221+
test('batch endpoint (multiple requests) with propertyBatchKey', async () => {
1222+
const config = {
1223+
resources: {
1224+
foo: {
1225+
isBatchResource: true,
1226+
docsLink: 'example.com/docs/bar',
1227+
batchKey: 'foo_ids',
1228+
newKey: 'foo_id',
1229+
propertyBatchKey: 'properties',
1230+
responseKey: 'id',
1231+
},
1232+
},
1233+
};
1234+
1235+
const resources = {
1236+
foo: ({ foo_ids, properties, include_extra_info }) => {
1237+
if (_.isEqual(foo_ids, [2, 1])) {
1238+
expect(include_extra_info).toBe(false);
1239+
return Promise.resolve([
1240+
{ id: 1, rating: 3, name: 'Burger King' },
1241+
{ id: 2, rating: 4, name: 'In N Out' },
1242+
]);
1243+
}
1244+
1245+
if (_.isEqual(foo_ids, [3])) {
1246+
expect(include_extra_info).toBe(true);
1247+
return Promise.resolve([
1248+
{
1249+
id: 3,
1250+
rating: 5,
1251+
name: 'Shake Shack',
1252+
},
1253+
]);
1254+
}
1255+
},
1256+
};
1257+
1258+
await createDataLoaders(config, async (getLoaders) => {
1259+
const loaders = getLoaders(resources);
1260+
1261+
const results = await loaders.foo.loadMany([
1262+
{ foo_id: 2, properties: ['name', 'rating'], include_extra_info: false },
1263+
{ foo_id: 1, properties: ['rating'], include_extra_info: false },
1264+
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
1265+
]);
1266+
1267+
expect(results).toEqual([
1268+
{ id: 2, name: 'In N Out', rating: 4 },
1269+
{ id: 1, rating: 3 },
1270+
{ id: 3, rating: 5 },
1271+
]);
1272+
});
1273+
});
1274+
1275+
test('batch endpoint with propertyBatchKey throws error for response with non existant items', async () => {
1276+
const config = {
1277+
resources: {
1278+
foo: {
1279+
isBatchResource: true,
1280+
docsLink: 'example.com/docs/bar',
1281+
batchKey: 'foo_ids',
1282+
newKey: 'foo_id',
1283+
propertyBatchKey: 'properties',
1284+
responseKey: 'foo_id',
1285+
},
1286+
},
1287+
};
1288+
1289+
const resources = {
1290+
foo: ({ foo_ids, properties, include_extra_info }) => {
1291+
if (_.isEqual(foo_ids, [1, 2, 3])) {
1292+
expect(include_extra_info).toBe(true);
1293+
return Promise.resolve([
1294+
{
1295+
foo_id: 1,
1296+
name: 'Shake Shack',
1297+
rating: 4,
1298+
},
1299+
// deliberately omit 2
1300+
{
1301+
foo_id: 3,
1302+
name: 'Burger King',
1303+
rating: 3,
1304+
},
1305+
]);
1306+
} else if (_.isEqual(foo_ids, [4])) {
1307+
expect(include_extra_info).toBe(false);
1308+
return Promise.resolve([
1309+
{
1310+
foo_id: 4,
1311+
name: 'In N Out',
1312+
rating: 3.5,
1313+
},
1314+
]);
1315+
}
1316+
},
1317+
};
1318+
1319+
await createDataLoaders(config, async (getLoaders) => {
1320+
const loaders = getLoaders(resources);
1321+
1322+
const results = await loaders.foo.loadMany([
1323+
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: true },
1324+
{ foo_id: 2, properties: ['rating'], include_extra_info: true },
1325+
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
1326+
{ foo_id: 4, properties: ['rating'], include_extra_info: false },
1327+
]);
1328+
1329+
expect(results).toEqual([
1330+
{ foo_id: 1, name: 'Shake Shack', rating: 4 },
1331+
expect.toBeError(
1332+
[
1333+
'Could not find foo_id = 2 in the response dict. Or your endpoint does not follow the contract we support.',
1334+
'Please read https://github.com/Yelp/dataloader-codegen/blob/master/API_DOCS.md.',
1335+
].join(' '),
1336+
'BatchItemNotFoundError',
1337+
),
1338+
{ foo_id: 3, rating: 3 },
1339+
{ foo_id: 4, rating: 3.5 },
1340+
]);
1341+
});
1342+
});
1343+
1344+
test('batch endpoint (multiple requests) with propertyBatchKey error handling', async () => {
1345+
const config = {
1346+
resources: {
1347+
foo: {
1348+
isBatchResource: true,
1349+
docsLink: 'example.com/docs/bar',
1350+
batchKey: 'foo_ids',
1351+
newKey: 'foo_id',
1352+
propertyBatchKey: 'properties',
1353+
responseKey: 'id',
1354+
},
1355+
},
1356+
};
1357+
1358+
const resources = {
1359+
foo: ({ foo_ids, properties, include_extra_info }) => {
1360+
if (_.isEqual(foo_ids, [2, 4, 5])) {
1361+
expect(include_extra_info).toBe(true);
1362+
return Promise.resolve([
1363+
{
1364+
id: 2,
1365+
name: 'Burger King',
1366+
rating: 3,
1367+
},
1368+
{
1369+
id: 4,
1370+
name: 'In N Out',
1371+
rating: 3.5,
1372+
},
1373+
{
1374+
id: 5,
1375+
name: 'Shake Shack',
1376+
rating: 4,
1377+
},
1378+
]);
1379+
}
1380+
if (_.isEqual(foo_ids, [1, 3])) {
1381+
expect(include_extra_info).toBe(false);
1382+
throw new Error('yikes');
1383+
}
1384+
},
1385+
};
1386+
1387+
await createDataLoaders(config, async (getLoaders) => {
1388+
const loaders = getLoaders(resources);
1389+
1390+
const results = await loaders.foo.loadMany([
1391+
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: false },
1392+
{ foo_id: 2, properties: ['name', 'rating'], include_extra_info: true },
1393+
{ foo_id: 3, properties: ['name'], include_extra_info: false },
1394+
{ foo_id: 4, properties: ['rating'], include_extra_info: true },
1395+
{ foo_id: 5, properties: ['name'], include_extra_info: true },
1396+
]);
1397+
1398+
expect(results).toEqual([
1399+
expect.toBeError(/yikes/),
1400+
{ id: 2, name: 'Burger King', rating: 3 },
1401+
expect.toBeError(/yikes/),
1402+
{ id: 4, rating: 3.5 },
1403+
{ id: 5, name: 'Shake Shack' },
1404+
]);
1405+
});
1406+
});
1407+
1408+
test('batch endpoint with propertyBatchKey with reorderResultsByKey handles response with non existant items', async () => {
1409+
const config = {
1410+
resources: {
1411+
foo: {
1412+
isBatchResource: true,
1413+
docsLink: 'example.com/docs/bar',
1414+
batchKey: 'foo_ids',
1415+
newKey: 'foo_id',
1416+
reorderResultsByKey: 'foo_id',
1417+
propertyBatchKey: 'properties',
1418+
responseKey: 'foo_id',
1419+
},
1420+
},
1421+
};
1422+
1423+
const resources = {
1424+
foo: ({ foo_ids, properties, include_extra_info }) => {
1425+
if (_.isEqual(foo_ids, [1, 2, 3])) {
1426+
expect(include_extra_info).toBe(true);
1427+
return Promise.resolve([
1428+
{ foo_id: 3, rating: 4, name: 'Shake Shack' },
1429+
{ foo_id: 1, rating: 3, name: 'Burger King' },
1430+
// deliberately omit 2
1431+
]);
1432+
} else if (_.isEqual(foo_ids, [4])) {
1433+
expect(include_extra_info).toBe(false);
1434+
return Promise.resolve([{ foo_id: 4, rating: 5, name: 'In N Out' }]);
1435+
}
1436+
},
1437+
};
1438+
await createDataLoaders(config, async (getLoaders) => {
1439+
const loaders = getLoaders(resources);
1440+
1441+
const results = await loaders.foo.loadMany([
1442+
{ foo_id: 1, properties: ['name', 'rating'], include_extra_info: true },
1443+
{ foo_id: 2, properties: ['name'], include_extra_info: true },
1444+
{ foo_id: 3, properties: ['rating'], include_extra_info: true },
1445+
{ foo_id: 4, properties: ['rating'], include_extra_info: false },
1446+
]);
1447+
1448+
expect(results).toEqual([
1449+
{ foo_id: 1, rating: 3, name: 'Burger King' },
1450+
expect.toBeError(
1451+
[
1452+
'Could not find foo_id = 2 in the response dict. Or your endpoint does not follow the contract we support.',
1453+
'Please read https://github.com/Yelp/dataloader-codegen/blob/master/API_DOCS.md.',
1454+
].join(' '),
1455+
'BatchItemNotFoundError',
1456+
),
1457+
{ foo_id: 3, rating: 4 },
1458+
{ foo_id: 4, rating: 5 },
1459+
]);
1460+
});
1461+
});

0 commit comments

Comments
 (0)