diff --git a/API_DOCS.md b/API_DOCS.md index c0c2101..cb0b5cb 100644 --- a/API_DOCS.md +++ b/API_DOCS.md @@ -188,6 +188,7 @@ Describes the shape and behaviour of the resources object you will pass to `getL | `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. | +| `maxBatchSize` | (Optional) Limits the number of items that can be batched together in a single request. When more items are requested than this limit, multiple requests will be made. This can help prevent hitting URI length limits or timeouts for large batches. | ### `typings` diff --git a/__tests__/implementation.test.js b/__tests__/implementation.test.js index 10725ce..09d93f3 100644 --- a/__tests__/implementation.test.js +++ b/__tests__/implementation.test.js @@ -1246,6 +1246,127 @@ test('bail if errorHandler does not return an error', async () => { }); }); +test('batch endpoint with maxBatchSize', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + responseKey: 'id', + maxBatchSize: 3, + }, + }, + }; + + // Track each batch of IDs that the resource function receives + const receivedBatches = []; + + const resources = { + foo: ({ foo_ids, properties }) => { + receivedBatches.push([...foo_ids]); + return Promise.resolve( + foo_ids.map(id => ({ + id, + name: `name-${id}`, + rating: id + 1 + })) + ); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + // Request 5 items at once, which should be split by maxBatchSize later in the test. + const results = await Promise.all([ + loaders.foo.load({ foo_id: 1, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 2, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 3, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 4, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 5, properties: ['name', 'rating'] }), + ]); + + // Verify that all results were returned correctly + expect(results).toEqual([ + { id: 1, name: 'name-1', rating: 2 }, + { id: 2, name: 'name-2', rating: 3 }, + { id: 3, name: 'name-3', rating: 4 }, + { id: 4, name: 'name-4', rating: 5 }, + { id: 5, name: 'name-5', rating: 6 }, + ]); + + // Verify that the requests were batched correctly + expect(receivedBatches.map(batch => batch.length)).toEqual([3, 2]); + + // Verify that all IDs were requested + const allRequestedIds = receivedBatches.flat().sort(); + expect(allRequestedIds).toEqual([1, 2, 3, 4, 5]); + }); +}); + +test('batch endpoint with propertyBatchKey and maxBatchSize', async () => { + const config = { + resources: { + foo: { + isBatchResource: true, + docsLink: 'example.com/docs/bar', + batchKey: 'foo_ids', + newKey: 'foo_id', + propertyBatchKey: 'properties', + responseKey: 'id', + maxBatchSize: 2, + }, + }, + }; + + // Track each batch of IDs that the resource function receives + const receivedBatches = []; + + const resources = { + foo: ({ foo_ids, properties }) => { + receivedBatches.push([...foo_ids]); + return Promise.resolve( + foo_ids.map(id => ({ + id, + name: `name-${id}`, + rating: id + 1 + })) + ); + }, + }; + + await createDataLoaders(config, async (getLoaders) => { + const loaders = getLoaders(resources); + + // Request 5 items at once, which should be split by maxBatchSize later in the test. + const results = await Promise.all([ + loaders.foo.load({ foo_id: 1, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 2, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 3, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 4, properties: ['name', 'rating'] }), + loaders.foo.load({ foo_id: 5, properties: ['name', 'rating'] }), + ]); + + // Verify that all results were returned correctly + expect(results).toEqual([ + { id: 1, name: 'name-1', rating: 2 }, + { id: 2, name: 'name-2', rating: 3 }, + { id: 3, name: 'name-3', rating: 4 }, + { id: 4, name: 'name-4', rating: 5 }, + { id: 5, name: 'name-5', rating: 6 }, + ]); + + // Verify that the requests were batched correctly + expect(receivedBatches.map(batch => batch.length)).toEqual([2, 2, 1]); + + // Verify that all IDs were requested + const allRequestedIds = receivedBatches.flat().sort(); + expect(allRequestedIds).toEqual([1, 2, 3, 4, 5]); + }); +}); + test('batch endpoint (multiple requests) with propertyBatchKey', async () => { const config = { resources: { diff --git a/src/config.ts b/src/config.ts index a57f83d..92a9268 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,6 +28,7 @@ export interface BatchResourceConfig { isBatchKeyASet?: boolean; propertyBatchKey?: string; responseKey?: string; + maxBatchSize?: number; } export interface NonBatchResourceConfig { diff --git a/src/implementation.ts b/src/implementation.ts index 43287ef..64849ab 100644 --- a/src/implementation.ts +++ b/src/implementation.ts @@ -416,7 +416,8 @@ function getBatchLoader(resourceConfig: BatchResourceConfig, resourcePath: Reado * flow errors :( */ '' } - ...cacheKeyOptions + ...cacheKeyOptions, + ${resourceConfig.maxBatchSize ? `maxBatchSize: ${resourceConfig.maxBatchSize},` : ''} } )`; } @@ -514,7 +515,8 @@ function getPropertyBatchLoader(resourceConfig: BatchResourceConfig, resourcePat * flow errors :( */ '' } - ...cacheKeyOptions + ...cacheKeyOptions, + ${resourceConfig.maxBatchSize ? `maxBatchSize: ${resourceConfig.maxBatchSize},` : ''} } )`; }